【Java从入门到精通】第七篇:##数组的定义与使用##6000字带你深入理解Java中的数组&&一大波细节内容@@快@你好姐妹一起来看一下把##

前言:此文章由自我学习所总结的,如果有哪些错误的地方或者还有什么不好的地方,请帮我指出,感谢万分!!!

一,数组的基本概念

1.1为什么要使用数组?

下面我给个例子:

上面的代码没什么问题,但是我们可以看到,这只是有五个,如果是上千个呢?要创建上千个变量吗?岂不是很麻烦?

所以我们在这里可以引入数组,通过观察不难发现:这里的所有类型都是相同的。

1.2,什么是数组?0

数组:可以看成是相同类型的元素得到一个集合。

在Java中包含6个整型类型的数组:(1)数组中存放的元素类型相同

(2)数组的空间是连在一起的

(3)每个空间有自己的编号,起始位置的编号为0,即是数组的下标。

1.3,数组的创建及初始化

1.3.1 数组的创建(如下)

T[]  数组名 = new T[N];

其中,T:表示数组中存放元素的类型

T[]:表示数组类型

N:表示数组的长度

例子:

1.3.2,数组的初始化

数组的初始化主要分为动态初始化以及静态初始化。

(1)动态初始化:在创建数组时,直接指定数组中元素的个数。

例子:

 此时默认数组中的值为0,我们可以给它赋值。 

(2)静态初始化:在创建数组时不直接指定数据元素个数,而直接将具体的数据内容进行指定

注意:(1)静态初始化虽然没有指定数组的长度,编译器在编译时会根据{}中元素个数来确定数组的长度。

(2)静态初始化时,{}中的数据类型必须与[]前的数据类型一致。

(3)静态初始化可以简写,省去后面的new  T[]。(如上面的array1,其实array1和array2是相同的)

(4)学过c语言程序设计的 都知道,数组都是中括号写在数组名后面的,其实在Java中也是可以这样写的,但是不建议,因为容易造成数组的类型就是int的误解[]如果在类型之后,就表示数组类型,因此int[]结合在一块写意思更清晰。

(5)如果是不确定数组中的内容时,就使用动态内存分配,否则建议使用静态初始化。

(6)静态初始化和动态初始化也可以分为两步,但是省略格式不可以。(如下例子)

上面中array3只有在定义数组的时候整体赋值,只有这一次机会。

(7)如果没有对数组初始化,数组中元素有默认值

如果数组中存储元素类型为基类类型,默认值为基类类型对应的默认值,比如:

如果数组中存储元素类型为引用类型,默认值为null。 

1.4,数组的使用

1.4.1数组中元素访问

数组在内存中是一段连续的空间,空间的编号都是从0开始的,依次递增,该编号称为数组的下标,数组可以通过下标访问其任意位置的元素,也可以通过[]对数组中的元素进行修改。

注意事项:

(1)数组是一段连续的内存空间,因此支持随机访问,即通过下标快速访问数组中任意位置的元素。

(2)下标从0开始,介于[0,N)之间不包含N,N为元素个数,不能越界,否则会报出下标越界异常。(就好比如下面的例子)

1.4.2,遍历数组

“遍历”是指将数组中的所有元素都访问一遍,访问是指对数组中的元素进行某种操作(如下例子)

 上面的代码可以起到对数组中元素遍历的目的,但是如果数组中从增加一个元素,就打印一条语句,如果有上千个元素,就需要打印上千条语句,再者如果把打印修改为给每个数组中每个元素增加1,那就特别麻烦。

所以观察上面的代码,我们可以发现对数组中的每个元素的操作都是相同的,可以用循环来打印(如下代码) 

还有一种方法,可以使用for-each(增强for循环)遍历数组

它的基本形式是:

for( 数组中每个元素的类型定义的变量:当前数组名){
   //会把当前数组里的每个元素取出来给你所定义的变量

}

例子: 

(1) for-each是for循环的另一种使用方式,能够更方便的完成对数组的遍历。可以避免循环条件和更新语句写错。

 (2)for-each循环还可以用来打印集合中的元素。

(3)我们对比一下这两个代码,可以发现普通的for循环可以拿到数组的下标,如果 for-each循环,使用的是一个定义的变量,和下标没有关系。如果要用下标做某些事情,比如用下标为2的元素*2,则使用普通的for循环,如果是打印,则这两种都可以使用。

第4打印方法:借助Java本身提供的方法来实现数组的打印。

我们先了解一个类:Arrays——》是一个工具类,主要的作用是帮你操作数组。

我们可以打开帮助手册,来了解Arrays,其中我们现在主要了解的是toString 方法,可以看到其作用是把参数的数组转换为字符串进行输出,如下:

例子 :

我们会看到打印出来的有中括号,为什么呢?

原因是,方法在进行转换的时候内部会自动帮你进行这样的组装,我们只要看里面的内容即可。

二,数组的引用类型

2.1,初始JVM的内存分布

内存是一段连续的存储空间,主要用来存储程序运行时数据的。比如:

(1)程序运行时代码需要加载到内存

(2)程序运行产生的中间数据要存放在内存中

(3)程序中的常量也要保存

(4)有些数据可能需要长时间存储,而有些数据当方法运行结束有就要被销毁。

JVM对所使用的内存按照功能的不同进行了划分:

他们之中的线程共享和线程隔离的关系: 

每个线程中的内存是一样的,但是内存中的数据各有不同。

 程序计数器:只是一个很小的空间,保存下一条执行的指令地址。

虚拟机栈:与方法调用相关的一些信息,每个方法在执行时,都会先创建一个栈帧,栈帧中包含有:局部变量表、操作数表、动态链接、返回地址以及其他的一些信息,保存的都是方法执行时相关的一些信息。比如:局部变量。当方法运行结束后,栈帧就被销毁了,即栈帧中保存的数据也被销毁了。

本地方法栈:与虚拟机栈的作用类似。只不过保存内容时Native方法的局部变量。在有些版本的JVM实现中(例如HotSpot),本地方法栈和虚拟机栈是一起的。作用时运行一些由c/c++代码编写的程序【JVM其实是一个由c/c++代码编写的一个软件】。

堆:JVM所管理的最大的内存区域。使用new创建的对象都是在堆上保存,堆是随着程序开始运行时而创建,随着程序退出而销毁,堆中的数据只要还有在使用,就不会被销毁。

方法区:用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译后的代码等数据。方法编译出的字节就是保存在这个区域的。

2.2,基本类型变量与引用类型变量的区别

2.2.1,基本数据类型创建的变量,称为基本变量,该变量空间中直接存放的是其所对应的值。而引用数据创建的变量,一般称为对象引用,其空间中存储的是对象所在的空间地址。

例子:

在上述代码中,a、b、 array都是函数内部的变量,因此其空间都在main方法对应的栈帧中分配。

(1)a、b是内置类型的变量,因此其空间中保存的就是给变量初始化的值。

(2)array是数组类型的引用变量,其内部保存的内容可以理解为是数组在堆空间中的首地址。(如下图)

从上图可以看到,引用变量并不直接存储对象本身,可以简单理解成存储的是对象在堆中空间的起始地址,引用变量便可以去操作对象。有点类似c语言中的指针,但Java中引用要比指针的操作更简单。 

我们也可以从下面的代码中看出:

 打印array的是它的地址。这个地址中@后面的值是真实地址的哈希值,其与真实地址一样也是独一无二的,也可以当成数组;@前面的字母表示当前数组的类型,此时I表示当前数组的类型为整型;最前面的[表示数组。

2.2.2,如果是一个引用引用了另一个引用里的对象,又是怎么实现的呢?(我们看下面的例子)

此代码中的array2引用了array1中的对象,具体实现过程如下图: 

 

 此时说明,这两个引用都指向了同一个对象。

所以当我们array2中的元素改变,array1中的元素也跟着改变。(如下图)

2.3,认识null

null在Java中表示“空引用”,也就是一个不指向对象的引用。

如下例子:

当我们给一个数组初始化为0的时候,编译器会报错,那正确的赋值方法是什么呢?看下面代码:

赋值为null ,此时代表array这个引用不指向任何对象。

null的作用类似于c语言中的NULL(空指针),都是表示一个无效的内存位置。因此不能对这个内存进行任何读写操作,一旦尝试读写,就会抛出NllPointerException。

注意:Java中并没有约定null和0号地址的内存有任何关联。

三,数组的应用场景

3.1,保存数据

例子:

3.2,作为函数的参数

3.2.1,参数传基本数据类型 

例子:

我们可以看到上面a传过去给b之后并没有改变它的值,所以 在传基本数据类型的参数时。func方法中的值改变不会影响实参的值。

3.2.2,参数传数组类型(引用数据类型)

例子:

 从上面代码可以知道:在func2中对array1的改变,并没有影响实参中array1的改变;但是在func3中对array2中下标为0的修改却改变了实参中array2的结果。为什么呢?因为数组是引用类型,按照引用类型来进行传递,是可以修改其中存放的内容的。

图形解析(不画出开辟的main和func2或func3的栈帧):

第一种:

第二种:

总结:所谓的“引用”,本质上只是存放了一个地址,Java将数组设定成引用类型,这样的话后续进行数组参数传参,其实只是将数组的地址传入到函数的形参中。这样可以避免对整个数组的拷贝(数组可能比较长,那么拷贝开销就会很大)。

3.3,作为函数的返回值

例子(这里以获取斐波那契数列的前n项为例子):

 通过这个代码,可以观察到,fib的返回值可以是一个数组。

四,数组拷贝

4.1,第一种拷贝方式:

例子:

 新new的copy通过for循环将array数组中的内容拷贝到copy中,因此copy打印出的数组元素和array中的一样。

4.2,第二种拷贝方式

使用Arrays.copyOf方法完成数组的拷贝。(常用)

我们先来了解一下copyOf,点进去即可看到如下图

可以看到,这个方法有两个 参数,一个是数组类型的,一个是int类型的;第一个参数写你所需要拷贝的数组,第二个参数写新的长度。

例子:

 copyOf方法在进行数组拷贝时,创建了一个新的数组。当在array.length前面加上数字的时候就相当于扩容。

4.3,第三种拷贝方式

我们要了解System.arraycopy()方法,点过去可以看到其组成,如下:

我们可以看到其中有5个参数,第一个参数写你所需要拷贝的数组,第二个 参数写开始拷贝的下标,第三个参数写目的地数组,第四个参数写目的地数组的位置,第五个参数写你要拷贝的数组长度。

 例子:

这种拷贝方式更快一点,因为从上一个图中我们可以看到“native”,这个单词代表“本地的”,所有本地的方法都是用c/c++代码实现的,我们是看不到的。但性能更高一点,所以更快一点。

4.4,第四种拷贝方式

使用“数组名.clone”方法,它是通过对array数组产生一个副本,两者的地址不同,在进行拷贝。

例子:

4.5,第五种拷贝方式

使用Array.copyOfRange()的拷贝方法,其可以拷贝一部分的数组元素,其组成如下:

 

原码:  

我们可以看到,第一个参数写的是你要拷贝的数组,第二第三个参数是写从你需要开始拷贝的元素的下标到结束的下标。 

例子:

 

 提醒:from,to的都是左边是闭区间,右端是开区间。就好比如上面的[1,3),打印不到下标为3的元素,只能打印到下标为2的元素。

 注意:数组当中存储的是基本类型数据时,不论怎么拷贝基本都不会出现什么问题,但如果存储的是引用数据类型,拷贝时需要考虑深浅拷贝的问题。

五,查找数组中的元素

5.1,顺序查找法

给定一个数组,再给定一个元素,找出该元素在数组中的位置。

例子:

上面代码中返回-1(随便写一个负数),原因是数组没有负数下标。

5.2,二分查找

使用二分查找的前提条件是:数组元素必须是有序的,也就是针对的是有序数组(包括升序和降序)。

基本思路:

以升序数组为例,先取中间位置的元素,然后使用待查找元素与数组中间元素进行比较:

(1)如果相等,即找到了返回该元素在数组中的下标。

(2)如果小于,以类似方式到数组左半侧查找。

(3)如果大于,以类似的方式到数组右半侧查找。

例子:

可以看到,针对一个长度为10000个元素的数组查找,二分查找只需要循环14次就可以完成查找。随着数组元素的个数越多,二分查找的优势就越大。 

注意:(1)sort方法默认是升序排序;

(2)Arrays.sort()底层什么排序都有,最主要的是:插入、归并和快排。

其实在Java中,我们不需要自己写二分查找,直接引用Array.binarySearch(数组名,所需要找的元素)方法即可,如下代码:

 其实这个方法的实现和上面自己所写的二分查找法的代码是差不多的。

六,冒泡排序

给定一个数组,让数组升序(降序)排序。

算法思路

假设排升序:

 (1),将数组中相邻元素从前往后依次进行比较,如果前一个元素比后一个元素大,则交换,一趟下来后最大元素就在数组的末尾。

(2)依次从上上述过程,直到数组中所有的元素都排序好。

例子: 

冒泡排序性能较低,Java中内置了更高效的排序算法,就是上面讲到的Arrays.sort方法。

七,数组逆序

给定一个数组,将里面的元素逆序排序。

思路:

设定两个下标,分别指向第一个元素和最后一个元素。交换两个位置的元素。然后让前一个下标自增,后一个下标自减。循环继续即可。

例子:

八,二维数组

二维数组本质上也就是一维数组,只不过每个元素又是一个一维数组。

基本语法:

数据类型[][]  数组名称 = new 数据类型 [行数][列数]  {初始化数据};

二维数组的几种定义方式:

(1)以直接赋值的方式定义:

 

注意:中括号里面不能有数字。

(2)重新new一个数组来定义:

 

中括号里面依旧不可以有任何数字。 

(3)当不知道初始值的时候:

 

例子1:打印这个二维数组

 这里的“3”是这个二位数组的行数,“4”是这个二维数组的列数。

那问题来了,如果我不想要数这个行数和列数,我该怎么办呢?(实话告诉你,凉拌。。哈哈哈哈额,假的)。

看下面代码:

为什么这样子改呢?其实二维数组就是特殊的一维数组(如图)(有点丑,哈哈哈哈额)

 那我们使用foreach来进行打印:

注意这里第一个foreach,里面写的是int[ ]类型,因为由上图可以看出,“行”是一个数组,所以用int[ ]类型。 

使用toString(打印地址)、deepToString(以字符串的形式进行打印)进行打印:

还有一些细节我们需要注意的(看下面代码):

当我们省略列数的时候,默认值为null,所以运行的时候会抛出一个空指针异常。

当给这个二维数组加上列数的时候(如下代码):

 可以发现上面的代码中二维数组的列数不相同,这就叫做不规则二维数组,我们可以根据需要来确定列数。

二位数组的用法和一维数组的用法是一样的,同样也存在三维数组、四维数组.....复杂程度更高,有兴趣的可以自己研究下。

后记:如果你觉得我写的对你有用,可以点个赞+关注+收藏,错过了就再也没有了哦!!我会持续更新,做好Java笔记。如果哪些有问题的、错误的,可以评论区或者私信告诉我,我会进行修改,谢谢大佬!!!

 

 

 

 

 

 

 

 

 

 

 

         

评论 38
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

爱撸猫的程序媛

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值