泛型

52 阅读5分钟

携手创作,共同成长!这是我参与「掘金日新计划 · 8 月更文挑战」的第14天,点击查看活动详情

范型

Java范型时JDK5引入的特性,允许在定义类、接口和方法的时候可以使用类型参数。声明的类型参数会在使用的时候替换为具体的类型。

范型是java提供的语法糖,我们所定义的范型在编译期间都会被类型擦除,使用泛型可提高代码的复用性。Java的集合框架都使用了范型,我们平常所定义的List、List这两个集合类型是相同的,在编译的时候会进行类型擦除,擦除后的类型变为原始类型List。

类型擦除(type erasue)

例子

写几个例子,反编译查看

例一:

final List<String> list = new ArrayList<>(10);
list.add("可可爱爱");
System.out.println(list.get(0));

反编译后:

List list = new ArrayList(10);
list.add("\u53EF\u53EF\u7231\u7231");
System.out.println((String)list.get(0));

编译过后List的类型被擦除,也就是说只有List类型,而不存在List这个类型。中间对于元素的具体操作,通过类型转化实现。

例二:

/**
 * 指定范型类型,那么CompareTo方法也必须指定类型,如果不指定那么就会替换为其左边界Object
 * 编译后Comparable<MyNumericValue>的类型被擦除,变为Comparable<Object>
 * 那么直接导致未能实现compareTo方法
 * 编译器检测到了,就给生成一个桥接方法compareTo(Object object)
 */
class MyNumericValue implements Comparable<MyNumericValue> {
    private int value;
    @Override
    public int compareTo(MyNumericValue o) {
        return this.value - o.value;
    }
}
class MyNumericValue2 implements Comparable {
    private int value;
    @Override
    public int compareTo(Object o) {
        return 0;
    }
}

反编译查看:

class MyNumericValue implements Comparable {
    MyNumericValue() {
    }
    public int compareTo(MyNumericValue o) {
        return value - o.value;
    }
    public volatile int compareTo(Object obj) {
        return compareTo((MyNumericValue) obj);
    }
    private int value;
}
//
class MyNumericValue2 implements Comparable {
    MyNumericValue2() {
    }
    public int compareTo(Object o) {
        return 0;
    }
    private int value;
}

对于一些范型接口的使用,如果接口的抽象方法的入参、出参是范型类型的话,如果在类型擦除时导致未能实现接口方法时,需要编译器生成桥接方法,通过此桥接方法调用原始方法。

例三:

public <T extends List<E>, E extends Comparable<E>> void max(List<E> list) {
    E max = list.get(0);
    for (E e : list) {
        if (e.compareTo(max) > 0) {
            max = e;
        }
    }
    System.out.println("最大值=>" + max);
}

反编译查看:

public void max(List list) {
    Comparable max = (Comparable) list.get(0);
    Iterator iterator = list.iterator();
    do {
        if (!iterator.hasNext())
            break;
        Comparable e = (Comparable) iterator.next();
        if (e.compareTo(max) > 0)
            max = e;
    } while (true);
    System.out.println((new StringBuilder()).append("\u6700\u5927\u503C=>").append(max).toString());
}

E extends Comparable限定了范型边界,那么首先将所有的范型E替换为最左边界 Comparable,然后进行类型擦除得到最终擦除后的结果 Comparable。

增强for循环底层使用的是迭代器进行遍历。

小结

类型擦除指的是通过类型参数合并,将范型类型实例关联到一份字节码文件上,编译器只为范型类生成一份字节码文件。

类型擦除的过程中jvm会将范型java代码转化为普通java代码,编译器直接操作的是字节码。

范型带来的问题

java的违范型机制,类似于语法糖,它方便了我们编写代码,并提高了代码的可复用性,那么复杂的工作就是由jvm待做。

重载

重载的条件是方法名相同、参数列表不同。那么如果对于相同的方法名,使用List、List,可以进行重载么?

显然是不行的,因为之前我们就说过jvm中只有List这一种类型,并不存在List、List,因为编译会进行类型擦除,

idea直接提示,两个方法拥有相同的范型,编译不可通过。

'method1(List<String>)' clashes with 'method1(List<Integer>)'; both methods have same erasure

public void method1(List<String> list){
}
public void method1(List<Integer> list){
}

范型通配符

常用泛型符号

范型符号都有对应含义,常用范型符号如下:

  • E element (在集合框架中表示元素的意思)

  • T Type java类

  • K key (键值)

  • V value (value值)

  • N number 数值类型

  • ? 未知java类型(无限制通配符类型)

对于普通泛型符号(除?以外),必须预先声明类型才可以使用,也就是在我们使用的时候必须确定类型,否则编译报错。

image-20220828152230268

而对于?通配符来说无需事先声明,表示任意类型。正是因为无需声明类型,那么对于编译器来说它不知道如何对类型进行强转,就造成了此方式的泛型无法添加元素,且获取得到的元素也只能是Object类型。

如下List<?> list 表示此方法可以接受任意类型的集合对象

class  TestClass2 {
    public void test(List<?> list){
        //list.add("");
        final Object o = list.get(0);
        System.out.println(o);
    }
    public static void main(String[] args) {
        final TestClass2 testClass2 = new TestClass2();
        testClass2.test(new ArrayList<Integer>(Arrays.asList(1,2)));
        testClass2.test(new ArrayList<String>(Arrays.asList("1","2")));
        testClass2.test(new ArrayList<Comparable>(Arrays.asList(1,2)));
    }
}
限定通配符

对于非限定通配符来说,也有不好的地方,它不限制类型,那么在方法逻辑中想使用指定类型的方法时需要强制转化,如此可能出现ClassCastException异常。

class TestClassC<T> {
    T t;
    public void test(T t1,T t2){
        final Comparable t1c = (Comparable) t1;
        System.out.println(t1c.compareTo(t2));
    }
}

如上我们可以限定test的参数类型为Comparable的实现类。所以java为我们引入了限定通配符

限定通配符对类型进行限制,java中有两种限定通配符:

  • 表示上界,<? extends T>

    泛型类型必须为T或T的派生类(可以是接口、也可以是子类)这里没有用任何意义上的继承关系。

    表示可接收任意T即T的派生类类型。 用于取值。

image-20220828164524819

  • 表示下界,<? super T>, 即类型必须为T或T的父类型

    泛型类型必须为T或T的父类。

    表示可接收任意T即T的父类类型。 用于存值。

image-20220828165307611

指定类型就不行了,因为String不是Fruit的父类类型

image-20220828165336604

关于存取值问题

image-20220828170849157

小结:

如果对于泛型没有限制,并且集合类型支持存取的话,直接使用非限定通配符T E等。

如果对于泛型有限制,则使用限定通配符 extents支持取,super支持存。

?对于取是不友好的,因为?表示任意java类型,那么取出来的一定是Object,而Object没有意义,强转存在ClassCastExpression异常风险。