Effective Java笔记:请不要使用原生态类型

在《Effective Java》的 第5章:泛型(Generics) 中,Joshua Bloch 强调了「优先使用泛型」这一规则,并通过丰富的实例和详细分析,展示了泛型如何改善代码的表达力、类型安全性和灵活性。泛型是 Java 的一大飞跃,使得开发者能够编写更加通用和安全的代码。


泛型的意义

泛型是 Java SE 5 引入的特性,其核心目标是:

  1. 增加类型安全 :在编译时就发现类型错误,而不是拖到运行时。
  2. 提升代码复用性:通过泛型写通用的代码逻辑,支持不同的类型。
  3. 增强代码的可读性和清晰度:开发者可以直接从代码签名中看出类或方法的类型要求。
  4. 消除大量强制类型转换:减少代码的复杂性和潜在的错误。

优先使用泛型 vs 原始类型

在 Java 中,原始类型(Raw Types) 是泛型引入之前所使用的一种写法,它不对类型进行参数化处理。例如,List 是原始类型,而 List<String> 是泛型的用法。

原始类型的缺点:

  1. 失去类型安全性:原始类型会允许插入任何对象,容易导致运行时的 ClassCastException
  2. 代码难以阅读和理解:用户无法通过类型声明获知集合中存储的元素类型。

为了规避这些问题,应该尽量使用泛型代替原始类型。


示例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);  // 类型已保证安全,运行时无风险
}

泛型的两点改进:

  1. 类型安全:强制集合中的元素都匹配声明的泛型类型,避免了潜在的运行时异常。
  2. 更好的可读性 :开发者通过 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);
        }
    }
}

问题:

  1. 缺乏类型安全 :因为 push 方法接受的是 Objectpop 方法返回的也是 Object,需要使用者手动转换类型,容易出现 ClassCastException
  2. 不易理解和维护:使用者需要反复查看代码来了解栈的实际用途。

泛型改进版本

泛型重构后的栈类:

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); // 编译时就会阻止错误行为
  1. 类型安全:在编译时强制类型检查,防止运行时出现错误。
  2. 简化代码pop 方法的调用者无需手动类型转换,负担减少。
  3. 提高可读性 :通过声明,调用者立刻明确 Stack<String> 的使用目的。
  4. 易于扩展 :重用 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

改进的好处:

  1. 类型安全union 方法确保输入和输出集的类型一致,避免拼装不同类型的集合。
  2. 灵活性:开发者可以通过范型支持任意类型组合,而无需一次性为所有类型实现多个重载方法。

示例4:优先使用泛型集合

在 Java 中,我们常用的集合类(如 ListMapSet)在 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 类型 ,即任何对象都可以存储到集合中(StringInteger 等都适用于 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<?> 更适合在需要处理任意类型的集合但不关心具体类型的场景。它代表一个通用工具方法或类,能够接受任何泛型集合。

  • 用于输入参数(有限制的通用读取场景):

    你只想遍历或读取集合中的元素,但不会修改集合。

    示例:打印集合内容

    java 复制代码
    public 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<?> 设计通用方法来适配各种泛型集合,例如合并、遍历等。

    java 复制代码
    public <T> void printSet(Set<T> set) {
        for (T element : set) {
            System.out.println(element); 
        }
    }
  • 只读集合的传递

    如果某个地方只需要传递一个只读集合 的数据,而集合类型是未知的,Set<?> 是最合适的选择。

    举例:统计集合中 null 的数量

    java 复制代码
    public int countNulls(Set<?> set) {
        int count = 0;
        for (Object obj : set) {
            if (obj == null) {
                count++;
            }
        }
        return count;
    }

    调用示例:

    java 复制代码
    Set<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>

    示例:

    java 复制代码
    Set<Object> objectSet = new HashSet<>();
    objectSet.add("String Element");
    objectSet.add(123);
    objectSet.add(45.67);
  • 动态接收不同数据类型:

    如果数据集合来源多样,但需要统一管理,可以使用 Set<Object> 作为容器。例如:

    java 复制代码
    public 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

总结

优先使用泛型的必要性:

  1. 编译时类型安全 :泛型通过在编译期约束类型,有效避免了运行时 ClassCastException
  2. 增强代码清晰度:用户可以直接从类型声明中明确类或方法的用途和限制。
  3. 更加通用和灵活:相比编写多个方法/类,只需通过泛型编写一次逻辑,适配所有类型。
  4. 消除繁琐的强制类型转换 :开发者无需再为繁琐的 Object 和实际类型之间的显式转换操心。

Joshua Bloch 的建议是:只要可以用泛型实现,就优先使用它,彻底告别原始类型。