Java 数组

Java 数组的长度、引用语义、多维形态与数组协变风险:固定长度容器如何承载同类型元素。

#type / concept #status / growing #tech / dev #resource / java

[!info] 关联笔记

Java 数组

这个概念为什么出现

需要固定数量、可按下标随机访问的同构数据时,数组是语言内建方案。它比集合更“贴内存与 JVM 数组对象”,但也更原始:长度不可变、缺乏丰富 API,且存在数组协变带来的运行时 ArrayStoreException

[!abstract] 一句话理解 数组是定长、可索引的对象;元素可以是基本类型或引用类型;T[] 在引用层面协变,因此运行时仍会检查存储类型。

最小可运行示例

场景:仓配系统按货位顺序装载 SKU 编号

分拣线有固定 4 个槽位,需要按下标写入/读取 SKU;若误把不兼容类型写入“商品数组”,应在运行时失败而不是静默污染。

public class SlotLoad {
    // loadFixedSlots:把扫描到的 SKU 填进固定槽位。
    //
    // 业务意图:槽位数固定为 4,未扫到的保持 null。
    // 教学点:
    // - new String[n] 长度固定;
    // - 引用数组元素默认 null;
    // - 数组是对象,变量保存引用。
    static String[] loadFixedSlots(String[] scanned) {
        String[] slots = new String[4];
        int n = Math.min(scanned.length, slots.length);
        for (int i = 0; i < n; i++) {
            slots[i] = scanned[i]; // 按下标写入
        }
        return slots;
    }

    // demoCovariance:展示 String[] 可当作 Object[] 传入,但非法存储会炸。
    static void demoCovariance() {
        String[] skus = {"A1", "B2"};
        Object[] objs = skus; // 数组协变:编译允许
        objs[0] = "C3";      // 仍是 String,OK
        try {
            objs[1] = Integer.valueOf(1); // 运行时 ArrayStoreException
        } catch (ArrayStoreException ex) {
            System.out.println("store-rejected: " + ex.getClass().getSimpleName());
        }
        System.out.println(skus[0] + "," + skus[1]);
    }

    public static void main(String[] args) {
        String[] slots = loadFixedSlots(new String[]{"S1", "S2"});
        System.out.println(slots.length + " " + slots[0] + " " + slots[2]); // 4 S1 null
        demoCovariance();
    }
}

建议运行:

javac SlotLoad.java && java SlotLoad

期望输出:

4 S1 null
store-rejected: ArrayStoreException
C3,B2

结合场景再看三个关注点

  1. 长度在创建时固定,扩容要自己 new + 拷贝(或改用集合)。
  2. 引用数组可含 null,遍历前要想清楚空槽语义。
  3. 协变让“看起来像泛型”的数组更危险;泛型集合通常更安全。

核心概念与准确模型

1. 创建与长度

int[] a = new int[3];     // 0,0,0
String[] b = {"x","y"}; // 长度 2
int len = b.length;       // 字段 length,不是方法

2. 基本类型数组 vs 引用数组

基本类型数组引用数组
元素默认0/false 等null
存 null可以
典型用途计量、位图、性能敏感缓冲对象列表快照

3. 多维数组

  • int[][] m = new int[2][3]:规则矩形
  • int[][] jagged = new int[2][]:锯齿数组,每行可不同长

4. 拷贝

  • 赋值 a=b 只复制引用
  • System.arraycopy / Arrays.copyOf 做元素级拷贝(浅拷贝对象引用)

5. 与集合边界

  • 需要动态伸缩、丰富查询 → List
  • 需要与变参、JNI、部分 API 交互 → 数组仍常见

边界情况与反直觉行为

  1. length 是字段;字符串才是 length()
  2. 负长度创建 → NegativeArraySizeException
  3. 越界 → ArrayIndexOutOfBoundsException
  4. 泛型不能直接 new T[](擦除限制),常见用 List 或反射数组 API。

常见误区

[!warning] 常见误区:数组赋值是深拷贝 错误理解:a=b 后改 a[i] 不影响 b
正确模型:共享同一数组对象。
为何易错:与“值复制”语言/结构体思维混淆。

工程实践

  1. 对外 API 优先 List,边界层再转数组。
  2. 返回内部数组前先拷贝,避免暴露可变内部状态。
  3. 批量处理用 Arrays 工具,但注意并行排序等的线程假设。
  4. 协变场景用泛型集合替代 Object[] 乱传。

可验证实验

  1. 打印 new int[3]new String[3] 的元素默认值。
  2. 复现 ArrayStoreException
  3. 比较 clone() 后修改引用元素字段(浅拷贝)的可见性。

本节总结

  • 数组定长、可索引、是对象
  • 引用语义与协变是两大心智点
  • 动态数据优先集合,数组留在性能与边界层

自测题

  1. 为什么 String[] 可以赋给 Object[] 变量,但泛型 List<String> 不能直接当 List<Object>
  2. arr.lengthstr.length() 差在哪?
参考答案
  1. 数组历史上协变且有运行时存储检查;泛型不变(invariant)且擦除后缺少同样的元素存储检查模型。
  2. 数组长度是字段;String 的长度是方法。

延伸阅读与资料来源

资料类型支撑内容
JLS Array Types规范数组类型与协变
Arrays (Java SE)API工具方法
创建于 2026/7/15 更新于 2026/7/15