Java 泛型
一、简介
Java的泛型(Generics)是Java 5引入的一项重要特性,允许在定义类、接口和方法时使用类型参数,从而增强代码的类型安全性和重用性。泛型的主要目的是在编译时提供类型检查,减少运行时的类型转换错误。
1.1 基本概念
泛型允许你编写可以操作多种类型的代码,而不需要为每种类型编写单独的类或方法。通过使用类型参数,可以在编译时指定具体的类型。
1.2 泛型类
泛型类是指具有一个或多个类型参数的类。类型参数在类定义时指定,并在实例化时确定具体类型。
public class Box<T> {
private T item;
public void setItem(T item) {
this.item = item;
}
public T getItem() {
return item;
}
}
在这个例子中,Box 类有一个类型参数 T,可以在实例化时指定具体的类型:
Box<String> stringBox = new Box<>();
stringBox.setItem("Hello");
String item = stringBox.getItem(); // 不需要类型转换
1.3 泛型方法
泛型方法是指具有一个或多个类型参数的方法。类型参数在方法定义时指定,并在调用时确定具体类型。
public <T> void printArray(T[] array) {
for (T element : array) {
System.out.println(element);
}
}
在这个例子中,printArray 方法有一个类型参数 T,可以在调用时指定具体的类型:
Integer[] intArray = {1, 2, 3};
String[] strArray = {"A", "B", "C"};
printArray(intArray); // 输出: 1 2 3
printArray(strArray); // 输出: A B C
1.4 泛型的类型参数
类型参数可以是任何有效的Java标识符,但通常使用单个大写字母,如 T、E、K、V 等。常见的类型参数约定如下:
- T:表示类型(Type)
- E:表示元素(Element),常用于集合类
- K:表示键(Key),常用于映射
- V:表示值(Value),常用于映射
1.5 泛型的通配符
Java泛型支持通配符,用于表示未知类型。通配符有两种形式:?
和 ? extends T
、? super T
。
- 无界通配符
- 无界通配符
?
表示任意类型。public void printList(List<?> list) { for (Object element : list) { System.out.println(element); } }
- 无界通配符
- 上界通配符
- 上界通配符 ? extends T 表示类型参数是 T 或其子类。
public void printNumbers(List<? extends Number> list) { for (Number number : list) { System.out.println(number); } }
- 上界通配符 ? extends T 表示类型参数是 T 或其子类。
- 下界通配符
- 下界通配符 ? super T 表示类型参数是 T 或其父类。
public void addNumbers(List<? super Integer> list) { list.add(1); list.add(2); }
- 下界通配符 ? super T 表示类型参数是 T 或其父类。
1.6 泛型的应用场景
- 集合框架:Java集合框架(如 ArrayList、HashMap 等)广泛使用泛型来保证类型安全。
- 工具类:泛型可以用于编写通用的工具类,如 Comparator、Function 等。
- 设计模式:泛型可以用于实现一些设计模式,如工厂模式、策略模式等。
二、泛型的类型擦除
泛型的类型擦除(Type Erasure) 是Java泛型实现的核心机制之一。它的主要目的是在编译时确保类型安全,同时在运行时保持与Java早期版本(Java 5之前)的兼容性。类型擦除的具体表现是:泛型类型参数在编译后被替换为它们的上限(通常是 Object),并在必要时插入类型转换。
2.1 类型擦除的工作原理
- 编译时:
- 编译器会检查泛型代码的类型安全性,确保类型参数的使用是正确的。
- 在编译后的字节码中,所有的泛型类型参数都会被擦除,替换为它们的上限(如果没有指定上限,则替换为 Object)。
- 编译器会在需要的地方插入类型转换代码,以确保运行时的类型安全。
- 运行时:
- 在运行时,JVM(Java虚拟机)并不知道泛型类型参数的具体信息。所有的泛型类型都被视为它们的上限类型(如 Object)。
- 由于类型擦除,泛型类型的具体信息在运行时是不可用的。
2.2 类型擦除的具体表现
-
泛型类中的类型擦除
以下是一个泛型类的例子:public class Box<T> { private T item; public void setItem(T item) { this.item = item; } public T getItem() { return item; } }
在编译后,类型参数 T 会被擦除,替换为 Object:
public class Box { private Object item; public void setItem(Object item) { this.item = item; } public Object getItem() { return item; } }
当使用泛型类时,编译器会自动插入类型转换代码:
Box<String> stringBox = new Box<>(); stringBox.setItem("Hello"); String item = stringBox.getItem(); // 编译后:String item = (String) stringBox.getItem();
-
泛型方法中的类型擦除
以下是一个泛型方法的例子:public <T> T getFirst(List<T> list) { return list.get(0); }
在编译后,类型参数 T 会被擦除,替换为 Object:
public Object getFirst(List list) { return list.get(0); }
当调用泛型方法时,编译器会自动插入类型转换代码:
List<String> stringList = Arrays.asList("A", "B", "C"); String first = getFirst(stringList); // 编译后:String first = (String) getFirst(stringList);
-
有界类型参数的类型擦除
如果泛型类型参数有上限(如 T extends Number),类型擦除时会替换为上限类型:public class Box<T extends Number> { private T item; public void setItem(T item) { this.item = item; } public T getItem() { return item; } }
在编译后,类型参数 T 会被擦除,替换为 Number:
public class Box { private Number item; public void setItem(Number item) { this.item = item; } public Number getItem() { return item; } }
2.3 类型擦除的优点
- 兼容性:
- 类型擦除确保了Java泛型与早期版本(Java 5之前)的兼容性,使得现有的非泛型代码可以继续运行。
- 性能:
- 由于类型擦除,泛型代码在运行时不需要额外的类型信息,减少了运行时的开销。
- 简单性:
- 类型擦除简化了JVM的实现,因为JVM不需要为泛型引入新的机制。
三、泛型的限制
Java的泛型虽然极大地增强了代码的类型安全性和重用性,但也带来了一些问题和限制。
3.1 常见限制
-
不能创建泛型类型的实例:
- 由于类型擦除,运行时无法知道类型参数的具体类型,因此不能直接创建泛型类型的实例。
- 例如:
public class Box<T> { private T item; // 错误:不能直接实例化泛型类型 public Box() { this.item = new T(); // 编译错误 } }
-
不能创建泛型数组:
- 由于类型擦除,运行时无法确保数组的类型安全,因此不能直接创建泛型数组。
- 例如:
// 错误:不能创建泛型数组 T[] array = new T[10]; // 编译错误 // 替代方案:使用 ArrayList List<T> list = new ArrayList<>();
-
不能使用基本类型作为类型参数:
- 泛型类型参数必须是引用类型,不能是基本类型(如 int、char 等)。
- 例如:
// 错误:不能使用基本类型 List<int> intList = new ArrayList<>(); // 编译错误 // 必须使用包装类 List<Integer> integerList = new ArrayList<>();
-
类型擦除导致的运行时类型信息丢失:
- 由于类型擦除,运行时无法获取泛型类型参数的具体信息。
- 例如:
List<String> stringList = new ArrayList<>(); List<Integer> integerList = new ArrayList<>(); // 运行时无法区分 System.out.println(stringList.getClass() == integerList.getClass()); // 输出: true
-
泛型类型不能用于静态上下文:
- 泛型类型参数不能用于静态字段、静态方法或静态初始化块,因为静态成员属于类级别,而泛型类型参数属于实例级别。
- 例如:
public class Box<T> { // 错误:不能使用泛型类型参数 private static T staticField; // 编译错误 // 错误:不能使用泛型类型参数 public static T staticMethod() { // 编译错误 return null; } }
-
泛型与重载的冲突:
- 两个泛型方法如果只有类型参数不同,会导致编译错误,因为它们在编译后会被擦除为相同的方法签名。
- 例如:
public class Example { // 错误:方法签名冲突 public void print(List<String> list) {} public void print(List<Integer> list) {} // 编译错误 }
-
泛型与异常处理的冲突:
- 泛型类型参数不能用于 catch 块中的异常类型。
- 例如:
public class Example { public <T extends Exception> void handle(T exception) { try { throw exception; } catch (T e) { // 编译错误 // 处理异常 } } }
-
泛型与多态性的冲突:
- 泛型类型参数在继承和多态性中可能会导致一些意外行为。
- 子类无法重写父类的泛型方法时,可能会导致类型不匹配的问题。
- 泛型类型参数在继承链中可能会导致类型安全问题。
class Parent<T> { public void set(T item) {} } class Child extends Parent<String> { // 错误:不能重写父类的泛型方法 public void set(Object item) {} // 编译错误 }
-
泛型与类型转换的冲突:
- 由于类型擦除,泛型类型在运行时无法进行类型检查,可能导致 ClassCastException。
- 如果泛型类型使用不当,可能会导致运行时的类型转换异常。
List<String> stringList = new ArrayList<>(); List rawList = stringList; // 原始类型 rawList.add(10); // 编译通过,但运行时会抛出 ClassCastException String item = stringList.get(0); // 抛出 ClassCastException
-
泛型与通配符的复杂性:
- 泛型通配符(如 ?、? extends T、? super T)虽然增强了灵活性,但也增加了代码的复杂性。
- 通配符的使用可能会导致代码难以理解和维护。
- 通配符的上界和下界可能会导致类型安全问题。
public void process(List<? extends Number> list) { // 不能添加元素,因为类型未知 list.add(10); // 编译错误 }
3.2 常见的绕过泛型限制的方法
Java泛型的限制主要源于类型擦除和语言设计,虽然无法完全绕过,但可以通过一些常用方法缓解这些限制。
- 使用Class传递类型信息
- 解决问题:类型擦除导致运行时无法获取泛型的具体类型。
public class Box<T> { private Class<T> type; public Box(Class<T> type) { this.type = type; } public T createInstance() throws Exception { return type.getDeclaredConstructor().newInstance(); } }
- 使用反射创建泛型实例
- 解决问题:无法直接实例化泛型类型(如new T())。
public <T> T createInstance(Class<T> clazz) throws Exception { return clazz.getDeclaredConstructor().newInstance(); }
- 使用Object[]并进行类型转换
- 解决问题:无法直接创建泛型数组(如new T[10])。
@SuppressWarnings("unchecked") public <T> T[] createArray(Class<T> clazz, int size) { return (T[]) java.lang.reflect.Array.newInstance(clazz, size); }
- 使用带界限的通配符(如<? super T>)来允许添加元素
- 解决问题:通配符集合(如List<?>)不能添加元素。
public void addNumber(List<? super Integer> list) { list.add(42); // 允许添加 }
四、桥接方法
4.1 桥接方法的作用
Java编译器会自动生成一个桥接方法,用于在类型擦除后保持方法重写的正确性。桥接方法的作用是将泛型类型参数的具体类型转换为擦除后的类型,并调用子类的具体实现。
4.2 桥接方法的实现
- 桥接方法的生成
- 编译器会为子类生成一个桥接方法,并调用子类的具体实现。
class Child extends Parent<String> { // 子类的具体实现 @Override public void set(String item) { System.out.println("Child set: " + item); } // 编译器生成的桥接方法 public void set(Object item) { set((String) item); // 调用子类的具体实现 } }
- 这个桥接方法的作用是:
- 将 Object 类型的参数强制转换为 String 类型。
- 调用子类的 set(String item) 方法。
- 通过这种方式,桥接方法确保了 Child 类的 set 方法能够正确重写 Parent 类的 set 方法。
- 编译器会为子类生成一个桥接方法,并调用子类的具体实现。
- 桥接方法的字节码
- 查看 Child 类的字节码,可以看到桥接方法的存在:
桥接方法的字节码如下:// 子类的具体实现 public void set(java.lang.String); // 编译器生成的桥接方法 public void set(java.lang.Object);
public void set(java.lang.Object); descriptor: (Ljava/lang/Object;)V flags: ACC_PUBLIC, ACC_BRIDGE, ACC_SYNTHETIC Code: aload_0 aload_1 checkcast #2 // 强制转换为 String invokevirtual #3 // 调用子类的 set(String) 方法 return
- ACC_BRIDGE 标志表示这是一个桥接方法。
- ACC_SYNTHETIC 标志表示这个方法是由编译器生成的,不会出现在源代码中。
- 查看 Child 类的字节码,可以看到桥接方法的存在:
4.3 桥接方法的典型场景
- 泛型方法的重写
- 桥接方法主要用于泛型方法的重写场景。例如:
在编译后,Parent 类的 get 方法会被擦除为:class Parent<T> { public T get() { return null; } } class Child extends Parent<String> { @Override public String get() { return "Hello"; } }
而 Child 类的 get 方法仍然是:public Object get() { return null; }
为了确保 Child 类的 get 方法能够正确重写 Parent 类的 get 方法,编译器会生成一个桥接方法:public String get() { return "Hello"; }
class Child extends Parent<String> { @Override public String get() { return "Hello"; } // 编译器生成的桥接方法 public Object get() { return get(); // 调用子类的具体实现 } }
- 桥接方法主要用于泛型方法的重写场景。例如:
- 泛型接口的实现
- 桥接方法也用于泛型接口的实现。例如:
在编译后,MyInterface 的 set 方法会被擦除为:interface MyInterface<T> { void set(T item); } class MyClass implements MyInterface<String> { @Override public void set(String item) { System.out.println("MyClass set: " + item); } }
而 MyClass 的 set 方法仍然是:void set(Object item);
为了确保 MyClass 的 set 方法能够正确实现 MyInterface 的 set 方法,编译器会生成一个桥接方法:public void set(String item) { System.out.println("MyClass set: " + item); }
class MyClass implements MyInterface<String> { @Override public void set(String item) { System.out.println("MyClass set: " + item); } // 编译器生成的桥接方法 public void set(Object item) { set((String) item); // 调用子类的具体实现 } }
- 桥接方法也用于泛型接口的实现。例如:
五、在泛型为String的List中存放Integer对象
在Java中,泛型提供了编译时的类型安全检查,确保在泛型集合中只能存储指定类型的对象。例如,一个声明为 List 的集合只能存储 String 类型的对象。如果尝试将 Integer 对象存入 List 中,编译器会直接报错。
List<String> stringList = new ArrayList<>();
stringList.add("Hello"); // 正常
stringList.add(100); // 编译错误:无法将 Integer 添加到 List<String>
然而,在某些情况下,可以通过一些特殊手段绕过编译器的类型检查,将不兼容类型的对象存入泛型集合中。绕过方法有如下:
- 通过原始类型绕过类型检查
- 通过将泛型集合转换为原始类型,可以绕过编译器的类型检查,从而存入不兼容类型的对象。
List<String> stringList = new ArrayList<>(); List rawList = stringList; // 转换为原始类型 rawList.add(100); // 绕过类型检查,存入 Integer 对象 System.out.println(stringList); // 输出: [100]
- 通过将泛型集合转换为原始类型,可以绕过编译器的类型检查,从而存入不兼容类型的对象。
- 通过反射绕过类型检查
- Java的反射机制可以在运行时动态操作对象,包括绕过泛型的类型检查。通过反射,可以直接向泛型集合中存入不兼容类型的对象。
import java.lang.reflect.Method; import java.util.ArrayList; import java.util.List; public class Main { public static void main(String[] args) throws Exception { List<String> stringList = new ArrayList<>(); // 获取 List 的 add 方法 Method addMethod = stringList.getClass().getMethod("add", Object.class); // 使用反射调用 add 方法,存入 Integer 对象 addMethod.invoke(stringList, 100); System.out.println(stringList); // 输出: [100] } }
- Java的反射机制可以在运行时动态操作对象,包括绕过泛型的类型检查。通过反射,可以直接向泛型集合中存入不兼容类型的对象。
虽然可以绕过了编译器的类型检查,但如果尝试从 List 中获取元素并强制转换为 String,会抛出异常。
String item = stringList.get(0); // 抛出 ClassCastException
正确的做法可以使用下面方法:
- 使用 List:
List<Object> list = new ArrayList<>(); list.add("Hello"); list.add(100);
- 使用泛型通配符:
List<?> list = new ArrayList<>(); list.add("Hello"); // 编译错误,但可以通过其他方式实现
六、泛型的PECS原则
PECS 是 Java 泛型中的一个重要原则,全称为 Producer Extends, Consumer Super。它是用来指导在使用泛型通配符(? extends T 和 ? super T)时的最佳实践。PECS 原则的核心思想是:
- Producer Extends:如果泛型集合是数据的生产者(即从中读取数据),使用 ? extends T。
- Consumer Super:如果泛型集合是数据的消费者(即向其中写入数据),使用 ? super T。
PECS 原则的目的是在保证类型安全的同时,最大化代码的灵活性和通用性。
6.1 Producer Extends
- 含义
- Producer 是指数据的生产者,即从集合中读取数据。
- Extends 表示使用 ? extends T,即集合中的元素类型是 T 或其子类。
- 使用场景
- 当我们需要从一个泛型集合中读取数据时,应该使用 ? extends T。这样可以确保从集合中读取的元素是 T 类型或其子类型。
public void printNumbers(List<? extends Number> numbers) { for (Number number : numbers) { System.out.println(number); } }
- List<? extends Number> 表示 numbers 是一个可以存储 Number 或其子类(如 Integer、Double)的集合。
- 我们可以安全地从 numbers 中读取元素,因为所有元素都是 Number 类型或其子类型。
- 当我们需要从一个泛型集合中读取数据时,应该使用 ? extends T。这样可以确保从集合中读取的元素是 T 类型或其子类型。
- 限制
- 由于 ? extends T 表示类型参数是 T 或其子类,因此无法向集合中添加元素(除了 null),因为编译器无法确定具体的类型。
List<? extends Number> numbers = new ArrayList<Integer>(); numbers.add(10); // 编译错误:无法添加元素 numbers.add(null); // 允许,因为 null 是所有类型的有效值
- 由于 ? extends T 表示类型参数是 T 或其子类,因此无法向集合中添加元素(除了 null),因为编译器无法确定具体的类型。
6.2 Consumer Super
- 含义
- Consumer 是指数据的消费者,即向集合中写入数据。
- Super 表示使用 ? super T,即集合中的元素类型是 T 或其父类。
- 使用场景
- 当我们需要向一个泛型集合中写入数据时,应该使用 ? super T。这样可以确保向集合中添加的元素是 T 类型或其父类型。
public void addNumbers(List<? super Integer> numbers) { numbers.add(1); numbers.add(2); }
- List<? super Integer> 表示 numbers 是一个可以存储 Integer 或其父类(如 Number、Object)的集合。
- 我们可以安全地向 numbers 中添加 Integer 类型的元素,因为 Integer 是 ? super Integer 的子类型。
- 当我们需要向一个泛型集合中写入数据时,应该使用 ? super T。这样可以确保向集合中添加的元素是 T 类型或其父类型。
- 限制
- 由于 ? super T 表示类型参数是 T 或其父类,因此无法从集合中读取元素(除了 Object),因为编译器无法确定具体的类型。
List<? super Integer> numbers = new ArrayList<Number>(); numbers.add(10); // 允许 Integer number = numbers.get(0); // 编译错误:无法读取元素 Object obj = numbers.get(0); // 允许,因为所有类型都是 Object 的子类
- 由于 ? super T 表示类型参数是 T 或其父类,因此无法从集合中读取元素(除了 Object),因为编译器无法确定具体的类型。
6.3 PECS 原则的综合应用
-
集合的复制
- 将一个集合中的元素复制到另一个集合中:
public static <T> void copy(List<? super T> dest, List<? extends T> src) { for (T item : src) { dest.add(item); } }
- src 是数据的生产者,因此使用 ? extends T。
- dest 是数据的消费者,因此使用 ? super T。
- 将一个集合中的元素复制到另一个集合中:
-
集合的最大值
- 查找集合中的最大值:
public static <T extends Comparable<? super T>> T max(List<? extends T> list) { if (list.isEmpty()) { throw new IllegalArgumentException("List is empty"); } T max = list.get(0); for (T item : list) { if (item.compareTo(max) > 0) { max = item; } } return max; }
- List<? extends T> 用于读取数据。
- Comparable<? super T> 确保 T 或其父类实现了 Comparable 接口。
- 查找集合中的最大值: