08、数组

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。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值