🎐 前情提要
✨本节目标:
- 理解数组基本概念
- 掌握数组的基本用法
- 数组与方法互操作
- 熟练掌握数组相关的常见问题和代码
👩🏻🏫作者: 初入编程的菜鸟哒哒
文章目录
- 1. 数组的基本概念
- 1.1 为什么要使用数组
- 1.2 什么是数组
- 1.3 数组的创建及初始化
- 1.4 如何访问数组当中的元素
- 1.5 如何求数组长度:
- 1.5 如何遍历数组:
- 2. 数组是引用类型
- 2.1 初始JVM的内存分布
- 2.2 再谈引用变量
- 2.3 认识 null
- 3. 数组拷贝
- 4.数组练习
- 4.1求数组中元素的平均值
- 4.2 查找数组中指定元素(顺序查找)
- 最简单的顺序查找:
- 二分查找:
- 4.3 数组排序(冒泡排序)
- 4.4 数组逆序
- 5. 二维数组
- 总结
1. 数组的基本概念
1.1 为什么要使用数组
如果我们想定义3个整型数据,你可能会这样定义:
public static void main(String[] args) {
int a = 8;
int b = 6;
int c = 9;
}
当然,这样定义是没有问题的。
但如果我们想定义10个整型数据呢?
很显然一个一个定义就显得有些麻烦…
在java当中有一个构造数据类型:数组
当你想创建10个变量并且他们是相同类型的时候,我们就可以通过数组来进行定义。
1.2 什么是数组
数组:可以看成是相同类型元素的一个集合。在内存中是一段连续的空间。
数组的特点:
- 数组中存放的元素其类型相同
- 数组的空间是连在一起的
- 每个空间有自己的编号,其实位置的编号为0,即数组的下标。
那在程序中如何创建数组呢?
1.3 数组的创建及初始化
第一种定义方式:
public static void main(String[] args) {
int[] arr = {1,2,3,4,5};
}
这样就定义好了一个数组。 int[]
代表一个整型的数组类型,arr
是数组名,虽然我们没有指定数组的长度,但编译器在编译时会根据{}
中元素个数来确定数组的长度。
第二种定义方式:
public static void main(String[] args) {
int[] arr = new int[]{1,2,3,4,5};
}
第三种定义方式:
public static void main(String[] args) {
int[] arr = new int[5];//这里面的5是数组的长度
}
这三种定义方式在内存中的存储方式都是一样的,第一种只不过是第二种的一种简写,第三种里面没有存储数据,默认为里面全都是0(boolean
默认为false
)。
如果不确定数组当中内容时,应该使用动态初始化。
为什么呢?
因为数组是没办法直接更改里面的所有元素的:
比如:
这样写是不被允许的。以定义方法一为例,我们直接打印数组名System.out.println(arr);
可以打印出整个数组吗?
可以看见结果不是整个数组:
这个时候我们可以发现arr
虽然是变量,但里面存了一个地址,这个时候就把这个变量叫做引用变量。
所以我们把里面存地址的变量都叫做引用。
关键字new
:
我们通过new关键字来创建一个对象。
引用里面存储了对象的地址,我们通常说: 引用指向了/引用了一个对象
1.4 如何访问数组当中的元素
数组名[合法的下标]
举个栗子:
public static void main(String[] args) {
int[] arr = {1,2,3,4,5};
System.out.println(arr[0]);
}
也可以修改:
public static void main(String[] args) {
int[] arr = {1,2,3,4,5};
System.out.println(arr[0]);
arr[0] = 9;
System.out.println(arr[0]);
}
那什么叫合法的下标呢?比如如果我把arr[0]
改成arr[5]
(不合法的下标):
这个时候就会报错:
如果再初始化只能改成动态的:
1.5 如何求数组长度:
只需要调用length
函数:数组名.length
public static void main(String[] args) {
int[] arr = {1,2,3,4,5};
int len = arr.length;//这个就是数组长度
System.out.println(len);
}
执行结果:
1.5 如何遍历数组:
所谓 “遍历” 是指将数组中的所有元素都访问一遍, 访问是指对数组中的元素进行某种操作。
比如:打印。
第一种打印方式:
使用for循环
:
第二种打印方式:
使用for each
:
for(int x : arr) {
System.out.println(x);
}
上面代码的意思是把arr
的每项都存在x
中,存一个打印一个,存一个打印一个。
第三种打印方式:
调用Arrays
中的toString
函数,将传进去的数组转化为字符串。
2. 数组是引用类型
2.1 初始JVM的内存分布
JVM一共有五块内存:
我们平时嘴边上说的(接触到的)栈都是Java虚拟机栈,堆也是我们现阶段经常用到的。
那什么叫线程隔离的数据区呢?
在多线程中,每一个线程都有方法区,程序方法区、程序计算器(单独的线程隔离的数据区),而Java数据栈、堆(所有线程共享的数据区)是各个线程共用的。
- 程序计数器:只是一个很小的空间,保存下一条执行指令的地址。
- 虚拟机栈:我们平时说的局部变量在栈上,方法在栈上开辟内存,所指的就是虚拟机栈。与方法调用相关的一些信息,每个方法在执行时,都会先创建一个栈帧,栈帧中包含有:局部变量表、操作数栈、动态链接、返回地址以及其他的一些信息,保存的都是与方法执行时相关的一些信息。比如:局部变量。当方法运行结束后,栈帧就被销毁了,即栈帧中保存的数据也被销毁了。
- 本地方法栈:执行的是Native方法,是C/C++实现的,在有些版本的 JVM 实现中(例如HotSpot), 本地方法栈和虚拟机栈是一起的。
- 堆:比如我们说的malloc在堆上分配内存,是JVM所管理的最大的内存区域,使用new创建的对象都是堆上保存。堆是随着程序开始运行时而创建,随着程序的退出而销毁,堆中的数据只要还有在使用,就不会被销毁。
- 方法区:存储当前类的一些信息,包含已被虚拟机加载的类信息(主要存储)、常量、静态变量、即时编译器编译后的代码等数据。方法编译出的的字节码就是保存在这个区域。
举例讲解一下:
这块代码在内存中的分配情况是怎么样的呢?
在执行main
函数的时候我们会在栈中给main
函数开辟一片栈帧。
这个时候我们就说arr
这个引用指向那个堆上的对象。
到这里就能看出来这个通过new
(省略了)创建的对象其实存储在堆上。
2.2 再谈引用变量
看这个代码:
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]);
}
}
public static void main(String[] args) {
func();
}
在执行arrar1 = array2
语句之前,java虚拟机的数据区是这样的。
执行了之后,array1
转为指向array2
:
所以下面的array1[2]
、array1[3
]指向的其实就是array2[2]
、array2[3]
,array2的指向没有发生改变,所以array2[4]
还是指向array2[4]
。
赋值之后堆上数据变成这样:
把array2
打印出来:
2.3 认识 null
java中的局部变量我们在使用的时候一定要初始化。
那如果这个变量是一个引用变量我们怎样给它初始化呢?
int[] arr3 = null;
意思是:array3
这个引用不指向任何对象。
那这样写有问题吗?
int[] arr3 = null;
System.out.println(arr3.length);
答案是不可以的:
编译器给我们报了一个空指针异常。
有一个很棒的方法排查空指针异常:
就是在编译器指向的那一行找.
号,一般空指针异常都是用空指针.
了一个东西。
或者:
这样也会报空指针异常。
因为arr3
这个引用是没有指向对象的。
接下来大家做一道题:
下面这个代码的执行结果是什么呢?
public static void func1(int[] array) {
array = new int[]{1,2,3};
}
public static void func2(int[] array) {
array[0] = 99;
}
public static void main(String[] args) {
int[] array = {9,8,7};
func1(array);
for (int i = 0; i < array.length; i++) {
System.out.print(array[i]+ " ");
}
System.out.println();
System.out.println("===========");
func2(array);
for (int i = 0; i < array.length; i++) {
System.out.print(array[i]+ " ");
}
System.out.println();
}
答案是:
为什么func1()
没有改变array
引用所指向对象里面的值,而func2()
就改变了呢?
因为
public static void func1(int[] array) {
array = new int[]{1,2,3};
}
这个fun1()
函数形参array
一开始确实存的是实参array
的值,但是array = new int[]{1,2,3};
这段代码改变了形参array
的指向,是在堆中又建立了一个{1,2,3}
这个数组,形参array
指向了这个新数组,并且出函数就销毁了。
而fun2()
中的形参array
没有改变,所以就直接改变实参array
指向的数组中的数据了。
3. 数组拷贝
拷贝:
- 要有原来的
- 拷贝出来一个新的
非常简单:一个一个放进去就可以了
第一种方法:
public class TestDemo {
public static int binarySearch(int[] arr,int num) {
int left = 0;
int right = arr.length;
while(left<=right) {
int mid = (left+right)/2;
if(arr[mid]<num) {
left = mid+1;
}else if(arr[mid]>num) {
right = mid - 1;
}else {
return mid;
}
}
return -1;
}
public static void main(String[] args) {
int[] arr = {1,2,3,4,5,6,7};
int ret = binarySearch(arr,6);
System.out.println(ret);
}
}
难道以后拷贝都用这个方法吗?
不是的。
第二种方法:
可以使用java
当中提供的工具类:
copyOf
方法在进行数组拷贝时,创建了一个新的数组。
全部拷贝:
public static void main(String[] args) {
int[] arr = {1,2,3,4,5};
int[] copy = Arrays.copyOf(arr,arr.length);
System.out.println(Arrays.toString(copy));
}
拷贝一部分:
public static void main(String[] args) {
int[] arr = {1,2,3,4,5};
int[] copy = Arrays.copyOf(arr,3);
System.out.println(Arrays.toString(copy));
}
扩容:
public static void main(String[] args) {
int[] arr = {1,2,3,4,5};
int[] copy = Arrays.copyOf(arr,arr.length*2);
System.out.println(copy.length);
}
第三种方法:
也可以使用Arrays.copyOfRange()
。
可以拷贝一部分:
在Java中一般使用from都是左闭右开的。
第四种方法:
System.arraycopy();
System.arraycopy();
是native方法。
native
: C/C++实现的方法。
优点: 快!
System.arraycopy()
参数:
- 你要拷贝的数组
- 你要从这个数组的哪个下标开始访问
- 你要拷贝到哪个数组
- 你要拷贝到这个数组的哪个位置开始
- 你要拷贝多大
下面这个代码叫拷贝吗?
int[] arr = {1,2,3,4,5,6};
int[] copy = arr;
System.out.println(Arrays.toString(copy));
根本没有发生拷贝,
只是copy
这个引用指向了arr
这个引用指向的对象。
简单介绍一下浅拷贝深拷贝:
如果能够做到修改拷贝之后的数组不影响原来的数组,就说这个拷贝是深拷贝。
如果arr
指向的对象里面的每一个元素都是一个引用,那copy
所指向的对象里面的每一个元素也都是一个引用。这时我们写这个代码:copy[0].a = 9;
就影响了原来的数组,这就是浅拷贝。
深拷贝和浅拷贝不是说拷贝基本数据类型还是引用类型,具体需要看代码的实现。
拿上面的例子来说如果拷贝arr的时候把arr所指对象 指向的对象也全部拷贝下来了的话,改变copy
还是不会改变arr
,这就变成了深拷贝。
4.数组练习
4.1求数组中元素的平均值
public static double avg(int[] array) {
int sum = 0;
for(int x:array) {
sum+=x;
}
return sum*1.0/array.length;
}
public static void main(String[] args) {
int[] arr = {1,2,3,4,7};
System.out.println(avg(arr));
}
4.2 查找数组中指定元素(顺序查找)
给定一个数组, 再给定一个元素, 找出该元素在数组中的位置.
查找数组中的某个元素:
最简单的顺序查找:
public static int findKey(int key,int[] arr) {
for (int i = 0; i < arr.length; i++) {
if(arr[i] == key) {
return i;
}
}
return -1;
}
public static void main(String[] args) {
int[] array = {1,2,3,4,5};
int index = findKey(3,array);
System.out.println(index);
}
二分查找:
public class TestDemo {
public static int binarySearch(int[] arr,int num) {
int left = 0;
int right = arr.length;
while(left<=right) {
int mid = (left+right)/2;
if(arr[mid]<num) {
left = mid+1;
}else if(arr[mid]>num) {
right = mid - 1;
}else {
return mid;
}
}
return -1;
}
public static void main(String[] args) {
int[] arr = {1,2,3,4,5,6,7};
int ret = binarySearch(arr,6);
System.out.println(ret);
}
}
4.3 数组排序(冒泡排序)
import java.util.Arrays;
public class TestDemo {
public static void bubbleSort(int[] arr) {
int len = arr.length;
for (int i = 0; i < arr.length-1; i++) {
int flag = 0;
for (int j = 0; j < arr.length-1-i; j++) {
if(arr[j]>arr[j+1]) {
int temp = arr[j];
arr[j] = arr[j+1];
arr[j+1] = temp;
flag = 1;
}
}
if(0 == flag) {
break;
}
}
}
public static void main(String[] args) {
int[] arr = {2,3,4,1,7,5};
bubbleSort(arr);
System.out.println(Arrays.toString(arr));
}
}
冒泡排序性能较低。 Java
中内置了更高效的排序算法:
public static void main(String[] args) {
int[] arr = {9, 5, 2, 7};
Arrays.sort(arr);
System.out.println(Arrays.toString(arr));
}
4.4 数组逆序
给定一个数组, 将里面的元素逆序排列
public static void main(String[] args) {
int[] arr = {1, 2, 3, 4};
reverse(arr);
System.out.println(Arrays.toString(arr));
}
public static void reverse(int[] arr) {
int left = 0;
int right = arr.length - 1;
while (left < right) {
int tmp = arr[left];
arr[left] = arr[right];
arr[right] = tmp;
left++;
right--;
}
}
5. 二维数组
二维数组本质上也就是一维数组, 只不过每个元素又是一个一维数组。
也可以理解为二维数组每行都存的是一个数组的引用。
定义二维数组的三种方法:
int[][] array = {{1,2,3},{4,5,6}};
int[][] array2 = new int[2][3];
int[][] array3 = new int[][] {{1,2,3},{4,5,6}};
第一种遍历方式:
int[][] array = {{1,2,3},{4,5,6}};
int[][] array2 = new int[2][3];
int[][] array3 = new int[][] {{1,2,3},{4,5,6}};
二维数组是特殊的一维数组。
第二种遍历方式:
for (int [] tmp:array) {
for (int x:tmp) {
System.out.print(x+" ");
}
System.out.println();
}
第三种遍历方式:
System.out.println(Arrays.deepToString(array));
二维数组的用法和一维数组并没有明显差别, 因此我们不再赘述。