java泛型类型擦除

类型擦除指的是通过类型参数合并,将泛型类型实例关联到同一份字节码上。编译器只为泛型类型生成一份字节码,并将其实例关联到这份字节码上。类型擦除的关键在于从泛型类型中清除类型参数的相关信息,并且在必要的时候添加类型检查和类型转换的方法。

类型擦除的主要过程如下
     1.将所有的泛型参数用其最左边界类型(限定类型)替换。<T>等同于<T extends Object>即T会用Object代替
     2.移除所有的类型参数。

class Collections {    
  public static <A extends Comparable<A>>A max(Collection <A> xs) {   
    Iterator <A> xi = xs.iterator();   
    A w = xi.next();   
    while (xi.hasNext()) {   
      A x = xi.next();   
      if (w.compareTo(x) < 0) w = x;   
    }   
    return w;   
  }   
}

类型擦除后:

class Collections {    
  public static Comparable max(Collection xs) {   
    Iterator xi = xs.iterator();   
    Comparable w = (Comparable) xi.next();   
    while (xi.hasNext()) {   
      Comparable x = (Comparable) xi.next();   
      if (w.compareTo(x) < 0) w = x;   
    }   
    return w;   
  }   
}
<span style="color: rgb(51, 51, 51); font-family: Arial; font-size: 14px; line-height: 26px;">示例中限定了类型参数的边界</span><span style="color: rgb(51, 51, 51); font-family: Arial; font-size: 14px; line-height: 26px;"><A extends Comparable<A>> A</span><span style="color: rgb(51, 51, 51); font-family: Arial; font-size: 14px; line-height: 26px;">,</span><span style="color: rgb(51, 51, 51); font-family: Arial; font-size: 14px; line-height: 26px;">A</span><span style="color: rgb(51, 51, 51); font-family: Arial; font-size: 14px; line-height: 26px;">必须为</span><span style="color: rgb(51, 51, 51); font-family: Arial; font-size: 14px; line-height: 26px;">Comparable<A></span><span style="color: rgb(51, 51, 51); font-family: Arial; font-size: 14px; line-height: 26px;">的子类,按照类型擦除的过程,先将所有的类型参数</span><span style="color: rgb(51, 51, 51); font-family: Arial; font-size: 14px; line-height: 26px;">替</span><span style="color: rgb(51, 51, 51); font-family: Arial; font-size: 14px; line-height: 26px;">换为最左边界</span><span style="color: rgb(51, 51, 51); font-family: Arial; font-size: 14px; line-height: 26px;">Comparable<A></span><span style="color: rgb(51, 51, 51); font-family: Arial; font-size: 14px; line-height: 26px;">,然后去掉参数类型</span><span style="color: rgb(51, 51, 51); font-family: Arial; font-size: 14px; line-height: 26px;">A</span><span style="color: rgb(51, 51, 51); font-family: Arial; font-size: 14px; line-height: 26px;">,得到最终的擦除后结果。</span>
<span style="font-family:Arial;">再通过两个例子证明类型擦除:</span>
<span style="font-family:Arial;">示例1:</span>
<span >	</span>public String test(List<String> s)
<span >	</span>{
		System.out.println("xx");
		return "1";
	}
	
	public  int test(List<Integer> i)
	{
		System.out.println("yy");
		return 1;
	}


由于泛型类型在编译后类型擦除,则这两个函数参数相同,均为List类型,仅仅只是返回值不同,不能重载。


示例2:

public static void main(String[] args)
	{
		List<String> al1 = new ArrayList<String>();
		List<Integer> al2 = new ArrayList<Integer>();
		System.out.println("al1==al2 : "+(al1.getClass()==al2.getClass()));
	}



结果:al1==al2 : true  说明泛型类型String和Integer都被擦除掉了,只剩下了原始类型。


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

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

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

既然说类型变量会在编译的时候擦除掉,那为什么我们往ArrayList<String> arrayList=new ArrayList<String>();所创建的数组列表arrayList中,不能使用add方法添加整型呢?不是说泛型变量String会在编译过后擦除变为原始类型Object吗,既然已经是Object了,那为什么不能存别的类型呢?如何去保证我们只能使用泛型变量限定的类型呢?


java是如何解决这个问题的呢?

java编译器是通过先检查代码中泛型的类型,然后再进行类型擦除,最后再进行编译的。即先检查add方法中添加的是否是String类型,检查通过之后再进行类型擦除,然后进行编译。

举个例子说明:

ArrayList<String> arrayList=new ArrayList<String>();  //限制了类型只能为String,并以此检查
   arrayList.add("123");  
   arrayList.add(123);//编译错误


编译 之前检查第二次add的是整型和泛型类型不同,因此不通过。




那么,这样的类型检查是针对谁的呢?


类型检查就是针对引用的,谁是一个引用,用这个引用调用泛型方法,就会对这个引用调用的方法进行类型检测,而无关它真正引用的对象。比如arrayList是引用,类型检查针对其调用的add方法进行类型检测。


2、自动类型转换

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

public E get(int index) {  
    RangeCheck(index);  
    return (E) elementData[index];  
    }


可以看到,在return之前,会根据泛型变量进行强转



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();  
    }  
}

经过类型擦除,父类变成了下面的样子:

class Pair {  
    private Object value;  
    public Object getValue() {  
        return value;  
    }  
    public void setValue(Object  value) {  
        this.value = value;  
    }  
} (<span style="color:#ff0000;"><strong>思考:这里是泛型类,不是实现泛型接口或者重新泛型方法</strong></span>)

再来分析一下子类的两个方法,来看是否可以重写父类的方法。先来分析setValue方法,父类的类型为Object,子类的类型是Date。按照普通的继承体系则此时不是重写,而是重载。然而对于泛型覆盖

Jvm采用 桥方法 实现了重写。

桥方法针对的是类(不论该类是否为泛型类),而不是接口(不论该接口是否为泛型接口),接口重写只能用泛型方法,不能用具体的方法!!

桥方法的参数类型都是Object,也就是说,子类中真正覆盖父类两个方法的是两个我们看不到的桥方法。而写在我们自己定义的setvalue和getValue方法上面的@Oveerride只不过是假象。而桥方法的内部实现,就只是去调用我们自己想要重写的那两个方法。

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

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

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

getValue却有普遍的意义,如果按照普通的继承体系,Java5.0放宽了限制,只要子类方法与超类方法具有相同的方法签名,或者子类方法的返回值是超类方法的子类型,就可以覆盖。即本身getValue就可以覆盖了,返回值Date是Object的子类。这是协变式覆盖。


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




总结

①协变式覆盖:

只要子类方法与超类方法具有相同的方法签名,或者子类方法的返回值是超类方法的子类型,就可以覆盖

②桥方法:(方法重写发生在运行时,即类型擦除后采用重写,桥方法非常好地解决了泛型中方法重写的问题)

特意为解决类型擦除而引起的不能使用多态的问题。

参考:http://www.importnew.com/1796.html

在此必须注意:泛型类、泛型接口与泛型方法,重写方法是不一样的。

比如:

<pre name="code" class="java">class Demo
{
	
	public <T> void setName(T name){System.out.println("kl");}
	public <T> T getName(){return (T) "d";}
}





public class Demo2 extends Demo
{
        public <T>void setName(String name)
	{
		System.out.println("xxxx");
	}
	public <T> T getName()
	{
		return (T) "3";
	}
}


非泛型类中的泛型方法重写必须这么写,否则就变成重载了。




interface Demo<T>
{
	public void setName(T name);
	public T getName();
}
public class Demo2 implements Demo<Number>
{
       //这个才是正确的实现了的方法
        public void setName(Number name) 
	{
		System.out.println("xxxx");
	}

	public Number getName()
	{
		return 3;
	}
}
</pre><p>②当接口不是泛型接口却包含泛型方法时</p><p></p><pre name="code" class="java">interface Demo
{
	
	public <T> void setName(T name);
	public <T> T getName();
}



public class Demo2 implements Demo
{      
       public <T> void setName(T name)  //此处只能使用泛型方法
	{
		System.out.println("xxxx");
	}
<span >	</span>public <T> T getName()  //最好这么做
<span >	</span>{
<span >		</span>return (T) "d";
<span >	</span>}
}





重中之重!!:非泛型类和非泛型接口中要重写泛型方法都必须不指定具体的类型参数进行重写,否则会变成重载,接口则会少一个实现(即重写的)。泛型类和泛型接口重写方法如果指定了继承或者实现接口的参数类型,则重写要使用该类型,比如extends Demo<Number>,则必须用Number。看是否重写了可以用注解@Override



③类型擦除是在编译时,运行时已被擦除。注意类型擦除的过程:先检查,再擦除,最后编译。检查所有的,如定义对象,引用调用方法等适合进行检查,整个程序检查通过后进行擦除。  如在实例化的时候限制了类型参数,则会按照该类型参数检查,调用方法返回会隐式地在return前强转为该参数类型。之后等到运行之前进行类型擦除。

如:

public class Demo<T>
{
	private T name;
	public void setName(T name)
	{
		this.name = name;
	}
	public T getName()
	{
		System.out.println(this.name.getClass().getName());
		return name;
	}
}
测试类:
public static void main(String[] args)
	{
		Demo<String> stu = new Demo<String>();//限制了类型
		
		stu.setName("wang");
		String name = stu.getName();
		System.out.println(name);
	}


在读到Demo<String> stu = new Demo<String>();时限制了T为String类型,getName()在return语句出默认加了return (String) name;在调用方法时同时限制了只能是String类型,并检查,这也就是在编译期间限制了不能传Integer等类型。当所有的都检查正确以后才开始进行类型擦除。运行之前擦除完毕,运行时setName(T name)已经变为了setName(Object name),传入String类型的“wang”,正确。假如类型参数限制了,如<T extends Number>则运行是setName(T name)变为setName(Number name).     (重中之重!)




④注意泛型在静态方法中的应用:

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

⑤注意java采用的是code sharing方式来解决泛型的,即List<String>和List<Integer>是同一类。

⑥注意类型擦除的方式:

将所有的泛型参数用其最左边界类型(限定类型)替换。<T>等同于<T extends Object>即T会用Object代替
     2.移除所有的类型参数。

Remember!

1.虚拟机中没有泛型,只有普通类和普通方法
2.所有泛型类的类型参数在编译时都会被擦除
3.创建泛型对象时请指明类型,让编译器尽早的做参数检查(Effective Java,第23条:请不要在新代码中使用原生态类型)
4.不要忽略编译器的警告信息,那意味着潜在的ClassCastException等着你。