《疯狂Java:突破程序员基本功的16课》读书笔记-第一章 数组与内存控制

时间:2020-12-02 17:50:26

很早以前就听过李刚老师的疯狂java系列很不错,所以最近找一本拿来拜读,再此做下读书笔记,促进更好的消化。

使用Java数组之前必须先对数组对象进行初始化。当数组的所有元素都被分配了合适的内存空间,并指定了初始值时,数组初始化完成。程序以后将不能重新改变数组对象在内存中的位置和大小。

从用法角度来看,数组元素相当于普通变量,程序既可把数组元素的值赋给普通变量,也可把普通变量的值赋给数组元素。

1.1数组初始化

1.1.1 Java数组是静态的

Java语言是典型的静态语言,因此Java的数组是静态的,即当数组被初始化之后,该数组的长度是不可变的。

Java程序中的数组必须经过初始化才可使用。所谓初始化,就是为数组对象的元素分配内存空间,并为每个数组元素指定初始值。

数组的初始化有以下两种方式。

(1)静态初始化:初始化时由程序员显示指定每个数组元素的初始值,由系统决定数组长度。

(2)动态初始化:初始化时程序员只指定数组长度,由系统为数组元素分配初始值。

不管采用哪种方式初始化Java数组,一旦初始化完成,该数组的长度就不可改变,Java语言允许通过数组的length属性来访问数组的长度。示例如下:

package cn.zhouyu.array;

public class ArrayTest
{
public static void main(String[] args)
{
//采用静态初始化方式初始化第1个数组
String[] books = new String[]{"疯狂java","轻量级java ee企业应用实战","疯狂ajax","疯狂XML"}; //采用静态初始化的简化形式初始化第2个数组
String[] names = {"孙悟空","白骨精","猪八戒"}; //采用动态初始化的方式初始化第3个数组
String[] strArray = new String[5]; System.out.println("第1个数组的长度:" + books.length);
//输出4
System.out.println("第2个数组的长度:" + names.length);
//输出3
System.out.println("第3个数组的长度:" + strArray.length);
//输出5
}
}

books,names,strArray这3个变量以及各自引用的数组在内存中的分配图:

《疯狂Java:突破程序员基本功的16课》读书笔记-第一章 数组与内存控制

从图中可以看出,对于静态初始化方式而言,程序员无需指定数组长度,指定该数组的数组元素,有系统来决定该数组的长度即可。

例如books数组,为它指定了4个数组元素,那它的长度就是4;对于names数组,为它指定了3个元素,那它的长度就是3.

执行动态初始化时,程序员只需指定数组的长度,即为每个数组元素指定所需的内存空间,系统将负责为这些数组元素分配初始值。指定初始值时,系统将按照如下规则分配初始值。

整型(byte,short,int,long)是0

浮点型(float,double)是0.0

字符型(char)是'\u0000'

布尔型是false

引用类型(类,接口,数组)是null

不要同时使用静态初始化和动态初始化。也就是说,不要在进行数组初始化时,即指定数组的长度,也为每个数组元素分配初始值。

Java的数组变量是一种引用类型的变量,数组变量并不是数组本身,它只是指向堆内存中的数组对象。因此,可以改变一个数组变量所引用的数组,这样可以造成数组长度可变的假象。

//让books数组变量,stArr数组变量指向names所引用的数组

books = names;

strArray = names;

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

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

//改变books数组变量所引用的数组的第2个元素值。

books[1] = "白骨精";

System.out.println("names数组的第2个元素是:" + books[1]);

现在books数组变量,strArray数组变量都指向names数组变量所引用的数组,这样做的结果就是books,strArray,names这3个变量引用同一个数组对象。如图:

《疯狂Java:突破程序员基本功的16课》读书笔记-第一章 数组与内存控制

从图中可以看出,此时strArr,names,books数组变量实际上引用同一个数组对象。因此,当访问books数组,strArr数组的长度时,将看到输出3.这很容易造成一个假想,books的数组长度从4变成3。实际上,数组对象本身的长度并没有发生改变,变的是books数组变量。books数组变量原本指向堆内存下面的数组,当执行了books=names后,books数组将改为指向堆内存中间的数组,而原来books变量所引用的数组长度依然是4。

从图中可以看出,原来books变量所引用的数组长度依然是4,但不再有任何引用变量引用该数组,因此它将会变成垃圾,等着垃圾回收机制来回收。此时,程序使用books,names,strArr这3个变量时,将会访问同一个数组对象,隐藏把books数组的第2个元素赋值为“白骨精”时,names数组的第2个元素的值也会随之改变。

1.1.2 数组一定要初始化吗

在使用Java数组之前必须先初始化数组,也就是为数组元素分配内存空间,并指定初始值。实际上,如果真正掌握了Java数组在内存中分配机制,那么完全可以换一个方式来初始化数组,也就是说,数组无需经过初始化。

package cn.zhouyu.array;

public class ArrayTest3
{
public static void main(String[] args)
{
int[] nums = new int[]{3,5,20,12};
int[] prices;
prices = nums; for(int i=0;i<prices.length;i++)
{
System.out.println(prices[i]);
} prices[2] = 34;
System.out.println("nums数组的第3个元素是:" + nums[2]);
}
}

程序定义了prices数组之后,并未对prices数组进行初始化。当执行int[] prices之后,如图

《疯狂Java:突破程序员基本功的16课》读书笔记-第一章 数组与内存控制

从图中可以看出,此时的prices数组变量还未指向任何有效的内存,未指向任何数组对象,此时的程序还不可使用prices数组变量。

当程序执行prices=nums后,prices变量将指向nums变量所引用的数组,此时prices变量和nums变量引用同一个数组对象。执行这条语句之后,prices变量已经指向有效的内存及一个长度为4的数组对象,因此程序完全可以正常使用prices变量了。

常常说使用java数组之前必须先进行初始化,可是现在prices变量却无需初始化,这不是互相矛盾吗?其实一点都不矛盾,关键是大部分时候,我们把数组变量和数组对象搞混了,数组变量只是一个引用变量(有点类似于C里的指针),通常存放在栈内存中(也可被放入堆内存中),而数组对象就是保存在堆内存中的连续内存空间。对数组执行初始化,其实并不是对数组变量执行初始化,而是要对数组对象执行初始化,也就是为该数组对象分配一块连续的内存空间,这块连续内存空间的长度就是数组的长度。虽然上面程序中的prices变量看似没有经过初始化,但执行prices=nums就会让prices变量直接指向一个已经执行初始化的数组。

对于数组变量来说,它并不需要进行所谓的初始化,只要让数组变量指向一个有效的数组对象,程序即可正常使用该数组变量。

对于Java程序中所有的引用变量,它们都不需要经过所谓的初始化操作,需要进行初始化操作的是该引用变量所引用的对象。比如,数组变量不需要进行初始化操作,而数组对象本身需要进行初始化;对象的引用变量也不需要进行初始化,而对象本身才需要进行初始化。

1.1.3基本类型数组的初始化

对于基本类型数组而言,数组元素的值直接存储在对应的数组元素中,因此基本类型数组的初始化比较简单:程序直接先为数组分配内存空间,再将数组元素的值 对应内存里。

下面程序采用静态初始化的方式初始化了一个基本类型的数组对象。

public class PrimitiveArrayTest
{
public static void main(String[] args)
{
//定义一个int[]类型的数组变量
Int[] iArr;
//静态初始化数组,数组长度为4
iArr = new int[]{2,5,-12,20};
}
}

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

执行第一行代码int[] iArr;时,仅定义一个数组变量,此时内存中的存储如图1.4所示。

执行了int[] iArr;代码后,仅在main方法栈中定义了一个iArr数组变量,它是一个引用类型的变量,并未指向任何有效的内存,没有真正指向实际的数组对象。此时还不能使用该数组对象。

当执行iArr = new int[]{2,5,-12,20};静态初始化后,系统会根据程序员指定的数组元素来决定数组的长度。此时指定了4个数组元素,系统将创建一个长度为4的数组对象,一旦该数组对象创建成功,该数组的长度将不可改变,程序只能改变数组元素的值。此时内存中的存储如图1.5所示。

《疯狂Java:突破程序员基本功的16课》读书笔记-第一章 数组与内存控制

静态初始化完成后,iArr数组变量所引用的数组所占用的内存空间被固定下来,程序员只能改变各数组元素内的值,但不能移动该数组所占用的内存空间,既不能扩大该数组对象所占用的内存,也不能缩减该数组对象所占用的内存。

有些书籍中总是不断地重复:基本类型变量的值存储在栈内存中,其实这句话是完全错误的。例如图1.5中的2,5,-12,20,他们都是基本类型的值,但实际上它们却存储在堆内存中。实际上应该说:所有局部变量都是存放在栈内存里保存的,不管其是基本类型的变量,还是引用类型的变量,都是存储在各自的方法栈区中;但引用类型变量所引用的对象(包括数组,普通java对象)则总是存储在堆内存中。

对于Java语言而言,堆内存中的对象(不管是数组对象,还是普通的Java对象)通常不允许直接访问,为了访问堆内存中的对象,通常只能通过引用变量。这也是很容易混淆的地方。例如,iArr本质上只有main栈区的引用变量,但使用iArr.length,iArr[2]时,系统将会自动变为访问堆内存中的数组对象。

对于很多Java程序员而言,他们最容易混淆的是:引用类型变量何时只是栈内存中的变量本身,何时又变为引用实际的Java对象。其实规则很简单:引用变量本质上只是一个指针,只要程序通过引用变量访问属性,或者通过调用引用变量来调用方法,该引用变量将会由它所引用的对象代替。

1.1.4 引用类型数组的初始化

引用类型数组的数组元素依然是引用类型的,因此数组元素里存储的还是引用,它指向另一块内存,这块内存里存储了该引用变量所引用的对象(包括数组和Java对象)。

为了说明引用类型数组的运行过程,下面程序先定义了一个Person类,然后定义了一个Person[]数组,并动态初始化了该Person[]数组,再显式为数组的不同数组元素指定值。

class Person
{
//年龄
public int age;
//身高
public double height;
//定义一个info方法
public void info()
{
System.out.println("我的年龄是:" + age + ",我的身高是:" + height);
}
}
public class ReferenceArrayTest
{
public static void main(String[] args)
{
//定义一个students数组变量,其类型是Person[]
Person[] students;
//执行动态初始化
students = new Person[2];
System.out.println("students所引用的数组的长度是:" + students.length);
//创建一个Person实例,并将这个Person实例赋给zhang变量
Person zhang = new Person();
//为zhang所引用的Person对象的属性赋值
zhang.age = 15;
zhang.height = 158;
//创建一个Person实例,并将这个Person实例赋给lee变量
Person lee = new Person();
//为lee所引用的Person对象的属性赋值
lee.age = 16;
lee.height = 161;
//将zhang变量的值赋给第一个数组元素
students[0] = zhang;
//将lee变量的值赋给第二个数组元素
students[1] = lee;
//下面两行代码的结果完全一样,因为lee和students[1]指向的是同一个Person实例。
lee.info();
students[1].info();
}
}

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

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

《疯狂Java:突破程序员基本功的16课》读书笔记-第一章 数组与内存控制

上图中的栈内存中定义了一个students变量,它仅仅是一个空引用,并未指向任何有效的内存。直到执行初始化,本程序对students数组执行动态初始化。动态初始化由系统为数组元素分配默认的初始值null,即每个数组元素的值都是null。执行动态初始化后的存储如图:

《疯狂Java:突破程序员基本功的16课》读书笔记-第一章 数组与内存控制

从上图可以看出,students数组的2个数组元素都是引用,而这2个引用并未指向任何有效的内存,因此,每个数组元素的值都是null。此时,程序可以通过students来访问它所引用的数组的属性。

Students数组是引用类型的数组,因此students[0],students[1]两个数组元素相当于两个引用类型的变量。如果程序只是直接输出这两个引用类型的变量,那程序完全正常。但程序依然不能通过students[0],students[1]来调用属性或方法,因此它们还未指向任何有效的内存区,所以这两个连续的Person变量(students数组的数组元素)还不能被使用。

接着,程序定义了zhang和lee两个引用变量,并让它们指向堆内存中的两个Person对象,此时的zhang,lee两个引用变量存储在main方法栈区中,而两个Person对象则存储在堆内存中。此时的内存存储如图:

《疯狂Java:突破程序员基本功的16课》读书笔记-第一章 数组与内存控制

对于zhang,lee两个引用变量来说,它们可以指向任何有效的Person对象,而students[0],students[1]也可指向任何有效的Person对象。从本质上来看,zhang,lee,students[0],students[1]所能存储的内容完全相同。接着,程序执行students[0] = zhang;student[1] = lee;两行代码,也就是让zhang和students[0]指向同一个Person对象,让lee和students[1]指向同一个Person对象,此时的内存存储如图:

《疯狂Java:突破程序员基本功的16课》读书笔记-第一章 数组与内存控制

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

1.2使用数组

当数组引用变量指向一个有效的数组对象之后,程序就可以通过该数组引用变量来访问数组对象。Java语言不允许直接访问堆内存中的数据,因此无法直接访问堆内存中的数组对象,程序将通过数组引用变量来访问数组。

1.2.1 数组元素就是变量

只要在已有数据类型之后增加方括号,就会产生一个新的数组类型:例如

int -> int[],String -> String[],Person -> Person[]。

当程序需要多个类型相同的变量来保存程序状态时,可以考虑使用数组来保存这些变量。当一个数组初始化完成,就相当于定义了多个类型相同的变量。

无论哪种类型的数组,其数组元素其实想当于一个普通变量,把数组类型之后的方括号去掉后得到的类型就是该数组元素的类型。

当通过索引来使用数组元素时,将该数组元素当初普通变量使用即可,包括访问该数组元素的值,为数组元素赋值,等等。

1.2.2 没有多维数组

前面已经指出:只要在已有数据类型之后增加方括号,就会产生一个新的数组类型。如果已有的类型是int,增加后是int[]类型,这是一个数组类型,如果再增加就是int[][],这依然是数组类型,如果再增加就是int[][][],这依然是数组类型。反过来也是一样。

从上面分析可以看出,所谓多维数组,其实只是数组元素依然是数组的1维数组:2维数组是数组元素是1维数组的数组,3维数组是数组元素是2维数组的数组,4维数组是数组元素是3维数组的数组……N维数组是数组元素是N-1维数组的数组。

Java允许将多维数组当成1维数组处理。初始化多维数组时可以先只初始化最左边的维数,此时该数组的每一个元素都相当于一个数组引用变量,这些数组元素还需要进一步初始化。

public class TwoDimensionTest
{
public static void main(String[] args)
{
//定义一个二维数组
int[][] a;
//把a当成一维数组进行初始化,初始化a是一个长度为3的数组
//a数组的数组元素又是引用类型
a = new int[4][];
//把a数组当成一维数组,遍历a数组的每个数组元素
for (int i = 0; i < a.length ; i++ )
{
System.out.println(a[i]);
}
//初始化a数组的第一个元素
a[0] = new int[2];
//访问a数组的第一个元素所指数组的第二个元素
a[0][1] = 6;
//a数组的第一个元素是一个一维数组,遍历这个一维数组
for (int i = 0 ; i < a[0].length ; i ++ )
{
System.out.println(a[0][i]);
}
}
}