Java数组详解/从JVM理解数组/数组反转/随机排名/数组在计算机如何存储
本文详细讲解了数组的定义、数组的访问方法、数组的遍历、静态数组和动态数组、以及数组中的自动类型转换、引用类型指向数组的地址、以及从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] +" "); } } }