Java数组详解/从JVM理解数组/数组反转/随机排名/数组在计算机如何存储

发布于:2025-03-07 ⋅ 阅读:(16) ⋅ 点赞:(0)

本文详细讲解了数组的定义、数组的访问方法、数组的遍历、静态数组和动态数组、以及数组中的自动类型转换、引用类型指向数组的地址、以及从JVM理解数组、空指针异常、数组反转、随机排名的案例。

数组是存放在连续内存空间上的相同类型数据的集合。

数组可以方便的通过下标索引的方式获取到下标对应的数据。

举一个字符数组的例子,如图所示:

需要两点注意的是

  • 数组下标都是从0开始的。

  • 数组内存空间的地址是连续的

正是因为数组在内存空间的地址是连续的,所以我们在删除或者增添元素的时候,就难免要移动其他元素的地址。


一、数组的定义和访问

1.静态初始化数组

  • 定义数组的时候直接给数组赋值。

完整模式:
数据类型[] 数组名 = new 数据类型[]{元素1,元素2,元素3};
int[] a = new int[]{1,2,3,4,5};
​
简化格式:
数据类型[] 数组名 = {元素1,元素2,元素3};
double[] score = {85.5,75.5,90.5};
​
其他写法:
左边:数据类型[] 数组名 也可以写成 数据类型 数组名[]
源码中可能会看到这种写法。
​

[重点]数组中的自动类型转换

虽然,数组只能保存相同数据类型的数据。

但,如果存储字符类型,编译的时候也不会报错

  • 我们知道byte、short、char能自动类型转换为int

  • float能自动类型转换为double类型。

数组中也可以:

int[] numbers = {10, (byte)20, (short)30, 'A'}; 
// 等价于 {10, 20, 30, 65},自动转换为int
double[] decimals = {1, 2.5f, 3L}; 
// 等价于 {1.0, 2.5, 3.0},自动转换为double

System.out.println(Arrays.toString(ages1)); // 输出 [12, 24, 36, 65]

二、数组在计算机中的基本原理

数组是引用数据类型,存储数组在内存中的地址信息。

int[] args = new int[]{1,2,3,4};

[重点]这里args是一个数组的对象,数组是引用数据类型,存储数组在内存中的地址信息。

[重点]数组地址分析

  • 那么我们打印这个数组会发生什么?

打印出数组(引用数据类型)的地址。

[I@776ec8df]

//@ 占位符,读法at

//[ 数组

//I int

//776ec8df 内存地址

详情见:Java中的对象存储形式及控制台输出哈希码问题

回顾:

1.数组的静态初始化的写法和特点是什么?

我们再默写一遍
数据类型[] 数组名 = new 数据类型[]{1,2,3};
​
数据类型[] 数组名 = {1,2,3,4};
​

如上,能够简写,或者源码写法。

数据类型 数组名[] = {1,2,3,4};

2.定义数组我们说了那几个注意点?

1.什么类型的数组就存放什么样的数据。也有特例,不过能编译。一般,数组只能存储一种数据类型的数据

2.可以源码写法

3.数组属于什么类型?数组变量名中存储的是什么?

数组属于引用数据类型,存储的是数组在内存中的地址信息


三、数组的访问

数组名称[索引]

数组的长度

数组名.length

[重点]末尾元素:数组名.length - 1的前提条件

ArrayIndexOutOfBoundsException

前提数组中的元素>0

回顾:

1.如何访问数组元素?

数字名[索引]

2.如何访问数组长度?

数组名.length

3.数组的最大索引/数组末尾元素是多少?

数组名.length -1

4.如果访问数组的时候,使用索引超过数组的最大索引会出现什么问题?

ArrayIndexOutOfBoundsException数组索引越界异常。

四、数组遍历

为什么,因为要进行数组元素的比较。

快捷键:args.fori,选择fori能自动快速生成遍历数组

案例-求和

package com.itheima.array.define;
​
public class ArrayTest4 {
    public static void main(String[] args) {
//        某员工的销售额是16,26,36,6,100,请计算他们部门的总销售额。
        int[] arg = {16,26,36,6,100};
//        求和思想:
        /*1.定义变量
        * 2.进行累加
        * */
        int sum = 0;
        for (int i = 0; i < arg.length; i++) {
            sum += arg[i];
        }
        System.out.println(sum);
​
    }
}
​

2.动态数组初始化

其实是没有规定元素是什么,而是规定元素的数据类型和个数

数据类型[] 数组名 = mew 数据类型[元素个数];
int args = new int[3];

args[0] =10;//后赋值

我们知道,对象都存在于堆内存中,这里动态数组也是,存在堆内存中,直接打印数组,会出现在堆内存中的地址(哈希码形式)。

注意:

不要混了

[重点]动态初始化数组元素默认值规则:
分类 数据类型 默认值
基本类型 byte, short, char(Unicode是\u0000)显示空白字符, int, long 0
浮点型 float, double 0.0
布尔型 boolean false
引用数据类型 , 接口, 数组, String null

其实String也是对象

注意boolean类型是false。

[重点]注意char数组的默认值为\u0000(Unicode空字符)对应的ASCII码值为0。控制台输出不是0

但直接打印时,\u0000不会显示为字符0,而是显示为“空白”(不可见字符)。

而是这个符号,代表Unicopde中的\u0000,不可见符号。

重点
package com.itheima.array.define;
​
public class ArrayDemo5 {
    public static void main(String[] args) {
        //注意char是Unicode编码(\u0000)
        char []arr = new char[3];
        System.out.println(arr[0]);
        System.out.println("===");
​
        int []arr1 = new int[3];
        System.out.println(arr1[0]);
        System.out.println("===");
​
        byte[]arr3 = new byte[3];
        System.out.println(arr3[0]);
        System.out.println("===");
​
        short[]arr2 = new short[3];
        System.out.println(arr2[0]);
        System.out.println("===");
​
​
        long[] arr31 = new long[3];
        System.out.println(arr31[0]);
        System.out.println("===");
​
        float[] arr4 = new float[3];
        System.out.println(arr4[0]);
        System.out.println("===");
​
        double[] arr5 = new double[3];
        System.out.println(arr5[0]);
        System.out.println("===");
​
        String[] arr6 = new String[3];
        System.out.println(arr6[0]);
        System.out.println("===");
    }
}
​

回顾:

1.动态初始化数组的写法是什么?

数据类型[] 数组名 = new 数据类型[长度];
int [] args = new int[5]

2.动态初始化数组的默认值是什么?

byte、short、int、char、long是0

float、double是0.0

boolean是false

String、类、接口、数组等引用数据类型是null。

3.两种数组定义的方法各自适用于什么业务场景?

静态初始化数组:适用于确定是数组元素的场景;

动态初始化:适用于只确定数组元素个数的场景


案例-求评委打分平均分

package com.itheima.array.define;
​
import java.sql.SQLOutput;
import java.util.Scanner;
​
public class ArrayTest5 {
    public static void main(String[] args) {
        //需求:现在有6个评委,给1名选手进行打分。请你求该选手的平均分
​
        //首先,数组存储
        //先解决单次一个人录入,然后存入数组
        //之后累加求和
        //最后求平均分sum/数组长度
​
        //完成单次录入
    /*    int[] arr = new int[6];
        Scanner sc = new Scanner(System.in);
        arr[0]= sc.nextInt();
        System.out.println("第一个评委的打分是"+ arr[0]);*/
​
        int[] arr = new int[6];
        //改为多次录入ctrl+alt+t
        for (int i = 0; i < arr.length; i++) {//0-5索引
//            System.out.println("请输入" + (i + 1) + "个评委的打分");
​
            Scanner sc = new Scanner(System.in);
            arr[i] = sc.nextInt();
            System.out.println("第" + (i + 1) + "个评委的打分是" + arr[i]);
        }
//        得到六个评委打分并存入数组
        //累加求和
        int sum = 0;
        //快捷键 arr.fori
        for (int i = 0; i < arr.length; i++) {
​
            sum += arr[i];
        }
        System.out.println("评委总分" + sum);
​
        //求平均分
        int avg = sum / arr.length;
        System.out.println("评委打分的平均分是" + avg);
    }
}
​

数组在计算机中的执行原理

JVM的内存划分

  • 方法区

  • 本地方法栈

  • 寄存器

方法区:加载字节码.class文件

栈内存:方法执行的时候进栈,方法中的变量存储也在栈内存。

堆内存:new出来的东西会在这块内存中开辟空间并产生地址。

讲解:

栈内存给main方法分配区域(栈帧)那么a就在栈内存中划分了一块区域,

执行打印,这里根据a找到区域,然后打印执行。

数组创建,那么就在堆内存中创建数组对象,数组对象也有地址,这个地址也会存到栈内存中。

通过arr存在栈内存的这个地址,找这个数组对象。

  • 其实arr是引用变量,是存在栈内存中的。

数组赋值之后真正产生变化的是堆内存的数据,而不栈中,栈中只有地址,

最后打印。

回顾:

方法区:执行class文件

执行方法,进入栈内存

new对象进入堆内存,

2.简单说说inta=20;int[]arr=new int[3]这两行代码的执行原理?

a是变量,直接放在栈内存中,a变量中存储的数据就是20这个值。

new int[3]是创建一个数组对象,会在堆内存中开辟区域存储3个整数。


[重点]案例:多个变量指向同一个数组

引用变量:引用数据类型的变量

1.多个变量指向同一个数组

引用变量arr1和arr2都是指向堆内存中同一个区域,那么其实赋值给arr2[1]==99;后修改的是堆内存中存储的数据。

2.赋值null

详见代码

package com.itheima.memory;
​
public class Demo1 {
    public static void main(String[] args) {
        //目标:多个变量指向同一个数组对象的执行原理
        //这个变量指的是引用变量,引用变量,就是引用数据类型的变量。数组就是引用数据类型
​
            //创建两个数组
        int arr1[] = {1,2,3};
        int arr2[] = arr1;//我们将arr1赋值给arr2,实际上是让 存在栈内存中的引用变量 指向 同一堆内存中的对象,也就是数组的元素。
        System.out.println(arr1);
        System.out.println(arr2);
        System.out.println("可以看到,两个数组的引用变量的地址是一致的,指向的是同一个堆内存中的区域");
            //如果我们修改arr2的一个元素,那arr1会改变吗?
        arr2[0] = 99;
        System.out.println(arr1[0]);//arr1也改变了
        System.out.println(arr1);
        System.out.println(arr2);//
        //以上,证明了arr1和arr2两个引用数据类型的变量,指向的是堆内存中的同一块区域,而且修改后同时发生改变。
​
        System.out.println("============【重点】如果赋值null===============");
        //2.如果赋值null
        arr2 = null;
//        System.out.println(arr2.length);
        System.out.println(arr1.length);
        System.out.println("编译不报错,因为arr2数组有length方法,输出arr2.length会运行报错");
        System.out.println("但是输出arr1.length不会报错,因为实际上修改的是引用变量arr2,arr1指向堆内存中的对象没有发生变化");
    }
​
}
​

执行过程图解:

[重点]空指针异常代表:引用数据类型的变量,该有东西,但是为空

[重点]回顾:

1.多个数组变量,指向同一个数组对象的原因是什么?需要注意什么?

因为创建数组的时候,这两个引用数据类型变量,指向的都是同一个数组对象。存储的都是同一个数组对象的地址。

注意:多变量修改的都是同一个数组对象的数据

2.如果某个数组变量中存储的null,代表什么意思?需要注意什么?

代表这个数组变量(引用类型变量),没有指向同一个对象。

可以输出这个变量,但是访问数组元素或获取数组长度,报NullPointerException

代码如下:

package com.itheima.memory;
​
public class Demo2 {
    //2.如果某个数组变量中存储的null,代表什么意思?需要注意什么?
    public static void main(String[] args) {
​
        int arr[] = new int[]{1,2};
    //引用变量赋值为null
        arr = null;
        System.out.println(arr);//可以打印出null
​
        int length1= arr.length;//访问数组长度:NullPointerException
        System.out.println(length1);
​
        int num = arr[0];//获取数组元素:NullPointerException
        System.out.println(num);
​
    }
​
}

代码如下:

package com.itheima.memory;
​
public class Demo1 {
    public static void main(String[] args) {
        //目标:多个变量指向同一个数组对象的执行原理
        //1.修改元素
        //这个变量指的是引用变量,引用变量,就是引用数据类型的变量。数组就是引用数据类型
​
            //创建两个数组
        int arr1[] = {1,2,3};
        int arr2[] = arr1;//我们将arr1赋值给arr2,实际上是让 存在栈内存中的引用变量 指向 同一堆内存中的对象,也就是数组的元素。
        System.out.println(arr1);
        System.out.println(arr2);
        System.out.println("可以看到,两个数组的引用变量的地址是一致的,指向的是同一个堆内存中的区域");
            //如果我们修改arr2的一个元素,那arr1会改变吗?
        arr2[0] = 99;
        System.out.println(arr1[0]);//arr1也改变了
        System.out.println(arr1);
        System.out.println(arr2);//
        //以上,证明了arr1和arr2两个引用数据类型的变量,指向的是堆内存中的同一块区域,而且修改后同时发生改变。
​
        System.out.println("============【重点】如果赋值null===============");
        //2.如果赋值null
        arr2 = null;
//        System.out.println(arr2.length);
        System.out.println(arr1.length);
        System.out.println("编译不报错,因为arr2数组有length方法,输出arr2.length会运行报错");
        System.out.println("但是输出arr1.length不会报错,因为实际上修改的是引用变量arr2,arr1指向堆内存中的对象没有发生变化");
    }
​
}
​

编译不报错,因为arr2数组有length方法,输出arr2.length会运行报错

但是输出arr1.length不会报错,因为实际上修改的是引用变量arr2,arr1指向堆内存

的对象没有发生变化。

[重点]案例,求数组元素中的最大值,最小值

要点:确定固定值max。以及。从第二个位置开始遍历元素。

1.首先用MAX变量作为数组中的一个元素,默认arr[0]。(就是一个普通的固定值,默认为0都可以)

2.从第二个位置开始遍历数组的元素,比较后,赋值到MAX变量中。因为是循环语句套在外面,跳出不了循环,那么会一直比较,所有的元素。大的就再次赋值,小的就继续循环。会直到循环结束。


package com.itheima.array;
​
public class ArrayTest3 {
    public static void main(String[] args) {
        //数组,求最大值
        int arr[] = {1,2,3,4,5,5,6,7,8,10,6};
​
        int max = arr[0];//仅仅是一个固定值而已,赋值为数组中的一个元素,这样普遍性更强,否则如果是0,0可能是数组元素中最大或最小的。
        //arr.length-1 是指数组最后一个元素
        for (int i = 0; i < arr.length; i++) {
            if(max < arr[i]){
                max = arr[i];
            }
        }
        System.out.println(max);
    }
}
​

[重点]数组反转

int temp1 = arr1[0];//仅仅是一个固定值,这里赋值0也可以。表示成这样,方便阅读。

package com.itheima.array.cases;
​
public class Test2 {
    public static void main(String[] args) {
        //1.变量互换
        int a = 250;
        int b = 985;
        int temp = 0;
​
        temp = a;
        a = b;
        b = temp;
        System.out.println("a:" + a + "\n" + "b:" + b);
        System.out.println("======2.数组元素互换==============");
        //2.数组元素互换
        //让第一个和倒数第一个互换,第二个和倒数第二个互换,第三个和倒数第三个互换...
        int arr1[] = new int[]{10, 20, 30, 40, 50};
        int temp1 = arr1[0];//仅仅是一个固定值,这里赋值0也可以。表示成这样,方便阅读
        for (int i = 0, j = (arr1.length - 1); i < j; i++, j--) {//当i不小于j的时候进行结束,也就是刚好等于的时候
            //相当于变量互换
            temp1 = arr1[j];
            arr1[i] = arr1[j];
            arr1[i] = temp1;
        }
        //输出
        for (int i = 0; i < arr1.length; i++) {
            System.out.print(arr1[i] + "\t");
        }
    }
}
​

[难点]随机排名

package com.itheima.array.cases;
​
import java.util.Random;
import java.util.Scanner;
​
public class ArrayRandomRange_import_3 {
    public static void main(String[] args) {
        //非常熟练
//需求:现在有五名创业者需要进行路演,他们分别有工号,但是为确保公平性,现在想随机上台路演。请你实现功能
        int c_num[] = new int[5];
        Scanner sc = new Scanner(System.in);
​
        //输入5个工号
        for (int i = 0; i < c_num.length; i++) {
            System.out.println("请你输入第"+(i+1)+"个工号");
            int num = sc.nextInt();
            c_num[i] = num;
        }
​
        //随机工号
        for (int j = 0; j < c_num.length; j++) {
            Random r = new Random();
            int ranDomIndex = r.nextInt(c_num.length);//nextInt()产生的是[0,4]与数组对应,不用[加减法]。
            //交换:(以随机索引的数组)=>数组的随机的另一个元素  与 本数组元素 按照迭代顺序交换。[首位呼应一条龙]
            //实际是:该数组的元素相互进行交换。
            //只是ranDomIndex是独立事件,会有重复数,会重复交换,导致随机的不是很彻底。
            int temp = 0;
            temp = c_num[j];
            c_num[j] = c_num[ranDomIndex];//先写,现在看就是数组俩元素进行交换,只是这个地方改成了ranDdonIndex
            c_num[ranDomIndex] = temp;
        }
        
        //输出
        for (int k = 0; k < c_num.length; k++) {
            System.out.print(c_num[k] +" ");       
        }
    }
}
​