要让 arraylist 不可变,最稳妥的做法是使用 collections.unmodifiablelist() 包装原列表。调用该方法会返回一个视图,任何试图修改该视图的操作都会抛出 unsupportedoperationexception。需要注意的是,如果原列表本身被外部持有并修改,这个“不可变”视图也会随之变化,因此必须确保原列表不再被其他代码访问。

如何防止外部通过原引用修改列表内容
仅仅使用 Collections.unmodifiableList() 只能防止通过返回的视图进行修改,如果调用者仍然持有原始 ArrayList 的引用,他们依然可以随意增删元素。要彻底实现不可变性,必须在封装时立即将原列表替换为一个新的、包含相同元素的独立副本,并将原引用设为私有或丢弃。

import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
public class ImmutableExample {
private final List<string> data;
public ImmutableExample(List<string> input) {
// 1. 创建副本,切断与原引用的联系
List<string> copy = new ArrayList(input);
// 2. 包装为不可变视图
this.data = Collections.unmodifiableList(copy);
}
public List<string> getData() {
return data;
}
public static void main(String[] args) {
List<string> original = new ArrayList();
original.add("A");
original.add("B");
ImmutableExample example = new ImmutableExample(original);
// 尝试通过视图修改 -> 抛出异常
try {
example.getData().add("C");
} catch (UnsupportedOperationException e) {
System.out.println("视图修改失败: " + e.getMessage());
}
// 尝试通过原引用修改(如果原引用仍有效)
// 注意:在上面的构造函数中,我们传入了original,
// 但example内部使用的是copy,所以original的修改不会影响example
original.add("C");
System.out.println("Original size: " + original.size()); // 3
System.out.println("Internal size: " + example.getData().size()); // 2
}
}</string></string></string></string></string>
上述代码展示了标准的防御性拷贝模式。构造函数接收输入列表后,立即通过 new ArrayList(input) 创建副本。随后对副本进行包装。这样,即使外部的 original 列表后续被修改,内部的 data 也不会受到影响,因为两者在内存中已完全分离。
为什么直接返回 unmodifiableList 的结果是不安全的
如果在构造器中直接使用传入的列表进行包装,而不先创建副本,会导致数据泄露。这是因为 Collections.unmodifiableList() 返回的是一个动态视图,它始终指向底层的原始集合对象。只要原始集合存在且可被修改,这个视图就不是真正不可变的。

import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
public class UnsafeImmutableExample {
private final List<string> data;
public UnsafeImmutableExample(List<string> input) {
// 错误做法:直接包装原引用
this.data = Collections.unmodifiableList(input);
}
public List<string> getData() {
return data;
}
public static void main(String[] args) {
List<string> externalRef = new ArrayList();
externalRef.add("X");
UnsafeImmutableExample example = new UnsafeImmutableExample(externalRef);
// 虽然通过 example.getData() 无法修改
// 但通过 externalRef 可以直接修改底层数据
externalRef.add("Y");
System.out.println(example.getData().get(1)); // 输出 Y
// 这证明了所谓的“不可变”实际上是被动的,受外部控制
}
}</string></string></string></string>
在这个不安全示例中,UnsafeImmutableExample 的构造函数直接将传入的 input 列表包装。当外部代码持有 externalRef 引用时,它可以绕过 getData() 的限制,直接向 externalRef 添加元素。由于 data 是 externalRef 的视图,这些更改会立即反映在 example 的内部状态中。这种设计违背了不可变性的初衷,使得对象的状态变得脆弱且不可预测。
基于程序中输入参数为空时的具体行为问题
当传入的输入列表为 null 时,Collections.unmodifiableList() 的行为取决于具体的实现版本和调用方式,但在标准 Java 库中,它通常允许传入 null 并返回一个空的不可变列表视图,或者在某些情况下抛出异常。更关键的是,如果我们在构造函数中先创建副本 new ArrayList(null),这会抛出 NullPointerException。因此,处理 null 输入是确保不可变对象健壮性的关键一步。
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
public class NullSafeImmutableExample {
private final List<string> data;
public NullSafeImmutableExample(List<string> input) {
if (input == null) {
// 方案一:返回空列表
this.data = Collections.emptyList();
} else {
// 方案二:创建副本并包装
this.data = Collections.unmodifiableList(new ArrayList(input));
}
}
public List<string> getData() {
return data;
}
public static void main(String[] args) {
// 测试 null 输入
NullSafeImmutableExample example1 = new NullSafeImmutableExample(null);
System.out.println(example1.getData().isEmpty()); // true
// 测试正常输入
List<string> normalInput = new ArrayList();
normalInput.add("Test");
NullSafeImmutableExample example2 = new NullSafeImmutableExample(normalInput);
System.out.println(example2.getData().get(0)); // Test
}
}</string></string></string></string>
在处理 null 输入时,直接调用 new ArrayList(null) 会触发 NullPointerException。为了保持不可变对象的稳定性,应在构造函数入口处显式检查 null。如果输入为 null,可以选择返回一个预定义的 Collections.emptyList(),这是一个真正的单例不可变空列表,既安全又节省内存。如果输入非空,则继续执行副本创建和包装流程。这种处理方式确保了无论输入如何,内部状态始终处于一致且安全的不可变状态。
Java免费学习笔记:立即使用
解锁 Java 大师之旅:从入门到精通的终极指南











