目录
1、前言
经过前几节Java基本语法的洗礼,相信大家对Java有了初步的了解,今天我们接着分享下一个知识点—数组,其中包括数组的基本概念、数组的初始化、数组的使用等内容,干货满满呦。
2、数组的基本概念
2.1 为什么使用数组
如果现在我们要存储5位同学的考试成绩,我们当然可以用如下代码:
public static void main(){
int a = 90;
int b = 80;
int c = 70;
int d = 95;
int d = 98;
System.out.println(a);
System.out.println(b);
System.out.println(c);
System.out.println(d);
}
- 这样创建局部变量的方法当然可以,但是如果存储的是500个同学的成绩呢,要创建500个变量吗?。仔细观察这些学生成绩发现:所有成绩的类型都是相同的,那Java中存在可以存储相同类型多个数据的类型吗?这就是本次blog需要详细讲解的数组。
2.2 什么是数组
- 数组:可以看成是相同类型元素的一个集合。
注意:
- 数组中存放的元素其类型相同。
- 数组的空间是连续的(无论是一维数组还是二维数组)。
- 每个数据的空间有自己的编号,起始位置的编号为0,即数组的下标。
那么我们该如何创建数组呢?
2.3 数组的创建及初始化
2.3.1 数组的创建
T[] 数组名 = new T[N];
其中:
- T:表示数组中存放元素的类型(如int、double等);
- T[]:表示数组的类型;
- N:表示数组的长度。
- 举例(我们用array数组这个英文单词表示数组名(下同),这个可以根据实际需求进行调整):
int[] array1 = new int[10]; // 创建一个可以容纳10个int类型元素的数组
double[] array2 = new double[5]; // 创建一个可以容纳5个double类型元素的数组
String[] array3 = new double[3]; // 创建一个可以容纳3个字符串元素的数组
2.3.2 数组的初始化
1、引用变量与对象:
- 这里涉及到了引用变量和对象的概念,我们放在类与对象的blog中进行详细讲解,这里只需要了解:array就是一个引用变量,他里面存储的是数组的地址,并且指向数组这个对象,对象中存储我们想要存储的数据即可。
2、数组的初始化主要分为动态初始化以及静态初始化。
- 动态初始化:
在创建数组时,指定数组中元素的个数,而不指定数组的内容,内容会被默认初始化为0。
int[] array = new int[3];
注意:区别于C语言,Java中引用变量存放在虚拟机栈区中,数组的内容存放在堆区中,在本次blog中的2.1节会简单介绍下内存中各个分区,看到这里还不点个赞嘛!
- 下面的图更方便大家理解动态初始化,引用变量array存放的是数组的地址,array指向在堆区开辟的对象,对象中存储的内容默认都是0;
- 静态初始化:
在创建数组时不直接指定数据元素个数,而直接将具体的数据内容进行指定。
- 语法格式:
T[] 数组名称 = new T[]{data1, data2, data3, ..., datan};
int[] array = new int[]{1,2,3};
或者它的简写形式:
T[] 数组名称 = {data1, data2, data3, ..., datan};
- 两种形式等价,相当于创建了一个对象。
- 代码举例:
int[] array1 = new int[]{0,1,2,3,4,5,6,7,8,9};
double[] array2 = new double[]{1.0, 2.0, 3.0, 4.0, 5.0};
String[] array3 = new String[]{"hell", "Java", "!!!"};
- 下面的图更方便大家理解,我们定义了一个引用变量数组,指向一个对象,对象里面存放的内容是:1、2、3。
3、注意事项
- 静态初始化虽然没有指定数组的长度,编译器在编译时会根据{}中元素个数来确定数组的长度。
- 静态初始化时, {}中数据类型必须与[]前数据类型一致。
- 静态初始化可以简写,省去后面的new T[]。
- 如果没有对数组进行初始化,数组中元素有其默认值。
- 如果数组中存储元素类型为基类类型,默认值为基类类型对应的默认值,比如:
- 如果数组中存储元素类型为引用类型,默认值为null
2.4 数组的使用
2.4.1 数组中元素访问
- 数组在内存中是一段连续的空间,空间的编号都是从0开始的,依次递增,该编号称为数组的下标,数组可以通过下标访问其任意位置的元素。比如:
int[]array = new int[]{10, 20, 30, 40, 50};
System.out.println(array[0]);
System.out.println(array[1]);
System.out.println(array[2]);
System.out.println(array[3]);
System.out.println(array[4]);
// 也可以通过[]对数组中的元素进行修改
array[0] = 100;
System.out.println(array[0]);
注意:
- 数组是一段连续的内存空间,因此支持随机访问,即通过下标访问快速访问数组中任意位置的元素。
- 下标从0开始,介于[0, N)之间不包含N,N为元素个数,不能越界,否则会报出下标越界异常。
int[] array = {1, 2, 3};
System.out.println(array[3]); // 数组中只有3个元素,下标一次为:0 1 2,array[3]下标越界
// 执行结果
Exception in thread "main" java.lang.ArrayIndexOutOfBoundsException: 100
at Test.main(Test.java:4)
- 抛出了 java.lang.ArrayIndexOutOfBoundsException 也就是数组越界异常. 使用数组一定要下标谨防越界。
2.4.2 遍历数组
- 所谓 "遍历" 是指将数组中的所有元素都访问一遍, 访问是指对数组中的元素进行某种操作,比如:打印。
int[]array = new int[]{10, 20, 30, 40, 50};
System.out.println(array[0]);
System.out.println(array[1]);
System.out.println(array[2]);
System.out.println(array[3]);
System.out.println(array[4]);
- 上述代码可以起到对数组中元素遍历的目的,但问题是:
- 如果数组中增加了一个元素,就需要增加一条打印语句;
- 如果输入中有100个元素,就需要写100个打印语句;
- 如果现在要把打印修改为给数组中每个元素加1,修改起来非常麻烦;
- 通过观察代码可以发现,对数组中每个元素的操作都是相同的,则可以使用循环来进行打印。
int[]array = new int[]{10, 20, 30, 40, 50};
for(int i = 0; i < 5; i++){
System.out.println(array[i]);
}
- 改成循环之后,上述三个缺陷可以全部2和3问题可以全部解决,但是无法解决问题1。那能否获取到数组的长度呢?
注意:在数组中可以通过 数组对象.length 来获取数组的长度
int[]array = new int[]{10, 20, 30, 40, 50};
for(int i = 0; i < array.length; i++){
System.out.println(array[i]);
}
- 也可以使用 for-each 遍历数组:左边是要存储的变量x :右边是数组名(array);会遍历整个数组,存到x当中,遍历一个,存储一个。
int[] array = {1, 2, 3};
for (int x : array) {
System.out.println(x);
}
- for-each 是 for 循环的另外一种使用方式. 能够更方便的完成对数组的遍历. 可以避免循环条件和更新语句写错。
- 如果你在循环里面,需要通过下标判断数据,就不适合用for each。
3. Java中的内存分布
3.1 JVM的内存分布
- JVM也对所使用的内存按照功能的不同进行了划分:
- 程序计数器 (PC Register): 只是一个很小的空间, 保存下一条执行的指令的地址。
- 虚拟机栈(JVM Stack): 与方法调用相关的一些信息,每个方法在执行时,都会先创建一个栈帧,栈帧中包含有:局部变量表、操作数栈、动态链接、返回地址以及其他的一些信息,保存的都是与方法执行时相关的一些信息。比如:局部变量。当方法运行结束后,栈帧就被销毁了,即栈帧中保存的数据也被销毁了。
- 本地方法栈(Native Method Stack): 本地方法栈与虚拟机栈的作用类似. 只不过保存的内容是Native方法的局部变量. 在有些版本的 JVM 实现中(例如HotSpot), 本地方法栈和虚拟机栈是一起的。
- 堆(Heap): JVM所管理的最大内存区域. 使用 new 创建的对象都是在堆上保存 (例如前面的 new int[]{1, 2,3} ),堆是随着程序开始运行时而创建,随着程序的退出而销毁,堆中的数据只要还有在使用,就不会被销毁。
- 方法区(Method Area): 用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据,方法编译出的的字节码就是保存在这个区域。
现在我们只简单关心堆 和 虚拟机栈这两块空间(在2.3.2节中已经介绍过),后序JVM中还会更详细介绍。
3.2 引用类型变量
- 基本数据类型创建的变量,称为基本变量,该变量空间中直接存放的是其所对应的值;
- 而引用数据类型创建的变量,一般称为对象的引用,其空间中存储的是对象所在空间的地址。如下代码:
public static void func() {
int a = 10;
int b = 20;
int[] array = new int[]{1,2,3};
}
- 在上述代码中,a、b、array,都是函数内部的变量,因此其空间都在main方法对应的栈帧中分配。
- a、b是内置类型的变量,因此其空间中保存的就是给该变量初始化的值。
- array是数组类型的引用变量,其内部保存的内容可以简单理解成是数组在堆空间中的首地址,并指向数组这个对象。如下图所示:
- 从上图可以看到,引用变量并不直接存储对象本身,可以简单理解成存储的是对象在堆中空间的起始地址。通过该地址,引用变量便可以去操作对象。有点类似C语言中的指针,但是Java中引用要比指针的操作更简单。
3.3 一个简单的例子
public static void func() {
int[] array1 = new int[3];
array1[0] = 10;
array1[1] = 20;
array1[2] = 30;
int[] array2 = new int[]{1,2,3,4,5};
array2[0] = 100;
array2[1] = 200;
array1 = array2;
array1[2] = 300;
array1[3] = 400;
array2[4] = 500;
for (int i = 0; i < array2.length; i++) {
System.out.println(array2[i]);
}
}
- 代码逻辑(配图讲解)
- 首先分别在堆区和虚拟机栈建立两个空间,array1指向地址为0x0012ff7c的对象,默认初始化为0,后将里面的元素改为10、20、30;
- array2指向地址为0x0012ffE0的对象,初始化为1、2、3、4、5,将第0和第1位元素改为100、200,现在存储的就是100、200、3、4、5;
- array1 = array2;是将array2的地址给到了array1,那么array现在指向的对象就是array2指向的对象,地址是0x0012ffE0,空间里存放的是100、200、3、4、5。
- 现在改变array1[]下标为2、3、4的元素,相当于改变原来array2中的元素,因为现在两个引用指向同一个对象。所以现在地址为0x0012ffE0的空间被改为100、200、300、400、500。
- 无论打印array1还是array2都是100、200、300、400、500。
- array1原来所指向的对象(0x0012ff7c)的空间如果没人用的话,会被Java的垃圾回收器自己回收,不用free(区别于C)。
3.4 初识 null
- null 在 Java 中表示 "空引用" , 也就是一个不指向对象的引用。
- 在Java中局部变量必须赋值,基本数据类型就赋值对应的“0”就可以了; 但是引用型要用null来赋值,否则就会报错。
- null 的作用类似于 C 语言中的 NULL (空指针), 都是表示一个无效的内存位置. 因此不能对这个内存进行任何读写操作, 一旦尝试读写, 就会抛出 NullPointerException(空指针异常)。
int[] arr = null;
System.out.println(arr[0]);
// 执行结果
Exception in thread "main" java.lang.NullPointerException
at Test.main(Test.java:6)
注意:Java 中并没有约定 null 和 0 号地址的内存有任何关联。
4、数组的应用场景
4.1 保存数据
public static void main4(String[] args) {
int[] array = {1,2,3,4};
int sum = 0;
for (int x:array) {
System.out.println(x);
}
}
4.2 作为函数的参数
public static void main(String[] args) {
int[] array = {1, 2, 3};
func(array);
System.out.println("arr[0] = " + arr[0]);
}
public static void func(int[] array) {
a[0] = 10;
System.out.println("a[0] = " + a[0]);
}
// 执行结果
a[0] = 10
arr[0] = 10
- 此时我们发现,原来array数组中的1被改为10了,但是改变的操作是在func()函数中实现的;
- 这是因为所谓的 "引用" 本质上只是存了一个地址. Java 将数组设定成引用类型,这样的话后续进行数组参数传参,其实只是将数组的地址传入到函数形参中,这样通过地址我们就可以修改数组中的内容。
4.3 作为函数的返回值
public static void main1(String[] args) {
int[] ret = test();
System.out.println(Arrays.toString(ret));
}
public static int[] test(){
int[] tmp = new int[]{1,2,3};
return tmp;
}
- 解决了C语言中返回参数只能返回1个的问题。
5、数组操作相关代码
5.1数组转字符串
- 通过Arrays.toString来实现基本类型转成字符串类型的操作,Java在这方面很便捷。
- Java 中提供了 java.util.Arrays 包, 其中包含了一些操作数组的常用方法。
import java.util.Arrays
int[] arr = {1,2,3,4,5,6};
String newArr = Arrays.toString(arr);
System.out.println(newArr);
// 执行结果
[1, 2, 3, 4, 5, 6]
5.2 数组拷贝
- Arrays包中为我们提供了相应的数组拷贝函数。
public static void main3(String[] args) {
double[] array = {1.0,56.0,66.0,89.0};
double[] copyArray = Arrays.copyOf(array,array.length+2);
double[] copyArray1 = Arrays.copyOfRange(array,0,4);
System.out.println(Arrays.toString(copyArray));
System.out.println(Arrays.toString(copyArray1));
}
- Arrays.copyOf中的参数分别为:源数组,以及要拷贝的长度。长度可以分为以下几种:
拷贝源数组的长度,相当于拷贝源数组中所有元素
double[] copyArray = Arrays.copyOf(array,array.length);
//输出结果:
{1.0,56.0,66.0,89.0};
拷贝源数组的二倍的长度,多余没有数据的部分用0补齐
double[] copyArray = Arrays.copyOf(array,array.length*2);
//输出结果:
{1.0,56.0,66.0,89.0,0.0,0.0,0.0,0.0};
拷贝源数组前两个元素
double[] copyArray = Arrays.copyOf(array,2);
//输出结果:
{1.0,56.0};
- 当然,如果你想指定拷贝的范围,可以调用 Arrays.copyOfRange
- Arrays.copyOfRange中的参数分别是:源数组,from(起始拷贝的位置,为数组下标的形式),to(终止拷贝的位置,为数组下标的形式)
- 注意:Java中遇到from和to的范围,一般都是左闭又开的形式)比如如下代码,最终拷贝的就是array[0]~array[2]的数据,因为给的范围是[0,3),不包括array[3]
public static void main3(String[] args) {
double[] array = {1.0,56.0,66.0,89.0};
double[] copyArray1 = Arrays.copyOfRange(array,0,3);
System.out.println(Arrays.toString(copyArray1));
}
//打印出来{1.0,56.0,66.0}
5.3 求数组中元素的平均值
public static void main4(String[] args) {
int[] array = {1,2,3,4};
int sum = 0;
for (int x:array) {
sum = sum+x;
}
System.out.println((sum*1.0)/array.length);
}
5.4 查找数组中指定元素(二分查找)
- 首先我们要明确,二分查找是针对有序的数组进行查找,无序的数组要先用Arrays.sort()先排序。
- 以升序数组为例, 二分查找的思路是先取中间位置的元素, 然后使用待查找元素与数组中间元素进行比较:
- 如果和待查找元素相等,即找到了返回该元素在数组中的下标;
- 如果小于待查找元素,将left = mid + 1;缩小一半范围,进行下一次查找
- 如果大于待查找元素,以类似方式到数组右半侧查找(right = mid - 1;)。
- 实现代码如下:
//模拟实现二分查找
public static void main6(String[] args) {
int[] array = {1,2,3,4};
int ret = myBinarySearch(array,3);
System.out.println(ret);
}
public static int myBinarySearch(int[] array,int key){
int left = 0;
int right = array.length - 1;
while(left <= right){
int mid = left+(right - left)/2;
if(array[mid] == key){
return mid;
}else if(array[mid] < key){
left = mid + 1;
}else{
right = mid -1;
}
}
return -1;
}
- 随着数组元素个数越多, 二分的优势就越大,查找的次数相对来说较少。
6、二维数组
- 二维数组本质上也就是一维数组, 只不过每个元素又是一个一维数组。
6.1 基本语法
- 数据类型[][] 数组名称 = new 数据类型 [行数][列数] { 初始化数据 };
6.1.1二维数组的定义
public static void main8(String[] args) {
int[][] array = {
{1,2,3},{4,5,6}};
int[][] array1 = new int[][] {
{1,2,3},{4,5,6}};
int[][] array2 = new int[2][3];
}
6.2 二维数组的遍历
//二维数组的遍历
public static void main8(String[] args) {
int[][] array = {
{1,2,3},{4,5,6}};
for (int i = 0; i < array.length; i++) {
for (int j = 0; j < array[0].length; j++) {
System.out.print(array[i][j] + " ");
}
System.out.println();
}
}
- 二维数组的用法和一维数组并没有明显差别, 因此我们不再赘述。
- 同理,还存在 "三维数组", "四维数组" 等更复杂的数组,只不过出现频率都很低,这里就不再赘述了。
7、总结
至此,Java中的数组就告一段落了,其中数组的冒泡排序算法我会单独写一篇blog来详细说明,请大家移步到Java代码实现—冒泡排序的blog即可,感谢大家的支持!!!