您的位置:首页 > 编程语言 > Java开发

Java泛型基础知识整理第一部分

2017-10-10 14:32 369 查看
1、泛型简介 博文来源 Java泛型的作用及实现原理

Java 1.5出现的新特性,本质是参数化类型。参数类型可以用于类、接口和方法的创建中,即泛型类、泛型接口、泛型方法。

在Java SE1.5出现之前,没有泛型的情况下,通过类型Object的引用来实现参数的任意化,缺点是要做显式的强制类型转换,而该类型转换要求开发者对实际参数类型可以预知的情况下进行。对于强制类型转换错误的情况,编译器可能不提示错误,在运行的时候才出现异常,这是一个安全隐患。

泛型的出现解决了上述的两个问题。1、所有的强制换换都是自动和隐藏的。2、编译时进行类型安全检查。

泛型在使用中的规则与限制

1、泛型的类型参数只能是类类型(包括自定义类)。

2、同一种泛型可以对应多个版本(因为参数类型是不确定的),不同版本的泛型类实例时不兼容的。

3、泛型的类型参数可以有多个。

4、泛型的类型参数可以使用extends语句。

5、泛型的类型参数还可以是通配符类型。

泛型作用

1、限定类型,开发中不用进行类型检查,提高开发效率。

2、编译期间类型检查。

3、封装共性。

4、比Object类范围缩小,提高运行效率。

2、实现原理 类型擦除 博文来源 Java泛型的实现原理 java泛型
泛型的内部原理:类型擦除以及类型擦除带来的问题

Java泛型时伪泛型,Java的泛型擦除基本是在编译器这个层次上实现的,生成的Java字节码不会包含泛型的类型信息,使用泛型时加上的类型参数会在编译器编译时去掉,该过程就是类型擦除(type erasure)。

例如List<String>类型在JVM运行时只能看到List,而由泛型附加的类型对JVM是不可见的。编译器会在编译时尽可能的发现出错的地方。但是,仍然无法避免在运行期间出现类型转换异常。

三、类型擦除后保留的原始类型

原始类型指的是擦除去泛型信息后,最后在字节码中的类型变量的真正类型。无论何时定义一个泛型类型,相应的原始类型都会被自动地提供。类型变量被擦除(crased),并使用其限定类型(无限定的变量用Object)替换。

class Pair<T> {    

  private T value;    

  public T getValue() {    

    return value;    

  }    

  public void setValue(T  value) {    

    this.value = value;    

  }    

}    

 因为在Pair<T>中,T是一个无限定的类型变量,所以用Object替换。在程序中可以包含不同类型的Pair,如Pair<String>或Pair<Integer>,但是,擦除类型后它们就成为原始的Pair类型了,原始类型都是Object。

class Pair {    

  private Object value;    

  public Object getValue() {    

    return value;    

  }    

  public void setValue(Object  value) {    

    this.value = value;    

  }    

}  

下面的例子用于证明泛型类型被擦除掉了,只剩下原始类型

ArrayList<String>a=new ArrayList<String>();
ArrayList<Integer>b=new ArrayList<Integer>();
a.add("abc");
b.add(123);
System.out.println(a.getClass()==b.getClass());

打印出的值是true,说明泛型类型String和Integer都被擦除了,字节码只剩下原始类型。

public class Test4 {  

    public static void main(String[] args) throws IllegalArgumentException, SecurityException, IllegalAccessException, InvocationTargetException, NoSuchMethodException {  

        ArrayList<Integer> arrayList3=new ArrayList<Integer>();  

        arrayList3.add(1);//这样调用add方法只能存储整形,因为泛型类型的实例为Integer  

        arrayList3.getClass().getMethod("add", Object.class).invoke(arrayList3, "asd");  

        for (int i=0;i<arrayList3.size();i++) {  

            System.out.println(arrayList3.get(i));  

        }  

    }  

在程序中定义了一个ArrayList泛型类型实例化为Integer的对象,如果直接调用add方法,那么只能存储整形的数据。不过当我们利用反射调用add方法的时候,却可以存储字符串。这说明了Integer泛型实例在编译之后被擦除了,只保留了原始类型。

其实在泛型类中,不指定泛型的时候,也差不多,只不过这个时候的泛型类型为Object,就比如ArrayList中,如果不指定泛型,那么这个ArrayList中可以放任意类型的对象。

ArrayList arrayList=new ArrayList(); 

如果类型变量有限定,那么原始类型就用第一个边界的类型变量来替换。public class Pair<T extends Comparable& Serializable> {   
那么原始类型就是Comparable。 如果Pair这样声明public class Pair<T extends Serializable&Comparable> ,那么原始类型就用Serializable替换,而编译器在必要的时要向Comparable插入强制类型转换。为了提高效率,应该将标签(tagging)接口(即没有方法的接口)放在边界限定列表的末尾。

   区分原始类型和泛型变量的类型

    在调用泛型方法的时候,可以指定泛型,也可以不指定泛型。

    在不指定泛型的情况下,泛型变量的类型为 该方法中的几种类型的同一个父类的最小级,直到Object

    在指定泛型的时候,该方法中的几种类型必须是该泛型实例类型或者其子类。

Java代码  收藏代码

public class Test{    

  public static void main(String[] args) {    

    /**不指定泛型的时候*/    

    int i=Test.add(1, 2); //这两个参数都是Integer,所以T为Integer类型    

    Number f=Test.add(1, 1.2);//这两个参数一个是Integer,以风格是Float,所以取同一父类的最小级,为Number    

    Object o=Test.add(1, "asd");//这两个参数一个是Integer,以风格是Float,所以取同一父类的最小级,为Object    

    

    /**指定泛型的时候*/    

    int a=Test.<Integer>add(1, 2);//指定了Integer,所以只能为Integer类型或者其子类    

    int b=Test.<Integer>add(1, 2.2);//编译错误,指定了Integer,不能为Float    

    Number c=Test.<Number>add(1, 2.2); //指定为Number,所以可以为Integer和Float    

  }    

  

  //这是一个简单的泛型方法    

  public static <T> T add(T x,T y){    

    return y;    

  }    

}    

类型擦除引起的问题以及解决方法

1、先检查再编译,检查编译的对象和引用传递问题

public static  void main(String[] args) {  

        ArrayList<String> arrayList=new ArrayList<String>();  

        arrayList.add("123");  

        arrayList.add(123);//编译错误  

    }  

上面直接报错说明是在编译之前类型检查的

2、参数化类型与原始类型的兼容

ArrayList<String>a=new ArrayList();

ArrayList b=new ArrayList<String>();

第一种和实现与完全使用泛型参数的效果一致,第二种完全没有效果。原因是new ArrayList()仅仅是开辟了一个内存空间,可以存储任意类型的对象,而其类型检查的是其引用。a引用调用方法时,a引用能完成泛型类型的检查。而b没有使用泛型类型,所以不行。上述两种情况不会报错,但是会有编译警告。

再如下面的例子

  ArrayList<String> arrayList1=new ArrayList();  

        arrayList1.add("1");//编译通过  

        arrayList1.add(1);//编译错误  

        String str1=arrayList1.get(0);//返回类型就是String  

        ArrayList arrayList2=new ArrayList<String>();  

        arrayList2.add("1");//编译通过  

        arrayList2.add(1);//编译通过  

        Object object=arrayList2.get(0);//返回类型就是Object    

        new ArrayList<String>().add("11");//编译通过  

        new ArrayList<String>().add(22);//编译错误  

        String string=new ArrayList<String>().get(0);//返回类型就是String  

    

得出的结论是:类型检查就是针对引用的,引用调用泛型方法,就会对这个引用调用的方法进行类型检查,而无关真正引用的对象。

泛型中参数化类型为何不考虑继承关系

ArrayList<String> arrayList1=new ArrayList<Object>();//编译错误  

ArrayList<Object> arrayList1=new ArrayList<String>();//编译错误  

将第一种情况拓展成下面的形式:

ArrayList<Object> arrayList1=new ArrayList<Object>();  

          arrayList1.add(new Object());  

          arrayList1.add(new Object());  

          ArrayList<String> arrayList2=arrayList1;//编译错误 

实际上,在第4行代码的时候,就会有编译错误。那么,我们先假设它编译没错。那么当我们使用arrayList2引用用get()方法取值的时候,返回的都是String类型的对象(上面提到了,类型检测是根据引用来决定的。),可是它里面实际上已经被我们存放了Object类型的对象,这样,就会有ClassCastException了。所以为了避免这种极易出现的错误,Java不允许进行这样的引用传递。(这也是泛型出现的原因,就是为了解决类型转换的问题,我们不能违背它的初衷)。
没错,这样的情况比第一种情况好的多,最起码,在我们用arrayList2取值的时候不会出现ClassCastException,因为是从String转换为Object。可是,这样做有什么意义呢,泛型出现的原因,就是为了解决类型转换的问题。我们使用了泛型,到头来,还是要自己强转,违背了泛型设计的初衷。所以java不允许这么干。再说,你如果又用arrayList2往里面add()新的对象,那么到时候取得时候,我怎么知道我取出来的到底是String类型的,还是Object类型的呢?
2、自动类型转换

疑问?类型擦除后,所有的泛型类型变量最后被替换为原始数据类型。但是在获取时,为何不用进行强制的类型转换。

public E get(int index) {
RangeCheck(index);
return (E) elementData[index];
}
看以看到,在return之前,会根据泛型变量进行强转。假设泛型类型变量为Date,虽然泛型信息会被擦除掉,但是会将(E)elementData[index],编译为(Date)elementData[index]。所以我们不用自己进行强转。

存取一个泛型域时也会自动插入强制类型转换。假设Pair类的value域是public的,那么,表达式:

Date date=pair.value
也会自动地在结果字节码中插入强制类型转换。

3、类型擦除与多态冲突的解决方法

    private T value;  

    public T getValue() {  

        return value;  

    }  

    public void setValue(T value) {  

        this.value = value;  

    }  

}  

然后我们想要一个子类继承它

class Pair<T> {  

class DateInter extends Pair<Date> {  

    @Override  

    public void setValue(Date value) {  

        super.setValue(value);  

    }  

    @Override  

    public Date getValue() {  

        return super.getValue();  

    }  

}  

在这个子类中,我们设定父类的泛型类型为Pair<Date>,在子类中,我们覆盖了父类的两个方法,我们的原意是这样的:

将父类的泛型类型限定为Date,那么父类里面的两个方法的参数都为Date类型:“

public Date getValue() {  

    return value;  

}  

public void setValue(Date value) {  

    this.value = value;  

}  

所以,我们在子类中重写这两个方法一点问题也没有,实际上,从他们的@Override标签中也可以看到,一点问题也没有,实际上是这样的吗?

分析:

实际上,类型擦除后,父类的的泛型类型全部变为了原始类型Object,所以父类编译之后会变成下面的样子:

class Pair {  

    private Object value;  

    public Object getValue() {  

        return value;  

    }  

    public void setValue(Object  value) {  

        this.value = value;  

    }  

}  

 再看子类的两个重写的方法的类型: 

 @Override  

public void setValue(Date value) {  

    super.setValue(value);  

}  

@Override  

public Date getValue() {  

    return super.getValue();  

}  

先来分析setValue方法,父类的类型是Object,而子类的类型是Date,参数类型不一样,这如果实在普通的继承关系中,根本就不会是重写,而是重载。

我们在一个main方法测试一下:

public static void main(String[] args) throws ClassNotFoundException {  

        DateInter dateInter=new DateInter();  

        dateInter.setValue(new Date());                  

                dateInter.setValue(new Object());//编译错误  

 }  

如果是重载,那么子类中两个setValue方法,一个是参数Object类型,一个是Date类型,可是我们发现,根本就没有这样的一个子类继承自父类的Object类型参数的方法。所以说,却是是重写了,而不是重载了。

为什么会这样呢?

原因是这样的,我们传入父类的泛型类型是Date,Pair<Date>,我们的本意是将泛型类变为如下:

class Pair {  

    private Date value;  

    public Date getValue() {  

        return value;  

    }  

    public void setValue(Date value) {  

        this.value = value;  

    }  

}  

然后再子类中重写参数类型为Date的那两个方法,实现继承中的多态。

可是由于种种原因,虚拟机并不能将泛型类型变为Date,只能将类型擦除掉,变为原始类型Object。这样,我们的本意是进行重写,实现多态。可是类型擦除后,只能变为了重载。这样,类型擦除就和多态有了冲突。JVM知道你的本意吗?知道!!!可是它能直接实现吗,不能!!!如果真的不能的话,那我们怎么去重写我们想要的Date类型参数的方法啊。

于是JVM采用了一个特殊的方法,来完成这项功能,那就是桥方法。

首先,我们用javap -c className的方式反编译下DateInter子类的字节码,结果如下:

class com.tao.test.DateInter extends com.tao.test.Pair<java.util.Date> {  

  com.tao.test.DateInter();  

    Code:  

       0: aload_0  

       1: invokespecial #8                  // Method com/tao/test/Pair."<init>"  

:()V  

       4: return  

  

  public void setValue(java.util.Date);  //我们重写的setValue方法  

    Code:  

       0: aload_0  

       1: aload_1  

       2: invokespecial #16                 // Method com/tao/test/Pair.setValue  

:(Ljava/lang/Object;)V  

       5: return  

  

  public java.util.Date getValue();    //我们重写的getValue方法  

    Code:  

       0: aload_0  

       1: invokespecial #23                 // Method com/tao/test/Pair.getValue  

:()Ljava/lang/Object;  

       4: checkcast     #26                 // class java/util/Date  

       7: areturn  

  

  public java.lang.Object getValue();     //编译时由编译器生成的巧方法  

    Code:  

       0: aload_0  

       1: invokevirtual #28                 // Method getValue:()Ljava/util/Date 去调用我们重写的getValue方法  

;  

       4: areturn  

  

  public void setValue(java.lang.Object);   //编译时由编译器生成的巧方法  

    Code:  

       0: aload_0  

       1: aload_1  

       2: checkcast     #26                 // class java/util/Date  

       5: invokevirtual #30                 // Method setValue:(Ljava/util/Date;   去调用我们重写的setValue方法  

)V  

       8: return  

}  

从编译的结果来看,我们本意重写setValue和getValue方法的子类,竟然有4个方法,其实不用惊奇,最后的两个方法,就是编译器自己生成的桥方法。可以看到桥方法的参数类型都是Object,也就是说,子类中真正覆盖父类两个方法的就是这两个我们看不到的桥方法。而打在我们自己定义的setvalue和getValue方法上面的@Oveerride只不过是假象。而桥方法的内部实现,就只是去调用我们自己重写的那两个方法。

所以,虚拟机巧妙的使用了巧方法,来解决了类型擦除和多态的冲突。

不过,要提到一点,这里面的setValue和getValue这两个桥方法的意义又有不同。

setValue方法是为了解决类型擦除与多态之间的冲突。

而getValue却有普遍的意义,怎么说呢,如果这是一个普通的继承关系:

那么父类的setValue方法如下:

public ObjectgetValue() {  

        return super.getValue();  

    }  

而子类重写的方法是:

public Date getValue() {  

        return super.getValue();  

    }  

其实这在普通的类继承中也是普遍存在的重写,这就是协变。

关于协变:。。。。。。

并且,还有一点也许会有疑问,子类中的巧方法  Object   getValue()和Date getValue()是同 时存在的,可是如果是常规的两个方法,他们的方法签名是一样的,也就是说虚拟机根本不能分别这两个方法。如果是我们自己编写Java代码,这样的代码是无法通过编译器的检查的,但是虚拟机却是允许这样做的,因为虚拟机通过参数类型和返回类型来确定一个方法,所以编译器为了实现泛型的多态允许自己做这个看起来“不合法”的事情,然后交给虚拟器去区别。

4、泛型类型变量不能是基本数据类型

不能用类型参数替换基本类型。就比如,没有ArrayList<double>,只有ArrayList<Double>。因为当类型擦除后,ArrayList的原始类型变为Object,但是Object类型不能存储double值,只能引用Double的值。

5、运行时类型查询

举个例子:

ArrayList<String> arrayList=new ArrayList<String>();    

因为类型擦除之后,ArrayList<String>只剩下原始类型,泛型信息String不存在了。

那么,运行时进行类型查询的时候使用下面的方法是错误的

if( arrayList instanceof ArrayList<String>)    

java限定了这种类型查询的方式 

if( arrayList instanceof ArrayList<?>)    

? 是通配符的形式 ,将在后面一篇中介绍。

6、异常中使用泛型的问题

1、不能抛出也不能捕获泛型类的对象。事实上,泛型类扩展Throwable都不合法。例如:下面的定义将不会通过编译: 

public class Problem<T> extends Exception{......}  

为什么不能扩展Throwable,因为异常都是在运行时捕获和抛出的,而在编译的时候,泛型信息全都会被擦除掉,那么,假设上面的编译可行,那么,在看下面的定义: 

try{  

}catch(Problem<Integer> e1){  

。。  

}catch(Problem<Number> e2){  

...  

}   

类型信息被擦除后,那么两个地方的catch都变为原始类型Object,那么也就是说,这两个地方的catch变的一模一样,就相当于下面的这样

try{  

}catch(Problem<Object> e1){  

。。  

}catch(Problem<Object> e2){  

...  

这个当然就是不行的。就好比,catch两个一模一样的普通异常,不能通过编译一样: 

try{  

}catch(Exception e1){  

。。  

}catch(Exception  e2){//编译错误  

...  

2、不能再catch子句中使用泛型变量 

public static <T extends Throwable> void doWork(Class<T> t){  

        try{  

            ...  

        }catch(T e){ //编译错误  

            ...  

        }  

   }  

因为泛型信息在编译的时候已经变味原始类型,也就是说上面的T会变为原始类型Throwable,那么如果可以再catch子句中使用泛型变量,那么,下面的定义呢: 

public static <T extends Throwable> void doWork(Class<T> t){  

        try{  

            ...  

        }catch(T e){ //编译错误  

            ...  

        }catch(IndexOutOfBounds e){  

        }                           

 }  

根据异常捕获的原则,一定是子类在前面,父类在后面,那么上面就违背了这个原则。即使你在使用该静态方法的使用T是ArrayIndexOutofBounds,在编译之后还是会变成Throwable,ArrayIndexOutofBounds是IndexOutofBounds的子类,违背了异常捕获的原则。所以java为了避免这样的情况,禁止在catch子句中使用泛型变量。

但是在异常声明中可以使用类型变量。下面方法是合法的。 

public static<T extends Throwable> void doWork(T t) throws T{  

    try{  

        ...  

    }catch(Throwable realCause){  

        t.initCause(realCause);  

        throw t;   

    }  

上面的这样使用是没问题的。

7、数组(这个不属于类型擦除引起的问题)

不能声明参数化类型的数组。如:

Pair<String>[] table = newPair<String>(10); //ERROR  

这是因为擦除后,table的类型变为Pair[],可以转化成一个Object[]。

Object[] objarray =table;  

  数组可以记住自己的元素类型,下面的赋值会抛出一个ArrayStoreException异常。 

objarray ="Hello"; //ERROR  

  对于泛型而言,擦除降低了这个机制的效率。下面的赋值可以通过数组存储的检测,但仍然会导致类型错误。  

 

objarray =new Pair<Employee>();  

提示:如果需要收集参数化类型对象,直接使用ArrayList:ArrayList<Pair<String>>最安全且有效。

8、泛型类型的实例化 

不能实例化泛型类型。如,

[java] view plaincopy 

first = new T(); //ERROR  

   是错误的,类型擦除会使这个操作做成new Object()。

   不能建立一个泛型数组。

  

 

public<T> T[] minMax(T[] a){  

     T[] mm = new T[2]; //ERROR  

     ...  

}  

   类似的,擦除会使这个方法总是构靠一个Object[2]数组。但是,可以用反射构造泛型对象和数组。

   利用反射,调用Array.newInstance:

 

publicstatic <T extends Comparable> T[]minmax(T[] a)  

  

   {  

  

      T[] mm == (T[])Array.newInstance(a.getClass().getComponentType(),2);  

  

       ...  

  

      // 以替换掉以下代码  

  

      // Obeject[] mm = new Object[2];  

  

      // return (T[]) mm;  

  

   }  

9、类型擦除后的冲突

1、

当泛型类型被擦除后,创建条件不能产生冲突。如果在Pair类中添加下面的equals方法:

 

class Pair<T>   {  

    public boolean equals(T value) {  

        return null;  

    }  

      

}  

考虑一个Pair<String>。从概念上,它有两个equals方法:

booleanequals(String); //在Pair<T>中定义

boolean equals(Object); //从object中继承

但是,这只是一种错觉。实际上,擦除后方法

boolean equals(T)

变成了方法 boolean equals(Object)

这与Object.equals方法是冲突的!当然,补救的办法是重新命名引发错误的方法。

2、

泛型规范说明提及另一个原则“要支持擦除的转换,需要强行制一个类或者类型变量不能同时成为两个接口的子类,而这两个子类是同一接品的不同参数化。”

下面的代码是非法的:

 

class Calendar implements Comparable<Calendar>{ ... }  

 

class GregorianCalendar extends Calendar implements Comparable<GregorianCalendar>{...} //ERROR  

GregorianCalendar会实现Comparable<Calender>和Compable<GregorianCalendar>,这是同一个接口的不同参数化实现。

这一限制与类型擦除的关系并不很明确。非泛型版本:

 

class Calendar implements Comparable{ ... }  

 

class GregorianCalendar extends Calendar implements Comparable{...} //ERROR  

是合法的。

10、泛型在静态方法和静态类中的问题

泛型类中的静态方法和静态变量不可以使用泛型类所声明的泛型类型参数

举例说明:

public class Test2<T> {    

    public static T one;   //编译错误    

    public static  T show(T one){ //编译错误    

        return null;    

    }    

}    

因为泛型类中的泛型参数的实例化是在定义对象的时候指定的,而静态变量和静态方法不需要使用对象来调用。对象都没有创建,如何确定这个泛型参数是何种类型,所以当然是错误的。

但是要注意区分下面的一种情况: 

public class Test2<T> {    

    public static <T >T show(T one){//这是正确的    

        return null;    

    }    

}    

因为这是一个泛型方法,在泛型方法中使用的T是自己在方法中定义的T,而不是泛型类中的T。(静态方法和普通方法一样都是在JVM类加载阶段加载到方法区的)。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: