Java泛型以及类型擦除带来的问题

3,115 阅读9分钟

Java在JDK5之后引入泛型,但是Java的泛型是伪泛型,Java虚拟机本身是不支持泛型的。Java的泛型是语法糖。

泛型的优点

  1. 增强编译类型检测,提前检测
  2. 泛型可以泛型算法,增加代码复用性

基本使用

  1. 泛型类/接口

    //泛型类的两种实现方式
    interface Generic1<T>{
    
    }
    
    class Generics2<T> implements Generic1<T>{
        public static void main(String[] args) {
            Generics2<Integer> generics2 = new Generics2<>();
        }
    }
    
    class Generics3 implements Generic1<String>{
        public static void main(String[] args) {
            Generics3 generics3 = new Generics3();
        }
    }
    
  2. 泛型方法

泛型方法,是在调用方法的时候指明泛型的具体类型

public class TestGenerics {

    //泛型方法,只有有定义了<T>才是
    public <T> void test(T t){

    }

    //只是方法里面的参数类型使用到了泛型,不是泛型方法,普通方法
    public void test2(List<String> list){

    }

    //普通方法
    public void test3(List<?> list){

    }

}

泛型与可变长参数

public <T> void show(T... ts) {
    for (T t : ts) {
    	System.out.println(t.toString());
    }
}
genericity.show("Demo", 2333, 23.22);

静态泛型方法

public class Genericity<T> {

    //静态泛型方法
    public static <T> void show(T t) {

    }
    //以下方法编译器报错:
    public static void show(T t) {

    }
}

静态方法无法访问类上定义的泛型,所以只能把泛型定义在方法上。

  1. 通配符

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

泛型的PESC原则

//PESC原则
//这就是Producer extends原则
//当只想从集合中获取元素,请把这个集合看成是生产者,请使用<? extends T>
//这就是Consumer super原则
//当你仅仅想增加元素到集合,把这个集合看成消费者,请使用<? extends T>

//上限通配符  只能读  <? extends Number>
//更灵活 PE List Producter生产者,可以从它里面拿数据,但是你没办法往里面添加数据
public static double sumOfList(List<? extends Number> list){
    //副作用
    //list.add(1) //上限 in 只读,但这个不是严格限制
    // 反射调用最新的不能调用
    // Cause by: java.lang.UnsupportedOperationExcepion

    Class<?> clazz = list.getClass();

    try {
        Method method = clazz.getMethod("add",java.lang.Object.class);
        method.setAccessible(true);
        method.invoke(list,100);
        System.out.println(list.toString());

    }catch (Exception e){

    }
    return 0d;

}

//下限通配符 只能写  super

public static void addNumber(List<? super Integer> list){

}

/**
 * 1.如果你正在编写一个可以使用Object类中提供的功能实现的方法。
 * 2.当代码使用通用类中不依赖与类型参数的方法时,例如list.size或list.clear
 * 事实上,Class<?>之所以这么经常使用,是因为Class<T>中的大部分方法都不依赖与T
 *
 * @param list
 */

//不受限
// ? 退化了,不能使用List中任何依赖类型参数[T]的方法
public static void productList(List<?> list){
    //也有副作用
    //list.add("sss"); //不能使用该方法

    list.size();
    list.add(null);
    list.get(0);
    list.contains(12);
}

上下限通配符的另一个作用

public class Genericity<T> {

    private T t;

    public Genericity(T t) {
        this.t = t;
    }

    public T getT() {
        return t;
    }

    public void setT(T t) {
        this.t = t;
    }
}

上限通配符:

private static void show(Genericity<? extends Number> genericity) {
    System.out.println(genericity.getT());
}

表示传入的实参必须是 Number 或 Number 的子类。

下限通配符:

private static void show(Genericity<? super Integer> genericity) {
    System.out.println(genericity.getT());
}

表示传入的实参必须是 Integer 或者其父类。

但是需要注意的是,泛型的上下边界添加,必须与泛型的声明在一起:

//错误声明    
private <T> T  show(Genericity<T extends Integer> genericity) {
        
}
//正确声明
private <T extends Integer> T  show(Genericity<T> genericity) {
	return genericity.getT();
}

  1. 类型擦除以及带来的问题

    1. 通过下面的例子,我们可以明白,类型检查就是针对引用的,谁是一个引用,用这个引用调用泛型方法,就会对这个引用调用的方法进行检测,而无关它真正引用的对象。
    public class Test10 {  
    
        public static void main(String[] args) {  
              
            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  
        }  
    } 
    
    
    1. 格外注意泛型中引用传递的问题

                ArrayList<String> arrayList1=new ArrayList<String>();  
      
                arrayList1.add(new String());  
                arrayList1.add(new String()); 
       
                ArrayList<Object> arrayList2=arrayList1;//编译错误  
      
      
      					ArrayList<Object> arrayList1=new ArrayList<Object>();  
      
                arrayList1.add(new Object());  
                arrayList1.add(new Object());  
      
                ArrayList<String> arrayList2=arrayList1;//编译错误 
      
      
    2. 虚拟机巧妙的使用了桥方法,来解决类型擦除和多态的冲突。

      类型擦除最后都转成Object了(原来Date),这样变成了方法重载。这样类型擦除就和多态有了冲突,JVM的本意知道吗,知道!但是它不能直接实现,于是JVM采用了一个特殊的方法,来完成这项功能,那就是桥方法。

      class DateInter extends Pair<Date> {  
      
          @Override  
          public void setValue(Date value) {  
              super.setValue(value);  
          }  
      
          @Override  
          public Date getValue() {  
              return super.getValue();  
          }  
      
      }
      
      //但是
      public static void main(String[] args) throws ClassNotFoundException {  
      
              DateInter dateInter=new DateInter();  
      
              dateInter.setValue(new Date());                  
              dateInter.setValue(new Object());//编译错误   确实报错了
       }
      
      

​ 首先,我们用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  
}  

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

不过,要提到一点,这里面的setValue和getValue这两个桥方法的意义又有不同。setValue方法是为了解决类型擦除与多态之间的冲突。 而getValue却有普遍的意义,怎么说呢,如果这是一个普通的继承关系:

那么父类的setValue方法如下:

public Object getValue() {  
        return super.getValue();  
    }  

而子类重写的方法是:

public Date getValue() {  
        return super.getValue();  
    }  

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

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

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

​ 5. 运行时类型查询

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

​ 因为类型擦除之后,ArrayList只剩下原始类型,泛型信息String不存在了。那么,运行时进行类 型查询的时候使用下面的方法是错误的

if( arrayList instanceof ArrayList<String>)

​ Java限定了这种类型查询的方式:

if( arrayList instanceof ArrayList<?>)

​ 6. 异常中使用泛型的问题

​ 7. 泛型在静态方法和静态类中的问题

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

​ 8. 类型擦除后的冲突

class Pair<T>   {  
    public boolean equals(T value) {  
        return null;  
    }  
      
} 

//实际上,擦除后方法boolean equals(T)变成了方法 boolean equals(Object)这与Object.equals方法是冲突	    // 的!当然,补救的办法是重新命名引发错误的方法

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

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


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

//合法
class Calendar implements Comparable{ ... }  

class GregorianCalendar extends Calendar implements Comparable{...} 

​ 10. 无法创建参数化类型数组。无法创建类型参数实例(new T()),但是可以通过类类型的反射来创建。重载时注意,比如参数的泛型问题。

  1. 其他:

泛型并不是最终都转换成Object

class Node1<T extends Comparable<T>>         //这里最终替换成Comparable类

受限的类型参数:

  1. 面试题

    • array(数组)中可以使用泛型吗?

      不能,Effective建立使用List,因为List可以提供编译器的类型安全保证,而Array却不能。

    • Java中Set与Set<?>到底区别在哪里?

      一个是受类型检查的,一个是不受类型检查的

      List<?>和List的区别在哪里?

      ?是一个位置类型的List,而List其实是任意类型的List。你可以List,List赋值给List<?>,却不能把List赋值给List

    • 泛型优点

      1. 类型检测提前到编译期,便于更早发现错误
      2. 代码复用
    • 限定通配符 extends super,非限定通配符? PESC原则

    • C++模板和java泛型有什么不同?提供了宏指令,细化成真正的模板代码,需要仔细研究(如果要求C++)。

    • 参考文章:

      www.jianshu.com/p/f5773dec6…

      blog.csdn.net/s10461/arti…