美文网首页Java 杂谈
你真的懂Java泛型吗?

你真的懂Java泛型吗?

作者: java高级编程中心 | 来源:发表于2019-05-10 22:23 被阅读15次

泛型实现参数化类型 的概念,使代码可以应用于多种类型,解除类或方法与所使用的类型之间的约束。在JDK 1.5开始引入了泛型,但Java实现泛型的方式与C++或C#差异很大。在平常写代码用到泛型时,仿佛一切都来得如此理所当然。但其实Java泛型还是有挺多tricky的东西的,编译器在背后为我们做了很多事。下面我们来看看有关Java泛型容易忽视的点。

泛型不支持协变

什么是协变?举个例子。

class Fruit{}

class Apple extends Fruit{}

Fruit[] fruit = new Apple[10]; // OK

子类数组可以赋给父类数组的引用。但泛型是不支持这种协变的。

ArrayList flist = new ArrayList(); // 无法通过编译

但我们可以使用通配符来解决

ArrayList flist = new ArrayList();// 使用通配符解决协变问题

通配符

上界通配符

List flist = Arrays.asList(new Apple());

Apple a = (Apple)flist.get(0); // No warning

flist.contains(new Apple()); // Argument is ‘Object’

flist.indexOf(new Apple()); // Argument is ‘Object’

//flist.add(new Apple());   无法编译

List 表示某种特定类型 ( Fruit 或者其子类 ) 的 List,但是编译器并不关心(不知道)这个实际的具体类型到底是什么。值得注意的是,这并不意味着这个List可以持有Fruit的任意类型!

由于List的具体类型是并不确定的,而且Java泛型是不支持协变的,因此带有泛型类型参数的方法都无法正常调用。比如 add(T item); ,即使是传Object也无法通过编译。

但对于返回类型是泛型的方法,比如 T get(int index); ,返回值类型与上界类型一样。如上面示例代码调用的 flist.get(0) 返回值是Fruit类型的。

下界通配符

static void add(List list) {

//        list.add(new Fruit()); // 无法编译

Object object = list.get(0);// pass

}

代码中的

List list 表明list持有的类型是Apple的父类类型,但与上界通配符类似,这并不意味list可以持有Apple任意的子类类型的对象,编译器并不知道list具体的类型是什么。因此, list.add(new Fruit()); 不能编译了。

无界通配符

List list 表示 list 是持有某种特定类型的 List,但是不知道具体是哪种类型。而单独的 List list ,也是没有传入泛型参数,表示这个 list 持有的元素的类型是 Object 。

所有泛型信息都被擦除了吗

所谓的擦除,仅仅是对方法的Code属性中的字节码(也是方法内的逻辑代码)进行擦除,实际上元数据(类和接口的声明,类字段的声明)中还是保留了泛型信息。

引用 R大 的话是:

位于声明一侧的,源码里写了什么到运行时能看到什么;

位于使用一侧的,源码里写什么到运行时都没了。

public class GenericClass {                // 1

private List list;                     // 2

private Map map;               // 3

public U genericMethod(Map m) { // 4

List list = new ArrayList<>(); // 5

return null;

}

}

上面的代码中,注释1到注释4的T和U是保留在Class文件当中的,源码是什么,那么通过反射获取得到的是什么。也是说,在运行时,是无法获取到具体的T和U是什么类型的。

但运行时,在方法内部的局部变量的泛型信息是被全部擦除的。如上的注释5中的list的具体类型是无法在运行时获取到的。

真的无法获取到泛型类型吗

当时头条的面试官问过我这个问题,我当时对泛型的认识比较浅薄,以为编译器会将所有的泛型信息擦除,那么运行时也无能获取到具体的泛型类型了。但其实并不是这样,如上面介绍到,JDK1.5之后,Class的格式有变化,编译器会将声明的类,接口,方法的泛型信息保留到字节码当中。那么通过反射,这些信息还是可以获取到的。但要获取到具体的泛型类型,一般也只能获取到继承父类所使用的泛型类型。

比如:

public class SubClass extends Base { }

那么Base所绑定的泛型类型可以被获取到的。对SubClass.class调用 getGenericSuperclass 可以获取到T所绑定的类型。

Type type = SubClass.class.getGenericSuperclass();

Type targ = ((ParameterizedType) type).getActualTypeArguments()[0];

System.out.println(type); // SubClass

System.out.println(targ); // class java.lang.String

具体的用法可以参考Gson和Guice的源码:

https://github.com/google/guice/blob/abc78c361d9018da211690b673accb580a52abf2/core/src/com/google/inject/TypeLiteral.java#L94

https://github.com/google/gson/blob/master/gson/src/main/java/com/google/gson/internal/%24Gson%24Types.java

桥方法

为了使Java的泛型方法生成的字节码与1.5以前的字节码相兼容,由编译期自己生成的方法。顾名思义,桥方法是一座桥,沟通着泛型与多态。

可以通过 Method.isBridge() 方法来判断一个方法是否是桥接方法,在字节码中桥接方法会被标记为 ACC_BRIDGE 和 ACC_SYNTHETIC 。

public class Fruit {

T value;

public T getValue() {

return value;

}

}

public class Apple extends Fruit {

@Override

public String getValue() {

return "foo was call";

}

}

反编译生成的字节码:

public class Apple extends Fruit {

public Apple();

Code:

0: aload_0

1: invokespecial #1                  // Method Fruit."":()V

4: return

public java.lang.String getValue();

Code:

0: ldc           #2                  // String calling

2: areturn

public java.lang.Object getValue();

Code:

0: aload_0

1: invokevirtual #3                  // Method getValue:()Ljava/lang/String;

4: areturn

}

编译器为我们自动生成了有一个桥方法,这个桥方法返回类型为Object,内部调用了我们自定义的另一个getValue方法。

在Java代码中,方法的特征签名只包括方法名称,参数顺序和参数类型,而字节码中的特征签名还包括方法返回值和受查异常表。因此,桥方法 public Object getValue() 与 public String getValue() 是可以被JVM区分而在同一个Class文件中共存的。

由于编译期泛型擦除机制,在父类中带泛型参数的方法会被替换成Object类型。要让子类重写父类带泛型参数的方法,需要通过桥方法直接复写父类的方法,然后桥方法再调用子类自定义的方法,以上面作为例子,子类Apple中的桥方法 public Object getValue() 直接override父类Fruit的 public Object getValue() ,然后桥方法内部再调用子类Apple的 public String getValue() 。因此,Java利用桥方法在保证多态机制不被破坏情况下实现了泛型。

其实做为一个开发者,有一个学习的氛围跟一个交流圈子特别重要这里我推荐一个Java交流群664389243,不管你是小白还是大牛欢迎入驻,大家一起交流成长。

相关文章

  • 你真的懂Java泛型吗?

    泛型实现参数化类型 的概念,使代码可以应用于多种类型,解除类或方法与所使用的类型之间的约束。在JDK 1.5开始引...

  • Java 泛型与通配符

    参考地址:《Java 泛型,你了解类型擦除吗?》 《Java中的逆变与协变》 《java 泛型中 T、E .....

  • Java泛型教程

    Java泛型教程导航 Java 泛型概述 Java泛型环境设置 Java泛型通用类 Java泛型类型参数命名约定 ...

  • 详解Java泛型之4——一个例子理解泛型带来的好处

    前面我介绍了关于泛型、通配符以及泛型擦除的相关知识点,大家可以参考以下文章: 详解Java泛型之1——入门泛型必懂...

  • 详解Java泛型之3——十分钟理解泛型擦除

    前面我们介绍了泛型以及通配符的基础概念,可以参考文章: 详解Java泛型之1——入门泛型必懂的知识点[https:...

  • 第二十八课:泛型

    泛型出现之前 泛型出现之后 Java深度历险(五)——Java泛型

  • Kotlin 泛型

    说起 kotlin 的泛型,就离不开 java 的泛型,首先来看下 java 的泛型,当然比较熟悉 java 泛型...

  • java泛型中类型擦除的一些思考

    java泛型 java泛型介绍 java泛型的参数只可以代表类,不能代表个别对象。由于java泛型的类型参数之实际...

  • Java泛型

    参考:Java知识点总结(Java泛型) 自定义泛型类 自定义泛型接口 非泛型类中定义泛型方法 继承泛型类 通配符...

  • 秒懂Java泛型

    版权申明】非商业目的附文章链接可自由转载博文地址:https://www.jianshu.com/p/fb6f33...

网友评论

    本文标题:你真的懂Java泛型吗?

    本文链接:https://www.haomeiwen.com/subject/arozoqtx.html