网站公告

本次更新:没啥更新的

“钱塘江上潮信起,今日方知我是我”

Skip to content

数据 ​

字数: 0 字 时长: 0 分钟

1. 概述 ​

数组(Array)是一种用于存储多个相同类型数据的数据结构。在 Java 中,数组本身也是一个对象,它可以保存固定数量的元素,并通过索引访问指定位置的数据。

数组一旦创建,其长度就不能再改变。数组中的每个元素都有对应的索引,Java 数组的索引从 0 开始,第一个元素的索引为 0,最后一个元素的索引为 数组长度 - 1。

在实际开发中,直接使用数组的场景相对较少,更多时候会使用 List 等集合类型。不过数组仍然是 Java 中非常基础的数据结构,例如 ArrayList 的底层实现就使用了数组。

为什么数组索引从 0 开始?

数组可以理解为一段连续的数据存储空间,而索引本质上可以看作元素相对于数组起始位置的偏移量。

第一个元素就在数组的起始位置,相对于起始位置的偏移量为 0,因此它的索引就是 0;第二个元素向后偏移一个位置,所以索引为 1,以此类推。

这种从 0 开始的索引方式也继承了 C/C++ 等语言的设计习惯,并被 Java 延续下来。

我们可以将数组理解为一个个整齐排列的单元格,每个单元格里面存放着一个元素。

比如说下图中的数组,值为[a,b,c,a,b,c,b,b],下标依次为 0、1、2、3、4、5、6、7。

image-20250301140522673

数组元素的类型可以是基本数据类型(比如说 int、double),也可以是引用数据类型(比如说 String),包括自定义类型。

2. 数组 ​

2.1 数组声明 ​

![[Pasted image 20260911000344.png]]

数组的声明方式分两种。

先来看第一种:

java
// 方式一:推荐写法 
// [] 跟在数据类型后面,表示声明一个 int 类型的数组变量 // 此时只是声明变量,还没有真正创建数组对象
int[] anArray;

再来看第二种:

java
// 方式二:另一种合法写法 
// [] 跟在变量名后面,同样表示声明一个 int 类型的数组 
// 语法没有问题,但实际开发中通常更推荐第一种写法
int anOtherArray[];

不同之处就在于中括号的位置,是跟在类型关键字的后面,还是跟在变量的名称的后面。前一种的使用频率更高一些,像 ArrayList 的源码中就用了第一种方式。

同样的,数组的初始化方式也有多种,最常见的是:

java
// 指定数组长度进行初始化 
// 使用 new 创建一个长度为 10 的 int 数组 
// int 数组中的元素会被自动初始化为默认值 0
int[] anArray = new int[10];

上面这行代码中使用了 new 关键字,这就意味着数组的确是一个对象,只有对象的创建才会用到 new 关键字,基本数据类型是不用的(基本数据的包装类型是可以 new 的,包装类型就是对象)。然后,我们需要在方括号中指定数组的长度。

这时候,数组中的每个元素都会被初始化为默认值,int 类型的就为 0,Object 类型的就为 null。 不同数据类型的默认值不同,可以参照之前的基本数据类型。

另外,还可以使用大括号的方式,直接初始化数组中的元素:

java
// 直接指定数组中的元素 
// 数组长度由元素个数自动确定,这里长度为 5 
// 元素依次为 1、2、3、4、5 
// 对应索引分别为 0、1、2、3、4
int anOtherArray[] = new int[] {1, 2, 3, 4, 5};

这时候,数组的元素分别是 1、2、3、4、5,索引依次是 0、1、2、3、4,长度是 5。

2.2 数组的常用操作 ​

前面提到过,可以通过索引来访问数组的元素,就像下面这样:

java
anArray[0] = 10;

变量名,加上中括号,加上元素的索引,就可以访问到数组,通过“=”操作符可以对元素进行赋值。

如果索引的值超出了数组的界限,就会抛出 ArrayIndexOutOfBoundException。由于数组的索引是从 0 开始,所以最大索引为 length - 1,不要使用超出这个范围内的索引访问数组,否则就会抛出数组越界的异常了。

比如说你声明了一个大小为 10 的数组,你用索引 10 来访问数组,就会抛出这个异常。因为数组的索引是从 0 开始的,所以数组的最后一个元素的索引是 length - 1,也就是 9。

当数组的元素非常多的时候,逐个访问数组就太辛苦了,所以需要通过遍历的方式。

第一种,使用 for 循环:

java
int anOtherArray[] = new int[] {1, 2, 3, 4, 5};

// 使用普通 for 循环遍历数组
// i 表示数组索引,从 0 开始
// 当 i 小于数组长度时继续循环
for (int i = 0; i < anOtherArray.length; i++) {

    // 通过索引访问数组中的每一个元素
    // anOtherArray[0] ~ anOtherArray[4]
    System.out.println(anOtherArray[i]);
}

通过 length 属性获取到数组的长度,然后从 0 开始遍历,就得到了数组的所有元素。

遍历数组时,通常使用 array.length 获取数组长度,并让索引从 0 遍历到 length - 1。这样可以避免访问到不存在的下标,从而防止 ArrayIndexOutOfBoundsException。

第二种,使用 for-each 循环:

java
// 使用增强 for 循环(for-each)遍历数组
// 每次循环都会从 anOtherArray 中取出一个元素,并赋值给 element
for (int element : anOtherArray) {

    // 依次输出数组中的每个元素
    System.out.println(element);
}

增强 for 循环不需要手动维护索引,适合只读取数组元素的场景。相比普通 for 循环,写法更简洁,但无法直接获取当前元素的索引。

如果不需要关心索引的话(意味着不需要修改数组的某个元素),使用 for-each 遍历更简洁一些。当然,也可以使用 while 和 do-while 循环。

2.3 可变参数与数组 ​

在 Java 中,可变参数用于将任意数量的参数传递给方法,来看 varargsMethod() 方法:

java
void varargsMethod(String... varargs) {}

该方法可以接收任意数量的字符串参数,可以是 0 个或者 N 个,本质上,可变参数就是通过数组实现的。为了证明这一点,我们可以看一下反编译一后的字节码:

java
public class VarargsDemo {

    public VarargsDemo() {
    }

    // 可变参数在编译后,本质上会被转换成数组
    // 源码通常写成:varargsMethod(String... args)
    // 编译后可以理解为:varargsMethod(String[] args)
    void varargsMethod(String[] args) {
    }
}

Java 的可变参数使用 类型... 参数名 定义,本质上是数组的语法糖,因此一个方法最多只能有一个可变参数,并且必须放在参数列表的最后。

所以,我们其实可以直接将数组作为参数传递给该方法:

java
// 创建 VarargsDemo 对象
VarargsDemo demo = new VarargsDemo();

// 可变参数在方法内部本质上就是数组
// 因此也可以直接传入一个 String[] 数组
String[] anArray = new String[] {"kerwim", "一名Java牛马"};

// 将整个数组作为可变参数传入
// 等价于:demo.varargsMethod("kerwim", "一名Java牛马");
demo.varargsMethod(anArray);

可变参数既可以一个一个传入,也可以直接传入对应类型的数组,因为它在底层本质上就是数组。

也可以直接传递多个字符串,通过逗号隔开的方式:

java
demo.varargsMethod("kerwim", "一名Java牛马");

2.4 数组与 List ​

在 Java 中,数组与 List 的关系非常密切。

数组适合存储一组固定长度、相同类型的数据,但数组本身提供的操作比较有限;List 属于 Java 集合框架,提供了添加、删除、查找等更加丰富的操作。

因此在实际开发中,经常会遇到数组与 List 之间相互转换的场景。

List 会在集合框架中详细介绍,这里先了解数组转换为 List 的几种常见方式。

最直接的方式就是遍历数组,将每一个元素依次添加到 List 中:

java
int[] anArray = new int[] {1, 2, 3, 4, 5};

List<Integer> aList = new ArrayList<>();

// 遍历数组,将每个元素添加到 List 中
for (int element : anArray) {
    aList.add(element);
}

如果数组是 Integer[]、String[] 等引用类型数组,可以使用 Arrays.asList() 快速转换为 List:

java
Integer[] anArray = new Integer[] {1, 2, 3, 4, 5};

// 将数组转换为 List
List<Integer> aList = Arrays.asList(anArray);

也可以直接传入元素:

java
List<Integer> aList = Arrays.asList(1, 2, 3, 4, 5);

需要注意,Arrays.asList() 更适合处理引用类型数组。对于 int[] 这样的基本数据类型数组,不能直接得到 List<Integer>。

如果需要将 int[] 转换为 List<Integer>,可以使用 Stream:

java
int[] anArray = new int[] {1, 2, 3, 4, 5};

List<Integer> aList = Arrays.stream(anArray)
        .boxed()
        .collect(Collectors.toList());

其中 boxed() 会将基本类型 int 转换为对应的包装类型 Integer。

Stream 会在后面单开一个 md 文档,这里先知道这种写法即可。

还有一点需要注意,Arrays.asList() 返回的并不是我们平时使用的 java.util.ArrayList,而是 Arrays 内部定义的一个 ArrayList。

其内部实现类似:

java
private static class ArrayList<E> extends AbstractList<E>
        implements RandomAccess, java.io.Serializable {
}

这个 List 的长度是固定的,因此不能添加或删除元素。

例如:

java
Integer[] anArray = new Integer[] {1, 2, 3};

List<Integer> aList = Arrays.asList(anArray);

// 不支持添加元素,会抛出 UnsupportedOperationException
aList.add(4);

如果后续需要添加或者删除元素,可以再转换为真正的 java.util.ArrayList:

java
Integer[] anArray = new Integer[] {1, 2, 3, 4, 5};

// 转换成可以正常增删元素的 ArrayList
List<Integer> aList = new ArrayList<>(Arrays.asList(anArray));

Java 8 引入了 Stream API,数组也可以直接转换为 Stream:

java
String[] anArray = new String[] {
        "kerwim",
        "一名 Java 纯牛马",
        "朝九晚九一周上 6 天"
};

// 将数组转换为 Stream
Stream<String> aStream = Arrays.stream(anArray);

转换为 Stream 后,可以继续进行过滤、转换、遍历等操作:

java
Arrays.stream(anArray).forEach(System.out::println);

数组、List 和 Stream 在实际开发中经常需要相互转换,后面学习集合框架和 Stream 时还会继续遇到。

2.5 数组的排序与查找 ​

如果想对数组进行排序的话,可以使用 Arrays 类提供的 sort() 方法。

  • 基本数据类型按照升序排列

  • 实现了 Comparable 接口的对象按照 compareTo() 的排序

来看第一个例子:

java
int[] anArray = new int[] {5, 2, 1, 4, 8};

// 使用 Arrays.sort() 对数组进行升序排序
Arrays.sort(anArray);

// 排序结果:[1, 2, 4, 5, 8]
System.out.println(Arrays.toString(anArray));

排序后的结果如下所示:

java
[1, 2, 4, 5, 8]

来看第二个例子:

java
String[] anArray = new String[] {"A", "E", "Z", "B", "C"};

// 对索引 [1, 3) 范围内的元素进行降序排序
// 注意:fromIndex 包含,toIndex 不包含
// 实际参与排序的是索引 1、2,也就是 "E" 和 "Z"
Arrays.sort(anArray, 1, 3, Comparator.reverseOrder());

// 排序结果:[A, Z, E, B, C]
System.out.println(Arrays.toString(anArray));

只对 1-3 位置上的元素进行反序,所以结果如下所示:

java
[A, Z, E, B, C]

有时候,我们需要从数组中查找某个具体的元素,最直接的方式就是通过遍历的方式

java
int[] anArray = new int[] {5, 2, 1, 4, 8};

// 遍历数组查找元素 4
for (int i = 0; i < anArray.length; i++) {

    if (anArray[i] == 4) {

        // 找到元素后输出对应索引
        System.out.println("找到了,索引为:" + i);

        // 已经找到目标元素,结束循环
        break;
    }
}

上例中从数组中查询元素 4,找到后通过 break 关键字退出循环。

如果数组提前进行了排序,就可以使用二分查找法,这样效率就会更高一些。Arrays.binarySearch() 方法可供我们使用,它需要传递一个数组,和要查找的元素。

java
int[] anArray = new int[] {1, 2, 3, 4, 5};

// binarySearch() 使用二分查找
// 使用前必须保证数组已经按照相同规则排好序
int index = Arrays.binarySearch(anArray, 4);

// 元素 4 位于索引 3
System.out.println(index);

Arrays.binarySearch() 使用二分查找,因此数组必须提前完成排序。如果查找到目标元素,会返回对应索引;如果没有找到,则返回一个负数。

如果需要对数组进行排序,可以使用 Arrays.sort() 方法。

  • 基本数据类型数组默认按照升序排列。
  • 对象数组默认按照元素的自然顺序排序,通常由 Comparable 的 compareTo() 方法决定。
  • 也可以通过 Comparator 自定义排序规则。

2.6 数组的复制 ​

有时候我们需要将一个数组中的全部或部分元素复制到另外一个数组中,这就涉及到数组复制。

Java 中常见的数组复制方式包括:

  • System.arraycopy()
  • Arrays.copyOf()
  • Arrays.copyOfRange()
  • 使用循环手动复制

在 String 类中,也可以看到数组复制相关方法的使用。

例如 substring() 方法的部分源码:

java
public String substring(int beginIndex) {
    return (beginIndex == 0) ? this : new String(value, beginIndex, subLen);
}

注意其中的 new String(),它会根据原字符串中的部分内容创建一个新的字符串。

继续查看对应的构造方法:

java
public String(char value[], int offset, int count) {
    this.value = Arrays.copyOfRange(value, offset, offset + count);
}

这里使用了 Arrays.copyOfRange() 对数组中的指定区间进行复制。

不同 JDK 版本中 String 的内部存储结构和源码实现有所不同,这里的代码主要用于理解数组复制在 Java 源码中的应用。

Arrays.copyOfRange() 底层会涉及数组复制操作,而 Java 中非常重要的底层数组复制方法就是 System.arraycopy()。

System.arraycopy() 是一个 native 方法,其底层由 JVM 提供本地实现,通常具有较高的数组复制效率。

image-20250301143419244

System.arraycopy() 方法的定义如下:

java
public static native void arraycopy(Object src, int srcPos,
                                    Object dest, int destPos,
                                    int length);

各个参数的含义:

  • src:源数组。
  • srcPos:从源数组的哪个索引开始复制。
  • dest:目标数组。
  • destPos:从目标数组的哪个索引开始存放。
  • length:需要复制的元素数量。

用法如下:

java
int[] array1 = {1, 2, 3};
int[] array2 = {4, 5, 6};

// 创建一个新数组,长度为两个数组长度之和
int[] mergedArray = new int[array1.length + array2.length];

// 将 array1 从索引 0 开始的全部元素
// 复制到 mergedArray 从索引 0 开始的位置
System.arraycopy(array1, 0, mergedArray, 0, array1.length);

// 此时第二部分还没有赋值,因此保持 int 的默认值 0
System.out.println(Arrays.toString(mergedArray));

// 将 array2 从索引 0 开始的全部元素
// 复制到 array1 后面的位置
System.arraycopy(
        array2,
        0,
        mergedArray,
        array1.length,
        array2.length
);

// 输出合并后的完整数组
System.out.println(Arrays.toString(mergedArray));

输出结果:

text
[1, 2, 3, 0, 0, 0]
[1, 2, 3, 4, 5, 6]

除了使用 System.arraycopy(),也可以通过循环手动完成数组复制:

java
int[] array1 = {1, 2, 3};
int[] array2 = {4, 5, 6};

// 创建一个新数组,长度为两个数组长度之和
int[] mergedArray = new int[array1.length + array2.length];

// 记录当前写入新数组的位置
int index = 0;

// 复制第一个数组
for (int element : array1) {
    mergedArray[index++] = element;
}

// 复制第二个数组
for (int element : array2) {
    mergedArray[index++] = element;
}

// 输出:[1, 2, 3, 4, 5, 6]
System.out.println(Arrays.toString(mergedArray));

Arrays 类还提供了更加方便的 copyOf() 和 copyOfRange() 方法。

Arrays.copyOf() 可以复制数组,并指定新数组的长度:

java
int[] array = {1, 2, 3, 4, 5};

// 复制原数组的前 3 个元素
int[] newArray = Arrays.copyOf(array, 3);

// 输出:[1, 2, 3]
System.out.println(Arrays.toString(newArray));

如果新数组长度大于原数组,多出来的位置会使用对应数据类型的默认值填充:

java
int[] array = {1, 2, 3};

// 新数组长度为 5
int[] newArray = Arrays.copyOf(array, 5);

// 输出:[1, 2, 3, 0, 0]
System.out.println(Arrays.toString(newArray));

Arrays.copyOfRange() 用于复制数组中的指定区间:

java
int[] array = {1, 2, 3, 4, 5};

// 复制索引 [1, 4) 范围内的元素
// 包含索引 1,不包含索引 4
int[] newArray = Arrays.copyOfRange(array, 1, 4);

// 输出:[2, 3, 4]
System.out.println(Arrays.toString(newArray));

需要注意,数组复制后会得到一个新的数组对象。修改新数组中的元素,不会直接修改原数组中的对应元素。

2.7 数组越界 ​

在我们进行数组操作的时候,最容易遇到的一个问题就是数组越界,也就是 ArrayIndexOutOfBoundsException 异常。

java
int[] anArray = new int[] {1, 2, 3, 4, 5};  
System.out.println(anArray[5]);

上面这段代码就会抛出数组越界的异常,因为数组的索引是从 0 开始的,所以最大索引为 length - 1,也就是 4,所以当我们使用 5 作为索引的时候,就会抛出异常。

所以在操作数组之前,一定要注意索引的范围。

3. 二维数组 ​

3.1 什么是二维数组 ​

二维数组可以理解为“数组中的每个元素又是一个数组”,通常用于表示具有行和列结构的数据。

例如,一个 3 行 4 列的二维数组可以表示为:

java
array = [
    [a, b, c, d],
    [e, f, g, h],
    [i, j, k, l]
]

对应的索引关系可以理解为:

java
        0   1   2   3
      ┌───┬───┬───┬───┐
0     │ a │ b │ c │ d │
      ├───┼───┼───┼───┤
1     │ e │ f │ g │ h │
      ├───┼───┼───┼───┤
2     │ i │ j │ k │ l │
      └───┴───┴───┴───┘

在这个例子中,第一行有4个元素,第二行有4个元素,第三行有4个元素,每个元素都有一个行索引和一个列索引。例如,元素 array [1] [2] 是第2行第3列的元素,它的值是 g。

使用二维数组可以有效地存储和处理表格数据,如矩阵、图像、地图等等。

3.2 创建二维数组 ​

要在 Java 中创建二维数组,你必须指定要存储在数组中的数据类型,后跟两个方括号和数组的名称。

语法如下所示:

java
data_type[][] array_name;

让我们看一个代码示例。

java
int[][] oddNumbers = {
        {1, 3, 5, 7},
        {9, 11, 13, 15}
};

这里创建了一个 int 类型的二维数组,共有 2 行 4 列。

其中:

java
oddNumbers[0] = {1, 3, 5, 7}
oddNumbers[1] = {9, 11, 13, 15}

例如访问:

oddNumbers[1][2]

得到结果

java
13

3.3 访问二维数组中的元素 ​

访问二维数组中的元素时,需要使用两个索引:

java
array[行索引][列索引]

第一个索引用于确定所在的行,第二个索引用于确定该行中的具体元素。

例如:

java
int[][] oddNumbers = {
        {1, 3, 5, 7},
        {9, 11, 13, 15}
};

// 访问第 1 行、第 1 列的元素
System.out.println(oddNumbers[0][0]);

输出结果:

text
1

二维数组中的索引同样从 0 开始:

text
oddNumbers[0] = {1, 3, 5, 7}
oddNumbers[1] = {9, 11, 13, 15}

如果要访问第二行中的第三个元素:

java
oddNumbers[1][2]

对应的值为:

text
13

再来看一个例子:

java
int[][] oddNumbers = {
        {1, 3, 5, 7},
        {9, 11, 13, 15},
        {17, 19, 21, 23}
};

如果要访问第三行中的 21:

text
第三行索引:2
21 的列索引:2

因此可以通过:

java
System.out.println(oddNumbers[2][2]);

输出结果:

text
21

二维数组通常使用嵌套循环进行遍历:

java
int[][] oddNumbers = {
        {1, 3, 5, 7},
        {9, 11, 13, 15},
        {17, 19, 21, 23}
};

// 外层循环遍历每一行
for (int i = 0; i < oddNumbers.length; i++) {

    // 内层循环遍历当前行中的每个元素
    for (int j = 0; j < oddNumbers[i].length; j++) {
        System.out.println(oddNumbers[i][j]);
    }
}

输出结果:

text
1
3
5
7
9
11
13
15
17
19
21
23

其中:

java
oddNumbers.length

表示二维数组的行数。

java
oddNumbers[i].length

表示第 i 行的元素个数。

由于 Java 中二维数组本质上是数组的数组,因此每一行的长度可以不同,使用 oddNumbers[i].length 遍历会更加安全。

3.4 二维数组打印杨辉三角 ​

杨辉三角中,每一行的第一个和最后一个数字都是 1,中间的数字等于上一行中相邻两个数字之和。

例如:

text
1
1 1
1 2 1
1 3 3 1
1 4 6 4 1
1 5 10 10 5 1

可以使用二维数组保存杨辉三角中的数据:

java
import java.util.Scanner;

public class YangHuiTriangle {

    public static void main(String[] args) {
        Scanner input = new Scanner(System.in);

        System.out.print("请输入要打印的行数:");
        int n = input.nextInt();

        printYangHuiTriangle(n);

        input.close();
    }

    public static void printYangHuiTriangle(int n) {

        // 创建二维数组,用于保存杨辉三角
        int[][] triangle = new int[n][n];

        // 计算杨辉三角中的数据
        for (int i = 0; i < n; i++) {

            // 每一行的第一个和最后一个数字都是 1
            triangle[i][0] = 1;
            triangle[i][i] = 1;

            // 中间的数字等于上一行相邻两个数字之和
            for (int j = 1; j < i; j++) {
                triangle[i][j] =
                        triangle[i - 1][j - 1]
                        + triangle[i - 1][j];
            }
        }

        // 遍历二维数组,打印杨辉三角
        for (int i = 0; i < n; i++) {

            // 每一行只需要打印到索引 i
            for (int j = 0; j <= i; j++) {
                System.out.print(triangle[i][j] + " ");
            }

            // 当前行打印完成后换行
            System.out.println();
        }
    }
}

运行结果:

text
请输入要打印的行数:6

1
1 1
1 2 1
1 3 3 1
1 4 6 4 1
1 5 10 10 5 1

这段代码使用二维数组保存杨辉三角中的数据。

首先创建一个 n × n 的二维数组,然后通过嵌套循环计算每一行的数据:

text
triangle[i][0] = 1
triangle[i][i] = 1

表示每一行的第一个和最后一个数字都是 1。

中间元素按照下面的规律计算:

java
triangle[i][j] =
        triangle[i - 1][j - 1]
        + triangle[i - 1][j];

也就是:

text
当前元素 = 上一行左上方元素 + 上一行正上方元素

计算完成后,再通过嵌套循环遍历二维数组,将杨辉三角打印出来。

3. 打印数组 ​

3.1 为什么不能直接打印数组 ​

首先来看一个常见的问题:为什么数组不能直接通过 System.out.println() 打印内容?

例如:

java
String[] cmowers = {"kerwim", "牛马", "一名纯 Java 牛马"};

// 直接打印数组
System.out.println(cmowers);

打印结果类似:

text
[Ljava.lang.String;@3d075dc0

这个结果显然不是我们想看到的数组内容。

其中:

text
[Ljava.lang.String;

表示这是一个 String[] 类型的数组,@ 后面的内容则是 hashCode() 转换得到的十六进制字符串。

之所以会出现这种结果,是因为数组本身没有重写 toString() 方法,因此直接打印数组时,使用的是 Object 类中的 toString() 实现:

java
public String toString() {
    return getClass().getName()
            + "@"
            + Integer.toHexString(hashCode());
}

这也说明了一个重要概念:

Java 中的数组也是对象。

数组虽然不像 String、ArrayList 那样通过一个普通的 Java 类来定义,但数组对象同样可以使用 Object 中定义的方法。

例如:

java
String[] cmowers = {"kerwim", "牛马", "一名纯 Java 牛马"};

// 获取数组运行时对应的 Class
System.out.println(cmowers.getClass());

// 获取数组的 hashCode
System.out.println(cmowers.hashCode());

那么,为什么 Java 没有定义一个类似 Array 的普通类来表示数组呢?

这是因为数组属于 Java 语言和 JVM 原生支持的数据类型。数组类型由 JVM 在运行时创建,并不是通过一个普通的 Array.java 类实现的。

可以简单理解为:

text
String      → java.lang.String 类
ArrayList   → java.util.ArrayList 类

String[]    → JVM 创建的数组类型
int[]       → JVM 创建的数组类型

数组内部同样需要保存一组元素,这一点和 String 这样的对象有一些相似之处。

例如在较早版本的 JDK 中,String 内部使用 char[] 保存字符数据:

java
public final class String
        implements java.io.Serializable,
                   Comparable<String>,
                   CharSequence {

    // 用于保存字符串中的字符
    private final char value[];
}

上面的 String 源码属于较早版本 JDK 的实现。JDK 9 之后,String 的内部存储已经改为 byte[],这里主要用于理解“对象内部也可以使用数组保存数据”。

如果想真正打印数组中的元素,一般应该使用 Arrays.toString():

java
String[] cmowers = {"kerwim", "牛马", "一名纯 Java 牛马"};

// 将数组转换为可读的字符串形式
System.out.println(Arrays.toString(cmowers));

输出结果:

text
[kerwim, 牛马, 一名纯 Java 牛马]

3.2 Stream 流打印 Java 数组 ​

我们来看第一种打印数组的方法:使用更“时髦”一点的 Stream 流。

假设数组内容如下:

java
String[] cmowers = {
        "kerwim",
        "牛马",
        "一名纯 Java 牛马"
};

可以使用下面几种方式进行遍历打印。

第一种写法:

java
Arrays.asList(cmowers)
        .stream()
        .forEach(s -> System.out.println(s));

第二种写法:

java
Stream.of(cmowers)
        .forEach(System.out::println);

第三种写法:

java
Arrays.stream(cmowers)
        .forEach(System.out::println);

这三种方式打印的结果都是一样的:

text
kerwim
牛马
一名纯 Java 牛马

没错,这三种方式都可以轻松完成数组元素的遍历打印,而且写法比较现代,因为用到了 Stream 和 Lambda 表达式。

其中,Stream.of(cmowers) 和 Arrays.stream(cmowers) 都可以直接将数组转换为 Stream,然后通过 forEach() 遍历数组中的每一个元素。

3.3 for循环打印 Java 数组 ​

普通 for 循环可以通过索引遍历数组:

java
// 通过索引依次访问数组中的每一个元素
for (int i = 0; i < cmowers.length; i++) {
    System.out.println(cmowers[i]);
}

增强 for 循环,也就是 for-each,可以直接获取数组中的每一个元素:

java
// 依次获取数组中的每一个元素
for (String s : cmowers) {
    System.out.println(s);
}

两种方式都可以完成数组遍历。

普通 for 循环可以直接获取当前元素的索引,而增强 for 循环写法更加简洁,适合只需要读取数组元素的场景。

3.4 Arrays 工具类打印 Java 数组 ​

我也认为 Arrays.toString() 是打印数组非常方便的一种方式。

Arrays.toString() 可以将数组中的元素转换成可读的字符串形式,基本数据类型数组和引用类型数组都可以使用。该方法针对不同数组类型提供了多个重载版本。

image-20250301150526037

例如:

java
String[] cmowers = {
        "kerwim",
        "牛马",
        "一名纯 Java 牛马"
};

// 将数组转换为字符串后打印
System.out.println(Arrays.toString(cmowers));

程序打印结果:

text
[kerwim, 牛马, 一名纯 Java 牛马]

这种输出格式比较直观:

text
[元素1, 元素2, 元素3]

其中,[] 表示数组整体,元素之间使用逗号和空格进行分隔。

相比直接使用 System.out.println(array),Arrays.toString() 更适合用来查看和打印一维数组中的实际内容。

3.5 Arrays 工具类打印二维数组 ​

如果需要直接打印二维数组,可以使用 Arrays.deepToString() 方法。

它会递归读取多维数组中的元素,并按照数组的层级结构转换成字符串。

例如:

java
String[][] deepArray = {
        {"kerwim", "牛马"},
        {"一名纯 Java 牛马"}
};

// 打印二维数组中的实际内容
System.out.println(Arrays.deepToString(deepArray));

打印结果:

text
[[kerwim, 牛马], [一名纯 Java 牛马]]

可以看到,外层的 [] 表示整个二维数组,内部的每一组 [] 表示其中的一行。

相比 Arrays.toString(),Arrays.deepToString() 更适合打印二维数组以及更高维度的数组。

3.6 POJO 的打印规约 ​

阿里巴巴《Java 开发手册》中有这样一条规约:

image-20250301150941773

POJO 是 Plain Ordinary Java Object 的缩写,通常指结构比较简单、没有依赖特定框架约束的普通 Java 对象。

在实际项目中,实体类、DTO、VO 等对象通常都会包含多个字段。为了方便日志查看和问题排查,一般会重写 toString() 方法,让对象能够直接打印出字段内容。

如果没有重写 toString(),默认会调用 Object 类中的实现,打印结果类似:

text
com.example.User@3d075dc0

这样的内容很难直接看出对象中保存了哪些数据。

例如:

java
public class User {

    private String name;
    private Integer age;

    @Override
    public String toString() {
        return "User{" +
                "name='" + name + '\'' +
                ", age=" + age +
                '}';
    }
}

重写之后,打印对象:

java
System.out.println(user);

得到的结果类似:

text
User{name='kerwim', age=18}

IntelliJ IDEA 可以直接帮助我们生成 toString() 方法,不需要手动编写。

不过在现在的 Java 项目中,更常见的做法还是使用 Lombok 小辣椒,通过注解自动生成这些样板代码。

例如:

java
@Data
public class User {

    private String name;

    private Integer age;
}

@Data 会自动生成 getter、setter、equals()、hashCode() 和 toString() 等方法。

如果只需要生成 toString(),也可以单独使用:

java
@ToString
public class User {

    private String name;

    private Integer age;
}

这样既能减少大量重复代码,也方便在日志和调试过程中直接查看对象中的字段内容。

Noctiflorous Garden · MIT License