深入理解泛型
引入泛型的意义何在?
- 泛型的提出是为了编写重用性更好的代码。
- 泛型的本质是参数化类型,也就是说所操作的数据类型被指定为一个参数。
在未引入泛型之前,需要用Object来实现通用、不同类型的处理。
缺点如下:
- 每次使用时都需要强制转换成想要的类型。
- 在编译时编译器并不知道类型转换是否正常,运行时才知道,不安全。
实际上引入泛型的主要目标有以下几点:
类型安全 :
- 泛型的主要目标是提高 Java 程序的类型安全
- 编译时期就可以检查出因 Java 类型不正确导致的 ClassCastException 异常
- 符合越早出错代价越小原则
消除强制类型转换 :
- 泛型的一个附带好处是,使用时直接得到目标类型,消除许多强制类型转换
- 所得即所需,这使得代码更加可读,并且减少了出错机会潜在的性能收益
潜在的性能收益:
- 由于泛型的实现方式,支持泛型(几乎)不需要 JVM 或类文件更改
- 所有工作都在编译器中完成
- 编译器生成的代码跟不使用泛型(和强制类型转换)时所写的代码几乎一致,只是更能确保类型安全而已
泛型的使用
泛型的本质是参数化类型,也就是说所操作的数据类型被指定为一个参数。
这种参数类型可以用在类、接口和方法的创建中,分别称为泛型类、泛型接口、泛型方法。
泛型类:泛型类最常见的用途就是作为容纳不同类型数据的容器类,比如 Java 集合容器类。
泛型接口:实现类在实现泛型接口时需要指明具体的参数类型,不然默认类型是 Object类型。
泛型方法:如果所在的类是泛型类,则直接使用类声明的参数,如果不是,则需自己声明参数类型。
泛型通配符
<?>:无限制通配符,表示可以持有任何类型。
<?>和<Object>不一样,<?>表示未知类型,<Object>表示任意类型。
<? extends E>:
在类型参数中使用 extends 表示这个泛型中的参数必须是 E 或者 E 的子类,这样有两个好处:
- 如果传入的类型不是 E 或者 E 的子类,编辑不成功。
- 泛型中可以使用 E 的方法,要不然还得强转成 E 才能使用。
<? super E>:
在类型参数中使用 super 表示这个泛型中的参数必须是 E 或者 E 的父类。
小结:
- 无限制通配符<?> 和 Object 有些相似,用于表示无限制或者不确定范围的场景。
- 两种有限制通配形式 < ? super E> 和 < ? extends E> 也比较容易混淆,我们再来比较下。
- 它们的目的都是为了使方法接口更为灵活,可以接受更为广泛的类型。
- < ? super E> 用于灵活写入或比较,使得对象可以写入父类型的容器,使得父类型的比较方法可以应用于子类对象。
- < ? extends E> 用于灵活读取,使得方法可以读取 E 或 E 的任意子类型的容器对象。
使用通配符的基本原则:
- 如果参数化类型表示一个 T 的生产者,使用 < ? extends T>;
- 如果它表示一个 T 的消费者,就使用 < ? super T>;
- 如果既是生产又是消费,那使用通配符就没什么意义了,因为你需要的是精确的参数类型。
- T 的生产者的意思就是结果会返回 T,这就要求返回一个具体的类型,必须有上限才够具体;
- T 的消费者的意思是要操作 T,这就要求操作的容器要够大,所以容器需要是 T 的父类,即 super T;
泛型的类型擦除
用泛型编写的 Java 程序和普通的 Java 程序基本相同,只是多了一些参数化的类型同时少了一些类型转换。
实际上泛型程序也是首先被转化成一般的、不带泛型的 Java 程序后再进行处理的,编译器自动完成了从 Generic Java 到普通 Java 的翻译,Java 虚拟机运行时对泛型基本一无所知。
当编译器对带有泛型的java代码进行编译时,它会去执行类型检查和类型推断,然后生成普通的不带泛型的字节码,这种普通的字节码可以被一般的 Java 虚拟机接收并执行,这在就叫做 类型擦除(type erasure)。
总之,泛型就是一个语法糖,它运行时没有存储任何类型信息。
泛型的情况称为不可变性,与之对应的概念是协变、逆变:
- 协变:如果 A 是 B 的父类,并且 A 的容器(比如 List<A>) 也是 B 的容器(List<B>)的父类,则称之为协变的(父子关系保持一致)。
- 逆变:如果 A 是 B 的父类,但是 A 的容器 是 B 的容器的子类,则称之为逆变(放入容器就篡位了)。
- 不可变:不论 A B 有什么关系,A 的容器和 B 的容器都没有父子关系,称之为不可变。
Java 中数组是协变的,泛型是不可变的。
如果想要让某个泛型类具有协变性,就需要用到边界。
- 我们知道,泛型运行时被擦除成原始类型,这使得很多操作无法进行。
- 如果没有指明边界,类型参数将被擦除为 Object。
- 如果我们想要让参数保留一个边界,可以给参数设置一个边界,泛型参数将会被擦除到它的第一个边界(边界可以有多个),这样即使运行时擦除后也会有范围。
泛型的规则
- 泛型的参数类型只能是类(包括自定义类),不可以是简单类型。
- 同一种泛型可以对应多个版本(因为参数类型是不确定的),不同版本的泛型类实例是不兼容的。
- 泛型的类型参数可以有多个。
- 泛型的参数类型可以使用 extends 语句,习惯上称为“有界类型”。
- 泛型的参数类型还可以是通配符类型,例如 Class。
泛型的使用场景
当类中要操作的引用数据类型不确定的时候,过去使用 Object 来完成扩展,JDK 1.5后推荐使用泛型来完成扩展,同时保证安全性。
Java中List<Object>和原始类型List之间的区别?
原始类型和带参数类型之间的主要区别是:
- 在编译时编译器不会对原始类型进行类型安全检查,却会对带参数的类型进行安全检查。
- 通过使用Object作为类型,可以告知编译器该方法可以接受任何类型的对象,比如String或Integer。
- 你可以把任何带参数的类型传递给原始类型 List,但却不能把List<String> 传递给接受List<Object>的方法,因为泛型的不可变性,会产生编译错误。