注:本篇内容参考了《Java编程思想(第四版)》和《Java语言程序设计》两本书书籍。
本人水平有限,文中如有错误或其它不妥之处,欢迎大家指正!
目录
7.1.6 在已排序的数组中查找binarySearch()
1. 数组的概念
在百度百科中,数组定义如下:
所谓数组,是有序的元素序列。若将有限个类型相同的变量的集合命名,那么这个名称为数组名。组成数组的各个变量称为数组的分量,也称为数组的元素,有时也称为下标变量。用于区分数组的各个元素的数字编号称为下标。数组是在程序设计中,为了处理方便, 把具有相同类型的若干元素按无序的形式组织起来的一种形式。这些无序排列的同类数据元素的集合称为数组。数组是用于储存多个相同类型数据的集合。
2. 数组的特点
数组有以下特点:
- 数组是相同数据类型的元素的集合;
- 数组中的各个元素的存储是有先后顺序的,它们在内在中按照先后顺序连续的存放在一起;
- 数组元素用整个数组的名字和它自己在数组中的顺序位置来表示,下标从0开始。如array[1]表示名称为array的数组的第二个元素。
一维数组是线性表,所以它有且仅有一个开始结点和结束结点;最多只有一个直接前趋和直接后继结点;各元素具有一对一的对应关系,元素之间是相连的。数组是顺序存储的,其数据是连续存储在一块存储空间中。
数组的插入和删除操作慢,因为是连续存储的,若往中间插入或删除一个元素,那么后面的元素需要进行移动,导致效率不高。但数组的查找速度快,因为数组的每个元素都有一个下标,查找时直接根据下标即可定位到。
3 一维数组
Java中的数组可以有一维数组、二维数组,还有其他多维数组。一维数组相对简单一些,明白了一维数组,再使用多维数组就不会觉得多难了。下面先描述一维数组的语法。
3.1 一维数组的声明
数组声明的格式如下:
Type arrayName[];
或者
Type[] arrayName;
其中Type表示数组的类型,arrayName表示数组的名称,[]是数组的符号。数组的声明有两种方式,但建议使用后面的一种。下面举例声明数组。
// 声明一个整数类型的数组
int[] intArr;
// 声明一个字符类型的数组
char charArr[];
数组在声明时无须指定数组元素的个数,也不会为数组元素分配内存空间。声明后的数组无法直接使用,必须经过初始化操作并为数组分配内存后才能使用。
3.2 数组的创建
包括数组在内,Java中的所有对象都是在运行时动态创建的,创建新对象的方法之一就是用关键字new显式的创建。在使用new创建数组时,指定数组的类型和数组元素的个数。数组创建的格式如下:
Type arrayName[] = new Type[size];
或者
Type[] arrayName = new Type[size];
其中,size表示数组的大小,值必须为0或正整数。例如:
String[] strArray = new String[10];
或者
String[] strArray;
strArray = new String[10];
3.3 数组的初始化
数组声明后必须经过初化才能引用,即数组的元素要被赋予初始值。创建数组后,若没有指定初始值,数组便被赋予默认值为初始值。比如数组的元素是基本类型int,默认的初始值为0;boolean类型的值默认为false;引用类型的默认为null。
要给数组指定初始值,可以在声明数组时赋予,也可以在数组的创建表达式中完成。语法格式如下:
// 声明式初始化数组
Type[] arrayName = {element1, element2, element3, element4, ...};
// 在数组的创建表达式中初始化
char charArr[] = new char[] {'a', 'b', 'c', 'd'};
例如下面的代码,就是声明时初始化数组的值。
// 声明一个整数类型的数组,在声明就赋予元素的值
int[] intArr = {1, 2, 3, 4, 5, 6};
3.4 数组的引用
声明并初始化数组后,就可以引用数组元素了。使用数组名和下标值来引用,格式如下。其中arrayName是数组名称,arrayIndex是数组元素的下标,下标必须是0或整型数字,下标从0开始,0对应数组的第一个元素,且要小于数组的长度。如果下标的值等于或大于数组长度,将会产生数组越界异常ArrayIndexOutOfBoundsException。
arrayName[arrayIndex]
例如
int[] intArr = {1, 2, 3, 4, 5, 6};
int a = intArr[0]; // a的值等于1,下标从0开始,0对应数组的第一个元素
下面举例来使用一维数组。
// 声明数组,并初始化数组元素的值
int[] intArr = {1, 2, 3, 4, 5, 6};
// 数组长度
int length = intArr.length;
System.out.println("数组长度:" + length);
for (int i = 0; i < length; i ++) {
System.out.println("数组第" + (i+1) + "个元素的值:" + intArr[i]);
}
打印内容如下:
每个数组都一个length成员变量,用来表示数组所包含的元素的个数,length只能是正整数或0。在数组创建之后,length就不能被改变。
数组的符号[],是原生的。没办法点击进去。而数组的方法clone()和成员变量length在IDEA这样的开发工具中也没办法进去查看。
String[] strArray = new String[10];
int len = strArray.length;
String[] newArray = strArray.clone();
4 二维数组
二维数组(Two-dimension Array)可视为一维数据的扩展,只不过须将二维转换为一维数组。如一个含有m*n个元素的二维数据A,m 代表行数, n 代表列数,请看下面的NO(2,3)的二维数组的立体示意图。
4.1 二维数组声明和初始化
二维数据声明和初始化的语法格式如下(使用Java语言):
数据类型[] [] 变量名称 = new 数据类型[第一维长度] [第二维长度];
4.2 二维数组区分
上图可以直观清楚的看到二维数组的内容。实际上计算机内存中是无法以矩阵方式存储的,必须以线性方式,视为一维数组的扩展来处理。通常按照不同的语言,又可区为分两种方式。
4.2.1 以行为主(Row - major)
例如在Java、C、C++、Pascal语言的数组存放方式。存储顺序为A(1,1),A(1,2),...,A(1,n),A(2,1),A(2,2),...,A(m-1,n),A(m,n)。假设 a 为数组A在内存中的起始地址,d 为单位空间,那么数组元素A(i,,j)与内存地址有下列关系:
Loc (A(i,j)) = a + n * (i - 1) * d + (j - 1) * d
4.2.2 以列为主(Column - major)
例如Fortran语言的数组存放方式。存储顺序为A(1,1),A(2,1),A(3,1),...,A(m,1),A(1,2),A(2,2),...,A(m,n)。假设a 为数组A在内存中的起始地址,d 为单位空间,那么数组元素A(i,,j)与内存地址有下列关系:
Loc (A(i,j)) = a + (i - 1) * d + (j - 1) * d * m
了解以上公式后,在此以下图为例进行说明。若声明数组A(1:2, 1:3),则表示法如下图中的右图:
上图中的右图是在内存中的实际排列方式。若A(3,3)在位置121,A(6,4)在位置159,则A(4,5)的位置为何(其中单位空间d = 1)?由Loc(A(3.3)) = 121, Loc(A(6,4)) = 159,可知数组A的分配是“以列为主”的方式。所以起始地址为 a,单位空间为1,则对数组A(1:m, 1:n)有如下推导:
=> a + (3 -1) * 1 + m * (3 - 1) * 1 = a + 2 * m + 2 = 121
=> a + (6 - 1) * 1 + (4 - 1) * m = a + 3 * m + 5 = 159
由上面的两公可得到: m = 35, a = 49,再计算Loc(A(4,5)) = 49 + (4 - 1) * 1 + (5 - 1) * 1 * 35 = 192。
上面计算数组元素地址的方法,都是以A(m,n)或写成A(1:m,1:n)的方式表示,这两种方式称为简单表示法,且 m 与 n 的起始值一定都是1,这里要介绍另一种“注标表示法”。也就是可以把数组A声明成A(:
,
:
),且对任意A(i,j),有
i
,
j
。此数组共有(
-
+ 1)行,(
-
+ 1)列。那么地址计算公式和上面以简单表示法,A(m, n)可视为A(1:m,1:n)。
假设 a 仍为起始地址,且 m = -
+ 1,n =
-
+ 1,则有下列公式:
- 以行为主(Row - Major):Loc(A(i,j)) = a + ((i -
+ 1) - 1) * n * d + ((j -
+ 1) - 1) * d
- 以列主为(Column - Major):Loc(A(i,j)) = a + ((j -
+ 1) - 1) * m * d + ((i -
+ 1) - 1) * d
// 声明
int myArray[][];
// 创建
myArray = new int[5][10];
int length = myArray.length;
System.out.println("二维数组的长度:" + length);
for (int i = 0; i < myArray.length; i ++) {
for (int j = 0; j < myArray[i].length; j ++) {
// 为元素赋值
myArray[i][j] = i * 10 + j;
// 打印元素的值
System.out.println(myArray[i][j]);
}
}
从上面的代码可以看出,二维数组需要有两个[],它的长度其实是第一个[]中的值,即长度会是第一个维度的大小。而要想知道第二个维度的长度,那直接用arrayName[index]即可,其实arrayName[index]是一个维数组,访问某个元素的值是arrayName[i][j]。总的来说,二维数组可以理解是由多个一维数组组合成而成的,也就是上面提到的一句话:多维数组的每一个元素为一个低维数组。可以发现这些跟一维数组是类似的,只是稍微复杂了一点。打印内容如下,结果是从0到49。
其实现过程如下图所示:
可以看出,二维数组就是从横向和纵向两个维度组合成的数组,可以认为它的两个维度是行和列,有点类似于数学中的矩阵。为了方便理解,可以将二维数组做一个表格,示意图如下。
5 三维数组
5.1 概述
三维数组(Three-dimension Array),基本上三维数组的表示法和二维数组一样,皆可视为一维数组的扩展,若数组为三维数组,则可以看作是一个立方体。三个维度分别是长宽高,也有人理解为行、列和页三种不同的维度,示意图如下。
声明和初始化的语句如下,其中Type表示数组类型,abc为正整数。
Type arrayName[][][] = new Type[a][b][c];
例如数组No[2][2][2]共有8个元素,可以使用立方体图形来表示,如下图中的左图:
基本上,三维数组若以线性的方式来处理,一样可分为“以行为主”和“以列为主”两种方式。若数组A声明为A(1:,1:
,1:
),表示 A为一个含有
*
*
个元素的三维数组。可以把A(i,j,k)元素想象成空间上的立方体。如下图中的右图。
5.2 三维排列方式
5.2.1 以行为主(Row-Major)
可以将数组A视为 个
*
的二维列阵,再将每个数组视为
个一维数组,每个二维数组可包含
个元素。另外每个元素有 d 个单位空间,且 a 为数组的起始地址:
在转换公式时,只要知道最终是要看看A(i,j,k)在一直线排列中是第几个,所以简单的得到以下地址计算公式:
Loc(A(i,j,k)) = a + (i - 1)
![]()
d + (j - 1)
d + (k - 1) d
若数组A声明为A(:
,
:
,
:
)模式,则:
5.2.2 以列为主(Column-Major)
可以将数组A视为 个
*
的二维列阵,再将每个数组视为
个一维数组,每个二维数组可包含
个元素。每个元素有 d 个单位空间,且 a 为数组的起始地址:
=> Loc(A(i,j,k)) = a +(k - 1)
d + (j - 1)
d + (i - 1) d,若数组声明为A(
:
,
:
,
:
)模式,则:
例如A(6, 4, 2)以行为主方式排列,若 a = 300,且 d = 1,求A(4, 4, 1)的地址。直接代入二维数组,以行为主的公式即可,Loc(A(4, 4, 1)) = 300 + (4 - 1) * 4 * 2 * 1 + (4 - 1) * 2 * 1 + (1 - 1) * 1 = 330。
6 多维数组
6.1 概述
有了一维、二维、三维数组,当然也可以有四维、五维或更多维的数组。但因为受限于计算机内存,通常程序设计语言中的数组声明都会有维数的限制。将三维以上的数组归纳为 n 维数组。在Java语言中声明初始化的方式如下:
数据类型[] [] ... [] 变量名称 = new 数据类型[第一维长度] [第二维长度] ... [第n维长度] ;
假设数组 A 声明为A(1:,1:
,1:
,...,1:
),则可将数组视为
个 n-1 维数组,每个 n-1 维数组中有
个 n-2 维数组,每个 n-2 维数组中有
个 n-3 维数组.....有
个一维数组,在每一个一维数组中有
个元素。数组数组可以看作是数组的数组,即多维数组的每一个元素为一个低维数组。
6.2 排列方式
若 a 为起始地址 (a = Loc (A (1, 1, 1,1, ..., 1))),d 为单位空间,则数组A元素中的内存分配公式有如下两种方式。
6.2.1 以行为主(Row-Major)
6.2.2 以列为主(Column-Major)
在四维数组A[1:4, 1:6, 1:5, 1:3]中,a = 200, d = 1,并已知以列为主方式进行排列的,求A[3, 1, 3, 1] 的地址。由于使用数组的简单表示法,不需要经过转换,直接代入计算公式即可。
Loc(A[3, 1, 3, 1]) = 200 + (1 - 1) * 5 * 6 * 4 * 1 + (3 - 1) * 6 * 4 * 1 + (1 - 1) * 4 * 1 + 3 - 1 = 250。
7 Java中的数组
7.1 数组工具类Arrays和System
Arrays是Java标准类库中的一个数组工具类,方法皆为static类型,它在java.util包下。提供了许多对于数组的处理方法,如排序、查找、复制、填充及比对等。它有6个基本方法:
- equals():比较两个数组是否相等(deepEquals()用于比较多维数组);
- fill():数组元素的填充;
- sort():对数组进行排序;
- binarySearch():在已经排序的数组中查找元素;
- toString():产生数组的String表示;
- hashCode():产生数组的散列码。
下面介绍下几种相对常用的方法。
7.1.1 数组填充fill()
它是只能用同一个值去填充数组中的各个位置,而对对象数组来说,就是复制同一个引用进行填充,同时所填充的位置也可以指定,如果指定就只填充指定位置的元素。fill()方法这里没有写参数,其实它是有很多种参数可以传的。如下图所示:
下面举例说明最简单的一种使用方法。代码如下:
// 声明数组,并初始化数组元素的值
int[] intArr = {1, 2, 3, 4, 5, 6};
// 数组长度
int length = intArr.length;
System.out.println("数组长度:" + length);
Arrays.fill(intArr, 8);
for (int i = 0; i < length; i ++) {
System.out.println("数组第" + (i+1) + "个元素的值:" + intArr[i]);
}
打印结果如下。可以看出这个一维数组中的元素都被替换成了8。
但如果换成了下面的代码,那就是只是第二个元素被替换成了8,其它元素不变。
Arrays.fill(intArr, 1, 2, 8);
7.1.2 数组的复制System.arraycopy()
这个方法是Java标准类库提供的方法System.arraycopy(),用它复制比用for循环复制要快很多。它针对所有类型做了重载。需要注意的是,此方法不会执行自动包装和自动拆包,所以两个数组必须要有相同的确切类型。
其中,5个参数的意思分别是:
- @param src:源数组,对象类型。
- @param srcPos:在源数组中的起始位置,整数类型;
- @param dest:目标数组,对象类型;
- @param destPos:在目标数据中的起始位置,整数类型;
- @param length:要复制的数组元素的数量,整数类型。
// 声明数组,并初始化数组元素的值
int[] intArr = {1, 2, 3, 4, 5, 6};
// 数组长度
int length = intArr.length;
System.out.println("数组长度:" + length);
int[] newArr = new int[6];
System.arraycopy(intArr, 1, newArr, 1, 4);
for (int i = 0; i < length; i ++) {
System.out.println("数组第" + (i+1) + "个元素的值:" + newArr[i]);
}
打印结果如下,可以看出,有四个元素被替换了。
7.1.3 数据的比较equals()
数组相等的条件是元素个数必须相等,并且对应位置的元素也要相等。可以通过对每一个元素使用equals()作比较来判断。对于基本类型,需要使用基本类型的包装器类的equals()方法。使用比较简单,下面看下源码:
public static boolean equals(int[] a, int[] a2) {
if (a==a2)
return true;
if (a==null || a2==null)
return false;
int length = a.length;
if (a2.length != length)
return false;
for (int i=0; i<length; i++)
if (a[i] != a2[i])
return false;
return true;
}
7.1.4 数组元素的比较
Java有两种方式来提供比较功能。第一种是实现java.lang.Comparable接口,使你的类具有“天生”的比较能力。此接口比较简单,只有compareTo()一个方法。它接收另一个Object为参数,若当前对象小于参数则返回负值,,若相等则返回0,若当前对象大于参数则返回正值。
另一种是自己编写一个类,实现Comparator接口(此类是一个函数式接口),实现时要指定所比较的类型,然后实现其compare()方法。方法有两个同样类型的参数,用这两个参数进行比较,若第一个大则返回正值,若相等则返回0,若第一个小则返回负值。
7.1.5 数组排序sort()
使用内置的排序方法,可以对任意的基本类型数组排序,也可以对任意的对象数组排序,只要该对象实现了Comparable接口或具有相关的Comparator。
// 声明数组,并初始化数组元素的值
int[] intArr = {1, 3, 2, 6, 5, 4};
// 数组长度
int length = intArr.length;
System.out.println("数组长度:" + length);
Arrays.sort(intArr);
for (int i = 0; i < length; i ++) {
System.out.println("数组第" + (i+1) + "个元素的值:" + intArr[i]);
}
打印结果如下。
7.1.6 在已排序的数组中查找binarySearch()
若数组已经排序好了,就可以使用Arrays.binarySearch()执行快速查找。若对没有排序的数组使用此方法,将产生不可预料的结果。如果找到了目标,方法返回值等于或大于0,其实返回的元素在数组中的位置,否则会返回负值。
int index = Arrays.binarySearch(intArr, 2);
8 Java中数组存储
上面提到了数组是顺序存储的,其数据是连续存储在一块存储空间中。下面具体说下数组在Java中的存储。
8.1 栈内存
在方法中定义的一些基本类型的变量和对象的引用变量都在方法的栈内存中分配,当在一段代码中定义一个变量时,Java就在栈内存中为这个变量分配内存空间。当超出变量的作用域时,Java会自动释放掉为该变量所分配的内存空间。
8.2 堆内存
堆内存用来存放由new关键字创建的对象数组。在堆中分配的内存,由JVM的自动垃圾回收器来管理。在堆中创建了一个数组或对象后,同时还在栈内存中定义了一个特殊的变量。让栈内存中的这个变量的取值等于数组或对象在堆内存中的首地址,栈中的这个变量就成了数组或对象的引用变量。引用变量实际上保存的是数组或对象在堆内存中的地址(也称为对象的句柄),以后就可以在程序中使用栈的引用变量来访问堆中的数组或对象。
数组中的所有元素都具有相同类型。数组中的元素存储在一个连续性的内存块中,并通过索引来访问。
8.3 它知识点
Java中数组与其他种类的容器之间的区别,主要体现在三方面:效率、类型和保存基本类型的能力。
在Java中,数组是一种效率最高的存储和随机访问对象引用序列的方式。数组就是一个简单的线性序列,这使得元素访问非常快。但这种速度所供出的代价就是数组对象的大小被固定。并且在其生命周期中不可改变。数组可以持有基本类型,而泛型之前的容器就不能。
在泛型之前,其它的容器类在处理对象时,都将它们视作没有任何具体类型。也就是说它们将这些对象都当作Java中所有类的根类Object处理。数组之所以优于泛型之前的容器,就是因为可以创建一个数组去持有某种具体类型。有了泛型,容器就可以指定并检查它们所持有对象的类型,并且有了自动包装机制,容器看起来还能持有基本类型。然后容器类也比数组具有更多的功能。
随着自动包装机制的出现,容器已经可以与数组几乎一样方便地使用基本类型了。数组硕果仅存的优点就是效率。然后,若要解决更一般的问题,那数组就可能会受到过多的限制,因此在有些情况下还是会选择容器。
无论使用哪种类型的数组,数组标识符其实只是一个引用,指向在堆中创建的一个真实对象,这个数组对象用以保存指向其他对象的引用。可以作为数组初始化语法的一部分隐式地创建此对象,或用new表达式的创建。只读成员length是数组对象的一部分(事实上,这是唯一一个可以访问的字段或方法),表示此数组对象可以存储多少元素。“[]”语法是访问数组对象的唯一方式。
对象数组和基本类型数组在使用上几乎是相同的,唯一的区别就是对象数组保存的是引用,基本类型数组直接保存基本类型的值。
如果一个程序只包含固定数量且生命周期都是已知的对象,那么这是一个非常简单的程序!
——摘自《Java编程思想(第四版)》第11章的开头