在《Effective Java》的 第5章:泛型(Generics) 中,Joshua Bloch 强调了「优先使用泛型」这一规则,并通过丰富的实例和详细分析,展示了泛型如何改善代码的表达力、类型安全性和灵活性。泛型是 Java 的一大飞跃,使得开发者能够编写更加通用和安全的代码。
泛型的意义
泛型是 Java SE 5 引入的特性,其核心目标是:
- 增加类型安全 :在编译时就发现类型错误,而不是拖到运行时。
- 提升代码复用性:通过泛型写通用的代码逻辑,支持不同的类型。
- 增强代码的可读性和清晰度:开发者可以直接从代码签名中看出类或方法的类型要求。
- 消除大量强制类型转换:减少代码的复杂性和潜在的错误。
优先使用泛型 vs 原始类型
在 Java 中,原始类型(Raw Types) 是泛型引入之前所使用的一种写法,它不对类型进行参数化处理。例如,List
是原始类型,而 List<String>
是泛型的用法。
原始类型的缺点:
- 失去类型安全性:原始类型会允许插入任何对象,容易导致运行时的
ClassCastException
。 - 代码难以阅读和理解:用户无法通过类型声明获知集合中存储的元素类型。
为了规避这些问题,应该尽量使用泛型代替原始类型。
示例1:避免原始类型(Raw Types)
错误的原始类型使用:
java
// 原始类型允许存储任意对象,而没有类型检查
List list = new ArrayList();
list.add("Hello");
list.add(42); // 添加了一个 Integer
// 需要手动类型转换,很危险
for (Object obj : list) {
String str = (String) obj; // 运行时可能抛出 ClassCastException
System.out.println(str);
}
使用泛型的类型安全替代:
java
// 使用泛型,确保类型安全
List<String> list = new ArrayList<>();
list.add("Hello");
// list.add(42); // 编译时会报错,防止错误加入
// 遍历时无需显式类型转换
for (String str : list) {
System.out.println(str); // 类型已保证安全,运行时无风险
}
泛型的两点改进:
- 类型安全:强制集合中的元素都匹配声明的泛型类型,避免了潜在的运行时异常。
- 更好的可读性 :开发者通过
List<String>
一目了然地知道集合存储的是什么类型的元素。
示例2:改进 Stack<E>
类
Joshua Bloch 在书中展示了如何使用泛型重构一个经典的数据结构:栈(Stack)。
传统写法(非泛型版本)
在泛型引入之前,Stack
类可能设计如下:
java
// 非泛型版本的 Stack
public class Stack {
private Object[] elements;
private int size = 0;
private static final int DEFAULT_INITIAL_CAPACITY = 16;
public Stack() {
elements = new Object[DEFAULT_INITIAL_CAPACITY];
}
public void push(Object e) { // 接受任何类型对象
ensureCapacity();
elements[size++] = e;
}
public Object pop() { // 返回 Object,需要手动类型转换
if (size == 0) {
throw new EmptyStackException();
}
return elements[--size];
}
private void ensureCapacity() {
if (size == elements.length) {
elements = Arrays.copyOf(elements, 2 * size + 1);
}
}
}
问题:
- 缺乏类型安全 :因为
push
方法接受的是Object
,pop
方法返回的也是Object
,需要使用者手动转换类型,容易出现ClassCastException
。 - 不易理解和维护:使用者需要反复查看代码来了解栈的实际用途。
泛型改进版本
泛型重构后的栈类:
java
public class Stack<E> { // E 表示栈中元素的类型
private E[] elements;
private int size = 0;
private static final int DEFAULT_INITIAL_CAPACITY = 16;
@SuppressWarnings("unchecked")
public Stack() {
elements = (E[]) new Object[DEFAULT_INITIAL_CAPACITY]; // 使用泛型数组
}
public void push(E e) { // 元素类型受限于泛型 E
ensureCapacity();
elements[size++] = e;
}
public E pop() { // 类型由泛型确定,无需强制转换
if (size == 0) {
throw new EmptyStackException();
}
return elements[--size];
}
private void ensureCapacity() {
if (size == elements.length) {
elements = Arrays.copyOf(elements, 2 * size + 1);
}
}
}
使用泛型栈的好处
使用泛型:
java
Stack<String> stack = new Stack<>(); // 栈只能存储 String 类型元素
stack.push("Hello");
stack.push("World");
System.out.println(stack.pop()); // 正常取出,无需强制类型转换
System.out.println(stack.pop());
// stack.push(42); // 编译时就会阻止错误行为
- 类型安全:在编译时强制类型检查,防止运行时出现错误。
- 简化代码 :
pop
方法的调用者无需手动类型转换,负担减少。 - 提高可读性 :通过声明,调用者立刻明确
Stack<String>
的使用目的。 - 易于扩展 :重用
Stack<E>
逻辑来支持其他类型,例如:Stack<Integer>
。
示例3:方法改写为使用泛型
非泛型方法
一个非泛型方法可能设计成如下:
java
public static Set union(Set s1, Set s2) {
Set result = new HashSet(s1);
result.addAll(s2);
return result; // 没有类型保证
}
**问题:**没有针对性地限制 Set
的类型,调用者需要自行确保类型安全。
泛型方法改进
使用泛型改进后:
java
public static <E> Set<E> union(Set<E> s1, Set<E> s2) { // 泛型方法
Set<E> result = new HashSet<>(s1);
result.addAll(s2);
return result; // 返回的 Set 类型与参数类型保持一致
}
调用时:
java
Set<String> set1 = Set.of("A", "B", "C");
Set<String> set2 = Set.of("D", "E", "F");
Set<String> unionSet = union(set1, set2); // 编译器推断 E 为 String
改进的好处:
- 类型安全 :
union
方法确保输入和输出集的类型一致,避免拼装不同类型的集合。 - 灵活性:开发者可以通过范型支持任意类型组合,而无需一次性为所有类型实现多个重载方法。
示例4:优先使用泛型集合
在 Java 中,我们常用的集合类(如 List
、Map
、Set
)在 Java SE 5 后被泛型化。如果仍然使用原始类型定义集合,会导致代码难以维护。
错误示例:使用未经泛型化的集合
java
Map favorites = new HashMap(); // 使用原始类型
favorites.put("Language", "Java");
favorites.put("Year", 1995); // 没有限制类型
String language = (String) favorites.get("Language");
Integer year = (Integer) favorites.get("Year"); // 手动强制转换
使用泛型集合的改进版本:
java
Map<String, Object> favorites = new HashMap<>(); // 泛型化
favorites.put("Language", "Java");
favorites.put("Year", 1995); // 类型明确,无需强制转换
String language = (String) favorites.get("Language");
Integer year = (Integer) favorites.get("Year");
尽管 get
方法仍需强制类型转换(因为 Object
存储多类型值),但泛型化集合仍显著提升了类型约束能力,减少了错误。
Set<?>和Set区别
在 Java 泛型中,Set<?>
(无限制通配符类型)和 Set<Object>
之间的区别,是很多人学习泛型时的一个常见疑惑。初看上去,它们似乎作用类似,都可以处理「任意类型」的 Set
,但两者实际上在概念、限制以及应用场景上有本质区别。
概念上的区别
Set<?>
(无限制通配符类型)
- 表示可以处理任意类型的
Set
。泛型通配符?
表示集合中的具体类型未知,但其类型在运行时已经固定(它是某种明确的类型,只是不知道具体是什么)。 - 常用于需要处理不同类型的泛型集合的场景。
- 限定:
- 元素类型是未知的,因此不能向其中插入除
null
以外的任何值。 - 只能以
Object
的形式读取集合中的值。
- 元素类型是未知的,因此不能向其中插入除
示例:
java
Set<?> wildcardSet = new HashSet<String>();
Set<Object>
- 表示集合中明确要求所有元素都是
Object
类型 ,即任何对象都可以存储到集合中(String
、Integer
等都适用于Object
)。 - 这是一种明确声明的泛型类型,而不是通配符,因此开发者使用它时必须按照泛型参数传递规则进行操作。
- 限定:
- 可以向
Set<Object>
中插入任何类型的对象(Object
是 Java 中的最终父类)。 - 元素可以正常存取,不需要类型转换。
- 可以向
示例:
java
Set<Object> objectSet = new HashSet<>();
2. 在插入元素方面的区别
Set<?>
- 不允许插入任何元素,除了
null
。 - 原因是
?
表示「某种特定但未知的类型」,为了避免破坏类型一致性,在编译时会禁止插入操作。
java
Set<?> wildcardSet = new HashSet<String>();
wildcardSet.add(null); // 允许插入 null
// wildcardSet.add("Hello"); // 编译错误:不能向未知类型集合中插入值
// wildcardSet.add(42); // 编译错误
Set<Object>
- 可以插入所有类型的对象,因为所有类型都依赖于 Java 的
Object
父类。
java
Set<Object> objectSet = new HashSet<>();
objectSet.add("Hello"); // 合法
objectSet.add(42); // 合法
objectSet.add(new Object()); // 合法
3. 在读取元素方面的区别
Set<?>
- 元素的具体类型是未知的,但所有的类都直接或间接继承自
Object
,因此只能以Object
的形式读取。
java
Set<?> wildcardSet = new HashSet<String>();
wildcardSet = Set.of("Hello", "World");
for (Object obj : wildcardSet) { // 读取数据时只能作为 Object 类型处理
System.out.println(obj);
}
Set<Object>
- 元素类型明确就是
Object
,因此直接以Object
类型读取即可,同样不需要任何限制。
java
Set<Object> objectSet = Set.of("Hello", 42, new Object());
for (Object obj : objectSet) {
System.out.println(obj);
}
虽然从语法上看起来两者类似,但 Set<Object>
有一个前提:集合必须明确声明为 Set<Object>
, 而 Set<?>
可以适配任何类型的集合(如 Set<String>
、Set<Integer>
等)。
4. 适用场景的区别
Set<?>
的适用场景
Set<?>
更适合在需要处理任意类型的集合但不关心具体类型的场景。它代表一个通用工具方法或类,能够接受任何泛型集合。
-
用于输入参数(有限制的通用读取场景):
你只想遍历或读取集合中的元素,但不会修改集合。
示例:打印集合内容
javapublic void printSet(Set<?> set) { for (Object element : set) { System.out.println(element); // 只能以 Object 读取 } } Set<String> stringSet = Set.of("Hello", "World"); Set<Integer> intSet = Set.of(1, 2, 3); printSet(stringSet); // 传递的泛型可以是任意类型 printSet(intSet);
-
集合处理工具方法:
可以使用
Set<?>
设计通用方法来适配各种泛型集合,例如合并、遍历等。javapublic <T> void printSet(Set<T> set) { for (T element : set) { System.out.println(element); } }
-
只读集合的传递
如果某个地方只需要传递一个只读集合 的数据,而集合类型是未知的,
Set<?>
是最合适的选择。举例:统计集合中
null
的数量javapublic int countNulls(Set<?> set) { int count = 0; for (Object obj : set) { if (obj == null) { count++; } } return count; }
调用示例:
javaSet<Object> objectSet = new HashSet<>(); objectSet.add(null); objectSet.add("Hello"); objectSet.add(42); System.out.println(countNulls(objectSet)); // 输出:1
Set<Object>
的适用场景
Set<Object>
更适合在需要存储可以包括任意类型的元素的场景,用于处理无法预测的多样化数据(如果你的应用场景允许集合中的元素具有完全不同的类型)。
-
用于明确存储任意类型的集合:
如果集合本身的设计目标是能够存储各种不同的类型(不需要类型一致性),可以使用
Set<Object>
。示例:
javaSet<Object> objectSet = new HashSet<>(); objectSet.add("String Element"); objectSet.add(123); objectSet.add(45.67);
-
动态接收不同数据类型:
如果数据集合来源多样,但需要统一管理,可以使用
Set<Object>
作为容器。例如:javapublic void addElements(Set<Object> set, Object... elements) { for (Object element : elements) { set.add(element); // 允许添加任意对象 } } Set<Object> mixedSet = new HashSet<>(); addElements(mixedSet, "Hello", 42, 3.14);
-
注意:
Set<Object>
不适合处理单一类型的数据,如Set<String>
或Set<Integer>
。此时使用具体的泛型类型更加合适。
5. 灵活性与类型限制的区别
特性 | Set<?> |
Set<Object> |
---|---|---|
类型限制 | 类型未知,可适配任意类型集合 | 明确存储 Object 类型(即所有类型的父类) |
写入能力 | 不能添加任何元素,除了 null |
可以添加任何类型的值 |
读取能力 | 只能以 Object 类型读取元素 |
明确读取 Object 类型 |
适用场景 | 方法参数、通用工具类 | 动态存储多种类型的对象,或者需要显式处理 Object |
总结
优先使用泛型的必要性:
- 编译时类型安全 :泛型通过在编译期约束类型,有效避免了运行时
ClassCastException
。 - 增强代码清晰度:用户可以直接从类型声明中明确类或方法的用途和限制。
- 更加通用和灵活:相比编写多个方法/类,只需通过泛型编写一次逻辑,适配所有类型。
- 消除繁琐的强制类型转换 :开发者无需再为繁琐的
Object
和实际类型之间的显式转换操心。
Joshua Bloch 的建议是:只要可以用泛型实现,就优先使用它,彻底告别原始类型。