前言:
本篇博客将为大家介绍Java中的数组的相关知识。
目录
基本介绍
概念相关
数组可以看成是相同类型元素的集合。在内存中是一段连续的空间,每个空间都有自己的编号,编号从0开始。
数组的使用
- 创建数组:T[] 数组名 = new T[N];
- T表示数组中存放的元素的数据类型
- T[]表示数组的类型
- N表示数组的长度
- 数组的初始化(一共有三种形式):
- 动态初始化:在创建数组时,直接指定数组元素的个数
- int[] arr = new int[10];
- 静态初始化:在创建数组的时候,不直接指定数组元素个数,而是对元素的具体内容进行指定
- int[] arr = new int[]{12,23,43,21,43};
- int[] arr = {12,23,43,21,43};(对上面的写法再次进行简化)
- 注意:
- 静态初始化虽然没有指定数组的长度,但是编译器会按照其存放的元素的个数来确定其长度
- 静态初始化中的后面的new T[]虽然可以省略,但在编译的时候还是会还原的
- 数组也可以按照C语言中数组的创建方式来创建,但是不推荐。T[]写在一起更加清晰明了,让人立刻就明白T[]是数组的类型
- 静态初始化和动态初始化都可以写成两步,但是省略后面new T[]的静态静态初始化方法不可以
- 如果没有对数组初始话,里面存放的是默认值。基本数据类型存基本数据类型的默认值;引用类型存引用类型的默认值。
- 基本数据类型的默认值:
- 动态初始化:在创建数组时,直接指定数组元素的个数
- 数组的使用:
- 数组中元素的访问:数组在内存中是一段连续空间,空间的编号都是从0开始,依次递增,该编号称为数组的下标,数组可以通过其下标访问数组中的任意元素(即,支持随机访问)。其编号是从)开始的,假设数组中共有N个元素,那么编号,也就是下标的大小只能在[0,N)这个范围,大于等于N都会导致数组越界(java.lang.ArrayIndexOutOfBoundsException)。
- 遍历数组:所谓遍历,就是把数组中的元素全部访问一遍;访问是指对数组中的元素进行某种操作,这里以打印为例。像数组中的元素的访问的例子那样,arr6中的所有元素都可以通过一个一个地打印出来,从而达到遍历的效果。但是我们不禁思考:如果一个数组有很多元素呢(比如100个),我们也要这样一个一个地去敲每一步的打印吗?这么做的工作量显然十分巨大。仔细思考,我们会发现我们可以通过循环很容易实现遍历,从而减少代码量:
- 我们可以通过for-each循环来遍历:
- 就通过for循环来实现:(当然,其他循环也是可以的,这里也可以只是举例)
- 这时,另外一个疑问又来了:我们要如何去获取数组的长度呢?此处我们自己举例写的arr6数组,且可以看到它的长度(起码我们可以自己数一下),但是总是有一些场景我们是不能确定其长度或者长度在改变的,那么我们要如何获取数组的长度呢?我们可以通过”数组对象.length“来获取其长度
- 这时,另外一个疑问又来了:我们要如何去获取数组的长度呢?此处我们自己举例写的arr6数组,且可以看到它的长度(起码我们可以自己数一下),但是总是有一些场景我们是不能确定其长度或者长度在改变的,那么我们要如何获取数组的长度呢?我们可以通过”数组对象.length“来获取其长度
- 我们可以通过for-each循环来遍历:
- 数组中元素的访问:数组在内存中是一段连续空间,空间的编号都是从0开始,依次递增,该编号称为数组的下标,数组可以通过其下标访问数组中的任意元素(即,支持随机访问)。其编号是从)开始的,假设数组中共有N个元素,那么编号,也就是下标的大小只能在[0,N)这个范围,大于等于N都会导致数组越界(java.lang.ArrayIndexOutOfBoundsException)。
数组是引用类型
首先,我们先基本了解一下JVM:
程序计数器(PC Register):一块很小的空间,保存下一条执行指令的地址。
虚拟机栈(JVM Stack):与方法调用的一些相关信息,每个方法在执行的时候,都会先创建一个栈帧,栈帧中包含有:局部变量表、操作数栈,动态链接,返回地址以及一些其他的信息,保存的都是一些和方法执行时的相关信息。如:局部变量。方法在运行完以后,栈帧就会被销毁,即保存在栈帧中的数据也会被销毁。
本地方法栈(Native Method Stack):本地方法栈和虚拟机栈相似,只不过保存的是Native方法的局部变量。有些版本的JVM(如HotSpot)实现中本地方法栈和虚拟机栈是一起的。
堆(Head):JVM所管理的最大内存区域,使用new创建的对象都是在堆上保存的。堆是随着程序开始运行时而创建的,随着程序的退出而销毁,堆中的数据只要还有在使用的,就不会被销毁。
方法区(Method Area):用于存储已经被虚拟机加载的类信息,常量、静态变量、即时编译器编译后的代码等数据。方法编译出的字节码就是保存在这个区域的。
接下来,我们将主要涉及两个部分:堆和虚拟机。
基本数据类型创建的变量,称为基本变量,该变量空间存放的就是其保存的值;
而引用数据类型创建的变量,一般称为对象的引用,其空间存放的对象的地址。
举例:
注意:
- 对于这种写法(如下图),我们不能说一个引用指向了一个引用(这句话是错的,引用不能指向引用),我们应该说,一个引用指向了另一个引用所指向的对象
图解:
- 在Java中,null表示空引用,也就是不指向任何对象的引用
- Java中并没有约定null和0有任何关系。
应用场景
保存数据
作为方法的参数
图解:
作为方法的返回值
把func3方法中返回的arr的地址给了ret,使得ret指向了ret这个引用指向的地址(func3中的代码也可以直接简化成return new int[]{1,2,3};)
注意:对象一定是在堆上的,引用变量不一定是在栈上的。
练习
数组转字符串
记得要导包:import java.util.Arrays;(java.util.Arrays包里面包含了一些操作数组的常用方法)
我们也可以自己尝试实现一下这个方法:
public class Demo3 {
public static String myToString(int[] arr){
String ret ="[";
for (int i = 0; i < arr.length; i++) {
ret += arr[i];
if(i != arr.length -1){
ret += ",";
}
}
ret += "]";
return ret;
}
public static void main(String[] args) {
int[] arr = {1,2,3};
System.out.println(myToString(arr));
}
}
数组拷贝
单纯的拷贝:
这里要注意,不能直接int[] copy = arr; 这样不是拷贝,是让copy拿到了arr里面放的地址,从而指向了它指向的引用,并没有创建新的数组,不是拷贝。
法二:
直接利用Arrays自带的拷贝方法(copyOf(原数组,新数组长度)):
值得注意的是:copyOf方法有很多方法和它构成重载,我们只需要在选择满足自己需求的那个即可。
copyOf方法还可以改变新的数组的长度:
拷贝一定范围的数据:
copyOfRang(原数组,初始拷贝下标,结束拷贝下标):
如果拷贝的范围超出了原来数组的长度,就只能能拷贝多少拷贝多少,那些没拷贝到的就还是初始值0。
System.arraycopy(原数组,原数组开始被拷贝的下标,新数组,新数组开始拷贝的下标,拷贝的长度);
注意:方法中的最后一个数是从原数组中拷贝过来的数据的长度要格外小心是否越界,要注意在起始拷贝下标的基础上新数组是否能接收这么长的数据,原数组是否能给出这么多的数据供拷贝。
求数组中元素的平均值
import java.util.Arrays;
public class Demo3 {
public static double avg(int[] arr){
int sum = 0;
for (int i = 0; i < arr.length; i++) {
sum += arr[i];
}
return sum/(double)arr.length;//注意类型转换
}
public static void main(String[] args) {
int[] arr = {1,2,3,4,5,6,7,8,21};
System.out.println(avg(arr));
}
}
查找数组中的指定元素(二分查找+顺序查找)
顺序查找:
import java.util.Arrays;
public class Demo3 {
//顺序查找:
public static int findVal(int[] arr, int i){
for (int j = 0; j < arr.length; j++) {
if(arr[j] == i){
return j;
}
}
return -1;//数组下标不可能是负数,返回负数说明没有找到
}
public static void main(String[] args) {
int[] arr = {1,2,3,4};
int ret = findVal(arr,2);
System.out.println(ret);
}
}
二分查找:针对有序数组,我们还可以通过二分查找来找出要查找的数据的下标:
public class Demo3 {
// 二分查找:
public static int binarySearch(int[] arr, int i){
int left = 0;
int right = arr.length - 1;
while(left <= right){
int mid = (right + left)/2;
if(arr[mid] < i){
left = mid + 1;
mid = (right + left)/2;
}else if(arr[mid] > i){
right = mid - 1;
mid = (right + left)/2;
}else if(arr[mid] == i){
return mid;
}
}
return -1;
}
public static void main(String[] args) {
int[] arr = {12,34,45,51,54,65,76,87,97,99};
int ret = binarySearch(arr,51);
System.out.println(ret);
}
}
图解:
当然,我们也可以通过二分查找查找无序的数组的特定元素:先通过Arrays里面的sort方法对数组进行排序(升序),然后再通过二分查找查出排序好以后的特定数组的下标,不过这样做的意义其实并不大,因为经过排序,数组的下标早已不是原来的样子了。
数组排序(冒泡排序)
import java.util.Arrays;
public class Demo3 {
//冒泡排序:
public static void bubbleSort(int[] arr){
for (int i = 0; i < arr.length-1; i++) {
//一共要排arr.length-1次序,每排一次都会让数组中的一个数有序(此处以最大值为例)
for (int j = 0; j < arr.length-i-1; j++) {
//每一次排序内部要走arr.length-1-i次排序,才能实现让数组中为排好序的数里面的最大值排到最后
if(arr[j]>arr[j+1]){
int tmp = arr[j];
arr[j] = arr[j+1];
arr[j+1] = tmp;
}
}
}
}
public static void main(String[] args) {
int[] arr = {12,45,97,76,51,34,87,54,65,99};
bubbleSort(arr);
System.out.println(Arrays.toString(arr));
}
}
由图可知,在i=4开始,数组就已经是有序的了,后面的比较就是冗余的了,所以我们可以再对这个代码进行优化:在方法中加一个变量用来判断是否发生了交换,且每有一趟循环走完开始下一趟循环的时候就要让这个变量变回到它原来的值。一旦发现不再发生交换的时候,就直接跳出循环。
import java.util.Arrays;
public class Demo3 {
//冒泡排序:
public static void bubbleSort(int[] arr){
for (int i = 0; i < arr.length-1; i++) {
//一共要排arr.length-1次序,每排一次都会让数组中的一个数有序(此处以最大值为例)
boolean flg = false;//每一趟开始的时候都要让flg为false
for (int j = 0; j < arr.length-i-1; j++) {
//每一次排序内部要走arr.length-1-i次排序,才能实现让数组中为排好序的数里面的最大值排到最后
if(arr[j]>arr[j+1]){
int tmp = arr[j];
arr[j] = arr[j+1];
arr[j+1] = tmp;
flg = true;//若发生交换,则改变flg的值,通过flg的值来确定是否发生交换,若未发生交换,则直接跳出循环
}
}
if(flg == false){
break;
}
}
}
public static void main(String[] args) {
int[] arr = {12,45,97,76,51,34,87,54,65,99};
bubbleSort(arr);
System.out.println(Arrays.toString(arr));
}
}
数组逆序
此处介绍的是直接通过交换对应下标的元素来实现数组逆序的效果。
import java.util.Arrays;
public class Demo3 {
//数组逆序:
public static void reserve(int[] arr){
int left = 0;
int right = arr.length-1;
while(left < right){
//不用管相等的情况,因为当right和left相等时就表明相等的那个数就是中间的那个,
// 既然在最中间,也没有交换的必要了
int tmp = arr[left];
arr[left] = arr[right];
arr[right] = tmp;
left++;
right--;
}
}
public static void main(String[] args) {
int[] arr = {12,45,97,76,51,34,87,54,65,99};
reserve(arr);
System.out.println(Arrays.toString(arr));
}
}
二维数组
- 语法:数据类型[][] 数组名称 = new 数据类型[行数][列数]{初始化数据};
- int[][] arr = new int[][]{{1,2,3},{4,5,6}};
- int[][] arr = {{1,2,3},{4,5,6}};
- int[][] arr = new int[2][3];(默认这两行三列里面存的全是0)
- 二维数组本质上也是一个数组,只不过每个数组里面又是一个数组。行里面存放的是各个列组成的的数组的地址。
- 遍历二维数组:
- 我们很容易想到一个最简单粗暴的方法:通过两层循环来遍历整个数组:
- 但是很明显,对于这种方法,我们是自己手动置入其行和列的长度的。有没有办法可以获取到其行和列的长度吗?当然是有的。结合前面介绍的二维数组的元素其实就是一维数组,我们可以很好地理解下面这种写法:
- 我们很容易想到一个最简单粗暴的方法:通过两层循环来遍历整个数组:
- 顺便一提,要想直接打印出二维数组,我们使用的方法不再是toString,而是deepToString
- 不规则的二维数组:
- 注意,未对不规则二维数组初始化的时候不能打印它。因为二位数组里面存的是一维数组,而数组是引用类型,引用类型未初始化默认存储的是null,也就是不指向任何地址,所以会报空指针异常的错误。
Arrays中的其他方法的介绍:
- equals:判断两个数组是否一样:
- fill:帮助数组填充数据:
同时,fill方法有很多重载方法,可以根据需求选择,如:把某个值从数组的n1下标填充到n2下标