Java数组相关知识

我们可以将数组理解为一个个整齐排列的单元格,每个单元格里面存放着一个元素。比如说下图中的数组,值为[a,b,c,a,b,c,b,b],下标依次为 0、1、2、3、4、5、6、7。

image-20250716200311448

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

数组的声明和初始化

数组的声明方式分两种。

  • 方式一:
int[] anArray;
  • 方式二:
int anOtherArray[];

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

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

int[] anArray = new int[10];

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

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

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

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

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

数组常用操作

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

anArray[0] = 10;

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

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

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

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

  • 使用for循环
int anOtherArray[] = new int[] {1, 2, 3, 4, 5};
for (int i = 0; i < anOtherArray.length; i++) {
    System.out.println(anOtherArray[i]);
}

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

  • 使用 for-each 循环
for (int element : anOtherArray) {
    System.out.println(element);
}

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

可变参数与数组

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

void varargsMethod(String... varargs) {}

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

public class VarargsDemo
{

    public VarargsDemo()
    {
    }

    transient void varargsMethod(String as[])
    {
    }
}

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

VarargsDemo demo = new VarargsDemo();
String[] anArray = new String[] {"我是", "Hanserwei"};
demo.varargsMethod(anArray);

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

demo.varargsMethod("我是", "Hanserwei");

数组与List

在 Java 中,数组与 List 关系非常密切。List 封装了很多常用的方法,方便我们对集合进行一些操作,而如果直接操作数组的话,有很多不便,因为数组本身没有提供这些封装好的操作,所以有时候我们需要把数组转成 List。最原始的方式,就是通过遍历数组的方式,一个个将数组添加到 List 中。

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

List<Integer> aList = new ArrayList<>();
for (int element : anArray) {
    aList.add(element);
}

更优雅的方式是通过Arrays类(戳链接了解详情)的asList()方法:

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

不过需要注意的是,Arrays.asList 的参数需要是Integer数组,而 anArray 目前是int类型。

可以这样写:

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

或者换另外一种方式。

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

还有一个需要注意的是,Arrays.asList 方法返回的 ArrayList 并不是 java.util.ArrayList,它其实是 Arrays 类的一个内部类:

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

如果需要添加元素或者删除元素的话,需要把它转成 java.util.ArrayList

new ArrayList<>(Arrays.asList(anArray));

Java 8 新增了 Stream 流的概念,这就意味着我们也可以将数组转成 Stream 进行操作。

String[] anArray = new String[] {"我是", "Hanserwei", "打死他"};
Stream<String> aStream = Arrays.stream(anArray);

数组排序和查找

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

  • 基本数据类型按照升序排列
  • 实现了 Comparable 接口的对象按照 compareTo() 的排序

来看第一个例子:

int[] anArray = new int[] {5, 2, 1, 4, 8};
Arrays.sort(anArray);

排序后的结果如下所示:

[1, 2, 4, 5, 8]

来看第二个例子:

String[] yetAnotherArray = new String[] {"A", "E", "Z", "B", "C"};
Arrays.sort(yetAnotherArray, 1, 3,
                Comparator.comparing(String::toString).reversed());

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

[A, Z, E, B, C]

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

int[] anArray = new int[] {5, 2, 1, 4, 8};
for (int i = 0; i < anArray.length; i++) {
    if (anArray[i] == 4) {
        System.out.println("找到了 " + i);
        break;
    }
}

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

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

int[] anArray = new int[] {1, 2, 3, 4, 5};
int index = Arrays.binarySearch(anArray, 4);

数组的复制

有时候我们需要将一个数组的值复制到另外一个数组当中,那就会涉及到数组复制的知识点。

在 String 类中其实会经常遇到数组复制,比如说substring()方法。

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

注意其中的 new String(),它会返回一个新的字符串,这个字符串的值就是原字符串的一部分,这个过程就涉及到了数组的复制。

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

其中的 Arrays.copyOfRange() 方法就是用来复制数组的,我们在讲 Arrays 类的时候就会讲到。

它底层调用的是 System.arraycopy() 方法,这个方法是一个 native 方法,它是用 C/C++ 实现的,效率非常高。

img

System.arraycopy方法的定义如下所示:

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

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

// 复制第一个数组到新数组
System.arraycopy(array1, 0, mergedArray, 0, array1.length);
System.out.println(Arrays.toString(mergedArray));

// 复制第二个数组到新数组
System.arraycopy(array2, 0, mergedArray, array1.length, array2.length);
System.out.println(Arrays.toString(mergedArray));

输出结果如下所示:

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

当然了,我们也可以使用循环来完成数组的复制:

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;
}

数组的越界

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

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

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

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