06-Housekeeping
第六章 初始化和清理
“不安全
利用构造器保证初始化
你可能想为每个类创建一个 initialize()
方法,该方法名暗示着在使用类之前需要先调用它。不幸的是,用户必须得记得去调用它。在
以下示例是包含了一个构造器的类:
// 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()
,内存被分配,构造器被调用。构造器保证了对象在你使用它之前进行了正确的初始化。
有一点需要注意,构造器方法名与类名相同,不需要符合首字母小写的编程风格。在
跟其他方法一样,构造器方法也可以传入参数来定义如何创建一个对象。之前的例子稍作修改,使得构造器接收一个参数:
// 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 t = new Tree(12); // 12-foot 树
如果
构造器消除了一类重要的问题,使得代码更易读。例如,在上面的代码块中,你看不到对 initialize()
方法的显式调用,而从概念上来看,initialize()
方法应该与对象的创建分离。在
构造器没有返回值,它是一种特殊的方法。但它和返回类型为 void
的普通方法不同,普通方法可以返回空值,你还能选择让它返回别的类型;而构造器没有返回值,却同时也没有给你选择的余地(new
表达式虽然返回了刚创建的对象的引用,但构造器本身却没有返回任何值
方法重载
任何编程语言中都具备的一项重要特性就是命名。当你创建一个对象时,就会给此对象分配的内存空间命名。方法是行为的命名。你通过名字指代所有的对象,属性和方法。良好命名的系统易于理解和修改。就好比写散文——目的是与读者沟通。
将人类语言细微的差别映射到编程语言中会产生一个问题。通常,相同的词可以表达多种不同的含义——它们被
大多数编程语言(尤其是print()
函数既能打印整型,也能打印浮点型——每个函数名都必须不同。
在
下例展示了如何重载构造器和方法:
// 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
一个
你也许想以多种方式调用 info()
方法。比如,如果你想打印额外的消息,就可以使用 info(String)
方法。如果你无话可说,就可以使用 info()
方法。用两个命名定义完全相同的概念看起来很奇怪,而使用方法重载,你就可以使用一个命名来定义一个概念。
区分重载方法
如果两个方法命名相同,
// 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
两个 f()
方法具有相同的参数,但是参数顺序不同,根据这个就可以区分它们。
重载与基本类型
基本类型可以自动从较小的类型转型为较大的类型。当这与重载结合时,这会令人有点困惑,下面是一个这样的例子:
// housekeeping/PrimitiveOverloading.java
// Promotion of primitives and overloading
public class PrimitiveOverloading {
void f1(char x) {
System.out.print("f1(char)");
}
void f1(byte x) {
System.out.print("f1(byte)");
}
void f1(short x) {
System.out.print("f1(short)");
}
void f1(int x) {
System.out.print("f1(int)");
}
void f1(long x) {
System.out.print("f1(long)");
}
void f1(float x) {
System.out.print("f1(float)");
}
void f1(double x) {
System.out.print("f1(double)");
}
void f2(byte x) {
System.out.print("f2(byte)");
}
void f2(short x) {
System.out.print("f2(short)");
}
void f2(int x) {
System.out.print("f2(int)");
}
void f2(long x) {
System.out.print("f2(long)");
}
void f2(float x) {
System.out.print("f2(float)");
}
void f2(double x) {
System.out.print("f2(double)");
}
void f3(short x) {
System.out.print("f3(short)");
}
void f3(int x) {
System.out.print("f3(int)");
}
void f3(long x) {
System.out.print("f3(long)");
}
void f3(float x) {
System.out.print("f3(float)");
}
void f3(double x) {
System.out.print("f3(double)");
}
void f4(int x) {
System.out.print("f4(int)");
}
void f4(long x) {
System.out.print("f4(long)");
}
void f4(float x) {
System.out.print("f4(float)");
}
void f4(double x) {
System.out.print("f4(double)");
}
void f5(long x) {
System.out.print("f5(long)");
}
void f5(float x) {
System.out.print("f5(float)");
}
void f5(double x) {
System.out.print("f5(double)");
}
void f6(float x) {
System.out.print("f6(float)");
}
void f6(double x) {
System.out.print("f6(double)");
}
void f7(double x) {
System.out.print("f7(double)");
}
void testConstVal() {
System.out.print("5: ");
f1(5);f2(5);f3(5);f4(5);f5(5);f6(5);f7(5);
System.out.println();
}
void testChar() {
char x = 'x';
System.out.print("char: ");
f1(x);f2(x);f3(x);f4(x);f5(x);f6(x);f7(x);
System.out.println();
}
void testByte() {
byte x = 0;
System.out.print("byte: ");
f1(x);f2(x);f3(x);f4(x);f5(x);f6(x);f7(x);
System.out.println();
}
void testShort() {
short x = 0;
System.out.print("short: ");
f1(x);f2(x);f3(x);f4(x);f5(x);f6(x);f7(x);
System.out.println();
}
void testInt() {
int x = 0;
System.out.print("int: ");
f1(x);f2(x);f3(x);f4(x);f5(x);f6(x);f7(x);
System.out.println();
}
void testLong() {
long x = 0;
System.out.print("long: ");
f1(x);f2(x);f3(x);f4(x);f5(x);f6(x);f7(x);
System.out.println();
}
void testFloat() {
float x = 0;
System.out.print("float: ");
f1(x);f2(x);f3(x);f4(x);f5(x);f6(x);f7(x);
System.out.println();
}
void testDouble() {
double x = 0;
System.out.print("double: ");
f1(x);f2(x);f3(x);f4(x);f5(x);f6(x);f7(x);
System.out.println();
}
public static void main(String[] args) {
PrimitiveOverloading p = new PrimitiveOverloading();
p.testConstVal();
p.testChar();
p.testByte();
p.testShort();
p.testInt();
p.testLong();
p.testFloat();
p.testDouble();
}
}
输出:
5: f1(int)f2(int)f3(int)f4(int)f5(long)f6(float)f7(double)
char: f1(char)f2(int)f3(int)f4(int)f5(long)f6(float)f7(double)
byte: f1(byte)f2(byte)f3(short)f4(int)f5(long)f6(float)f7(double)
short: f1(short)f2(short)f3(short)f4(int)f5(long)f6(float)f7(double)
int: f1(int)f2(int)f3(int)f4(int)f5(long)f6(float)f7(double)
long: f1(long)f2(long)f3(long)f4(long)f5(long)f6(float)f7(double)
float: f1(float)f2(float)f3(float)f4(float)f5(float)f6(float)f7(double)
double: f1(double)f2(double)f3(double)f4(double)f5(double)f6(double)f7(double)
如果传入的参数类型大于方法期望接收的参数类型,你必须首先做下转换,如果你不做的话,编译器就会报错。
返回值的重载
经常会有人困惑
void f(){}
int f() {return 1;}
有些情况下,编译器很容易就可以从上下文准确推断出该调用哪个方法,如 int x = f()
。
但是,你可以调用一个方法且忽略返回值。这叫做调用一个函数的副作用,因为你不在乎返回值,只是想利用方法做些事。所以如果你直接调用 f()
,
无参构造器
如前文所说,一个无参构造器就是不接收参数的构造器,用来创建一个
// housekeeping/DefaultConstructor.java
class Bird {}
public class DefaultConstructor {
public static void main(String[] args) {
Bird bird = new Bird(); // 默认的
}
}
表达式 new 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 关键字
对于两个相同类型的对象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()
,那么怎么知道调用的是对象peel()
方法还是对象peel()
方法呢?编译器做了一些底层工作,所以你可以像这样编写代码。peel()
方法中第一个参数隐密地传入了一个指向操作对象的
引用。因此,上述例子中的方法调用像下面这样:
Banana.peel(a, 1)
Banana.peel(b, 2)
这是在内部实现的,你不可以直接这么编写代码,编译器不会接受,但能说明到底发生了什么。假设现在在方法内部,你想获得对当前对象的引用。但是,对象引用是被秘密地传达给编译器——并不在参数列表中。方便的是,有一个关键字
// housekeeping/Apricot.java
public class Apricot {
void pick() {
/* ... */
}
void pit() {
pick();
/* ... */
}
}
在 pit()
方法中,你可以使用 this.pick()
,但是没有必要。编译器自动为你做了这些。
// 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()
通过
// 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
Peeler.peel()
做一些行为。必须使用
在构造器中调用构造器
当你在一个类中写了多个构造器,有时你想在一个构造器中调用另一个构造器来避免代码重复。你通过
通常当你说
// 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.s
表明你指的是成员变量printPetalCount()
方法中,编译器不允许你在一个构造器之外的方法里调用构造器。
static 的含义
记住了
垃圾回收器
程序员都了解初始化的重要性,但通常会忽略清理的重要性。毕竟,谁会去清理一个finalize()
的方法。
它的工作原理finalize()
方法,并在下一轮的垃圾回收动作发生时,才会真正回收对象占用的内存。所以如果你打算使用 finalize()
,就能在垃圾回收时做一些重要的清理工作。finalize()
是一个潜在的编程陷阱,因为一些程序员(尤其是
- 对象可能不被垃圾回收。
- 垃圾回收不等同于析构。
这意味着在你不再需要某个对象之前,如果必须执行某些动作,你得自己去做。finalize()
方法中加入某种擦除功能,那么当垃圾回收发生时,finalize()
方法被调用(不保证一定会发生
也许你会发现,只要程序没有濒临内存用完的那一刻,对象占用的空间就总也得不到释放。如果程序执行结束,而垃圾回收器一直没有释放你创建的任何对象的内存,则当程序退出时,那些资源会全部交还给操作系统。这个策略是恰当的,因为垃圾回收本身也有开销,要是不使用它,那就不用支付这部分开销了。
finalize()
的用途
如果你不能将 finalize()
作为通用的清理方法,那么这个方法有什么用呢?
这引入了要记住的第
- 垃圾回收只与内存有关。
也就是说,使用垃圾回收的唯一原因就是为了回收程序不再使用的内存。所以对于与垃圾回收有关的任何行为来说(尤其是 finalize()
方法
但这是否意味着如果对象中包括其他对象,finalize()
方法就应该明确释放那些对象呢?不是,无论对象是如何创建的,垃圾回收器都会负责释放对象所占用的所有内存。这就将对 finalize()
的需求限制到一种特殊情况,即通过某种创建对象方式之外的方式为对象分配了存储空间。不过,你可能会想,
看起来之所以有 finalize()
方法,是因为在分配内存时可能采用了类似malloc()
函数系列来分配存储空间,而且除非调用 free()
函数,不然存储空间永远得不到释放,造成内存泄露。但是,free()
是finalize()
方法里用本地方法调用它。
读到这里,你可能明白了不会过多使用 finalize()
方法。对,它确实不是进行普通的清理工作的合适场所。那么,普通的清理工作在哪里执行呢?
你必须实施清理
要清理一个对象,用户必须在需要清理的时候调用执行清理动作的方法。这听上去相当直接,但却与finalize()
,所以这也不是一种解决方案
记住,无论是
终结条件
通常,不能指望 finalize()
,你必须创建其他的finalize()
只对大部分程序员很难用到的一些晦涩内存清理里有用了。但是,finalize()
还有一个有趣的用法,它不依赖于每次都要对 finalize()
进行调用,这就是对象终结条件的验证。
当对某个对象不感兴趣时——也就是它将被清理了,这个对象应该处于某种状态,这种状态下它占用的内存可以被安全地释放掉。例如,如果对象代表了一个打开的文件,在对象被垃圾回收之前程序员应该关闭这个文件。只要对象中存在没有被适当清理的部分,程序就存在很隐晦的finalize()
可以用来最终发现这个情况,尽管它并不总是被调用。如果某次 finalize()
的动作使得finalize()
的可能使用方式:
// 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
本例的终结条件是:所有的main()
方法中,有一本书没有登记。要是没有 finalize()
方法来验证终结条件,将会很难发现这个
你可能注意到使用了 @Override
。@
意味着这是一个注解,注解是关于代码的额外信息。在这里,该注解告诉编译器这不是偶然地重定义在每个对象中都存在的 finalize()
方法——程序员知道自己在做什么。编译器确保你没有拼错方法名,而且确保那个方法存在于基类中。注解也是对读者的提醒,@Override
在
注意,System.gc()
用于强制进行终结动作。但是即使不这么做,只要重复地执行程序(假设程序将分配大量的存储空间而导致垃圾回收动作的执行
你应该总是假设基类版本的 finalize()
也要做一些重要的事情,使用Book.finalize()
中看到的那样。本例中,它被注释掉了,因为它需要进行异常处理,而我们到现在还没有涉及到。
垃圾回收器如何工作
如果你以前用过的语言,在堆上分配对象的代价十分高昂,你可能自然会觉得
例如,你可以把
你可能意识到了,
要想理解
在更快的策略中,垃圾回收器并非基于引用计数。它们依据的是:对于任意
在这种方式下,
当对象从一处复制到另一处,所有指向它的引用都必须修正。位于栈或静态存储区的引用可以直接被修正,但可能还有其他指向这些对象的引用,它们在遍历的过程中才能被找到(可以想象成一个表格,将旧地址映射到新地址
这种所谓的
其二在于复制本身。一旦程序进入稳定状态之后,可能只会产生少量垃圾,甚至没有垃圾。尽管如此,复制回收器仍然会将所有内存从一处复制到另一处,这很浪费。为了避免这种状况,一些
“标记
“停止
如前文所述,这里讨论的
成员初始化
void f() {
int i;
i++;
}
你会得到一条错误信息,告诉你
要是类的成员变量是基本类型,情况就会变得有些不同。正如在
// housekeeping/InitialValues.java
// Shows default initial values
public class InitialValues {
boolean t;
char c;
byte b;
short s;
int i;
long l;
float f;
double d;
InitialValues reference;
void printInitialValues() {
System.out.println("Data type Initial value");
System.out.println("boolean " + t);
System.out.println("char[" + c + "]");
System.out.println("byte " + b);
System.out.println("short " + s);
System.out.println("int " + i);
System.out.println("long " + l);
System.out.println("float " + f);
System.out.println("double " + d);
System.out.println("reference " + reference);
}
public static void main(String[] args) {
new InitialValues().printInitialValues();
}
}
输出:
Data type Initial value
boolean false
char[NUL]
byte 0
short 0
int 0
long 0
float 0.0
double 0.0
reference null
可见尽管数据成员的初值没有给出,但它们确实有初值(
在类里定义一个对象引用时,如果不将其初始化,那么引用就会被赋值为
指定初始化
怎么给一个变量赋初值呢?一种很直接的方法是在定义类成员变量的地方为其赋值。以下代码修改了
// 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();
// ...
}
如果没有为
你也可以通过调用某个方法来提供初值:
// 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/Counter.java
public class Counter {
int i;
Counter() {
i = 7;
}
// ...
}
初始化的顺序
在类中变量定义的顺序决定了它们初始化的顺序。即使变量定义散布在方法定义之间,它们仍会在任何方法(包括构造器)被调用之前得到初始化。例如:
// 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
}
}
输出:
Window(1)
Window(2)
Window(3)
House()
Window(33)
f()
在
由输出可见,引用
静态数据的初始化
无论创建多少个对象,静态数据都只占用一份存储区域。
如果在定义时进行初始化,那么静态变量看起来就跟非静态变量一样。
下面例子显示了静态存储区是何时初始化的:
// housekeeping/StaticInitialization.java
// Specifying initial values in a class definition
class Bowl {
Bowl(int marker) {
System.out.println("Bowl(" + marker + ")");
}
void f1(int marker) {
System.out.println("f1(" + marker + ")");
}
}
class Table {
static Bowl bowl1 = new Bowl(1);
Table() {
System.out.println("Table()");
bowl2.f1(1);
}
void f2(int marker) {
System.out.println("f2(" + marker + ")");
}
static Bowl bowl2 = new Bowl(2);
}
class Cupboard {
Bowl bowl3 = new Bowl(3);
static Bowl bowl4 = new Bowl(4);
Cupboard() {
System.out.println("Cupboard()");
bowl4.f1(2);
}
void f3(int marker) {
System.out.println("f3(" + marker + ")");
}
static Bowl bowl5 = new Bowl(5);
}
public class StaticInitialization {
public static void main(String[] args) {
System.out.println("main creating new Cupboard()");
new Cupboard();
System.out.println("main creating new Cupboard()");
new Cupboard();
table.f2(1);
cupboard.f3(1);
}
static Table table = new Table();
static Cupboard cupboard = new Cupboard();
}
输出:
Bowl(1)
Bowl(2)
Table()
f1(1)
Bowl(4)
Bowl(5)
Bowl(3)
Cupboard()
f1(2)
main creating new Cupboard()
Bowl(3)
Cupboard()
f1(2)
main creating new Cupboard()
Bowl(3)
Cupboard()
f1(2)
f2(1)
f3(1)
由输出可见,静态初始化只有在必要时刻才会进行。如果不创建
初始化的顺序先是静态对象(如果它们之前没有被初始化的话main()
方法,必须加载main()
方法之前被加载。实际情况通常并非如此,因为在典型的程序中,不会像本例中所示的那样,将所有事物通过
概括一下创建对象的过程,假设有个名为
- 即使没有显式地使用
static 关键字,构造器实际上也是静态方法。所以,当首次创建 Dog 类型的对象或是首次访问 Dog 类的静态方法或属性时, Java 解释器必须在类路径中查找,以定位Dog.class 。 - 当加载完
Dog.class 后(后面会学到,这将创建一个 Class 对象 ) ,有关静态初始化的所有动作都会执行。因此,静态初始化只会在首次加载Class 对象时初始化一次。 - 当用
new Dog()
创建对象时,首先会在堆上为Dog 对象分配足够的存储空间。 - 分配的存储空间首先会被清零,即会将
Dog 对象中的所有基本类型数据设置为默认值(数字会被置为 0 ,布尔型和字符型也相同) ,引用被置为null 。 - 执行所有出现在字段定义处的初始化动作。
- 执行构造器。你将会在
" 复用" 这一章看到,这可能会牵涉到很多动作,尤其当涉及继承的时候。
显式的静态初始化
你可以将一组静态初始化动作放在类里面一个特殊的
// housekeeping/Spoon.java
public class Spoon {
static int i;
static {
i = 47;
}
}
这看起来像个方法,但实际上它只是一段跟在
// housekeeping/ExplicitStatic.java
// Explicit static initialization with "static" clause
class Cup {
Cup(int marker) {
System.out.println("Cup(" + marker + ")");
}
void f(int marker) {
System.out.println("f(" + marker + ")");
}
}
class Cups {
static Cup cup1;
static Cup cup2;
static {
cup1 = new Cup(1);
cup2 = new Cup(2);
}
Cups() {
System.out.println("Cups()");
}
}
public class ExplicitStatic {
public static void main(String[] args) {
System.out.println("Inside main()");
Cups.cup1.f(99); // [1]
}
// static Cups cups1 = new Cups(); // [2]
// static Cups cups2 = new Cups(); // [2]
}
输出:
Inside main()
Cup(1)
Cup(2)
f(99)
无论是通过标为
非静态实例初始化
// 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
看起来它很像静态代码块,只不过少了
数组初始化
数组是相同类型的、用一个标识符名称封装到一起的一个对象序列或基本类型数据序列。数组是通过方括号下标操作符
int[] a1;
方括号也可放在标识符的后面,两者的含义是一样的:
int a1[];
这种格式符合
编译器不允许指定数组的大小。这又把我们带回有关
int[] a1 = {1, 2, 3, 4, 5};
那么为什么在还没有数组的时候定义一个数组引用呢?
int[] a2;
在
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/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]
数组的大小是通过 Random.nextInt()
随机确定的,这个方法会返回Arrays.toString()
是
本例中,数组也可以在定义的同时进行初始化:
int[] a = new int[rand.nextInt(20)];
如果可能的话,应该尽量这么做。
如果你创建了一个非基本类型的数组,那么你创建的是一个引用数组。以整型的包装类型
// 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));
}
}
输出:
length of a = 18
[55, 193, 361, 461, 429, 368, 200, 22, 207, 288, 128, 51, 89, 309, 278, 498, 361, 20]
这里,即使使用
Integer[] a = new Integer[rand.nextInt(20)];
它只是一个引用数组,直到通过创建新的
a[i] = rand.nextInt(500);
如果忘记了创建对象,但试图使用数组中的空引用,就会在运行时产生异常。
也可以用花括号括起来的列表来初始化数组,有两种形式:
// housekeeping/ArrayInit.java
// Array initialization
import java.util.*;
public class ArrayInit {
public static void main(String[] args) {
Integer[] a = {
1, 2,
3, // Autoboxing
};
Integer[] b = new Integer[] {
1, 2,
3, // Autoboxing
};
System.out.println(Arrays.toString(a));
System.out.println(Arrays.toString(b));
}
}
输出:
[1, 2, 3]
[1, 2, 3]
在这两种形式中,初始化列表的最后一个逗号是可选的(这一特性使维护长列表变得更容易
尽管第一种形式很有用,但是它更加受限,因为它只能用于数组定义处。第二种形式可以用在任何地方,甚至用在方法的内部。例如,你创建了一个main()
方法,如下:
// 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
Other.main()
的参数是在调用处创建的,因此你甚至可以在方法调用处提供可替换的参数。
可变参数列表
你可以以一种类似
// 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
printArray()
的参数是toString()
方法的话,后面会讲这个方法)就是打印类名和对象的地址。
你可能看到像上面这样编写的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()
可以使用printArray()
。显然,编译器会发现这是一个数组,不会执行转换。因此,如果你有一组事物,可以把它们当作列表传递,而如果你已经有了一个数组,该方法会把它们当作可变参数列表来接受。
程序的最后一行表明,可变参数的个数可以为
// housekeeping/OptionalTrailingArguments.java
public class OptionalTrailingArguments {
static void f(int required, String... trailing) {
System.out.print("required: " + required + " ");
for (String s: trailing) {
System.out.print(s + " ");
}
System.out.println();
}
public static void main(String[] args) {
f(1, "one");
f(2, "two", "three");
f(0);
}
}
输出:
required: 1 one
required: 2 two three
required: 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
getClass()
方法属于
然而,可变参数列表与自动装箱可以和谐共处,如下:
// housekeeping/AutoboxingVarargs.java
public class AutoboxingVarargs {
public static void f(Integer... args) {
for (Integer i: args) {
System.out.print(i + " ");
}
System.out.println();
}
public static void main(String[] args) {
f(1, 2);
f(4, 5, 6, 7, 8, 9);
f(10, 11, 12);
}
}
输出:
1 2
4 5 6 7 8 9
10 11 12
注意吗,你可以在单个参数列表中将类型混合在一起,自动装箱机制会有选择地把
可变参数列表使得方法重载更加复杂了,尽管乍看之下似乎足够安全:
// housekeeping/OverloadingVarargs.java
public class OverloadingVarargs {
static void f(Character... args) {
System.out.print("first");
for (Character c: args) {
System.out.print(" " + c);
}
System.out.println();
}
static void f(Integer... args) {
System.out.print("second");
for (Integer i: args) {
System.out.print(" " + i);
}
System.out.println();
}
static void f(Long... args) {
System.out.println("third");
}
public static void main(String[] args) {
f('a', 'b', 'c');
f(1);
f(2, 1);
f(0);
f(0L);
//- f(); // Won's compile -- ambiguous
}
}
输出:
first a b c
second 1
second 2 1
second 0
third
在每种情况下,编译器都会使用自动装箱来匹配重载的方法,然后调用最明确匹配的方法。
但是如果调用不含参数的 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');
}
}
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');
}
}
输出:
first
second
你应该总是在重载方法的一个版本上使用可变参数列表,或者压根不用它。
枚举类型
// housekeeping/Spiciness.java
public enum Spiciness {
NOT, MILD, MEDIUM, HOT, FLAMING
}
这里创建了一个名为
要使用
// housekeeping/SimpleEnumUse.java
public class SimpleEnumUse {
public static void main(String[] args) {
Spiciness howHot = Spiciness.MEDIUM;
System.out.println(howHot);
}
}
输出:
MEDIUM
在你创建toString()
方法,以便你方便地显示某个ordinal()
方法表示某个特定static values()
方法按照
// 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
尽管
// 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.
由于
通常,你可以将
这些介绍对于你理解和使用基本的
本章小结
构造器,这种看起来精巧的初始化机制,应该给了你很强的暗示:初始化在编程语言中的重要地位。
在
由于要保证所有对象被创建,实际上构造器比这里讨论得更加复杂。特别是当通过组合或继承创建新类的时候,这种保证仍然成立,并且需要一些额外的语法来支持。在后面的章节中,你会学习组合,继承以及它们如何影响构造器。