// 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();
}
}
}
// 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 类型的对象。
// housekeeping/Overloading.java
// Both constructor and ordinary method overloading
class Tree {
int height;
Tree() {
System.out.println("Planting a seedling");
height = 0;
}
Tree(int initialHeight) {
height = initialHeight;
System.out.println("Creating new Tree that is " + height + " feet tall");
}
void info() {
System.out.println("Tree is " + height + " feet tall");
}
void info(String s) {
System.out.println(s + ": Tree is " + height + " feet tall");
}
}
public class Overloading {
public static void main(String[] args) {
for (int i = 0; i < 5; i++) {
Tree t = new Tree(i);
t.info();
t.info("overloaded method");
}
new Tree();
}
}
输出:
Creating new Tree that is 0 feet tall
Tree is 0 feet tall
overloaded method: Tree is 0 feet tall
Creating new Tree that is 1 feet tall
Tree is 1 feet tall
overloaded method: Tree is 1 feet tall
Creating new Tree that is 2 feet tall
Tree is 2 feet tall
overloaded method: Tree is 2 feet tall
Creating new Tree that is 3 feet tall
Tree is 3 feet tall
overloaded method: Tree is 3 feet tall
Creating new Tree that is 4 feet tall
Tree is 4 feet tall
overloaded method: Tree is 4 feet tall
Planting a seedling
一个 Tree 对象既可以是一颗树苗,使用无参构造器创建,也可以是一颗在温室中已长大的树,已经有一定高度,这时候,就需要使用有参构造器创建。
// housekeeping/OverloadingOrder.java
// Overloading based on the order of the arguments
public class OverloadingOrder {
static void f(String s, int i) {
System.out.println("String: " + s + ", int: " + i);
}
static void f(int i, String s) {
System.out.println("int: " + i + ", String: " + s);
}
public static void main(String[] args) {
f("String first", 1);
f(99, "Int first");
}
}
输出:
String: String first, int: 1
int: 99, String: Int first
// 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() ,编译器会提示找不到匹配的构造器。当类中没有构造器时,编译器会说"你一定需要构造器,那么让我为你创建一个吧"。但是如果类中有构造器,编译器会说"你已经写了构造器了,所以肯定知道你在做什么,如果你没有创建默认构造器,说明你本来就不需要"。
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, 2)
这是在内部实现的,你不可以直接这么编写代码,编译器不会接受,但能说明到底发生了什么。假设现在在方法内部,你想获得对当前对象的引用。但是,对象引用是被秘密地传达给编译器——并不在参数列表中。方便的是,有一个关键字: this 。this 关键字只能在非静态方法内部使用。当你调用一个对象的方法时,this 生成了一个对象引用。你可以像对待其他引用一样对待这个引用。如果你在一个类的方法里调用该类的其他方法,不要使用 this,直接调用即可,this 自动地应用于其他方法上了。因此你可以像这样:
// 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 才能将自身传递给外部方法。
在构造器中调用构造器
当你在一个类中写了多个构造器,有时你想在一个构造器中调用另一个构造器来避免代码重复。你通过 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,从而避免重复。你经常会在 Java 代码中看到这种用法,同时本书中也会多次出现这种写法。在 printPetalCount() 方法中,编译器不允许你在一个构造器之外的方法里调用构造器。
程序员都了解初始化的重要性,但通常会忽略清理的重要性。毕竟,谁会去清理一个 int 呢?但是使用完一个对象就不管它并非总是安全的。Java 中有垃圾回收器回收无用对象占用的内存。但现在考虑一种特殊情况:你创建的对象不是通过 new 来分配内存的,而垃圾回收器只知道如何释放用 new 创建的对象的内存,所以它不知道如何回收不是 new 分配的内存。为了处理这种情况,Java 允许在类中定义一个名为 finalize() 的方法。
它的工作原理"假定"是这样的:当垃圾回收器准备回收对象的内存时,首先会调用其 finalize() 方法,并在下一轮的垃圾回收动作发生时,才会真正回收对象占用的内存。所以如果你打算使用 finalize() ,就能在垃圾回收时做一些重要的清理工作。finalize() 是一个潜在的编程陷阱,因为一些程序员(尤其是 C++ 程序员)会一开始把它误认为是 C++ 中的析构函数(C++ 在销毁对象时会调用这个函数)。所以有必要明确区分一下:在 C++ 中,对象总是被销毁的(在一个 bug-free 的程序中),而在 Java 中,对象并非总是被垃圾回收,或者换句话说:
// housekeeping/TerminationCondition.java
// Using finalize() to detect a object that
// hasn't been properly cleaned up
import onjava.*;
class Book {
boolean checkedOut = false;
Book(boolean checkOut) {
checkedOut = checkOut;
}
void checkIn() {
checkedOut = false;
}
@Override
protected void finalize() throws Throwable {
if (checkedOut) {
System.out.println("Error: checked out");
}
// Normally, you'll also do this:
// super.finalize(); // Call the base-class version
}
}
public class TerminationCondition {
public static void main(String[] args) {
Book novel = new Book(true);
// Proper cleanup:
novel.checkIn();
// Drop the reference, forget to clean up:
new Book(true);
// Force garbage collection & finalization:
System.gc();
new Nap(1); // One second delay
}
}
输出:
Error: checked out
本例的终结条件是:所有的 Book 对象在被垃圾回收之前必须被登记。但在 main() 方法中,有一本书没有登记。要是没有 finalize() 方法来验证终结条件,将会很难发现这个 bug。
例如,你可以把 C++ 里的堆想象成一个院子,里面每个对象都负责管理自己的地盘。一段时间后,对象可能被销毁,但地盘必须复用。在某些 Java 虚拟机中,堆的实现截然不同:它更像一个传送带,每分配一个新对象,它就向前移动一格。这意味着对象存储空间的分配速度特别快。Java 的"堆指针"只是简单地移动到尚未分配的区域,所以它的效率与 C++ 在栈上分配空间的效率相当。当然实际过程中,在簿记工作方面还有少量额外开销,但是这部分开销比不上查找可用空间开销大。
// 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;
}
// 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;
}
}
// housekeeping/OrderOfInitialization.java
// Demonstrates initialization order
// When the constructor is called to create a
// Window object, you'll see a message:
class Window {
Window(int marker) {
System.out.println("Window(" + marker + ")");
}
}
class House {
Window w1 = new Window(1); // Before constructor
House() {
// Show that we're in the constructor:
System.out.println("House()");
w3 = new Window(33); // Reinitialize w3
}
Window w2 = new Window(2); // After constructor
void f() {
System.out.println("f()");
}
Window w3 = new Window(3); // At end
}
public class OrderOfInitialization {
public static void main(String[] args) {
House h = new House();
h.f(); // Shows that construction is done
}
}
所有的数组(无论是对象数组还是基本类型数组)都有一个固定成员 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));
}
}
// housekeeping/ArrayClassObj.java
// Creating an array of nonprimitive objects
import java.util.*;
public class ArrayClassObj {
public static void main(String[] args) {
Random rand = new Random(47);
Integer[] a = new Integer[rand.nextInt(20)];
System.out.println("length of a = " + a.length);
for (int i = 0; i < a.length; i++) {
a[i] = rand.nextInt(500); // Autoboxing
}
System.out.println(Arrays.toString(a));
}
}
// 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
OverloadingVarargs2.java:14:error:reference to f is ambiguous f('a', 'b');
\^
both method f(float, Character...) in OverloadingVarargs2 and method f(Character...) in OverloadingVarargs2 match 1 error
如果你给这两个方法都添加一个非可变参数,就可以解决问题了:
// housekeeping/OverloadingVarargs3
public class OverloadingVarargs3 {
static void f(float i, Character... args) {
System.out.println("first");
}
static void f(char c, Character... args) {
System.out.println("second");
}
public static void main(String[] args) {
f(1, 'a');
f('a', 'b');
}
}
// housekeeping/Burrito.java
public class Burrito {
Spiciness degree;
public Burrito(Spiciness degree) {
this.degree = degree;
}
public void describe() {
System.out.print("This burrito is ");
switch(degree) {
case NOT:
System.out.println("not spicy at all.");
break;
case MILD:
case MEDIUM:
System.out.println("a little hot.");
break;
case HOT:
case FLAMING:
default:
System.out.println("maybe too hot");
}
}
public static void main(String[] args) {
Burrito plain = new Burrito(Spiciness.NOT),
greenChile = new Burrito(Spiciness.MEDIUM),
jalapeno = new Burrito(Spiciness.HOT);
plain.describe();
greenChile.describe();
jalapeno.describe();
}
}
输出:
This burrito is not spicy at all.
This burrito is a little hot.
This burrito is maybe too hot.
构造器,这种看起来精巧的初始化机制,应该给了你很强的暗示:初始化在编程语言中的重要地位。C++ 的发明者 Bjarne Stroustrup 在设计 C++ 期间,在针对 C 语言的生产效率进行的最初调查中发现,错误的初始化会导致大量编程错误。这些错误很难被发现,同样,不合理的清理也会如此。因为构造器能保证进行正确的初始化和清理(没有正确的构造器调用,编译器就不允许创建对象),所以你就有了完全的控制和安全。
在 C++ 中,析构器很重要,因为用 new 创建的对象必须被明确地销毁。在 Java 中,垃圾回收器会自动地释放所有对象的内存,所以很多时候类似的清理方法就不太需要了(但是当要用到的时候,你得自己动手)。在不需要类似析构器行为的时候,Java 的垃圾回收器极大地简化了编程,并加强了内存管理上的安全性。一些垃圾回收器甚至能清理其他资源,如图形和文件句柄。然而,垃圾回收器确实增加了运行时开销,由于 Java 解释器从一开始就很慢,所以这种开销到底造成多大的影响很难看出来。随着时间的推移,Java 在性能方面提升了很多,但是速度问题仍然是它涉足某些特定编程领域的障碍。