1 泛型是什么
泛型是一种在编译期间进行集合中的元素进行限定的机制。使用了泛型,在运行期见可以安全的将元素强转成指定的元素。下面举个例子看一下有和没有泛型的区别
1.1 假如没有泛型
List arrayList = new ArrayList();
arrayList.add("aaaa");
arrayList.add(100);
for(int i = 0; i< arrayList.size();i++){
String item = (String)arrayList.get(i);
Log.d("泛型测试","item = " + item);
}
在没有使用泛型的情况下,我们往例子中的arrayList存进Integer 或者 String 都是合法的,但是在从arrayList往外取数据的时候,就会因为强转而报错(毕竟我们不可能一直使用Object)
1.2 使用泛型
List arrayList = new ArrayList();
arrayList.add("aaaa"); // IDE报异常arrayList.add(100);
当我们使用了泛型,上面代码中第二行IDE就会报异常。
在这个例子中,JAVA通过泛型这个机制帮助我们规范集合中的数据,避免我们在使用集合的时候出现异常,提前帮我们发现问题。
泛型的作用大抵可以概括为限制。
2 泛型是怎么做到的
在Java中,泛型分为两部分:类型擦除 和强转
2.1 类型擦除
当我们在代码中使用泛型的时候,编译器(例如:Javac)就会分析代码运行时要装入的数据是否符合我们的要求,如果顺利通过检查,就会生成普通的不带泛型的字节码,这种字节码可以被一般的 Java 虚拟机接收并执行。这种技术被称为擦除(erasure)。
也就是说,泛型的检查是发生在编译期,经过编译器检查而生成的字节码和普通的不使用泛型的代码所生成的字节码没什么区别。可以理解为在实际运行中,上面例子中的List 的字节码和 List 没区别,列表中本质上都是Object
举个例子
List l1 = new ArrayList();
List l2 = new ArrayList();
System.out.println(l1.getClass() == l2.getClass());
运行结果是true,证明Java对泛型进行了类型擦除,或者说是 一个泛型类被其所有调用共享。即 List被所有的 List 共享。
为什么Java要进行类型擦除,在编译器而不是在JVM进行限制呢?
一个说话是为了向下兼容
泛型是JDK1.5的“新特性”,如果选择在JVM层面来实现泛型,那么就无法兼容使用JDK1.5版本之前的服务器。而如果在编译器进行检查,然后生成同样的字节码的话,就可以完美兼容之前的版本。
2.2 强转
经过类型擦除,字节码被编译成了统一的Object,那为什么当我们在使用泛型的,得到的是一个具体类,而不是一个Object?
这是因为Java的编译器帮我们做了强转,避免我们自己去手动强转对象。比如
LinkedList ys = new LinkedList();
ys.add("zero");
ys.add("one");
String y = ys.iterator().next();
可以理解为
LinkedList ys = new LinkedList();
ys.add("zero");
ys.add("one");
String y = (String)ys.iterator().next();
总结一句话就是 泛型是一个在编译时期检查类型,在存入将对象转成Object类型,在取出的时候进行强转的机制。
3 Java实现泛型的代价-桥方法(bridge method)
我们通过编写一个Byte类来发现问题
class Byte implements Comparable {
private byte value;
public Byte(byte value) {
this.value = value;
}
public byte byteValue() {
return value;
}
public int compareTo(Byte that) {
return this.value - that.value;
}
}
我们知道,泛型本质上是一个编译器检查,在运行期中是以Object的形式存在的。在 Byte 这个例子中,如果他在运行期中是如何调用 public int compareTo(Byte that) 这个方法的。要知道,在运行期中只有Object。
为此,Java用了一种叫桥方法的技术。
我们将这个类编译成字节码文件,然后通过jdk提供的javap工具查看 Byte.class文件,如下所示
Compiled from "Byte.java"
class com.joe.test.demo.controller.testJava.Byte implements com.joe.test.demo.controller.testJava.Comparable {
private byte value;
public com.joe.test.demo.controller.testJava.Byte(byte);
public byte byteValue();
public int compareTo(com.joe.test.demo.controller.testJava.Byte);
public int compareTo(java.lang.Object);
}
可以发现,反编译出来的文件比较原始Java文件多了 public int compareTo(java.lang.Object); 这个方法,该方法的实现也很简单,先将Object参数强转成 Byte,然后调用 public int compareTo(com.joe.test.demo.controller.testJava.Byte);。
compareTo(java.lang.Object); 就是一个桥方法,通过引用桥方法,使得泛型代码在运行时期也能执行响应的方法。桥方法顾名思义,他的作用就是一个桥梁,连接Object对象和泛型参数的方法。
3.1 两个有意思的例子
3.1.1 使用泛型时要避开的方法名
class Pair {
public boolean equals(T value) {
return null;
}
}
上面这个类在IDE上会报异常,因为桥方法生成的方法和Object默认的方法竟然一模一样,这样的方法自然不被允许。
3.1.2 “不合法”的Java代码
举一个关于桥方法的极端例子,假设我们有以下这个类
class Pair {
private T value;
public T getValue() {
return value;
}
public void setValue(T value) {
this.value = value;
}
}
然后我们想要一个子类继承它
class DateInter extends Pair {
@Override
public void setValue(Date value) {
super.setValue(value);
}
@Override
public Date getValue() {
return super.getValue();
}
}
在这个子类中,我们设定父类的泛型类型为Pair。根据我们上面讨论的结果,泛型的本质是Object,同时会生成桥方法,如下所示
public void setValue(java.util.Date);
public java.util.Date getValue();
public void setValue(java.lang.Object);
public java.lang.Object getValue();
我们可以看到Object getValue()和Date getValue()是同 时存在的,可是如果是常规的两个方法,他们的方法签名是一样的,也就是说虚拟机根本不能分别这两个方法。如果是我们自己编写Java代码,这样的代码是无法通过编译器的检查的,但是虚拟机却是允许这样做的,因为虚拟机通过参数类型和返回类型来确定一个方法,所以编译器为了实现泛型的多态允许自己做这个看起来“不合法”的事情,然后交给虚拟器去区别。
4 源文信息
4.1 版权本文是由作者阅读了大量博客,根据作者的笔记整理而来。因为时间跨度大,无法一一指出来源,因此会在文章结尾列出所有参照的博客源地址。
如果您认为本文侵权,请联系作者
转载本文麻烦注明源文
4.2 参考博客Java 语言类型擦除www.ibm.com多角度看 Java 中的泛型www.ibm.com
人类身份验证 - SegmentFaultsegmentfault.comhttps://blog.csdn.net/briblue/article/details/76736356blog.csdn.nethttps://blog.csdn.net/lonelyroamer/article/details/7868820blog.csdn.net