源本科技 | 码上会

Java 中的不可变数组

2025/12/26
35
0

学习目标

  • 理解 final 关键字对数组引用与内容的不同影响

  • 掌握 final 数组的合法操作与禁止操作

  • 区分“引用不可变”与“对象内容不可变”的本质区别

  • 避免在实际开发中因误解 final 而引发的逻辑错误


final 数组的核心概念

在 Java 中,final 修饰的是变量的引用,而不是对象的内容

  • 允许:修改数组元素(如 arr[2] = 99

  • 禁止:让 arr 指向另一个数组(如 arr = new int[]{...}

关键理解final 锁定的是“指针”,不是“指向的内容”


合法操作 vs 非法操作

操作

是否允许

说明

arr[0] = 100;

✅ 允许

修改数组内部元素

arr[i]++;

✅ 允许

对元素进行运算赋值

Arrays.sort(arr);

✅ 允许

排序会改变元素顺序

arr = anotherArray;

❌ 编译错误

试图改变引用

arr = new int[5];

❌ 编译错误

重新分配新数组


代码示例解析

修改 final 数组的元素(合法)

import java.util.Arrays;

public class FinalArrayDemo {
    public static void main(String[] args) {
        final int[] numbers = {1, 2, 3, 4, 5};
        
        // 修改元素 —— 完全合法
        numbers[0] = 10;
        numbers[4] = 50;
        
        System.out.println(Arrays.toString(numbers)); 
        // 输出: [10, 2, 3, 4, 50]
    }
}

输出验证:数组内容已变,但 numbers 仍指向原数组对象


尝试重赋值(编译错误)

public class ReassignmentError {
    public static void main(String[] args) {
        final int[] data = {100, 200};
        
        // 下面这行会导致编译错误!
        // data = new int[]{300, 400}; 
        // Error: Cannot assign a value to final variable 'data'
    }
}

编译器提示Cannot assign a value to final variable 'data'


final 对象引用同理

class Student {
    String name;
    int score;
    
    Student(String name, int score) {
        this.name = name;
        this.score = score;
    }
}

public class FinalObjectDemo {
    public static void main(String[] args) {
        final Student stu = new Student("张三", 85);
        
        // 允许:修改对象状态
        stu.score = 92;
        stu.name = "李四";
        
        System.out.println(stu.name + ": " + stu.score); // 李四: 92
        
        // 禁止:stu = new Student("王五", 78); // 编译错误
    }
}

结论final 对数组和普通对象的行为一致 —— 引用不变,内容可变


如何实现真正不可变的数组?

如果需要内容也不可变,可采用以下方案:

方案 1:封装 + 返回副本

public class ImmutableIntArray {
    private final int[] data;
    
    public ImmutableIntArray(int[] input) {
        this.data = input.clone(); // 防止外部修改原始数组
    }
    
    public int get(int index) {
        return data[index];
    }
    
    public int length() {
        return data.length;
    }
    
    // 不提供 set() 方法!
}

方案 2:使用不可变集合(推荐)

import java.util.*;

List<Integer> immutableList = List.of(1, 2, 3, 4, 5);
// 或
List<Integer> safeList = Collections.unmodifiableList(Arrays.asList(1, 2, 3));

这些集合在尝试修改时会抛出 UnsupportedOperationException


最佳实践

  1. 明确意图:用 final 表示“此引用不应被重新赋值”

  2. 避免混淆:不要误以为 final 能保护数据安全

  3. 防御性编程:若方法接收数组参数且不希望被修改,应复制一份

  4. 优先使用集合:对于需要不可变性的场景,考虑 List.of()Collections.unmodifiableXxx()


重点总结

  • final int[] arr引用不可变,内容可变

  • 可以修改 arr[i],但不能执行 arr = ...

  • final 对象行为一致:字段可改,引用不可换

  • 如需真正不可变数据结构,请使用不可变集合自定义封装类

  • final 主要用于防止重赋值错误表达设计语义


思考题

  1. 以下代码是否合法?为什么?

    final int[][] matrix = {{1, 2}, {3, 4}};
    matrix[0] = new int[]{5, 6}; // ?
    matrix[0][0] = 99;           // ?
  2. 如果一个方法接收 final int[] data 作为参数,调用方能否通过该引用修改数组内容?这对 API 设计有何启示?

  3. 在多线程环境中,仅将数组声明为 final 能否保证线程安全?为什么?