掘金 后端 ( ) • 2024-04-02 10:44

啥是泛型?

泛型的本质是参数化(可变)类型,定义时给类型指定一个参数,使用时再指定此参数的具体的类型。

这种类型可以用在类、接口、方法中,分别称为泛型类、泛型接口、泛型方法。

为啥要引入泛型?

代码复用

话不多说 直接举例:

private static int add(int a, int b){ 
     return a + b;
} 
private static float add(float a, float b) {
    return a + b; 
} 

没有泛型,每种数据类型都要一个方法。有了泛型,可以复用为一个方法,灰常方便。

private static <T extends Number> double add(T a, T b) { 
    return a.doubleValue() + b.doubleValue(); 
}

泛型的基本使用

泛型类

class Node<T> {    // 泛型符号、可以任意字符, 也可以多个
    private T value ; 
    public T getValue(){ 
        return value ; 
    }
    public void setValue(T value){
        this.value = value ; 
    }
}

泛型接口

interface Node<T>{        // 在接口上定义泛型、同泛型类相似
    public T getValue() ; 
}  

泛型

public <T> T getObject(Class<T> clazz) { // 简单来说就是<> 放在方法前面
    T t = clazz.newInstance();
}

泛型的上下限

在使用泛型的时候,可以为传入的泛型类型实参上下边界做限制,如:类型实参只准传入某种类型的父类或某种类型的子类。

泛型上限

class Node<T extends Number>{  // 此处泛型只能是数字类型
     private T value ; 
     

泛型下限

class Node<T>{
     private T value ; 
}
class Demo {
    public static void fun(Node<? super String> node){ // 只能接收String或者其父类 
        System.out.print(node) ; 
    }
}
 

泛型擦除的一些小问题

是指在编译器处理带泛型定义的类、接口或方法时,会在字节码指令集里抹去全部泛型类型信息,泛型被擦除后在字节码里只保留泛型的原始类型(raw type)。

简单来说:编译后的字节码里面 不存在泛型的信息。

如何证明泛型擦除

原始类型相等

    ArrayList<String> list1 = new ArrayList<String>(); 

    ArrayList<Integer> list2 = new ArrayList<Integer>(); 
    
    System.out.println(list1.getClass() == list2.getClass()); // true

通过反射添加其他类型元素

    ArrayList<Integer> list = new ArrayList<Integer>(); 
    
    list.add(1); //这样调用 add 方法只能存储整形,因为泛型类型为 Integer 
    
    list.getClass().getMethod("add", Object.class).invoke(list, "aaa");

泛型的多态?

类型擦除会造成多态的冲突,而JVM解决方法就是桥接方法。

// 父类
class Node<T> { 
    private T value ; 
    public T getValue(){ 
        return value ; 
    }
    public void setValue(T value){
        this.value = value ; 
    }
}
// 子类
class IntegerNode extends Node<Integer> { 
    private Integer value ; 
    
    @Override
    public Integer getValue(){ 
        return value ; 
    }
    
    @Override
    public void setValue(Integer value){
        this.value = value ; 
    }
}

看起来相当之合理,编译器也不报错,但是问题来了,从泛型擦除我们知道,父类的T会变成Object。父类的类型是Object,子类的类型是Integer,这不是重写,这是重载!

如果是重载,那么应该是子类继承public void setValue(Object value)方法,然后重写一个public void setValue(Integer value)方法,但是子类setValue方法传Object对象时,会发生编译错误。

难道子类IntegerNode继承父类Node的时候,父类中泛型就会从Object变成Integer?

--不可能,绝对不可能。反例:如果一个类被多个类继承,父类泛型都乱套了。

JVM桥接方法

将IntegerNode反编译,其实发现有4个方法,以setValue举例

public void setValue(java.lang.Integer);  // 子类重写的方法

public void setValue(java.lang.Object);  //编译器生成的桥接方法

可以看到桥方法的参数类型都是Object,而桥接方法的内部实现,就只是去调用我们自己重写的那两个方法。

也就是说,子类中真正重写的父类是我们看不到的桥接方法。我们自己定义的setvalue和getValue方法上面的@Oveerride只不过是假象。

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

如何获取泛型的参数类型?

可以通过反射(java.lang.reflect.Type)获取泛型

//getActualTypeArguments 返回确切的泛型参数, 如Map<String, Integer>返回[String, Integer] 

Type type = ((ParameterizedType) clazz).getActualTypeArguments()[0]; 

System.out.println(type);//class java.lang.String