1 不安全的编程是造成编程代价昂贵的主因之一
两个安全性问题
1.1 初始化
C 语言中很多的 bug 都是因为程序员忘记初始化导致的。尤其是很多类库的使用者不知道如何初始化类库组件,甚至当侠客们必须得初始化这些三方组件时(很多可怜的掉包侠根本不会管初始化问题)
1.2 清理
当使用一个元素做完事后就不会去关心这个元素,所以你很容易忘记清理它。这样就造成了元素使用的资源滞留不会被回收,直到程序消耗完所有的资源(特别是内存)。
C++ 引入了构造器的概念,这是一个特殊的方法,每创建一个对象,这个方法就会被自动调用。Java 采用了构造器的概念,另外还使用了垃圾收集器(Garbage Collector, GC)去自动回收不再被使用的对象所占的资源。这一章将讨论初始化和清理的问题,以及在 Java 中对它们的支持。
2 利用构造器保证初始化
你可能想为每个类创建一个initialize()方法,该方法名暗示着在使用类之前需要先调用它。不幸的是,用户必须得记得去调用它。在 Java 中,类的设计者通过构造器保证每个对象的初始化。如果一个类有构造器,那么 Java 会在用户使用对象之前(即对象刚创建完成)自动调用对象的构造器方法,从而保证初始化。下个挑战是如何命名构造器方法。存在两个问题:第一个是任何命名都可能与类中其他已有元素的命名冲突;第二个是编译器必须始终知道构造器方法名称,从而调用它。C++ 的解决方法看起来是最简单且最符合逻辑的,所以 Java 中使用了同样的方式:构造器名称与类名相同。在初始化过程中自动调用构造器方法是有意义的。
以下示例是包含了一个构造器的类:
// housekeeping/SimpleConstructor.java // Demonstration of a simple constructor class Rock { Rock() { // 这是一个构造器 System.out.print("Rock "); } } public class SimpleConstructor { public static void main(String[] args) { for (int i = 0; i < 10; i++) { new Rock(); } } }
输出:
Rock Rock Rock Rock Rock Rock Rock Rock Rock Rock
现在,当创建一个对象时:new Rock()
,内存被分配,构造器被调用。构造器保证了对象在你使用它之前进行了正确的初始化。
构造器方法名与类名相同,不需要符合首字母小写的编程风格。
在 C++ 中,无参构造器被称为默认构造器,这个术语在 Java 出现之前使用了很多年。但是,出于一些原因,Java 设计者们决定使用无参构造器这个名称,我(作者)认为这种叫法笨拙而且没有必要,所以我打算继续使用默认构造器。Java 8 引入了 default 关键字修饰方法,所以算了,我还是用无参构造器的叫法吧。
跟其他方法一样,构造器方法也可以传入参数来定义如何创建一个对象。之前的例子稍作修改,使得构造器接收一个参数:
/
// housekeeping/SimpleConstructor2.java // Constructors can have arguments class Rock2 { Rock2(int i) { System.out.print("Rock " + i + " "); } } public class SimpleConstructor2 { public static void main(String[] args) { for (int i = 0; i < 8; i++) { new Rock2(i); } } }
输出:
Rock 0 Rock 1 Rock 2 Rock 3 Rock 4 Rock 5 Rock 6 Rock 7
如果类 Tree 有一个构造方法,只接收一个参数用来表示树的高度,那么你可以像下面这样创建一棵树:
Tree t = new Tree(12); // 12-foot 树
如果 Tree(int) 是唯一的构造器,那么编译器就不允许你以其他任何方式创建 Tree 类型的对象。
构造器消除了一类重要的问题,使得代码更易读。例如,在上面的代码块中,你看不到对 initialize() 方法的显式调用,而从概念上来看,initialize()方法应该与对象的创建分离。在 Java 中,对象的创建与初始化是统一的概念,二者不可分割。
构造器没有返回值,它是一种特殊的方法。但它和返回类型为void的普通方法不同,普通方法可以返回空值,你还能选择让它返回别的类型;而构造器没有返回值,却同时也没有给你选择的余地(new表达式虽然返回了刚创建的对象的引用,但构造器本身却没有返回任何值)。如果它有返回值,并且你也可以自己选择让它返回什么,那么编译器就还得知道接下来该怎么处理那个返回值(这个返回值没有接收者)。
4 无参构造器
如前文所说,一个无参构造器就是不接收参数的构造器,用来创建一个"默认的对象"。如果你创建一个类,类中没有构造器,那么编译器就会自动为你创建一个无参构造器。例如:
// housekeeping/DefaultConstructor.java class Bird {} public class DefaultConstructor { public static void main(String[] args) { Bird bird = new Bird(); // 默认的 } }
表达式 new Bird()创建了一个新对象,调用了无参构造器,尽管在 Bird 类中并没有显式的定义无参构造器。试想如果没有构造器,我们如何创建一个对象呢。但是,一旦你显式地定义了构造器(无论有参还是无参),编译器就不会自动为你创建无参构造器。如下:
// housekeeping/NoSynthesis.java class Bird2 { Bird2(int i) {} Bird2(double d) {} } public class NoSynthesis { public static void main(String[] args) { //- Bird2 b = new Bird2(); // No default Bird2 b2 = new Bird2(1); Bird2 b3 = new Bird2(1.0); } }
如果你调用了 new Bird2() ,编译器会提示找不到匹配的构造器。当类中没有构造器时,编译器会说"你一定需要构造器,那么让我为你创建一个吧"。但是如果类中有构造器,编译器会说"你已经写了构造器了,所以肯定知道你在做什么,如果你没有创建默认构造器,说明你本来就不需要"。
5 this关键字
对于两个相同类型的对象 a 和 b,你可能在想如何调用这两个对象的 peel() 方法:
// housekeeping/BananaPeel.java class Banana { void peel(int i) { /*...*/ } } public class BananaPeel { public static void main(String[] args) { Banana a = new Banana(), b = new Banana(); a.peel(1); b.peel(2); } }
如果只有一个方法 peel(),那么怎么知道调用的是对象 a 的 peel()方法还是对象 b 的 peel() 方法呢?编译器做了一些底层工作,所以你可以像这样编写代码。peel()方法中第一个参数隐密地传入了一个指向操作对象的引用。因此,上述例子中的方法调用像下面这样:
Banana.peel(a, 1) Banana.peel(b, 1)
这是在内部实现的,你不可以直接这么编写代码,编译器不会接受,但能说明到底发生了什么。假设现在在方法内部,你想获得对当前对象的引用。但是,对象引用是被秘密地传达给编译器——并不在参数列表中。方便的是,有一个关键字: this 。this 关键字只能在非静态方法内部使用。当你调用一个对象的方法时,this 生成了一个对象引用。你可以像对待其他引用一样对待这个引用。如果你在一个类的方法里调用其他该类中的方法,不要使用 this,直接调用即可,this 自动地应用于其他方法上了。因此你可以像这样:
/
// housekeeping/Apricot.java public class Apricot { void pick() { /* ... */ } void pit() { pick(); /* ... */ } }
在 pit() 方法中,你可以使用 this.pick(),但是没有必要。编译器自动为你做了这些。this 关键字只用在一些必须显式使用当前对象引用的特殊场合。例如,用在 return 语句中返回对当前对象的引用。
// housekeeping/Leaf.java // Simple use of the "this" keyword public class Leaf { int i = 0; Leaf increment() { i++; return this; } void print() { System.out.println("i = " + i); } public static void main(String[] args) { Leaf x = new Leaf(); x.increment().increment().increment().print(); } }
输出:
i = 3
因为 increment()
通过 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
Apple 因为某些原因(比如说工具类中的方法在多个类中重复出现,你不想代码重复),必须调用一个外部工具方法 Peeler.peel()
做一些行为。必须使用 this 才能将自身传递给外部方法。