第六章 初始化和清理
- 两个安全性问题:初始化和清理。
- C 语言中很多的 bug 都是因为程序员忘记初始化导致的。
- 忘记清理会造成资源滞留不会被回收,占用内存。
- 解决方法:
- Java 采用了构造器来初始化
- 另外还使用了垃圾收集器(Garbage Collector, GC)去自动回收不再被使用的对象所占的资源。
6.1 利用构造器初始化
- Java 会自动调用对象的构造器方法,从而保证初始化。
- 构造器名称和类名一样。
- 如果不手写构造器,默认有一个无参构造器。如果自己写了构造器,那么构造器只有你所写的。
- 构造器没有返回值。
package chapter06;
public class Rock {
Rock(){
System.out.println("无参构造器");
}
Rock(String str){
System.out.println("有参构造器"+str);
}
public static void main(String[] args) {
new Rock();
new Rock("hello world");
}
}
- 当创建一个对象时:new Rock() ,内存被分配,构造器被调用。构造器保证了对象在你使用它之前进行了正确的初始化。
6.2 方法重载
- 一个类中多个方法的方法名相同,但参数列表不同,形成重载。
package chapter06;
public class Dog {
public Dog(){
System.out.println("狗");
}
//构造器重载
public Dog(String str){
System.out.println("狗"+str);
}
public void eat(){
System.out.println("木阿木啊");
}
//方法重载
public void eat(String str){
System.out.println("木阿木啊"+str);
}
}
6.2.1 区分重载方法
- 有一条简单的规则:每个被重载的方法必须有独一无二的参数列表
- 参数列表:参数种类、参数个数、参数顺序,都可区分重载的方法。
package chapter06;
public class Dog {
//形参个数都是一个,但类型不同构成重载
public void eat(long i){ }
public void eat(String str){ }
//数据类型相同,但形参的个数不同,构成重载
public void eat(int i){
}
public void eat(int i,int j){
}
//调换两个形参的书序,也形成了重载
public void eat(int i,String str){ }
public void eat(String str,int i){ }
}
6.2.2 重载与基本类型
package chapter06;
public class Dog1 {
// void m(byte i){}
// void m(short i){}
// void m(int i){}
void m(long i){
System.out.println("long"+i);
}
void m(float i){
System.out.println("float"+i);
}
void m(double i){
System.out.println("double"+i);
}
// void m(char i){}
void m(boolean i){
System.out.println("boolean"+i);
}
public static void main(String[] args){
//低转高
//当我们调用 m(10) 方法时,程序会使用上面哪一个呢?
new Dog1().m(10); //用的是void m(int i); 因为整型数据默认为int类型
//如果将上面方法的m(int i) 注释掉,那么则用哪个?
new Dog1().m(10); //用的是void m(long i); 因为找不到一致的数据类型时,则整型数据会自动向上转型为long.
//int数据会先看有没有匹配int的方法,没有则找有无long ,没有则float,最后是double.都没有则编译报错.
// int -> long -> float -> double.
new Dog1().m('a');
//char数据类型时, 先看有没有匹配char的,没有则向上转 (char类型参与运算时 会转为int类型再运算)
// char -> int ->long ->float->double
new Dog1().m(false); //查找有无boolean类型的方法 没有编译报错
//如果想使用 short byte ,则需要调用方法时 显示转化 (因此存在精度丢失)
new Dog1().m((byte)97);
new Dog1().m((short)97);
}
}
6.2.3 返回值的重载
- 方法返回值无法区分重载方法。
- 因为:下面这个看似重载的方法。方法名相同、参数列表也相同,只有返回值不同。但当你只想调用方法中的打印功能时,不在乎返回值,编译器会不知道该调用哪个。
package chapter06;
public class Dog {
public int eat(int i){ //编译报错
System.out.println("yes");
return 0;
}
public String eat(int i){
System.out.println("no");
return "";
}
}
6.3 无参构造器
- 如果你创建一个类,类中没有构造器,那么编译器就会自动为你创建一个无参构造器。一旦你显式地定义了构造器(无论有参还是无参),编译器就不会自动为你创建无参构造器。
6.4 this关键字
- this 关键字只能在非静态方法内部使用。
- 如果你在一个类的方法里调用该类的其他方法,不要使用 this,直接调用即可,this 自动地应用于其他方法上了。
package chapter06;
public class Apricot {
void pick(){}
void pit(){
this.pick();//没必要加this
pick();
}
}
- this 关键字只用在一些必须显式使用当前对象引用的特殊场合
6.4.1 在构造器中调用构造器
package chapter06;
public class Flower {
public Flower(){
this("*******"); //调用有参的构造器,并且代码必须在第一行
//this(100)//错误 一个构造中只能调用一次
}
public Flower(String str){
System.out.println("flower"+str);
}
public Flower(int i){
System.out.println("flower"+i):
}
}
6.4.2 static的含义
- static 是与类绑定的,而不是对象。
- static方法中不会存在 this。
- 你不能在静态方法中调用非静态方法(反之可以)。静态方法是
为类而创建的,不需要任何对象。
6.5 垃圾回收器
- Java 中有垃圾回收器回收无用对象占用的内存。
- 但垃圾回收器无法回收不是通过new 分配的内存。为此,Java 允许在类中定义一个名为 finalize()的方法。
6.5.1 finalize()的用途
- finalize() 工作原理 :一旦垃圾收集器准备好释放对象占用的存储空间,它首先调用finalize(),而且只有在下一次垃圾收集过程中,才会真正回收对象的内存。
- 之所以有 finalize() 方法,是因为在分配内存时可能采用了类似 C 语言中的做法,而非 Java 中的通常做法(本地方法)。因此,不会过多使用 finalize() 方法。
6.5.2 你必须实施清理
6.5.3 终结条件
- finalize() 一个有趣的用法,它不依赖于每次都要调用 finalize(),这就是对象终结条件的验证。
package chapter06;
public class Book {
boolean checkedOut = false;
public Book(boolean checkedOut){
this.checkedOut = checkedOut;
}
void checkIn(){
checkedOut = false;
}
@Override
protected void finalize(){
if(checkedOut){
System.out.println("Error: checked out");
}
}
}
package chapter06;
public class TerminationCondition {
public static void main(String[] args) {
Book book1 = new Book(true);
book1.checkIn();
new Book(true); //这本书没有登记,被finalize()调用时发现。
System.gc();
}
}
- 输出:Error: checked out
要是没有 finalize() 方法来验证终结条件,将会很难发现这个 bug。
6.5.4 垃圾回收器如何工作
- 堆:堆是利用完全二叉树的结构来维护一组数据,然后进行相关操作
- 栈:顺序栈,链式栈。
- 垃圾回收机制:
- 引用计数:
- 每个对象中含有一个引用计数器,每当有引用指向该对象时,引用计数加 1。当引用离开作用域或被置为 null 时,引用计数减 1。垃圾回收器会遍历含有全部对象的列表,当发现某个对象的引用计数为 0 时,就释放其占用的空间。
- 缺点:如果对象之间存在循环引用,那么它们的引用计数都不为 0,就会出现应该被回收但无法被回收的情况。对垃圾回收器而言,定位这样的循环引用所需的工作量极大。
- 停止-复制(stop-and-copy):
- 顾名思义,这需要先暂停程序的运行,然后将所有存活的对象从当前堆复制到另一个堆,没有复制的就是需要被垃圾回收的。
- 效率低下主要因为两个原因。其一:得有两个堆,然后在这两个分离的堆之间来回折腾,得维护比实际需要多一倍的空间。其二:在于一旦程序进入稳定状态之后,可能只会产生少量垃圾,甚至没有垃圾。复制回收器仍然会将所有内存从一处复制到另一处,这很浪费。
- 标记-清扫(mark-and-sweep):
- “标记-清扫” 所依据的思路仍然是从栈和静态存储区出发,遍历所有的引用,找出所有存活的对象。每当找到一个存活对象,就给对象设一个标记,当标记过程完成后,没有标记的对象将被释放,不会发生任何复制动作。标记-清扫” 后剩下的堆空间是不连续的,垃圾回收器要是希望得到连续空间的话,就需要重新整理剩下的对象。
6.6 成员初始化
- 成员变量:编译器默认赋予初始值 。
- boolean false
char
byte 0
short 0
int 0
long 0
float 0.0
double 0.0
对象 null
- 局部变量:必须手动赋予初值
6.6.1 指定初始化
public class MethodInit3 {
//- int j = g(i); // Illegal forward reference
int i = f();
int f() {
return 11;
}
int g(int n) {
return n * 10;
}
}
- 上述程序的正确性取决于初始化的顺序,而与其编译方式无关。所以,编译器恰当地对 “向前引用” 发出了警告。
6.7 构造器初始化
- 可以用构造器进行初始化,这种方式给了你更大的灵活性。但是,这无法阻止自动初始化的进行,他会在构造器被调用之前发生。
public class Counter {
int i;
Counter() {
i = 7;
}
}
6.7.1 初始化的顺序
6.7.2 静态数据的初始化
- 无论创建多少个对象,静态数据都只占用一份存储区域。static 关键字不能应用于局部变量,所以只能作用于属性(字段、域)。
- 初始化的顺序先是静态对象(如果它们之前没有被初始化的话),然后是非静态对 象。
6.7.3 显示的静态初始化
public class Spoon {
static int i;
static {
i = 47;
}
}
6.7.4 非静态实例初始化
public class Mugs {
Mug mug1;
Mug mug2;
{ // [1]
mug1 = new Mug(1);
mug2 = new Mug(2);
System.out.println("mug1 & mug2 initialized");
}
Mugs() {
System.out.println("Mugs()");
}
}
6.8 数组初始化
- 数组是相同类型的、用一个标识符名称封装到一起的一个对象序列或基本类型数据序列。
int[] a1 = {1, 2, 3, 4, 5};
public class ArraysOfPrimitives {
public static void main(String[] args) {
int[] a1 = {1, 2, 3, 4, 5};
int[] a2;
a2 = a1; //只是将a1的引用复制给了a2,a1 a2指向的是一个数组
for (int i = 0; i < a2.length; i++) {
a2[i] += 1; }
for (int i = 0; i < a1.length; i++) {
System.out.println("a1[" + i + "] = " + a1[i]);
}
}
}
输出:
a1[0] = 2;
a1[1] = 3;
a1[2] = 4;
a1[3] = 5;
a1[4] = 6;
6.8.1 动态数组创建
int[] a;
Random rand = new Random(47);
a = new int[rand.nextInt(20)];
- 数组的大小是通过 Random.nextInt() 随机确定的
6.8.2 可变参数列表
public class NewVarArgs {
static void printArray(Object... args) {
for (Object obj: args) {
System.out.print(obj + " ");
}
System.out.println();
}
public static void main(String[] args) {
// Can take individual elements:
printArray(47, (float) 3.14, 11.11);
printArray(47, 3.14F, 11.11);
printArray("one", "two", "three");
printArray(new A(), new A(), new A());
// Or an array:
printArray((Object[]) new Integer[] {1, 2, 3, 4});
printArray(); // Empty list is OK
}
}
输出:
47 3.14 11.11
47 3.14 11.11
one two three
A@15db9742 A@6d06d69c A@7852e922
1 2 3 4
- 有了可变参数,你就再也不用显式地编写数组语法了,当你指定参数时,编译器实际上会为你填充数组。你获取的仍然是一个数组。
6.9 枚举类型
public enum Spiciness {
NOT, MILD, MEDIUM, HOT, FLAMING
}
public class SimpleEnumUse {
public static void main(String[] args) {
Spiciness howHot = Spiciness.MEDIUM;
System.out.println(howHot);
}
}
输出:
MEDIUM
- 可以将 enum 当作其他任何类。事实上,enum 确实是类,并且具有自己的方法。
- 由于 switch 是在有限的可能值集合中选择,因此它与 enum 是绝佳的组合。
6.10 本章小结
- 构造器能保证进行正确的初始化和清理(没有正确的构造器调用,编译器就不允许创建对象)
- 垃圾回收器会自动地释放所有对象的内存,极大地简化了编程,并加强了内存管理上的安全性。然而,垃圾回收器确实增加了运行时开销,速度问题仍然是它涉足某些特定编程领域的障碍。
- 由于要保证所有对象被创建,实际上构造器比这里讨论得更加复杂。特别是当通过组合或继承创建新类的时候,这种保证仍然成立,并且需要一些额外的语法来支持。