《On Java 8》读书笔记006_初始化和清理

初始化和清理

https://github.com/LingCoder/OnJava8

"不安全"的编程是造成编程代价昂贵的罪魁祸首之一。有两个安全性问题:初始化和清理。C 语言中很多的 bug 都是因为程序员忘记初始化导致的。尤其是很多类库的使用者不知道如何初始化类库组件,甚至他们必须得去初始化。清理则是另一个特殊的问题,因为当你使用一个元素做完事后就不会去关心这个元素,所以你很容易忘记清理它。这样就造成了元素使用的资源滞留不会被回收,直到程序消耗完所有的资源(特别是内存)。

C++ 引入了构造器的概念,这是一个特殊的方法,每创建一个对象,这个方法就会被自动调用。Java 采用了构造器的概念,另外还使用了垃圾收集器(Garbage Collector, GC)去自动回收不再被使用的对象所占的资源。这一章将讨论初始化和清理的问题,以及在 Java 中对它们的支持。

1、利用构造器保证初始化

2、方法重载

方法重载允许方法具有相同的方法名但接收的参数不同。尽管方法重载对于构造器是重要的,但是也可以很方便地对其他任何方法进行重载。

2.1、区分重载方法

如果两个方法命名相同,Java是怎么知道你调用的是哪个呢?有一条简单的规则:每个被重载的方法必须有独一无二的参数列表。你稍微思考下,就会很明了了,除了通过参数列表的不同来区分两个相同命名的方法,其他也没什么方式了。你甚至可以根据参数列表中的参数顺序来区分不同的方法,尽管这会造成代码难以维护。

2.2、返回值的重载

不能根据返回值的不同而重载方法,因为编译器分辨不出究竟应该通过哪个方法进行返回。

3、无参构造器

一旦你显式地定义了构造器(无论有参还是无参),编译器就不会自动为你创建无参构造器。

4、this关键字

类方法中第一个参数隐密地传入了一个指向操作对象的引用。
this 关键字在向其他方法传递当前对象时也很有用:

// housekeeping/PassingThis.java

class Person {
    public void eat(Apple apple) {
        Apple peeled = apple.getPeeled();
        System.out.println("Yummy");
    }
}

public class Peeler {
    static Apple peel(Apple apple) {
        // ... remove peel:去皮
        return apple; // Peeled:去了皮的苹果
    }
}

public class Apple {
    Apple getPeeled() {
        return Peeler.peel(this);//类名。静态方法
    }
}

public class PassingThis {
    public static void main(String[] args) {
        new Person().eat(new Apple());
    }
}

Yummy

5、在构造器中调用构造器

当你在一个类中写了多个构造器,有时你想在一个构造器中调用另一个构造器来避免代码重复。你通过 this 关键字实现这样的调用。

通常当你说 this,意味着"这个对象"或"当前对象",它本身生成对当前对象的引用。在一个构造器中,当你给 this 一个参数列表时,它是另一层意思。它通过最直接的方式显式地调用匹配参数列表的构造器:

// housekeeping/Flower.java
// Calling constructors with "this"

public class Flower {
    int petalCount = 0;
    String s = "initial value";

    Flower(int petals) {
        petalCount = petals;
        System.out.println("Constructor w/ int arg only, petalCount = " + petalCount);
    }

    Flower(String ss) {
        System.out.println("Constructor w/ string arg only, s = " + ss);
        s = ss;
    }

    Flower(String s, int petals) {
        this(petals);
        //- this(s); // Can't call two!
        this.s = s; // Another use of "this"
        System.out.println("String & int args");
    }

    Flower() {
        this("hi", 47);
        System.out.println("no-arg constructor");
    }

    void printPetalCount() {
        //- this(11); // Not inside constructor!
        System.out.println("petalCount = " + petalCount + " s = " + s);
    }

    public static void main(String[] args) {
        Flower x = new Flower();
        x.printPetalCount();
    }
}

Constructor w/ int arg only, petalCount = 47
String & int args
no-arg constructor
petalCount = 47 s = hi

6、static的含义

记住了 this 关键字的内容,你会对 static 修饰的方法有更加深入的理解:static 方法中不会存在 this。你不能在静态方法中调用非静态方法(反之可以)静态方法是为类而创建的,不需要任何对象。事实上,这就是静态方法的主要目的,静态方法看起来就像全局方法一样,但是 Java 中不允许全局方法,一个类中的静态方法可以访问其他静态方法和静态属性。一些人认为静态方法不是面向对象的,因为它们的确具有全局方法的语义。使用静态方法,因为不存在 this,所以你没有向一个对象发送消息。的确,如果你发现代码中出现了大量的 static 方法,就该重新考虑自己的设计了。然而,static 的概念很实用,许多时候都要用到它。至于它是否真的"面向对象",就留给理论家去讨论吧。

7、垃圾回收器

程序员都了解初始化的重要性,但通常会忽略清理的重要性。毕竟,谁会去清理一个 int 呢?但是使用完一个对象就不管它并非总是安全的。Java 中有垃圾回收器回收无用对象占用的内存。但现在考虑一种特殊情况:你创建的对象不是通过 new 来分配内存的,而垃圾回收器只知道如何释放用 new 创建的对象的内存,所以它不知道如何回收不是 new 分配的内存。为了处理这种情况,Java 允许在类中定义一个名为 finalize() 的方法。

它的工作原理"假定"是这样的:当垃圾回收器准备回收对象的内存时,首先会调用其 finalize() 方法,并在下一轮的垃圾回收动作发生时,才会真正回收对象占用的内存。所以如果你打算使用 finalize() ,就能在垃圾回收时做一些重要的清理工作。finalize() 是一个潜在的编程陷阱,因为一些程序员(尤其是 C++ 程序员)会一开始把它误认为是 C++ 中的析构函数(C++ 在销毁对象时会调用这个函数)。所以有必要明确区分一下:在 C++ 中,对象总是被销毁的(在一个 bug-free 的程序中),而在 Java 中,对象并非总是被垃圾回收,或者换句话说:
1、对象可能不被垃圾回收。
2、垃圾回收不等同于析构。
这意味着在你不再需要某个对象之前,如果必须执行某些动作,你得自己去做。Java 没有析构器或类似的概念,所以你必须得自己创建一个普通的方法完成这项清理工作。例如,对象在创建的过程中会将自己绘制到屏幕上。如果不是明确地从屏幕上将其擦除,它可能永远得不到清理。如果在 finalize() 方法中加入某种擦除功能,那么当垃圾回收发生时,finalize() 方法被调用(不保证一定会发生),图像就会被擦除,要是"垃圾回收"没有发生,图像则仍会保留下来。

也许你会发现,只要程序没有濒临内存用完的那一刻,对象占用的空间就总也得不到释放。如果程序执行结束,而垃圾回收器一直没有释放你创建的任何对象的内存,则当程序退出时,那些资源会全部交还给操作系统。这个策略是恰当的,因为垃圾回收本身也有开销,要是不使用它,那就不用支付这部分开销了。

8、finalize()的用途

如果你不能将 finalize() 作为通用的清理方法,那么这个方法有什么用呢?

这引入了要记住的第3点:

3、垃圾回收只与内存有关。
也就是说,使用垃圾回收的唯一原因就是为了回收程序不再使用的内存。所以对于与垃圾回收有关的任何行为来说(尤其是 finalize() 方法),它们也必须同内存及其回收有关。

但这是否意味着如果对象中包括其他对象,finalize() 方法就应该明确释放那些对象呢?不是,无论对象是如何创建的,垃圾回收器都会负责释放对象所占用的所有内存。这就将对 finalize() 的需求限制到一种特殊情况,即通过某种创建对象方式之外的方式为对象分配了存储空间。不过,你可能会想,Java 中万物皆对象,这种情况怎么可能发生?

看起来之所以有 finalize() 方法,是因为在分配内存时可能采用了类似 C 语言中的做法,而非 Java 中的通常做法。这种情况主要发生在使用"本地方法"的情况下,本地方法是一种用 Java 语言调用非 Java 语言代码的形式(关于本地方法的讨论,见本书电子版第2版的附录B)。本地方法目前只支持 C 和 C++,但是它们可以调用其他语言写的代码,所以实际上可以调用任何代码。在非 Java 代码中,也许会调用 C 的 malloc() 函数系列来分配存储空间,而且除非调用 free() 函数,不然存储空间永远得不到释放,造成内存泄露。但是,free() 是 C 和 C++ 中的函数,所以你需要在 finalize() 方法里用本地方法调用它。

读到这里,你可能明白了不会过多使用 finalize() 方法。对,它确实不是进行普通的清理工作的合适场所。那么,普通的清理工作在哪里执行呢?

9、实施清理

要清理一个对象,用户必须在需要清理的时候调用执行清理动作的方法。这听上去相当直接,但却与 C++ 中的"析构函数"的概念稍有抵触。在 C++ 中,所有对象都会被销毁,或者说应该被销毁。如果在 C++ 中创建了一个局部对象(在栈上创建,在 Java 中不行),此时的销毁动作发生在以"右花括号"为边界的、此对象作用域的末尾处。如果对象是用 new 创建的(类似于 Java 中),那么当程序员调用 C++ 的 delete 操作符时(Java 中不存在),就会调用相应的析构函数。如果程序员忘记调用 delete,那么永远不会调用析构函数,这样就会导致内存泄露,对象的其他部分也不会得到清理。这种 bug 很难跟踪,也是让 C++ 程序员转向 Java 的一个主要因素。相反,在 Java 中,没有用于释放对象的 delete,因为垃圾回收器会帮助你释放存储空间。甚至可以肤浅地认为,正是由于垃圾回收的存在,使得 Java 没有析构函数。然而,随着学习的深入,你会明白垃圾回收器的存在并不能完全替代析构函数(而且绝对不能直接调用 finalize(),所以这也不是一种解决方案)。如果希望进行除释放存储空间之外的清理工作,还是得明确调用某个恰当的 Java 方法:这就等同于使用析构函数了,只是没有它方便。

记住,无论是"垃圾回收"还是"终结",都不保证一定会发生。如果 Java 虚拟机(JVM)并未面临内存耗尽的情形,它可能不会浪费时间执行垃圾回收以恢复内存。

10、终结条件

你可能注意到使用了 @Override。@ 意味着这是一个注解,注解是关于代码的额外信息。在这里,该注解告诉编译器这不是偶然地重定义在每个对象中都存在的 finalize() 方法——程序员知道自己在做什么。编译器确保你没有拼错方法名,而且确保那个方法存在于基类中。注解也是对读者的提醒,@Override 在 Java 5 引入,在 Java 7 中改善,本书通篇会出现。

11、垃圾回收器如何工作

12、成员初始化

未初始化的局部变量更有可能是程序员的疏忽,所以采用默认值反而会掩盖这种失误。强制程序员提供一个初始值,往往能帮助找出程序里的 bug。
要是类的成员变量是基本类型,情况就会变得有些不同。正如在"万物皆对象"一章中所看到的,类的每个基本类型数据成员保证都会有一个初始值。

13、指定初始化

怎么给一个变量赋初值呢?一种很直接的方法是在定义类成员变量的地方为其赋值。以下代码修改了 InitialValues 类成员变量的定义,直接提供了初值:

// housekeeping/InitialValues2.java
// Providing explicit initial values

public class InitialValues2 {
    boolean bool = true;
    char ch = 'x';
    byte b = 47;
    short s = 0xff;
    int i = 999;
    long lng = 1;
    float f = 3.14f;
    double d = 3.14159;
}

你也可以用同样的方式初始化非基本类型的对象。如果 Depth 是一个类,那么可以像下面这样创建一个对象并初始化它:

// housekeeping/Measurement.java

class Depth {}

public class Measurement {
    Depth d = new Depth();
    // ...
}

如果没有为 d 赋予初值就尝试使用它,就会出现运行时错误,告诉你产生了一个异常(详细见"异常"章节)。
你也可以通过调用某个方法来提供初值:

// housekeeping/MethodInit.java

public class MethodInit {
    int i = f();

    int f() {
        return 11;
    }

}

这个方法可以带有参数,但这些参数不能是未初始化的类成员变量。因此,可以这么写:

// housekeeping/MethodInit2.java

public class MethodInit2 {
    int i = f();
    int j = g(i);

    int f() {
        return 11;
    }

    int g(int n) {
        return n * 10;
    }
}

但是你不能这么写:

// housekeeping/MethodInit3.java

public class MethodInit3 {
    //- int j = g(i); // Illegal forward reference
    int i = f();

    int f() {
        return 11;
    }

    int g(int n) {
        return n * 10;
    }
}

显然,上述程序的正确性取决于初始化的顺序,而与其编译方式无关。所以,编译器恰当地对"向前引用"发出了警告。

14、构造器初始化

14.1、初始化顺序

在类中变量定义的顺序决定了它们初始化的顺序。即使变量定义散布在方法定义之间,它们仍会在任何方法(包括构造器)被调用之前得到初始化。

14.2、静态变量的初始化

无论创建多少个对象,静态数据都只占用一份存储区域。static 关键字不能应用于局部变量,所以只能作用于属性(字段、域)。如果一个字段是静态的基本类型,你没有初始化它,那么它就会获得基本类型的标准初值。如果它是对象引用,那么它的默认初值就是 null。

概括一下创建对象的过程,假设有个名为 Dog 的类:
1、即使没有显式地使用 static 关键字,构造器实际上也是静态方法。所以,当首次创建 Dog 类型的对象或是首次访问 Dog 类的静态方法或属性时,Java 解释器必须在类路径中查找,以定位 Dog.class。
2、当加载完 Dog.class 后(后面会学到,这将创建一个 Class 对象),有关静态初始化的所有动作都会执行。因此,静态初始化只会在首次加载 Class 对象时初始化一次。
3、当用 new Dog() 创建对象时,首先会在堆上为 Dog 对象分配足够的存储空间。
4、分配的存储空间首先会被清零,即会将 Dog 对象中的所有基本类型数据设置为默认值(数字会被置为 0,布尔型和字符型也相同),引用被置为 null。
5、执行所有出现在字段定义处的初始化动作。
6、执行构造器。你将会在"复用"这一章看到,这可能会牵涉到很多动作,尤其当涉及继承的时候。

15、显式的静态初始化

你可以将一组静态初始化动作放在类里面一个特殊的"静态子句"(有时叫做静态块)中。像下面这样:

// housekeeping/Spoon.java

public class Spoon {
    static int i;

    static {
        i = 47;
    }
}

这看起来像个方法,但实际上它只是一段跟在 static 关键字后面的代码块。与其他静态初始化动作一样,这段代码仅执行一次:当首次创建这个类的对象或首次访问这个类的静态成员(甚至不需要创建该类的对象)时。

16、非静态实例初始化

Java 提供了被称为实例初始化的类似语法,用来初始化每个对象的非静态变量,例如:

// housekeeping/Mugs.java
// Instance initialization

class Mug {
    Mug(int marker) {
        System.out.println("Mug(" + marker + ")");
    }
}

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()");
    }

    Mugs(int i) {
        System.out.println("Mugs(int)");
    }

    public static void main(String[] args) {
        System.out.println("Inside main()");
        new Mugs();
        System.out.println("new Mugs() completed");
        new Mugs(1);
        System.out.println("new Mugs(1) completed");
    }
}

Inside main
Mug(1)
Mug(2)
mug1 & mug2 initialized
Mugs()
new Mugs() completed
Mug(1)
Mug(2)
mug1 & mug2 initialized
Mugs(int)
new Mugs(1) completed

看起来它很像静态代码块,只不过少了 static 关键字。这种语法对于支持"匿名内部类"(参见"内部类"一章)的初始化是必须的,但是你也可以使用它保证某些操作一定会发生,而不管哪个构造器被调用。从输出看出,实例初始化子句是在两个构造器之前执行的。

17、数组的初始化

在 Java 中可以将一个数组赋值给另一个数组,所以可以这样:

a2 = a1;

其实真正做的只是复制了一个引用,就像下面演示的这样:

// housekeeping/ArraysOfPrimitives.java

public class ArraysOfPrimitives {
    public static void main(String[] args) {
        int[] a1 = {1, 2, 3, 4, 5};
        int[] a2;
        a2 = a1;
        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;

数组引用的传递:

// housekeeping/DynamicArray.java
// Array initialization

public class DynamicArray {
    public static void main(String[] args) {
        Other.main(new String[] {"fiddle", "de", "dum"});
    }
}

class Other {
    public static void main(String[] args) {
        for (String s: args) {
            System.out.print(s + " ");
        }
    }
}

fiddle de dum 

可变的参数列表:

// housekeeping/VarArgs.java
// Using array syntax to create variable argument lists

class A {}

public class VarArgs {
    static void printArray(Object[] args) {
        for (Object obj: args) {
            System.out.print(obj + " ");
        }
        System.out.println();
    }

    public static void main(String[] args) {
        printArray(new Object[] {47, (float) 3.14, 11.11});
        printArray(new Object[] {"one", "two", "three"});
        printArray(new Object[] {new A(), new A(), new A()});
    }
}

47 3.14 11.11 
one two three 
A@15db9742 A@6d06d69c A@7852e922

18、枚举类型

// housekeeping/Spiciness.java

public enum Spiciness {
    NOT, MILD, MEDIUM, HOT, FLAMING
}

这里创建了一个名为 Spiciness 的枚举类型,它有5个值。由于枚举类型的实例是常量,因此按照命名惯例,它们都用大写字母表示(如果名称中含有多个单词,使用下划线分隔)。
要使用 enum,需要创建一个该类型的引用,然后将其赋值给某个实例:

// housekeeping/SimpleEnumUse.java

public class SimpleEnumUse {
    public static void main(String[] args) {
        Spiciness howHot = Spiciness.MEDIUM;
        System.out.println(howHot);
    }
}

MEDIUM

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值