Java泛型的实现:原理与问题

很久没写博客了,因为项目和一些个人原因。最近复习找工作,看书+回想项目后有一些心得,加上博客停更这么长时间以来的积累,很是有些东西可写。从今儿开始,慢慢把之前积累的东西补上来,方便以后查漏补缺。

先从最近的开始。昨天看到Java泛型相关的内容,有些疑惑,查资料之后发现这部分很有些有意思的东西,比如类型擦除带来的重写问题等等,一并记录在这篇文章里。

1. 泛型定义

看了很多泛型的解释百度百科解释1解释2,都不是我想要的“以用为本”答案(没讲明白泛型的作用或者说设计目的),这里我自己总结一下:

泛型编程是一种通过参数化的方式将数据处理与数据类型解耦的技术,通过对数据类型施加约束(比如Java中的有界类型)来保证数据处理的正确性,又称参数类型或参数多态性。

泛型最著名的应用就是容器,C++的STL、Java的Collection Framework。

2. 泛型的实现方式

不同的语言在实现泛型时采用的方式不同,C++的模板会在编译时根据参数类型的不同生成不同的代码,而Java的泛型是一种违反型,编译为字节码时参数类型会在代码中被擦除,单独记录在Class文件的attributes域内,而在使用泛型处做类型检查与类型转换。

假设参数类型的占位符为T,擦除规则如下:

  • <T>擦除后变为Obecjt
  • <? extends A>擦除后变为A
  • <? super A>擦除后变为Object

上述擦除规则叫做保留上界

3. 擦除带来的问题

对于<? extends A>和<? super A>的擦除,因为保留上界,所以擦除后并没有破坏里氏替换原则

设有类Super与Sub:

class Super{}
class Sub extends Super{}

对于有界类型的协变性与逆变性:

List<? extends Super> list = new ArrayList<Sub>();  //协变
List<? super Sub> list2 = new ArrayList<Super>();   //逆变

类型擦除后,等价于:

List<Super> list = new ArrayList<Sub>();
List<Object> list2 = new ArrayList<Super>(); 

可以看出,参数类型的擦除并没有破坏里氏替换原则,这也是保留上界的原因。

感谢 Java中的逆变与协变这篇博文的作者,让我很好理解了协变与逆变、PECS规则。有机会我会再写一篇自己的总结。

对于<T>的擦除,根据T在类中出现位置的不同,分以下5种情况讨论:

  1. T是成员变量的类型
  2. T是泛型变量(无论成员变量还是局部变量)的类型参数,常见如Class<T>,List<T>。
  3. T是方法抛出的Exception(要求<T extends Exception>)
  4. T是方法的返回值
  5. T是方法的参数

情况1的擦除不会有任何影响,因为编译器会在泛型被调用的地方加上类型转换;

情况2的擦除也不会有问题,这个问题有点像“要实现不可变类,就要保证成员变量中引用指向的类型也是不可变的”,是个递归定义;

情况3的擦除,我认为讨论这种情况意义不大。想在方法中抛出T,那得先实例化T,而如何通过泛型进行实例化,原谅我不知道怎么能做到(有人说反射能做到,怪我反射学的不好……);假设现在得到并可以抛出泛型T的实例,来看一下会出现什么情况。

设有类Super与Sub:

class Super<T extends SQLException>{
    public void test() throws T{}    //别怀疑,这段代码是可以编译通过的......
}

class Sub extends Super<BatchUpdateException>{
    @Override
    public void test() throws BatchUpdateException{} //这里必须与参数类型保持一致,否则编译不通过。
}

Super的参数类型被擦除之后,变成了:

class Super<SQLException>{
    public void test() throws SQLException{}
}

与Sub类对比后,发现并没有违背Java中方法重写(Override)的规则。

Java中Override的规则有一个好记的口诀,叫“两同两小一大”(其实叫“两同两窄一宽”我觉得更好),说的是子类方法与父类方法的异同:

- 子类方法的方法名&参数列表与父类方法的相同。

- 子类方法的返回类型是父类方法返回类型的子类(协变返回类型,范围更窄);

- 子类方法抛出的异常少于父类方法抛出的异常(范围更窄);

- 子类方法的访问控制权限大于父类方法(访问范围更宽)。

这个规则可以很方便的用里氏替换原则反推出来。显然这里类型擦除后并没有违反重写时对异常的规定。

情况4是讲T作为返回类型时的被擦除,因为协变返回类型的存在,它同样不会有问题。

设有两个类Super与Sub:

class Super<T>{
    T test(){}
}
class Sub extends Super<String>{
    @Override
    protected String test(){}  //这里抖个包袱:protected拥有比package更高的访问权限,可以被同一包内的类访问
}

类型擦除后,Super变为:

class Super{
    Object test(){}
}

与Sub类对比后,能看到它并没有违反“两同两小一大”口诀,所以也不会有问题。

这个叫做协变返回类型,即子类方法的返回值是父类方法的子类(绕口令一样…)。JVM在实现它时用到了桥方法(ACC_BRIDGE),后面会有介绍。

情况1,2,3,4都做了分析,发现在现有的语言规范下,类型擦除并不会带来影响,而情况5会有些不一样。

设有类Super与Sub

class Super<T>{
    public void test(T arg){}
}
class Sub extends Super<String>{
    @Override
    public void test(String str){}
}

再来看Super的参数类型被擦除后:

class Super{
    public void test(Object arg){}
}
class Sub extends Super{
    @Override
    public void test(String str){}
}

这次我连Sub一并写出来了,是为了方便对比:上述代码编译时不通过的,因为子类重写方法的参数列表与父类的不一致了!子类是String而父类是Object

但是,我们按照类型擦除前的写法来写,编译器并没有报错,执行结果也证明我们真的重写了方法,那么Java(准确的说编译器)是怎么做到的呢?请看下面一张图,是Sub类的字节码:

注意到,Sub类有两个test方法,一个的参数类型是String,这是Sub中重写的方法;另外一个的参数类型是Object,并且flags中多了ACC_BRIDGEACC_SYNTHETIC两个标签。查看深入理解Java虚拟机 6.3.6节 表6-12ACC_BRIDGE表示这是由编译器生成的桥方法,ACC_SYNTHETIC表示这个方法是由编译器自动生成的。注意看这个方法都做了什么:

checkcast #19
invokevirtual #21

Sub的常量池见下表:

可以看到,桥方法首先判断了ObjectString的类型转换是否正确。invokevirtual是调用对象方法的指令,根据对象的实际类型进行分派。从常量池中可以看出,桥方法调用了Sub类中原有的重写方法。这样就保证了情况5下的类型擦除不会破坏方法重写的语义。

4. 协变返回类型的桥方法

协变返回类型也是使用桥方法来实现的,下图是字节码:

有趣的是:一个Class文件中出现了两个签名一样,只是返回值不一样的方法。如果是在Java源代码中出现这种情况,编译是不会通过的。为什么编译之后的Class文件中就可以了呢?

仔细想一下,Java源代码中之所以不允许这么重载方法,是为了避免方法调用时产生歧义,比如:

public Object test(){
    return "obj";
}
public String test(){
    return "str";
}
public static void main(String[] args){
    System.out.println(new Super().test());
}

此时编译器是无法确定调用哪个test()的,所以干脆禁止出现这种情况。而在运行期,JVM有足够的方法去区分这种二义性(比如用ACC_BRIDGEACC_SYNTHETIC这两个flag),所以就可以允许这种情况存在了。

5. 总结

通篇讲了两个问题:Java泛型是如何实现的(简单讲),这种实现会带来什么问题(着重在讲)。据此牵扯出了里氏替换原则协变与逆变协变返回类型两同两小一大口诀(就这个名字最low…)、桥方法这些概念。

这篇博文旨在讲解原理,对实际应用太多帮助。后面还会陆续整理一些博文,比如Arrays.sort的源码、比如泛型如何实现协变与逆变。

时间: 2024-10-09 07:59:31

Java泛型的实现:原理与问题的相关文章

Java泛型的内部原理设计泛型的好处

分享一些工作的经验:不存在脱离业务的技术.所有新技术都是为了解决一些业务痛点,让特定业务更爽. 当我们掌握足够多的技术,在遇到问题时就可以选择适合的技术进行解决.反之,如果没有技术储备,就会手足无措,又或者说选择一些不太恰当的技术进行解决,最终都会走一些弯路.踩一些坑.走弯路.踩坑固然是所有项目都会遇到的一个问题,一个人走弯路.踩坑也许不是什么大问题,但整个项目走弯路,这个最终苦的还是我们这些技术人员. 技术储备至关重要,不论是团队还是个人.有了足够的技术储备,才可以游刃有余,做到胸有成竹,遇到

JAVA泛型的实现原理

1.基本学过JAVA的人都知道一点泛型,明白常出现的位置和大概怎么使用. 在类上为:class 类名<T> {} 在方法上为:public <T> void 方法名 (T x){} 就不再赘述了. 2.泛型就是将类型变成了参数去传入,使得可以使用的类型多样化,进而实现解耦. JAVA因为泛型是在1.5以后出现的,为了保持对以前版本的兼容,使用了擦除的方法实现泛型.所以比起C++等在使用上限制较多. 擦除是什么呢,实际上就是一定程度无视了类型参数T,直接从T所在的类开始向上T的父类去

JAVA泛型的基本使用

Java1.5版本号推出了泛型,尽管这层语法糖给开发者带来了代码复用性方面的提升,可是这只是是编译器所做的一层语法糖,在真正生成的字节码中,这类信息却被擦除了. 笔者发现非常多几年开发经验的程序猿,依旧不善于使用Java泛型,本文将从Java泛型的基本使用入手,在今后的多篇博文里.对泛型的使用做个总结.本文不会深入Java泛型的实现原理.仅仅会介绍Java泛型的使用. 实验准备 首先须要创建一个类继承体系.以商品为例,每种商品都有主要的名称属性.在大数据应用中,数据表和服务都能够作为商品,表有行

JAVA泛型——基本使用

Java1.5版本推出了泛型,虽然这层语法糖给开发人员带来了代码复用性方面的提升,但是这不过是编译器所做的一层语法糖,在真正生成的字节码中,这类信息却被擦除了.笔者发现很多几年开发经验的程序员,依然不善于使用Java泛型,本文将从Java泛型的基本使用入手,在今后的多篇博文里,对泛型的使用做个总结.本文不会深入Java泛型的实现原理,只会介绍Java泛型的使用. 实验准备 首先需要创建一个类继承体系.以商品为例,每种商品都有基本的名称属性.在大数据应用中,数据表和服务都可以作为商品,表有行数属性

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

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

Java泛型简明教程

Java泛型简明教程 博客分类: Java综合 JavaApple数据结构CC++ Java泛型简明教程 本文是从 Java Generics Quick Tutorial 这篇文章翻译而来. 泛型是Java SE 5.0中引入的一项特征,自从这项语言特征出现多年来,我相信,几乎所有的Java程序员不仅听说过,而且使用过它.关于Java泛型的教程,免费的,不免费的,有很多.我遇到的最好的教材有: The Java Tutorial Java Generics and Collections ,

Java泛型-- 通配符

转自:http://blog.csdn.net/flfna/article/details/6576394 ———————————————————————————————————————————— 通配符 在本文的前面的部分里已经说过了泛型类型的子类型的不相关性.但有些时候,我们希望能够像使用普通类型那样使用泛型类型: ◆ 向上造型一个泛型对象的引用 ◆ 向下造型一个泛型对象的引用 向上造型一个泛型对象的引用 例如,假设我们有很多箱子,每个箱子里都装有不同的水果,我们需要找到一种方法能够通用的处

Java 泛型(Generics) 综述

一. 引子 一般的类和方法,只能使用具体类型:要么是基本类型,要么是自定义类型.如果要编写可以应用于多种类型的代码,这种刻板的限制对代码的束缚就会很大. 多态算是一种泛化机制,但对代码的约束还是太强(要么继承父类,要么实现接口). 有许多原因促成了泛型的出现,而最引人注目的一个原因,就是为了创造容器类.(泛型的主要目的之一就是用来指定容器要持有什么类型的对象,而且由编译器来保证类型的正确性) 例如,在 Java 实现加入泛型前,ArrayList 只维护一个 Object 类型的数组: publ

关于Java泛型的新解

////////////////////////////////////////////////////////////////////////////////为了方便您的观看,请在web版式视图在观看本文章.////////////////////////////////////////////////////////////////////////////////////////////// <关于泛型> -----------------------------------à入门泛型的基