数组

数组的简介

数组是一种数据结构,存储同一基本数据类型的数据、或具有相同父类/接口的引用的集合 。

基本类型数组的元素是同一基本数据类型,引用数组的元素可以是不同类/接口的引用,但这些类/接口必须继承同一个类/接口。

数组是一种线性表的结构,数组元素之间有相对次序,通过用一段连续的内存空间存储一组相同类型的数据、并用物理内存的连续性来表达元素之间的前后关系。

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\n");
   } 
   
   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

 

posted @ 2019-07-08 22:52  JeremyChan  阅读(318)  评论(0编辑  收藏  举报