Skip to content

Java 数组

数组是最基础的数据容器。集合框架底层大量使用数组,比如 ArrayList 本质上就是动态扩容的数组。理解数组,有助于理解集合、缓存、批量处理和性能优化。

数组是什么

数组是一块连续的、长度固定的容器,用下标访问元素。

java
int[] scores = new int[3];
scores[0] = 90;
scores[1] = 95;
scores[2] = 88;

数组特点:

特点说明
长度固定创建后长度不能改变
下标访问第一个元素下标是 0
类型统一一个数组只能保存同一种类型
访问快根据下标可以快速定位

数组内存原理

mermaid
flowchart TD
    A["局部变量 scores"] --> B["堆中的 int 数组"]
    B --> C["index 0: 90"]
    B --> D["index 1: 95"]
    B --> E["index 2: 88"]

scores 是引用变量,真正的数组对象在堆里。数组元素有默认值:

类型默认值
整数0
浮点数0.0
booleanfalse
引用类型null

数组本身也是对象,所以它有 length 属性,也会参与 GC。局部变量里保存的只是数组引用,方法结束后如果没有其他地方引用这个数组,数组对象就可以被垃圾回收。

基本类型数组和引用类型数组

基本类型数组里直接保存值:

java
int[] ids = {1001, 1002, 1003};

引用类型数组里保存的是对象引用,不是对象本身:

java
class Order {
    long id;
    String status;

    Order(long id, String status) {
        this.id = id;
        this.status = status;
    }
}

Order[] orders = new Order[2];
orders[0] = new Order(1001L, "CREATED");
orders[1] = new Order(1002L, "PAID");

内存关系:

mermaid
flowchart TD
    A["局部变量 orders"] --> B["堆中的 Order[] 数组"]
    B --> C["index 0 保存引用"]
    B --> D["index 1 保存引用"]
    C --> E["Order 对象 1001"]
    D --> F["Order 对象 1002"]

这解释了两个常见问题:

  1. new Order[10] 只创建了一个长度为 10 的数组,没有创建 10 个 Order 对象。
  2. 引用类型数组的默认值是 null,使用元素前必须确认该位置已经放入对象。

错误示例:

java
Order[] orders = new Order[10];
// orders[0].status = "PAID"; // NullPointerException,因为 orders[0] 还是 null

正确写法:

java
orders[0] = new Order(1001L, "CREATED");
orders[0].status = "PAID";

下标为什么从 0 开始

数组底层可以理解为:起始位置 + 偏移量。

text
第 0 个元素位置 = 起始地址 + 0 * 元素大小
第 1 个元素位置 = 起始地址 + 1 * 元素大小
第 2 个元素位置 = 起始地址 + 2 * 元素大小

所以第一个元素偏移量是 0。

遍历数组

java
public class ArrayLoopDemo {
    public static void main(String[] args) {
        int[] scores = {90, 95, 88};

        for (int i = 0; i < scores.length; i++) {
            System.out.println("第 " + i + " 个分数:" + scores[i]);
        }

        for (int score : scores) {
            System.out.println("分数:" + score);
        }
    }
}

如果需要下标,用普通 for;如果只关心元素,用增强 for

数组越界

java
public class ArrayOutDemo {
    public static void main(String[] args) {
        int[] scores = {90, 95, 88};
        // System.out.println(scores[3]); // ArrayIndexOutOfBoundsException
    }
}

长度为 3 的数组,合法下标是 012。访问 3 会越界。

数组越界属于运行时异常,编译器不一定能提前发现。

数组拷贝

java
import java.util.Arrays;

public class ArrayCopyDemo {
    public static void main(String[] args) {
        int[] source = {1, 2, 3};
        int[] target = Arrays.copyOf(source, 5);

        System.out.println(Arrays.toString(target));
    }
}

Arrays.copyOf 会创建一个新数组,再把原数组内容复制过去。

mermaid
flowchart TD
    A["原数组长度 3"] --> B["创建新数组长度 5"]
    B --> C["复制已有元素"]
    C --> D["多出来的位置填默认值"]

这也是 ArrayList 扩容的核心思路。

浅拷贝和深拷贝

Arrays.copyOfSystem.arraycopy 对引用类型数组做的是浅拷贝:复制的是引用,不会自动复制引用指向的对象。

java
import java.util.Arrays;

class User {
    String name;

    User(String name) {
        this.name = name;
    }
}

public class ShallowCopyDemo {
    public static void main(String[] args) {
        User[] source = {new User("Tom")};
        User[] target = Arrays.copyOf(source, source.length);

        target[0].name = "Jerry";
        System.out.println(source[0].name); // Jerry
    }
}

为什么源数组也变了?因为两个数组的第 0 个位置保存的是同一个 User 对象引用。

mermaid
flowchart TD
    A["source[0]"] --> C["同一个 User 对象"]
    B["target[0]"] --> C
    C --> D["name = Jerry"]

如果要深拷贝,必须创建新的对象:

java
public class DeepCopyDemo {
    public static User[] deepCopy(User[] source) {
        User[] target = new User[source.length];
        for (int i = 0; i < source.length; i++) {
            if (source[i] != null) {
                target[i] = new User(source[i].name);
            }
        }
        return target;
    }
}

订单、库存、用户权限这类对象数组,如果只是浅拷贝,后续修改副本可能影响原数据。商业项目里要明确:你复制的是容器,还是连元素对象一起复制。

二维数组

二维数组可以理解为数组里面再放数组。

java
public class MatrixDemo {
    public static void main(String[] args) {
        int[][] table = {
                {1, 2, 3},
                {4, 5, 6}
        };

        System.out.println(table[0][1]);
    }
}

内存上它不是一整块严格的二维表,而是多个一维数组的组合。

Java 二维数组每一行长度可以不同:

java
int[][] table = new int[3][];
table[0] = new int[]{1, 2};
table[1] = new int[]{3, 4, 5};
table[2] = new int[]{6};

所以访问二维数组时不要默认每一行长度一样,应该使用 table[i].length

java
for (int i = 0; i < table.length; i++) {
    for (int j = 0; j < table[i].length; j++) {
        System.out.println(table[i][j]);
    }
}

可变参数

可变参数底层也是数组。

java
public class VarArgsDemo {
    public static int sum(int... values) {
        int total = 0;
        for (int value : values) {
            total += value;
        }
        return total;
    }

    public static void main(String[] args) {
        System.out.println(sum(1, 2, 3));
    }
}

int... values 在方法内部就是 int[] values

Arrays 工具类

java
import java.util.Arrays;

public class ArraysUtilDemo {
    public static void main(String[] args) {
        int[] values = {3, 1, 2};

        Arrays.sort(values);
        System.out.println(Arrays.toString(values));
        System.out.println(Arrays.binarySearch(values, 2));
    }
}

常用方法:

方法作用
Arrays.toString打印数组内容
Arrays.sort排序
Arrays.copyOf拷贝并扩容
Arrays.asList数组转 List
Arrays.binarySearch二分查找

注意:Arrays.asList 转出来的 List 长度固定,不能直接 addremove

Arrays.asList 的底层坑

Arrays.asList(array) 返回的是 java.util.Arrays 内部的固定长度 List,它背后仍然持有原数组。它支持 set 修改元素,但不支持改变长度的 add/remove

java
import java.util.Arrays;
import java.util.List;

public class AsListDemo {
    public static void main(String[] args) {
        String[] values = {"A", "B"};
        List<String> list = Arrays.asList(values);

        list.set(0, "X");
        System.out.println(values[0]); // X

        // list.add("C"); // UnsupportedOperationException
    }
}

如果需要独立可变集合:

java
List<String> list = new java.util.ArrayList<>(Arrays.asList(values));

还有一个坑:基本类型数组传给 Arrays.asList 不会变成多个元素,而是把整个数组当成一个元素。

java
int[] ids = {1, 2, 3};
List<int[]> list = Arrays.asList(ids);
System.out.println(list.size()); // 1

原因是泛型不能直接使用基本类型,int[] 本身是一个引用类型对象。

binarySearch 的前提

Arrays.binarySearch 使用二分查找,前提是数组已经按同样规则排序。

java
int[] values = {3, 1, 2};
System.out.println(Arrays.binarySearch(values, 2)); // 结果不可靠

Arrays.sort(values);
System.out.println(Arrays.binarySearch(values, 2)); // 正确

二分查找为什么快?因为每次比较都能排除一半数据,时间复杂度是 O(log n)。但如果数组没有排序,“排除一半”的前提不存在,结果就没有意义。

商业 Demo:批量计算订单金额

java
import java.math.BigDecimal;

public class OrderAmountArrayDemo {
    public static BigDecimal sum(BigDecimal[] amounts) {
        BigDecimal total = BigDecimal.ZERO;
        for (BigDecimal amount : amounts) {
            if (amount == null) {
                continue;
            }
            total = total.add(amount);
        }
        return total;
    }

    public static void main(String[] args) {
        BigDecimal[] amounts = {
                new BigDecimal("19.90"),
                new BigDecimal("88.00"),
                null
        };
        System.out.println(sum(amounts));
    }
}

这个例子强调两点:

  1. 引用类型数组元素可能是 null
  2. 金额汇总不要使用 double

商业 Demo:批量导入错误收集

数组常用于固定批次、固定列数、底层缓冲区。下面用数组模拟一批导入记录的校验结果。

java
public class ImportErrorArrayDemo {
    public static void main(String[] args) {
        String[] rows = {
                "1001,CREATED",
                "1002,",
                "",
                "1004,PAID"
        };

        String[] errors = new String[rows.length];

        for (int i = 0; i < rows.length; i++) {
            errors[i] = validate(rows[i]);
        }

        for (int i = 0; i < errors.length; i++) {
            if (errors[i] != null) {
                System.out.println("第 " + (i + 1) + " 行失败:" + errors[i]);
            }
        }
    }

    private static String validate(String row) {
        if (row == null || row.trim().length() == 0) {
            return "空行";
        }
        String[] parts = row.split(",", -1);
        if (parts.length != 2) {
            return "列数不正确";
        }
        if (parts[0].length() == 0) {
            return "订单号为空";
        }
        if (parts[1].length() == 0) {
            return "订单状态为空";
        }
        return null;
    }
}

这里 split(",", -1) 很关键。默认 split(",") 会丢弃末尾空字符串,"1002," 可能只得到一个元素,导致你无法准确判断“第二列为空”。批量导入、CSV 解析、接口字段解析都要注意这个细节。

数组和性能

数组的优势是结构简单、连续存放引用或基本类型值、下标访问快。代价是长度固定,插入删除不灵活。

操作数组特点原因
按下标读取快,通常 O(1)下标能直接计算位置
尾部写入快,前提是还有空间直接写到指定位置
中间插入慢,通常 O(n)后续元素要整体右移
中间删除慢,通常 O(n)后续元素要整体左移
扩容慢,通常 O(n)必须创建新数组并复制元素

这也是为什么 ArrayList 随机访问快、尾部追加快,但中间插入删除慢。

线上问题排查

mermaid
flowchart TD
    A["数组相关异常"] --> B{"异常类型"}
    B -- "ArrayIndexOutOfBoundsException" --> C["检查循环边界和下标计算"]
    B -- "NullPointerException" --> D["检查引用类型数组元素是否初始化"]
    B -- "UnsupportedOperationException" --> E["检查是否 Arrays.asList 后 add/remove"]
    B -- "数据被意外修改" --> F["检查是否浅拷贝共享了元素对象"]
    B -- "内存上涨" --> G["检查大数组、批量缓存、一次性读取文件"]

排查建议:

  1. 越界先看循环条件是不是 i <= array.length,正确通常是 i < array.length
  2. 对象数组空指针先确认 array[i] 是否创建了对象,而不只是创建了数组。
  3. 大批量数据不要一次性放入巨大数组,优先分页、分批、流式处理。
  4. 复制对象数组前明确浅拷贝还是深拷贝。
  5. 使用 Arrays.asList 后如果要增删,立刻包一层 new ArrayList<>(...)

数组和集合的区别

对比项数组集合
长度固定通常可变
类型支持基本类型和引用类型只能放引用类型
API较少丰富
性能下标访问快,开销小功能多,有额外封装
场景固定长度、底层结构业务数据处理

常见风险

问题后果建议
下标越界运行时报错循环条件使用 < array.length
引用类型元素为空空指针使用前判空
误以为数组可扩容数据丢失或逻辑错误扩容必须创建新数组
Arrays.asList 后 add抛出异常转成 new ArrayList<>(...)
对象数组只 new 数组没 new 元素空指针逐个创建元素对象
浅拷贝后修改对象原数组数据也被改需要独立对象时做深拷贝
未排序就 binarySearch查找结果不可靠先按同样规则排序

面试常问

数组为什么下标从 0 开始?
可以把数组访问理解为起始位置加偏移量。第一个元素相对起始位置的偏移是 0,所以它的下标是 0;第 n 个元素的偏移是 n * 元素大小

对象数组创建后,里面的对象也创建了吗?
没有。new User[10] 只创建了一个长度为 10 的数组,里面每个位置默认是 null。必须给每个位置赋值为具体对象后,才能访问对象字段或方法。

数组拷贝是深拷贝还是浅拷贝?
对基本类型数组,复制的是值;对引用类型数组,默认复制的是对象引用,也就是浅拷贝。两个数组可能指向同一批元素对象,修改对象内容会互相影响。需要完全独立时要手动创建新元素对象。

Arrays.asList 有什么坑?
它返回固定长度 List,不支持 add/remove;它背后仍然持有原数组,set 会影响原数组;基本类型数组传进去会被当成一个元素。需要可变集合时用 new ArrayList<>(Arrays.asList(array))

数组和 ArrayList 区别?
数组长度固定,能存基本类型和引用类型,API 简单;ArrayList 底层基于数组,长度可动态扩容,只能存引用类型,提供更多集合操作。ArrayList 的自动扩容本质上仍然是创建新数组并复制旧元素。

本章小结

数组是长度固定、下标访问、类型统一的基础容器。学习数组要理解引用关系、默认值、越界、拷贝、二维数组和工具类。集合框架中很多“自动扩容”的能力,底层仍然离不开数组拷贝。