目录
Java 泛型(generics)是 Jdk 5 中引入的一个新特性, 泛型提供了编译时类型安全检测机制, 该机制允许程序员在编译时检测到非法的类型。
比如 ArrayList这行代码就指明了该 ArrayList 对象只能 存储String类型,如果传入其他类型的对象就会报错。
让我们时光回退到Jdk5的版本,那时ArrayList内部其实就是一个Object[] 数组,配合存储一个当前分配的长度,就可以充当“可变数组”:
- public class ArrayList {
- private Object[] array;
- private int size;
- public void add(Object e) {...}
- public void remove(int index) {...}
- public Object get(int index) {...}
- }
我们来举个简单的例子,
- ArrayList list = new ArrayList();
- list.add("test");
- list.add(666);
我们本意是用ArrayList来装String类型的值,但是突然混进去了Integer类型的值,由于ArrayList底层是Object数组,可以存储任意的对象,所以这个时候是没啥问题的,但我们不能只存不用啊,我们需要把值给拿出来使用,这个时候问题来了:
- for(Object item: list) {
- System.out.println((String)item);
- }
结果:
Exception in thread "main" java.lang.ClassCastException: java.lang.Integer cannot be cast to java.lang.String
由于我们需要String类型的值,我们需要把ArrayList的Object值强制转型,但是之前混进去了Integer ,虽然编译阶段通过了,但程序的运行结果会以崩溃结束,报ClassCastException异常
为了解决这个问题,在Jdk 5版本中就引入了泛型的概念,而引入泛型的很大一部分原因就是为了解决我们上述的问题,允许程序员在编译时检测到非法的类型。不是同类型的就不允许在一块存放,这样也避免了ClassCastException异常的出现,而且因为都是同一类型,也就没必要做强制类型转换了。
我们可以把ArrayList 变量参数化:
- public class ArrayList
{ - private T[] array;//我们 假设 ArrayList
内部会有个T[] array - private int size;
- public void add(T e) {...}
- public void remove(int index) {...}
- public T get(int index) {...}
- }
其中T叫类型参数 ,T可以是任何class类型,现在ArrayList我们可以如下使用:
- // 存储String的ArrayList
- ArrayList
list = new ArrayList(); - list.add(666);//编译器会在编译阶段发现问题,从而提醒开发者
泛型其本质是参数化类型,也就是说数据类型 作为 参数,解决不确定具体对象类型的问题。
泛型一般有三种使用方式,分别为:泛型类、泛型接口、泛型方法,我们简单介绍一下泛型的使用
- //此处T可以随便写为任意标识,常见的如T、E、K、V等形式的参数常用于表示泛型
- //在实例化泛型类时,必须指定T的具体类型
- public class Generic
{ -
- private T key;
-
- public Generic(T key) {
- this.key = key;
- }
-
- public T getKey(){
- return key;
- }
- }
如何实例化泛型类:
- Generic
genericInteger = new Generic(666); - Generic
genericStr = new Generic("hello");
- //定义一个泛型接口
- public interface Generator
{ - public T method();
- }
-
- //实现泛型接口,不指定类型
- class GeneratorImpl
implements Generator{ - @Override
- public T method() {
- return null;
- }
- }
-
- //实现泛型接口,指定类型
- class GeneratorImpl
implements Generator{ - @Override
- public String method() {
- return "hello";
- }
- }
-
泛型方法
- public class GenericMethods {
- public
void f(T x){ - System.out.println(x.getClass().getName());
- }
- public static void main(String[] args) {
- GenericMethods gm = new GenericMethods();
- gm.f("啦啦啦");
- gm.f(666);
- }
- }
结果:
java.lang.String
java.lang.Integer
通过上文我们知道,为了让ArrayList存取各种数据类型的值,我们需要把ArrayList模板化,将变量的数据类型 给抽象出来,作为类型参数
- public class ArrayList
{ - private T[] array;// 我们以为ArrayList
内部会有个T[] array - private int size;
- public void add(T e) {...}
- public void remove(int index) {...}
- public T get(int index) {...}
- }
但当我们查看Jdk8 的ArrayList源码,底层数组还是Object数组:transient Object[] elementData;
那ArrayList为什么还能进行类型约束和自动类型转换呢?
我们再看一个经典的例子:
- public class genericTest {
- public static void main(String [] args) {
- String str="";
- Integer param =null;
-
- ArrayList
l1 = new ArrayList(); - l1.add("aaa");
- str = l1.get(0);
-
- ArrayList
l2 = new ArrayList(); - l2.add(666);
- param = l2.get(0);
-
-
- System.out.println(l1.getClass() == l2.getClass());
-
- }
- }
结果竟然是true,ArrayList.class 和 ArrayList.class 应该是不同的类型。通过getClass()方法获取他们的类的信息,竟然是一样的。我们来查看这个文件的class文件:
- public class genericTest {
- public genericTest() {
- }
-
- public static void main(String[] var0) {
- String var1 = "";
- Integer var2 = null;
- ArrayList var3 = new ArrayList();//泛型被擦擦了
- var3.add("aaa");
- var1 = (String)var3.get(0);
- ArrayList var4 = new ArrayList();//泛型被擦擦了
- var4.add(666);
- var2 = (Integer)var4.get(0);
- System.out.println(var3.getClass() == var4.getClass());
- }
- }
-
我们在对其反汇编一下:
- $ javap -c genericTest
-
- ▒▒▒▒: ▒▒▒▒▒▒▒ļ▒genericTest▒▒▒▒com.zj.demotest.test5.genericTest
- Compiled from "genericTest.java"
- public class com.zj.demotest.test5.genericTest {
- public com.zj.demotest.test5.genericTest();
- Code:
- 0: aload_0
- 1: invokespecial #1 // Method java/lang/Object."
":()V - 4: return
-
- public static void main(java.lang.String[]);
- Code:
- 0: ldc #2 // String
- 2: astore_1
- 3: aconst_null
- 4: astore_2
- 5: new #3 // class java/util/ArrayList
- 8: dup
- 9: invokespecial #4 // Method java/util/ArrayList."
":()V - 12: astore_3
- 13: aload_3
- 14: ldc #5 // String aaa
- 16: invokevirtual #6 // Method java/util/ArrayList.add:(Ljava/lang/Object;)Z
- 19: pop
- 20: aload_3
- 21: iconst_0
- 22: invokevirtual #7 // Method java/util/ArrayList.get:(I)Ljava/lang/Object;
- 25: checkcast #8 // class java/lang/String
- 28: astore_1
- 29: new #3 // class java/util/ArrayList
- 32: dup
- 33: invokespecial #4 // Method java/util/ArrayList."
":()V - 36: astore 4
- 38: aload 4
- 40: sipush 666
- 43: invokestatic #9 // Method java/lang/Integer.valueOf:(I)Ljava/lang/Integer;
- 46: invokevirtual #6 // Method java/util/ArrayList.add:(Ljava/lang/Object;)Z
- 49: pop
- 50: aload 4
- 52: iconst_0
- 53: invokevirtual #7 // Method java/util/ArrayList.get:(I)Ljava/lang/Object;
- 56: checkcast #10 // class java/lang/Integer
- 59: astore_2
- 60: getstatic #11 // Field java/lang/System.out:Ljava/io/PrintStream;
- 63: aload_3
- 64: invokevirtual #12 // Method java/lang/Object.getClass:()Ljava/lang/Class;
- 67: aload 4
- 69: invokevirtual #12 // Method java/lang/Object.getClass:()Ljava/lang/Class;
- 72: if_acmpne 79
- 75: iconst_1
- 76: goto 80
- 79: iconst_0
- 80: invokevirtual #13 // Method java/io/PrintStream.println:(Z)V
- 83: return
- }
-
类型转换检查 ,在结合class文件var1 = (String)var3.get(0);``var2 = (Integer)var4.get(0);我们知晓编译器自动帮我们强制类型转换了,我们无需手动类型转换
经过上面的种种现象,我们可以发现,在类加载的编译阶段,泛型类型String和Integer都被擦除掉了,只剩下原始类型,这样他们类的信息都是Object,这样自然而然就相等了。这种机制就叫泛型擦除。
我们需要了解一下类加载生命周期:
泛型是和编译器的约定,在编译期对代码进行检查的,由编译器负责解析,JVM并无识别的能力,一个类继承泛型后,当变量存入这个类的时候,编译器会对其进行类型安全检测,当从中取出数据时,编译器会根据与泛型的约定,会自动进行类型转换,无需我们手动强制类型转换。
泛型类型参数化,并不意味这其对象类型是不确定的,相反它的对象类型 对于JVM来说,都是确定的,是Object或Object[]数组
来看一个经典的例子,我们想要实现一个ArrayList对象能够储存所有的泛型:
ArrayList
但可以的是编译器提示报错:
明明 String是Object类的子类,我们可以发现,泛型不存在继承、多态关系,泛型左右两边要一样
别担心,JDK提供了通配符?来应对这种场景,我们可以这样:
- ArrayList> list = new ArrayList
(); - list = new ArrayList
();
通配符>表示可以接收任意类型,此处?是类型实参,而不是类型形参。我们可以把它看做是String、Integer等所有类型的"父类"。是一种真实的类型。
通配符还有:
?是开放限度最大的,可指向任意类型,但在对于其的存取上也是限制最大的:
- //泛型的上限只能是该类型的类型及其子类,其中Number是Integer、Long、Float的父类
- ArrayList extends Number> list = new ArrayList
(); - ArrayList extends Number> list2 = new ArrayList
(); - ArrayList extends Number> list3 = new ArrayList
(); -
- list.add(1);//报错,extends不允许存入
-
- ArrayList
longList = new ArrayList<>(); - longList.add(1L);
- list = longList;//由于extends不允许存入,list只能重新指向longList
-
- Number number = list.get(0); // extends 取出来的元素(Integer,Long,Float)都可以转Number
extends指向性被砍了一半,只能指向子类型和父类型,但方法使用上又适当放开了:
ArrayList extends Number> list可以接收Integer、Long、Float,但是泛型本质是保证两边类型确定,这样的话在程序运行期间,再存入数据,编译器可无法知晓数据的类型,所以只能禁止了。ArrayList extends Number> list可以重新指向longList来变向地"存储"值,那是因为ArrayList longList = new ArrayList<>(); 这边的泛型已经约束两边的类型了,编译器知晓longList储存的数据都是Long类型
- //泛型的下限只能是该类型的类型及其父类,其中Number是Integer、Long、Float的父类
- ArrayList super Integer> list = new ArrayList
(); - ArrayList super Integer> list2 = new ArrayList
(); - ArrayList super Integer> list3 = new ArrayList
();//报错 - ArrayList super Integer> list4 = new ArrayList
();//报错 -
- list2.add(123);//super可以存入,只能存Integer及其子类型元素
- Object aa = list2.get(0);//super可以取出,类型只能是Object
super允许存入编辑类型及其子类型元素,但取出元素只能为Object类型
泛型通配符的出现,是为了获得最大限度的灵活性。如果要用到通配符,需要结合业务考虑,《Effective Java》提出了:PECS(Producer Extends Consumer Super)
? 表示不确定的 java 类型,一般用于只接收任意类型,而不对其处理的情况Java 编译器通过如下方式实现擦除:
当类定义中的类型参数没有任何限制时,在类型擦除中直接被替换为Object,即形如和>的类型参数都被替换为Object
当类定义中的类型参数存在限制(上下界)时,在类型擦除中替换为类型参数的上界或者下界,
形如
和 extends Number>的类型参数被替换为Number, super Number>被替换为Object
擦除方法定义中的类型参数原则和擦除类定义中的类型参数是一样的,额外补充 擦除方法定义中的有限制类型参数的例子
桥接方法和泛型的多态
- public class A
{ - public T get(T a){
- //进行一些操作
- return a;
- }
- }
- public class B extends A
{ - @override
- public String get(String a){
- //进行一些操作
- return a;
- }
- }
由于类型擦出机制的存在,按理说编译后的文件在翻译为java应如下所示:
- public class A{
- public Object get(Object a){
- //进行一些操作
- return a;
- }
- }
- public class B extends A{
- @override
- public String get(String a){
- //进行一些操作
- return a;
- }
- }
但是,我们可以发现 @override意味着B对父类A中的get方法进行了重写,但是依上面的程序来看,只是重载,依然可以执行父类的方法,这和期望是不附的,也不符合java继承、多态的特性。
- 重写是子类对父类的允许访问的方法的实现过程进行重新编写, 返回值和形参都不能改变。即外壳不变,核心重写!
- 重载(overloading) 是在一个类里面,方法名字相同,而参数不同。返回类型可以相同也可以不同。
为了解决这个问题,java在编译期间加入了桥接方法。编译后再翻译为java原文件其实是:
- public class A{
- public Object get(Object a){
- //进行一些操作
- return a;
- }
- }
- public class B extends A{
- @override
- public String get(String a){
- //进行一些操作
- return a;
- }
- //桥接方法!!!
- public Object get(Object a){
- return get((String)a)
- }
- }
桥接方法重写了父类相同的方法,并且桥接方法中,最终调用了期望的重写方法,并且桥接方法在调用目的方法时,参数被强制转换为指定的泛型类型。桥接方法搭起了父类和子类的桥梁。
桥接方法是伴随泛型方法而生的,在继承关系中,如果某个子类覆盖了泛型方法,则编译器会在该子类自动生成桥接方法。所以我们实际使用泛型的过程中,无需担心桥接方法。
不能用类型参数代替基本类型(byte 、short 、int 、long、float 、 double、char、boolean)
比如, 没有 Pair。 其原因是泛型擦除,擦除之后只有原始类型Object, 而 Object 无法存储 double等基本类型的值。
但Java同时有自动拆装箱特性,可以将基本类型装箱成包装类型,这样就使用泛型了,通过中转,即可在功能上实现“用基本类型实例化类型化参数”。
| 数据类型 | 封装类 |
|---|---|
| byte | Byte |
| short | Short |
| int | Integer |
| long | Long |
| float | Float |
| double | Double |
| char | Character |
| boolean | Boolean |
- List
[] l1 = new ArrayList[10];// Error - List
[] l2 = new ArrayList[10];// Error
上文我们知晓ArrayList,底层仍旧采用Object[],Integer,String类型信息都被擦除
借助无限定通配符 ?,可以创建泛型数组,但是涉及的操作都基本上与类型无关
List>[] l1 = new ArrayList>[10];
如果想对数组进行复制操作的话,可以通过Arrays.copyOfRange()方法
- public class TestArray {
-
- public static void main(String[] args) {
- Integer[] array = new Integer[]{2, 3, 1};
- Integer[] arrNew = copy(array);
- }
-
- private static
E[] copy(E[] array) { - return Arrays.copyOfRange(array, 0, array.length);
- }
-
- }
由于我们知晓java是通过泛型擦除来实现泛型的,JVM只能识别原始类型Object,所以我们只需骗过编译器的校验即可,反射是程序运行时发生的,我们可以借助反射来波骚操作
- List
l1 = new ArrayList<>(); - l1.add(111);
- //l1.add("骚气的我"); // 泛型会报错
- try {
- Method method = l1.getClass().getDeclaredMethod("add",Object.class);
- method.invoke(l1,"骚气的我 又出现了");
- } catch (NoSuchMethodException e) {
- e.printStackTrace();
- } catch (IllegalAccessException e) {
- e.printStackTrace();
- } catch (InvocationTargetException e) {
- e.printStackTrace();
- }
- for ( Object o: l1){
- System.out.println(o);
- }
结果:
111
骚气的我 又出现了

如果你了解其他语言(例如 C++ )的参数化机制,你会发现,Java 泛型并不能满足所有的预期。由于泛型出来前,java已经有了很多项目了,为了兼容老版本,采用了泛型擦除来“实现泛型”,这会遇到很多意料之外的麻烦,但这并不是说 Java 泛型毫无用处,它大多数情况能够让代码更加优雅,后面有机会我们会继续深入聊聊泛型擦除带来的麻烦及其历史渊源。