数组
概念
- 数组:若干个相同数据类型按一定顺序排列的集合。
- 数组中的概念:
- 数组名:这组相同数据类型的变量,使用的一个统一的名称
- 下标(索引):区分数组中的每一个数据/变量,我们需要引入索引/编号/下标/下脚标,从0开始,范围是[0,array.length - 1]。越界则会产生
ArrayIndexOutOfBoundsException
- 元素:数组的每一个数据
- 长度:这组数的总个数,用array.length表示
特点
- 数组本身是
引用数据类型
,而数组中的元素可以是任何数据类型
,包括基本数据类型和引用数据类型。 - 创建数组对象会在内存中开辟一整块
连续的空间
。占据的空间的大小,取决于数组的长度和数组中元素的类型。 - 数组中的元素在内存中是依次紧密排列的,有序的。
- 数组,一旦初始化完成,其长度就是确定的。数组的
长度一旦确定,就不能修改
。 - 我们可以直接通过下标(或索引)的方式调用指定位置的元素,速度很快。
- 数组名中引用的是这块连续空间的
首地址
。
分类
1、按照元素类型分:
- 基本数据类型元素的数组:每个元素位置存储基本数据类型的值
- 引用数据类型元素的数组:每个元素位置存储对象(本质是存储对象的首地址)
2、按照维度分:
- 一维数组:存储一组数据
- 二维数组:存储多组数据,相当于二维表,一行代表一组数据。
一维数组
声明
elementType[] arrayRefVar; //即元素类型[] 数组名称
int[] arr;
double[] arr1;
String[] arr2;
//-------------------------------------------下述不推荐-----------------------------
elementType arrayRefVar[];
数组的声明,需要明确:
-
数组的维度:在Java中数组的符号是[],[]表示一维,[][]表示二维。
-
数组的元素类型:即创建的数组容器可以存储什么数据类型的数据。元素的类型可以是任意的Java的数据类型。例如:int、String、Student等。
-
数组名:就是代表某个数组的标识符,数组名其实也是变量名,按照变量的命名规范来命名。数组名是个引用数据类型的变量,因为它代表一组数据。
声明数组时不能指定长度,如int a[5]是非法的
初始化
静态初始化
-
如果数组变量的初始化和数组元素的赋值操作同时进行,那就称为静态初始化。
-
静态初始化,本质是用静态数据(编译时已知)为数组初始化。此时数组的长度由静态数据的个数决定。
格式1
elementType[] arrayRefVar = new elementType[]{value1, value2, value3 ...};
//或
elementType[] arrayRefVar;
arrayRefVar = new elementType[]{value1, value2, value3 ...};
new:不同于声明基本数据类型变量,声明一个数组变量时不会给数组分配任何内存空间,它知识创建一个对数组引用的存储位置。如果变量不包含对数组的引用,那么这个变量的值为null。除非数组已经被创建,否则不能给它分配任何元素。声明数组变量之后可以采用new操作符创建数组并将其引用赋给一个变量。
格式2
elementType[] arrayRefVar = {value1, value2, value3, ...}; //不使用操作符new且不能把声明、创建和初始化分开!
//比如下面是错误的
double[] list;
list = {1.1, 2.2, 3.3, ...};
动态初始化
-
数组变量的初始化和数组元素的赋值操作分开进行,即为动态初始化。
-
动态初始化中,只确定了元素的个数(即数组的长度),而元素值此时只是默认值,还并未真正赋自己期望的值。真正期望的数据需要后续单独一个一个赋值。
elementType[] arrayRefVar = new elementType[arraySize];
//或
elementType[] arrayRefVar;
arrayRefVar = new elementType[arraySize];
- 长度一旦指定,不可更改!
使用
长度
- 数组元素的总个数,使用类如
arr.length
来指明arr的长度。 - 长度一旦确定就无法更改!
引用
-
数组存在于一块连续的存储空间,下标从0开始标
-
使用arr[i]来表示数组中的一个元素
-
i的取值范围是[0, arr.length - 1]
-
[]中可以是整型常量或表达式,如arr[i * 2], arr[3]
默认值
- 数组是引用类型,使用动态初始化方式创建数组时,元素是默认值。
数组元素类型 | 默认值 |
---|---|
byte | 0 |
short | 0 |
int | 0 |
long | 0L |
float | 0.0F |
double | 0.0 |
char | \u0000 |
boolean | false |
引用类型 | null |
- 注意:NULL和0不同
遍历
- 遍历就是把数组的元素一个个拿出来,for循环比较契合这一使用特性。
for循环
import java.util.Scanner;
public class Main {
public static void main(String[] args) {
//输入若干个数
double[] myList = new double[10];
Scanner input = new Scanner(System.in);
System.out.println("Enter 10 numbers: ");
for (int i = 0; i < myList.length; i++) {
myList[i] = input.nextDouble();
}
}
}
foreach循环
for(elementType element : arrayRefVar) {
//do_sth
}
不适用下标变量就可以顺序遍历整个数组,上述代码可以认为对arrayRefVar中的每个元素element执行do_sth操作。
当需要以其他顺序遍历数组或改变数组的值时,还是需要使用下标变量。
使用foreach避免了使用for循环可能会导致的两个错误:
- 下标从1开始
- 结尾是arrayRefVar.length
内存分析
Java虚拟机的内存划分
区域名称 | 作用 |
---|---|
虚拟机栈 | 用于存储正在执行的每个Java方法的局部变量表等。局部变量表存放了编译期可知长度 的各种基本数据类型、对象引用,方法执行完,自动释放。 |
堆内存 | 存储对象(包括数组对象),new来创建的,都存储在堆内存。 |
方法区 | 存储已被虚拟机加载的类信息、常量、(静态变量)、即时编译器编译后的代码等数据。 |
本地方法栈 | 当程序中调用了native的本地方法时,本地方法执行期间的内存区域 |
程序计数器 | 程序计数器是CPU中的寄存器,它包含每一个线程下一条要执行的指令的地址 |
数组名中记录的数组的首地址
数组的元素地址计算方式: 首地址 + 每一个元素的宽度 * 下标
- 以int[] 类型为例,假设首地址0x6666
- 取第1个元素:0x6666 + 4 * 0 = 0x6666
- 取第2个元素:0x6666 + 4 * 1 = 0x666a
一维数组在内存中的存储
public static void main(String[] args) {
int[] arr = new int[3];
System.out.println(arr);
}
两个一维数组内存图
- 两个数组变量,两个数组对象
public static void main(String[] args) {
int[] arr = new int[3];
int[] arr2 = new int[2];
System.out.println(arr);
System.out.println(arr2);
}
两个变量指向一个一维数组
- 两个数组变量,一个数组对象
public static void main(String[] args) {
// 定义数组,存储3个元素
int[] arr = new int[3];
//数组索引进行赋值
arr[0] = 5;
arr[1] = 6;
arr[2] = 7;
//输出3个索引上的元素值
System.out.println(arr[0]);
System.out.println(arr[1]);
System.out.println(arr[2]);
//定义数组变量arr2,将arr的地址赋值给arr2
int[] arr2 = arr;
arr2[1] = 9;
System.out.println(arr[1]);
}
一个数组变量先后指向不同数组对象
public static void main (String[] args) {
int[] a = {1, 2, 3};
a = new int[a.length];
a[0] = 100;
System.out.println(a[0]);
}
复制数组
上面已经了解了,形如list2 = list1
这种不是对数组的复制,复制数组的办法如下:
- 使用循环语句逐个复制
int[] arr1 = {1, 2, 3, 4, 5};
int[] arr2 = new int[arr1.length];
for (int i = 0;i < arr1.length;i ++) {
arr2[i] = arr1[i];
}
- 使用
System
类中的静态方法arraycopy
int[] arr1 = {1, 2, 3, 4, 5};
int[] arr2 = new int[arr1.length];
System.arraycopy(arr1, 0, arr2, 0, arr1.length); //arraycopy(arr1, pos1, arr2, pos2, length); pos1和pos2分别表示数组arr1和arr2中的起始位置,复制length个
arraycopy方法没有给目标数组分配空间,使用前必须创建目标数组并分配好空间。复制完成后,arr1和arr2具有相同的内容但位于独立的内存空间
- 使用clone方法复制数组
数组传递给方法
public class Main {
public static void main(String[] args) {
printArray(new int[]{1, 2, 3, 4, 5, 6});
}
public static void printArray(int[] arr) {
for(int i = 0;i < arr.length;i++) {
System.out.print(arr[i] + " ");
}
}
}
上述代码中new int[]{1,2,3,4,5,6}没有显式的引用变量,这样的数组称为匿名数组。语法为:new elementType[]{value1,value2,…};
Java使用按值传递的方式把实参传递给方法,传递基本数据类型变量和传递数组有很大不同:
- 传递基本数据类型参数,传递的是实参的
值
- 传递数组类型参数,参数值是数组的
引用
,即方法中的数组和传递的数组是同一个。如果改变方法中的数组,方法外数组也会改变。
public class Main {
public static void main(String[] args) {
int[] a = {1,2};
System.out.println(a[0] + " " + a[1]);
swap(a[0], a[1]);
System.out.println(a[0] + " " + a[1]);
swapFirstTwoInArray(a);
System.out.println(a[0] + " " + a[1]);
}
public static void swap(int x, int y) {
int temp = x;
x = y;
y = temp;
}
public static void swapFirstTwoInArray(int[] arr) {
int temp = arr[0];
arr[0] = arr[1];
arr[1] = temp;
}
}
/*
输出为:
1 2
1 2
2 1
*/
可见swap没能交换两个元素,但swapFirstTwoInArray方法实现了。因为swap方法的参数为基本数据类型,所以调用swap(a[0], a[1])时,a[0]和a[1]的值传给了方法内部的x和y。x和y的内存位置独立于a[0]和a[1]的内存位置。而swapFirstTwoInArray方法参数是一个数组,数组的引用传给方法。这样,方法外的变量a和方法内的arr都指向在同意内存位置的同一个数组。
方法返回数组
import java.util.Arrays;
public class Main {
public static void main(String[] args) {
int[] list1 = {1, 2, 3, 4, 5, 6};
int[] list2 = reverse(list1);
System.out.println(Arrays.toString(list1));
}
public static int[] reverse(int[] list) {
int[] result = new int[list.length];
for (int i = 0,j = result.length - 1;i < list.length;i ++,j --) {
result[j] = list[i];
}
return result;
}
}
可变长参数列表
可以将可变数量的相同类型的参数传递给方法,并将其视为数组,声明如下:
typeName... parameterName;
在方法声明中,指定类型后紧跟省略号(…)。只能在方法中指定一个可变长参数,同时该参数必须是最后一个参数。
public class Main {
public static void main(String[] args) {
printMax(34, 3, 3, 2, 56.6);
printMax(new double[]{1, 2, 3});
}
public static void printMax(double... numbers) {
//求数组最大值
if (numbers.length == 0) {
System.out.println("Empty array");
return;
}
double result = numbers[0];
for (int i = 1; i < numbers.length; i++) {
if (numbers[i] > result) {
result = numbers[i];
}
}
System.out.println(result);
}
}
Arrays类
java.util.Arrays类即为操作数组的工具类,包含了用来操作数组(比如排序和搜索)的各种方法。 比如:
数组元素拼接
- static String toString(int[] a) :字符串表示形式由数组的元素列表组成,括在方括号(“[]”)中。相邻元素用字符 ", "(逗号加空格)分隔。形式为:[元素1,元素2,元素3。。。]
- static String toString(Object[] a) :字符串表示形式由数组的元素列表组成,括在方括号(“[]”)中。相邻元素用字符 ", "(逗号加空格)分隔。元素将自动调用自己从Object继承的toString方法将对象转为字符串进行拼接,如果没有重写,则返回类型@hash值,如果重写则按重写返回的字符串进行拼接。
数组排序
- static void sort(int[] a) :将a数组按照从小到大进行排序
- static void sort(int[] a, int fromIndex, int toIndex) :将a数组的[fromIndex, toIndex)部分按照升序排列
- static void sort(Object[] a) :根据元素的自然顺序对指定对象数组按升序进行排序。
- static void sort(T[] a, Comparator<? super T> c) :根据指定比较器产生的顺序对指定对象数组进行排序。
数组元素的二分查找
- static int binarySearch(int[] a, int key) 、static int binarySearch(Object[] a, Object key) :要求数组有序,在数组中查找key是否存在,如果存在返回第一次找到的下标,不存在返回-(insectionIndex + 1)。
insectionIndex
是如果查找失败要插入该元素使得数组保持有序的位置
- static int binarySearch(int[] a, int key) 、static int binarySearch(Object[] a, Object key) :要求数组有序,在数组中查找key是否存在,如果存在返回第一次找到的下标,不存在返回-(insectionIndex + 1)。
数组的复制
- static int[] copyOf(int[] original, int newLength) :根据original原数组复制一个长度为newLength的新数组,并返回新数组
- static T[] copyOf(T[] original,int newLength):根据original原数组复制一个长度为newLength的新数组,并返回新数组
- static int[] copyOfRange(int[] original, int from, int to) :复制original原数组的[from,to)构成新数组,并返回新数组
- static T[] copyOfRange(T[] original,int from,int to):复制original原数组的[from,to)构成新数组,并返回新数组
比较两个数组是否相等
- static boolean equals(int[] a, int[] a2) :比较两个数组的长度、元素是否完全相同
- static boolean equals(Object[] a,Object[] a2):比较两个数组的长度、元素是否完全相同
填充数组
- static void fill(int[] a, int val) :用val值填充整个a数组
- static void fill(Object[] a,Object val):用val对象填充整个a数组
- static void fill(int[] a, int fromIndex, int toIndex, int val):将a数组[fromIndex,toIndex)部分填充为val值
- static void fill(Object[] a, int fromIndex, int toIndex, Object val) :将a数组[fromIndex,toIndex)部分填充为val对象
*命令行参数
二维数组
声明和初始化
声明
elementType[][] arrayRefVar;
静态初始化
int[][] arr = new int[][]{{3,8,2},{2,7},{9,0,1,6}};
定义一个名称为arr的二维数组,二维数组中有三个一维数组
- 每一个一维数组中具体元素也都已初始化
- 第一个一维数组 arr[0] = {3,8,2};
- 第二个一维数组 arr[1] = {2,7};
- 第三个一维数组 arr[2] = {9,0,1,6};
- 第三个一维数组的长度表示方式:arr[2].length;
- 注意特殊写法情况:int[] x,y[]; x是一维数组,y是二维数组。
例子1:
int[][] arr = {{1,2,3},{4,5,6},{7,8,9,10}};//声明与初始化必须在一句完成
int[][] arr = new int[][]{{1,2,3},{4,5,6},{7,8,9,10}};
int[][] arr;
arr = new int[][]{{1,2,3},{4,5,6},{7,8,9,10}};
arr = new int[3][3]{{1,2,3},{4,5,6},{7,8,9,10}};//错误,静态初始化右边new 数据类型[][]中不能写数字
例子2:
public class TwoDimensionalArrayInitialize {
public static void main(String[] args) {
//存储多组成绩
int[][] grades = {
{89,75,99,100},
{88,96,78,63,100,86},
{56,63,58},
{99,66,77,88}
};
//存储多组姓名
String[][] names = {
{"张三","李四", "王五", "赵六"},
{"刘备","关羽","张飞","诸葛亮","赵云","马超"},
{"曹丕","曹植","曹冲"},
{"孙权","周瑜","鲁肃","黄盖"}
};
}
}
动态初始化
如果二维数组的每一个数据,甚至是每一行的列数,需要后期单独确定,那么就只能使用动态初始化方式了。动态初始化方式分为两种格式:
格式1:规则二维表:每一行的列数是相同的
//(1)确定行数和列数
elementType[][] arrayRefVar = new elementType[m][n];
//其中,m:表示这个二维数组有多少个一维数组。或者说一共二维表有几行
//其中,n:表示每一个一维数组的元素有多少个。或者说每一行共有一个单元格
//此时创建完数组,行数、列数确定,而且元素也都有默认值
//(2)再为元素赋新值
二维数组名[行下标][列下标] = 值;
举例:
int[][] arr = new int[3][2];
-
定义了名称为arr的二维数组
-
二维数组中有3个一维数组
-
每一个一维数组中有2个元素
-
一维数组的名称分别为arr[0], arr[1], arr[2]
-
给第一个一维数组1脚标位赋值为78写法是:
arr[0][1] = 78;
格式2:不规则:每一行的列数不一样
//(1)先确定总行数
elementType[][] arrayRefVar = new elementType[num_rows][];
//此时只是确定了总行数,每一行里面现在是null
//(2)再确定每一行的列数,创建每一行的一维数组
arrayRefVar[行下标] = new elementType[该行的总列数];
//此时已经new完的行的元素就有默认值了,没有new的行还是null
//(3)再为元素赋值
arrayRefVar[行下标][列下标] = 值;
举例:
int[][] arr = new int[3][];
- 二维数组中有3个一维数组。
- 每个一维数组都是默认初始化值null (注意:区别于格式1)
- 可以对这个三个一维数组分别进行初始化:arr[0] = new int[3]; arr[1] = new int[1]; arr[2] = new int[2];
- 注:
int[][]arr = new int[][3];
//非法
获取长度
- 二维数组的长度/行数:二维数组名.length
- 二维数组的某一行:二维数组名[行下标],此时相当于获取其中一组数据。它本质上是一个一维数组。行下标的范围:[0, 二维数组名.length-1]。此时把二维数组看成一维数组的话,元素是行对象。
- 某一行的列数:二维数组名[行下标].length,因为二维数组的每一行是一个一维数组。
- 某一个元素:二维数组名[行下标][列下标],即先确定行/组,再确定列。
public class Test22TwoDimensionalArrayUse {
public static void main(String[] args){
//存储3个小组的学员的成绩,分开存储,使用二维数组。
/*
int[][] scores1;
int scores2[][];
int[] scores3[];*/
int[][] scores = {
{85,96,85,75},
{99,96,74,72,75},
{52,42,56,75}
};
System.out.println(scores);//[[I@15db9742
System.out.println("一共有" + scores.length +"组成绩.");
//[[:代表二维数组,I代表元素类型是int
System.out.println(scores[0]);//[I@6d06d69c
//[:代表一维数组,I代表元素类型是int
System.out.println(scores[1]);//[I@7852e922
System.out.println(scores[2]);//[I@4e25154f
//System.out.println(scores[3]);//ArrayIndexOutOfBoundsException: 3
System.out.println("第1组有" + scores[0].length +"个学员.");
System.out.println("第2组有" + scores[1].length +"个学员.");
System.out.println("第3组有" + scores[2].length +"个学员.");
System.out.println("第1组的每一个学员成绩如下:");
//第一行的元素
System.out.println(scores[0][0]);//85
System.out.println(scores[0][1]);//96
System.out.println(scores[0][2]);//85
System.out.println(scores[0][3]);//75
//System.out.println(scores[0][4]);//java.lang.ArrayIndexOutOfBoundsException: 4
}
}
遍历
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();
}
内存
多维数组
- 声明类似于二维数组,继续堆[]即可。