Java全栈知识(5)泛型机制详解

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

image.pngimage.png

验证泛型擦除机制:

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只不过是假象。而桥方法的内部实现,就只是去调用我们自己重写的那两个方法。

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

东莞呵呵

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值