P235-P256
《Head First Java》20200930读书笔记
栈与堆:生存空间
在Java中,程序员会在乎内存中的两种区域:对象的生存空间堆(heap)和方法调用及变量的生存空间(stack)。
当Java虚拟机启动时,它会从底层的操作系统取得一块内存,并以此区域来执行Java程序。
至于有多少内存,并以及你是否能够调整它都要看Java虚拟机与平台的版本而定。
我们知道所有的对象都存活于可垃圾回收的堆上,但我们还没看过变量的生存空间。
而变量存在于哪一个空间要看它是哪一种变量而定。
这里说的“哪一种”不是它的类型,而是实例变量或局部变量。
后者这种区域变量又被称为栈变量,该名称已经说明了它所存在的区域。
- 实例变量:实例变量是被声明在类,而不是方法里面。
它们代表每个独立对象的“字段”(每个实例都能有不同的值)。实例变量存在于所属的对象中。 - 局部变量:局部变量和方法的参数都是被声明在方法中。
它们是暂时的,且生命周期只限于方法被放在栈上的这段期间(也就是方法调用至执行完毕为止)。
栈上的对象引用:有关对象局部变量
非primitive的变量只是保存对象的引用,而不是对象本身。
不论对象是否声明或创建,如果局部变量是个对该对象的引用,只有变量本身会放在栈上。
对象只会存在堆上。
- 实例变量是声明在类中方法之外的地方。
- 局部变量声明在方法或方法的参数上。
- 所有局部变量都存在于栈上相对应的堆栈块中。
- 对象引用变量与primitive主数据类型变量都是放在栈上的。
- 不管是实例变量或局部变量,对象本身都会在堆上。
实例变量
如果局部变量生存在栈上,那么实例变量呢?
当你要新建一个CellPhone()时,Java必须在堆上帮CellPhone找一个位置。
这会需要多少空间呢?
足以存放该对象所有实例变量的空间。
没错,实例变量存在于对象所属的堆空间上。
记住对象的实例变量的值是存放于该对象中。
如果实例变量全都是primitive主数据类型的,则Java会依据primitive主数据类型的大小为该实例变量留下空间。
int需要32位,long需要64位,以此类推。
Java并不在乎私有变量的值,不管是32或32,000,000的int都会占用32位。
但若实例变量是个对象呢?
如果CellPhone对象带有一个Antenna对象呢?
也就是说CellPhone带有Antenna类型的引用变量呢?
当一个新建对象带有对象引用的变量时,此时真正的问题是:
是否需要保留对象带有的所有对象的空间?
不是这样的。
无论如何,Java会留下空间给实例变量的值。
但是引用变量的值并不是对象本身,所以若CellPhone带有Antanna,Java只会留下Antenna引用量而不是对象本身所用到的空间。
使用构造函数来初始化对象状态
构造函数让你有机会可以介入new的过程。
大部分的人都是使用构造函数来初始化对象的状态,也就是说,设置和给对象的实例变量赋值。
- 实例变量是保存在所属的对象,位于堆上。
- 如果实例变量是个对对象的引用,则引用与对象都是在堆上。
- 构造函数是个会在新建对象的时候执行程序代码。
- 构造必须与类同名且没有返回类型。
- 你可以用构造函数来初始被创建对象的状态。
- 如果你没有写构造函数,编译器会帮你安排一个没有参数。
- 一个类可以有很多个构造函数,但不能有相同的参数类型和顺序,这叫作重载过的构造函数。
- 默认的构造函数是没有参数的。
- 如果你写了构造函数,则编译器就不会调用。
父类的构造函数在对象的生命中所扮演的角色
在创建新对象时,所有继承下来的构造函数都会执行。
这代表每个父类都有一个构造函数(因为每个类至少都会有一个构造函数),且每个构造函数都会在子类对象创建时期执行。
执行new的指令是个重大事件,它会启动构造函数连锁反应。
还有,就算是抽象的类也有构造函数。
虽然你不能对抽象的类执行new操作,但抽象的类还是父类,因此它的构造函数会在具体的子类创建出实例时执行。
在构造函数中用super调用父类的构造函数的部分。
要记得子类可能会根据父类的状态来继承方法(也就是父类的实例变量)。
完整的对象需要也是完整的父类核心,所以这就是为什么父类构造函数必须执行的原因。
就算Animal上有些变量是Hippo不会用到的,但Hippo可能会用到某些继承下来的方法必须读取Animal的实例变量。
构造函数在执行的时候,第一件事是去执行它的父类的构造函数,这会连锁反应到Object这个类为止。
-
如何调用父类的构造函数?
-
调用父类构造的唯一方法是调用super()。
在你的构造函数中super()会把父类的构造函数放在堆栈的最上方。
父类的构造函数会调用它的父类构造函数。
这会一路上去直到Object的构造函数为止。
然后再一路执行,弹出回到原来的构造函数。 -
父类的部分必须在子类创建完成之前就必须完整地成型。
如果你把父类想象成子类的父母,那就可以看出来谁是先存在的。
记住,子类对象可能需要动用到从父类继承下来的东西,所以那些东西必须要先完成。
父类的构造函数必须在子类的构造函数之前结束。
你会发现Hippo的构造函数是第一个被调用的(在堆栈上的第一个),却也是最后一个完成的!
每个子类的构造函数会立即调用父类的构造函数,如此一路往上直到Object。
等到Object完成后会回去执行Animal的,然后等Animal完成后又回去执行Hippo剩下的构造函数。
对super()的调用必须是构造函数的第一个语句。
从某个构造函数调用重载版的另一个构造函数
- 使用this()来从某个构造函数调用同一个类的另外构造函数。
- this()只能用在构造函数中,且必须是第一行语句。
- super()与this()不能兼得。