转载

Java 中的泛型

泛型,即“参数化类型”。一提到参数,最熟悉的就是定义方法时有形参,然后调用此方法时传递实参。那么参数化类型怎么理解呢?顾名思义,就是将类型由原来的具体的类型参数化,类似于方法中的变量参数,此时类型也定义成参数形式(可以称之为类型形参),然后在使用/调用时传入具体的类型(类型实参)。

泛型的本质是为了参数化类型(在不创建新的类型的情况下,通过泛型指定的不同类型来控制形参具体限制的类型)。也就是说在泛型使用过程中,操作的数据类型被指定为一个参数,这种参数类型可以用在类、接口和方法中,分别被称为泛型类、泛型接口、泛型方法。

为什么我们需要泛型?

java.lang.ClassCastException

泛型类的定义

引入一个变量 T(其他任意字母都可以),并用<>括起来,放到类的后面,

public class GenericType<T> {
    private T data;
}

泛型方法的辨析

普通方法

public T getData() {
    return data;
}

虽然这个方法使用了泛型,但是这只是一个普通方法.

只不过在返回值是在声明泛型类时已经声明过的泛型.

泛型方法

public static <T> T genericMethod(T t) {
    System.out.println(t.getClass().getSimpleName());
    return t;
}

首先在 public 与返回值之间声明一个泛型 T ,表明这是一个泛型方法.

这个 T 可以出现在泛型方法的任意位置.

泛型的数量可以是任意多个 <K,V>

限定类型变量

当我们需要对类型变量进行约束,如需要这两个变量一定要有 compareTo 方法时,可以使用 T extends Comparable 将 T 限制为实现某个接口或类.

public static <T extends Comparable> T min(T a, T b) {
    if (a.compareTo(b) > 0)return b;else return a;
}

同时extends左右都允许有多个,如 T,V extends Comparable & Serializable

注意限定类型中,只允许有一个类,而且如果有类,这个类必须是限定列表的第一个。

这种类的限定既可以用在泛型方法上也可以用在泛型类上。

public static <T extends Comparable & Serializable> T min(T a, T b) {
    return a.compareTo(b) > 0 ? b : a;
}

泛型中的约束和局限性

1. 不能使用基本类型实例化类型参数

2. 运行时类型检查只适用于原始类型

GenericType<String> stringGenericType = new GenericType<>();
GenericType<Integer> integerGenericType = new GenericType<>();
//编译出错
//if (stringGenericType instanceof GenericType<String>) {}
//相等
System.out.println(stringGenericType.getClass() == integerGenericType.getClass());
//都是 com.caimuhao.examples.generic.wildchar.GenericType
System.out.println(stringGenericType.getClass().getName());
System.out.println(integerGenericType.getClass().getName());

3. 泛型类的静态上下文中类型变量失效

public class GenericType<T> {
    // 编译出错
    //静态域或方法里不能引用泛型类型变量
    private static T instance;
}

4. 不能创建参数化类型的数组

//编译报错
GenericType<Double>[] doubles = new GenericType<Double>[10];

5. 不能实例化类型变量

private T data;
public GenericType(){
    data = new T();
}

泛型类型的继承规则

首先我们有一个类和他的子类

public class Employee {}

public class Work extends Employee {}

有一个泛型类

public class Pair<T> {}

虽然 Work 继承 Employee,但是他们没有关系

//编译报错
Pair<Employee> employeePair = new Pair<Work>();

但是泛型类可以继承或扩展其他泛型类,比如 List 和 ArrayList

public class ExtentPair<T> extends Pair<T>{}
Pair<Employee> pair = new ExtendPair<>();

通配符类型

? extend X

表示传递给方法的参数,必须 X 的子类(包括 X 本身)

public static void print2(GenericType<? extends Fruit> f) {
    System.out.println(f.getData().getClass());
}

// 可以传递本身
GenericType<Fruit> a = new GenericType<>();
print2(a);
//可以传递子类
GenericType<Orange> b = new GenericType<>();
print2(b);

当泛型类 GenericType 中存在 set 方法时, set 方法是不允许调用的,会出现编译错误.

public void setData(T data) {
    this.data = data;
}
GenericType<? extends Fruit> c = new GenericType<>();
Fruit fruit = new Fruit();
//编译错误
//c.setData(fruit);
Apple apple = new Apple();
//编译错误
//c.setData(apple);

get 方法返回正确,会返回一个 Fruit 类型的值.

Fruit data = c.getData();

原因?

? extends X 表示类型的上界,类型参数是 X 的子类,那么可以肯定 get 方法返回的一定是 X(不管是 X 还是 X 的子类),编译器是可以确定的.但 set 方法只知道传入的是个 X,不知道具体是那个子类.

总结:

主要用于安全地访问数据.可以访问 X 及其子类,并不能传入 null .

? super X

表示传递给方法的参数,必须是 X 的超类(包括 X 本身)

public static void printSuper(GenericType<? super Apple> g) {
    System.out.println(g.getData().getClass());
}

GenericType<Fruit> fruitGenericType = new GenericType<>();
GenericType<Apple> appleGenericType = new GenericType<>();
GenericType<Hongfushi> hongfushiGenericType = new GenericType<>();
GenericType<Orange> orangeGenericType = new GenericType<>();

printSuper(fruitGenericType);
printSuper(appleGenericType);
//编译出错
//printSuper(hongfushiGenericType);
//printSuper(orangeGenericType);

当泛型类 GenericType ,提供 get 和 set 泛型参数方法,set 方法是可以调用,且只能传入 X 或 X 的子类.

private T data;

public T getData() {
    return data;
}

public void setData(T data) {
    this.data = data;
}

GenericType<? super Apple> x = new GenericType<>();
x.setData(new Apple());
x.setData(new Hongfushi());
//x.setData(new Fruit());
Object data = x.getData();

原因?

? super X 表示类型的下界,类型参数是 X 的超类(包括 X 本身),get方法返回的 一定是 X 的超类,但是不知道是那个超类,所以返回的 Object.对于 set 方法,编译器不知道确切类型,但是 X 和 X 的子类都可以安全的转型为 X.

总结

主要用于安全的写入数据,可以写入 X 及其子类型.

虚拟机是如何实现泛型的?

在 Java 版本早期是没有泛型的,只能通过 Object 是所有类的父类和类型强制转换两个特点来实现泛型化,之后为了兼容早期的版本,Java在实现泛型时,使用了与 C++不同的方式,只将泛型保留在源代码中,当编译器进行编译时,对泛型类型进行了擦除,在一些地方使用了类型强转.所以 Java 中的泛型技术实际上只是语法糖.

public static String method(List<String> stringList){
    return "OK";
}
    
public static Integer method(List<Integer> integerList){
    return 1;
}

上面两个方法是不能编译的,因为 List<String>List<Integer> 编译后都被擦除了,变成了一样的原生类型 List<E> ,擦除导致两种方法签名变成一样的.

由于Java泛型的引入,各种场景(虚拟机解析、反射等)下的方法调用都可能对原有的基础产生影响和新的需求,如在泛型类中如何获取传入的参数化类型等。因此,JCP组织对虚拟机规范做出了相应的修改,引入了诸如 SignatureLocalVariableTypeTable 等新的属性用于解决伴随泛型而来的参数类型的识别问题, Signature 是其中最重要的一项属性,它的作用就是存储一个方法在字节码层面的特征签名[3],这个属性中保存的参数类型并不是原生类型,而是包括了参数化类型的信息。修改后的虚拟机规范要求所有能识别49.0以上版本的 Class 文件的虚拟机都要能正确地识别 Signature 参数。

另外,从 Signature 属性的出现我们还可以得出结论,擦除法所谓的擦除,仅仅是对方法的 Code 属性中的字节码进行擦除,实际上元数据中还是保留了泛型信息,这也是我们能通过反射手段取得参数化类型的根本依据。

原文  http://caimuhao.com/2019/12/18/Java-Generic/
正文到此结束
Loading...