数组的简介
数组是一种数据结构,存储同一基本数据类型的数据、或具有相同父类/接口的引用的集合 。
基本类型数组的元素是同一基本数据类型,引用数组的元素可以是不同类/接口的引用,但这些类/接口必须继承同一个类/接口。
数组是一种线性表的结构,数组元素之间有相对次序,通过用一段连续的内存空间存储一组相同类型的数据、并用物理内存的连续性来表达元素之间的前后关系。
Java的数组有以下基本特点:
数据元素的访问是通过整数下标进行的(支持随机访问),数组元素之间是有序的(下标从0开始);
数组可以动态分配,数组变量的声明与数组的创建可以不在同一个位置进行;
数组一旦创建,就不能再改变其大小(创建时指定的大小必须是int型,不能是short、long),如果希望在动态扩展大小,那么应该使用另一种数据结构ArrayList;
数组可以是基本数据类型。可以是引用类型。如果是基本数据类型,则实际数值被存在一段连续的内存中。如果是引用类型,则连续内存存的只是一个个引用,实际对象被存在堆中。
数组本质上是一个对象,其直接父类是Object。而既然是对象,就应该有成员,如我们可以通过length成员来获取数组的长度(这与C++不同,在C++中只能通过sizeof)。此外,每种数组类型都实现了Cloneable接口、java.io.Serializable接口。
下面,我针对这些基本特点进行讨论。
一、关于数组下标
(一)按数组下标随机访问
访问数组的任一元素是通过一个整型下标的方式,这点与C++类似。
与C++不同的是,Java会在运行时(编译时不会)做边界检查:如果创建一个100个元素的数组,当试图访问0-99以外的下标时,程序就会抛出IndexOutOfBoundsException异常而终止执行。C++则可以通过指针运算,访问到不属于数组的内存中的数值,这很可能引发一些致命的问题。数组边界检查是Java相对于C++的好处之一。
#include <stdio.h> int main() { int i = 0; int arr[3] = {0}; for (; i<=3; i++) { arr[i] = 5; printf("hello world "); } return 0; }
上面的例子会打印4个hello world,而第四次执行时,尽管arr数组没有arr[3]这个元素,但是编译、运行时却没有报告异常。实际开发中,就有可能出现上述程序正常运行但是其中有个致命的bug的情况,只不过还没有爆发严重问题而已。
char[] charArray = new char[3]; System.out.println(charArray[-1]);
结果抛出异常:
Exception in thread "main" java.lang.ArrayIndexOutOfBoundsException: -1
在Java中,当访问超过数组范围的索引时,则抛出IndexOutOfBoundsException异常,有助于我们修改bug。
(二)随机访问特性的实现
数组之所以可以随机访问,有两个原因:连续的内存空间+相同类型的数据。
我们知道,计算机会给每个内存单元分配一个地址,然后计算机通过地址访问内存中的数据。当计算机要随机访问数组的某个元素时,会通过如下的寻址公式,计算出该元素的内存地址:
address of a[i] = base_address + i * data_type_size
这也就是数组的内存模型。可以看到,连续的内存空间让计算机知道要找下一个元素往哪里走;相同类型的数据让计算机知道,步长为多少算一个元素。这,就是数组可以随机访问的原因。
(三)为什么数组下标不是从1开始,而是从0开始?
数组下标从1开始不是更符合人类的思维吗?关于这个问题,有2个可能原因。
其一,历史原因。我们知道,很多高级语言都是模仿C语言设计的,而C语言的数组就是从0开始,所以这些语言如Java就保留了从0开始的习惯。当然,有些语言则不是,像MATLAB就是从1开始的。而Python则支持负数的下标,Python的下标可以理解成偏移,其正数下标即从首元素正向偏移多少个元素,其负数下标即从尾元素逆向偏移多少个元素。
其二,从数组的内存模型来看。若用a表示数组a的首地址,则a[0]的地址为偏移为0的位置(即首地址),a[k]的地址为偏移为k个data_type_size内存的位置。所以,我们计算a[k]的内存地址时是这么计算的:
address of a[k] = base_address + k * data_type_size
但如果下标是从1开始的话,则计算a[k]的内存地址是这么计算的:
address of a[k] = base_address + (k-1) * data_type_size
这么一来,每次随机访问数组元素就多了一次减法操作,对CPU而言就多了一次减法操作的指令。而数组作为非常基础的数据结构,经常用到随机访问的操作,所以效率方面应该尽可能做到极致,这大概就是下标从0开始的原因吧。
二、数组变量的声明&数组的创建&数组的初始化
(一)数组变量的声明
type[] array_name;
数组变量的声明方式如上所示,声明语句有部分:数组类型、数组名。其中type声明了数组的元素的类型,它可以是八种基本数据类型,也可以是Object、Collection、自定义类等引用类型。type决定了哪种类型数据可以存放在数组中,如果存放不适当的数据,在运行期间会出现异常。注意,这里只是声明了一个数组变量,只是告诉编译器可以用它来指向一个存放type类型数据的数组,但是此时还没有创建数组。
(二)数组的创建
array_name = new type[size];
数组的创建,即数组的实例化,指的是分配一段内存来组成一个数组。数组的创建方式如上,type指定了数组的内存单位,一个单位是一个元素,它同样可以是基本数据类型,也可以是引用类型。size指定了数组的元素个数或者说数组的长度。array_name将特定的引用指向数组,以后就可以通过array_name访问数组的内容。如果只保留等号右边部分,则创建了一个匿名数组,匿名数组可以在调用方法时作为参数传递给方法。
可以看到,得到一个数组是一个两步过程。首先,声明一个数组变量指向特定的数组类型;然后,创建一个数组实例并将数组变量指向该实例。因此,Java中的数组是动态分配的。
数组变量的声明&数组的创建可以写在一条语句上:
type[] array_name = new type[size];
(三)数组的初始化
当以上面的方式创建数组时,Java会默认对数组进行初始化,如果type是数值类型,则所有元素初始化为0;如果是布尔类型,则初始化为false;如果是char类型,则初始化为空格字符" ";如果是引用类型,则初始化为null。
此外,还有另外一种创建数组的方式,如下所示,这种方式允许我们在创建数组的时候,顺带着进行指定初始化。
int[] intArray = new int[] {1, 2, 3, 4, 5};
创建的数组的长度取决于大括号里面元素的个数。我们可以去掉右边的new int[ ],简写为:
int[] intArray = {1, 2, 3, 4, 5};
四、数组的常规操作
(一)数组的循环遍历
循环遍历,就是通过循环访问数组的每一个元素。数组有两种遍历方式,一种是传统的for循环,一种是for-each循环。
传统的for循环
// traverse the array using for loop int[] intArray = {1, 2, 3, 4, 5}; for (int i=0; i<intArray.length; i++) { System.out.print(intArray[i] +" "); }
//输出:1 2 3 4 5
for-each循环
int[] intArray = {1, 2, 3, 4, 5}; for (int element : intArray) { System.out.print(element +" "); }
//输出:1 2 3 4 5
for-each循环的语法格式为:
for (type var : array) { statements using var; }
这种方式是迭代array中的每一个元素,并将它们赋值给变量var,然后在内部的statements中应用这个var。
它等价于:
for (int i=0; i<array.length; i++) { type var = array[i]; statements using var; }
for-each特性是Java5引入的,它使用的依然还是for关键字,常常用于遍历数组或者一个集合类,如ArrayList。
for-each的好处是:可以依次处理数组中的每个元素,并且不必为下标的起始值、终止值操心,所以,自然就不会担心运行时抛出IndexOutOfBoundsException异常了。
不过,for-each也有不足之处:
无法在遍历的时候对元素进行更新,这是因为它主要将数组元素赋值给var变量,在那之后无论你对var进行何种操作,都与元素无关;
无法在遍历中使用下标、返回下标,不能返回某个指定元素的下标;
无法指定遍历的步长,只能一个个遍历完所有元素;
无法从后往前遍历,只能固定地从前往后。
下面举个例子。
// the shortage of for-each loop int[] intArray = {1, 2, 3, 4, 5}; for (int element : intArray) { element = 2 * element; } for (int element : intArray) { System.out.print(element +" "); }
//结果:1 2 3 4 5
如例子的结果所示,我们无法在for-each遍历中更新数组元素。
这里对什么时候使用for循环,什么时候使用for-each循环做个小结:
如果只是需要遍历所有元素或者做一些简单的处理,那么可以用for-each;
如果只是遍历某个范围不希望遍历所有元素、遍历时需要更新元素、循环内部需要用到下标、需要从后往前遍历,那么应该使用传统的for循环
(二)打印数组信息
我们可以使用Arrays的toString()方法打印数组内容。
import java.util.Arrays; public class ArrayTest { static class Student { public int id; public String name; public Student(int id, String name) { this.id = id; this.name = name; } @Override public String toString() { return this.id +" "+ this.name; } } public static void main(String[] args) { // print the info of array int[] intArray = {1, 2, 3, 4, 5}; System.out.println(Arrays.toString(intArray)); Student[] stus = new Student[3]; for (int i=0; i<stus.length; i++) { stus[i] = new Student(i+1, "stu_"+i); } System.out.println(Arrays.toString(stus)); } }
其结果如下:
如果数组是自定义类型,那么需要重写自定义类的toString()方法,否则打印的是各个元素的引用。
(三)数组的排序
可以使用Arrays的sort()方法对数组进行排序。
// sort the array short[] shortArray = {2, 3, 1, 7, 4, 5, 9, 8, 6}; Arrays.sort(shortArray); System.out.println(Arrays.toString(shortArray));
//结果:[1, 2, 3, 4, 5, 6, 7, 8, 9]
(四)数组元素的查找、更新
按值查找
按下表查找
更新
五、多维数组
Java中的多维数组有规则的,也有不规则的。其中规则的多维数组跟C++的多维数组一样,逻辑上是一个矩形,每一行拥有相同的列数;不规则的多维数组,则不同行可以有不同的列数,不规则的多维数组称为Jagged Arrays。
Java的多维数组本质上还是一维数组,只不过这个一维数组的元素是数组而已,这些数组元素可以有各自的length。
(一)规则多维数组的声明、创建、初始化、遍历
多维数组的声明和创建方式为:
int[][] intArray = new int[10][20]; int[][][] intArray = new int[10][20][30];
这种方式会将所有子数组的元素都默认初始化为0,强调一下,多维数组的元素是一个个数组,像上面的intArray二维数组中,intArray[1][1]不是它的元素,它的元素是10个子数组intArray[i],i=0,1,...,9,intArray[1][1]是子数组intArray[1]的第二个元素。
举个例子:
// multidimensional array int[][] intArray = new int[3][3]; for (int i=0; i<3; i++) { for (int j=0; j<3; j++) { intArray[i][j] = i * j; } } for (int i=0; i<3; i++) { for (int j=0; j<3; j++) { System.out.print(intArray[i][j] +" "); } System.out.println(); }
结果为:
(二)不规则多维数组的声明、创建、初始化、遍历
// multidimensional array //Declaring and Creating int[][] intArray = new int[2][]; intArray[0] = new int[3]; intArray[1] = new int[2]; //Initializing int count = 0; for (int i=0; i<intArray.length; i++) { for (int j=0; j<intArray[i].length; j++) { intArray[i][j] = ++count; } } //print info of array for (int i=0; i<intArray.length; i++) { for (int j=0; j<intArray[i].length; j++) { System.out.print(intArray[i][j] +" "); } System.out.println(); }
结果为:
当遍历不规则多维数组时,就不能使用固定的数字边界了,而应该使用数组的length属性。
(三)采用显式初始化的方式创建多维数组
// multidimensional array int[][] intArray = {{2, 7, 9}, {3, 6}, {7, 4, 2}}; for (int i=0; i<intArray.length; i++) { for (int j=0; j<intArray[i].length; j++) { System.out.print(intArray[i][j] +" "); } System.out.println(); } System.out.println("======="); for (int[] element : intArray) { for (int item : element) { System.out.print(item +" "); } System.out.println(); }
结果为:
六、数组的应用
(一)数组的拷贝
(1)一维数组的深拷贝
// copy array int[] intArray = {1, 2, 3}; int[] cloneArray = intArray.clone(); System.out.println(intArray == cloneArray); System.out.println("======="); System.out.println(Arrays.equals(intArray, cloneArray));
结果为:
可见利用数组的clone方法返回的是一个新建的相对于原数组独立的数组。因此,我们称为深拷贝。
另外,我们可以用Arrays的equals()方法判断两个数组的内容是否相同。
(2)多维数组的浅拷贝
// copy array int[][] intArray = {{1, 2, 3}, {4, 5}}; int[][] cloneArray = intArray.clone(); System.out.println(intArray == cloneArray); System.out.println(intArray[0] == cloneArray[0]); System.out.println(intArray[1] == cloneArray[1]); System.out.println("======="); System.out.println(Arrays.equals(intArray, cloneArray)); System.out.println(Arrays.deepEquals(intArray, cloneArray)); System.out.println("======="); int[][] intArray2 = {{1, 2, 3}, {4, 5}}; System.out.println(Arrays.equals(intArray, intArray2)); System.out.println(Arrays.deepEquals(intArray, intArray2));
结果为:
从结果中可以看出,尽管intArray和cloneArray是不相等的引用,但是它们的元素(子数组)却一一对应相等,所以说,多维数组的拷贝是浅拷贝。
由于intArray、cloneArray两个二维数组的元素是指向相同子数组,所以它们的元素是相同的引用值,这也是为什么Arrays.equals(intArray, cloneArray)返回的是true。而由于intArray2是不同于intArray的引用,所以,Arrays.equals(intArray, intArray2)返回的是false。
Arrays的deepEquals()方法可以用于比较多维数组的内容是否相同。尽管intArray2不等于intArray,但是它们的内容相同。
(二)传递数组给一个方法(作为方法的参数)
像普通变量一样,我们可以传递数组给方法,如下面的例子所示。
public class ArrayTest { public static void main(String[] args) { // pass an array to method int[] intArray = {3, 1, 2, 5, 4}; sum(intArray); } //get the sum of array values public static void sum(int[] arr) { int sum = 0; for (int i=0; i<arr.length; i++) { sum += arr[i]; } System.out.println("sum of array values : "+ sum); } }
结果为:
sum of array values : 15
(三)在方法中返回数组
同样也可以在方法中返回一个数组,如下面所示:
import java.util.Arrays; public class ArrayTest { public static void main(String[] args) { // return an array from method int[] intArray = createArray(); System.out.println(Arrays.toString(intArray)); } //create an array and return it to the caller public static int[] createArray() { return new int[] {1, 2, 3}; } }
结果为:
[1, 2, 3]
七、数组的本质
Java中数组本质上是一个对象。从前面也可以看到,数组有length属性,也有一些方法可以调用如clone()方法,所以说,数组具有对象的特点:封装了一些属性和方法,从这个角度来讲,数组就是对象。
下面,我们验证一下。
// an array is also an object int[] intArray = {1, 2, 3}; System.out.println(intArray instanceof int[]); System.out.println(intArray instanceof Object); System.out.println(intArray.getClass()); System.out.println(intArray.getClass().getSuperclass());
结果为:
可以看到,数组的确是对象,其直接父类是Object类。
再来看看不同类型数组的对应类
Object[] objArray = new Object[0]; String[] strArray = new String[0]; Integer[] integerArray = new Integer[0]; byte[] byteArray = new byte[0]; char[] charArray = new char[0]; long[] longArray = new long[0]; System.out.println(objArray.getClass()); System.out.println(strArray.getClass()); System.out.println(integerArray.getClass()); System.out.println(byteArray.getClass()); System.out.println(charArray.getClass()); System.out.println(longArray.getClass());
结果为:
从结果可以知道,对于引用类型,它对应的类为“[L”后面加上完整的类名。
Java数组的对象性与C++不同,C++中数组名只是一个指针,指向数组的首元素,没有属性也没有方法可以调用。当C++的方法调用数组时,除了传递数组名,还需要用sizeof传递数组长度,否则运行中很可能产生越界问题。相比之下,Java传递数组给方法就简单些,只需要传递数组名(数组的引用)即可,当方法内部需要获取数组的长度,直接调用数组的length属性就行了。
数组对象的成员有:
常数length,数组创建完就不再改变,可以是0、正整数;
继承自直接父类Object类的所有成员(除了clone()方法),数组中覆盖掉了Object的clone()方法;
公有的clone()方法,此方法是重写了Object的clone()方法。
注意数组的equals()方法只是简单地继承Object的该方法,所以比较的是两个对象的引用是否相同,等同于用==判断两个对象是否是同一个对象。
int[] intArray = {1, 2, 3}; int[] intArray2 = {1, 2, 3};
System.out.println(intArray == intArray2); System.out.println(intArray.equals(intArray2)); System.out.println(Arrays.equals(intArray, intArray2));
结果为:
false
false
true
总结:
1234