0%

Java泛型

前言

泛型(Generic)是 Java 编程语言的一个强大功能。它们提高了代码的类型安全性,在编译时可以检测到更多的错误。

目录

一、为什么使用泛型

泛型使在定义类、接口和方法时,使类型成为参数。与方法声明中使用的形式参数非常相似,类型形参为你提供了一种让不同的输入重用相同的代码的方法。区别在于形式参数的输入是值,而类型形参的输入是类型。

使用泛型的好处:

  • 在编译时进行更强大的类型检查,Java 编译器将强类型检查应用于泛型代码,并在代码违反类型安全时发出错误。
  • 消除类型转换,泛型中的类型在使用时指定。
  • 能够实现通用算法,让多种不同数据类型执行相同的代码。

二、泛型使用

泛型,即“参数化类型“,将类型由原来的具体的类型参数化,定义成参数形式(类型形参),然后在使用/调用时传入具体的类型(类型实参)。在泛型使用过程中,操作的数据类型被指定为一个参数,这种参数类型可以用在类、接口和方法中,分别被称为泛型类、泛型接口、泛型方法。

泛型类/接口

在类型上参数化的泛型类或接口

1
2
3
4
5
//泛型类
class name<T1, T2, ..., Tn> { /* ... */ }
由尖括号(<>)分隔的类型形参部分跟在类名后面
//泛型接口
interface name<T1, T2, ..., Tn> { /* ... */ }
泛型方法

是引入自己的类型形参的方法。这类似于声明泛型类型,但类型形参的范围仅限于声明它的方法。允许使用静态和非静态泛型方法,以及泛型类构造函数。

泛型方法的语法包括类型形参列表,声明在尖括号内,且必须出现在方法的返回类型之前。

1
public <T> 返回类型 方法名(){ /* ... */ } <类型形参列表> 必须在返回类型前且不能少

注:若泛型类中声明一个泛型方法,类型形参都是T,此时泛型方法定义的T是一种全新的类型,与泛型类中的T不是同一种类型,泛型类中的T只影响其内部普通方法。

三、限定类型形参

有时你可能希望限制可用作参数化类型中的类型实参的类型。例如,对数字进行操作的方法可能只想接受 Number 或其子类的实例

1
2
3
public <T extends Comparable> T min(T a, T b){
if(a.compareTo(b)>0) return a;else return b; //确保T实现了Comparable,运算符(>)仅适用于基本类型,如 short,int,double,long,float,byte 和 char。不能使用>来比较对象,需限定类型形参
}

多重边界:一个类型参数可以具有多个限定:

1
<T extends B1 & B2 & B3>

具有多个限定的类型变量是范围中列出的所有类型的子类型。如果范围之一是类,则必须首先指定它。

1
2
3
4
Class A { /* ... */ } 
interface B { /* ... */ }
interface C { /* ... */ }
class D <T extends A & B & C> { /* ... */ }

注:限定类型中,只允许有一个类,而且如果有类,这个类必须是限定列表的第一个。

四、泛型中的约束和局限性

  • 无法使用基本类型实例化泛型类型。
  • 无法创建类型形参的实例。
  • 无法声明类型为类型参数的静态字段,静态域或非泛型静态方法里不能引用类型变量。(对象创建时,静态代码执行会先于对象的构造方法,无法获知具体类型)
  • 不能使用参数化类型进行类型转换或使用 instanceof(编译器会擦除泛型的所有类型参数)
  • 无法创建参数化类型的数组。
  • 无法创建,捕获或抛出参数化类型的对象,泛型类不能直接或间接地继承 Throwable/Exception
  • 无法重载每个重载的形式参数类型都擦除为相同原始类型的方法。

五、泛型继承规则

给定两种具体类型 AB(例如,NumberInteger),泛型MyClass<A>MyClass<B> 没有任何关系,无论 AB 是否相关。MyClass<A>MyClass<B> 的公共父类是 Object

通过继承泛型类或实现泛型接口来对其进行子类型化。一个类或接口的类型形参与另一个的类型形参之间的关系由 extendsimplements 确定。

Collections 类为例,ArrayList<E> 实现 List<E>List<E> extends Collection<E>。所以 ArrayList<String>List<String> 的子类型,它是 Collection<String> 的子类型。只要不改变类型实参,就会在类型之间保留子类型关系。

1
2
3
public interface Collection<E> { /* ... */ } 
public interface List<E> extends Collection<E> { /* ... */ }
public class ArrayList<E> extends AbstractList<E> implements List<E> { /* ... */ }

六、通配符类型

在泛型中,通配符(?)问号表示未知类型。通配符一般用于方法中:作为参数、字段或局部变量的类型,有时作为返回类型。通配符从不用作泛型方法调用,泛型类实例创建或超类型的类型实参。

上界通配符
表示类型的上界,类型参数是T的子类或本身 主要用于安全地访问数据(get方法,返回类型为T),可以访问T及其子类型,并且不能写入非null的数据。 ##### 下界通配符 表示类型的下界,类型参数是T的超类或本身 主要用于安全地写入数据(set方法),可以写入T及其子类型。 ##### 无界通配符

表示对类型没有什么限制,可以把?看成所有类型的父类,默认实现是<? extends Object>

PECS

方法的参数化类型代表生产者(producer)则使用extends,代表消费者(consumer)则使用super

只从方法的形参集合获取值使用? extends T,只从方法的形参集合写入值使用? super T

PECS 原则即 Producer Extends,Consumer Super 。如果参数化类型是一个生产者,则使用 <? extends T>;如果它是一个消费者,则使用 <? super T>。生产者表示频繁往外读取数据 T,而不从中添加数据。消费者表示只往里插入数据 T,而不读取数据。

七、虚拟机是如何实现泛型的

类型擦除

泛型被引入到 Java 语言中,以便在编译时提供更严格的类型检查并支持泛型编程。为了实现泛型,Java 编译器将类型擦除应用于:

  • 将泛型类型中的所有类型形参替换为其边界或 Object(如果类型形参是无界的)。因此,生成的字节码仅包含普通的类,接口和方法。
  • 如有必要,插入类型转换以保持类型安全。
  • 生成桥接方法以保留继承泛型类型中的多态性。

类型擦除确保不会为参数化类型创建新类,因此,泛型不会产生运行时开销。由于JDK 5.0开始引入,为兼容之前版本采用类型擦除。

在类型擦除过程中,Java 编译器将擦除所有类型形参,并在类型形参有界时将其替换为第一个边界,如果类型形参无界,则替换为 Object

类型识别

由于Java泛型的引入,各种场景(虚拟机解析、反射等)下的方法调用都可能对原有的基础产生影响和新的需求,如在泛型类中如何获取传入的参数化类型等。因此,JCP组织对虚拟机规范做出了相应的修改,引入了Signature、LocalVariableTypeTable等新的属性用于解决伴随泛型而来的参数类型的识别问题,Signature是其中最重要的一项属性,它的作用就是存储一个方法在字节码层面的特征签名,这个属性中保存的参数类型并不是原生类型,而是包括了参数化类型的信息。修改后要求所有能识别Class文件的虚拟机都要能正确地识别Signature参数。

另外,从Signature属性的出现我们还可以得出结论,擦除法所谓的擦除,仅仅是对方法的Code属性中的字节码进行擦除,实际上元数据中还是保留了泛型信息,这也是我们能通过反射手段取得参数化类型的根本依据。

总结

Java语言中的泛型,它只在程序源码中存在,在编译后的字节码文件中,就已经替换为原来的原始类型(Raw Type,也称为裸类型)了,并且在相应的地方插入了强制转型代码,因此,对于运行期的Java语言来说,ArrayList<Integer>与ArrayList<String>就是同一个类,所以泛型技术实际上是Java语言的一颗语法糖,Java语言中的泛型实现方法称为类型擦除,基于这种方法实现的泛型称为伪泛型。