Java中的泛型(Generics)是一种在编译时提供类型安全检查的机制,允许开发者编写灵活且可重用的代码。泛型的核心思想是参数化类型,即定义类、接口或方法时使用类型参数,使用时再指定具体类型。
目录
作用
- 类型不安全
//在 Java 5 之前,集合类(如 ArrayList、HashMap)只能存储 Object 类型的数据。 //这意味着你可以将任何类型的对象放入集合中,但在取出时需要强制类型转换。 //如果类型转换错误,会导致运行时异常(ClassCastException)。 //在没有泛型的情况下,类型错误只能在运行时被发现,而不是在编译时提示错误。 List list = new ArrayList(); list.add("Hello"); list.add(123); // 可以放入不同类型的对象 String str = (String) list.get(0); // 需要强制类型转换 Integer num = (Integer) list.get(1); // 需要强制类型转换 String wrongStr = (String) list.get(1); // 运行时抛出 ClassCastException //----------------------------使用泛型--------------------------- List<String> list = new ArrayList<>(); list.add("Hello"); // list.add(123); // 编译错误,类型不匹配 String str = list.get(0); // 无需强制类型转换
- 代码冗余
//如果没有泛型,开发者需要为每种类型编写单独的类或方法。 //例如,如果要实现一个存储 String 和 Integer 的容器,可能需要分别编写 StringBox 和 IntegerBox。 class StringBox { private String content; public void setContent(String content) { this.content = content; } public String getContent() { return content; } } class IntegerBox { private Integer content; public void setContent(Integer content) { this.content = content; } public Integer getContent() { return content; } } //----------------------------使用泛型--------------------------- class Box<T> { private T content; public void setContent(T content) { this.content = content; } public T getContent() { return content; } } Box<String> stringBox = new Box<>(); stringBox.setContent("Hello"); Box<Integer> intBox = new Box<>(); intBox.setContent(123);
使用方法
- 泛型类
//在类名后添加类型参数 <T>,T 是占位符,可替换为任何非基本类型。 public class Box<T> { private T content; public void setContent(T content) { this.content = content; } public T getContent() { return content; } public static void main(String[] args) { Box<String> stringBox = new Box<>(); stringBox.setContent("Hello"); System.out.println(stringBox.getContent()); // 输出: Hello Box<Integer> intBox = new Box<>(); intBox.setContent(123); System.out.println(intBox.getContent()); // 输出: 123 } }
- 泛型方法
//在返回类型前定义类型参数 <T>,方法可以独立于类的泛型参数。 public class ArrayUtils { // 泛型方法:打印任意类型数组 public static <T> void printArray(T[] array) { for (T element : array) { System.out.print(element + " "); } System.out.println(); } public static void main(String[] args) { Integer[] intArr = {1, 2, 3}; String[] strArr = {"A", "B", "C"}; ArrayUtils.printArray(intArr); // 输出: 1 2 3 ArrayUtils.printArray(strArr); // 输出: A B C } }
- 泛型接口
//接口定义类型参数,实现类需指定具体类型。 public interface Pair<K, V> { K getKey(); V getValue(); } public class SimplePair<K, V> implements Pair<K, V> { private K key; private V value; public SimplePair(K key, V value) { this.key = key; this.value = value; } @Override public K getKey() { return key; } @Override public V getValue() { return value; } public static void main(String[] args) { Pair<String, Integer> pair = new SimplePair<>("Age", 25); System.out.println(pair.getKey() + ": " + pair.getValue()); // 输出: Age: 25 } }
通配符
泛型通配符(?
)用于增加泛型的灵活性,常用于通用API编写,减少代码冗余。
- 上界通配符
<? extends T>
//表示参数可以是 T 或其子类的集合 public static double sumOfList(List<? extends Number> list) { double sum = 0.0; for (Number num : list) { sum += num.doubleValue(); } return sum; } public static void main(String[] args) { List<Integer> integers = List.of(1, 2, 3); System.out.println(sumOfList(integers)); // 输出: 6.0 }
- 下界通配符
<? super T>
//表示参数可以是 T 或其父类的集合。 public static void addNumbers(List<? super Integer> list) { for (int i = 1; i <= 5; i++) { list.add(i); } } public static void main(String[] args) { List<Number> numbers = new ArrayList<>(); addNumbers(numbers); System.out.println(numbers); // 输出: [1, 2, 3, 4, 5] }
- PECS原则
PECS 原则(Producer Extends, Consumer Super)用于指导在使用泛型集合时如何正确选择通配符(<? extends T>
和 <? super T>
)。核心思想是:
-
Producer Extends:如果集合是生产者(提供数据,即可读取),使用
<? extends T>
。//当集合是生产者(即从集合中读取数据)时,使用 <? extends T>。 //这表示集合中的元素是 T 或其子类型,因此可以安全地从集合中读取数据。 public void processNumbers(List<? extends Number> numbers) { for (Number number : numbers) { System.out.println(number); } } //不能向 <? extends T> 集合中添加元素(除了 null) //当你尝试向 List<? extends Double> 中添加元素时,例如: List<? extends Double> numbers = new ArrayList<>(); numbers.add(new Double(1)); //List<? extends Double> 可能是 List<Double>,也可能是 List<MyDouble>(假设 MyDouble 是 Double 的子类) //如果 numbers 实际上是 List<MyDouble>,那么添加 Double 类型的元素就会破坏类型安全,因为 Double 不是 MyDouble 的子类。 //为了避免这种潜在的类型安全问题,Java 禁止向 List<? extends Double> 中添加任何元素(除了 null,因为 null 是所有引用类型的默认值)。
-
Consumer Super:如果集合是消费者(消费数据,即可添加),使用
<? super T>
。//当集合是消费者(即向集合中写入数据)时,使用 <? super T>。 //这表示集合中的元素是 T 或其父类型,因此可以安全地向集合中添加 T 类型的元素。 public void addIntegers(List<? super Integer> integers) { integers.add(1); integers.add(2); } //不能从 <? super T> 集合中读取具体类型的元素(只能读取 Object 类型),因为编译器无法确定具体的父类型。
-
PECS的应用案例
//java.util.Collections 中的 copy 方法就使用了 PECS 原则: //dest 是消费者(接受数据),因此使用 <? super T>。 //src 是生产者(提供数据),因此使用 <? extends T>。 //该方法将src中的内容拷贝到dest中 public static <T> void copy(List<? super T> dest, List<? extends T> src) { for (int i = 0; i < src.size(); i++) { dest.set(i, src.get(i)); } }
类型擦除
- 含义
类型擦除(Type Erasure)是 Java 泛型实现的一种机制。在编译时,泛型类型参数会被替换为它们的上界(通常是 Object
),并且在使用泛型的地方会插入强制类型转换,并在字节码中移除所有泛型类型信息。这意味着泛型类型信息在运行时是不可用的。
-
泛型类:泛型类中的类型参数会被替换为
Object
或指定的上界。 -
泛型方法:泛型方法的类型参数会被替换为
Object
或指定的上界。 -
泛型类型检查:类型检查仅在编译时进行,运行时无法获取泛型类型信息。
// 编译前 public class Box<T> { private T content; public void setContent(T content) { this.content = content; } public T getContent() { return content; } } public class Main { public static void main(String[] args) { Box<Integer> box = new Box<>(); box.setContent(123); Integer boxValue = box.getContent(); System.out.println(boxValue); } } // 编译后(类型擦除) public class Box { private Object content; // T 被替换为 Object public void setContent(Object content) { this.content = content; } public Object getContent() { return content; } } public class Main { public static void main(String[] args) { Box box = new Box(); // 泛型类型信息被擦除 box.setContent(123); // 自动装箱,int -> Integer // 编译器插入强制类型转换 Integer boxValue = (Integer) box.getContent(); // Object -> Integer System.out.println(boxValue); } }
- 原因
Java 泛型是在 Java 5 中引入的,而在此之前,Java 集合类(如 ArrayList、HashMap)都是基于 Object 实现的。为了确保旧版本的代码能够在新版本的 JVM 上运行,Java 采用了类型擦除机制,使得泛型代码在编译后与旧版本的非泛型代码兼容。
同时,类型擦除避免了在运行时维护泛型类型信息,从而减少了 JVM 的负担。
- 局限性
- 无法获取泛型类型信息
//由于类型信息在运行时被擦除,无法直接获取泛型类型参数的具体类型。 public class Box<T> { private T content; public void setContent(T content) { this.content = content; } public T getContent() { return content; } } Box<String> box = new Box<>(); System.out.println(box.getClass().getTypeParameters()[0]); // 输出: T,而不是 String
- 无法实例化泛型类型
//由于类型擦除,无法直接实例化泛型类型参数。 public class Box<T> { private T content; public Box() { // this.content = new T(); // 编译错误,无法实例化泛型类型 } }
- 无法重载泛型方法
//由于类型擦除,以下代码会导致编译错误,因为擦除后的方法签名相同。 public class Utils { public void print(List<String> list) {} public void print(List<Integer> list) {} // 编译错误,方法签名冲突 }
- 无法使用基本类型作为泛型参数
//Java 泛型是通过 类型擦除(Type Erasure) 实现的。在编译时,泛型类型参数会被替换为它们的上界(通常是 Object)。由于基本类型不是对象,它们不能作为 Object 的子类,因此无法直接用于泛型。 List<int> list = new ArrayList<>(); // 编译错误 List<Integer> list = new ArrayList<>(); // 正确
- 无法获取泛型类型信息