文章目录
- Java基础语法(六)——数组的定义与使用
- 一、 数组基本用法
- 1.什么是数组
- 2. 创建数组
- 3.定义数组的方式
- 4. 数组的使用
- 5.数组在内存中的存储
- 二、数组作为方法的参数
- 1.基本用法
- 2.理解引用类型
- 3.认识 null
- 4.初识JVM内存区域划分
- 三、数组作为方法的返回值
- 四、数组练习
- 五、二维数组
- 完!
本次内容介绍大纲
\
接上篇博客 Java基础语法(五)——方法的使用
\
Java基础语法(六)——数组的定义与使用
\
一、 数组基本用法
\
1.什么是数组
\
数组本质上就是让我们能 “批量” 创建相同类型的变量.
\
例如:
如果需要表示两个数据, 那么直接创建两个变量即可 int a; int b
int a = 10;
int b = 20;
如果需要表示五个数据, 那么可以创建五个变量 int a1; int a2; int a3; int a4; int a5;
int a1 = 10;
int a2 = 20;
int a3 = 30;
int a4 = 40;
int a5 = 50;
但是如果需要表示一万个数据, 那么就不能创建一万个变量了. 这时候就需要使用数组, 帮我们批量创建.
int[] array = {10,20,30,40,50};
上面就是整型数组.
注意事项: 在 Java 中, 数组中包含的变量必须是 相同类型.
\
2. 创建数组
基本语法
在上面的实例中,我们就以整型数组为例,创建了一个 int [ ] 类型的array数组,并且存放了5个整形数据.讲解了数组创建时的用法,Java数组的创建与C语言很是相似,但是还是有区别的.
\
C语言版本数组的创建:
int arr[5] = {1,2,3,4,5};
Java版本数组的创建:
int[] arr = {1,2,3,4,5};
\
我们可以通过两种写法看到其中的区别. Java当中一定要 [ ] 和数据类型紧挨在一起.
数组的数据在内存中是连续存放的.继续以上面的代码为例:
\
数组的每一元素都有其对应的下标,而下标->从0开始,我们要想找到这个数组中的某个数据,是通过数组的下标来进行访问的.
\
注意:
数组也被称为存放一组相同类型的数据的集合!!
下标是从0号位置开始的.
\
3.定义数组的方式
\
定义方式一
上面我们写的代码是,定义并初始化了一个数组.
这样的方式才只是定义了一个数组.这就是我们定义数组的第一种方式.且这样定义的数组默认大小为0.
\
定义方式二
这样也是定义了一个数组,不过这个数组定义是通过new 这样一个关键字,给 array 分配了一块内存,这块内存有10个数据可以存放.
int [ 10 ] 分配的连续内存空间.
不过此时,我们还未给 array 这个数组进行初始化,所以 array 此时的这十个数据默认值为0.
\
定义方式三
在Java当中在已经初始化之后, = 左边的 [ ] 中是不能填数字的, = 右边的 [ ] 只有在 定义方式二 中可以给定值.
\
在定义方式三中
总结定义数组的三种方式:
int[] array1 = {1,2,3,4,5}; // 定义一
int[] array2 = new int[10]; // 定义二
int[] array3 = new int[]{1,2,3,4,5}; // 定义三
在这三种方式中,Java中定义数组最常用的是方式一。
\
4. 数组的使用
\
(1)获取长度
\
注意事项
- 使用 arr.length 能够获取到数组的长度. . 这个操作为成员访问操作符. 后面在面向对象中会经常用到
代码示例:
public static void main(String[] args) {
int[] array = {1,2,3,4,5};
System.out.println(array.length);
}
编译结果:
\
(2)访问数组中的元素
\
数组访问的方式:
\
注意事项:
- 使用 [ ] 按下标取数组元素. 需要注意, 下标从 0 开始计数
- 使用 [ ] 操作既能读取数据, 也能修改数据.
- 下标访问操作不能超出有效范围 [0, length) , 如果超出有效范围, 会出现下标越界异常
代码示例:
public static void main(String[] args) {
int[] array = {1,2,3,4,5};
//获取数组中的元素的个数
//System.out.println(array.length);
//访问数组中的元素
System.out.println(array[1]); // 执行结果: 2
System.out.println(array[2]); // 执行结果: 3
//修改数组中的元素
array[2] = 100;
System.out.println(array[2]); // 执行结果: 100
}
编译结果:
\
(3)下标越界
\
数组下标从0开始,范围是 [0,arr.length) ,左闭右开的区间,或者是[ 0,arr.length-1].
如果我们将下标的值超出数组的范围…
\
如下所示
public static void main(String[] args) {
int[] arr = {1,2,3,4,5,6};
System.out.println(arr[100]);
}
编译结果:
\
(4)遍历数组
\
所谓 “遍历” 是指将数组中的所有元素都访问一遍, 不重不漏. 通常需要搭配循环语句
\
1.遍历方式(一)-----for循环
public static void main(String[] args) {
int[] arr = {1, 2, 3};
for (int i = 0; i < arr.length; i++) {
System.out.println(arr[i]);
}
}
编译结果:
我们可以看到,用 for 循环 将数组中的元素一 一遍历 并打印出来.
\
2.遍历方式(二)---->for-each
for-each 是 for 循环的另外一种使用方式. 能够更方便的完成对数组的遍历. 可以避免循环条件和更新语句写错.
for-each 的基本使用
代码示例:
public static void main(String[] args) {
int[] arr = {1,2,3,4,5};
for (int x:arr) {
System.out.print(x+" ");
}
}
编译结果:
for-each 遍历的原理
遍历array 里面的每一个元素,把每一个元素取出来,然后赋值给了 x ,最后打印 x ,直到 array 里面的元素全部都遍历完成.
两种遍历的方式我们介绍完了,那么for循环和for-each有什么区别?
for循环是可以拿到数组下标的,for-each拿不到数组下标,所以for-each只能够全部遍历,无法对数组元素进行修改或进行操作.
\
3.遍历方式(三)——使用操作数组的工具类进行数组的打印
Arrays 就是操作Java数组的工具类,你要对数组做什么事情,就可以通过他来做,当然有些事情他是完成不了的.
比如说:我们要打印数组,我们本来是用for循环 或者 for-each 来写的,但是我们也可以用Arrays的工具类打印.
通过JDK的工具文档,我们查找到了相应的工具类.
好,我们对arr数组进行操作如下:
public static void main(String[] args) {
int[] arr = {1,2,3,4,5};
System.out.println(Arrays.toString(arr));
}
编辑结果:
\
5.数组在内存中的存储
\
我们在之前的博客已经很简单介绍了Java中的内存区域划分,那么今天我们认识了数组这个引用类型,那么它在内存中如何进行存储呢?
\
我们先简单回顾一下Java的内存区域
我们知道局部变量都在Java虚拟机栈上存放,而数组的数据则在堆上进行存放, 数组的数据在堆上都有特定的地址,而数组的变量存放的其实是这组数据的地址,栈上的这个变量根据这个地址找到堆上的数据.
\
数组的具体存储由下图所示:
注意点:
上图为arr指向的数据在堆中的地址,这个地址并不是真正的地址,它是通过正式的地址 hash 得到的.但是我们可以把它当作真实的地址,因为这个地址也是唯一的.
那么真实的地址为什么要hash操作呢?
这就是Java的安全性了,不会轻易暴露自己数据的地址.
\
二、数组作为方法的参数
\
1.基本用法
\
代码示例: 打印数组内容
public static void main(String[] args) {
int[] arr = {1, 2, 3};
printArray(arr);
}
public static void printArray(int[] a) {
for (int x : a) {
System.out.println(x);
}
}
在这个代码中
1.int [ ] a 是函数的形参, int [ ] arr 是函数实参.
2.如果需要获取到数组长度, 同样可以使用 a.length
\
2.理解引用类型
\
在上一期博客 方法的使用中 ,我们介绍了一个用方法来交换两个变量 的具体情况.现在我们来回顾一下.
\
(1)参数传内置类型
\
\
我们用 内置类型作为参数,进行交换变量,但是最后编译的结果 两个变量却并未发生交换.
这是为什么呢?
交换形参 的值, 不影响实参的 值.
\
(2)参数传数组类型
\
我们用 数组作为参数,进行交换变量,编译运行后,发现成功交换两个变量的值。此时数组名 arr 是一个 “引用” . 当传参的时候, 是按照引用传参.
那么为什么传引用类型可以 形参可对实参进行操作呢?
这里我们就要先从内存说起
在上面我们介绍了数组在内存中的储存,
我们可以知道,数组这个在栈中存放的变量实际存放的是 堆中数据的地址,当我们 arr 数组作为参数 传入 方法里,我们就把 堆中数据的地址 传入了进去,在方法内部,我们可以根据 这个地址 找到堆中的数据进而修改数据,从而实现了形参改变了实参的操作.
\
\
总结:
所谓的 “引用” 本质上只是存了一个地址. Java 将数组设定成引用类型, 这样的话后续进行数组参数传参, 其实只是将数组的地址传入到函数形参中. 这样可以避免对整个数组的拷贝(数组可能比较长, 那么拷贝开销就会很大).
\
3.认识 null
\
引用类型的0值 就是 null.
\
当我们将 null 赋给arr 这个引用类型时,这又是什么意思呢?
\
代表 arr 这个引用,不指向任何对象.
\
当我们运行这个代码时,显示的结果就是 null ,不是数值 0.
\
\
那我们再来看一个问题
当我们将 null 赋给 arr 之后,那么arr 数组的长度是多少呢?
我们猜测可能为0,现在来运行代码.
运行结果如下:
\
此时,编辑器报错,错误类型:空指针异常.
好了,这时我们可以知道,null 赋给了 arr ,arr 没有指向任何一个数组对象,在堆上也没有开辟内存空间,所以我们也就无法求它的长度.
\
总结:
null.任何东西,都会发生 空指针异常的错误.
经验:只要以后出现这样的异常,肯定是这一个引用是 null.
\
4.初识JVM内存区域划分
\
一个宿舍楼会划分成几个不同的区域: 大一学生, 大二学生… 计算机专业学生, 通信专业学生…
\
内存也是类似, 这个大走廊被分成很多部分, 每个区域存放不同的数据.
JVM 的内存被划分成了几个区域, 如图所示:
\
程序计数器 (PC Register):
只是一个很小的空间, 保存下一条执行的指令的地址.
虚拟机栈(JVM Stack):
重点是存储局部变量表(当然也有其他信息). 我们刚才创建的 int[] arr 这样的存储地址的引用就是在这里保存.
本地方法栈(Native Method Stack)
本地方法栈与虚拟机栈的作用类似. 只不过保存的内容是Native方法的局部变量. 在有些版本的 JVM 实现中(例如HotSpot), 本地方法栈和虚拟机栈是一起的.
堆(Heap):
JVM所管理的最大内存区域. 使用 new 创建的对象都是在堆上保存 (例如前面的 new int[]{1, 2, 3} )
方法区(Method Area):
用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据. 方法编译出的的字节码就是保存在这个区域.
运行时常量池(Runtime Constant Pool):
是方法区的一部分, 存放字面量(字符串常量)与符号引用. (注意 从JDK1.7 开始, 运行时常量池在堆上)
关于上面的划分方式, 我们随着后面的学习慢慢理解. 此处我们重点理解 虚拟机栈 和 堆.
1.局部变量和引用保存在栈上, new 出的对象保存在堆上.
2.堆的空间非常大, 栈的空间比较小.
3.堆是整个 JVM 共享一个, 而栈每个线程具有一份(一个 Java 程序中可能存在多个栈).
\
三、数组作为方法的返回值
\
代码示例:
public static void main(String[] args) {
int[] arr = {1, 2, 3};
transform(arr);
printArray(arr);
}
public static void printArray(int[] arr) {
for (int i = 0; i < arr.length; i++) {
System.out.println(arr[i]); }
}
public static void transform(int[] arr) {
for (int i = 0; i < arr.length; i++) {
arr[i] = arr[i] * 2; }
}
\
这个代码固然可行, 但是破坏了原有数组. 有时候我们不希望破坏原数组, 就需要在方法内部创建一个新的数组, 并由方法返回出来。
\
修改后的代码:
public static void main(String[] args) {
int[] arr = {1, 2, 3};
int[] output = transform(arr);
printArray(output);
}
public static void printArray(int[] arr) {
for (int i = 0; i < arr.length; i++) {
System.out.println(arr[i]);
}
}
public static int[] transform(int[] arr) {
int[] ret = new int[arr.length];
for (int i = 0; i < arr.length; i++) {
ret[i] = arr[i] * 2;
}
return ret;
}
这样的话就不会破坏原有数组了.
另外由于数组是引用类型, 返回的时候只是将这个数组的首地址返回给函数调用者, 没有拷贝数组内容, 从而比较高效。
\
四、数组练习
\
1.数组转字符串
\
题目要求:
实现一个方法 toString, 把一个整型数组转换成字符串.
例如数组 {1, 2, 3} , 返回的字符串为 “[1, 2, 3]”, 注意 逗号 的位置和数量.
public static String mytoString(int[] arr){
String ret = "[";
for (int i = 0; i <arr.length ; i++) {
if(i!=arr.length-1){
ret+=arr[i]+",";
}else if(i==arr.length-1){
ret+=arr[i]+"]";
}
}
return ret;
}
public static void main3(String[] args) {
int[] arr ={1,5,6,78,7,87,8,5,45,4,12,12};
//System.out.println(Arrays.toString(arr));
System.out.println(mytoString(arr));
}
\
2.数组拷贝
\
数组的拷贝方式
\
1.for循环拷贝
//数组拷贝
public static void main(String[] args) {
int[] arr = {1,2,3,4,5};
int[] copy = new int[arr.length];
for (int i = 0; i <arr.length ; i++) {
copy[i] = arr[i];
}
System.out.println(Arrays.toString(copy));
}
在这种拷贝方式中,我们首先通过 new 一个新的和 arr 一样长度数组 copy,再通过 for 循环将 arr 数组中的内容 一 一赋给copy数组,达到 最终的数组拷贝的效果.
\
2.Arrays数组的工具类
(1)copyOf()
我们先通过JKD的工具文档,来看一下拷贝工具的用法
功能:复制指定的数组,用零截取或填充(如有必要),以便复制具有指定的长度.
具体看一下Java当中copyOf方法的具体实现
首先 Arrays.copyOf() 的返回类型是 int [ ] ,第一个参数是 原数组(要拷贝的数组),第二个参数是新数组的长度(可以自己定),如果新的数组的长度比原数组长的话,大于原数组长度的元素都补为0 。 具体如下所示…
(2)copyOfRange()
我们先通过JDK文档来查看这个工具类的功能
\
功能:将指定数组的指定范围复制到新的数组中.
具体看一下Java当中copyof方法的具体实现
\
\
copyOfRange 方法的返回类型是 int [ ] ,第一个参数是 原数组 ,第二、三参数是要拷贝原数组数据的下标 ,一定要切记 是左闭右开的区间 , [ from , to ).
\
代码示例
\
3.System.arraycopy
我们打开 System.arraycopy 方法的具体实现,发现没有和上面几种拷贝方法的实现过程,System.arraycopy 有前面的 native 可知,这是一个本地方法。
\
本地方法
1.运行在本地方法栈上
2.底层是由C/C++代码实现的
\
System.arraycopy 没有返回值,第一个参数原数组(要拷贝的数组),第二个参数是原始数组要拷贝的下标,第三个参数是目的地数组 ,第四个参数是 目的地数组的下标,第五个数组是要拷贝的长度.
\
代码示例:
注意点:
\
System.arraycopy 最后一个参数 ——要拷贝的数组长度,这个数据不能超过原数组的长度,否则编辑器会发生错误报告:数组越界。
\
- 数组名.clone ----> 产生当前数组的一个副本
\
功能: 产生一个当前数组的一个副本。
\
代码示例:
\
3. 找数组中的最大元素
\
题目内容:
给定一个整型数组, 找到其中的最大元素 (找最小元素同理)
代码实现:
public static int findMax(int[] arr){
int max = arr[0];
for (int i = 1; i < arr.length; i++) {
if (arr[i] > max) {
max = arr[i];
}
}
return max;
}
public static void main(String[] args) {
int[] arr={10,20,900,40,100,500};
int ret = findMax(arr);
System.out.println(ret);
}
\
4.求数组中元素的平均值
\
题目内容
给定一个整型数组, 求平均值
代码实现:
public static double aver(int[] arr){
int sum = 0;
for (int i = 0; i <arr.length ; i++) {
sum += arr[i];
}
double average = (sum*1.0)/arr.length;
return average;
}
public static void main(String[] args) {
int[] arr = {1,2,8,4,5};
double ret = aver(arr);
System.out.println(ret);
}
注意点:
最后求 aver 方法中平均值 average 时 sum要记得 sum * 1.0,这样算出的平均值才是 double 类型的数据。
\
5.查找数组中指定元素(顺序查找)
\
题目内容
给定一个数组, 再给定一个元素, 找出该元素在数组中的位置.
代码实现
public static int toFind(int[] arr,int key){
for (int i = 0; i <arr.length ; i++) {
if(key==arr[i])
return i;
}
return -1;
}
public static void main(String[] args) {
int[] arr = {1,2,3,4,5};
System.out.println(toFind(arr, 3));
}
\
6.查找数组中指定元素(二分查找)
\
针对有序数组, 可以使用更高效的二分查找.
什么叫有序数组?
有序分为 “升序” 和 “降序” 如 1 2 3 4 , 依次递增即为升序. 如 4 3 2 1 , 依次递减即为降序.
以升序数组为例, 二分查找的思路是先取中间位置的元素, 看要找的值比中间元素大还是小. 如果小, 就去左边找; 否则就去右边找.
代码实现:
public static int binarySearch(int[] arr,int key){
int i = 0;
int j = arr.length-1;
while(i<=j){
int mid = (i+j)/2;
if(arr[mid]==key){
return mid;
}else if(arr[mid]>key){
j=mid-1;
}else if(arr[mid]<key){
i=mid+1;
}
}
//从循环里跳出时还未找到要查找的数字,返回-1(不能是数组下标的值)
return -1;
}
public static void main2(String[] args) {
int[] arr = {1,2,3,4,5,6,77,9};
int ret = binarySearch(arr,1);
System.out.println("查找数字的下标是"+ret);
}
\
二分查找的具体思路可以到我的往期博客——有序数组中查找具体数字n(二分查找)了解详情。
\
7.检查数组的有序性
\
题目内容
给定一个整型数组, 判断是否该数组是有序的(升序)
代码实现:
public static boolean isSorted(int[] array){
if(array==null){
return false;
}
for (int i = 0; i <array.length-1 ; i++) {
if(array[i+1]<array[i]){
return false;
}
}
return true;
}
public static void main1(String[] args) {
int[] array = null;
System.out.println(isSorted(array));
}
注意点:
要记得检查传入数组是否为 null ,如果传入的 arr 数组为 null ,此时返回 false 类型。
\
8.数组排序(冒泡排序)
\
题目内容
给定一个数组, 让数组升序 (降序) 排序.
算法思路
每次尝试找到当前待排序区间中最小(或最大)的元素, 放到数组最前面(或最后面).
代码实现:
public static void main1(String[] args) {
int[] array = {10,51,20,14,64,54};
bubbleSort(array);
System.out.println(Arrays.toString(array));
}
public static void bubbleSort(int[] array){
boolean flg = false;
for (int i = 0; i <array.length-1 ; i++) {
for(int j = 0;j<array.length-1-i;j++){
if(array[j]>array[j+1]){
int tmp = array[j];
array[j] = array[j+1];
array[j+1] = tmp;
flg= true;
}
}
if(!flg)
break;
}
}
\
冒泡排序的具体详解可以到我的往期博客——排序算法之冒泡排序查看,这里就不过多介绍。
\
9.数组逆序
\
题目内容
给定一个数组, 将里面的元素逆序排列.
思路
设定两个下标, 分别指向第一个元素和最后一个元素. 交换两个位置的元素.
然后让前一个下标自增, 后一个下标自减, 循环继续即可
代码实现:
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--;
}
}
public static void main(String[] args) {
int[] arr= {1,2,3,4,5,6,7};
reverse(arr);
System.out.println(Arrays.toString(arr));
}
\
五、二维数组
\
二维数组本质上也就是一维数组, 只不过每个元素又是一个一维数组.
\
规则的二维数组
\
(1)二维数组的定义
(2)内存中的二维数组
\
以上面的 int [ 2 ][ 3 ]为例
int[][] = {{1,2,3},{4,5,6}};
\
我们之前说过二维数组本质上是一个特殊的一维数组。
这个数组的每一行 arr [0] 、arr [1] ,构成了一个一维数组,每一行存放着指向每一列数据的地址。
每一列也是一个单独的一维数组,指向堆中的每一个数据。
\
(3) 二维数组的打印
\
知道了二维数组在内存中的存放与指向,所以我们知道 arr.length 得出的就是 行的个数 , arr [ i ].length 得出的就是 列的个数。
\
- for 循环打印
我们对 这个二维数据进行 for 循环 打印。
\
代码示例:
public static void main(String[] args) {
int[][] arr = {{1,2,3},{4,5,6}};
for (int i = 0; i <arr.length ; i++) {
for (int j = 0; j <arr[i].length ; j++) {
System.out.print(arr[i][j]+" ");
}
System.out.println();
}
}
打印结果:
\
- for-each 打印
如果我们用 for-each 打印,代码示例:
public static void main(String[] args) {
int[][] arr = {{1,2,3},{4,5,6}};
for (int[] array:arr) {
for (int x:array) {
System.out.print(x+" ");
}
System.out.println();
}
}
打印结果如下:
\
- Arrays 工具类 打印
在一维数组中,我们想要将数组转化为字符串打印用的是 Arrays.toString ( ) 。那么二维数组转化为字符串的工具类是什么呢?
如果我们用 Arrays.toString ( ) 进行打印
\
结果打印的是 arr 行代表的数组内容——列代表的一维数组的地址。显而易见 Arrays.toString ( ) 无法打印出二维数组的全部内容。
\
我们查找 JDK 文档发现了 deepToString( ) 工具类。
功能:返回指定数组的 “ 深度内容 ” 的字符串表示形式。
我们用 deepToString()进行打印…
结果如下:
\
成功打印出二维数组的内容。
deepToString ( ) 可以正确的打印出二维数组的全部数据。
\
不规则的二维数组
\
在C语言中,我们定义二维数组可以 只定义列,不用规定行的值。
C语言中数组的定义
int[][2] = {1,2,3,4,5,6,7};
而在Java中我们只能 定义行,列不用规定值。
int[][] arr = new int[2][];
\
Java中不规则二维数组的定义
\
什么是不规则的二维数组?
在之前的规则的二维数组中,每一行的数据个数都相同,列数也相同。而不规则的二维数组,规定了行数,列数有我们自己定,每一行有多少列由我们自己规定。
\
(1)不规则二维数组的定义
public static void main(String[] args) {
int[][] arr = new int[2][];
arr[0] = new int[]{1,2,3};
arr[1] = new int[]{4,5};
System.out.println(Arrays.deepToString(arr));
}
编译结果:
首先我们规定了一个 有两行的二维数组
int [ ] [ ] arr = new int [2] [ ];
我们自己给每一行的数组规定有多少列。
arr [ 0 ] = new int [ ] { 1,2,3 }
arr [ 1 ] = new int [ ] { 4,5 };
这就是不规则的二维数组的定义。
\
(2)内存中的二维数组
与规则的二维数组内存存储基本相同。
\
(3)打印方式
\
不规则的二维数组打印方式同规则二维数组打印方式一样。
\
\
最后,同理, 还存在 “三维数组”, “四维数组” 等更复杂的数组, 只不过出现频率都很低。
\
好了,这次的Java基础语法——数组的定义与使用的知识就分享到这里,感谢大家的欣赏与关注!!
\
谢谢欣赏!!
\