你真的懂Java泛型吗

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

泛型不支持协变

什么是协变?举个例子。

class Fruit{}
class Apple extends Fruit{}
Fruit[] fruit = new Apple[10]; // OK

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

ArrayList<Fruit> flist = new ArrayList<Apple>(); // 无法通过编译

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

ArrayList<? extends Fruit> flist = new ArrayList<Apple>();// 使用通配符解决协变问题

通配符

上界通配符

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

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

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

下界通配符

    static void add(List<? super Apple> list) {
//        list.add(new Fruit()); // 无法编译
        Object object = list.get(0);// pass
    }

代码中的

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

无界通配符

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

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

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

引用R大的话就是:

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

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

public class GenericClass<T> {                // 1
    private List<T> list;                     // 2
    private Map<String, T> map;               // 3
    public <U> U genericMethod(Map<T, U> m) { // 4
        List<String> list = new ArrayList<>(); // 5
        return null;
    }
}  

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

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

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

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

比如:

public class SubClass extends Base<String> { }

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

        Type type = SubClass.class.getGenericSuperclass();
        Type targ = ((ParameterizedType) type).getActualTypeArguments()[0];
        System.out.println(type); // SubClass<java.lang.String>
        System.out.println(targ); // class java.lang.String

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

桥方法

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

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

public class Fruit<T> {
    T value;
    public T getValue() {
        return value;
    }
}
public class Apple extends Fruit<String> {
    @Override
    public String getValue() {
        return "foo was call";
    }
}

反编译生成的字节码:

public class Apple extends Fruit<java.lang.String> {
  public Apple();
    Code:
       0: aload_0
       1: invokespecial #1                  // Method Fruit."<init>":()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利用桥方法在保证多态机制不被破坏情况下实现了泛型。

参考

时间: 2024-10-14 17:28:20

你真的懂Java泛型吗的相关文章

你真的懂JAVA吗

宏观方面 一.JAVA.要想成为JAVA(高级)工程师肯定要学习JAVA.一般的程序员或许只需知道一些JAVA的语法结构就可以应付了.但要成为JAVA(高级) 工程师,您要对JAVA做比较深入的研究.您应该多研究一下JDBC.IO包.Util包.Text包.JMS.EJB.RMI.线程.如果可能,希望您 对JAVA的所有包都浏览一下,知道大概的API,这样您就发现其实您想实现的很多功能,通过JAVA的API都可以实现了,就不必自己费太多的脑经 了. 二.设计模式.其实写代码是很容易的事情,我相信

1月21日 - (转)Java 泛型

java泛型 什么是泛型? 泛型(Generic type 或者 generics)是对 Java 语言的类型系统的一种扩展,以支持创建可以按类型进行参数化的类.可以把类型参数看作是使用参数化类型时指定的类型的一个占位符,就像方法的形式参数是运行时传递的值的占位符一样. 可以在集合框架(Collection framework)中看到泛型的动机.例如,Map 类允许您向一个 Map 添加任意类的对象,即使最常见的情况是在给定映射(map)中保存某个特定类型(比如 String)的对象. 因为 M

java泛型的讲解

java泛型 什么是泛型? 泛型(Generic type 或者 generics)是对 Java 语言的类型系统的一种扩展,以支持创建可以按类型进行参数化的类.可以把类型参数看作是使用参数化类型时指定的类型的一个占位符,就像方法的形式参数是运行时传递的值的占位符一样. 可以在集合框架(Collection framework)中看到泛型的动机.例如,Map 类允许您向一个 Map 添加任意类的对象,即使最常见的情况是在给定映射(map)中保存某个特定类型(比如 String)的对象. 因为 M

java泛型(二)、泛型的内部原理:类型擦除以及类型擦除带来的问题

java泛型(二).泛型的内部原理:类型擦除以及类型擦除带来的问题 参考:java核心技术 一.Java泛型的实现方法:类型擦除 前面已经说了,Java的泛型是伪泛型.为什么说Java的泛型是伪泛型呢?因为,在编译期间,所有的泛型信息都会被擦除掉.正确理解泛型概念的首要前提是理解类型擦出(type erasure). Java中的泛型基本上都是在编译器这个层次来实现的.在生成的Java字节码中是不包含泛型中的类型信息的.使用泛型的时候加上的类型参数,会在编译器在编译的时候去掉.这个过程就称为类型

[置顶]云计算乱局:你真的懂,什么叫做云吗?(二)

上一章讲了什么是云,把云的概念给搭建简单的做了分析.这一章我把国内.国外的PaaS平台,即应用托管平台进行了测评对比,包括:国外的AWS.GAE和Windows Azure和国内的京东云擎JAE.百度BAE.新浪SAE.以及阿里云.IBM也宣布了落地国内的计划,IBM的公有云有好几套方案,一个是现有IBM Smarter Cloud的公有云方案,由TSAM和Websphere Pure application这种厚重的企业软件构成,最终在国内落地哪个还不得而知,这里暂不做比较. 比较可以看出,很

你真的懂 ajax 吗?

前言 总括: 本文讲解了ajax的历史,工作原理以及优缺点,对XMLHttpRequest对象进行了详细的讲解,并使用原生js实现了一个ajax对象以方便日常开始使用. damonare的ajax库:damonare的ajax库 原文博客地址:你真的懂ajax吗? 知乎专栏&&简书专题:前端进击者(知乎)&&前端进击者(简书) 博主博客地址:Damonare的个人博客 古之立大事者,不惟有超世之才,亦必有坚忍不拔之志. 正文 相信每个前端程序员日常工作中都避免不了的工作就是

【转】was mutated while being enumerated 你是不是以为你真的懂For...in... ??

原文网址:http://www.jianshu.com/p/ad80d9443a92 支持原创,如需转载, 请注明出处你是不是以为你真的懂For...in... ??哈哈哈哈, 我也碰到了这个报错 .究其原因, 顾名思义 "在枚举的时候发生了变化"for...in...利用了快速枚举NSFastEnumerate当我们想要改变数组变量中的数据或者删除数组中的数据的时候,不能用for...in... 应该是Objective-C中的foreach循环与Java中的相似,在内部是用iter

java泛型好处及案例

       Java 泛型是java SE 1.5的新特性,泛型的本质是参数化类型,也就是说所操作的数据类型被指定为一个参数.这种参数类型可以用在类.接口和方法的创建中,分别称为泛型类.泛型接口.泛型方法. 泛型(Generic type 或者 generics)是对 Java 语言的类型系统的一种扩展,以支持创建可以按类型进行参数化的类.可以把类型参数看作是使用参数化类型时指定的类型的一个占位符,就像方法的形式参数是运行时传递的值的占位符一样.        可以在集合框架(Collecti

一个小栗子聊聊JAVA泛型基础

背景 周五本该是愉快的,可是今天花了一个早上查问题,为什么要花一个早上?我把原因总结为两点: 日志信息严重丢失,茫茫代码毫无头绪. 对泛型的认识不够,导致代码出现了BUG. 第一个原因可以通过以后编码谨慎的打日志来解决,我们今天主要来一起回顾下JAVA泛型基础. 一个小栗子 先看下面一个例子,test1实例化一个List容器的时候没有指定泛型参数,那么我们可以往这个容器里面放入任何类型的对象,这样是不是很爽?但是当我们从容器中取出容器中的对象的时候我们必须小心翼翼,因为容器中的对象具有运行时的类