个人主页: 进朱者赤

阿里非典型程序员一枚 ,记录平平无奇程序员在大厂的打怪升级之路。 一起学习Java、大数据、数据结构算法(公众号同名

引言

数组是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](https://www.cs.usfca.edu/~galles/visualization/Algorithms.html)

学习界面如下图,对于学习数据结构底层原理很有用
* 首页
![](https://files.mdnice.com/user/62667/ecf8efaa-c53a-44d6-ba35-f91dfd61270d.png)

* 栈(数组实现)
![](https://files.mdnice.com/user/62667/7b4d738e-db23-4cd9-8f39-2cb89a108f3e.png)


朱进
1 声望0 粉丝