Java 数组知多少

                                          Java 数组知多少

 

        数组是编程语言中最常见的一种数据结构,可用于存储多个数据,每个数组元素存放一个数据,通常可通过数据元素的索引来访问数据元素,包括为数组元素赋值和取出数组元素的值。通俗的来说,就是数组是将一堆相同类型的数据有规律的排列组合在一起,通过数组这种数据结构,我们可以很容易地通过数组的索引来访问到我们需要的数据,也可以和轻松的为这堆数据赋值等操作。下面,我们一起来学习一下 Java 数组这一种特殊的数据结构。

 

1 . 数据类型

        Java 的数组要求所有的数组元素都具有相同的数据类型。因此,在一个数组中,数组元素的类型是唯一的,即一个数组里只能存储一种数据类型的数据,而不能存储多种数据类型的数据。一旦数组的初始化完成,数组在内存中的空间将被固定下来,因此数组的长度是不能改变的,这既是数组的一个特点,也是它的一个缺点。即是把数组中某个或多个数组元素删除,那也是数组的元素变少了,但数组在内存中的长度依然没变,所以说数组的长度一旦确定下来就不能改变了。

 

2 . 数组的定义

        数组是引用类型,也是一种数据类型。下面看看数组的两种定义方式:

  • type [] arrayName ;
  • type arrayName [] ;

        这两种语法格式都是正确的,但是就代码的可读性角度来分析,第一种格式明显是推荐使用的。首先,按照普通变量的定义习惯来看,arrayName 明显就是变量名,而 type [] 就是变量类型,只不过这是一种数组类型。

        其次,定义数组的时候不能指定数组的长度。因为数组是一种引用类型的变量,当使用数组来定义一个变量时,仅仅表示定义了一个引用变量(也就是定义了一个指针),这个引用还为指向任何有效的内存,即还没有内存空间来存储数组元素。因此还不能使用这个数组,除非对数据进行了初始化操作。

 

3 . 数据的初始化

        初始化就是为数组里的数组元素分配内存空间,并为每个数组元素赋初始值。所以数组必须先初始化,然后才能使用。数组的初始化有两种方式:

 

3 . 1 . 静态初始化

      初始化时显示指定每个数组元素的初始值,由系统决定数组的长度。

  • 静态初始化语法格式一:

         type [] arrayName;

         arrayName = new type [] { element1 , element2 , element3,……};

        上面语法中用 new 关键字来初始化数据,并用花括号把所有的数组元素括起来,多个数组元素之间用英文逗号( , )隔开。需要注意的是,在上面代码中,显示指定的数组元素值类型必须与 new 关键字后面的 type 类型相同,或者是其子类的实例。

  • 静态初始化语法格式二:

        type [] arrayName = { element1 , element2 , element3,……};

        上面语法中直接用花括号来定义一个数组,多个数组元素之间用英文逗号( , )隔开。需要注意的是,只有在定义数组的同时执行数组的初始化才可以使用这种简化方式。在开发过程中,更多的是使用这种方式来完成数组的初始化操作。

 

3 . 2 . 动态初始化

        初始化时只指定数组的长度,由系统为数组分配默认初始值。

  • 动态初始化语法格式:

        type [] arrayName ;

        arrayName = new type [length] ;

        上面语法中,需要指定一个 int 类型的 length 参数,这个参数指定了数据的长度,也就是可以容纳数组元素的个数。与静态初始化一样,此处的 type 必须与定义数组时使用的 type 类型相同,或者是定义数组时使用的 type 类型的子类。

        执行动态初始化时,只需要指定数组的长度就可以为每一个数组元素指定所需的内存空间了,随之系统将负责为这些数组元素分配默认初始值,各种数据类型的默认初始值如下:

  • 基本类型中的整数类型( byte 、short 、int 和 long ),则数组元素的值是 0 。
  • 基本类型中的浮点类型( float 、double ),则数组元素的值是 0.0 。
  • 基本类型中的字符类型( char ),则数组元素的值是 ' \u0000 '。
  • 基本类型中的布尔类型( boolean ),则数组元素的值是 false 。
  • 基本类型中的引用类型( 类、接口和数组 ),则数组元素的值是 null 。

       数组初始化完成后,就可以使用数组了,包括为数组元素赋值、访问数组元素值和获得数组的长度。

 

4 . 数组的使用

        数组最常规的用法就是访问数组里的元素了,包括对数组元素进行赋值和取出数组元素的值。访问数组元素是通过在数组引用变量后紧跟一个方括号( [] ),方括号里是数组的索引值,这样就可以很轻松的访问到数组元素了。访问到数组元素后就可以把数组元素当成一个普通的变量来使用了。

         所有数组(无论它们的元素是对象还是基本类型)都有一个固定的成员,可以通过它获知数组内包含了多少个元素,但不能对其修改,这个成员就是 length ,通过 typeName.length 可以获取到数据的长度。

        Java 语言的数组索引是从 0 开始的,也就是说第一个数组元素的索引值为 0 ,即 arrayName [0],最后一个数组元素的索引值为数组长度减 1( 即 typeName [typeName.length-1] )。但是,如果访问数据元素时,索引值小于 0 ,或者是大于数组的长度,编译程序不会出现任何问题,但是运行时会出现 java.lang.ArrayIndexOutOfBoundsException : N ( 数据索引越界异常,N 表示程序试图访问的数组的索引 ) 异常。

 

5 . 数组的遍历

        从 Java 5 之后,Java 提供了一种更简单的 foreach 循环来遍历数据,它无须获取数据的长度,无须根据索引来访问数组元素,下面是 foreach 的语法格式:

for ( type variableName : array )

{

   //variableName 自动迭代访问每个元素……

}

具体 foreach循环例子如下:

public class ArrayDemo {

public static void main(String[] args) {

int [] numbers={6,5,4,3,2,1};

for(int exampleNumber:numbers)

{

System.out.print(exampleNumber);

}

}

}

输出结果如下:

 

下面是使用 for 循环来遍历数组:

 

public class ArrayDemo {

public static void main(String[] args) {

int [] numbers={6,5,4,3,2,1};

for(int i=0;i<numbers.length;i++)

{

System.out.print(numbers[i]);

}

}

}

输出结果如下:

 

        从上面程序可以看出,使用 for 循环来遍历数据需要获取数组的长度,根据数组索引来遍历数组的元素,而使用 foreach 循环遍历数组元素时,无须获取数组长度,也无须根据索引来访问数组元素。foreach循环和普通循环不同的是,它无须循环条件,无须循环迭代语句,这些部分都由系统来完成,foreach 循环自动迭代数组的每个元素,当每个元素都被迭代一次后,foreach 循环自动结束。

        当使用 foreach循环来迭代输出数组元素时,通常不要对循环变量进行赋值,虽然这样赋值在语法上是允许的,但没有太大的实际意义,而且很容易引起错误。代码如下:

public class ArrayDemo {

public static void main(String[] args) {

String [] numbers={"6","5","4","3","2","1"};

for(String exampleNumber:numbers)

{

exampleNumber="java";

System.out.println(exampleNumber);

}

System.out.println(numbers[0]);

}

}

输出结果如下:

 

        上面程序由于在 foreach 循环中对数组元素进行赋值,结果导致不能正确遍历数组元素,不能正确地取出每个数组元素的值。而且程序最后再次访问第一个数组元素时,发现数组元素的值依旧没有改变。可以看出,当使用 foreach 来迭代访问数组元素时, foreach 中的循环变量相当于一个临时变量,系统会把数组元素依次赋给这个临时变量,而这个临时变量并不是数组元素,它只是保存了数组元素的值。因此,如果希望改变数组元素的值,则不能使用 foreach 循环。

        接下来,我们试试看,用 for 循环会不会上上面的结果一样呢?代码如下:

public class ArrayDemo {

public static void main(String[] args) {

String [] numbers={"6","5","4","3","2","1"};

for(int i=0;i<numbers.length;i++)

{

numbers[i]="java";

System.out.println(numbers[i]);

}

System.out.println(numbers[0]);

}

}

输出结果如下图:

        由上面的程序可以看到,用 for 循环成功的给数组的元素赋值了,并且在程序的最后尝试着访问数组的第一个元素时,输出的是数组改变后的值,这说明使用 for 循环可以对数组元素进行赋值,也能正确地取出数组元素的值,因此,如果只是单纯的对数组进行遍历的话,可以使用 foreach 循环或者是 for 循环,但如果想给数组元素赋值,并使用赋值后的数组,则需要使用 for 循环。

 

6 . 数组的进阶

        学习完上面的知识,恭喜你,你可以很好地使用数组了。但是要想深入的了解数组,我们还需修炼最后一关,那便是数组在内存中运行机制。数组是一种引用数据类型,数组引用变量只是一个引用,数组元素和数组变量在内存中是分开存放的。下面,我们继续完成最后的修炼。

6 . 1 内存中的数组 

        数组引用变量知识一个引用,这个引用变量可以指向任何有效的内存,只有当该引用变量指向有效内存后,才可通过该数组变量来访问到数组元素。

        与所有引用变量相同的是,引用变量是访问真实对象的根本方式。也就是说,如果希望在程序中访问数组对象本身,则只能通过这个数组的引用变量来访问。

       实际的数据对象被存储在堆( heap )内存中,如果引用该数组对象的数组引用比昂亮是一个局部变量,那么它被存储在栈( stack )内存中,数组在内存中的存储示意图如下:

        如果需要访问上图堆内存中的数组元素,则程序中只能通过 p [index] 的形式实现。也就是说,数组引用变量是访问堆内存中数组元素的根本方式。 

        如果堆内存中数组不再有任何引用变量指向自己,则这个数组将变成垃圾,该数组所占的内存将会被系统的垃圾回收机制回收。因此,为了让垃圾回收机制回收一个数组所占的内存空间,可以将该数组变量赋值为 null ,也就切断了数组引用变量和实际数组之间的引用关系。实际的数组也就成为了垃圾。

        只要类型相互兼容,就可以让一个数组变量指向另一个实际的数组,这种操作会让人超生数组的长度可变的错觉。下面通过一个例子来说明:

public class ArrayDemo {

public static void main(String[] args) {

int [] p={5,7,20,2,3};

int [] a=new int[4];

System.out.println("a数组的长度为:"+a.length);

for(int i=0;i<p.length;i++)

{

System.out.println("p["+i+"]="+p[i]);

}

for(int i=0;i<a.length;i++)

{

System.out.println("a["+i+"]="+a[i]);

}

a=p;

System.out.println("a数组的长度为:"+a.length);

}

}

输出结果如下:

        从上面的运行结果可以看到程序先输出 a 数组的长度是4,然后依次输出 p 数组和  a 数组的每个数组元素,接着会输出 a 数组的长度为 5 。看起来似乎数组的长度是可变的,但这只是一个假象。必须牢记:定义并初始化一个数组后,在内存中分配了两个空间,一个用于存放数组的引用变量,另一个用于存放数组本身。下面将结合示意图来详细说明程序的运行过程。

     (1)当程序定义并初始化了 p 、a 两个数组后,系统内存中实际上产生了4块内存区,其中栈内存中两个引用变量:p 和 a ;堆内存中也有两块内存区,分别用于存储 p 和 a 引用所指向的数据本身。此时计算机内存的存储示意图如下:

     (2)从上图可以非常清楚地看出 p 引用和 a 引用个字所引用的数组对象,并可以很清楚地看出 p 变量引用的数组长度是 5 ,a 变量所引用的数组长度为 4 。   

     (3)当执行上面程序的 a=p 时,系统将会把 p 的值赋给 a,p 和 a 都是引用类型变量,存储的是地址,因此把 p 的值赋给 a 后,就是让 a 指向 p 所指向的地址,此时计算机内存的存储示意图如下:

     (4)从上图可以看出,当执行了 a=p; 之后,堆内存中的第一个数组就有了2 个引用,即 p 变量和 a 变量都引用了第一个数组。此时第二个数组失去了引用,变成垃圾,只有等待垃圾回收机制来回收它——但它在内存中的长度始终不会改变,直到它被回收彻底消失。

 

6 . 2 基本类型数组的初始化

        对于基本类型数组而言,数据元素的值直接存储在对应的数组元素中,因此,初始化数组时,先为该数组分配内存空间,然后直接将数组的值存入对应数组中。

       下面程序定义了一个 int [ ] 类型的数组变量,采用动态初始化的方式初始化了该数组,并显示为每个数组元素赋值。

public class ArrayDemo {

public static void main(String[] args) {

int [] number ;

number=new int [5] ;

for(int i=0;i<number.length;i++)

{

number[i]=i+10 ;

}

}

}

        上面代码的执行过程代表了基本类型数组初始化的典型过程。下面将结合示意图详细介绍这段代码的执行过程。

     (1)执行第一行代码 int [] number ;时,仅定义一个数组变量,此时内存中的存储示意图如下:

     (2)执行了int [] number ; 代码后,仅在栈内存中定义了一个空引用(就是 number 数组变量),这个引用并未指向任何有效的内存,当然无法指定数组的长度。

    (3)当执行 number=new int [5] ; 动态初始化后,系统将负责为该数组分配内存空间,并分配默认的初始化值:所有数组元素都被赋为值 0 ,此时内存中的存储示意图如下:

    (4)此时 number 数组的每个数组元素的值都是 0,当循环为该数组的每个数组元素依次赋值后,此时每个数组元素的值都变成程序显示指定的值。显示指定每个数组元素值后的存储示意图如下:

    (5)从上如可以看到基本类型数组的存储示意图,每个数组元素的值直接存储在对应的内存中,操作基本类型数组的数组元素时,实际上相当于操作基本类型的变量。

 

6 . 3 引用类型数组的初始化

        引用类型数据的数组元素是引用,因此情况变得更加复杂。每个数组元素里存储的还是引用,它指向另一块内存,这块内存里存储了有效数据。

       为了更好地说明引用类型数组的运行过程,下面先定义一个 Person 类(所有类型都是引用类型),代码如下:

上面代码的执行过程代表了引用类型数组初始化的典型过程。下面将结合示意图详细介绍这段代码的执行过程。

    (1)执行 Person [ ] p ;代码时,这行代码仅仅在栈内中定义了一个引用变量,也就是一个指针,这个指针并未指向任何有效的内存区。此时内存中存储示意图如下:

     (2)在上图所示的栈内存中定义了一个 p 变量,它仅仅是一个引用,并未指向任何有效的内存。知道执行初始化,本程序堆 p 数组执行动态初始化,动态初始化由系统为数组元素分配默认的初始值 null ,即每个数组元素的值都是 null 。执行动态初始化后的示意图如下:

     (3)从上如可以看出,p 数组的两个数组元素都是引用,而且这歌引用并未指向任何有效的内存,因此每个数组元素的值都是 null ,这意味着依然不能直接使用 p 数组元素,因为每个数组元素都是 null ,这相当于定义了两个连续的 Person 变量( p 数组的数组元素 )还不能使用。

     (4)接着的代码定义了 p1 和 p2 两个 Person 实例,定义这两个实例实际上分配了 4 块内存,在栈内存中存储了 p1 和 p2 两个引用变量,还在堆内存中存储了两个 Person 实例。此时的内存存储示意图如下:

     (5)此时 Person 数组的两个数组元素依旧是 null ,直到程序依次将 p1 赋给 p 数组的第一个元素,把 p2 赋给 p 数组的第二个元素,p 数组的两个数组元素将会指向有效的内存区。此时的内存存储示意图如下:

     (6)从上图可以看出,此时 p1 和 p[0] 指向同一个内存区,而且它们都是引用类型变量,因此通过 p1 和 p[0] 来访问 Person 实例的实例变量和方法的效果完全一样,不论修改 p[0] 所指向的 Person 实例的实例变量,还是修改 p1 变量指向的 Person 实例的实例变量,所修改的其实是同一个内存区,所以必然相互影响。同理,p2 和 p[1] 也是引用同一个 Person 对象,也具有相同的效果。

 

7 . 多维数据

        从前面定义数组类型的语法:type [] arrName ,这是典型的意味数组的定义语法,其中 type 是数组元素的类型。从这个角度分析,如果希望数组元素也是一个引用,而且是指向 int 数组的引用,则可以把 type 具体成 int [] (前面已经指出,int [] 类型的用法与普通类型并无任何区别),那么上面定义数组的语法就是 int [] [] arrName 。

        如果把 int 这个类型扩大到 Java 的所有类型(不包括数组类型),则出现了定义二维数据的语法:

type [] [] arrName ;

        Java 语言采用上面的语法格式来定义二维数组,但它的实质还是以为数组,只是其数组元素也是引用,数组元素里保存的引用指向以为数组。

        接着对二维数组执行初始化操作,同样可以把这个数组当成一维数组来初始化,把这个“二维数组”当成一个一维数组,其元素的类型是 type [] 类型,则可以采用如下语法进行初始化:

arrName=new type [length] [] ;

        上面的初始化语法相当于一个一维数组,这个一维数组的长度是 length 。同样,因为这个一维数组的数组元素是引用类型(数组类型)的,所以系统为每个数组元素都分配初始值:null。

        这个二维数组实际上完全可以当成一维数组使用:使用 new type [length] 初始化一维数组后,相当于定义了 length 个 type 类型的变量;类似的,使用 new type [length] [] 初始化这个数组后,相当于定义了length 个 type [] 类型的变量,当然,这些 type [] 类型的变量都是数据类型,因此必须再次初始化这些数组。

        下面通过一个例子来详细说明二维数组,代码如下:

public class ArrayDemo {

public static void main(String[] args) {

//定义一个二维数组

int [][] arrName;

//把 arrName当成一维数组进行初始化,初始化arrName是一个长度为4的数组

// arrName数组的数组元素又是引用类型

arrName=new int[4][];

//把 arrName 数组当成一维数组,遍历arrName数组的每一个数组元素

for(int i=0;i<arrName.length;i++)

{

System.out.println(arrName[i]);

}

//初始化arrName数组的第一个元素

arrName[0]=new int[2];

//访问 arrName数组的第一个元素所指向的第二个元素

arrName[0][1]=6;

//arrName数组的第一个元素是一个一维数组,遍历这个一维数组

for(int i=0;i<arrName[0].length;i++)

{

System.out.println(arrName[0][i]);

}

}

}

输出结果如下:

 

        上面程序中代码先是把 arrName 这个二维数组当成一维数组处理,只是每个数组元素都是 null ,所以看到输出结果都是 null 。下面我们结合示意图来说明这个程序的执行过程。

     (1)程序的第一行 int [] [] arrName ;将在栈内存中定义一个引用变量,这个变量并未指向任何有效的内存空间,此时的堆内存中还未为这行代码分配任何存储区。

     (2)程序对 arrName 数组执行初始化,:arrName=new int [] [] ;这行代码让 arrName 变量指向一块长度为 4 的数组内存,这个长度为 4 的数组里每个数组元素都是引用类型(数组类型),系统为这些数组分配默认的初始值:null 。此时 arrName 数组在内存中的存储示意图如下:

     (3)从上图来看,虽然声明 arrName 是一个二维数组,但这里丝毫看不出它是一个二维数组的样子,完全是一维数组的样子。这个一维数组的长度是 4 ,只是这 4 个数组元素都是引用类型,它们的默认值是 null 。所以程序中可以把 arrName 数组当成一维数组处理,一次遍历 arrName 数组的每个元素,将看到每个数组元素的值都是 null 。

     (4)由于 arrName 数组的元素必须是 int [] 数组,所以接下来的程序对 arrName [0] 元素执行初始化,也就是让上图右边堆内存中的第一个数组元素指向一个有效的数组内存,指向一个长度为 2 的 int 数组。因为程序采用动态初始化 arrName [0] 数组,因此系统将为 arrName [0] 所引用数组的每个元素分配默认的初始值:0 ,然后程序显示为 arrName [0] 的第二个元素赋值为 6 。此时在内存中的示意图如下:

     (5)上图中灰色覆盖的数组元素都是程序显示指定的数组元素值,程序输出 arrName [0] 数组的每个数组元素,将看到输出 0 和 6 。

从上面程序中可以看出,初始化多维数组时,可以只指定最左边维的大小;当然也可以一次指定每一维的大小,如下:

int [] [] arrName = new int [3] [4] ; //同时初始化二维数组的两个维数

        上面代码将定义一个 arrName 数组变量,这个数组变量指向一个长度为 3 的数组,这个数组的每个数组元素又是一个数组类型,它们各指向对象的长度为 4 的 int [] 数组,每个数组元素的值为 0 。上面代码执行后在内存中的存储示意图如下:

       还可以使用静态初始化方式来初始化二维数组。使用静态初始化来初始化二维数组时,二维数组的每个数组元素都是一维数组,因此必须指定多个一维数组作为二维数组的初始化值,如下:

 

//使用静态初始化语法来初始化一个二维数组

String [] [] str1 = new String [] [] { new  String [3] , new String [] { " hello" } } ;

//使用简化的静态初始化语法来初始化二维数组

String [] [] str2 = new String [] [] { new String [3] , new String [] { " hello" }} ;

       上面代码执行后内存中的存储示意图如下:

 

        通过上面讲解可以得出一个结论:二维数组是一维数组,其数组元素是一维数组;三维数组也是一维数组,其数组元素是二维数组……从这个角度来看,Java 语言里没有多维数组。

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值