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

  1. ArrayList可以存放任意类型,例子中添加了一个String类型,添加了一个Integer类型,再使用时都以String的方式使用,因此程序崩溃了。为了解决类似这样的问题(在编译阶段就可以解决),泛型应运而生。
  2. 我们将第一行声明初始化list的代码更改一下,编译器会在编译阶段就能够帮我们发现类似这样的问题。
  3. ```java
  4. List arrayList = new ArrayList();
  5. 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>

  • 元组

元组,它是将一组对象直接打包存储于单一对象中。可以从该对象读取其中的元素,但不允许向其中存储新对象(这个概念也称为 数据传输对象 或 信使 )。
通常,元组可以具有任意长度,元组中的对象可以是不同类型的。不过,我们希望能够为每个对象指明类型,并且从元组中读取出来时,能够得到正确的类型。要处理不同长度的问题,我们需要创建多个不同的元组。

泛型接口

略,比较常用不多解释。

泛型方法

泛型方法独立于类而改变方法。作为准则,请“尽可能”使用泛型方法。通常将单个方法泛型化要比将整个类泛型化更清晰易懂。

  • 定义泛型方法

对于泛型类,必须在实例化该类时指定类型参数。使用泛型方法时,通常不需要指定参数类型,因为编译器会找出这些类型。 这称为 类型参数推断。因此,在调用泛型方法时,看起来和普通方法调用没撒区别。
要定义泛型方法,请将泛型参数列表放置在返回值之前,如下所示:
83e29fa6-91cf-4621-ab25-788b01108f3e.png

  • 调用泛型方法

44c5ecdd-a84c-43ac-8c2d-4a584308dc12.png
泛型方法在调用的时候,传递的是什么类型,表示的就是什么类型

构造复杂的模型

泛型的一个重要好处是能够简单安全地创建复杂模型。例如:

  1. public class TupleList<A, B, C, D>
  2. extends ArrayList<Tuple4<A, B, C, D>> {
  3. public static void main(String[] args) {
  4. TupleList<Vehicle, Amphibian, String, Integer> tl =
  5. new TupleList<>();
  6. tl.add(TupleTest2.h());
  7. tl.add(TupleTest2.h());
  8. tl.forEach(System.out::println);
  9. }
  10. }
  11. /* Output:
  12. (Vehicle@7cca494b, Amphibian@7ba4f24f, hi, 47)
  13. (Vehicle@3b9a45b3, Amphibian@7699a589, hi, 47)
  14. */

Why

泛型擦除

Java中的泛型是在编译器这个层次来实现的。在生成的Java字节代码中是不包含泛型中的类型信息的。使用泛型的时候加上的类型参数,会被编译器在编译的时候去掉。这个过程就称为类型擦除。
即 泛型只在编辑阶段有效。这意味着当你在使用泛型时,运行阶段任何具体的类型信息都被擦除了,你唯一知道的就是你在使用一个对象。但,即使擦除移除了方法或类中的实际类型的信息,编译器仍可以确保方法或类中使用的类型的内部一致性。

  • 示例:

image.png
代码很简单,看起来没什么问题,但是编译器却报错:
listMethod(List) 方法在编译时擦除类型后的方法是listMethod(List),它与另外一个方法重复,也就是方法签名重复。
通过上面的例子可以证明,在编译之后程序会采取去泛型化的措施。也就是说Java中的泛型,只在编译阶段有效。在编译过程中,正确检验泛型结果后,会将泛型的相关信息擦出,并且在对象进入和离开方法的边界处添加类型检查和类型转换的方法。也就是说,泛型信息不会进入到运行时阶段。

  • 一言蔽之:泛型类型在逻辑上看以看成是多个不同的类型,实际上都是相同的基本类型。

    为什么要擦除?

  1. 避免 JVM 的大换血。如果 JVM 将泛型类型延续到运行期,那么到运行期时 JVM 就需要进行大量的重构工作;
  2. 版本兼容。在编译期擦除可以更好地支持原生类型(Raw Type)。

    擦除的代价

    擦除的代价是显著的。泛型不能用于显式地引用运行时类型的操作中,例如转型、instanceof 操作和 new 表达式。因为所有关于参数的类型信息都丢失了,当你在编写泛型代码时,必须时刻提醒自己,你只是看起来拥有有关参数的类型信息而已。

    擦除补偿

  • 由于擦除了类型信息,因此使用 instanceof 将会失败。而通过类型标签isInstance() 可以;
  • 编译器动态生成;

image.png
当类型信息被擦除之后,上述类的声明变成了class MyString implements Comparable。但是这样的话,类MyString就会有编译错误,因为没有实现接口Comparable声明的int compareTo(Object o)方法。这个时候就由编译器来动态生成这个方法。

  • 为了确保类型安全, 编译器禁止如下示例的泛型使用方式;

image.png
这段代码中,inspect方法接收List作为参数,为什么会出现编译错误呢?假设这样的做法是允许的,那么在inspect方法就可以通过list.add(1)来向集合中添加一个数字。这样在test方法看来,其声明为List的集合中却被添加了一个Integer类型的对象。这显然是违反类型安全的原则的,在某个时候肯定会抛出ClassCastException。因此,编译器禁止这样的行为。
编译器会尽可能的检查可能存在的类型安全问题。对于确定是违反相关原则的地方,会给出编译错误。当编译器无法判断类型的使用是否正确的时候,会给出警告信息。

泛型边界

边界允许我们对泛型使用的参数类型施加约束。
由于擦除会删除类型信息,因此唯一可用于无限制泛型参数的方法是那些 Object 可用的方法。但是,如果将该参数限制为某类型的子集,就能调用该子集中的方法。Java 泛型通过 extends、super关键字来实现约束。

  • 泛型通配符

即 在泛型参数表达式中通过问号? 表示,通配符将会把范围限制在单个类型(子类或父类),而不是单个类。

上界通配符

上界通配符主要用于读数据。用 extends 关键字声明,表示参数化的类型可能是所指定的类型,或者是此类型的子类。

  1. class Fruit {
  2. private String name;
  3. //get/set/construction...
  4. }
  5. class Apple extends Fruit {
  6. }
  7. class test {
  8. public static void main(String[] args) {
  9. List<Apple> apple = new ArrayList<>();
  10. getNames(apple); //编译通过
  11. getNames2(apple); //编译异常
  12. }
  13. private String getNames (List<? extends Fruit> furits) {
  14. String names = "";
  15. for ( Fruit fruit : furits ){
  16. names += furit.getName();
  17. }
  18. return names;
  19. }
  20. private String getNames2 (List< Fruit> furits){
  21. String names = "";
  22. for ( Fruit fruit : furits ){
  23. names += furit.getName();
  24. }
  25. return names;
  26. }
  27. }

下界通配符

下界通配符主要用于写数据。用 super 进行声明,表示参数化的类型可能是所指定的类型,或者是此类型的父类型,直至 Object。

  1. class Fruit {
  2. private String name;
  3. //get/set/construction...
  4. }
  5. class Apple extends Fruit {
  6. }
  7. class test {
  8. public static void main(String[] args) {
  9. List<Apple> apples = new ArrayList<>();
  10. List<Fruit> fruits = new ArrayList<>();
  11. test(fruits,apples);
  12. }
  13. private <T> void test (List<? super T> d, List<T> s) {
  14. for (T t: s){
  15. //此时,子类向上转型为了父类,是可以的
  16. d.add(t);
  17. }
  18. }
  19. }

无界通配符

直接使用无界通配符?,一般表示可以是任意元素,但这样做是没啥意义的,一般是配合super 或者 extends关键字来使用,也就是上界通配符和下界通配符。

?和 T 的区别

  • T 是一个 确定的 类型,通常用于泛型类和泛型方法的定义;
  • ?是一个 不确定 的类型,通常用于泛型方法的调用代码和形参,不能用于定义类和泛型方法。