youyichannel

志于道,据于德,依于仁,游于艺!

0%

谈谈 Java 中的泛型

前言:Java 泛型是从 JDK 1.5才开始引入的,因此为了兼容之前的版本,Java 中的泛型实现采取了「伪泛型」,即在语法上支持放心,但在编译阶段会进行「泛型擦除」,将所有的泛型表示都替换成具体的类型,即其对应的原生类型。

什么是泛型?

泛型就是指在定义一个类、接口或者方法的时候,可以指定类型参数。这个类型参数可以在使用类、接口或者方法时动态指定。

泛型的本质是为了参数化类型(在不创建新的类型的情况下,通过泛型指定的不同类型来控制形参具体限制的类型)。

泛型上下限

<?> // 无限制通配符
<? extends E> // extends 关键字声明了类型的上界,表示参数化的类型可能是所指定的类型,或者是此类型的子类
<? super E> // super 关键字声明了类型的下界,表示参数化的类型可能是指定的类型,或者是此类型的父类

在《Effective Java》中有关于泛型的使用原则,为了获得最大限度的灵活性,要在表示生产者或者消费者的输入参数上使用通配符,使用的规则就是:生产者有上限、消费者有下限

  1. 如果参数化类型表示一个 T 的生产者,使用 <? extends T>
  2. 如果它表示一个 T 的消费者,就使用 <? super T>
  3. 如果既是生产又是消费,那使用通配符就没什么意义了,此时需要的是精确的参数类型。

泛型数组

泛型数组的声明:

List<String>[] list11 = new ArrayList<String>[10]; // 编译错误,非法创建
List<String>[] list12 = new ArrayList<?>[10]; // 编译错误,需要强制类型转换
List<String>[] list13 = (List<String>[]) new ArrayList<?>[10]; // 编译通过,但是 warning
List<?>[] list14 = new ArrayList<String>[10]; // 编译错误,非法创建
List<?>[] list15 = new ArrayList<?>[10]; // 编译通过
List<String>[] list6 = new ArrayList[10]; // 编译通过,但是 warning

合理使用泛型数组:

public class ArrayWithTypeToken<T> {
private T[] array;

public ArrayWithTypeToken(Class<T> type, int size) {
array = (T[]) Array.newInstance(type, size);
}

public void put(int index, T item) {
array[index] = item;
}

public T get(int index) {
return array[index];
}

public T[] create() {
return array;
}
}

ArrayWithTypeToken<Integer> arrayToken = new ArrayWithTypeToken<Integer>(Integer.class, 100);
Integer[] array = arrayToken.create();

泛型中 EKVT 的含义

  • E: Element ,在集合中使用,因为集合中存放的是元素。
  • T: Type,Java类。
  • K: Key,键。
  • V: Value,值。
  • N: Number,数值类型。
  • ?: 表示不确定的 Java 类型。
  • S、U、V: 2nd、3rd、4th types。
public class ArrayList<E> extends AbstractList<E> implements List<E>, RandomAccess, Cloneable, java.io.Serializable{
...
}
public class HashMap<K,V> extends AbstractMap<K,V> implements Map<K,V>, Cloneable, Serializable {
...
}

泛型优势

使用泛型的优势

  • 编译时类型检查:在使用泛型时,加入向容器中存入非特定对象是,在编译阶段就会报错。假如不使用泛型,可以向容器中存入任意类型,容易出现类型转换异常。
  • 不需要进行类型强制转换:使用泛型后容器可以记住存入容器中的对象类型。
  • 代码可读性提升:使用泛型后,很容易得知容器中对象的类型。

泛型擦除

原则

泛型的类型擦除原则是:

  • 消除类型参数声明,即删除<>及其包围的部分。
  • 根据类型参数的上下限推断并替换所有的类型参数为原生态类型:如果类型参数是无限制通配符或没有上下限定,则替换为 Object;如果存在上下限定,则根据子类替换原则取类型参数的最左边限定类型(即父类)。
  • 为了保证类型安全,必要时插入强制类型转换代码。
  • 自动产生「桥接方法」,保证擦除类型后的代码仍然具有泛型的「多态性」。

如何擦除?

1)擦除类定义中的类型参数 => 无限制类型擦除

当类定义中的类型参数没有任何限制时,在类型擦除中直接被替换为 Object,即形如<T><?>的类型参数都被替换为Object

2)擦除类定义中的类型参数 => 有限制类型擦除

当类定义中的类型参数存在限制(上下限)时,在类型擦除中替换为类型参数的上界或者下界,比如形如<T extends Number><? extends Number>的类型参数被替换为Number<? super Number>被替换为 Object

3)擦除方法定义中的类型参数

同擦除类定义中的类型参数。

关于List、List<?>、List<Object>的区别

https://segmentfault.com/a/1190000018189575

  • List,即原始类型,其引用变量可以接受任何对应List<E>的参数化类型, 包括List<?>,并且可以添加任意类型的元素。但其缺点在于不安全性、不便利性、不表述性(不应该使用原生类型的原因)。
  • List<?>,即通配符类型,其引用变量,同样可以接受任何对应List<E>的参数化类型,包括List,但不能添加任何元素,保证了安全性和表述性,但不具有表述性,从中取出的元素时Object类型,要通过手动转换才能得到原本的类型。
  • List<Object>,即实际类型参数为Object的参数化类型,其引用变量可以接受List,可以添加元素,但不能接受除了其本身外的任何参数化类型(泛型的子类型化原则)。

  • https://pdai.tech/md/java/basic/java-basic-x-generic.html
  • https://segmentfault.com/a/1190000018189575
  • https://www.cnblogs.com/54chensongxia/p/12470672.html