1. “原始”集合(raw types)的问题
简单回顾一下历史。直到 Java 5,所有集合都是“来者不拒”的。它们存放的是 Object,编译器并不会检查你到底往里放了什么。想放字符串?可以。数字?也行。Cat?也能放。
// “原始”集合(raw types)示例,Java 5 之前
List list = new ArrayList();
list.add("Hello");
list.add(42);
list.add(new Object());
问题在于“取出并使用”时才会暴露:
String s = (String) list.get(0); // OK,这是字符串
String s2 = (String) list.get(1); // 砰!ClassCastException
编译器不吭声,但你会在运行时得到 ClassCastException。这就像一个标着“苹果”的盒子,里面放着杯子、香蕉和刺猬。
为什么不好?
- 错误只会在运行时才暴露。
- 类型混乱:不得不手动进行类型转换(cast)。
- 代码可读性差且更危险。
解决方案——泛型(Generics)
Generics(泛型)是一种允许为类、接口和方法引入类型参数的机制。也就是说,你可以对集合说:“只存放字符串”,编译器会严格强制执行。
List<String> words = new ArrayList<>();
words.add("Hello");
words.add("World");
// words.add(42); // 编译错误!不能向 List<String> 添加 int
现在编译器不会允许你往列表里放入字符串以外的东西。错误在程序启动前的编译阶段就会被捕获。
泛型的核心思想:
为集合(以及其他结构)提供类型安全,使错误在编译期而不是在运行时被捕获。
2. 泛型语法:代码中如何表达
在尖括号中指定类型
创建集合时,请在 <> 中指定元素类型:
List<String> names = new ArrayList<>();
names.add("Alice");
names.add("Bob");
// names.add(123); // 错误:不能向字符串列表添加数字
String first = names.get(0); // 不需要类型转换!
经典用法:
- List<String> — 字符串列表
- List<Integer> — 整数列表
- Set<Double> — 浮点数集合
- Map<String, Integer> — 键为 String,值为 Integer
为什么不能只写 List?
能写,但你会失去泛型的全部好处,编译器也会给出警告:
List list = new ArrayList(); // raw type — 不推荐!
list.add("Hello");
list.add(7.5);
String s = (String) list.get(1); // Hello,ClassCastException!
现代 Java 代码一律使用泛型。
菱形操作符 <>
从 Java 7 开始,如果右侧类型可由上下文推断,可不再显式写出:
List<String> list = new ArrayList<>(); // 编译器会自行推断这里是 <String>
3. 在不同集合中使用泛型
List、Set、Map 的示例
List<Integer> numbers = new ArrayList<>();
numbers.add(10);
numbers.add(20);
Set<String> uniqueNames = new HashSet<>();
uniqueNames.add("Alice");
uniqueNames.add("Bob");
Map<String, Integer> ages = new HashMap<>();
ages.put("Alice", 23);
ages.put("Bob", 31);
自定义类的示例
class Student {
String name;
int age;
// ...
}
List<Student> students = new ArrayList<>();
students.add(new Student());
4. 实用细节
泛型的优势
类型安全。 编译器会确保集合中只出现指定类型的元素。
无需类型转换。 以前:String s = (String) list.get(0);。现在:String s = list.get(0);。
代码更可读、更可靠。 运行时的意外更少。
泛型的限制
不能使用原始类型(primitive)。 泛型只适用于对象而非原始类型(int、double、boolean)。请使用包装类:Integer、Double、Boolean。
List<Integer> numbers = new ArrayList<>();
numbers.add(10); // int 会自动装箱为 Integer(autoboxing)
关于类型擦除(type erasure)的简述
在 Java 中,泛型是通过类型擦除机制实现的:编译后,类型参数信息会被擦除,运行时 JVM 并不知道它曾是 List<String> 而不只是 List。这样做是为了向后兼容。
结果:不能使用带具体类型实参的 instanceof 来检查类型。
List<String> list = new ArrayList<>();
// if (list instanceof List<String>) { ... } // 编译错误!
试图添加其他类型的元素——编译错误
List<String> words = new ArrayList<>();
words.add("Hello");
// words.add(123); // 编译错误:incompatible types: int cannot be converted to String
Map<String, Integer> map = new HashMap<>();
map.put("Cat", 5);
// map.put(3, "Elephant"); // 错误:键必须是 String,值必须是 Integer
这很棒:错误在编译阶段就会被捕获。
不仅仅是集合
泛型也可以用于自定义的类和方法。比如一个通用的“盒子”:
class Box<T> {
private T value;
public void set(T value) { this.value = value; }
public T get() { return value; }
}
Box<String> stringBox = new Box<>();
stringBox.set("Hello");
System.out.println(stringBox.get());
Box<Integer> intBox = new Box<>();
intBox.set(42);
System.out.println(intBox.get());
在集合中使用泛型是标准做法,但你也会在其他地方见到它们,例如 Stream API 和 Optional。
5. 使用泛型时的常见错误
错误 #1:使用“原始”集合。 例如 List list = new ArrayList(); 会失去类型安全。务必指定类型参数,例如 List<String>。
错误 #2:尝试使用原始类型。 不能写 List<int>,请使用 List<Integer>。
错误 #3:从集合读取时手动进行类型转换。 如果你使用了泛型,就不需要形如 (String) list.get(i) 的转换。如果还需要——说明某处破坏了类型约束。
错误 #4:以为类型参数在运行时可用。 由于类型擦除,不能像 List<String> 那样配合具体类型参数使用 instanceof 检查。
错误 #5:在一个集合中混用不同类型。 如果声明了 List<String>,就不要尝试添加 Integer —— 编译器不会放行,这正是我们想要的。
GO TO FULL VERSION