Java 泛型 (Generics) 是在 JDK 5.0 中引入的一项重要语言特性,它允许在定义类、接口和方法时,使用类型参数 (Type Parameters) 来表示不确定的类型。这样,编译器可以在编译时对类型进行检查,从而在运行时避免 ClassCastException 等类型转换异常,提高了代码的类型安全性 (Type Safety) 、重用性 (Reusability) 和可读性 (Readability) 。
核心思想:Java 泛型通过引入类型参数,使得代码可以操作各种类型的数据而无需运行时强制类型转换,从而在编译时提供了更强的类型检查,减少了运行时错误,并提升了代码的通用性和安全性。
一、为什么需要泛型? 在泛型出现之前,Java 集合框架(如 ArrayList, HashMap)可以存放任何类型的对象,因为它们操作的是 Object 类型。这带来了两个主要问题:
类型不安全 :编译器无法检查集合中存储的实际类型。如果从集合中取出一个对象并强制转换为不正确的类型,就会在运行时抛出 ClassCastException。
代码冗余 :每次从集合中取出对象时,都需要进行强制类型转换,代码显得冗长且易错。
示例:没有泛型的问题
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 import java.util.ArrayList;import java.util.List;public class LegacyListExample { public static void main (String[] args) { List list = new ArrayList (); list.add("Hello" ); list.add(123 ); String s1 = (String) list.get(0 ); System.out.println(s1); } }
泛型通过在编译时捕获这些类型错误,解决了上述问题。
二、泛型的基本语法 泛型的核心思想是类型参数化 ,即在定义类、接口或方法时,使用一个或多个类型参数来代替实际的类型。类型参数通常用单个大写字母表示,如 E (Element), T (Type), K (Key), V (Value), N (Number) 等。
2.1 泛型类 (Generic Class) 定义一个类时,可以在类名后使用 <T> 来声明一个或多个类型参数。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 class Box <T> { private T content; public Box (T content) { this .content = content; } public T getContent () { return content; } public void setContent (T content) { this .content = content; } public <U> void inspect (U otherContent) { System.out.println("T: " + content.getClass().getName()); System.out.println("U: " + otherContent.getClass().getName()); } } public class GenericClassDemo { public static void main (String[] args) { Box<String> stringBox = new Box <>("Hello Generics" ); String message = stringBox.getContent(); System.out.println("String Box Content: " + message); Box<Integer> integerBox = new Box <>(123 ); Integer number = integerBox.getContent(); System.out.println("Integer Box Content: " + number); stringBox.inspect(456.78 ); } }
2.2 泛型接口 (Generic Interface) 接口也可以声明类型参数,其实现类在实现泛型接口时,可以指定具体的类型,也可以继续保持泛型。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 interface Generator <T> { T next () ; } class StringGenerator implements Generator <String> { private String[] data = {"Apple" , "Banana" , "Cherry" }; private int index = 0 ; @Override public String next () { if (index < data.length) { return data[index++]; } return null ; } } class RandomGenerator <T> implements Generator <T> { private List<T> items; private java.util.Random random = new java .util.Random(); public RandomGenerator (List<T> items) { this .items = items; } @Override public T next () { if (items.isEmpty()) { return null ; } return items.get(random.nextInt(items.size())); } } public class GenericInterfaceDemo { public static void main (String[] args) { StringGenerator sg = new StringGenerator (); System.out.println("Next string: " + sg.next()); System.out.println("Next string: " + sg.next()); List<Integer> numbers = List.of(1 , 2 , 3 , 4 , 5 ); RandomGenerator<Integer> rg = new RandomGenerator <>(numbers); System.out.println("Random number: " + rg.next()); } }
2.3 泛型方法 (Generic Method) 在方法签名中声明类型参数,使其可以独立于类本身的泛型类型。
类型参数 T 放在方法返回类型之前。
静态方法也可以是泛型方法,因为它们不依赖于类的实例。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 public class GenericMethodDemo { public static <E> void printArray (E[] inputArray) { for (E element : inputArray) { System.out.printf("%s " , element); } System.out.println(); } public static <T extends Comparable <T>> T maximum (T x, T y, T z) { T max = x; if (y.compareTo(max) > 0 ) { max = y; } if (z.compareTo(max) > 0 ) { max = z; } return max; } public static void main (String[] args) { Integer[] intArray = {1 , 2 , 3 , 4 , 5 }; Double[] doubleArray = {1.1 , 2.2 , 3.3 , 4.4 }; String[] stringArray = {"Hello" , "World" , "Generics" }; printArray(intArray); printArray(doubleArray); printArray(stringArray); System.out.println("Max Integer: " + maximum(3 , 5 , 2 )); System.out.println("Max Double: " + maximum(6.6 , 8.8 , 7.7 )); System.out.println("Max String: " + maximum("apple" , "orange" , "banana" )); } }
三、类型通配符 (Wildcards) 类型通配符 (?) 用于处理泛型类型之间的兼容性问题,特别是当泛型类作为方法参数时。它表示未知类型。
3.1 无界通配符 (<?>) 表示任意类型,等同于 <Object>。通常用于:
你不知道或不关心集合中存储的类型。
对集合进行只读操作时 (因为你不知道具体类型,所以不能安全地添加元素)。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 import java.util.Arrays;import java.util.List;public class UnboundedWildcardDemo { public static void printList (List<?> list) { for (Object elem : list) { System.out.print(elem + " " ); } System.out.println(); } public static void main (String[] args) { List<Integer> li = Arrays.asList(1 , 2 , 3 ); List<String> ls = Arrays.asList("one" , "two" , "three" ); printList(li); printList(ls); } }
3.2 上界通配符 (<? extends T>) 表示类型必须是 T 或 T 的子类。
读取数据 (Producer Extends) :你可以从 List<? extends T> 中读取类型为 T 或其子类的元素。
不能添加数据 :因为编译器不知道 ? 的具体子类型,为了保证类型安全,除了 null 之外,不能向其中添加任何元素。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 import java.util.ArrayList;import java.util.List;class Shape { }class Circle extends Shape { }class Rectangle extends Shape { }public class UpperBoundedWildcardDemo { public static double sumAreas (List<? extends Shape> shapes) { double totalArea = 0 ; for (Shape shape : shapes) { System.out.println("Processing shape: " + shape.getClass().getSimpleName()); } return totalArea; } public static void main (String[] args) { List<Circle> circles = new ArrayList <>(); circles.add(new Circle ()); circles.add(new Circle ()); List<Rectangle> rectangles = new ArrayList <>(); rectangles.add(new Rectangle ()); sumAreas(circles); sumAreas(rectangles); List<Shape> shapes = new ArrayList <>(); shapes.add(new Circle ()); shapes.add(new Rectangle ()); sumAreas(shapes); } }
3.3 下界通配符 (<? super T>) 表示类型必须是 T 或 T 的父类。
添加数据 (Consumer Super) :你可以向 List<? super T> 中添加类型为 T 或 T 的子类的元素。
读取数据 :从列表中读取出的元素只能被当作 Object 处理 (因为你只知道它是 T 的父类,具体是哪个父类不确定)。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 import java.util.ArrayList;import java.util.List;public class LowerBoundedWildcardDemo { public static void addIntegerToList (List<? super Integer> list) { list.add(10 ); list.add(20 ); Object o = list.get(0 ); System.out.println("Added integers. First element as Object: " + o); } public static void main (String[] args) { List<Integer> ints = new ArrayList <>(); addIntegerToList(ints); System.out.println("Ints: " + ints); List<Number> numbers = new ArrayList <>(); addIntegerToList(numbers); System.out.println("Numbers: " + numbers); List<Object> objects = new ArrayList <>(); addIntegerToList(objects); System.out.println("Objects: " + objects); } }
3.4 PECS 原则 (Producer-Extends, Consumer-Super) 这是一个用于指导何时使用 extends 和 super 通配符的助记符:
如果你需要从泛型结构中读取 (Produce) 数据,使用 <? extends T>。
如果你需要向泛型结构中写入 (Consume) 数据,使用 <? super T>。
四、泛型擦除 (Type Erasure) Java 泛型是编译时类型检查 的,这意味着在运行时,所有的泛型信息都会被擦除 (Erasure) 。JVM 看到的只是原始类型 (Raw Type),通常是类型参数的第一个上界(如果没有指定上界,则为 Object)。
这意味着:
无法在运行时获取泛型类型信息 :例如,你不能在运行时判断 List<String> 和 List<Integer> 是不同的类型。它们都被擦除为 List。1 2 3 List<String> stringList = new ArrayList <>(); List<Integer> integerList = new ArrayList <>(); System.out.println(stringList.getClass() == integerList.getClass());
不能创建泛型类型的实例 :new T() 是不允许的。
不能创建泛型数组 :new T[size] 是不允许的。
instanceof 运算符不能用于泛型类型 :obj instanceof List<String> 是编译错误的。
泛型方法重载 :由于擦除,void method(List<String> list) 和 void method(List<Integer> list) 在编译后签名相同,导致无法重载。
泛型擦除的实现原理 :
类型参数替换 :所有类型参数都被替换为其第一个上界(如果没有指定,则为 Object)。
插入强制类型转换 :在从泛型结构中取出元素的地方,编译器会自动插入必要的强制类型转换,以确保类型安全。
graph TD
A[Java 源代码 with Generics]
B[编译时 Type Checking] --> C[JVM 字节码 <br>without Generics]
C --类型擦除 (Type Erasure)--> D["运行时 Raws Types <br>(Object/Bound)"]
A -- "List<String> list = new ArrayList<>()" --> B
B -- "list.add('hello')" --> C
C -- "(String) list.get(0)" <br>插入强制类型转换--> D
五、泛型的一些限制 除了类型擦除带来的限制外,泛型还有其他一些使用限制:
不能用基本数据类型实例化泛型 :List<int> 是不允许的,必须使用包装类,如 List<Integer>。
不能捕获泛型类的实例 :catch (MyException<T> e) 是不允许的。
不能在静态字段上使用泛型类型参数 :static T instance; 是不允许的,因为静态成员属于类,与任何特定的泛型实例无关。
泛型类型不能是异常类 :class MyException<T> extends Exception 是不允许的。
六、总结 Java 泛型是现代 Java 编程中不可或缺的一部分,它通过在编译时引入类型检查,极大地提升了代码的类型安全性、可读性和重用性。虽然其底层的类型擦除机制带来了一些限制,但通过合理利用类型参数、通配符(特别是遵循 PECS 原则),以及理解其工作原理,开发者可以编写出更健壮、更灵活的泛型代码。掌握泛型是编写高质量 Java 代码的关键技能之一。