likes
comments
collection
share

你真的了解 Java 数组?

作者站长头像
站长
· 阅读数 19

前言

  • 大家好,我是 Lorin,数组是我们日常开发中使用非常广泛的一种数据结构,但是大家真的已经了解它了?我们最大可以开辟多大的数组?数组底层如何存储?什么时候我们应该选择数组?接下来我和大家一起来一一解决这些问题。
  • 本文源码版本:JDK 8

如何创建一个数组

静态初始化

  • 在声明数组时就为其分配和初始化元素的值。
int[] arr = {1, 2, 3, 4, 5}; // 初始化一个整数数组
String[] arr = {"Alice", "Bob", "Charlie"}; // 初始化一个字符串数组

Java 5及更高版本支持数组初始化表达式,允许你在创建数组时同时为其赋初值:
int[] arr = new int[] {1, 2, 3, 4, 5}; // 使用数组初始化表达式

动态初始化

  • 在声明数组后,通过循环或其他逻辑逐个分配元素的值。
int[] arr = new int[5]; // 创建一个包含5个整数的数组,初始值为0

// for 循环赋值
for (int i = 0; i < arr.length; i++) {
    arr[i] = i + 1; // 为数组元素赋值
}

// 使用Arrays类的方法初始化
Arrays.fill(arr, 7); // 填充整个数组

常见问题

数组大小不够如何处理

  • 普通数组无法支持动态拓展,一般有以下三个方案:

优化代码

  • 在一些情况下,可以通过更有效的方式来处理大量数据而无需增加数组大小。

手动扩展

  • 如果你使用的是普通数组,你可以手动创建一个更大的数组,将数据从旧数组复制到新数组,然后使用新数组。这需要更多的手动管理,但可以有效解决数组大小不足的问题。
int[] oldArray = new int[10];
int[] newArray = new int[20];
System.arraycopy(oldArray, 0, newArray, 0, oldArray.length);
// 更新引用
oldArray = newArray;

使用集合

  • 如果你需要处理可变大小的数据集合,可以考虑使用Java集合框架中的数据结构,如ArrayList、LinkedList、HashSet等,这些数据结构支持动态拓展。

数组的默认值

  • 当你创建一个普通数组并且没有显式初始化它的元素时,所有元素将被自动初始化为相应数据类型的默认值。例如:
int[] intArray = new int[5]; // 所有元素初始化为0
char[] charArray = new char[3]; // 所有元素初始化为'\u0000'
boolean[] booleanArray = new boolean[4]; // 所有元素初始化为false

int 数组的默认值是0
double 数组的默认值是0.0
float 数组的默认值是0.0f
char 数组的默认值是'\u0000',即空字符
byte 数组的默认值是0
short 数组的默认值是0
long 数组的默认值是0L
boolean 数组的默认值是false
对象数组(数组元素为引用类型),默认值是null

最大可用数组影响因素

数组索引长度限制

  • 在Java中,数组的长度是由int类型的索引来表示的,因此数组的最大长度受到int类型的范围限制。因此,数组的最大长度可以达到Integer.MAX_VALUE,它的值是 2,147,483,647。
int[] arr = new int[Integer.MAX_VALUE + 1];

Exception in thread "main" java.lang.NegativeArraySizeException
	at com.example.springboottestmaven.Test.main(Test.java:44)

堆内存限制

  • 普通数组存储在堆内存中,因此受到堆内存限制。
int[] arr = new int[Integer.MAX_VALUE - 1];

Exception in thread "main" java.lang.NegativeArraySizeException
	at com.example.springboottestmaven.Test.main(Test.java:44)
	
Exception in thread "main" java.lang.OutOfMemoryError: Requested array size exceeds VM limit
	at com.example.springboottestmaven.Test.main(Test.java:44)

底层存储方式

  • 数组的底层存储是连续的内存块,每个元素占用固定大小的内存。具体的存储方式取决于元素的类型,因为 Java 数组支持不同类型的元素,包括基本数据类型和引用类型。
  • 在 Java 实现中,栈存储了数组的引用,实际数据存储在堆中,根据存储数据类型不同,存在一些差异,具体差异见下文。

你真的了解 Java 数组?

基本数据类型数组

  • 对于基本数据类型(如int、char、byte等)的数组,每个元素的值直接存储在连续的内存位置上。这些数据类型的大小是已知的,因此可以通过简单的偏移量来访问每个元素。
  • 例如,对于int数组,每个int元素占用4个字节(32位),可以通过元素的索引和每个元素的大小来计算偏移量,以快速访问数组中的元素。

引用类型数组

  • 对于引用类型(对象类型)的数组,数组存储的是对象引用,而实际的对象存储在堆内存中。每个元素的大小是引用的大小,通常是4字节或8字节,具体取决于系统的位数和JVM配置。

多维数组

  • 多维数组的存储方式是数组的数组,它们的元素也是连续存储的,但每个元素可以是另一个数组,从而构成多维数组。多维数组的存储方式类似于矩阵,每个行数组存储在连续内存中,并且各行之间也是连续排列的。

多维数组行内存连续,行与行之间的内存连续?

  • 二维数组的行通常是连续存储的,但不同行之间的内存不一定连续。这意味着每个行数组的元素在内存中是紧密排列的,但各行之间可能存在间隙。
// 具体的内存布局可能会受到Java虚拟机的实现和底层硬件的影响,因此不同的系统和编译器可能会有不同的细节。
  +---+---+---+    +---+---+---+    +---+---+---+
  | 1 | 2 | 3 |    | 4 | 5 | 6 |    | 7 | 8 | 9 |
  +---+---+---+    +---+---+---+    +---+---+---+
    Row 1            Row 2            Row 3
# Tips
Java数组在内存中的存储是由Java虚拟机(JVM)和底层硬件来管理的,开发者不直接操作内存地址。
因此,数组的底层存储方式可能在不同的JVM实现和硬件平台上有所不同.
上述说明提供了一种一般性的概念,实际实现可能有细微差异。

优缺点

优点

快速随机访问

  • 数组中的元素可以通过索引快速访问,具有O(1)的时间复杂度。这使得数组在需要快速查找或访问元素时非常有用。

内存连续性

  • 数组的元素在内存中是连续存储的,这有助于提高缓存性能,因为现代计算机系统倾向于预读连续的内存块。

缺点

大小固定,不支持动态拓展

  • 数组的大小在创建时就被确定,难以动态扩展。如果需要更多空间,通常需要创建一个新的数组,将数据复制到新数组中,然后释放旧数组。

插入和删除低效

  • 在数组中插入或删除元素通常需要大量的数据迁移,因为需要保持元素的连续性。这可能导致性能问题。

最佳实践

注意适宜场景

  • 数组底层是连续存储的,对于随机存取具有O(1)的时间复杂度,适合连续存储的数据,但对于需要插入、删除效率较低。

优先考虑集合

  • 在大多数情况下,使用集合类(如 ArrayList 底层就是数组,, LinkedList, HashSet)而不是数组可以更方便地管理数据,因为它们具有自动大小调整、插入和删除操作效率更高等优势。只有在需要特定的性能、内存或数据结构特性时,才使用数组。
  • 如 ArrayList 底层实现是数组,但是基于数组实现了更多的功能,比如动态扩容等。

合适的数组大小

  • 如果你知道数组的大小,尽量在创建数组时指定初始大小,以避免后续的数组大小调整操作。或申请较多的内存导致内存浪费。

注意边界检查

  • 确保在访问数组元素时进行足够的边界检查,以避免数组索引越界异常。