1、数组 及 其特点
- 数组(Array)是一种线性的数据结构,用一组连续的内存空间,来存储同一类型值的集合。
- 通过一个整型索引(index,或称下标) 可以访问数组中的每一个值。
特点 :
1、数组是一种 线性表
2、连续的 内存空间 和 相同类型的数据
3、长度是固定的,一旦创建,大小就不能再改变。 因为,创建完成后,就是申请了一块固定内存。如果大小可以再变化,不能保证内存连续了。
4、数组的数据类型可以是任意数据类型,包括基本数据类型和引用类型。
5、数组在使用之前 必须 先声明 后使用 。
2、数组 的 声明
- 声明数组是告诉编译器,数组的变量名和存储 元素的数据类型 (后面紧跟 [] )。但不分配内存空间。
- 此时,分配一个存储引用值的内存空间。
- 这个引用指向数组的内存地址。
int[] numbers; // 声明一个整型数组
String[] names; // 声明一个字符串数组
double values[]; // 合法但不推荐
3、数组 的 初始化
- 初始化数组是为数组分配内存空间并赋值。
- 此时,分配一个存储数组元素(基本数据类型)或数组元素的引用值(引用数据类型)的内存空间。
- 有三种方式:
1、静态初始化
// 静态初始化 的 完整形式
String[] fruits = new String[]{"Apple", "Banana"};
// 静态初始化 的 简写形式
int[] arr1 = {1, 2, 3};
// 静态初始化
int[][] matrix1 = {{1, 2}, {3, 4}, {5, 6}};
2、动态初始化 – 手动为元素赋指定值
- 使用 new 关键字分配 内存,指定数组长度,元素赋指定值。
int[] a=new int[2];
a[0]=1;
a[1]=2;
3、动态初始化(默认初始化) – 元素赋默认值。
- 使用 new 关键字分配 内存,指定数组长度,元素赋默认值。
- 默认值 规则:
- 数值类型(int, double 等):默认值为 0 或 0.0。
- 布尔类型(boolean):默认值为 false。
- 引用类型(String, 对象等):默认值为 null。
int[] arr2 = new int[5]; // 元素默认值为 0
String[] cities = new String[3]; // 元素默认值为 null
// 动态初始化(指定行和列)
int[][] matrix2 = new int[3][2]; // 3 行 2 列,默认值为 0。
// 动态初始化(逐行分配)
int[][] matrix3 = new int[3][]; // 行数固定,列数可动态分配
matrix3[0] = new int[2]; // 第一行 2 列,每列默认值为 0。
matrix3[1] = new int[3]; // 第二行 3 列,每列默认值为 0。
4、多维数组(规则数组、不规则数组)
- Java 实际上没有多维数组,只有一维数组。多维数组被解释为“数组的数组”。
- “规则”数组,即:数组的每一行有相同的长度。
- 如:int[][] twoDimensionalArr = {{1,2,3},{4,5,6},{7,8,9}};
- 第一行:有 3 列。
- 第二行:有 3 列。
- 第三行:有 3 列。
- “不规则”数组,即:数组的每一行有不同的长度。
- 如:int[][] twoDimensionalArr = {{1,2,3},{4,5},{6,7,8,9}};
- 第一行:有 3 列。
- 第二行:有 2 列。
- 第三行:有 4 列。
public class MultiDimensionalArrays {
public static void main(String[] args) {
// 一维数组
int[] arr = {1,2,3};
// 3
System.out.println(arr[2]);
// 二维数组(规则数组)
int[][] twoDimensionalArr = {{1,2,3},{4,5,6},{7,8,9}};
// 二维数组(不规则数组)
int[][] twoDimensionalArr = {{1,2,3},{4,5},{6,7,8,9}};
// [4, 5]
System.out.println(Arrays.toString(twoDimensionalArr[1]));
// 5
System.out.println(twoDimensionalArr[1][1]);
// 三维数组(不规则数组)
int[][][] threeDimensionalArr = {{{1,2,3}},{{3,4,5},{5,6,7}},{{8}}};
// [[3, 4, 5], [5, 6, 7]]
System.out.println(Arrays.deepToString(threeDimensionalArr[1]));
// [5, 6, 7]
System.out.println(Arrays.toString(threeDimensionalArr[1][1]));
// 8
System.out.println(threeDimensionalArr[2][0][0]);
// 2
System.out.println(threeDimensionalArr[0][0][1]);
}
}
- 图解
5、数组 的 协变性(Covariance)
1、定义
- 协变性规则:若 B 是 A 的子类,则 B[] 是 A[] 的子类型。
- 赋值兼容性:可以将 B[] 直接赋值给 A[] 的变量。
- 运行时类型检查:数组在运行时仍会记录其实际类型,插入元素时,仍会检查类型是否匹配。
String[] strings = new String[] {"a", "b", "c"};
Object[] objects = strings; // 协变性允许此赋值
// 尝试插入非 String 类型元素
objects[0] = 123; // 编译通过,但运行时抛出 ArrayStoreException 。
- 缺陷:协变性绕过了编译时的严格类型检查,将错误推迟到运行时。
void process(Object[] array) {
array[0] = new Object(); // 如果传入的是 String[],这里会抛出异常
}
String[] strings = new String[1];
process(strings); // 合法调用,但可能导致运行时错误
2、与泛型不变性(Invariance)的对比
- Java 泛型是 不变(Invariant) 的,即 List 不是 List 的子类型,即使:B extends A。
- 这种设计避免了协变性导致的问题。
- 示例:泛型不允许协变
- 编译时 直接报错,阻止潜在的类型安全问题。
List<String> stringList = new ArrayList<>();
List<Object> objectList = stringList; // 编译错误:类型不兼容
3、协变性的替代方案
- 1、通过通配符 <? extends T> 实现安全的 “只读” 协变:
List<? extends Number> numbers = new ArrayList<Integer>(); // 协变赋值
Number num = numbers.get(0); // 允许读取为 Number
// numbers.add(123); // 编译错误:无法写入(防止类型污染)
- 2、防御性编程:
- 对协变数组进行封装,避免直接暴露:
class SafeContainer<T> {
private T[] array;
public SafeContainer(Class<T> type, int size) {
array = (T[]) Array.newInstance(type, size); // 安全创建数组
}
public void set(int index, T value) {
array[index] = value; // 类型安全
}
public T get(int index) {
return array[index];
}
}
6、JVM 中数组长度的限制
- Java 语言规范规定数组的索引类型为 int。
- 因此,最大理论长度为 Integer.MAX_VALUE = 231-1,即 2,147,483,647 。
- 实际 JVM 实现(如:HotSpot)会施加更严格的限制。
- HotSpot 虚拟机对 数组长度 的实际限制为 Integer.MAX_VALUE - 8。
- 某些版本为 Integer.MAX_VALUE - 2。
- 因为,数组对象头(Header)需要占用部分内存。
1、常见错误
- JVM 在分配数组时需预留 内存空间存储 对象头 (Object Header) 和 元数据,导致实际允许的数组长度 略小于理论最大值。
错误类型 | 触发场景 | 解决方案 |
---|---|---|
OutOfMemoryError: Java heap space | 若堆内存不足,但请求的数组长度合法 | 增大堆内存(-Xmx) |
OutOfMemoryError: Requested array size exceeds VM limit | 请求的数组长度, 超过 JVM 内部允许的最大值 | 减小数组长度或分块处理数据 |
2、测试到的数组的最大长度为 Integer.MAX_VALUE - 2
- 在内存足够时,当数组长度 大于等于 Integer.MAX_VALUE - 1 时。
- jvm 抛出了 OutOfMemoryError: Requested array size exceeds VM limit 异常。
7、Java 中数组复制的方式有哪几种?复制效率的比较?
1、System.arraycopy()
- 是一个本地方法,方法对应的实现不在当前文件里,而是在其他语言实现的的文件的。
- 如: C、C++ 中。
- 使用本地方法,理论上来说效率应该最高,
public static native void arraycopy(Object src, int srcPos, Object dest, int destPos, int length);
2、Arrays.copyOf()
- 本质上调用的是 System.arraycopy() 方法,也就是前一种方法,那么效率肯定比不上前一种数组复制方法。
public static <T,U> T[] copyOf(U[] original, int newLength, Class<? extends T[]> newType) {
@SuppressWarnings("unchecked")
T[] copy = ((Object)newType == (Object)Object[].class)
? (T[]) new Object[newLength] : (T[]) Array.newInstance(newType.getComponentType(), newLength);
System.arraycopy(original, 0, copy, 0, Math.min(original.length, newLength));
return copy;
}
3、clone()
- Object 类中的一个本地方法,这里虽然返回 Object,看着需要强制类型转换,但 Object 子类重写了这个方法,会返回相应的类型。
protected native Object clone() throws CloneNotSupportedException;
4、for 循环实现
- 最简单粗暴的一种方式,循环原始数组并直接赋值到目标数组中。
5、这几种方式的效率对比
package org.rainlotus.materials.javabase.array;
import java.util.Arrays;
/**
* @author zhangxw
* @since 1.0.0
*/
public class TestArrayCopy {
public static void testSystemArrayCopy(String[] orginal) {
long startTime = System.nanoTime();
String[] target = new String[orginal.length];
System.arraycopy(orginal, 0, target, 0, target.length);
long endTime = System.nanoTime();
System.out.println("使用 System.arraycopy 方法耗时:" + (endTime - startTime));
}
public static void testArraysCopyOf(String[] orginal) {
long startTime = System.nanoTime();
String[] target = new String[orginal.length];
target = Arrays.copyOf(orginal, orginal.length);
long endTime = System.nanoTime();
System.out.println("使用 Arrays.copyOf 方法耗时:" + (endTime - startTime));
}
public static void testClone(String[] orginal) {
long startTime = System.nanoTime();
String[] target = new String[orginal.length];
target = orginal.clone();
long endTime = System.nanoTime();
System.out.println("使用 clone 方法耗时:" + (endTime - startTime));
}
public static void testFor(String[] orginal) {
long startTime = System.nanoTime();
String[] target = new String[orginal.length];
for (int i = 0; i < orginal.length; i++) {
target[i] = orginal[i];
}
long endTime = System.nanoTime();
System.out.println("使用 for 循环耗时:" + (endTime - startTime));
}
public static void test1() {
//需要改变原始数组的大小
String[] original = new String[300];
Arrays.fill(original, "abcd");
System.out.println("原始数组的大小:" + original.length);
// 使用 System.arraycopy 方法耗时:8989
testSystemArrayCopy(original);
// 使用 Arrays.copyOf 方法耗时:10888
testArraysCopyOf(original);
// 使用 clone 方法耗时:9526
testClone(original);
// 使用 for 循环耗时:8974
testFor(original);
}
public static void test2() {
//需要改变原始数组的大小
String[] original = new String[3000];
Arrays.fill(original, "abcd");
System.out.println("原始数组的大小:" + original.length);
// 使用 System.arraycopy 方法耗时:9181
testSystemArrayCopy(original);
// 使用 Arrays.copyOf 方法耗时:19165
testArraysCopyOf(original);
// 使用 clone 方法耗时:18181
testClone(original);
// 使用 for 循环耗时:80933
testFor(original);
}
public static void test3() {
//需要改变原始数组的大小
String[] original = new String[30000];
Arrays.fill(original, "abcd");
System.out.println("原始数组的大小:" + original.length);
// 使用 System.arraycopy 方法耗时:57054
testSystemArrayCopy(original);
// 使用 Arrays.copyOf 方法耗时:135478
testArraysCopyOf(original);
// 使用 clone 方法耗时:111652
testClone(original);
// 使用 for 循环耗时:537069
testFor(original);
}
public static void test4() {
//需要改变原始数组的大小
String[] original = new String[300000];
Arrays.fill(original, "abcd");
System.out.println("原始数组的大小:" + original.length);
// 使用 System.arraycopy 方法耗时:570891
// 使用 System.arraycopy 方法耗时:599383
testSystemArrayCopy(original);
// 使用 Arrays.copyOf 方法耗时:1094669
testArraysCopyOf(original);
// 使用 clone 方法耗时:1091584
testClone(original);
// 使用 for 循环耗时:2854010
// 使用 for 循环耗时:3090976
testFor(original);
}
public static void test5() {
// 需要改变原始数组的大小
String[] original = new String[3000000];
Arrays.fill(original, "abcd");
System.out.println("原始数组的大小:" + original.length);
// 使用 System.arraycopy 方法耗时:8494116
// 使用 System.arraycopy 方法耗时:8295569
testSystemArrayCopy(original);
// 使用 Arrays.copyOf 方法耗时:17015338
testArraysCopyOf(original);
// 使用 clone 方法耗时:40843881
// 使用 clone 方法耗时:31550487
testClone(original);
// 使用 for 循环耗时:6533733
// 使用 for 循环耗时:3902672
testFor(original);
}
public static void test6() {
// 需要改变原始数组的大小
String[] original = new String[30000000];
Arrays.fill(original, "abcd");
System.out.println("原始数组的大小:" + original.length);
// 使用 System.arraycopy 方法耗时:99079980
// 使用 System.arraycopy 方法耗时:93092657
// 使用 System.arraycopy 方法耗时:91000678
// 使用 System.arraycopy 方法耗时:89735266
testSystemArrayCopy(original);
// 使用 Arrays.copyOf 方法耗时:132670467
testArraysCopyOf(original);
// 使用 clone 方法耗时:149369243
// 使用 clone 方法耗时:109118539
testClone(original);
// 使用 for 循环耗时:86380313
// 使用 for 循环耗时:117587703
// 使用 for 循环耗时:80211546
// 使用 for 循环耗时:77874887
testFor(original);
}
public static void main(String[] args) {
test1();
System.out.println("\n");
test2();
System.out.println("\n");
test3();
System.out.println("\n");
test4();
System.out.println("\n");
test5();
System.out.println("\n");
test6();
}
}
- 原始数组长度,不管是多少的时候,Arrays.copyOf() 的效率都比 System.arraycopy() 差。
- 原始数组长度,几百以内,for 循环表现十分优异。
- 百万以上,for 循环表现也十分优异。比 System.arraycopy() 效率都高
- 原始数组长度,在万、十万为单位的时候,System.arraycopy() 方法的优势体现出来了,力压其他三种方式。
8、连续的内存空间和相同类型的数据
1、 优点:支持“随机访问”
- 可通过数组的内存首地址、元素的下标值、数据类型占用内存的大小,可以快速计算出元素的内存地址。
2、为什么大多数编程语言中,数组要从 0 开始编号,而不是从 1 开始呢?
- 为了减少一次减法操作,数组选择了从 0 开始编号,而不是从 1 开始。
3、缺点:
1、在数组中删除、插入一个数据,为了保证连续性,就需要做大量的数据搬移工作。
2、当没有足够的连续内存空间分配给数组,导致“内存不足(out of memory)”。
9、 数组删除 – 特殊场景【JVM 标记清除算法】
- 数组 a[10] 中存储了 8 个元素:a,b,c,d,e,f,g,h。现在,我们要依次删除 a,b,c 三个元素。
- 为了避免 d,e,f,g,h 这几个数据会被搬移三次,我们可以先记录下已经删除的数据。
- 每次的删除操作并不是真正地搬移数据,只是记录数据已经被删除。
- 当数组满了,执行一次真正的删除操作,大大减少了删除操作导致的数据搬移。
10、容器能否完全替代数组?
- 1、Java ArrayList 无法存储基本类型。
- 如:int、long,需要封装为 Integer、Long 类,而 Autoboxing、Unboxing 则有一定的性能消耗。
- 所以,如果特别关注性能,或者希望使用基本类型,就可以选用数组。
- 2、如果数据大小事先已知,并且对数据的操作非常简单。
- 用不到 ArrayList 提供的大部分方法,也可以直接使用数组。
- 3、当要表示多维数组时,用数组往往会更加直观。
- 如:Object[][] array;而用容器的话则需要这样定义:ArrayList array。