数组
作者:互联网
数组的简介
数组是一种数据结构,存储同一基本数据类型的数据、或具有相同父类/接口的引用的集合 。
基本类型数组的元素是同一基本数据类型,引用数组的元素可以是不同类/接口的引用,但这些类/接口必须继承同一个类/接口。
数组是一种线性表的结构,数组元素之间有相对次序,通过用一段连续的内存空间存储一组相同类型的数据、并用物理内存的连续性来表达元素之间的前后关系。
Java的数组有以下基本特点:
数据元素的访问是通过整数下标进行的,数组支持随机访问;
数组一旦创建,就不能再改变其大小,如果希望在动态扩展大小,那么应该使用另一种数据结构ArrayList;
数组本质上是一个对象,而既然是对象,就应该有成员。
下面,我针对这些基本特点进行讨论。
一、关于数组下标
(一)按数组下标随机访问
访问数组的任一元素是通过一个整型下标的方式,这点与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的情况,只不过还没有爆发严重问题而已。
(二)随机访问特性的实现
数组之所以可以随机访问,有两个原因:连续的内存空间+相同类型的数据。
我们知道,计算机会给每个内存单元分配一个地址,然后计算机通过地址访问内存中的数据。当计算机要随机访问数组的某个元素时,会通过如下的寻址公式,计算出该元素的内存地址:
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、自定义类等引用类型。
(二)数组的创建
array_name = new type[20];
数组的创建,即数组的实例化的方式如上,如果只保留右边部分,则创建了一个匿名数组,匿名数组可以在调用方法时作为参数传递给方法。type的类型同样可以是基本数据类型,也可以是引用类型。
数组变量的声明&数组的创建可以写在一条语句上:
type[] array_name = new type[20];
(三)数组的初始化
当以上面的方式创建数组时,Java会默认对数组进行初始化,如果type是数值类型,则所有元素初始化为0;如果是布尔类型,则初始化为false;如果是引用类型,则初始化为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的好处是:可以依次处理数组中的每个uansu,并且不必为下标的起始值、终止值操心,所以,自然就不会担心运行时抛出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,但是它们的内容相同。
(二)在方法中返回数组
(三)传递数组给一个方法(作为方法的参数)
标签:数组,int,元素,System,Arrays,intArray 来源: https://www.cnblogs.com/JeremyChan/p/11149872.html