泛型,即“参数化类型”。一提到参数,最熟悉的就是定义方法时有形参,然后调用此方法时传递实参。那么参数化类型怎么理解呢?顾名思义,就是将类型由原来的具体的类型参数化,类似于方法中的变量参数,此时类型也定义成参数形式(可以称之为类型形参),然后在使用/调用时传入具体的类型(类型实参)。
泛型的本质是为了参数化类型(在不创建新的类型的情况下,通过泛型指定的不同类型来控制形参具体限制的类型)。也就是说在泛型使用过程中,操作的数据类型被指定为一个参数,这种参数类型可以用在类、接口和方法中,分别被称为泛型类、泛型接口、泛型方法。
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());
public class GenericType<T> { // 编译出错 //静态域或方法里不能引用泛型类型变量 private static T instance; }
//编译报错 GenericType<Double>[] doubles = new GenericType<Double>[10];
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<>();
表示传递给方法的参数,必须 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 .
表示传递给方法的参数,必须是 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组织对虚拟机规范做出了相应的修改,引入了诸如 Signature
、 LocalVariableTypeTable
等新的属性用于解决伴随泛型而来的参数类型的识别问题, Signature
是其中最重要的一项属性,它的作用就是存储一个方法在字节码层面的特征签名[3],这个属性中保存的参数类型并不是原生类型,而是包括了参数化类型的信息。修改后的虚拟机规范要求所有能识别49.0以上版本的 Class
文件的虚拟机都要能正确地识别 Signature
参数。
另外,从 Signature
属性的出现我们还可以得出结论,擦除法所谓的擦除,仅仅是对方法的 Code
属性中的字节码进行擦除,实际上元数据中还是保留了泛型信息,这也是我们能通过反射手段取得参数化类型的根本依据。