zoukankan      html  css  js  c++  java
  • 泛型总结

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

     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
     
    当神已无能为力,那便是魔渡众生
  • 相关阅读:
    【持续更新】Java知识点整理-util
    【持续更新】Java知识点整理-基础
    【持续更新】Java知识点整理-JVM
    notepad++中写markdown
    VirtualBox中Alpine Linux + Docker安装记录
    Alpine Linux配置网络
    从编码的历史了解编码
    关于摄影器材的一些知识点
    服务器(Linux)上运行python总结
    命令行运行Python脚本时传入参数的三种方式
  • 原文地址:https://www.cnblogs.com/liuzhongfeng/p/5080989.html
Copyright © 2011-2022 走看看