目录
6.4. 使用Arrays.asList()和forEach()方法进行遍历
有意者可加
作者:研J小政
课堂:wclass
(有什么弊端请私信我,目前参考众多资料精华整理过程过中)
章节:主要带领大家彻底认识 泛型设计的绝大部分盲点,让大家学习 泛型设计 章节无弯路可走!
QQ:3544839383
资料学习群:
进度:持续更新迭代!
录课状态:待录
参考文献:
知识串联:
一、前言
在说数组之前,我们要理解一个概念:线性表。
线性表是数据结构中最基本,最简单的一种。一个线性表是由n个具有相同特性的元素组成的有序序列。
而我们今天要学习的数组就是线性表的一种。
数组和线性表的关系就像是父子一样,数组更像是线性表的儿子,它继承了线性表特性的同时又拥有自己
的特性。
那什么是数组呢?
数组(Array)是一种线性数据结构,它是由一段连续的内存单元组成,用来存储具有相同特性的数据。
二、引言
周六一早李雷就来到了韩梅梅的家里,原因无外乎两人相约今天一起去逛街。不过由于来的太早,韩梅梅
还没有准备好出门,李雷只能坐在韩梅梅旁看着她化妆。李雷发现,虽然韩梅梅的换妆品很多,但是她却
可以准确的找到每一种需要的化妆品,好奇促使着李雷向韩梅梅询问。
李雷: 老韩,你有这么多口红,是怎么精确的找到你需要的色号呢?
韩梅梅: 因为有口红收纳箱啊,他是一个一个连续的格子组成的,我可以将所有口红按照色号的顺序存放
进去。
李雷: 这么神奇的吗,那其他化妆品呢?
韩梅梅: 其他化妆品也一样啊,不同的化妆品都有不同的收纳箱,这样我无论需要哪种换妆品,只需要去
寻找对应的收纳箱就好了。
李雷: 你真聪明!
三、特点
1. 通用说法
既然我们说数组具有线性表的所有特性,因此我们可以简单的推断数组具备:
- 数组是有序序列,数组中存储的元素是按照固定的位置一个接一个的排列的
(这里强调的是逻辑上的有序而并非是物理空间上的连续)。 - 数组用来存放的元素一定是具有相同特性的。
接下来我们还要说一些专属于数组的特性
- 数组是由一段连续的内存空间组成的,这导致了我们需要在创建数组的时候就为它指定大小,如果数组内的元素超出数组大小的时候,数组是无法扩容的,我们只能够创建一个新的更大的数组并将之前的元素复制进新数组中。
- 由于数组采用连续的内存空间存放的,这也为它带来了最大的优点:允许随机访问。
2. Java层面
- 数组是一种数据结构,本质上就是用来存储一批同种类型数据的内存区域,占用一片连续的空间区域
- 数组是引用类型,引用指向的是一个对象,也可以说,有地址的变量就称为引用。
- 数组通过索引访问元素,访问格式为 数组名称[索引]
- 数组固定长度,超过固定长度存储或者访问都会抛出异常错误
- 访问数组的长度可以通过length属性访问,数组名.length
- 直接打印数组是以地址形式打印,我们需要借助数组工具类的Arrays的toString方法来帮助我们打印数组元素内容!
注意:在Java中,数组中的变量必须是相同类型的。
3. 优缺点
优点:
- 由于数组空间是连续的,因此我们拥有了随机访问的特性,增加了查询效率。
- 由于保存的是具有相同特性的元素,因此可以更加方便的体现元素间的关联性(如排序)。
缺点:
- 数组长度固定,无法动态扩容。
四、常见操作
1. 数组的创建
1.1. 两种方法
数组的创建有两种方法:
动态初始化:数据类型[] 数组名称 = new 数据类型 [] { 初始化数据 };
静态初始化:数据类型[] 数组名称 = { 初始化数据 };
代码示例:
int[] array2 = new int[] {1,2,3,4,5};
int[] array3 = new int[5];
int[] array1 = {1,2,3,4,5};//常用
1.1.1. 静态初始化数组
定义数组的时候直接给数组赋值
- 数据类型[] 数组名= new数据类型[]{元素1,元素2 ,元素3… };
- 数据类型[] 数组名= { 元素1,元素2 ,元素3,… };
1.1.2. 动态初始化数组
定义数组的时候只确定元素的类型和数组的长度进行默认初始化, 之后再存入具体数据
- 数据类型[] 数组名= new 数据类型[长度];
动态初始化数组后元素的默认值是什么样的?
- byte、short、int 、char、long类型数组元素的默认值都是0
- float、double类型数组元素的默认值都是0.0
- boolean类型数组元素的默认值是false、String和对象类型(也就是引用数据类型)数组元素的默认值是null
1.1.3. 两种初始化应用场景
动态初始化只指定数组长度,后期赋值,适合开始知道数据的数量,但是不确定具体元素值的业务场景
静态初始化开始就存入元素值,适合一开始就能确定元素值的业务场景
1.2. 创建过程
下图是一个简易的内存单元模型图
接下来我们需要创建一个容量为7的数组:
根据数组的特性我们晓得数组是一个连续且大小固定的内存结构,因此我们创建的数组在内存中的体现就
是下图蓝色部分:
当然这只是最简单的模型,真实的内存中不会所有的内存单元都是空闲的。
2. 数组的插入
2.1. 插入过程
我们现在需要将元素 a 插入的数组中,如果我们没有特意强调,元素 a 会被数组安排到第一个空闲的空间
中,如下图:
有的同学会问我为什么说特别强调呢?还记得之前我们提到过的由于数组空间是连续的,因此当我们晓得
我们的数组第一个元素内存地址
的时候,我们就可以通过简单的算术清楚剩下的其他所有的元素地址,因此我们就可以直接通过内存单元
的地址直接去修改指定空间的元素。这也就是我们说的数组支持随机访问。
简单举例,我们可以直接为数组第三个空闲地址(下标为2)的空间插入元素b:
在这里插入图片描述
3. 数组的扩容
3.1. 扩容过程
首先,数组长度是固定的,并没有办法再原有数组的基础上扩容。
假定我们的数组被填满了元素:
此时我们想要插入元素h会发现此时数组已经没有空间可以插入了,那么此时我们就需要为数组进行扩容。
这里需要注意: 数组的扩容并不是简单的增加新的内存空间那么简单。由于数组是连续的内存空间,此时
数组最后一个存放元素 g 的内存空间后面已经被其他应用使用,因此我们无法简单的去征用后续的内存空
间完成扩容。这代表着如果我们想要插入新的元素h,就需要在其他空间创建一个全新的连续内存空间的数
组,并将之前数组的元素复制进入新的空间。
4. 数组大小和默认值
当给数组分配空间时,必须指定该数组能够存储的元素个数,从而确定数组大小。
创建数组之后就不能再修改它的大小。
通过数组的length属性可以知道数组元素个数
5. 访问数组元素
数组元素可以通过下标访问。
数组下标是基于0的,也就是说,其范围从0开始到arrayRefVar.length-1结束
6. 数组遍历
数组遍历就是对数组内的元素一个一个的进行访问
6.1. 使用for循环进行遍历
可以使用普通的for循环来遍历数组。
示例代码如下:
int[] arr = {1, 2, 3, 4, 5};
for(int i = 0; i < arr.length; i++) {
System.out.println(arr[i]);
}
6.2. 使用for-each循环进行遍历
在Java5及其以上版本中,可以使用for-each循环来遍历数组。
示例代码如下:
int[] arr = {1, 2, 3, 4, 5};
for(int num : arr) {
System.out.println(num);
}
6.3. 使用Java8 Stream API遍历
在Java8及其以上版本中,可以使用Stream API来遍历数组。
示例代码如下:
int[] arr = {1, 2, 3, 4, 5};
Arrays.stream(arr).forEach(num -> System.out.println(num));
6.4. 使用Arrays.asList()和forEach()方法进行遍历
在Java8及其以上版本中,也可以使用Arrays.asList()和forEach()方法来遍历数组。
示例代码如下:
Integer[] arr = {1, 2, 3, 4, 5};
List<Integer> list = Arrays.asList(arr);
list.forEach(num -> System.out.println(num));
6.5. while循环
while循环是一种基本的控制结构,它允许程序员在指定的布尔表达式为true的情况下,
反复执行一组语句,直到该布尔表达式为false为止。
while循环通常用于不确定循环次数的情况。
while循环的语法如下:
while (布尔表达式) {
// 执行语句
}
在每次循环开始时,首先计算布尔表达式的值,如果该值为true,则执行循环中的语句,否则跳过循环并
继续执行后续的代码。
在每次循环结束后,再次计算布尔表达式的值,如果值仍为true,则再次执行循环中的语句,以此类推,
直到布尔表达式的值为false时跳出循环。
下面是一个使用while循环的例子,它计算1到10的累加和:
int sum = 0;
int i = 1;
while (i <= 10) {
sum += i;
i++;
}
System.out.println("1到10的累加和为:" + sum);
另一个使用while循环的例子是读取文件中的数据,直到文件的末尾:
import java.io.File;
import java.io.FileNotFoundException;
import java.util.Scanner;
public class Example {
public static void main(String[] args) {
try {
Scanner scanner = new Scanner(new File("data.txt"));
while (scanner.hasNext()) {
String line = scanner.nextLine();
System.out.println(line);
}
scanner.close();
} catch (FileNotFoundException e) {
System.out.println("文件未找到:" + e.getMessage());
}
}
}
在这个例子中,while循环的布尔表达式是scanner.hasNext(),表示只要文件还有数据可以读取,就继续
执行循环中的语句,即读取一行数据并输出。当文件的末尾被读取到时,布尔表达式的值为false,跳出循
环并关闭文件。
6.6. do...while循环
do-while允许在指定的布尔表达式为true的情况下,先执行一次循环中的语句,然后再反复执行该组语
句,直到布尔表达式为false为止。
与while循环不同的是,do-while循环至少执行一次循环中的语句。
do-while循环的语法如下:
do {
// 执行语句
} while (布尔表达式);
在循环开始时,先执行循环中的语句,然后计算布尔表达式的值。
如果该值为true,则再次执行循环中的语句,以此类推,直到布尔表达式的值为false时跳出循环。
下面是一个使用do-while循环的例子,它计算输入的数字的平均值:
import java.util.Scanner;
public class Example {
public static void main(String[] args) {
Scanner scanner = new Scanner(System.in);
int sum = 0;
count = 0;
do {
System.out.print("请输入一个数字(输入0表示结束):");
int num = scanner.nextInt();
sum += num;
count++;
} while (num != 0);
double average = (double) sum / (count - 1);
System.out.println("平均值为:" + average);
}
}
在这个例子中,do-while循环的布尔表达式是num != 0,表示只要用户输入的数字不为0,就继续执行循
环中的语句,即将数字累加到sum中,并将count加1。当用户输入0时,布尔表达式的值为false,跳出循
环并计算平均值。
由于输入的数字中包含0,因此计算平均值时需要将count减1。
6.7. 总结
以上是常用的Java数组遍历方法,每种方法有其优缺点。在选择遍历方法时,应根据实际情况进行选择。
五、名词概念
1. 数组是对象
数组是引用数据类型
数组对象在堆中创建,声明的变量是引用类型,引用存储的是数组对象的地址值,指向数组对象
可以将数组中的元素看成对象的成员变量,只不过类型完全一致而已。
2. 数组内存图
一维数组内存图
二维数组内存图
六、常见异常
1. 数组索引越界异常
Exception in thread "main" java.lang.ArrayIndexOutOfBoundsException: Index 3 out of bounds for length 3
2. 空指针异常
Exception in thread "main" java.lang.NullPointerException: Cannot load from int array because "arr" is null
七、二维数组
1. 简介
二维数组就是元素为一维数组的数组,N维数组就是元素为N-1维数组的数组!
比如:二维数组定义如下:
2. 二维数组的创建
2.1 声明并赋值
//变量数据类型[] 变量名={{值1,值2...},{值1,值2,值3}};例子如下所示:
int[][] a={{1,4,3},{2,6},{7},{1,9}};
/*一维数组的个数由最内层的花括号个数决定,每个一维数组的数组长度由赋值的内容的多少来决定,其中第一个一维数组的长度为3,第二个一维数组的长度为2,第三个一维数组长度为1,第四个一维数组长度为2,a[0][0]=1,a[0][1]=4,a[0][2]=3,a[1][0]=2,a[1][1]=6
a[2][0]=7,a[3][0]=1,a[3][1]=9
*/
2.2 通过实例化创建一个数组对象
//变量数据类型[][] 变量名=new 变量数据类型[一维数组的个数][每个一维数组的长度];如下所示给出了一个创建的例子
int[][] a=new int[5][6];
//定义一个能存放5个一维数组的二维数组,其中每个一维数组的长度都为6
2.3 先声明后实例化数组对象
//分为a.b两步
//a.变量数据类型[][] 变量名 b. 变量名=new 变量数据类型[一维数组个数][每个一维数组的长度];例子如下所示:
int[][] a;
a=new int[5][6];
/*定义了一个可以存放5个一维数组的二维数组a,其中每个一维数组的长度为6,这种方法不建议使用,因为没有必要一个数组定义都占着两行*/
2.4 通过实例化创建数组对象,并给其进行初始化操
//变量数据类型[][] 变量名=new 变量数据类型[][]{{值1,值2...},{值1,值2...},{值1,值2,值3,值4...}}
int[][] a=new int[][]{{1,4,6},{7,99}}
/*内层花括号个数决定了能存放的一维数组的个数,每个一维数组的长度由最内层花括号里面的值的个数决定,其中a[0][0]=1,a[0][1]=4,a[0][2]=6,a[1][0]=7,
a[1][1]=99 */
3. 二维数组的长度问题
3.1 array.length
在二维数组的初始化里面我们初始化了一个array的数组,具体运用的第三种方法中的代码创建的,其总共
有两行,第一行只有一个元素,第二行有两个元素,所以这个二维数组的长度是多少呢?
array.length 表示的是数组的长度,此时用到了本文之前提得到的方法,将二维数组抽象等价为一个集
合,这个集合具有的元素个数就是这个二维数组的长度,即array.length 。
显而易见,根据上面代码图我们创建的二维数组里有两个元素(每个元素是一个一维数组)
3.2 array[i].length
array[i].length 表示的是(二维数组所等价的)集合中第i+1个元素(该元素本身是一个一维素组)作为一
维素组的数组的长度。
具体在上图的代码中二维数组的第一行只有一个元素,第二行有两个元素,即array[0].length = 1;
array[1].length = 2.
4. 二维数组遍历方式
4.1 for循环遍历
int[][] array = new int[3][]; //java行不能省略,列可以省略
array[0] = new int[]{0, 1, 2};
array[2] = new int[]{4, 5, 6,7};
for(int i=0;i< array.length;i++){
if(array[i]==null) {
System.out.println("null"+" ");
continue;
}//当二维数组某一行为空时直接跳过循环遍历下一行
for (int j = 0; j < array[i].length; j++) {
System.out.print(array[i][j]+" ");
}//二维数组的每一行元素都相当于一个一维数组,
//遍历一维数组,长度就是array[i].length
System.out.println();
}
运行截图:
4.2 通过Arrays.deepToString()遍历
int[][] array = new int[3][]; //java行不能省略,列可以省略
array[0] = new int[]{0, 1, 2};
array[2] = new int[]{4, 5, 6,7};
System.out.println(Arrays.deepToString(array));
运行截图:
4.3 通过for( : )遍历
int[][] array = new int[3][]; //java行不能省略,列可以省略
array[0] = new int[]{0, 1, 2};
array[2] = new int[]{4, 5, 6,7};
int i=0;
for(int[] ret:array){
if(array[i]==null) {
System.out.println("null"+" ");
i++;
continue;
}
for(int x:ret){
System.out.print(x+" ");
}
System.out.println();
i++;
注意:冒号左边填写的是数组每个元数的类型,右边填写的是数组名
八、多维数组
1. 简介
其实与其说多维数组,不如说二维数组,主要是由于除了二维数组外,其他的我们基本都不会使用。那
么,什么是二维数组?
我们之前说过数组是用来存放一组具有相同特性的元素的集合,而数组与数组之间就具有相同的特性:它
们都是数组。
那自然而然我们可以认为,我们可以将具有相同特性的数组可以存放入另一个数组中,而这个数组就是一
个二维数组。
2. 二维数组模型
其实二维数组理解起来很简单,总结起来就是用一个数组去存放具有相同特点数组的引用信息,当然后续
我们还会通过代码实战来更详细的演示。
注意: 二维数组又称为矩阵,行列数相等的矩阵称为方阵。对称矩阵ai = aj,对角矩阵:n阶方阵主对角线
外都是零元素。
九、其他类型数组
1. 锯齿数组
锯齿状数组是一个多维数组,其中成员数组的大小不同。
例如,我们可以创建一个2D数组,其中第一个数组包含3个元素,并且包含4个元素。
以下是演示锯齿状数组概念的示例。
public class Tester {
public static void main(String[] args){
int[][] twoDimenArray = new int[2][];
//第一行有3列
twoDimenArray[0] = new int[3];
//第二行有4列
twoDimenArray[1] = new int[4];
int counter = 0;
//初始化数组
for(int row=0; row < twoDimenArray.length; row++){
for(int col=0; col < twoDimenArray[row].length; col++){
twoDimenArray[row][col] = counter++;
}
}
//打印数组
for(int row=0; row < twoDimenArray.length; row++){
System.out.println();
for(int col=0; col < twoDimenArray[row].length; col++){
System.out.print(twoDimenArray[row][col] + " ");
}
}
}
}
输出结果
0 1 2
3 4 5 6
2. 压缩数组(稀疏数组)
就像它的名字一样,压缩数组实际上是一种用来减少常规数组空间消耗的特殊应用。稀疏数组平时其实大
家很少能遇到,它最长见于保存棋盘信息的程序应用中。
我们先看个最简单的应用:如下图所见,这是一个5×5的方格,我们需要用程序来模拟它的状态
常见实现(二维数组-java):
public class Demo {
public static void main(String[] args) {
/* 我们用0代表白色,1代表黑色*/
int[][] arr = {{0,0,0,0,0},{0,1,0,0,0},{0,0,0,0,0},{0,0,0,1,0},{0,0,0,0,0}};
for (int[] vertical:arr) {
for (int transverse:vertical ) {
System.out.print(transverse + " ");
}
System.out.println("");
}
}
}
结果:
0,0,0,0,0
0,1,0,0,0
0,0,0,0,0
0,0,0,1,0
0,0,0,0,0
我们没有办法说这里有什么错误,但是假定我们是个 500*500 的方格,显然我们就需要使用 250000 个内存
单元来保存这个二维数组,我们有没有办法优化呢?当然有!
我们通过观察可以发现,当前数组大多数元素为相同元素,因此我们可以将这部分元素省略,只保存不同的部
分。
常见的稀疏数组一般格式如下:
- 稀疏数组本身也是个二维数组
- 数组的首个元素用来存放三个信息(总行数,总列数,总变更元素数)
- 第二个开始存放变更的元素信息(行数,列数,真实值)
我们将上面的数组转换为稀疏数组存放:
稀疏数组(简单实现-java):
int [][] arr ={{5,5,2},{1,1,1},{3,3,1}};
当然我们如果想要看效果,我们将压缩数组转换为传统二维数组:
public class Demo {
public static void main(String[] args) {
int[][] arr = {{5, 5, 2}, {1, 1, 1}, {3, 3, 1}};
int[][] oldArr = new int[arr[0][0]][arr[0][1]];
for (int i = 1; i < arr.length; i++) {
oldArr[arr[i][0]][arr[i][1]] = arr[i][2];
}
for (int[] vertical : oldArr) {
for (int transverse : vertical) {
System.out.print(transverse + " ");
}
System.out.println("");
}
}
}
结果:
0,0,0,0,0
0,1,0,0,0
0,0,0,0,0
0,0,0,1,0
0,0,0,0,0
十、可变长参数列表
1. 简介
所谓可变长参数列表,就是具有同样类型的可变长度的参数可以传递给方法,并将作为数组对待。
可以把类型相同但个数可变的参数传递给方法。
2. 定义格式
type Name... parameterName(类型名... 参数名)
在方法声明中,指定类型后紧跟着省略号(...)
只能给方法中指定一个可变长参数,同时该参数必须是最后一个参数。任何常规参数必须在它之前。
Java将可变长参数当成数组对待。可以将一个数组或数目可变的参数传递给可变长参数。
当用数目可变的参数调用方法时,Java会创建一个数组并把参数传给它。
3. 代码演示
package tempExample;
import java.util.Arrays;
import java.util.Collection;
import java.util.List;
//author @zhengge
public class VarLenParamList {
/**
* 空字符
*/
public static final String BLANK = "";
/**
* 符号(:)
*/
public static final String SIGN_NEGATIVE = ":";
public static boolean isEmpty(Collection<?> collection) {
return collection == null || collection.isEmpty();
}
public static boolean isNotEmpty(Collection<?> collection) {
return !isEmpty(collection);
}
/**
* 如果对象为空,则转换为空
*
* @param str 转换对象
* @return 处理结果
*/
public static String nullToEmpty(String str) {
if (str == null) {
return BLANK;
}
return str;
}
/**
* 收集字符串
*
* @param concatMark 连接符号
* @param strArray 可变长度参数列表(String...)
* @return 字符串
*/
public static String concatStringWithSeparate(String concatMark, String... strArray) {
return concatStringWithSeparate(concatMark, Arrays.asList(strArray));
}
/**
* 收集字符串
*
* @param concatMark 连接符号
* @param strList 字符串列表
* @return 字符串
*/
public static String concatStringWithSeparate(String concatMark, List<String> strList) {
StringBuilder sb = new StringBuilder();
concatMark = nullToEmpty(concatMark);
if (isNotEmpty(strList)) {
for (int i = 0; i < strList.size(); i++) {
String str = strList.get(i);
sb.append(nullToEmpty(str));
if (i != strList.size() - 1) {
sb.append(concatMark);
}
}
}
return sb.toString();
}
public static void main(String[] args) {
System.out.println(concatStringWithSeparate(SIGN_NEGATIVE,nullToEmpty("Java方法参数(String...)是"),nullToEmpty("可变长度参数列表")));
System.out.println(concatStringWithSeparate(SIGN_NEGATIVE,nullToEmpty("Java方法参数(String...)是"),nullToEmpty("可变长度参数列表")
,nullToEmpty("可变长度参数列表"),nullToEmpty("可变长度参数列表"),nullToEmpty("可变长度参数列表")));
System.out.println(concatStringWithSeparate(SIGN_NEGATIVE,nullToEmpty(""),nullToEmpty("")));
System.out.println(concatStringWithSeparate(SIGN_NEGATIVE,nullToEmpty(""),nullToEmpty(""),nullToEmpty(""),nullToEmpty("")));
}
}
十一、java中main函数的参数
1. 简介
public static void main(String[] args){}
main方法作为java程序的入口,能够被JVM自动识别并调用。
其参数是一个String类型的数组,当以命令行的形式运行java程序时,可以在类名后面输入任意多个字符串
作为实参传递给main函数。
public class MainDemo
{
public static void main(String[] args)
{
System.out.println(args);
System.out.println(args.length);
for(int i = 0; i < args.length; i++)
System.out.println(args[i]);
}
}
命令行中首先编译
javac MainDemo.java
然后运行
java MainDemo a b c
注意:参数之间以空格隔开
结果:
3
a
b
c
2. 总结
我们知道,main方法可以从命令行接收字符串参数,它具有String[]类型参数args。
我们可以在运行字节码文件的时候传入相应字符串参数!
例如,下面的命令行用三个字符串a、b、c 启动程序MainDemo:
java MainDemo a b c
其中,参数a、b和c都是字符串,但是在命令行中出现时,不需要放在双引号中。
这些字符串用空格分隔,如果字符串包含空格,那就必须使用双引号括住
注意:如果运行程序时没有传递字符串,那么使用newString[0]创建数组。
在这种情况下,该数组是长度为0的空教组,args是对这个空数组的引用。