1、为什么要引入泛型
泛型的引入是为了实现类型的参数化,使得我们在创建函数但是需要调用者来指定使用的类型的时候,不用使用Object类型。而泛型被作用于类,接口,方法中。这些也被叫做泛型类,泛型接口,泛型方法。
泛型的好处:
1.1、提高了代码的复用性,
如果不使用泛型,下列代码应该这样写:
private static int add(int a, int b) {
System.out.println(a + "+" + b + "=" + (a + b));
return a + b;
}
private static float add(float a, float b) {
System.out.println(a + "+" + b + "=" + (a + b));
return a + b;
}
private static double add(double a, double b) {
System.out.println(a + "+" + b + "=" + (a + b));
return a + b;
}
但是通过泛型我们就可以减少代码的冗余程度:
private static <T extends Number> double add(T a, T b) {
System.out.println(a + "+" + b + "=" + (a.doubleValue() + b.doubleValue()));
return a.doubleValue() + b.doubleValue();
}
1.2、类型安全
如果我们使用List集合而不使用泛型的时候,取出的元素都是Object类型,需要我们认为进行元素强制类型转换,转换到目标类型。但是这个过程中就容易出现java.lang.ClassCastException异常,
而且,例如不使用泛型的Arraylist的元素类型都是Object,无法约束其中的元素类型。引入泛型将进行编译前类型检查。约束了其中可以存放元素的类型,取出时也不用进行强制类型转换。
2、泛型的上下限
泛型上限:
public static void funC(List<? extends A> listA) {
// ...
}
泛型上限使用extends完成,编译器在编译的时候会把泛型擦除为A类型,所以我们可以传入A的子类或A类型不报错。
泛型下限:
public static void fun(Info<? super String> temp){
//...
}
下限规定之后,只能接收String或Object类型的泛型,String类的父类只有Object类
3、泛型擦除机制
泛型机制是JDK1.5引入的新内容,所以说需要与老的代码兼容,所以说Java实现的其实是一种”伪泛型“的机制,即在编译期会把<>里的内容全部擦去,替换为具体的类型。
替换规则:
总会替换为当前范围内上限最高的类型,例如:
- 如果没有限制的时候,将替换为最高的Object;
- 如果<? extends A>的时候,将替换为A;
- 如果是<?super String>的时候,将替换为Object
验证泛型擦除机制:
public class Test {
public static void main(String[] args) {
ArrayList<String> list1 = new ArrayList<String>();
list1.add("abc");
ArrayList<Integer> list2 = new ArrayList<Integer>();
list2.add(123);
System.out.println(list1.getClass() == list2.getClass()); // true
}
}
4、泛型的编译器检查
我们上面说了泛型会有擦除机制,会把所有的擦除改变为Object。那为什么我们在往ArrayList里面添加整数会报错呢?
Java编译器是先检查了代码中的泛型类型,在进行泛型的擦除,再进行编译。
public static void main(String[] args) {
ArrayList<String> list = new ArrayList<String>();
list.add("123");
list.add(123);//编译错误
}
根据这段代码我们就能看出来,泛型检查就在擦除前。
我们在写的时候会出现下面两种情况:
ArrayList<String> list1 = new ArrayList(); //第一种 情况
ArrayList list2 = new ArrayList<String>(); //第二种 情况
然后我们分别往两个list中添加整形元素,我们发现list1在添加的时候编译报错,但是list2却能正常的添加。
这是因为本质上两个list都是new了一个ArrayList对象,并没有。而泛型的类型检查是通过对象的类型进行的编译前检查,从而保证类型安全。
5、泛型的桥接方法
类型擦除会造成多态的冲突,而JVM解决方法就是桥接方法。
如果我们有这么一个泛型类:
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();
}
}
我们可以看到在子类中我们覆盖了弗雷德两个方法,但是因为泛型擦除的机制,编译的时候父类方法中的泛型都被擦除为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();
}
为我们的本意是把子类中的泛型也都指定为Date类型,但是由于泛型擦除机制,此时的父类和子类中同名方法的返回类型就变得不相同了,而重写也就变成了重载,泛型擦除就和多态发生冲突。
为了解决这种冲突,JVM引入了泛型的桥接方法。
我们反编译子类的代码
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方法上面的@Override只不过是假象。而桥方法的内部实现,就只是去调用我们自己重写的那两个方法。