泛型总结

简介:    1介绍   Java泛型编程是JDK1.5版本后引入的。泛型让编程人员能够使用类型抽象,通常用于集合里面。下面是一个不用泛型例子:   1 List myIntList=new LinkedList(); 3 myIntList.

 

  第十二章 泛型程序设计 - 冰魂雪魄 - 冰魂雪魄

 1介绍

   Java 泛型编程是JDK1.5版本后引入的。泛型让编程人员能够使用类型抽象,通常用于集合里面。下面是一个不用泛型例子:
 
1 List myIntList=new LinkedList();
3 myIntList.add(newInteger(0));
5 Integer x=(Integer)myIntList.iterator().next();
  注意第三行的代码,让人很不爽,因为程序员肯定知道自己存储在List里面的对象类型是Integer,但是在返回的时候,列表中元素必须强制转换,这是为什么呢?原因在于,编译器只能保证迭代器的next()方法返回的是Object类型的对象,为保证Interger变量的类型安全,必须强制转换。
这种转换不仅显得混乱,而且导致转换异常ClassCastException,运行时异常往往让人难以检测到。保证列表中的元素为一个特定的数据类型,这样就可以取消类型转换,减少发生错误的机会,这也是泛型设计的初衷。下面给出一个泛型的例子:
1 List<Integer> myIntList=newLinkedList<Integer>;
3 myIntList.add(newInteger(0));
5 Integer x=myIntList.iterator().next();
  在第一行代码中指定List中存储的对象类型是Integer,这样在获取列表中的对象时,不必强制类型转换了。
  2 定义简单的泛型
  下面是一个引用java.util包中的借口List和Iterator的定义,其中用到了泛型技术。
 1 public interface List<E>{
 3    void add(E,x);
 5         Iterator<E> iterator();
 7 }
 9 public interface Iterator<E>{
11     E next();
13     boolean hasNext();
15 }
   这跟原生态类型没有什么区别,只是在接口后面加入了一个尖括号,尖括号里面是一个类型参数(定义时就是一个格式化的类型参数,在调用时会使用一个具体的类型来替换该类型)。
  也许可以这样认为,List<Integer>表示List中的类型参数E会被替换成Integer。
1 public interface List<Integer>{
2     void add(Integer x);
3         terator<Integer> iterator();
4     }  
  类型擦除指的是通过类型参数的合并,将泛型类型实例关联到同一个字节码上,编译器只为泛型类型生成一个字节码,并将其关联到这上面,因此泛型类型中的静态变量是所有实例共享的。此外需要注意,一个static方法,无法访问泛型类的类型参数,因为类还没有实例化,所以若static方法需要使用泛型,必须使其成为泛型方法。
  类型擦除的关键在于从泛型类中清除类型参数的相关信息,并且再必要的时候添加类型检查和类型转换的方法。使用泛型时,任何具体的类型都被擦除,唯一知道的是你在使用一个对象。比如List<String>和List<Integer>是相同的类型。它们都被擦除成原始类型,即List。
  因为编译的时候会有类型擦除,所以不能通过一个泛型类的实例来区分方法,如下面的例子编译会出错,因为类型擦除后,两个方法都是List类型的参数。因此不能根据泛型类的类型来区分方法。
1 /*会导致编译时出错*/
2 public class Erasure{
3     public void test(List<String> ls){
4         System.out.println("String");
5     }
6     public void test(List<Integer> ls){
7         System.out.println("Integer");
8     }
9 }    
  那么有问题了,既然编译时会在方法和类中擦除实际类型的信息,那么返回对象时又是如何知道具体类型的呢?如List<String>编译后会擦除String信息,那么在运行时通过迭代器返回List中的对象时,又是如何知道List中存储的是String类型的对象的呢?
擦除在方法中的类型信息,所以在运行时的问题是边界: 即对象进入和离开方法的地点,这正是编译器在编译期执行类型检查并出入转换代码的地点。泛型中的所有动作都发生在边界处:对传递进来的值进行额外的编译器检查,并插入对传递出去的值的转换。
3 泛型和子类型
  为了彻底理解泛型,看个例子,(Apple为Fruit的子类)
1 List<Apple> apples=new ArrayList<Apple>();
2 List<Fruit> fruits=apples;
  这里第一行显然是对的, 但是第2行是否对呢?我们知道Fruit fruit = new Apple(),这样肯定是对的,即苹果肯定是水果,但是第2行在编译的时候会出错。这会让人比较纳闷的是一个苹果是水果,为什么一箱苹果就不是一箱水果了呢?可以这样考虑,我们假定第2行代码没有问题,那么我们可以使用语句fruits.add(new Strawberry())(Strawberry为Fruit的子类)在fruits中加入草莓了,但是这样的话,一个List中装入了各种不同类型的子类水果,这显然是不可以的,因为我们在取出List中的水果对象时,就分不清楚到底该转型为苹果还是草莓了(因为擦除后,苹果和草莓都变成了Fruit类型,具体返回对象的时候,不能区分哪个是苹果的对象,哪个是草莓的对象。)
  通常来说,如果Foo是Bar的子类型,G是一种带泛型的类型,则G<Foo>不是G<Bar>的子类型。这是容易混淆的地方。
4 通配符
4.1 通配符?
  先看一个打印集合所有元素的代码。
1 //不使用泛型
2 void printCollection(Collection c){
3     Iterator i=c.iterator();
4         for(k=0;k<c.size();k++){
5         System.out.println(i.next());
6     }
7 }    
1 //使用泛型
2 void printCollection(Collection c)<Object> c{
3     for(Object e:c){
4         System.out.println(e);
5     }
6 }
  很容易发现,使用泛型的版本只接受类型为Object类型的集合,如ArrayList<Object>();如果是ArrayList<String>,则会出错。因为前面说过,Collection<Object>并不是所有集合的超类。而老版本可以打印任意类型的集合,那么改造新版本以便能接受所有类型的集合呢?这个问题可以通过通配符解决。修改后的代码如下:
1 void printCollection(Collection c)<?> c{
2     for(Object e:c){
3         System.out.println(e);
4     }
5 }    
   这里使用了通配符?指定可以使用任何类型的集合作为参数。读取元素使用了Objectect类型表示,这是安全的,因为所有的类都是Object的子类。
 又有另一个问题,如下面代码所示,如果试图往使用通配符?的集合中加入对象,会出错。需要注意,不管加入什么类型的对象都会出错。这是因为统配符表示该集合存储的元素类型未知,可以是任意的。
1 Collection<?> c=new ArrayList<String>();
2 c.add(newObject());//编译出错,不管加入什么对象都出错,除了null外。
3 c.add(null);//OK!
  另一方面,我们可以从List<?>lists中获取值,如for(Object obj:lists),这是合法的,因为可以肯定存储类型一定是Object的子类型,所以可以用Object类型来获取。
4.2 边界通配符
1)?extends通配符
  假定有一个画图的应用,可以活各种形状。为了在程序里面表示,定义如下的类层次。
 1 public abstract class Shape{
 2     public abstract void draw(Canvas c);
 3 }
 4 public class Circle extends Shape{
 5     private int x,y;
 6     public void draw(Canvas c){...};
 7 }
 8 public class Rectangle extends Shape{
 9     private int x,y,width,height;
10     public void draw(Canvas c){...}
11 }
12 
13 //原始版本
14 public void drawAll(List<Shape> shapes){
15     for(Shapes :shapes){
16         s.draw(this);
17     }
18 }
19 
20 //使用边界通配符的版本
21 public void drawALL(List<? extends Shape> shapes){
22     for(Shapes :shapes){
23         s.draw(this);
24     }
25 }    
  有一个问题,如果我们希望List<? extends Shapes> shapes中加入一个矩形对象,如下所示:
shapes.add(0, new Rectangle());//编译出错。
  原因是:我们只知道shapes中的元素时Shapes类型的子类型。具体是什么子类不知道。所以不能加入任何类型的对象。不过我们在取出对象时,可以用Shape类型来取值,因为虽然不知道列表中元素是什么类型,但是它一定是Shape类的子类型。
2)?super通配符
  这里还有一种边界通配符?super。如:
1 List<Shape> shapes=new ArrayList<Shape>();
2 List<? super Cicle> cicleSupers=shapes;
3 circleSupers.add(new Cicle());//OK,subclss of Cicle also OK
4 cicleSupers.add(new Shape());//ERROR
  这里cicleSupers列表中元素是Cicle的超类,因此,我们可以往其中加入Cicle对象或者是Cicle子类的对象,但是不能加入Shape对象。这里的原因在于列表cicleSupers存储的是Cicle的超类,但具体类型未知。
3)边界通配符总结

<!--[if !supportLists]-->l        <!--[endif]-->如果你想从一个数据类型里获取数据,使用 ? extends 通配符

<!--[if !supportLists]-->l        <!--[endif]-->如果你想把对象写入一个数据结构里,使用 ? super 通配符

<!--[if !supportLists]-->l        <!--[endif]-->如果你既想存,又想取,那就别用通配符。

5 泛型方法

  考虑实现一个方法,该方法拷贝一个数组中的所有对象到集合中。下面是初始的版本。
1 static void fromArrayToCollection(Object[]a,Collection<?>c){
2     for(Object o:a){
3         c.add(o);//编译错误
4     }
5 }
  可以看到显然会出现错误,原因在于之前讲过,因为集合c中的类型未知,所以不能往其中加入任何的对象(当然,null除外)。解决该问题的好方法是使用泛型方法。
1 static<T> void fromArrayToCollection(T[] a,Collection<T>c){
2     for(T O:a){
3         c.add(o);//OK
4     }
5 }
  泛型方法的格式,类型参数<T>要放在函数返回值之前,然后参数和返回值中就可以使用泛型参数了,具体一些调用方法的实例如下:
 1 Object[] oa=new Object[100];
 2 Collection<Object>co=new ArrayList<Object>();
 3 fromArrayToCollection(oa,co);//T inferred to be Object
 4 String[] sa=new String[100];
 5 Collection<String>cs=new ArrayList<String>();
 6 fromArrayToCollection(sa,cs);//T inferred to be String
 7 fromArrayToCollection(sa,co);//T inferred to be Object
 8 Integer[] ia=new Integer[100];
 9 Float[] fa=new Float[100];
10 Number[] na=new Number[100];
11 Collection<Number>cn=new ArrayList<Number>();
12 fromArrayToCollection(ia,cn);//T inferred to be Number
13 fromArrayToCollection(fa,cn);//T inferred to be Number
14 fromArrayToCollection(na,cn);//T inferred to be Number
15 fromArrayToCollection(na,co);//T inferred to be Object
16 fromArrayToCollection(na,cs);//编译错误
  注意到我们调用该方法时并不需要传递类型参数,系统会自动判断参数并调用合适的方法。当然在某些情况下需要制定传递类型参数,比如当存在与泛型方法相同的方法的时候(方法参数不一样),如下面的例子:
 1 public <T> void go(T t){
 2     System.out.println("generic function");
 3 }
 4 public void go(String str){
 5     System.out.println("normal function");
 6 }
 7 public static void main(String[] args){
 8     FuncGenric fg=new FuncGenric();
 9     fg.go("haha");//打印normal function
10     fg.<String>go("haha");//打印normal function,String是多余的,并且不报错。
11     fg.go(new Object());//打印generic function
12     fg.<Object>go(new Object());//打印generic function
13 }
  当不指定类型参数时,调用的是普通的方法,如果指定了类型参数,则调用泛型方法。可这样理解,因为泛型方法编译后类型擦除,如果不指定类型参数,则泛型方法此时相当于是public void go(Object t)。而普通的方法接收参数为String类型,因此String类型的实参调用函数,肯定会调用形参为String的普通方法了。如果是以Object类型的实参调用函数,肯定会调用泛型方法。
6 需要注意的地方
1)方法重载
  在JAVA里面方法重载是不能通过返回值类型来区分的,比如代码一中一个类中定义两个如下的方法是不容许的。但是当参数为泛型类型时,确实可以的,如代码二,虽然形参经过类型擦除后都以List类型,但是返回类型不同,这是可以的。
1 //代码一:编译出错
2 public class Erasure{
3     public void test(int i){
4         System.out.printlnl("String");
5     }
6     public void test(int i){
7         System.out.println("Integer");
8     }
9 }
1 //代码二:正确
2 public class Erasure{
3     public void test(List<String>ls){
4         System.out.println("String");
5     }    
6     public void test(List<Integer>li){
7         System.out.println("Integer");
8     }
9 }    
2)泛型类型是被所有调用共享的
  所有泛型类型的实例都共享同一个运行时类,类型参数信息会在编译时被擦除。因此考虑如下代码,虽然ArrayList<String>和ArrayList<Integer>类型参数不同,但是它们都共享ArrayList类,所以结果会是true。
1 List<Sting>l1=mew ArrayList<String>();
2 List<Integer>l2=mew ArrayList<Integer>();
3 System.out.println(l1.getClass()==l2.getClass());//True

3)instanceof

  不能对确切的泛型类型使用instanceof()操作,下面代码是违法的。
1 Collection cs=new CollectionList<String>();
2 if (cs instanceof Collection<String>){....}//编译出错,如果改成instanceof Collection<?>则正确。
4)泛型数组问题
  不能创建一个确切反省类型的数组,否则出错。
1 List<String>[] lsa=new ArrayList<String>[10];//错误。因为如果可以这样,那么考虑如下代码,会导致运行时错误。
2 List<String>[] lsa=new ArrayList<String>[10];//实际上并不允许这样创建数组
3 Object 0=lsa;
4 Object[] oa=(Object[])o;
5 List<Integer>li=new ArrayList<Integer>();
6 li.add(new Integer(3));
7 oa[1]=li;//unsound,but passes run time store check
8 String s=lsa[1].get[0];//run-time error-classCastException
  因此只能创建带通配符的泛型数组,如下面例子所示,这回可以通过编译,但倒数第二行代码必须显示的转型才行,即便如此,最后还是会抛出类型转换异常,因为存储在lsa中的List<Integer>类型的对象,而不是List<String>类型。最后一行代码是正确的,类型匹配,不会抛出异常。
1 List<?>[] lsa=new List<?>[10];
2 Object 0=lsa;
3 Object[] oa=(Object[])o;
4 List<Integer>li=new ArrayList<Integer>();
5 li.add(new Integer(3));
6 oa[1]=li;/OK
7 String s=lsa[1].get(0);//run-time error,but cast is explicit
8 Integer it=(Integer)lsa[1].get(0);//OK

 

 

 

当神已无能为力,那便是魔渡众生
目录
相关文章
|
6月前
|
安全 算法 Java
深入理解泛型
深入理解泛型
|
6月前
|
安全 编译器 Scala
何时需要指定泛型:Scala编程指南
本文是Scala编程指南,介绍了何时需要指定泛型类型参数。泛型提供代码重用和类型安全性,但在编译器无法推断类型、需要提高代码清晰度、调用泛型方法或创建泛型集合时,应明确指定类型参数。通过示例展示了泛型在避免类型错误和增强编译时检查方面的作用,强调了理解泛型使用时机对编写高效Scala代码的重要性。
46 1
何时需要指定泛型:Scala编程指南
|
6月前
|
Java 编译器 语音技术
泛型的理解
泛型的理解
35 0
|
6月前
|
存储 算法 容器
什么是泛型?
什么是泛型?
27 0
|
6月前
什么是泛型,泛型的具体使用?
什么是泛型,泛型的具体使用?
35 0
|
6月前
|
存储 安全 Java
这还是你认识的泛型吗???!
这还是你认识的泛型吗???!
48 0
|
Java 编译器
|
存储 安全 Java
泛型的相关知识
泛型的相关知识
|
存储 C#
【C#】什么是泛型?
泛型是C#编程中一个非常重要的概念,它能够编写更加通用、灵活和可重用的代码,能够编写可以在不同类型之间工作的代码,同时还可以提高代码的效率和可维护性。在本文中,我们将探讨泛型及其在C#编程中的应用。
|
存储 Java 编译器
对泛型的认识
对泛型的认识