深入理解Java之泛型

1. Why ——引入泛型机制的原因

假如我们想要实现一个String数组,并且要求它可以动态改变大小,这时我们都会想到用ArrayList来聚合String对象。然而,过了一阵,我们想要实现一个大小可以改变的Date对象数组,这时我们当然希望能够重用之前写过的那个针对String对象的ArrayList实现。

在Java 5之前,ArrayList的实现大致如下:

1 public class ArrayList {
2     public Object get(int i) { ... }
3     public void add(Object o) { ... }
4     ...
5     private Object[] elementData;
6 }

从以上代码我们可以看到,用于向ArrayList中添加元素的add函数接收一个Object型的参数,从ArrayList获取指定元素的get方法也返回一个Object类型的对象,Object对象数组elementData存放这ArrayList中的对象, 也就是说,无论你向ArrayList中放入什么类型的类型,到了它的内部,都是一个Object对象。

基于继承的泛型实现会带来两个问题:第一个问题是有关get方法的,我们每次调用get方法都会返回一个Object对象,每一次都要强制类型转换为我们需要的类型,这样会显得很麻烦;第二个问题是有关add方法的,假如我们往聚合了String对象的ArrayList中加入一个File对象,编译器不会产生任何错误提示,而这不是我们想要的。

所以,从Java 5开始,ArrayList在使用时可以加上一个类型参数(type parameter),这个类型参数用来指明ArrayList中的元素类型。类型参数的引入解决了以上提到的两个问题,如以下代码所示:

ArrayList<String> s = new ArrayList<String>();
s.add("abc");
String s = s.get(0); //无需进行强制转换
s.add(123);  //编译错误,只能向其中添加String对象
...

在以上代码中,编译器“获知”ArrayList的类型参数String后,便会替我们完成强制类型转换以及类型检查的工作。

2. 泛型类

    所谓泛型类(generic class)就是具有一个或多个类型参数的类。例如:

public class Pair<T, U> {
    private T first;
    private U second;

    public Pair(T first, U second) {
        this.first = first;
        this.second = second;
    }

    public T getFirst() {
        return first;
    }

    public U getSecond() {
        return second;
    }

    public void setFirst(T newValue) {
        first = newValue;
    }

    public void setSecond(U newValue) {
        second = newValue;
    }
}

上面的代码中我们可以看到,泛型类Pair的类型参数为T、U,放在类名后的尖括号中。这里的T即Type的首字母,代表类型的意思,常用的还有E(element)、K(key)、V(value)等。当然不用这些字母指代类型参数也完全可以。

实例化泛型类的时候,我们只需要把类型参数换成具体的类型即可,比如实例化一个Pair<T, U>类我们可以这样:

Pair<String, Integer> pair = new Pair<String, Integer>();

3. 泛型方法

所谓泛型方法,就是带有类型参数的方法,它既可以定义在泛型类中,也可以定义在普通类中。例如:

public class ArrayAlg {
    public static <T> T getMiddle(T[] a) {
        return a[a.length / 2];
    }
}

以上代码中的getMiddle方法即为一个泛型方法,定义的格式是类型变量放在修饰符的后面、返回类型的前面。我们可以看到,以上泛型方法可以针对各种类型的数组调用,在这些数组的类型已知切有限时,虽然也可以用过重载实现,不过编码效率要低得多。调用以上泛型方法的示例代码如下:

String[] strings = {"aa", "bb", "cc"};
String middle = ArrayAlg.getMiddle(names);

4. 类型变量的限定

在有些情况下,泛型类或者泛型方法想要对自己的类型参数进一步加一些限制。比如,我们想要限定类型参数只能为某个类的子类或者只能为实现了某个接口的类。相关的语法如下:

<T extends BoundingType>(BoundingType是一个类或者接口)。其中的BoundingType可以多于1个,用“&”连接即可。

5. 深入理解泛型的实现

实际上,从虚拟机的角度看,不存在“泛型”概念。比如上面我们定义的泛型类Pair,在虚拟机看来(即编译为字节码后),它长的是这样的:

public class Pair {
    private Object first;
    private Object second;

    public Pair(Object first, Object second) {
        this.first = first;
        this.second = second;
    }

    public Object getFirst() {
        return first;
    }

    public Object getSecond() {
        return second;
    }

    public void setFirst(Object newValue) {
        first = newValue;
    }

    public void setSecond(Object newValue) {
        second = newValue;
    }
}

上面的类是通过类型擦除得到的,是Pair泛型类对应的原始类型(raw type)。类型擦除就是把所有类型参数替换为BoundingType(若未加限定就替换为Object)。

我们可以简单地验证下,编译Pair.java后,键入“javap -c -s Pair”可得到:

上图中带“descriptor”的行即为相应方法的签名,比如从第四行我们可以看到Pair构造方法的两个形参经过类型擦除后均已变为了Object。

由于在虚拟机中泛型类Pair变为它的raw type,因而getFirst方法返回的是一个Object对象,而从编译器的角度看,这个方法返回的是我们实例化类时指定的类型参数的对象。实际上, 是编译器帮我们完成了强制类型转换的工作。也就是说编译器会把对Pair泛型类中getFirst方法的调用转化为两条虚拟机指令:

第一条是对raw type方法getFirst的调用,这个方法返回一个Object对象;第二条指令把返回的Object对象强制类型转换为当初我们指定的类型参数类型。

类型擦除也会发生于泛型方法中,如以下泛型方法:

public static <T extends Comparable> T min(T[] a)

编译后经过类型擦除会变成下面这样:

public static Comparable min(Comparable[] a)

方法的类型擦除会带来一些问题,考虑以下的代码:

class DateInterval extends Pair<Date, Date> {
    public void setSecond(Date second) {
        if (second.compareTo(getFirst()) >= 0) {
            super.setSecond(second);
        }
    }
    ...
}

以上代码经过类型擦除后,变为:

class DateInterval extends Pair {
    public void setSecond(Date second) { ... }
    ...
}

而在DateInterval类还存在一个从Pair类继承而来的setSecond的方法(经过类型擦除后)如下:

public void setSecond(Object second)

现在我们可以看到,这个方法与DateInterval重写的setSecond方法具有不同的方法签名(形参不同),所以是两个不同的方法,然而,这两个方法不应该是不同的方法(因为是override)。考虑以下的代码:

DateInterval interval = new DateInterval(...);
Pair<Date, Date> pair = interval;Date aDate = new Date(...);
pair.setSecond(aDate);

由以上代码可知,pair实际引用的是DateInterval对象,因此应该调用DateInterval的setSecond方法,这里的问题是类型擦除与多态发生了冲突。

我们来梳理下为什么会发生这个问题:pair在之前被声明为类型Pair<Date, Date>,该类在虚拟机看来只有一个“setSecond(Object)”方法。因此在运行时,虚拟机发现pair实际引用的是DateInterval对象后,会去调用DateInterval的“setSecond(Object)",然而DateInterval类中却只有”setSecond(Date)"方法。

解决这个问题的方法是由编译器在DateInterval中生成一个桥方法

public void setSecond(Object second) {
    setSecond((Date) second);
}

6. 注意事项

(1)不能用基本类型实例化类型参数

也就是说,以下语句是非法的:

Pair<int, int> pair = new Pair<int, int>();

不过我们可以用相应的包装类型来代替。

(2)不能抛出也不能捕获泛型类实例

泛型类扩展Throwable即为不合法,因此无法抛出或捕获泛型类实例。但在异常声明中使用类型参数是合法的:

public static <T extends Throwable> void doWork(T t) throws T {    try {        ...    } catch (Throwable realCause) {        t.initCause(realCause);        throw t;    }}

(3)参数化类型的数组不合法

在Java中,Object[]数组可以是任何数组的父类(因为任何一个数组都可以向上转型为它在定义时指定元素类型的父类的数组)。考虑以下代码:

String[] strs = new String[10];Object[] objs = strs;obj[0] = new Date(...);

在上述代码中,我们将数组元素赋值为满足父类(Object)类型,但不同于原始类型(Pair)的对象,在编译时能够通过,而在运行时会抛出ArrayStoreException异常。

基于以上原因,假设Java允许我们通过以下语句声明并初始化一个泛型数组:

Pair<String, String>[] pairs = new Pair<String, String>[10];

那么在虚拟机进行类型擦除后,实际上pairs成为了Pair[]数组,我们可以将它向上转型为Object[]数组。这时我们若往其中添加Pair<Date, Date>对象,便能通过编译时检查和运行时检查,而我们的本意是只想让这个数组存储Pair<String, String>对象,这会产生难以定位的错误。因此,Java不允许我们通过以上的语句形式声明并初始化一个泛型数组。

可用如下语句声明并初始化一个泛型数组:

Pair<String, String>[] pairs = (Pair<String, String>[]) new Pair[10];

(4)不能实例化类型变量

不能以诸如“new T(...)", "new T[...]", "T.class"的形式使用类型变量。Java禁止我们这样做的原因很简单,因为存在类型擦除,所以类似于"new T(...)"这样的语句就会变为”new Object(...)", 而这通常不是我们的本意。我们可以用如下语句代替对“new T[...]"的调用:

arrays = (T[]) new Object[N];

(5)泛型类的静态上下文中不能使用类型变量

注意,这里我们强调了泛型类。因为普通类中可以定义静态泛型方法,如上面我们提到的ArrayAlg类中的getMiddle方法。关于为什么有这样的规定,请考虑下面的代码:

public class People<T> {
    public static T name;
    public static T getName() {
        ...
    }
}

我们知道,在同一时刻,内存中可能存在不只一个People<T>类实例。假设现在内存中存在着一个People<String>对象和People<Integer>对象,而类的静态变量与静态方法是所有类实例共享的。那么问题来了,name究竟是String类型还是Integer类型呢?基于这个原因,Java中不允许在泛型类的静态上下文中使用类型变量。

7. 类型通配符

介绍类型通配符前,首先介绍两点:

(1)假设Student是People的子类,Pair<Student, Student>却不是Pair<People, People>的子类,它们之间不存在"is-a"关系。

(2)Pair<T, T>与它的原始类型Pair之间存在”is-a"关系,Pair<T, T>在任何情况下都可以转换为Pair类型。

现在考虑这样一个方法:

public static void printName(Pair<People, People> p) {
    People p1 = p.getFirst();
    System.out.println(p1.getName()); //假设People类定义了getName实例方法
}

在以上的方法中,我们想要同时能够传入Pair<Student, Student>和Pair<People, People>类型的参数,然而二者之间并不存在"is-a"关系。在这种情况下,Java提供给我们这样一种解决方案:使用Pair<? extends People>作为形参的类型。也就是说,Pair<Student, Student>和Pair<People, People>都可以看作是Pair<? extends People>的子类。

形如”<? extends BoundingType>"的代码叫做通配符的子类型限定。与之对应的还有通配符的超类型限定,格式是这样的:<? super BoundingType>。

现在我们考虑下面这段代码:

Pair<Student> students = new Pair<Student>(student1, student2);
Pair<? extends People> wildchards = students;
wildchards.setFirst(people1); 

以上代码的第三行会报错,因为wildchards是一个Pair<? extends People>对象,它的setFirst方法和getFirst方法是这样的:

void setFirst(? extends People)? extends People getFirst()

对于setFirst方法来说,会使得编译器不知道形参究竟是什么类型(只知道是People的子类),而我们试图传入一个People对象,编译器无法判定People和形参类型是否是”is-a"的关系,所以调用setFirst方法会报错。而调用wildchards的getFirst方法是合法的,因为我们知道它会返回一个People的子类,而People的子类“always is a People”。(总是可以把子类对象转换为父类对象)

而对于通配符的超类型限定的情况下,调用getter方法是非法的,而调用setter方法是合法的。

除了子类型限定和超类型限定,还有一种通配符叫做无限定的通配符,它是这样的:<?>。这个东西我们什么时候会用到呢?考虑一下这个场景,我们调用一个会返回一个getPairs方法,这个方法会返回一组Pair<T, T>对象。其中既有Pair<Student, Student>,  还有Pair<Teacher, Teacher>对象。(Student类和Teacher类不存在继承关系)显然,这种情况下,子类型限定和超类型限定都不能用。这时我们可以用这样一条语句搞定它:

Pair<?>[] pairs = getPairs(...);

8. 参考资料

    JAVA核心技术(卷1) (豆瓣)

时间: 2024-07-30 23:53:11

深入理解Java之泛型的相关文章

你是怎么理解java的泛型的?

解答: 在Java SE 1.5之前,没有泛型的情况的下,通过对类型Object的引用来实现参数的"任意化","任意化"带来的缺点是要做显式的强制类型转换,而这种转换是要求开发者对实际参数类型可以预知的情况下进行的.对于强制类型转换错误的情况,编译器可能不提示错误,在运行的时候才出现异常,这是一个安全隐患. 泛型是Java SE 1.5的新特性,泛型的本质是参数化类型,也就是说所操作的数据类型被指定为一个参数.这种参数类型可以用在类.接口和方法的创建中,分别称为泛型

夯实Java基础系列13:深入理解Java中的泛型

目录 泛型概述 一个栗子 特性 泛型的使用方式 泛型类 泛型接口 泛型通配符 泛型方法 泛型方法的基本用法 类中的泛型方法 泛型方法与可变参数 静态方法与泛型 泛型方法总结 泛型上下边界 泛型常见面试题 参考文章 微信公众号 Java技术江湖 个人公众号:黄小斜 本系列文章将整理到我在GitHub上的<Java面试指南>仓库,更多精彩内容请到我的仓库里查看 https://github.com/h2pl/Java-Tutorial 喜欢的话麻烦点下Star.Fork.Watch三连哈,感谢你的

Java 中泛型的全面解析

Java泛型(generics) 是JDK 5中引入的一个新特性,允许在定义类和接口的时候使用类型参数(type parameter).声明的类型参数在使用时用具体的类型来替换.泛型最主要的应用是在JDK 5中的新集合类框架中.对于泛型概念的引入,开发社区的观点是褒贬不一.从好的方面来说,泛型的引入可以解决之前的集合类框架在使用过程中通常会出现的运行时刻类型错误,因为编译器可以在编译时刻就发现很多明显的错误.而从不好的地方来说,为了保证与旧有版本的兼容性,Java泛型的实现上存在着一些不够优雅的

Java 之泛型

两种快速理解泛型的描述 1. 数据类型是抽象的,这个类可以处理很多数据类型.里面的T在被实例化的时候可以用任意类型代替. 2.  泛型,想成一个桶,你装水或者装沙子都行,这个桶只负责提供装满水或者装满沙子的功能 . 官方解释: 泛型是程序设计语言的一种特性.允许程序员在强类型程序设计语言中编写代码时定义一些可变部份,那些部份在使用前必须作出指明. 泛型的定义主要有以下两种: 在程序编码中一些包含类型参数的类型,也就是说泛型的参数只可以代表类,不能代表个别对象.(这是当今较常见的定义) 在程序编码

深入理解Java注解类型(@Annotation)

"-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd"> 深入理解Java注解类型(@Annotation) - zejian的博客 - 博客频道 - CSDN.NET zejian的博客 目录视图 摘要视图 订阅 [活动]2017 CSDN博客专栏评选 &nbsp [5月书讯]流畅的Python,终于等到你!    &am

步步理解 JAVA 泛型编程

步步理解 JAVA 泛型编程 转载自: 隔叶黄莺 Unmi Blog------步步理解 JAVA 泛型编程(一) 隔叶黄莺 Unmi Blog------步步理解 JAVA 泛型编程(二) 隔叶黄莺 Unmi Blog------步步理解 JAVA 泛型编程(三) 步步理解 JAVA 泛型编程(一) JDK 1.5 相对于 JDK 1.4 来说变化最大的部分就是泛型,甚至可以说自 Java 1.0 发布以来也是最大的一次语言变化,因为要涉及到编译器的大改动.很早的时候大家对泛型的呼声很大,正如

JAVA基础—泛型小结

概念: 泛型是Java SE 1.5的新特性,泛型的本质是参数化类型,也就是说所操作的数据类型被指定为一个参数.这种参数类型可以用在类.接口和方法的创建中,分别称为泛型类.泛型接口.泛型方法. Java语言引入泛型的好处是安全简单. 泛型的常用字符代表意思: ? 表示不确定的java类型. T  表示java类型. K V 分别代表java键值中的Key Value. E 代表Element. 下面转载于cnblog上一个写的很好的例子 真的很好,我写了半天,当看到他这个后,立即删除~ 普通泛型

java之 ------ 泛型【从基础到加强】

泛型 基础篇 一.为什么要泛型 1.原因 先看一个集合的例子(至于集合,前面有讲解,集合详解链接,不懂得可以先去看看那篇) import java.util.ArrayList; import java.util.Collection; import java.util.HashSet; import java.util.Iterator; import cn.hncu.bean.Person; public class CollectionDemo { public static void m

Java的泛型机制

------<a href="http://www.itheima.com" target="blank">Java培训.Android培训.ios培训..Net培训</a>.期待与您交流!------/*Java的泛型机制 概念:泛型(Generic type 或者 generics)是对 Java 语言的类型系统的一种扩展,以支持创建可以按类型进行参数化的类.简单的说,就是对要操作的对象指定数据类型,其用法和形参类似. 如:class D