本篇我们来讲解 Java 泛型~
1. 泛型是什么?为什么要用泛型?
- 核心概念 :泛型是 JDK 5 引入的特性,允许在定义类、接口或方法时使用类型参数 (Type Parameter)。你可以将这个类型参数看作一个占位符,表示某种具体的类型,但具体是什么类型,要到使用这个类、接口或方法时才指定。
- 解决的问题 :
-
类型安全 :在 JDK 5 之前,集合类(如
ArrayList,HashMap)默认存储Object类型。当你从集合中取出元素时,需要强制转型(Casting)为你期望的类型。如果实际存储的类型与你强制转型的目标类型不匹配,就会在运行时 抛出ClassCastException。泛型在编译时 就检查类型是否匹配,大大减少了这类运行时错误。-
没有泛型的问题示例 :
javaArrayList list = new ArrayList(); // 存储 Object list.add("Hello"); list.add(123); // 不小心存入了 Integer String str = (String) list.get(1); // 编译通过,但运行时抛出 ClassCastException! -
使用泛型的改进 :
javaArrayList<String> list = new ArrayList<>(); // 指定存储 String list.add("Hello"); // list.add(123); // 编译错误!编译器阻止存入 Integer String str = list.get(0); // 无需强制转型,直接是 String
-
-
消除强制转型:如上例所示,使用泛型后,从集合中取出元素时不需要再进行强制转型,代码更简洁清晰。
-
提高代码复用性 :你可以编写适用于多种类型的通用代码。例如,一个
List<T>接口可以用于存放任何类型T的元素,而不需要为String、Integer等分别编写StringList、IntegerList。
-
2. 泛型类
-
定义 :在类名后面用尖括号
<>声明一个或多个类型参数。这些参数可以在类体中像普通类型一样使用(作为字段类型、方法参数类型、方法返回类型等)。 -
语法 :
javapublic class ClassName<T1, T2, ..., Tn> { // 类体可以使用 T1, T2, ..., Tn } -
实例化 :创建泛型类的对象时,在类名后的尖括号
<>中指定具体的类型参数(称为类型实参 ,Type Argument)。javaClassName<具体类型1, 具体类型2, ..., 具体类型n> obj = new ClassName<>(); -
示例 :定义一个简单的泛型
Box类javapublic class Box<T> { private T content; // T 表示某种类型的内容 public void setContent(T content) { this.content = content; } public T getContent() { return content; } } -
使用 :
javaBox<String> stringBox = new Box<>(); // T 被指定为 String stringBox.setContent("Hello Generics!"); String message = stringBox.getContent(); // 直接是 String,无需转型 Box<Integer> intBox = new Box<>(); // T 被指定为 Integer intBox.setContent(42); int number = intBox.getContent(); // 自动拆箱为 int -
注意 :
- 泛型类可以有多个类型参数,如
Pair<K, V>。 - 类型参数通常用单个大写字母表示(如
T、E、K、V),但这只是约定俗成。 - 实例化时,构造函数后的
<>称为菱形语法(Diamond Operator),允许省略类型实参(编译器会根据声明推断)。
- 泛型类可以有多个类型参数,如
3. 泛型接口
-
定义:与泛型类类似,在接口名后声明类型参数。
-
语法 :
javapublic interface InterfaceName<T1, T2, ..., Tn> { // 接口方法可以使用 T1, T2, ..., Tn } -
实现 :
-
方式一 :实现类在实现接口时指定具体类型。
javapublic interface Producer<T> { T produce(); } public class StringProducer implements Producer<String> { @Override public String produce() { return "Generated String"; } } -
方式二 :实现类本身也声明为泛型类,类型参数与接口一致。
javapublic class GenericProducer<T> implements Producer<T> { @Override public T produce() { // ... 生产 T 类型对象的逻辑 ... return result; } }- 使用时再指定具体类型:
GenericProducer<Integer> intProducer = new GenericProducer<>();
- 使用时再指定具体类型:
-
4. 泛型方法
-
定义:在方法签名上声明类型参数,该方法可以在不同类型上操作。
-
语法 :在方法的返回类型之前(或修饰符之后)用尖括号
<>声明类型参数。javapublic <T1, T2, ..., Tn> 返回类型 方法名(参数列表) { // 方法体可以使用 T1, T2, ..., Tn } -
特点 :
- 类型参数的作用域仅限于该方法本身。
- 泛型方法可以定义在普通类中,也可以定义在泛型类中(此时,泛型方法的类型参数可以与类的类型参数同名但含义不同)。
- 编译器通常能根据传入的参数类型推断出类型实参。
-
示例 :
javapublic class Util { // 泛型方法:交换数组中两个元素的位置 public static <T> void swap(T[] array, int i, int j) { T temp = array[i]; array[i] = array[j]; array[j] = temp; } // 泛型方法:查找数组中最大值 (要求 T 实现了 Comparable<T>) public static <T extends Comparable<T>> T max(T[] array) { if (array == null || array.length == 0) return null; T maxVal = array[0]; for (T element : array) { if (element.compareTo(maxVal) > 0) { maxVal = element; } } return maxVal; } } -
使用 :
javaInteger[] intArray = {1, 5, 3, 2}; Util.swap(intArray, 1, 2); // <Integer> 被编译器推断出来 Integer maxInt = Util.max(intArray); // 同样推断出 <Integer> String[] strArray = {"apple", "banana", "cherry"}; String maxStr = Util.max(strArray); // 推断出 <String> -
注意 :示例中的
<T extends Comparable<T>>是类型边界 (Type Bound),用于约束T必须是实现了Comparable<T>接口的类型,这样方法体内才能安全地调用compareTo方法。我们稍后会详细讲解边界。
5. 类型擦除
-
关键机制 :Java 泛型是通过类型擦除 (Type Erasure)实现的。这意味着:
- 在编译时 ,编译器会检查泛型代码的类型安全(确保你放入
List<String>的是String)。 - 在编译后 ,生成的字节码(.class 文件)中,所有的类型参数都会被擦除掉,替换成它们的上界 (如果没有指定上界,则替换成
Object),并在必要的地方插入强制转型。
- 在编译时 ,编译器会检查泛型代码的类型安全(确保你放入
-
目的:为了兼容 JDK 5 之前的代码(非泛型集合类)。
-
示例 :
java// 源代码 (编译前) List<String> list = new ArrayList<>(); list.add("Hi"); String s = list.get(0); // 经过类型擦除后的等效代码 (编译后,近似表示) List list = new ArrayList(); // 类型参数 <String> 被擦除 list.add("Hi"); // 添加 String 没问题 String s = (String) list.get(0); // 编译器插入的强制转型 -
影响 :
- 无法获取运行时类型参数 :例如
List<String>.class或new T()都是不合法的,因为运行时T已经不存在了(被擦除为Object或边界类型)。 - 不能创建参数化类型的数组 :如
new List<String>[10]通常会导致编译警告或错误,因为数组需要确切知道其元素类型,而擦除后List<String>和List<Integer>在运行时都是List,数组无法区分。 - 泛型类的不同实例化共享同一个类 :
Box<String>.class == Box<Integer>.class结果为true。
- 无法获取运行时类型参数 :例如
6. 通配符: ?
-
目的:增加泛型的灵活性,表示"未知类型"。主要用于方法参数、局部变量,有时也用于字段。
-
类型 :
-
无界通配符
<?>:表示任何类型。-
用途 :当你编写的方法只需要读取集合元素(作为
Object或某个公共父类使用),而不关心具体类型时。 -
示例 :
javapublic static void printList(List<?> list) { for (Object obj : list) { // 元素被当作 Object 处理 System.out.println(obj); } // list.add(new Object()); // 错误!不能添加 (除了 null),因为不知道具体类型 } -
限制 :不能向声明为
List<?>的变量添加除null以外的任何元素(因为你不知道里面具体是什么类型)。
-
-
上界通配符
<? extends UpperBound>:表示UpperBound类型或其子类型 。-
用途 :支持协变(Covariance)。你可以安全地从这样的结构中读取 元素(读取的元素至少是
UpperBound类型),但通常不能添加 元素(除了null)。 -
示例 :
javapublic static double sumOfList(List<? extends Number> list) { double sum = 0.0; for (Number num : list) { // 安全读取,每个元素都是 Number 或其子类 sum += num.doubleValue(); } return sum; // list.add(new Integer(1)); // 错误!不能添加,可能是 List<Double> } List<Integer> intList = Arrays.asList(1, 2, 3); List<Double> doubleList = Arrays.asList(1.1, 2.2, 3.3); sumOfList(intList); // OK, Integer extends Number sumOfList(doubleList); // OK, Double extends Number
-
-
下界通配符
<? super LowerBound>:表示LowerBound类型或其父类型 。-
用途 :支持逆变(Contravariance)。你可以安全地写入 元素(写入的元素是
LowerBound或其子类),但读取时只能当作Object(因为不知道具体父类是什么)。 -
示例 :
javapublic static void addNumbers(List<? super Integer> list) { for (int i = 1; i <= 5; i++) { list.add(i); // 安全写入,Integer 是 LowerBound } // Integer num = list.get(0); // 错误!读取出来可能是 Number 或 Object Object obj = list.get(0); // 只能当作 Object 读取 } List<Number> numList = new ArrayList<>(); List<Object> objList = new ArrayList<>(); addNumbers(numList); // OK, Number super Integer addNumbers(objList); // OK, Object super Integer
-
-
-
PECS 原则 :Producer-Extends, Consumer-Super。
- 如果你需要一个结构提供 (生产)元素(
Producer),使用<? extends T>。 - 如果你需要一个结构接受 (消费)元素(
Consumer),使用<? super T>。 - 如果一个结构同时生产 和 消费,你可能需要使用确切的类型参数
T。
- 如果你需要一个结构提供 (生产)元素(
7. 类型边界
-
目的 :约束类型参数可以代表哪些类型。使用
extends关键字(在泛型中,extends可以表示类继承或接口实现)。 -
语法 :
- 单个边界 :
<T extends ClassOrInterface> - 多个边界 :
<T extends ClassA & InterfaceB & InterfaceC>(类只能有一个且必须在第一个,接口可以有多个)。
- 单个边界 :
-
示例 :
java// T 必须是 Number 或其子类 public class NumericBox<T extends Number> { private T value; // ... getter, setter ... public double getValueAsDouble() { return value.doubleValue(); // 安全调用,因为 T 是 Number } } // T 必须实现 Comparable 接口,并且能够和自己比较 (Comparable<T>) public static <T extends Comparable<T>> T max(T a, T b) { return (a.compareTo(b) > 0) ? a : b; } // 多个边界:T 必须是 Serializable 的子类 且 实现 Comparable public class SerializableComparable<T extends Serializable & Comparable<T>> { // ... } -
注意 :边界在编译时被检查,确保类型安全。类型擦除时,类型参数会被替换为其最左边的边界 (或
Object如果没有边界)。
8. 泛型在继承和子类型中的规则
- 泛型类本身 :
Box<Number>和Box<Integer>没有 继承关系。即使Integer是Number的子类,Box<Integer>也不是Box<Number>的子类。 - 通配符与子类型 :
List<? extends Number>是List<?>的子类型。List<Number>是List<? super Number>的子类型? (不是直接的父子关系,但List<Number>可以赋值给List<? super Number>变量)- 更重要的关系由通配符捕获:
List<Integer>可以赋值给List<? extends Number>变量(因为Integer extends Number)。 List<Number>可以赋值给List<? super Integer>变量(因为Number super Integer)。
9. 边界用例和限制
-
不能实例化类型参数 :
new T()是非法的,因为运行时T被擦除。- 变通方法 :通过反射(需要
Class<T>clazz 参数)或工厂模式。
- 变通方法 :通过反射(需要
-
不能用于静态上下文 :类的类型参数不能用于静态方法或静态字段,因为静态成员属于类,而类型参数属于实例。
javapublic class Box<T> { // private static T staticField; // 错误! // public static T staticMethod() { ... } // 错误! public static <U> U genericStaticMethod(U u) { ... } // OK,泛型方法有自己的类型参数 } -
不能创建基本类型的参数化类型 :泛型类型参数必须是引用类型。不能有
List<int>,只能用List<Integer>。自动装箱/拆箱缓解了这个问题。 -
不能抛出或捕获泛型类的实例 :
catch (T e)是不允许的。泛型类也不能直接或间接继承Throwable。 -
方法重载冲突 :类型擦除可能导致两个方法签名在编译后变得相同,引起编译错误。
javapublic class Example { public void print(List<String> list) { ... } public void print(List<Integer> list) { ... } // 编译错误!擦除后都是 print(List) }
总结
泛型通过类型参数、类型擦除、通配符和类型边界等机制,提供了强大的类型安全性和代码复用能力。理解类型擦除是深入掌握泛型行为的关键,而通配符(尤其是 extends 和 super)则提供了处理不同类型集合时的灵活性。遵循 PECS 原则有助于正确使用通配符。虽然泛型有一些限制(主要是由类型擦除带来的),但它们极大地提升了 Java 程序的健壮性和可读性。