What
泛型,即“参数化类型”。
一提到参数,最熟悉的就是定义方法时有形参,然后调用此方法时传递实参。那么参数化类型怎么理解呢?顾名思义,就是将类型由原来的具体的类型参数化,类似于方法中的变量参数,此时类型也定义成参数形式(可以称之为类型形参),然后在使用/调用时传入具体的类型(类型实参)。
泛型的本质是为了参数化类型(在不创建新的类型的情况下,通过泛型指定的不同类型来控制形参具体限制的类型)。也就是说在泛型使用过程中,操作的数据类型被指定为一个参数,这种参数类型可以用在类、接口和方法中,分别被称为泛型类、泛型接口、泛型方法。
- 举个栗子: ```java List arrayList = new ArrayList(); arrayList.add(“aaaa”); arrayList.add(100); for(int i = 0; i< arrayList.size();i++){ String item = (String)arrayList.get(i); Log.d(“泛型测试”,”item = “ + item); }
============================================ //毫无疑问,程序的运行结果会以崩溃结束: java.lang.ClassCastException: java.lang.Integer cannot be cast to java.lang.String
ArrayList可以存放任意类型,例子中添加了一个String类型,添加了一个Integer类型,再使用时都以String的方式使用,因此程序崩溃了。为了解决类似这样的问题(在编译阶段就可以解决),泛型应运而生。
我们将第一行声明初始化list的代码更改一下,编译器会在编译阶段就能够帮我们发现类似这样的问题。
```java
List arrayList = new ArrayList();
arrayList.add(100); //在编译阶段,编译器就会报错
泛型的作用
- 做参数化校验,实现安全的类型转换;
- 比如在集合中使用泛型。
- 解除方法签名的局限性,实现解耦。
- 不使用泛型时,普通的类和方法只能使用签名中固定的类型,如果编写的代码需要应用于多种类型,这种严苛的限制对代码的束缚就会很大,而使用泛型就可以解除这种束缚。
How
常用的 T,E,K,V,?
本质上这些个都是通配符,没啥区别,只不过是编码时的一种约定俗成的东西。比如上述代码中的 T ,我们可以换成 A-Z 之间的任何一个 字母都可以,并不会影响程序的正常运行,但是如果换成其他的字母代替 T ,在可读性上可能会弱一些。通常情况下,T,E,K,V,?是这样约定的:
T | (type) 表示具体的一个java类型 |
---|---|
K V | (key value) 分别代表java键值中的Key Value |
E | (element) 代表Element |
? | 表示不确定的 java 类型 |
泛型的三种方式
泛型类
- 怎么实现父类的泛型必须是子类?
Base
- 元组
元组,它是将一组对象直接打包存储于单一对象中。可以从该对象读取其中的元素,但不允许向其中存储新对象(这个概念也称为 数据传输对象 或 信使 )。
通常,元组可以具有任意长度,元组中的对象可以是不同类型的。不过,我们希望能够为每个对象指明类型,并且从元组中读取出来时,能够得到正确的类型。要处理不同长度的问题,我们需要创建多个不同的元组。
泛型接口
略,比较常用不多解释。
泛型方法
泛型方法独立于类而改变方法。作为准则,请“尽可能”使用泛型方法。通常将单个方法泛型化要比将整个类泛型化更清晰易懂。
- 定义泛型方法
对于泛型类,必须在实例化该类时指定类型参数。使用泛型方法时,通常不需要指定参数类型,因为编译器会找出这些类型。 这称为 类型参数推断。因此,在调用泛型方法时,看起来和普通方法调用没撒区别。
要定义泛型方法,请将泛型参数列表放置在返回值之前,如下所示:
- 调用泛型方法
泛型方法在调用的时候,传递的是什么类型,表示的就是什么类型
构造复杂的模型
泛型的一个重要好处是能够简单安全地创建复杂模型。例如:
public class TupleList<A, B, C, D>
extends ArrayList<Tuple4<A, B, C, D>> {
public static void main(String[] args) {
TupleList<Vehicle, Amphibian, String, Integer> tl =
new TupleList<>();
tl.add(TupleTest2.h());
tl.add(TupleTest2.h());
tl.forEach(System.out::println);
}
}
/* Output:
(Vehicle@7cca494b, Amphibian@7ba4f24f, hi, 47)
(Vehicle@3b9a45b3, Amphibian@7699a589, hi, 47)
*/
Why
泛型擦除
Java中的泛型是在编译器这个层次来实现的。在生成的Java字节代码中是不包含泛型中的类型信息的。使用泛型的时候加上的类型参数,会被编译器在编译的时候去掉。这个过程就称为类型擦除。
即 泛型只在编辑阶段有效。这意味着当你在使用泛型时,运行阶段任何具体的类型信息都被擦除了,你唯一知道的就是你在使用一个对象。但,即使擦除移除了方法或类中的实际类型的信息,编译器仍可以确保方法或类中使用的类型的内部一致性。
- 示例:
代码很简单,看起来没什么问题,但是编译器却报错:
listMethod(List
通过上面的例子可以证明,在编译之后程序会采取去泛型化的措施。也就是说Java中的泛型,只在编译阶段有效。在编译过程中,正确检验泛型结果后,会将泛型的相关信息擦出,并且在对象进入和离开方法的边界处添加类型检查和类型转换的方法。也就是说,泛型信息不会进入到运行时阶段。
- 避免 JVM 的大换血。如果 JVM 将泛型类型延续到运行期,那么到运行期时 JVM 就需要进行大量的重构工作;
- 版本兼容。在编译期擦除可以更好地支持原生类型(Raw Type)。
擦除的代价
擦除的代价是显著的。泛型不能用于显式地引用运行时类型的操作中,例如转型、instanceof 操作和 new 表达式。因为所有关于参数的类型信息都丢失了,当你在编写泛型代码时,必须时刻提醒自己,你只是看起来拥有有关参数的类型信息而已。擦除补偿
- 由于擦除了类型信息,因此使用 instanceof 将会失败。而通过类型标签
isInstance()
可以; - 编译器动态生成;
当类型信息被擦除之后,上述类的声明变成了class MyString implements Comparable。但是这样的话,类MyString就会有编译错误,因为没有实现接口Comparable声明的int compareTo(Object o)方法。这个时候就由编译器来动态生成这个方法。
- 为了确保类型安全, 编译器禁止如下示例的泛型使用方式;
这段代码中,inspect方法接收List