这是一篇学习笔记。
本文会介绍对象如何创建、存在与何处、堆、栈、构造器等知识。
栈与堆:生存空间
- 栈:存放方法调用和局部变量。可以被垃圾回收。
- 堆:存放对象。
当 Java 虚拟机启动时,它会从底层的操作系统中取得一块内存,并以此区段来执行 Java 程序。内存空间大小有虚拟机来决定,不同的虚拟机空间大小可能会不同。
- 实例变量:声明在类中。有默认值。生命周期与对象一样,对象不死它不死。
- 局部变量:声明在方法中。无默认值,必须初始化。局部变量随着方法执行完毕会被销毁。
public class Test {
int a; // 实例变量
public void method() {
int b = 0; // 局部变量
}
}
方法会被堆在一起
当我们调用一个方法,这个方法会被放到栈的栈顶,方法调用完毕会被从栈顶弹出。假设栈中有两个方法 A 和 B,A 调用 B,则 B 会在 A 的上面,也就是 B 在栈顶且执行完毕先弹出去,再弹 A。
有关对象局部变量
非 primitive 的变量只保存对象的引用而不是对象本身。不论对象是否被声明或创建,如果局部变量是个对该对象的引用,则该变量存放在栈上。再说一次,对象本身只会存在堆中。
看一段代码就清楚了:
public class StackRef {
public void foof() {
barf();
}
public void barf() {
Duck d = new Duck(24);
}
}
要点:
- 对象引用变量和 primitive 主数据类型变量都是存在栈上
- 所有局部变量都存在栈上相对应的堆栈块中
- 对象本身存在堆上
实例变量存放的地方
我们知道,局部变量是存放在栈上的,那么实例变量呢?
实例变量存放在对象所属的堆空间上,也就是说,对象的实例变量的值是存放在该对象中的。
创建对象的奇迹
我们已经知道了变量和对象的生存空间,现在可以开始深入对象的创建。声明对象和赋值有三个步骤:
-
声明引用变量
Duck myDuck
-
创建对象
new Duck()
-
连接对象和引用
Duck myDuck = new Duck();
创建对象看起来很像在调用 Duck() 方法,但其实是在调用 Duck() 的构造函数。
构造函数通常伴随着 new 关键字出现,构造函数带有 new 的时候会执行的代码,换句话说,这段代码会在你初始化一个对象的时候执行。
我们没有在 Duck 里写构造函数,哪里来的呢?
其实就算你没有写,编译器会自动帮你加一个默认没有参数的构造函数:
public Duck() {
}
这跟方法很像,但**构造函数不是方法。**构造函数和方法的区别在于,前者没有返回类型,而后者有返回类型。
构造函数的一项关键特征是,它会在对象被赋值给引用之前就执行。也就是说你可以在对象被使用之前介入。比如说,你可以打印一段话:
public class Duck {
public Duck() {
System.out.println("芜湖~起飞~");
}
}
或者给对象的实例变量赋值:
public class Duck {
int size; // 实例变量
public Duck() {
size = 34;
}
}
这样在 new Duck() 的时候就会执行构造函数里的代码:
我们也可以在构造函数加上参数,用来初始化 Duck 的状态:
public class Duck {
int size;
public Duck(int duckSize) {
size = duckSize;
}
}
但是这样会带来一个问题:如果我不想设置 size 会怎么样?也就是说想使用无参数的构造函数。不过这里却只有一个构造函数,而且还是带参数的,所以我们必须再有一个构造函数,并且这个构造函数是无参的。如果一个类有一个以上的构造函数,这代表它们也是重载的。
也就是说,一个类必须要有无参构造函数,如果你设置了一个有参数的构造函数,那么必须将无参构造函数也写出来:
public class Duck {
int size;
public Duck() {} // 无参构造
public Duck(int duckSize) { // 有参构造
size = duckSize;
}
}
构造函数小回顾:
- 构造函数是在新建类时会执行的程序
- 构造函数必须与类的名字一样,且无返回类型
- 如果你没有写构造函数,编译器会帮你写一个没有参数的
- 一个类可以有很多个构造函数,但不能有相同参数类型和顺序,这叫重载过的构造函数
对象的生命周期
这部分涉及到 JVM 相关知识
参考
《Head First Java(第二版)》第九章:对象的前世今生