之所以会想来写泛型相关的内容,是因为看到这样的一段代码:
当时我的内心是这样的:
所以就赶紧去复习了下,记录下来。基础不扎实,源码看不懂啊。
Java 泛型(generics)是 JDK 5 中引入的一个新特性,泛型提供了编译时类型安全检测机制,该机制允许程序员在编译时检测到非法的类型。泛型的本质是参数化类型,也就是说所操作的数据类型被指定为一个参数,在Java集合框架里使用的非常广泛。
定义的重点是提供了编译时类型安全检测机制。比如有这样的一个泛型类:
public class Generics <T> { private T value; public T getValue() { return value; } public void setValue(T value) { this.value = value; } } 复制代码
然后写这样一个类:
public class Generics { private Object value; public Object getValue() { return value; } public void setValue(Object value) { this.value = value; } } 复制代码
它们同样都能存储所有值,但是泛型类有编译时类型安全检测机制:
一个类定义了一个或多个类型变量,那么就是泛型类。语法是在类名后用尖括号括起来,类型变量写在里面用逗号分开。然后就可以在方法的返回类型、参数和域、局部变量中使用类型变量了,但是不能在有 static
修饰符修饰的方法或域中使用。例子:
类定义参考上文例子 使用形式 Generics<String> generics = new Generics<String>(); 后面尖括号内的内容在Jdk7以后可以省略 Generics<String> generics = new Generics<>(); 复制代码
一个方法定义了一个或多个类型变量,那么就是泛型方法。语法是在方法修饰符后面、返回类型前面用尖括号括起来,类型变量写在里面用逗号分开。泛型方法可以定义在普通类和泛型类中,泛型方法可以被 static
修饰符修饰。 例子:
private <U> void out(U u) { System.out.println(u); } 调用形式, Test.<String>out("test"); 大部分情况下<String>都可以省略,编译器可以推断出来类型 Test.out("test"); 复制代码
有时候我们会有希望限定类型变量的情况,比如限定指定的类型变量需要实现 List
接口,这样我们就可以在代码对类型变量调用 List
接口里的方法,而不用担心会没有这个方法。
public class Generics <T extends List> { private T value; public T getValue() { return value; } public void setValue(T value) { this.value = value; } public void add(Object u) { value.add(u); } public static void main(String[] args) { Generics<List> generics = new Generics<>(); generics.setValue(new ArrayList<>()); generics.add("ss"); System.out.println(generics.getValue()); } } 复制代码
限定的语法是在类型变量的后面加 extends
关键字,然后加限定的类型,多个限定的类型要用 &
分隔。类型变量和限定的类型可以是类也可以是接口,因为Java中类只能继承一个类,所以限定的类型是类的话一定要在限定列表的第一个。
类型擦除是为了兼容而搞出来的,大意就是在虚拟机里是没有泛型类型,泛型只存在于编译期间。泛型类型变量会在编译后被擦除,用第一个限定类型替换(没有限定类型的用 Object
替换)。上文中的 Generics <T>
泛型类被擦除后会产生对应的一个原始类型:
public class Generics { private Object value; public Object getValue() { return value; } public void setValue(Object value) { this.value = value; } } 复制代码
之所以我们能设置和返回正确的类型是因为编译器自动插入了类型转换的指令。
public static void main(String[] args) { Generics<String> generics = new Generics<>(); generics.setValue("ss"); System.out.println(generics.getValue()); } javac Generics.java javap -c Generics 编译后的代码 public static void main(java.lang.String[]); Code: 0: new #3 // class generics/Generics 3: dup 4: invokespecial #4 // Method "<init>":()V 7: astore_1 8: aload_1 9: ldc #5 // String ss 11: invokevirtual #6 // Method setValue:(Ljava/lang/Object;)V 14: getstatic #7 // Field java/lang/System.out:Ljava/io/PrintStream; 17: aload_1 18: invokevirtual #8 // Method getValue:()Ljava/lang/Object; 21: checkcast #9 // class java/lang/String 24: invokevirtual #10 // Method java/io/PrintStream.println:(Ljava/lang/String;)V 27: return 复制代码
我们可以看到在21行插入了一条类型转换的指令。
类型擦除还带来了另一个问题,如果我们有一个类继承了泛型类并重写了父类的方法:
public class SubGenerics extends Generics<String> { @Override public void setValue(String value) { System.out.println(value); } public static void main(String[] args) { Generics<String> generics = new SubGenerics(); generics.setValue("ss"); } } 复制代码
因为类型擦除所以 SubGenerics
实际上有两个 setValue
方法, SubGenerics
自己的 setValue(String value)
方法和从 Generics
继承来的 setValue(Object value)
方法。例子中的 generics
引用的是 SubGenerics
对象,所以我们希望调用的是 SubGenerics.setValue
。为了保证正确的多态性,编译器在 SubGenerics
类中生成了一个 桥方法
:
public void setValue(Object value) { setValue((String) value); } 复制代码
我们可以编译验证下:
Compiled from "SubGenerics.java" public class generics.SubGenerics extends generics.Generics<java.lang.String> { public generics.SubGenerics(); Code: 0: aload_0 1: invokespecial #1 // Method generics/Generics."<init>":()V 4: return public void setValue(java.lang.String); Code: 0: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream; 3: aload_1 4: invokevirtual #3 // Method java/io/PrintStream.println:(Ljava/lang/String;)V 7: return public static void main(java.lang.String[]); Code: 0: new #4 // class generics/SubGenerics 3: dup 4: invokespecial #5 // Method "<init>":()V 7: astore_1 8: aload_1 9: ldc #6 // String ss 11: invokevirtual #7 // Method generics/Generics.setValue:(Ljava/lang/Object;)V 14: return public void setValue(java.lang.Object); Code: 0: aload_0 1: aload_1 2: checkcast #8 // class java/lang/String 5: invokevirtual #9 // Method setValue:(Ljava/lang/String;)V 8: return } 复制代码
引用《Java核心技术 卷一》
总之,需要记住有关 Java 泛型转换的事实: 1.虚拟机中没有泛型,只有普通的类和方法。 2.所有的类型参数都用它们的限定类型替换。 3.桥方法被合成来保持多态。 4.为保持类型安全性,必要时插人强制类型转换。
int,double
等。应该使用它们的包装类 Integer,Double
。 if (generics instanceof Generics<String>) // Error
Generics<String>[] generics = new Generics<String>[10]; // Error
new T(...) new T[...] 或 T.class
通配符类型和上文中的类型变量的限定有些类似,区别是通配符类型是运用在声明的时候而类型变量的限定是在定义的时候。比如通配符类型 Generics<? extends List>
代表任何泛型 Generics
类型的类型变量是 List
和 List
的子类。
Generics<? extends List> generics = new Generics<ArrayList>();
不过这样声明之后 Generics
的方法也发生了变化,变成了
这样就导致了不能调用 setValue
方法
而 getValue
方法是正常的
通配符限定还可以限定超类,比如通配符类型 Generics<? super ArrayList>
代表任何泛型 Generics
类型的类型变量是 ArrayList
和 ArrayList
的超类。
Generics<? super ArrayList> generics = new Generics<List>();
同样的, Generics
的方法也发生了变化,变成了
调用 getValue
方法只能赋值给 Object
变量
调用 setValue
方法只能传入 ArrayList
和 ArrayList
的子类,超类 List,Object
等都不行
虽然因为类型擦除,在虚拟机里是没有泛型的。不过被擦除的类还是保留了一些关于泛型的信息,可以使用反射相关的 Api
来获取。
类似地,看一下泛型方法
public static <T extends Comparable<? super T>> T min(T[] a)
这是擦除后
public static Comparable min(Coniparable[] a)
可以使用反射 API 来确定:
T
周一就建好的草稿,到了星期天才写好,还是删掉了一些小节情况下,怕是拖延症晚期了......不过也是因为泛型的内容够多,虽然日常业务里很少自己去写泛型相关的代码,但是在阅读类库源码时要是不懂泛型就寸步难行了,特别是集合相关的。这次的大部分内容都是《Java核心技术 卷一》里的,这可是本关于 Java
基础的好书。不过还是老规矩,光读可不行,还是要用自己的语言记录下来。众所周知,人类的本质是复读机,把好书里的内容重复一遍,就等于我也有责任了!