【Java基础】数组基础


1.数组基础概念

1.1为什么要引入数组

首先我们引入一个例子:当我们需要输出5个学生的成绩Score(假设为10,20,30,40,50)时,我们首先想到的是使用sout的方式直接输出对应的五个数据。即

System.out.println(10);
System.out.println(20);
System.out.println(30);
System.out.println(40);
System.out.println(50);

那么首先我们可以发现,Score中的数据类型都是相同的。在只有五个数据的时候我们使用sout的方式可以很快的输出,那么当数据量为成百上千的时候呢?
因此,我们引入了数组。我们可以在数组中存储相同类型的多个数据

1.2数组的概念

数组:可以看成是相同类型元素的一个集合。在内存中是一段连续的空间
数组示意图
在内存中,由于数组的排列是连续的。因此我将他转化为上图的排列方式。而数组的每个数据都有自己的一个存储空间。为了获取对应空间中的数据,我们就需要进入到对应内存地址,最终获取到这段内存中的数据。
在Java中,底层代码已经为我们解决了这件事情,同时Java为数组中的每个数据进行了以0开始计数的编号,这个编号也是数组的下标

1.3 数组的创建及初始化

1.3.1数组的创建

数组的基础创建公式为

T[] array = new T[N]; // T:数组中存放的数据类型,array为数组名,N为数据个数(数组长度)。
//举例
int[] array2 = new int[10];//在数组array中存放数据类型为int,数组长度为10

1.3.2 数组的初始化

数组的初始化主要分为静态初始化和动态初始化
动态初始化:在创建数组的时候直接指定数组中元素个数。

int[] array = new int[10];

静态初始化:在创建数组的时候不直接指定数组数据个数,而直接将数据内容固定。

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

【注意事项】
1.在静态初始化过程中,{1,2,3,4}表示为数组中的内容,我们在定义的过程中并没有明确表示数组的长度为4。但是不用担心,Java在编译过程中会为我们自己计算数组的元素个数。
2.在静态初始化中,因为直接存入数据的原因,需要注意的是存入的数据类型必须与[]前的数据类型一致。
3.静态初始化可以进行简写:

int[] array1 = {0,1,2,3,4,5,6,7,8,9};
double[] array2 = {1.0, 2.0, 3.0, 4.0, 5.0};
String[] array3 = {"hell", "Java", "!!!"};

虽然在简写过程中省略了new T[],但是在编译器编译代码时还是会还原的。
4.静态和动态初始化可以分为两步,但是不可以省略格式。

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

5.如果创建数组的过程中没有对数组进行初始化,数组中的元素存在默认值

  • 如果数组中存在的元素类型为基本元素类型,默认值为基类类型对应的默认值。
类型默认值
byte0
int0
short0
long0
float0.0f
double0.0
booleanfalse
char/u0000

在基本类型默认值中,char类型是比较特殊的“0”值,在编译运行后可能打印不出来,为空。

  • 当数组类型为引用类型时,默认值为null
	String[] str = new String[5];
    System.out.println(str[1]);//null

在这里插入图片描述

1.4数组的使用

1.4.1 数组的访问

在上文中我们知道,数组在内存中是一段连续的空间,Java通过数字对数组进行编号。空间的编号从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[3] = 99;

数组下标从0开始,介于[0,N)之间的而不包含N。//N为元素个数,不能越界,否则会报出数组越界异常。

int[] arr = new int[3];
System.out.println(array[4]);//error

在这里插入图片描述
在报错信息中,我们大致可以发现错误原因为ArrayIndexOutOfBoundException,即数组越界,因此在使用数组时一定要注意数组的范围。

1.4.2 数组长度的获取

在Java中,由于数组是**引用类型**,底层代码中已经为我们实现了获取数组长度的方法:array.length 。因此我们可以通过Array.length直接获取到数组的长度。
int[] array1 = new int[]{1,2,3,4};
System.out.println(array1.length);//4

1.4.3 数组数据的存放

在前面我们已经学会了访问数组中的元素,那么我们直接查看数组会出现什么情况呢?

	int[] arr = new int[3];
    System.out.println(arr);

在这里插入图片描述
如上图所示,直接输出数组的值是一段不知道什么东西的字符串。
我们姑且可以这么认为:"["代表着这是一个数组;"I"代表着这是一个int类型;而@后面的字符串我们可以认为是数组的地址(以哈希值的形式表现)。
数组arr中存放的就是对象的地址。
在这里插入图片描述

1.4.4数组数据的获取(遍历)

1.for循环
通过array.length这个方法我们可以获得数组的长度,也因此我们可以通过循环获取数组中的数据。(直接看下面的代码)

	int[] array = {1,2,3,4};
        //遍历数组的方式
        for (int i = 0; i < array.length; i++) {
            System.out.print(array[i] +" ");
        }

2.for-each循环
在for循环的基础上Java还创建了一个for-each循环(增强for循环).
这种增强for循环的基本语法格式为for(数据类型 变量名 : 数组)
应用如下列代码所示:

 		for (int x:array) {
            System.out.print(x + " ");
        }

for循环和for-each循环都是很好的遍历方式,而在上面的应用中我们可以知道,for循环便于我们对各个下标进行精确获取,而for-each只能适用于全局遍历的方式,因此在遍历过程中我们要注意二者的区别。
3.Java自带的方式Arrays.toString()
我们直接看下列代码:

		//1.第一种写法
        String ret = Arrays.toString(array);//将数组转换成字符串
        System.out.println(ret);
        //2.第二种写法
        System.out.println(Arrays.toString(array));

结果:
在这里插入图片描述
在上面的代码演示中,我们可以发现toString是一个十分好用的方法,通过联想(见下图)我们可以看到toString方法已经完成重载,适配了各种类型的数组,使用起来简直无脑,而且运行的结果我们可以发现和数组的构造是一样的,这真是太好用辣!!!
在这里插入图片描述

1.4.5 自制一个toString()

我们在前文中已经知道了Arrays.toString()方法的便利性,身为智慧的程序猿,我们怎么不能写出一个和他一样的toString()方法呢?那么接下来我们自制一个toString().
首先我们看看Arrays.toString打印出来的是什么样的。

public static void main(String[] args) {
        int[] array = {1,2,3,4};
        
        System.out.println("Java自带toString");
        System.out.println(Arrays.toString(array));
    }

结果是这样的在这里插入图片描述
接下来我们依葫芦画瓢,首先toString方法返回的是字符串,本着字符串“万物皆可拼”的想法,我们设置一个MytoString方法,形参为数组,返回值为String.即

public static String MytoString(int[] array){}

思路如下:
1.在官方的输出中是以[数据1,数据2……]的形式打印的,因此首先设定字符串ret=“[”;
2.为了获取每个数据,我们可以使用for循环遍历数组并打印;
3.在每个数据后面都加上了", “直到最后一个元素,因此我们可以得出:当 i != array.length - 1时,获取数组元素后就可以加上”,“;
4.在遍历完数组后,我们用ret拼接上”]"即可
那么说做就做,我们看看下列代码:


	public static String myToString(int[] array){
        String ret = "[";
        for (int i = 0; i < array.length; i++) {
            ret += array[i];
            if (i != array.length -1){
                ret += ", ";
            }
        }
        ret += "]";
        return ret;
    }
    public static void main(String[] args) {
        int[] array = {1,2,3,4};
        System.out.println("Java自带toString");
        System.out.println(Arrays.toString(array));
        System.out.println("MytoString");
        System.out.println(MytoString(array));
    }

结果如下图所示:
在这里插入图片描述

在经历过上面的代码以及运行结果我们可以看出来,似乎没有问题了
我们再验证其他情况下的数据,请看:

public static void main(String[] args) {
        // int[] array = {1,2,3,4};
       int[] array = null;
        
        System.out.println("Java自带toString");
        System.out.println(Arrays.toString(array));

        System.out.println("MytoString");
        System.out.println(MytoString(array));
    }

在这里插入图片描述
请看好了,当array = null (即没有引用时),Java自带的toString方法返回值为null,而MytoString方法则报错为空指针异常,原因是在MytoString方法中默认是数组不为空的情况,当数组为空时,MytoString方法找不到堆中的数组空间,因此无法获取数据,最终导致空指针异常报错。
接下来我们讨论当array = null并进行代码的改进。

public static String MytoString(int[] array){
        if (array == null){
            return null;
        }
        String ret = "[";
        for (int i = 0; i < array.length; i++) {
            ret += array[i];
            if (i != array.length -1){
                ret += ", ";
            }
        }
        ret += "]";
        return ret;
    }

重新运行后,代码正常运行,我们算是做出来了一个属于自己的MytoString()方法。大成功大成功!!!
在这里插入图片描述

1.4.6 什么是引用类型?

Java当中的数组在栈中被使用,而数组指向堆中的数据才是真实的数据。数组只是引用了这些数据,因此,我们称数组为引用类型。当数组指向对应的数据时,我们称为”引用指向对象“。如下图所示,我们假设0x99是堆中的数据,数组array在栈中存放着这些数据的地址,通过调用指向堆中各自对应的数据,这就是”引用“。
在这里插入图片描述

那么有没有可能存在数组不指向对象呢?

int[] arr2 = null;
System.out.println(array[0]);

在这里插入图片描述

在上面的代码中,arr2并不存在引用的情况,arr2这个引用不指向任何一个对象,只对数组进行了初始化。在运行了array[0]之后,则数组出现空指针异常报错,这也从侧面证明了数组是引用类型。

1.4.7 数组的初始化

int[] arr2 = new int[10];
int[] arr3; 
System.out.println(arr3);

上面的代码中,arr2是我们已经熟练使用的定义方法,在new int[10] 之后,在堆中将会开辟一个长度为10,元素值为0的数组空间。
如果像arr3这样的写法定义数组,既没有设置null表示数组指向为空,又没有存放数组地址,这样的写法是否能过关呢?
在这里插入图片描述
我们知道,如果直接打印arr3的话,理论上会出现一串我们难以理解的数值(类似哈希值,见1.4.2图示),这串数值里面存放着数组引用的数值。而在我们运行arr3的时候,编译器提示我们未初始化变量arr3.因此我们结合之前所学过的知识点可以得出原因:在main方法中,变量arr3属于局部变量,一定是需要初始化的。

2.Java虚拟机运行时的数据区

我们在使用Java语言的时候,都会配置jdk和jre,而当我们编译运行的时候需要将程序数据进行存储。如果内存中的数据随意划分,不加以管理,将会对我们日后的程序引起很大的麻烦。
因此,Java设置了jvm虚拟机用以存储数据,同时jvm也对所使用的内存按照功能进行了不同区域的划分。(如图)
在这里插入图片描述

  1. 程序计数器 (PC Register): 只是一个很小的空间, 保存下一条执行的指令的地址
  2. 虚拟机栈(JVM Stack): 与方法调用相关的一些信息,每个方法在执行时,都会先创建一个栈帧,栈帧中包含有:局部变量表、操作数栈、动态链接、返回地址以及其他的一些信息,保存的都是与方法执行时相关的一些信息。比如:局部变量。当方法运行结束后,栈帧就被销毁了,即栈帧中保存的数据也被销毁了
  3. 本地方法栈(Native Method Stack): 本地方法栈与虚拟机栈的作用类似. 只不过保存的内容是Native方法的局部变量. 在有些版本的 JVM 实现中(例如HotSpot), 本地方法栈和虚拟机栈是一起的。
  4. (Heap): JVM所管理的最大内存区域. 使用 new 创建的对象都是在堆上保存 (例如前面的 new int[]{1, 2,3} ),堆是随着程序开始运行时而创建,随着程序的退出而销毁,堆中的数据只要还有在使用,就不会被销毁
  5. 方法区(Method Area): 用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据. 方法编译出的的字节码就是保存在这个区域。

2.1 数组在jvm的形态

老规矩,直接上代码

public static void main(String[] args) {
        //堆栈概念
        int[] arr1 = new int[]{1,2,3};
        int[] arr2 = new int[5];
        arr2[0] = 100;
        arr2[1] = 200;
        arr2[2] = 300;
        arr1 = arr2;
        arr1[3] = 40;
        arr1[4] = 500;
        System.out.println(Arrays.toString(arr1));
    }

为了方便大家理解,我画了一个好图
在这里插入图片描述
在这个图中,一开始对arr1和arr2进行初始化后,在栈中存放了各自对应数据的地址。堆中两个数组都有被引用到,因此jvm无法回收
在之后,出现了一段代码:arr1 = arr2,很明显,这是将arr2的地址赋值给了arr1,用人话解释就是arr1指向堆中的数据已经不再是[1,2,3]了,而是和arr2一样的元素集合。
在这里插入图片描述
那么这时候,有意思的就来了,后面的arr1[3]和arr1[4]的值是到哪个地方去了?显而易见是arr2的数据空间了,否则arr1的数组长度为3,超过长度则编译器报错。
最后我们润一下代码。
在这里插入图片描述
在编译完成后,由于堆中的arr1没有人引用,arr1这个引用指向arr2这个引用所指向的对象,因此堆中的arr1将会被jvm回收。因此,最终无论是arr1打印数组还是arr2打印数组,最终值都是相同的。

3.数组的应用场景

3.1 数组的保存(见1.4.3)

3.2 数组作为参数传递

数组作为一个引用类型的数据类型,也是可以作为形参实参进行传递的。那么在这种地方,数组的行为会有什么变化?

3.2.1 数组作为形参传递

public static void print(int[] arr){
        for (int x :
                arr) {
            System.out.print(x + " ");
        }
        System.out.println();
    }
    public static void main(String[] args) {
        int[] array = {1,2,3,4};
        print(array);
    }

在上面的代码中,arr是print方法中的形参,在main方法中,向print方法传递数组array。编译过程中,jvm为print方法开辟一个栈帧,在其中存入arr内存地址。既然将array作为参数传递过去,也意味着array的“地址”赋值给了arr(arr = array)(如图示)
在这里插入图片描述
因此在代码块中我们通过for-each循环获取的数值就是数组array中的元素啦。
虽然但是,不是传了引用就一定能修改实参的值,

public static void func1(int[] array){
         array = new int[]{11,22,33,44};
    }
    public static void func2(int[] array){
        array[0] = 11;

    }
    public static void main6(String[] args) {
        int[] array = {1,2,3,4};
        func1(array);
        //func2(array)
        System.out.println(Arrays.toString(array));
    }

在运行了上面的代码后,我先后运行func1()和func2(),让我们看看二者有何区别:
在这里插入图片描述
在这里插入图片描述
这就奇怪了 从上面我们知道数组是引用类型,并且通过画图我们也能够大致明白数组在栈和堆中的运行过程。为什么func1()运行后array值不变呢?func2()又为什么可以成功修改array的值。让我再画图试试:
在func1()中,我们传入main方法中的数组array,按道理来说这时候的形参array应该是相等于实参array 的。
我们发现func1的代码中多了一个 “new int[]”,我们也可以从中看出端倪,在func1()方法中,原本传入的是实参array的地址,但是在这时候new了一个新的数组,于是func1在堆中又开辟了一个新的空间,用来存放这组数据,同时引用了这组数据。
在这里插入图片描述
在func2中,并没有设置new int[] 在堆中重新开辟一个新的空间,因此func2能够成功的修改实参中array的值。
在这里插入图片描述

3.3基本类型和引用类型在形参中的区别

通过以上的例子,我们已经理解了引用类型在堆栈中的情况,那么举一反三,基本数据类型能不能通过形参在方法中修改呢?

public static void func9(int n ){
        n = 100;
    }
    public static void main(String[] args) {
        int n = 11;
        func9(n);
        System.out.println(n);
    }

在运行这段代码之后,我们发现输出值n=11,值不变。
因此,和引用数据类型不同,基本数据类型如int,double……是不能通过形参改变数值的!!!
基本数据类型在定义后已经在栈中开辟栈帧,但是并没有在堆中重新开辟一段新的空间,所以不能像数组那样通过形参修改数值。

结尾

数组是在学习Java中最先学到的引用数据类型之一,是十分常见的一种数据结构。
源码需求可以到——>数组基础获取源码
感谢观看!!!

  • 28
    点赞
  • 34
    收藏
    觉得还不错? 一键收藏
  • 5
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 5
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值