Head First Java学习:第九章-构造器和垃圾收集器
对象的前世今生
对象如何创建、存在何处以及如何让保存和抛弃更有效率。
会述及堆、栈、范围、构造器、超级构造器、空引用等。
1、内存的两个区域:堆和栈
堆(heap):对象的生存空间,又称为可垃圾回收的堆
栈(stack):方法调用和局部变量。
2、变量的生存空间
- 实例变量:声明在类中方法之外的地方,存在于所属的对象中,因此保存在堆中。
- 局部变量:局部变量和方法的参数都声明在方法中,是暂时的,生命周期仅限于方法被放在栈的这段时间(方法调用至执行完毕),保存在栈中。
3、方法在栈中的存放顺序
根据调用顺序依次放在栈顶,最先释放的在最上面。
4、对象的局部变量
非primitive变量只是保存对象的引用。
对象存在堆中,不论对象是否声明或创建,如果局部变量是个对该对象的引用,只有变量本身会放在栈上。
对象本身存在堆上。
5、对象的实例变量:存放于对象所属的堆空间中
需要多大的存放空间:
- 实例变量是 primitive主数据类型:
java会根据主数据类型的大小,在对象所属的堆空间为该实例变量留下空间。如int需要32位,long需要64位。
变量所需要的空间在对象中。
- 实例变量是 对象的引用
对象带有对象引用的变量:此时真正的问题是,是否要保留对象带有的所有对象的空间?--不带
举例1:
public class CellPhone{
// 有声明变量,未赋值;Antenna对象在堆上只会留下变量的空间
private Antenna ant;
}
举例2:
public class CellPhone{
// 引用变量被赋值一个新的对象,新的Antenna对象在堆上占有堆空间
private Antenna ant = new Antenna();
}
结论:引用和对象都在堆中。
6、构造函数
对象创建三部曲:声明引用变量,创建对象,连接对象和引用。
Duck myDuck = new Duck();
其中,创建对象是在调用 Duck的构造函数。
- 什么是构造函数:构造函数带有你在初始化对象时,会执行的程序代码。新建一个对象时就会被执行。
- 如果没有写构造函数,编译器会帮你写一个:public Duck(){}
- 构造函数的特点:无返回类型;与类同名
7、构造Duck
代码:
public class Duck{
public Duck(){
System.out.println("Quck!");
}
}
public class UseADuck {
public static void main(String[] args) {
Duck d = new Duck();
}
}
输出:Quck!
总结:构造函数让你有机会介入new的过程中。
8、新建Duck状态的初始化
代码:
public class Duck {
int size;
public Duck(){
System.out.println("Quack!");
}
public void setSize(int newSize){
size = newSize;
}
}
代码:
public class UseADuck {
public static void main(String[] args) {
Duck d = new Duck();
d.setSize(45);
}
}
执行结果:Quack!
总结:
大部分人都是使用构造函数来初始化对象的状态,即设置和给对象的实例变量赋值。在上面的代码中,可以使用setSize()来设定大小,但这会让Duck暂时处于没有大小数值的状态(实例变量没有默认值),且需要两行搞定。
问题:先构造对象再设置大小会很危险,万一忘记设置会出问题。
9、使用构造函数来初始化Duck的状态
代码:
public class Duck02 {
int size;
public Duck02(int duckSize){
System.out.println("Quack!");
// 把初始化的程序代码放到构造函数中,然后把构造函数设定成需要参数的
size = duckSize;
System.out.println("size is "+size);
}
}
public class UseADuck02 {
public static void main(String[] args) {
// 只需要一行就可以创建出新的Duck并且设定好大小
Duck02 d = new Duck02(43);
}
}
结果:
Quack!
size is 43
总结:给构造函数加参数,使用参数的值设定size的实例变量。只需要一行就可以创建出新的Duck并且设定好大小。
10、有参和无参构造方法
让用户创建对象的时候有选择。
代码:
/**
* 重载构造参数
*/
public class Duck03 {
int size;
// 无参构造方法
public Duck03(){
size = 27;
System.out.println("size is "+ size);
}
// 有参构造方法
public Duck03(int duckSize){
size = duckSize;
System.out.println("size is "+ size);
}
}
代码:
public class useADuck03 {
public static void main(String[] args) {
// 调用无参构造方法
Duck03 d1 = new Duck03();
System.out.println("-------------------");
// 调用有参构造方法
Duck03 d2 = new Duck03(45);
}
}
结果:
size is 27
-------------------
size is 45
11、编译器一定会帮你写出没有参数的构造函数吗?
- 完全没有设定构造函数:编译器帮你调用一个无参构造函数
- 写了有参构造函数:自己要写上无参构造函数,编译器不会调用
12、重载构造函数
代码:
/**
* 重载构造参数:
* 代表你有一个以上的构造函数且参数都不相同
* 不能有相同的参数类型和顺序
*/
public class Mushroom {
//要知道参数多大
public Mushroom(int size){}
// 不知道参数多大
public Mushroom(){}
// 知道是否有魔力,不知道多大
public Mushroom(boolean isMagic){}
// 知道是否有魔力,知道多大
public Mushroom(boolean isMagic,int size){}
// 和上面相同,但是参数顺序不同所以过关
public Mushroom(int size,boolean isMagic){}
}
实例变量的默认值:0/0.0/false;引用变量的默认值:null
13、父类、继承和构造函数的关系
1) 实例变量
继承下来的父类的实例变量也会保存在对象中。
创建某个对象时(new一个对象),对象会取得所有实例变量所需要的空间,包括继承下来的实例变量的空间。
2) 父类的构造函数
创建对象时,所有继承下来的构造函数都会执行。
执行new的指令,会启动构造函数连锁反应。
3) 构造函数链:
Hippo对象IS-A Animal,Animal IS-A Object。如果你要创建出Hippo,也得创建出 Animal 与 Object的部分。
所以构造函数在执行的时候,第一件事情时去执行它的父类的构造函数,这会连锁反应到Object这个类为止。
4) 调用过程举例:
代码:
public class Animal {
public Animal(){
System.out.println("Making an Anilmal");
}
}
public class Hippo extends Animal{
public Hippo(){
System.out.println("Making a Hippo");
}
}
public class TestHippo {
public static void main(String[] args) {
System.out.println("starting...");
Hippo h = new Hippo();
}
}
结果:
starting...
Making an Anilmal
Making a Hippo
说明:先调用父类的构造函数,再调用自身的构造函数。
执行过程如下:

5) 如何调用父类的构造函数:super()
唯一方法:super()
含义:调用其父类的无参构造器。
代码举例:
public class Animal {
public Animal(){
System.out.println("Making an Anilmal");
}
}
public class Hippo02 extends Animal{
int size;
public Hippo02(int newSize){
// 调用父类的构造函数
super();
size = newSize;
}
}
6) 如果没有调用super() 会发生什么?
编译器会帮我们加上super() 的调用
编译器有两种涉入构造函数的方法:
第一种:没有编写构造函数
编译器会加super()及构造函数。
public ClassName(){
super();
}
第二种:有构造函数但是没有调用super()
编译器会帮你对每个重载版本的构造函数,加上这种调用:super().
编译器帮忙加的一定是没有参数的版本,即使父类有多个重载版本,也只有无参数的版本会被调用到。
7)对super()的调用必须是构造函数的第一个语句。
8)有参数的父类构造函数:怎么传参?

实例变量name私有的,不能被继承。Hippo有getName()方法但是没有name实例变量,所以需要通过Animal维持name实例变量,然后从getName()来返回这个值。
代码:
public abstract class Animal02 {
// 每个Animal02都有名字
private String name;
// Hippo03 会继承这个getter
public String getName(){
return name;
}
// 有参数的构造函数,用来设定name
public Animal02(String theName){
name = theName;
}
}
public class Hippo03 extends Animal02{
public Hippo03(String name){
// 传给Animal的构造函数
super(name);
}
}
public class makeHippo {
public static void main(String[] args) {
Hippo03 h= new Hippo03("Buffy");
System.out.println(h.getName());
}
}
结果:
Buffy
总结:
通过super()来引用父类,所以要从这里把name值都传进去,让Animal把它存到私有的name 实例变量中。
13、this() 从构造函数调用另一个重载版的另一个构造函数
- 使用this() 来从某个构造函数调用同一个类的另外一个构造函数
- this()只能用在构造函数中,且必须是第一行语句
- super() 与 this() 不可兼得
- this() 中的参数,根据需要调用的构造方法决定
举例:
import java.awt.*;
public class Mini extends Car{
Color color;
public Mini(){// 无参数的构造函数以默认颜色调用真正的构造函数
this(Color.RED);
}
public Mini(Color c){// 真正的构造函数
super("Mini");
color = c;
// 初始化动作
}
public Mini(int size){// 有问题,不能同时调用super()和this()
this(Color.RED);
super(size);
}
}
14、对象会存活多久?
- 对象:生命周期看引用到它的“引用”。如果引用还活着,对象也会继续活着;如果引用死了,对象也会跟着“陪葬”
- 引用变量:
实例变量:寿命和对象相同,对象活着,实例变量也活着。
局部变量:只存活在声明该变量的方法中。
15、局部变量的生命期和作用域
life:只要变量的堆栈块还存在于堆栈中上,局部变量就算活着,活到方法执行完毕。
Scope:局部变量的范围只限于声明它的方法之内。
局部变量在堆栈中,状态保存;
局部变量所在方法在栈顶,才能被使用。
16、引用变量的生命期和作用域
1)变量的生命周期如何影响对象的生命周期
引用活着,对象活着。
当对象的最后一个引用消失,对象就会变成可回收的。
2)释放对象引用的三种方法

浙公网安备 33010602011771号