文章目录
前言
该系列为我阅读Thinking in Java(第五版)时整理的一些摘录和笔记,写成博客以作记录。
Tinking in Java具体地址:https://github.com/LingCoder/OnJava8
一、利用构造器保证初始化
1.为什么要这么设计
你可能想为每个类创建一个 initialize() 方法,该方法名暗示着在使用类之前需要先调用它。不幸的是,用户必须得记得去调用它。在 Java 中,类的设计者通过构造器保证每个对象的初始化。如果一个类有构造器,那么 Java 会在用户使用对象之前(即对象刚创建完成)自动调用对象的构造器方法,从而保证初始化。下个挑战是如何命名构造器方法。存在两个问题:第一个是任何命名都可能与类中其他已有元素的命名冲突;第二个是编译器必须始终知道构造器方法名称,从而调用它。C++ 的解决方法看起来是最简单且最符合逻辑的,所以 Java 中使用了同样的方式:构造器名称与类名相同。在初始化过程中自动调用构造器方法是有意义的。
2.定义
构造器没有返回值,它是一种特殊的方法。但它和返回类型为 void 的普通方法不同,普通方法可以返回空值,你还能选择让它返回别的类型;而构造器没有返回值,却同时也没有给你选择的余地(new 表达式虽然返回了刚创建的对象的引用,但构造器本身却没有返回任何值)。如果它有返回值,并且你也可以自己选择让它返回什么,那么编译器就还得知道接下来该怎么处理那个返回值(这个返回值没有接收者)。
3.方法重载
PS:这里我删掉了一些,主要觉得比较简单,所以没有过多摘录。所谓构造器重载,和方法重载类似,主要目的就是利用构造器犯法参数类型和个数的不同来适应不同环境构造同一类型的对象
一个 Tree 对象既可以是一颗树苗,使用无参构造器创建,也可以是一颗在温室中已长大的树,已经有一定高度,这时候,就需要使用有参构造器创建。
你也许想以多种方式调用 info()
方法。比如,如果你想打印额外的消息,就可以使用 info(String)
方法。如果你无话可说,就可以使用 info()
方法。用两个命名定义完全相同的概念看起来很奇怪,而使用方法重载,你就可以使用一个命名来定义一个概念。
4.区分重载方法
如果两个方法命名相同,Java是怎么知道你调用的是哪个呢?有一条简单的规则:**每个被重载的方法必须有独一无二的参数列表。**你稍微思考下,就会很明了了,除了通过参数列表的不同来区分两个相同命名的方法,其他也没什么方式了。你甚至可以根据参数列表中的参数顺序来区分不同的方法,尽管这会造成代码难以维护。
5.无参构造器
如前文所说,一个无参构造器就是不接收参数的构造器,用来创建一个"默认的对象"。如果你创建一个类,类中没有构造器,那么编译器就会自动为你创建一个无参构造器。例如:
// 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()
,编译器会提示找不到匹配的构造器。当类中没有构造器时,编译器会说"你一定需要构造器,那么让我为你创建一个吧"。但是如果类中有构造器,编译器会说"你已经写了构造器了,所以肯定知道你在做什么,如果你没有创建默认构造器,说明你本来就不需要"。
6.this关键字
假设现在在方法内部,你想获得对当前对象的引用。但是,对象引用是被秘密地传达给编译器——并不在参数列表中。方便的是,有一个关键字: this 。this 关键字只能在非静态方法内部使用。当你调用一个对象的方法时,this 生成了一个对象引用。你可以像对待其他引用一样对待这个引用。如果你在一个类的方法里调用该类的其他方法,不要使用 this,直接调用即可,this 自动地应用于其他方法上了。因此你可以像这样:
// housekeeping/Apricot.java
public class Apricot {
void pick() {
/* ... */
}
void pit() {
pick();
/* ... */
}
}
在 pit()
方法中,你可以使用 this.pick()
,但是没有必要。编译器自动为你做了这些。this 关键字只用在一些必须显式使用当前对象引用的特殊场合。例如,用在 return 语句中返回对当前对象的引用。
7.在构造器中调用构造器
当你在一个类中写了多个构造器,有时你想在一个构造器中调用另一个构造器来避免代码重复。你通过 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
从构造器 Flower(String s, int petals)
可以看出,其中只能通过 this 调用一次构造器。另外,必须首先调用构造器,否则编译器会报错。这个例子同样展示了 this 的另一个用法。参数列表中的变量名 s 和成员变量名 s 相同,会引起混淆。你可以通过 this.s
表明你指的是成员变量 s,从而避免重复。
8.static 的含义
记住了 this 关键字的内容,你会对 static 修饰的方法有更加深入的理解:**static 方法中不会存在 this。**你不能在静态方法中调用非静态方法(反之可以)。静态方法是为类而创建的,不需要任何对象。事实上,这就是静态方法的主要目的,静态方法看起来就像全局方法一样,但是 Java 中不允许全局方法,**一个类中的静态方法可以访问其他静态方法和静态属性。**一些人认为静态方法不是面向对象的,因为它们的确具有全局方法的语义。使用静态方法,因为不存在 this,所以你没有向一个对象发送消息。的确,如果你发现代码中出现了大量的 static 方法,就该重新考虑自己的设计了。然而,static 的概念很实用,许多时候都要用到它。至于它是否真的"面向对象",就留给理论家去讨论吧。
二、垃圾回收器如何工作
方式列举
1.引用计数
每个对象中含有一个引用计数器,每当有引用指向该对象时,引用计数加 1。当引用离开作用域或被置为 null 时,引用计数减 1。因此,管理引用计数是一个开销不大但是在程序的整个生命周期频繁发生的负担。垃圾回收器会遍历含有全部对象的列表,当发现某个对象的引用计数为 0 时,就释放其占用的空间(但是,引用计数模式经常会在计数为 0 时立即释放对象)。这个机制存在一个缺点:如果对象之间存在循环引用,那么它们的引用计数都不为 0,就会出现应该被回收但无法被回收的情况。对垃圾回收器而言,定位这样的循环引用所需的工作量极大。引用计数常用来说明垃圾回收的工作方式,但似乎从未被应用于任何一种 Java 虚拟机实现中。
2.停止-复制(stop-and-copy)
这需要先暂停程序的运行(不属于后台回收模式),然后将所有存活的对象从当前堆复制到另一个堆,没有复制的就是需要被垃圾回收的。另外,当对象被复制到新堆时,它们是一个挨着一个紧凑排列,然后就可以按照前面描述的那样简单、直接地分配新空间了。
当对象从一处复制到另一处,所有指向它的引用都必须修正。位于栈或静态存储区的引用可以直接被修正,但可能还有其他指向这些对象的引用,它们在遍历的过程中才能被找到(可以想象成一个表格,将旧地址映射到新地址)。
这种所谓的"复制回收器"效率低下主要因为两个原因。其一:得有两个堆,然后在这两个分离的堆之间来回折腾,得维护比实际需要多一倍的空间。某些 Java 虚拟机对此问题的处理方式是,按需从堆中分配几块较大的内存,复制动作发生在这些大块内存之间。
其二在于复制本身。一旦程序进入稳定状态之后,可能只会产生少量垃圾,甚至没有垃圾。尽管如此,复制回收器仍然会将所有内存从一处复制到另一处,这很浪费。为了避免这种状况,一些 Java 虚拟机会进行检查:要是没有新垃圾产生,就会转换到另一种模式(即"自适应")。
这种模式称为标记-清扫(mark-and-sweep),Sun 公司早期版本的 Java 虚拟机一直使用这种技术。对一般用途而言,"标记-清扫"方式速度相当慢,但是当你知道程序只会产生少量垃圾甚至不产生垃圾时,它的速度就很快了。
3.标记-清扫(mark-and-sweep)
"标记-清扫"所依据的思路仍然是从栈和静态存储区出发,遍历所有的引用,找出所有存活的对象。但是,每当找到一个存活对象,就给对象设一个标记,并不回收它。只有当标记过程完成后,清理动作才开始。在清理过程中,没有标记的对象将被释放,不会发生任何复制动作。"标记-清扫"后剩下的堆空间是不连续的,垃圾回收器要是希望得到连续空间的话,就需要重新整理剩下的对象。
自适应的垃圾回收技术
如前文所述,这里讨论的 Java 虚拟机中,内存分配以较大的"块"为单位。如果对象较大,它会占用单独的块。严格来说,"停止-复制"要求在释放旧对象之前,必须先将所有存活对象从旧堆复制到新堆,这导致了大量的内存复制行为。有了块,垃圾回收器就可以把对象复制到废弃的块。每个块都有年代数来记录自己是否存活。通常,如果块在某处被引用,其年代数加 1,垃圾回收器会对上次回收动作之后新分配的块进行整理。这对处理大量短命的临时对象很有帮助。垃圾回收器会定期进行完整的清理动作——大型对象仍然不会复制(只是年代数会增加),含有小型对象的那些块则被复制并整理。Java 虚拟机会监视,如果所有对象都很稳定,垃圾回收的效率降低的话,就切换到"标记-清扫"方式。同样,Java 虚拟机会跟踪"标记-清扫"的效果,如果堆空间出现很多碎片,就会切换回"停止-复制"方式。这就是"自适应"的由来,你可以给它个啰嗦的称呼:"自适应的、分代的、停止-复制、标记-清扫"式的垃圾回收器。
一些新的技术
Java 虚拟机中有许多附加技术用来提升速度。尤其是与加载器操作有关的,被称为"即时"(Just-In-Time, JIT)编译器的技术。这种技术可以把程序全部或部分翻译成本地机器码,所以不需要 JVM 来进行翻译,因此运行得更快。当需要装载某个类(通常是创建该类的第一个对象)时,编译器会先找到其 .class 文件,然后将该类的字节码装入内存。你可以让即时编译器编译所有代码,但这种做法有两个缺点:一是这种加载动作贯穿整个程序生命周期内,累加起来需要花更多时间;二是会增加可执行代码的长度(字节码要比即时编译器展开后的本地机器码小很多),这会导致页面调度,从而一定降低程序速度。另一种做法称为惰性评估,意味着即时编译器只有在必要的时候才编译代码。这样,从未被执行的代码也许就压根不会被 JIT 编译。新版 JDK 中的 Java HotSpot 技术就采用了类似的做法,代码每被执行一次就优化一些,所以执行的次数越多,它的速度就越快。
三、初始化
1.初始化过程
①成员初始化
Java 尽量保证所有变量在使用前都能得到恰当的初始化。对于方法的局部变量,这种保证会以编译时错误的方式呈现。
PS:当成员变量没有显示赋值时,程序会自动赋初值,比如int类型的成员变量默认为0,所以我们在编程时,为了减少歧义,一般会进行显示赋值(指定初始化)。
②构造器初始化
可以用构造器进行初始化,这种方式给了你更大的灵活性,因为你可以在运行时调用方法进行初始化。但是,这无法阻止自动初始化的进行,他会在构造器被调用之前发生。
**
③初始化的顺序
在类中变量定义的顺序决定了它们初始化的顺序。即使变量定义散布在方法定义之间,它们仍会在任何方法(包括构造器)被调用之前得到初始化。
④静态数据的初始化
无论创建多少个对象,静态数据都只占用一份存储区域。static 关键字不能应用于局部变量,所以只能作用于属性(字段、域)。如果一个字段是静态的基本类型,你没有初始化它,那么它就会获得基本类型的标准初值。如果它是对象引用,那么它的默认初值就是 null。
如果在定义时进行初始化,那么静态变量看起来就跟非静态变量一样。
⑤初始化过程总结
PS:以下为本章重点
概括一下创建对象的过程,假设有个名为 Dog 的类:
- 即使没有显式地使用 static 关键字,构造器实际上也是静态方法。所以,当首次创建 Dog 类型的对象或是首次访问 Dog 类的静态方法或属性时,Java 解释器必须在类路径中查找,以定位 Dog.class。
- 当加载完 Dog.class 后(后面会学到,这将创建一个 Class 对象),有关静态初始化的所有动作都会执行。因此,静态初始化只会在首次加载 Class 对象时初始化一次。
- 当用
new Dog()
创建对象时,首先会在堆上为 Dog 对象分配足够的存储空间。 - 分配的存储空间首先会被清零,即会将 Dog 对象中的所有基本类型数据设置为默认值(数字会被置为 0,布尔型和字符型也相同),引用被置为 null。
- 执行所有出现在字段定义处的初始化动作。
- 执行构造器。你将会在"复用"这一章看到,这可能会牵涉到很多动作,尤其当涉及继承的时候。
2.一些数据的初始化方式
显式的静态初始化
你可以将一组静态初始化动作放在类里面一个特殊的"静态子句"(有时叫做静态块)中。像下面这样:
// housekeeping/Spoon.java
public class Spoon {
static int i;
static {
i = 47;
}
}
非静态实例初始化
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 关键字。这种语法对于支持"匿名内部类"(参见"内部类"一章)的初始化是必须的,但是你也可以使用它保证某些操作一定会发生,而不管哪个构造器被调用。从输出看出,实例初始化子句是在两个构造器之前执行的。
数组初始化
数组是相同类型的、用一个标识符名称封装到一起的一个对象序列或基本类型数据序列。数组是通过方括号下标操作符 [] 来定义和使用的。要定义一个数组引用,只需要在类型名加上方括号:
int[] a1;
方括号也可放在标识符的后面,两者的含义是一样的:
int a1[];
编译器不允许指定数组的大小。这又把我们带回有关"引用"的问题上。你所拥有的只是对数组的一个引用(你已经为该引用分配了足够的存储空间),但是还没有给数组对象本身分配任何空间。为了给数组创建相应的存储空间,必须写初始化表达式。对于数组,初始化动作可以出现在代码的任何地方,但是也可以使用一种特殊的初始化表达式,它必须在创建数组的地方出现。这种特殊的初始化是由一对花括号括起来的值组成。这种情况下,存储空间的分配(相当于使用 new) 将由编译器负责。例如:
int[] a1 = {1, 2, 3, 4, 5};
在 Java 中可以将一个数组赋值给另一个数组,其实真正做的只是复制了一个引用。
所有的数组(无论是对象数组还是基本类型数组)都有一个固定成员 length,告诉你这个数组有多少个元素,你不能对其修改。与 C 和 C++ 类似,Java 数组计数也是从 0 开始的,所能使用的最大下标数是 length - 1。超过这个边界,C 和 C++ 会默认接受,允许你访问所有内存,许多声名狼藉的 bug 都是由此而生。但是 Java 在你访问超出这个边界时,会报运行时错误(异常),从而避免此类问题。
动态数组创建
如果在编写程序时,不确定数组中需要多少个元素,可以使用 new 在数组中创建元素。如下例所示,使用 new 创建基本类型数组。new 不能创建非数组以外的基本类型数据:
// housekeeping/ArrayNew.java
// Creating arrays with new
import java.util.*;
public class ArrayNew {
public static void main(String[] args) {
int[] a;
Random rand = new Random(47);
a = new int[rand.nextInt(20)];
System.out.println("length of a = " + a.length);
System.out.println(Arrays.toString(a));
}
}
输出:
length of a = 18
[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]
PS:这里可能会有点歧义,作者这里指的动态数组创建,并不是说数组大小可以实时改变,这里指的是数组大小可以在程序运行中确定,而不一定要在初始化阶段但是一旦确定则不能更改
**
可变参数列表
PS:这里也不能完全算一种初始化方式,这里更确切的来说是一种参数传递的方式,只是作者把这归在这里来说,我也就归在一起
你可以以一种类似 C 语言中的可变参数列表(C 通常把它称为"varargs")来创建和调用方法。这可以应用在参数个数或类型未知的场合。由于所有的类都最后继承于 Object 类(随着本书的进展,你会对此有更深的认识),所以你可以创建一个以 Object 数组为参数的方法,并像下面这样调用:
// 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
你可能看到像上面这样编写的 Java 5 之前的代码,它们可以产生可变的参数列表。在 Java 5 中,这种期盼已久的特性终于添加了进来,就像在 printArray()
中看到的那样:
// housekeeping/NewVarArgs.java
// Using array syntax to create variable argument lists
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
有了可变参数,你就再也不用显式地编写数组语法了,当你指定参数时,编译器实际上会为你填充数组。你获取的仍然是一个数组,这就是为什么 printArray() 可以使用 for-in 迭代数组的原因。但是,这不仅仅只是从元素列表到数组的自动转换。注意程序的倒数第二行,一个 Integer 数组(通过自动装箱创建)被转型为一个 Object 数组(为了移除编译器的警告),并且传递给了 printArray()。显然,**编译器会发现这是一个数组,不会执行转换。**因此,如果你有一组事物,可以把它们当作列表传递,而如果你已经有了一个数组,该方法会把它们当作可变参数列表来接受。
这段程序展示了如何使用除了 Object 类之外类型的可变参数列表。这里,所有的可变参数都是 String 对象。可变参数列表中可以使用任何类型的参数,包括基本类型。下面例子展示了可变参数列表变为数组的情形,并且如果列表中没有任何元素,那么转变为大小为 0 的数组:
// housekeeping/VarargType.java
public class VarargType {
static void f(Character... args) {
System.out.print(args.getClass());
System.out.println(" length " + args.length);
}
static void g(int... args) {
System.out.print(args.getClass());
System.out.println(" length " + args.length)
}
public static void main(String[] args) {
f('a');
f();
g(1);
g();
System.out.println("int[]: "+ new int[0].getClass());
}
}
输出:
class [Ljava.lang.Character; length 1
class [Ljava.lang.Character; length 0
class [I length 1
class [I length 0
int[]: class [I
PS:参数为零就会出现以下情况
在每种情况下,编译器都会使用自动装箱来匹配重载的方法,然后调用最明确匹配的方法。
但是如果调用不含参数的 f()
,编译器就无法知道应该调用哪个方法了。尽管这个错误可以弄清楚,但是它可能会使客户端程序员感到意外。
你可能会通过在某个方法中增加一个非可变参数解决这个问题:
// housekeeping/OverloadingVarargs2.java
// {WillNotCompile}
public class OverloadingVarargs2 {
static void f(float i, Character... args) {
System.out.println("first");
}
static void f(Character... args) {
System.out.println("second");
}
public static void main(String[] args) {
f(1, 'a');
f('a', 'b');
}
}
枚举类型
下面是个简单的例子:
// 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
在你创建 enum 时,编译器会自动添加一些有用的特性。例如,它会创建 toString() 方法,以便你方便地显示某个 enum 实例的名称,这从上面例子中的输出可以看出。编译器还会创建** ordinal() 方法表示某个特定 enum 常量的声明顺序**,static values() 方法按照 enum 常量的声明顺序,生成这些常量值构成的数组:
// housekeeping/EnumOrder.java
public class EnumOrder {
public static void main(String[] args) {
for (Spiciness s: Spiciness.values()) {
System.out.println(s + ", ordinal " + s.ordinal());
}
}
}
输出:
NOT, ordinal 0
MILD, ordinal 1
MEDIUM, ordinal 2
HOT, ordinal 3
FLAMING, ordinal 4
尽管 enum 看起来像是一种新的数据类型,但是这个关键字只是在生成 enum 的类时,产生了某些编译器行为,因此在很大程度上你可以将 enum 当作其他任何类。事实上,enum 确实是类,并且具有自己的方法。
本章小结
构造器,这种看起来精巧的初始化机制,应该给了你很强的暗示:初始化在编程语言中的重要地位。C++ 的发明者 Bjarne Stroustrup 在设计 C++ 期间,在针对 C 语言的生产效率进行的最初调查中发现,错误的初始化会导致大量编程错误。这些错误很难被发现,同样,不合理的清理也会如此。因为构造器能保证进行正确的初始化和清理(没有正确的构造器调用,编译器就不允许创建对象),所以你就有了完全的控制和安全。
在 C++ 中,析构器很重要,因为用 new 创建的对象必须被明确地销毁。在 Java 中,垃圾回收器会自动地释放所有对象的内存,所以很多时候类似的清理方法就不太需要了(但是当要用到的时候,你得自己动手)。在不需要类似析构器行为的时候,Java 的垃圾回收器极大地简化了编程,并加强了内存管理上的安全性。一些垃圾回收器甚至能清理其他资源,如图形和文件句柄。然而,垃圾回收器确实增加了运行时开销,由于 Java 解释器从一开始就很慢,所以这种开销到底造成多大的影响很难看出来。随着时间的推移,Java 在性能方面提升了很多,但是速度问题仍然是它涉足某些特定编程领域的障碍。
由于要保证所有对象被创建,实际上构造器比这里讨论得更加复杂。特别是当通过_组合_或_继承_创建新类的时候,这种保证仍然成立,并且需要一些额外的语法来支持。在后面的章节中,你会学习组合,继承以及它们如何影响构造器。