JavaSE 基础语法 - 数组的定义与使用 - ① 在前面的学习中我们知道变量能够 保存数据例如intscore190;intscore285;intscore392;intscore488;intscore595;如果现在需要保存 5 个学生的成绩这样写并没有什么问题。但是如果需要保存 100 个学生、1000 个学生甚至更多学生的成绩呢难道要定义 100 个甚至上千个变量吗intscore1;intscore2;intscore3;...intscore1000;很显然这种方式不仅代码冗长而且维护起来也非常困难。例如想计算所有学生的平均成绩想统计最高分和最低分想遍历所有成绩如果使用普通变量几乎无法高效完成这些操作。因此Java 提供了一种能够存储一组相同类型数据的数据结构——数组Array。数组可以把多个相同类型的数据统一管理大大提高了开发效率。一、什么是数组什么是数组数组Array是一种用于存储多个相同数据类型元素的数据结构。简单来说数组就是一块连续的内存空间用来保存多个相同类型的数据。例如一个数组可以保存多个学生成绩9085928895也可以保存多个年龄1819202122还可以保存多个姓名TomJackLucyAlice在 Java 中可以使用一个数组统一管理这些数据而不需要定义多个独立的变量。例如int[]scores{90,85,92,88,95};这里scores 表示数组变量数组中保存了 5 个整数每一个数据称为数组元素Element。数组的特点数组具有以下几个特点。1、数组中只能存储相同类型的数据例如下面的数组是合法的。int[]nums{10,20,30};因为所有元素都是int类型。下面的代码则是不合法的。int[]nums{10,Hello,true};因为数组中出现了intStringboolean三种不同的数据类型。Java 不允许同一个数组存放不同类型的数据。2、数组长度固定数组一旦创建长度就不能再改变。例如int[]numsnewint[5];表示创建了一个长度为 5 的数组。以后只能存放 5 个元素。不能变成6 个也不能缩小为3 个如果需要更多空间只能重新创建新的数组。后续学习 ArrayList 时就能够实现动态扩容。3、数组中的元素有顺序数组中的每个元素都有自己的位置。Java 使用索引来表示元素的位置。例如int[]nums{10,20,30,40};对应关系如下索引0123元素10203040需要注意Java 数组的索引从 0 开始而不是从 1 开始。例如System.out.println(nums[0]);输出10而System.out.println(nums[3]);输出404、数组属于引用数据类型很多同学学习数组时会认为数组和int、double一样也是一种普通数据类型。实际上并不是。数组属于引用数据类型Reference Type。例如int[]numsnewint[5];这里真正创建的是数组对象而变量nums保存的是数组对象在内存中的地址引用。这也是为什么数组作为方法参数时修改数组元素会影响原数组。关于数组为什么属于引用类型以及数组在 JVM 中是如何存储的我们后续讲。数组的应用场景数组适用于保存学生成绩保存考试分数保存商品价格保存多个姓名保存每天的气温保存多个对象。凡是需要保存一组相同类型的数据都可以考虑使用数组。二、为什么需要数组学习数组之前我们先来看一个实际需求。假设需要保存 10 个学生的 Java 成绩。如果不用数组只能这样定义变量intscore190;intscore285;intscore392;intscore488;intscore595;intscore676;intscore781;intscore889;intscore994;intscore1087;虽然能够保存数据但会带来很多问题。例如问题一变量太多如果有100 个学生。就需要100 个变量。程序将变得非常臃肿。问题二无法方便地遍历例如想输出所有成绩。只能这样写System.out.println(score1);System.out.println(score2);System.out.println(score3);...代码重复非常繁琐。问题三统计困难例如计算平均分。需要(score1score2score3...score10)/10如果学生人数发生变化还需要重新修改代码。使用数组之后使用数组只需要定义一个变量。int[]scores{90,85,92,88,95,76,81,89,94,87};输出所有成绩for(inti0;iscores.length;i){System.out.println(scores[i]);}计算总分intsum0;for(inti0;iscores.length;i){sumscores[i];}可以看到使用数组后数据统一管理可以使用循环遍历更容易统计和计算代码更加简洁。因此数组最大的作用就是使用一个变量统一管理多个相同类型的数据提高程序的开发效率和可维护性。三、数组的定义在 Java 中定义数组主要有两种方式动态初始化Dynamic Initialization静态初始化Static Initialization3.1 动态初始化动态初始化是指只指定数组长度由系统自动为数组元素赋默认值。语法格式数据类型[]数组名new数据类型[数组长度];例如int[]numsnewint[5];表示创建一个能够存放5 个整数的数组。此时数组中的元素都有默认值。00000再例如double[]pricesnewdouble[3];默认值0.00.00.03.2 静态初始化静态初始化是指创建数组的同时直接指定数组中的元素。语法格式数据类型[]数组名new数据类型[]{元素1,元素2,元素3...};例如int[]numsnewint[]{10,20,30,40,50};数组长度由元素个数自动决定。这里数组长度就是5静态初始化简写Java 提供了更简洁的写法。例如int[]nums{10,20,30,40,50};这是开发中最常见的数组定义方式。需要注意这种简写形式只能在声明数组的同时使用。例如下面的代码是错误的int[]nums;nums{10,20,30};如果数组已经声明则必须使用完整写法int[]nums;numsnewint[]{10,20,30};3.3 动态初始化与静态初始化的区别比较项动态初始化静态初始化是否指定元素否是是否指定长度是否由元素个数决定元素默认值有无由程序员指定适用场景暂时不知道具体数据已知全部数据一般来说已知数据内容优先使用静态初始化只知道需要多少个元素不知道具体值使用动态初始化。四、数组的组成一个完整的数组主要由以下几个部分组成。1、数组变量数组变量用于保存数组对象的引用地址值。例如int[]nums{10,20,30};这里nums就是数组变量。2、数组对象真正存放数据的是数组对象。例如┌────────────────────┐ │10│20│30│ └────────────────────┘数组对象创建后会存储在 JVM 的堆内存中。3、数组元素Array Element数组中的每一个数据都称为数组元素。例如int[]nums{10,20,30};其中102030都是数组元素。4、数组索引索引用于访问数组中的元素。索引从 0 开始。例如nums[0]表示第一个元素。nums[2]表示第三个元素。访问数组时索引不能超过数组的范围否则会抛出数组越界异常ArrayIndexOutOfBoundsException五、JVM 内存分析前面我们已经学习了数组的定义和初始化方式。例如int[]nums{10,20,30};这行代码看起来非常简单但在 JVM 内部却发生了很多事情。例如数组到底存放在哪里nums 变量保存的是什么为什么数组属于引用数据类型为什么修改数组元素会影响原数组要是想知道这些就要先了解 JVM 的内存结构。5.1 JVM 内存区域简介当 Java 程序运行时JVMJava Virtual MachineJava 虚拟机会向操作系统申请一块内存用于存放程序运行过程中产生的数据。内存是一段连续的存储空间主要用来存储程序运行时数据的。比如程序运行时代码需要加载到内存程序运行产生的中间数据要存放在内存程序中的常量也要保存有些数据可能需要长时间存储而有些数据当方法运行结束后就要被销毁如果对内存中存储的数据不加区分的随意存储那对内存管理起来将会非常麻烦。比如一个非常凌乱的房间和一个非常整洁的房间如果说需要找一个东西那么在整洁的房间里面就非常容易找到。因此JVM也对所使用的内存按照功能的不同进行了划分程序计数器 (PC Register)只是一个很小的空间, 保存下一条执行的指令的地址。虚拟机栈(JVM Stack)与方法调用相关的一些信息每个方法在执行时都会先创建一个栈帧栈帧中包含有局部变量表、操作数栈、动态链接、返回地址以及其他的一些信息保存的都是与方法执行时相关的一些信息。比如局部变量。当方法运行结束后栈帧就被销毁了即栈帧中保存的数据也被销毁了。本地方法栈(Native Method Stack)本地方法栈与虚拟机栈的作用类似。只不过保存的内容是Native方法的局部变量。在有些版本的 JVM 实现中(例如HotSpot)本地方法栈和虚拟机栈是一起的。堆(Heap)JVM所管理的最大内存区域使用 new 创建的对象都是在堆上保存 (例如 new int[]{1, 2, 3} )堆是随着程序开始运行时而创建随着程序的退出而销毁堆中的数据只要还有在使用就不会被销毁。方法区(Method Area)用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。方法编译出的的字节码就是保存在这个区域。现在我们只简单关心堆和虚拟机栈这两块空间后序JVM中还会更详细介绍。5.2 栈内存栈内存主要负责方法调用方法参数局部变量。例如publicstaticvoidmain(String[]args){intage18;}变量age直接存放在栈内存中。当main()方法执行结束后整个栈帧都会被销毁。因此局部变量也随之消失。5.3 堆内存堆内存主要负责存放对象数组字符串对象等引用类型数据。例如int[]numsnewint[3];执行后真正创建的是数组对象这个数组对象会存放到堆而nums这个变量仍然位于栈因此数组变量和数组对象不是同一个东西。5.4 一个数组的内存分析来看下面这段代码。publicclassDemo{publicstaticvoidmain(String[]args){int[]numsnewint[3];}}程序运行后第一步执行newint[3]JVM 会在堆内存中创建一个数组对象。例如这里三个元素都为0因为int 类型默认值就是0第二步变量nums保存在栈内存中。它里面保存的是数组对象的地址值例如因此数组变量保存的是地址值而真正的数据保存在堆内存中的数组对象里。这也是为什么数组属于引用数据类型。5.5 修改数组元素例如int[]numsnewint[3];nums[0]100;nums[1]200;nums[2]300;此时堆内存变成而栈中的nums始终指向0x100这个地址。因此修改的是数组对象中的数据而不是 数组变量5.6 两个数组的内存分析例如int[]arr1{1,2,3};int[]arr2{4,5,6};运行后栈内存堆内存可以看到两个数组分别对应两个不同的数组对象。因此修改arr1[0]100;不会影响arr2因为它们指向的是不同的对象。5.7 两个引用指向同一个数组来看下面代码。int[]arr1{1,2,3};int[]arr2arr1;很多同学会认为创建了两个数组。实际上并没有这里只创建了一个数组对象。第二行代码只是把arr1保存的地址值复制给arr2内存如下可以看到虽然有两个变量arr1、arr2但是它们都指向同一个数组对象。如果执行arr2[1]999;堆内存变成此时输出System.out.println(arr1[1]);结果999因为arr1 和 arr2 访问的是同一个数组对象。修改其中一个引用看到的数据本质上就是修改了同一个对象因此另一个引用也能看到最新的数据。六、数组参数传递例如以下代码publicclassDemo{publicstaticvoidprintArray(int[]arr){for(inti0;iarr.length;i){System.out.print(arr[i] );}}publicstaticvoidmain(String[]args){int[]nums{10,20,30};printArray(nums);}}这里的printArray(nums);把数组传递给了方法那么是把整个数组传递进去了吗还是把数组复制了一份为什么方法中能够访问原来的数组6.1 数组作为方法参数数组属于引用数据类型因此方法参数也可以定义为数组类型。例如publicstaticvoidprintArray(int[]arr){}这里int[]arr就是一个数组类型的形参。调用时int[]nums{10,20,30};printArray(nums);这里nums就是实参。需要注意的是这里传递的并不是整个数组对象而是数组变量中保存的地址值对象引用。6.2 数组参数传递过程来看下面这段代码publicclassDemo{publicstaticvoidprint(int[]arr){System.out.println(arr[0]);}publicstaticvoidmain(String[]args){int[]nums{10,20,30};print(nums);}}程序执行过程如下。第一步创建数组执行int[]nums{10,20,30};JVM 创建数组对象第二步调用方法执行print(nums);很多初学者误以为Java 把整个数组复制了一份。其实不是。Java 只是把nums里面保存的地址值0x100复制了一份。于是内存情况为可以看到这里有两个变量nums、arr但是它们保存的地址值相同。因此共同指向同一个数组对象。第三步访问数组元素执行System.out.println(arr[0]);实际上访问的是地址0x100 的第一个元素因此输出106.3 为什么修改数组会影响原数组来看下面代码publicclassDemo{publicstaticvoidchange(int[]arr){arr[0]100;}publicstaticvoidmain(String[]args){int[]nums{10,20,30};change(nums);System.out.println(nums[0]);}}运行结果100为什么因为两个变量共同指向同一个数组。6.4 为什么重新创建数组不会影响原数组再来看一个容易混淆的例子。publicclassDemo{publicstaticvoidchange(int[]arr){arrnewint[]{1,2,3};}publicstaticvoidmain(String[]args){int[]nums{10,20,30};change(nums);System.out.println(nums[0]);}}运行结果10为什么运行在没调用方法前nums 指向地址 0x100 的对象。调用方法在没执行arr new int[]{1,2,3};前arr还是指向地址 0x100 的对象。执行arr new int[]{1,2,3};创建了新数组随后arr就指向了新对象的地址。