泛型(Generics)是在 JDK 5 版本中引入的。泛型的引入解决了许多类型安全的问题,同时减少了类型转换的需求。
⨳ JDK 5 引入的泛型,允许在定义类、接口和方法时使用类型参数(即泛型参数),从而可以在不指定具体类型的情况下进行编程。泛型保证了在编译时进行类型检查,而不是在运行时进行强制转换。
⨳ JDK 7 引入了钻石操作符(Diamond Operator),使得泛型的使用更加简洁。例如,List list = new ArrayList<>();,这里的 <> 是钻石操作符,它允许编译器根据右侧的类型推断出泛型类型。
⨳ JDK 8 引入了类型注解(Type Annotations),并提供了更强大的类型推断功能,特别是在 lambda 表达式和方法引用中。
⨳ ...
总体而言,Java 泛型自引入以来在语法简化和类型推断方面不断得到改进,特别是在简化代码和提升可读性方面。
泛型基本使用
泛型(Generics)允许在类、接口和方法中使用类型参数化。这意味着你可以编写可以适应多种不同类型的代码,而不必为每种类型重复编写相似的代码。
泛型定义
泛型的核心思想是 类型参数化,即你可以在定义类、接口或方法时不指定具体的类型,而是使用一个符号(通常是大写字母如 T, E, K, V, N 等)作为类型的占位符,在使用该类或方法时再指定具体的类型。
如下,定义一个简单的容器 Box,这个容器 可以存储任意类型的元素 Item:
package com.cango.generics;
public class Box<T> { // 在类上定义泛型 T
private T item;
public void setItem(T item) { // 泛型作为方法参数类型
this.item = item;
}
public T getItem() {// 泛型作为方法返回值类型
return item;
}
}
泛型可以定义在类上、接口上、属性上或方法参数与返回值上,再使用该类的时候只要指定T是什么类型,就可以避免类型转换,如下:
public static void main(String[] args) {
Box<String> stringBox = new Box<String>();
stringBox.setItem("Hello, Generics");
stringBox.setItem(123); // 编译错误,防止错误的类型插入集合
System.out.println(stringBox.getItem());
Box<Integer> integerBox = new Box<Integer>();
integerBox.setItem(123);
System.out.println(integerBox.getItem());
}
这里主要注意,Java 泛型只支持引用类型,不能使用基本类型(如 int, char)作为泛型类型参数。
泛型推断
在 Java 7 及以后的版本中,泛型推断在对象创建时得到了简化。这就是所谓的“钻石语法”(Diamond Syntax),它允许你在实例化泛型类时省略右侧的泛型类型参数,编译器会根据左侧的声明自动推断类型。
Box<String> stringBox = new Box<>();
stringBox.setItem("Hello, Generics");
System.out.println(stringBox.getItem());
Java 8 及以后的版本改进了泛型推断的能力,特别是在 Lambda 表达式和方法引用的使用上。编译器可以在更复杂的上下文中进行类型推断。
Function<String, Integer> stringLength = s -> s.length(); // 推断 s 为 String
System.out.println(stringLength.apply("Hello")); // 输出 5
关于 Lambda 表达式和方法引用,可以参考《Lambda篇》。
泛型上下界
Java 的泛型支持上下界的定义,允许开发者对泛型类型参数进行约束,以限制它们可以接受的类型。这些上下界主要通过关键字 extends 和 super 来实现,分别用于指定泛型的上界和下界。
⨳ ? extends T:表示类型参数必须 T 或 T 的子类型。
⨳ ? super T:表示类型参数必须是 T 或T的父类型。
// 使用上界通配符 `? extends Number` 处理任意 Number 类型的子类
public static void printNumbers(List<? extends Number> list) {
for (Number number : list) {
System.out.println(number);
}
}
public static void main(String[] args) {
List<Integer> intList = Arrays.asList(1, 2, 3);
List<Double> doubleList = Arrays.asList(1.1, 2.2, 3.3);
printNumbers(intList); // 输出 1, 2, 3
printNumbers(doubleList); // 输出 1.1, 2.2, 3.3
}
这里 printNumbers 方法能够处理所有 Number 类型的子类,如 Integer 和 Double。
泛型上下界为 Java 泛型提供了灵活的类型约束机制:
⨳ 当你只想从一个泛型对象中读取数据,而不对其进行修改时,可以使用上界。例如,在处理从列表中读取数据的操作时,可以使用 ? extends。
⨳ 当你需要向泛型对象中写入数据时,可以使用下界。例如,当向列表中添加数据时,可以使用 ? super 来确保写入的安全性。
泛型擦除
Java 泛型是在 编译时 实现的,在编译过程中,所有的泛型信息都会被擦除,这就是所谓的“类型擦除”(Type Erasure)。类型擦除机制保证了泛型在 Java 中的向后兼容性,使得泛型代码能够与非泛型代码(如 Java 1.5 之前的代码)无缝集成。
要了解泛型擦除,得看看泛型类编译后变成了什么。使用 JAD 反编译 Box.class,得到下述代码:
package com.cango.generics;
public class Box
{
public Box()
{
}
public void setItem(Object item)
{
this.item = item;
}
public Object getItem()
{
return item;
}
private Object item;
}
可以看到在编译后的字节码中,泛型类型 T 会被擦除,被替换为了 Object,难怪可以使用任意类型替换泛型。
其实类型擦除并不只是使用 Object 替换,在 Java 中,类型擦除的过程有两种情况:
⨳ 未指定上界:如果泛型类型参数没有指定上界(如 <T>),编译器会将泛型类型擦除为 Object。
⨳ 指定了上界:如果泛型类型参数指定了上界(如 <T extends Number>),编译器会将泛型类型擦除为上界类型(如 Number)。
由于类型擦除的存在,Java 的泛型在运行时并没有类型参数的具体信息。例如,不能在运行时判断某个对象是否是某个特定的泛型类。
List<String> stringList = new ArrayList<>();
List<Integer> integerList = new ArrayList<>();
System.out.println(stringList.getClass() == integerList.getClass()); // true
因为所有的泛型信息都会在运行时被擦除替换,所以无法在运行时判断一个 List 实例是否是 List<String> 还是 List<Integer>,因为它们都会被擦除为 List。在运行时,stringList 和 integerList 的类型实际上是相同的(都是 ArrayList),因为泛型信息被擦除。
虽然List<String>和List<Integer>在运行时都是 List,但编译时仍然会进行类型检查,因此不能将 List<String> 赋值给 List<Integer>,也不能赋值给 List<Object> 。
而且无法通过 new T() 创建泛型类型的实例。
class Box<T> {
T item = new T(); // 编译错误
}
尽管类型擦除在运行时丢失了泛型信息,还是可以在一定程度上规避这个限制,比如使用反射结合类型参数。
反射操作泛型
要想使用反射操作泛型,需要了解泛型的相关类。
java.lang.reflect.Type 表示 Java 中的所有类型,包括类、接口、基本类型、参数化类型、数组类型、类型变量和通配符类型。
public interface Type {
default String getTypeName() {
return toString();
}
}
《反射》那篇文章主要讲解了表示类和接口的Class类,下面详解介绍一下 Type 的其他子接口或实现类。
ParameterizedType 参数化类型
java.lang.reflect.ParameterizedType 是 Type 接口的一个子接口,它表示带有实际类型参数的泛型类型。
例如List<String>中的String就是一个类型参数。
ParameterizedType有两个重要的方法,分别获取List<String>中的 List 和 String:
⨳ Type[] getActualTypeArguments() : 返回参数化类型的实际类型参数数组。对于 List<String>,该方法将返回包含 String 类型的数组。
⨳ Type getRawType() : 返回不带类型参数的原始类型。对于 List<String>,getRawType() 将返回 List.class。
import java.lang.reflect.Field;
import java.lang.reflect.Type;
import java.lang.reflect.ParameterizedType;
import java.util.Map;
public class ParameterizedTypeDemo {
public Map<String,Integer> map;
public static void main(String[] args) throws NoSuchFieldException {
// 获取 ParameterizedTypeDemo 类中的字段 "map"
Field field = ParameterizedTypeDemo.class.getField("map");
// 获取字段的泛型类型
Type genericFieldType = field.getGenericType();
// 检查是否为 ParameterizedType
if (genericFieldType instanceof ParameterizedType) {
ParameterizedType parameterizedType = (ParameterizedType) genericFieldType;
// 获取实际的类型参数
Type[] actualTypeArguments = parameterizedType.getActualTypeArguments();
// 打印原始类型
System.out.println("Raw Type: " + parameterizedType.getRawType());
// 打印类型参数信息
for (Type typeArgument : actualTypeArguments) {
System.out.println("Type argument: " + typeArgument.getTypeName());
}
}
}
}
输出结果如下:
Raw Type: interface java.util.Map
Type argument: java.lang.String
Type argument: java.lang.Integer
通过 ParameterizedType 可以在运行时获取被擦除的泛型类型信息,使得更动态和灵活的代码成为可能。
TypeVariable 类型变量
java.lang.reflect.TypeVariable<D> 也是 Type 接口的一个子接口,表示类型变量。
类型变量通常是在泛型类、接口或方法中定义的占位符,用于表示未知的类型。
例如 class GenericClass<T> {} 中的 T 就是类型变量。
TypeVariable 也提供了一些方法用于获取类型变量的相关信息:
⨳ Type[] getBounds() : 返回类型变量的上界数组。默认情况下,类型变量的上界是 Object,但如果类型变量被显式地限制为某些类或接口,那么该方法将返回这些限制的类型。
⨳ D getGenericDeclaration() : 返回声明该类型变量的实体(类、接口或方法)。D 是泛型参数,通常是 Class、Method 或 Constructor。
⨳ String getName() : 返回类型变量的名称。例如,对于 T,该方法将返回 "T"。
package com.cango.generics;
import java.lang.reflect.TypeVariable;
import java.lang.reflect.Type;
public class TypeVariableDemo <T extends Number & Comparable<T>> {
public static void main(String[] args) {
// 获取 TypeVariableDemo 类的类型变量
TypeVariable<Class<TypeVariableDemo>>[] typeParameters = TypeVariableDemo.class.getTypeParameters();
for (TypeVariable<Class<TypeVariableDemo>> typeVariable : typeParameters) {
// 打印类型变量的名称
System.out.println("Type Variable Name: " + typeVariable.getName());
// 获取类型变量的上界
Type[] bounds = typeVariable.getBounds();
for (Type bound : bounds) {
System.out.println("Bound: " + bound.getTypeName());
}
}
}
}
输出结果如下:
Type Variable Name: T
Bound: java.lang.Number
Bound: java.lang.Comparable<T>
这么看类型变量和参数化类型还是不同的:TypeVariable 代表的是泛型声明中的类型变量,即泛型的占位符(如T、E、K、V),而 ParameterizedType 代表的是一个具体的参数化类型(如 List<String>),即带有实际类型参数的泛型类型。
GenericArrayType: 泛型数组类型
java.lang.reflect.GenericArrayType 也是Type 接口的一个子接口,表示泛型数组类型。
例如,T[] 或 List<String>[] 这样的类型在运行时就被表示为泛型数组类型。
它只有一个方法,用于获取数组中的组件类型:
⨳ Type getGenericComponentType() : 返回数组的组件类型。这意味着它返回的是数组中每个元素的类型,可能是普通类型或泛型类型。
package com.cango.generics;
import java.lang.reflect.Field;
import java.lang.reflect.GenericArrayType;
import java.lang.reflect.Type;
import java.util.List;
public class GenericArrayTypeDemo<T> {
public T[] genericArray;
public List<String>[] listArray;
public static void main(String[] args) throws NoSuchFieldException {
// 获取泛型数组字段
Field genericArrayField = GenericArrayTypeDemo.class.getField("genericArray");
Type genericArrayType = genericArrayField.getGenericType();
if (genericArrayType instanceof GenericArrayType) {
GenericArrayType arrayType = (GenericArrayType) genericArrayType;
Type componentType = arrayType.getGenericComponentType();
System.out.println("Generic Array Component Type: " + componentType.getTypeName());
}
// 获取带有泛型的数组字段
Field listArrayField = GenericArrayTypeDemo.class.getField("listArray");
Type listArrayType = listArrayField.getGenericType();
if (listArrayType instanceof GenericArrayType) {
GenericArrayType arrayType = (GenericArrayType) listArrayType;
Type componentType = arrayType.getGenericComponentType();
System.out.println("List Array Component Type: " + componentType.getTypeName());
}
}
}
输出结果如下:
Generic Array Component Type: T
List Array Component Type: java.util.List<java.lang.String>
T[] 是一个带有泛型组件类型的数组,其中组件类型是类型变量 T;:List<String>[] 是一个带有泛型组件类型的数组,其中组件类型是 List<String>。
WildcardType 通配符类型
java.lang.reflect.WildcardType 同样是Type 接口的一个子接口,用于表示带有通配符的泛型类型。
通配符类型就是前文讲的上下边界,例如 ? extends Number 或 ? super Integer。
通常用在泛型中,表示一种不确定的类型边界,例如 ? extends Number 或 ? super Integer。
WildcardType 有两个方法分别用于获取获取类型的上下边界:
⨳ Type[] getUpperBounds() : 返回通配符的上界数组。
⨳ Type[] getLowerBounds() : 返回通配符的下界数组。
import java.lang.reflect.Field;
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
import java.lang.reflect.WildcardType;
import java.util.List;
public class WildcardTypeDemo {
public List<? extends Number> numbers;
public static void main(String[] args) throws NoSuchFieldException {
// 获取带有 ? extends Number 的字段
Field numbersField = WildcardTypeDemo.class.getField("numbers");
Type numbersType = numbersField.getGenericType();
if (numbersType instanceof ParameterizedType) {
ParameterizedType parameterizedType = (ParameterizedType) numbersType;
Type[] actualTypeArguments = parameterizedType.getActualTypeArguments();
for (Type typeArgument : actualTypeArguments) {
if (typeArgument instanceof WildcardType) {
WildcardType wildcardType = (WildcardType) typeArgument;
// 打印上界
for (Type upperBound : wildcardType.getUpperBounds()) {
System.out.println("Upper bound: " + upperBound.getTypeName());
}
// 打印下界
for (Type lowerBound : wildcardType.getLowerBounds()) {
System.out.println("Lower bound: " + lowerBound.getTypeName());
}
}
}
}
}
}
输出结果如下:
Upper bound: java.lang.Number
总结
泛型使得 Java 代码更加灵活、通用,并且提供了类型安全和更好的代码重用性。泛型的引入减少了代码的冗余性,特别是在处理集合类和算法时非常有用。
⨳ 代码的可重用性:泛型提供了编写通用代码的能力,使得类、接口和方法可以适应不同的数据类型,而无需重复编写相同功能的代码。这显著提高了代码的可重用性。
⨳ 类型安全:使用泛型后,编译器会强制类型检查,从而防止类型转换异常(ClassCastException)。
⨳ 消除强制类型转换:在没有泛型的情况下,通常需要手动进行类型转换,而泛型通过类型推断可以消除这些强制类型转换,使代码更简洁,避免错误。
// 没有泛型之前的代码:
List list = new ArrayList();
list.add("Hello");
String item = (String) list.get(0); // 需要进行强制类型转换
使用泛型后的代码:
List<String> list = new ArrayList<>();
list.add("Hello");
String item = list.get(0); // 不需要类型转换
泛型虽然很好,但要注意由于类型擦除等机制,但这也没事,在编译时擦除的泛型可以通过反射获取,这就需要我们了解这几个与泛型相关的类,才能更加得心应手。