掘金 后端 ( ) • 2024-04-11 10:00

引言

数组是Java中非常重要的数据结构之一,它允许我们存储相同类型的数据元素的集合,并可以通过索引来访问这些元素。本文将详细介绍Java数组的基础定义、使用方法、底层存储原理,以及与其他数据类型的转换,帮助读者深入理解和应用Java数组。

一、数组的基础定义

数组是一种引用数据类型,用于存储相同类型的元素的集合。在Java中,数组可以通过以下方式定义:

dataType[] arrayName; // 推荐方式
// 或者
dataType arrayName[]; // 早期Java版本中常见,但现在已不推荐

其中dataType表示数组元素的类型,arrayName是数组变量的名称。数组定义后,需要进行初始化才能使用。

二、数组的使用

2.1. 数组的初始化

数组可以通过静态初始化和动态初始化两种方式来进行初始化。

// 静态初始化
int[] numbers = {1, 2, 3, 4, 5};

// 动态初始化
int[] anotherNumbers = new int[5];
anotherNumbers[0] = 10;
anotherNumbers[1] = 20;
// ... 其他元素赋值

2.2. 访问数组元素

通过索引可以访问数组中的元素,索引从0开始。

int firstElement = numbers[0]; // 访问第一个元素

2.3. 数组长度

通过数组的length属性可以获取数组的长度(元素个数)。

int length = numbers.length;

4. 数组相关方法

Java的java.util.Arrays类提供了许多操作数组的有用方法,如排序、搜索、填充等。

import java.util.Arrays;

// 排序数组
Arrays.sort(numbers);

// 打印数组内容
System.out.println(Arrays.toString(numbers));

三、数组的底层存储原理

在Java中,数组在内存中是连续存储的。数组名实际上是一个引用,它指向数组在内存中的首地址。通过索引访问数组元素时,Java会根据首地址和索引计算出具体元素的内存地址,然后进行操作。这种连续存储的方式使得数组在访问元素时具有较高的效率。

3.1. 内存分配

当我们声明一个数组时,Java会根据数组的类型和长度在堆内存中分配一块连续的内存空间。 一组连续内存空间,在这里插入图片描述

  1. 假设上图为内存区,那么必须保证红色格子连续,中间不能有灰色或橙色的格子
  2. 假设最后一个红色格子2,是灰色的格子。那么数组的内存区域不能放在这里,而是整个向下用第三行的橙色格子。必须保证内存空间连续

3.2.内存布局

数组按照索引顺序存储元素,并且每个元素的大小是固定的。Java中的数组是通过偏移量来访问元素的。偏移量是相对于数组头部的固定偏移量,通过偏移量可以计算出元素在内存中的位置。

3.3. 访问元素

通过索引可以直接访问数组中的元素。Java虚拟机会根据索引和偏移量计算出元素的内存地址,然后直接访问该地址来获取或修改元素的值。 详见下图

注意

Java的数组是固定长度的,一旦分配了内存空间,其长度在运行时是不可改变的。如果需要动态改变数组大小,需要使用其他数据结构,如ArrayList。

四、数组与其他数据类型的转换

4.1. 数组与List的转换

Java中的List(如ArrayList)是另一种常用的集合类型,它提供了动态数组的功能。Java 5及以上版本提供了方便的转换方法。

import java.util.ArrayList;
import java.util.List;

// 数组转为List
int[] array = {1, 2, 3, 4, 5};
List<Integer> list = new ArrayList<>(Arrays.asList(array));

// List转为数组
Integer[] listArray = list.toArray(new Integer[0]);
// 或者转换为原始类型数组
int[] primitiveArray = list.stream().mapToInt(Integer::intValue).toArray();
  • 注意: 数组的长度是固定的,而集合框架的长度是可变的。在进行数组到集合的转换时,数组的长度不会改变;而在进行集合到数组的转换时,需要事先确定目标数组的长度。

4.2. 数组与集合框架的其他类型的转换

除了List,Java集合框架还提供了Set、Queue等其他类型的集合。虽然它们没有直接的转换方法,但可以通过遍历数组或使用Stream API来实现转换。

五、其他

5.1. 多维数组

Java还支持多维数组,如二维数组、三维数组等。多维数组可以看作是数组的数组,用于存储更复杂的数据结构。

int[][] matrix = new int[2][3]; // 定义一个2x3的二维数组

5.1.1.二维数组

二维数组本质上是一维数组的数组,即每个元素都是一个一维数组。二维数组常用于表示表格或矩阵数据。

5.1.1.1. 定义与初始化

二维数组可以通过静态初始化和动态初始化两种方式进行。

// 静态初始化  
int[][] staticArray = {{1, 2, 3}, {4, 5, 6}, {7, 8, 9}};  
  
// 动态初始化  
int[][] dynamicArray = new int[3][]; // 只指定了第一维的长度  
dynamicArray[0] = new int[2];  
dynamicArray[1] = new int[3];  
dynamicArray[2] = new int[4];  
// 为每个一维数组的元素赋值

5.1.1.2.访问元素

通过两个索引可以访问二维数组中的元素,第一个索引表示行,第二个索引表示列。

int element = staticArray[0][1]; // 访问第一行第二列的元素

5.1.1.3.使用场景

二维数组常用于表示具有行和列结构的数据,如矩阵运算、图像处理等。

5.1.2.三维数组

三维数组是数组的数组的数组,即每个元素都是一个二维数组。三维数组常用于表示具有三个维度的数据结构。

5.1.2.1. 定义与初始化

三维数组同样可以通过静态初始化和动态初始化进行。

// 静态初始化  
int[][][] static3DArray = {  
    {{1, 2, 3}, {4, 5, 6}},  
    {{7, 8, 9}, {10, 11, 12}}  
};  
  
// 动态初始化  
int[][][] dynamic3DArray = new int[2][2][3];  

5.1.2.2.访问元素

通过三个索引可以访问三维数组中的元素,分别表示第一维、第二维和第三维的位置。

int element = static3DArray[0][1][2]; // 访问第一维第0个,第二维第1个,第三维第2个元素

5.1.2.3.使用场景

三维数组在科学计算、图像处理、空间数据处理等领域有着广泛的应用,用于表示具有三个维度特征的数据结构。

5.2. 数组的复制

Java中数组的复制需要注意浅拷贝和深拷贝的区别

  • 浅拷贝: 拷贝之后,通过新数组修改值,如果原数组的值也被改变, 就是浅拷贝
  • 深拷贝: 拷贝之后,通过新数组修改值,如果原数组的值没有被改变,就是深拷贝

5.2.1 浅拷贝

使用Arrays.copyOf()或数组赋值操作是浅拷贝,只复制了引用而不是实际对象。

5.2.2 深拷贝

5.2.2.1.循环遍历方式

遍历原数组,逐个复制元素到新数组中。对于引用类型的元素,还需要对每个元素执行深拷贝操作。

  • 简易版(以String []为例)
public static String[] deepCopyArray(String[] source) {
    if (source == null) {
        return null;
    }
    
    String[] destination = new String[source.length];
    for (int i = 0; i < source.length; i++) {
        destination[i] = new String(source[i]);
    }
    
    return destination;
}
  • 通用方法:
public static <T> T[] deepCopyArray(T[] source) {
    if (source == null) {
        return null;
    }
    
    // 创建一个新的数组作为目标数组,并拷贝源数组的内容
    T[] destination = Arrays.copyOf(source, source.length);
    
    // 遍历源数组,对引用类型的元素执行深拷贝操作
    for (int i = 0; i < source.length; i++) {
        if (source[i] instanceof Cloneable) {
            try {
                // 获取元素的类对象
                Class<?> clazz = source[i].getClass();
                // 获取元素的clone()方法
                Method cloneMethod = clazz.getMethod("clone");
                // 调用clone()方法得到元素的副本,并将其赋值给目标数组
                destination[i] = (T) cloneMethod.invoke(source[i]);
            } catch (NoSuchMethodException | IllegalAccessException | InvocationTargetException e) {
                e.printStackTrace();
            }
        }
    }
    
    return destination;
}
  • 注意: 要实现深拷贝,源数组中的引用类型元素需要实现Cloneable接口并重写clone()方法。否则,在获取clone()方法时可能会抛出NoSuchMethodException异常。另外,需要注意捕获和处理可能抛出的异常,以确保代码的健壮性。

5.2.2.1.方式二:序列化方式

将原数组进行序列化,然后再反序列化成一个新数组

public static <T> T[] deepCopyArray(T[] source) {
    if (source == null) {
        return null;
    }
    
    try {
        // 创建一个字节数组输出流
        ByteArrayOutputStream bos = new ByteArrayOutputStream();
        // 创建一个对象输出流,并将其连接到字节数组输出流
        ObjectOutputStream oos = new ObjectOutputStream(bos);
        // 将原数组写入对象输出流,实现序列化
        oos.writeObject(source);
        oos.flush();
        oos.close();
        
        // 创建一个字节数组输入流,并用字节数组输出流的数据来初始化
        ObjectInputStream ois = new ObjectInputStream(new ByteArrayInputStream(bos.toByteArray()));
        // 从对象输入流读取对象数据,实现反序列化
        T[] destination = (T[]) ois.readObject();
        ois.close();
        
        return destination;
    } catch (IOException | ClassNotFoundException e) {
        e.printStackTrace();
    }
    
    return null;
}

说明: 需要确保数组中的对象类型是可序列化的,否则在执行序列化的过程中可能会抛出异常。另外,在使用该方法时,需要考虑序列化和反序列化的性能消耗以及所引起的额外开销。

六、总结

数组是Java编程中非常基础且重要的数据结构,它提供了存储和操作同类型数据元素的有效方式。通过本文的介绍,读者应该对Java数组的定义、使用、底层存储原理以及与其他数据类型的转换有了更深入的了解。在实际编程中,应根据具体需求选择合适的数据结构,并灵活运用数组及其相关方法。

By the way

推荐数据结构可视化的学习网站 Data Structure Visualizations

学习界面如下图,对于学习数据结构底层原理很有用

  • 首页

  • 栈(数组实现)