Java泛型的原理和引发的问题你真的知道吗?

什么是泛型?

JDK 1.5 的新特性,泛型的本质是参数化类型,也就是说所操作的数据类型被指定为一个参数。Java泛型被引入的好处是安全简单。

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

泛型的好处是在编译的时候检查类型安全,并且所有的强制转换都是自动和隐式的,提高代码的重用率。


泛型在使用时的一些规则和限制:
  • 泛型的类型参数只能是类类型(包括自定义类),不能是简单类型。
  • 同一种泛型可以对应多个版本(因为参数类型是不确定的),不同版本的泛型类实例是不兼容的。
  • 泛型的类型参数可以有多个。
  • 泛型的参数类型可以使用extends语句,表示改类型得是继承类或其的子类
  • 泛型的参数类型还可以是通配符类型。

泛型的作用
  • 限定类型就已经有很大作用了,特别是写基础架构的时候,不需要以前那样的检查,我们的代码量和开发速度都可以提升一大截;
  • Think IN JAVA : 能够进行编译期间类型检查;(官方)
  • 限定类型啊 通俗点比喻 (箱子贴标签)这个箱子是放苹果的 那个箱子是放橘子的
  • 封装一些共性问题,可以简化很多代码,使代码更加有层次,简单
  • 比object类范围明显缩小了,提高了程序运行的效率

java泛型实现原理:类型擦出
  • Java的泛型是伪泛型。在编译期间,所有的泛型信息都会被擦除掉。正确理解泛型概念的首要前提是理解类型擦出(type erasure)

  • Java中的泛型基本上都是在编译器这个层次来实现的。在生成的Java字节码中是不包含泛型中的类型信息的。使用泛型的时候加上的类型参数,会在编译器在编译的时候去掉。这个过程就称为类型擦除。

举个例子吧

如在代码中定义的List和List等类型,在编译后都会编程List。**JVM看到的只是List,而由泛型附加的类型信息对JVM来说是不可见的。**Java编译器会在编译时尽可能的发现可能出错的地方,但是仍然无法避免在运行时刻出现类型转换异常的情况。类型擦除也是Java的泛型实现方法与C++模版机制实现方式之间的重要区别。

    //我们可以从下面例子看出,java定义的泛型并不是真正的泛型,它的实现是在编译时对泛型信息进行擦除而已
	public static void main(String[] args) throws Exception {
        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));
        }
    }
    /**输出:
    		1
    		asd
    */

类型擦除后保留的原始类型,原始类型(raw type)就是擦除去了泛型信息,最后在字节码中的类型变量的真正类型。无论何时定义一个泛型类型,相应的原始类型都会被自动地提供。类型变量被擦除(crased),并使用其限定类型替换。它默认会使用所有添加的对象的最小共同父类( 在不指定泛型的情况下,泛型变量的类型为 该方法中的几种类型的同一个父类的最小级,直到Object)。

public class Test2{  
    public static void main(String[] args) {  
        /**不指定泛型的时候*/  
        int i=Test2.add(1, 2); //这两个参数都是Integer,所以T为Integer类型  
        Number f=Test2.add(1, 1.2);//这两个参数一个是Integer,以风格是Float,所以取同一父类的最小级,为Number  
        Object o=Test2.add(1, "asd");//这两个参数一个是Integer,以风格是Float,所以取同一父类的最小级,为Object  
  
                /**指定泛型的时候*/  
        int a=Test2.<Integer>add(1, 2);//指定了Integer,所以只能为Integer类型或者其子类  
        int b=Test2.<Integer>add(1, 2.2);//编译错误,指定了Integer,不能为Float  
        Number c=Test2.<Number>add(1, 2.2); //指定为Number,所以可以为Integer和Float  
    }  
      
    //这是一个简单的泛型方法  
    public static <T> T add(T x,T y){  
        return y;  
    }  
}  
  • list类型对象是否可以赋给一个List类型引用?
public class Test {
    //编译不通过
    public static void main(String[] args) {
        List<String> a = null;
        List<Object> bNew = a;
    }
}

这段代码会报编译错误,原因如下:

这里的Object 和 String 仅仅是给编译器做编译的时候检查用的。这里的List 和List 并没有什么父子类的关系,仅仅是表示一个用来装Obejct型对像,一个用来装String型对像。

这种转换只能在子类与父类之间转换,虽然Object是String的父类,但是List和List在编译器看来,是两种完全不同的东西,不允许你这样转换。

如果要转换,可以用下面方法:跳过编译器的检查

    public static void main(String[] args) {
        List<String> a = new ArrayList<>();
        List<Object> b = convert(a);
    }

    @SuppressWarnings("unchecked")
    public static <T> List<T> convert(List<?> list) {
        return (List<T>)list;
    }

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

java不能实现真正的泛型,只能使用类型擦除来实现伪泛型,这样虽然不会有类型膨胀的问题,但是也引起了许多新的问题。所以,Sun对这些问题作出了许多限制,避免我们犯各种错误。

1、先检查,在编译,以及检查编译的对象和引用传递的问题
public static  void main(String[] args) {  
        ArrayList<String> arrayList=new ArrayList<String>();  
        arrayList.add("111");  
        arrayList.add(111);//编译错误  
}  

在 String 中添加 integer 时,编译器会检查,如果是在编译后检查,在类型擦除后会使object类型,反而是可以添加的,这说明泛型检查是在编译前检查的。

我们再看一下下面这个例子,在继续讨论编译时检查是检查哪个地方

    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  
    } 

本来类型检查就是编译时完成的。new ArrayList()只是在内存中开辟一个存储空间,可以存储任何的类型对象。而真正涉及类型检查的是它的引用,因为我们是使用它引用arrayList1 来调用它的方法,比如说调用add()方法。所以arrayList1引用能完成泛型类型的检查。而引用arrayList2没有使用泛型,所以不行。

那么它为什么要检查对象的引用呢?我们看一下下面的例子

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

如果编译通过,那我们调用 get 方法时,获得到的对象是 String 类型的,可是实际上它存放的是 Object 类型的,那么我们会遇到 ClassCastException 异常,所以为了避免这样的错误,java不允许这样的引用传递

那我们能不能反过来呢?我们看一下下面这个例子

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

也是报错,那为什么还会报错呢?这样不是不会又 ClassCastException 错误吗?我们将 String 转为 Object 有什么意义呢?泛型出现的原因,就是为了解决类型转换的问题。我们使用了泛型,到头来,还是要自己强转,违背了泛型设计的初衷。所以 java 也不允许这样的情况出现。

2、自动类型转换

因为类型擦除的问题,所以所有的泛型类型变量最后都会被替换为原始类型。这样就引起了一个问题,既然都被替换为原始类型,那么我们在获取的时候,为什么不需要进行强制类型转换呢?我们看一下 ArrayList 的 get 方法的实现。

    @SuppressWarnings("unchecked")
    E elementData(int index) {
        return (E) elementData[index];//类型强制转换
    }

    public E get(int index) {
        rangeCheck(index);//范围判断
        return elementData(index);//返回元素
    }

看以看到,在return之前,会调用另一个方法进行强转。假设泛型类型变量为 String ,虽然泛型信息会被擦除掉,但是会将(E) elementData[index],编译为(String )elementData[index]。所以我们不用自己进行强转。

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

我们先来看一个问题来理解这个问题,假设我们有一个泛型类

class Pair<T> {
	private T value;
	public T getValue() {
		return value;
	}
	public void setValue(T value) {
		this.value = value;
	}
}

然后我们再定义一个子类继承它

class DateInter extends Pair<Date> {
	@Override
	public void setValue(Date value) {
		super.setValue(value);
	}
	@Override
	public Date getValue() {
		return super.getValue();
	}
}

在这个子类中,我们设定父类的泛型类型为Pair,在子类中,我们覆盖了父类的两个方法,我们的原意是这样的:将父类的泛型类型限定为Date,那么父类里面的两个方法的参数都为Date类型,我们再子类中也看到了@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,参数类型不一样,这如果实在普通的继承关系中,根本就不会是重写,更像是重载,我们测试一下

public static void main(String[] args) throws ClassNotFoundException {
		DateInter dateInter=new DateInter();
		dateInter.setValue(new Date());                
        dateInter.setValue(new Object());//编译错误,不允许存放 Object 类型
 }

如果是重载,那么子类中会有两个setValue方法,一个是参数Object类型,一个是Date类型,可是我们发现,根本就没有这样的一个子类继承自父类的 Object 类型参数的方法。所以说,确实是重写,而不是重载。那为什么是这种情况呢?底层 JVM 是怎么实现的呢?我们想的是传入父类 Date ,然后将父类变成如下

class Pair {
	private Date value;
	public Date getValue() {
		return value;
	}
	public void setValue(Date value) {
		this.value = value;
	}
}

然后子类再去继承父类重写get和set方法,实现继承中的多态,可是由于种种原因,虚拟机并不能将泛型类型变为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方法
)
       8: return
}

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

所以,虚拟机巧妙的使用了桥方法,来解决了类型擦除和多态的冲突。不过,要提到一点,这里面的setValue和getValue这两个桥方法的意义又有不同。setValue方法是为了解决类型擦除与多态之间的冲突。而getValue却有普遍的意义,怎么说呢,如果这是一个普通的继承关系,那么父类的 setValue 方法以及子类 setValue 方法如下:

//这两个方法同时存在一个类中编译是会报错的,而两个setValue是可以的
public ObjectgetValue() {
	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不存在了,所以下面第三行会报错,java限定了这种类型查询的方式,所以使用通配符

public static void main(String[] args) {
    ArrayList<String> arrayList = new ArrayList<>();
    if( arrayList instanceof ArrayList<String>)//报错
    if( arrayList instanceof ArrayList<?>)//没报错
}
6、异常中使用泛型的问题
  • 不能抛出也不能捕获泛型类的对象。

事实上,泛型类扩展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两个一模一样的普通异常,不能通过编译一样。

  • 不能在 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 = new Pair<String>(10); //报错

这是因为擦除后,table的类型变为Pair[],可以转化成一个Object[]。数组可以记住自己的元素类型,下面的赋值会抛出一个ArrayStoreException异常。

objarray ="Hello"; //ERROR

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

 objarray =new Pair<Employee>();

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

8、泛型类型的实例化

不能实例化泛型类型或者泛型数组。如:

first = new T(); //ERROR
public<T> T[] minMax(T[] a){
    T[] mm = new T[2]; //ERROR
    ...
}

但是,可以用反射构造泛型对象和数组。利用反射,调用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、类型擦除后的冲突
  • 当泛型类型被擦除后,创建条件不能产生冲突。如果在Pair类中添加下面的equals方法:
class Pair<T>   {
//会报错,编译器会让你将public boolean equals(T value)变成继承的方法public boolean equals(Object value)
	public boolean equals(T value) {
		return null;
	}
}

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

boolean equals(String); //在Pair中定义

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

但是,这只是一种错觉。实际上,擦除后方法 boolean equals(T) 变成了方法 boolean equals(Object)

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

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

下面的代码是非法的:

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

而下面代码则是合法的:

class Calendar implements Comparable{ ... }
class GregorianCalendar extends Calendar implements Comparable{...}
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。


总结

  • 泛型的原理
  • 泛型的使用规则
  • 泛型的优点
  • 泛型引起的问题
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值