Java编程思想学习(五)----第5章:初始化与清理

随着计算机革命的发展,“不安全”的编程方式已逐渐成为编程代价高昂的主因之一。

C++引入了构造嚣(constructor)的概念,这是一个在创建对象时被自动调用的特殊方法。Java中也采用了构造器,并额外提供了“垃圾回收器”。对于不再使用的内存资源,垃圾回收器能自动将其释放。

5.1 用构造器确保初始化

//:initialization/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();
}
}
}/*Output
Rock Rock Rock Rock Rock Rock Rock Rock Rock Rock
*///:~

  在创建对象时:new Rock();将会为对象分配存储空间,并调用相应的构造器。这就确保了在你能操作对象之前,它已经被恰当地初始化了。

  请注意,由于构造器的名称必须与类名完全相同,所以“每个方法首字母小写”的编码风格并不适用于构造器。

//:initialization/SimpleConstructor2.java
class Rock2
{
Rock2(int i)
{
System.out.print("Rock2 "+i+" ");
}
}
public class SimpleConstructor2
{
public static void main(String[] args)
{
for (int i = 0; i < 8; i++)
{
new Rock2(i);
}
}
}/*Output
Rock2 0 Rock2 1 Rock2 2 Rock2 3 Rock2 4 Rock2 5 Rock2 6 Rock2 7
*///:~

  有了构造器形式参数,就可以在初始化对象时提供实际参数。例知,假设类Tree有一个构造器,它接受一个整型变量来表示树的高度,就可以这样创建一个Tree对象:

Tree t = new Tree(12); //12-foot tree

  如果Tree(int)是Tree类中唯一的构造器,那么编译器将不会允许你以其他任何方式创建Tree对象。

  构造器有助于减少错误,并使代码更易于阅读。从概念上讲,“初始化”与“创建”是彼此独立的,然而在上面的代码中,你却找不到对initialize()方法的明确调用。在Java中,“初始化”和“创建”捆绑在一起,两者不能分离。

  • 练习1:(1)创建一个类,它包含一个未初始化的String引用。验证该引用被Java初始化成了null。
  • 练习2:(2)创建一个类,它包含一个在定义时就被初始化了的String域,以及另一个通过构造器初始化的String域。这两种方式有何差异?

5.2 方法重载

  当创建一个对象时,也就给此对象分配到的存储空间取了一个名字。所谓方法则是给某个动作取的名字。

  大多数程序设计语言(尤其是C)要求为每个方法(在这些语言中经常称为函数)都提供一个独一无二的标识符。

  所以绝不能用名为print()的函数显示了整数之后,又用一个名为print()的函数显示浮点数——每个函数都要有唯一的名称。

  构造器是强制重载方法名的另一个原因。既然构造器的名字已经由类名所决定,就只能有一个构造器名。那么要想用多种方式创建一个对象该怎么办呢?假设你要创建一个类,既可以用标准方式进行初始化,也可以从文件里读取信息来初始化。这就需要两个构造器:一个默认构造器,另一个取字符串作为形式参数——该字符串表示初始化对象所需的文件名称。由于都是构造器,所以它们必须有相同的名字,即类名。为了让方法名相同而形式参数不同的构造器同时存在,必须用到方法重载。

下面这个例子同时示范了重载的构造器和重载的方法:

class Tree
{
int height; public Tree()
{
height = 0;
System.out.println("种植树苗");
} public Tree(int initialHeight)
{
height = initialHeight;
System.out.println("新创建了一颗 " + height + " 高的树");
} void info()
{
System.out.println("本树高为 " + height);
} void info(String s)
{
System.out.println(s + ":本树高为 " + height);
}
} public class Overloading
{
public static void main(String[] args)
{
for (int i = 0; i < 3; i++)
{
Tree t = new Tree(i);
t.info();
t.info("重载的方法");
}
//重载构造器
new Tree();
}
}/*Output
新创建了一颗 0 高的树
本树高为 0
重载的方法:本树高为 0 新创建了一颗 1 高的树
本树高为 1
重载的方法:本树高为 1 新创建了一颗 2 高的树
本树高为 2
重载的方法:本树高为 2 种植树苗
*///:~

5.2.1 区分重载方法

规则很简单:每个重载的方法都必须有一个独一无二的参数类型列表。

甚至参数顺序的不同也足以区分两个方法。不过,一般情况下别这么做,因为这会使代码 
难以维护:

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", 11);
f(99, "int first");
}
}/*Output
String: String first, int: 11
int: 99, String: int first
*///:~

上例中两个f()方法虽然声明了相同的参数,但顺序不同,因此得以区分。

5.2.2 涉及基本类型的重载

  基本类型能从一个“较小一的类型自动提升至一个“较大”的类型,此过程一旦牵涉到重载,可能会造成一些混淆。以下例子说明了将基本类型传递给重载方法时发生的情况:

public class PrimitiveOverloading
{
//*******************f1***************//
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) ");
} //********************f2**************//
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) ");
} //*******************f3***************//
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) ");
} //********************f4***************//
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) ");
} //********************f5**************//
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) ");
}
//********************f6**************// void f6(float x)
{
System.out.print("f6(float) ");
} void f6(double x)
{
System.out.print("f6(double) ");
} //********************f7**************//
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();
}
}/*Output
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)
*///:~

  你会发现常数值5被当作int值处理,所以如果有某个重载方法接受int型参数,它就会被调用。至于其他情况,如果传入的数据类型(实际参数类型)小于方法中声明的形式参数类型,实际数据类型就会被提升。 char型略有不同,如果无法找到恰好接受char参数的方法,就会把char直接提升至int型。

  如果传入的实际参数大于重载方法声明的形式参数,会出现什么情况呢?修改上述程序,就能得到答案。

public class Demotion()
{ //*******************f1***************//
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) ");
} //********************f2**************//
void f2(char x)
{
System.out.print("f2(char) ");
} 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) ");
} //*******************f3***************//
void f3(char x)
{
System.out.print("f3(char) ");
} void f3(byte x)
{
System.out.print("f3(byte) ");
} 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("f4(long) ");
} //********************f4***************//
void f4(char x)
{
System.out.print("f4(char) ");
} void f4(byte x)
{
System.out.print("f4(byte) ");
} void f4(short x)
{
System.out.print("f4(short) ");
} void f4(int x)
{
System.out.print("f4(int) ");
} //********************f5**************//
void f5(char x)
{
System.out.print("f5(char) ");
} void f5(byte x)
{
System.out.print("f5(byte) ");
} void f5(short x)
{
System.out.print("f5(short) ");
} //********************f6**************// void f6(char x)
{
System.out.print("f6(char) ");
} void f6(byte x)
{
System.out.print("f6(byte) ");
} //********************f7**************//
void f7(char x)
{
System.out.print("f7(char) ");
} void testDouble()
{
double x = 0;
System.out.print("double argument: ");
f1(x);
f2((float) x);
f3((long) x);
f4((int) x);
f5((short) x);
f6((byte) x);
f7((char) x);
System.out.println();
} public static void main(String[] args)
{
Demotion d = new Demotion();
d.testDouble();
}
}/*Output
double argument: f1(double) f2(float) f4(long) f4(int) f5(short) f6(byte) f7(char)
*///:~

  在这里,方法接受较小的基本类型作为参数。如果传入的实际参数较大,就得通过类型转换来执行窄化转换。如果不这样做,编译器就会报错。

5.2.3 以返回值区分重载方法

void f()
{
} int f()
{
return 1;
}
f();

  此时Java如何才能判断该调用哪一个f()呢?别人该如何理解这种代码呢?因此,根据方法的返回值来区分重载方法是行不通的。

5.3 默认构造器

  如果你写的类中没有构造器,则编译器会自动帮你创建一个默认构造器。

//: initialization/DefaultConstructor.java
class Bird
{
} public class DefaultConstructor
{
public static void main (String[] args)
{
Brid b = new Bird();//Default
}
}///:~

  表达式 new Bird()行创建了一个新对象,并调用其默认构造器——即使你没有明确定义它。没有它的话,就没有方法可调用,就无法创建对象。但是,如果已经定义了一个构造器(无论是否有参数),编译器就不会帮你自动创建默认构造器:

//: initialization/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()编译器就会报错:没有找到匹配的构造器。

  • 练习3:(1)创建一个带默认构造器(即无参构造器)的类,在构造器中打印一条消息。为这个类创建一个对象。
  • 练习4:(1)为前一个练习中的类添加一个重载构造器,令其接受一个字符串参数,并在构造器中把你自己的消息和接收的参数一起打印出来。
  • 练习5:(2)创建一个名为Dog的类,它具有重载的bark()方法。此方法应根据不同的基本数据类型进行重载,并根据被调用的版本,打印出不同类型的狗吠(barking)、咆哮(howling)等信息。编写main()来调用所有不同版本的方法。
  • 练习6:(1)修改前一个练习的程序,让两个重载方法各自接受两个类型的不同的参数,但二者顺序相反。验证其是否工作。
  • 练习7:(1)创建一个没有构造器的类,并在main()中创建其对象,用以验证编译器是否真的自动加入了默认构造器。

5.4 this关键字

  如果有同一类型的两个对象,分别是a和b。你可能想知道,如何才能让这两个对象都能调用peel()方法呢:

public class BananaPeel
{
public static void main(String[] args)I
{
Banana a = new Banana(),b = new Banana();
a.peel(1);
b.peel(2);
}
}///:~

  如果只有一个peel()方法,它如何知道是被a还是被b所调用的呢?

  它暗自把“所操作对象的引用”作为第一个参数传递给peel()。所以上述两个方法的调用就变成了这样:

Banana.peel(a,1);
Banana.peel(b,2);

  this关键字只能在方法内部使用,表示对“调用方法的那个对象”的引用。this的用法和其他对象引用并无不同。但要注意,如果在方法内部调用同一个类的另一个方法,就不必使用this,直接调用即可。当前方法中的this引用会自动应用于同一类中的其他方法。所以可以这样写代码:

public class Apricot
{
void pick ()
{
/*...*/
} void pit()
{
pick();
/*...*/
}
}///:~

  在pit()内部,你可以写this.pick(),但无此必要。编译器能帮你自动添加。只有当需要明确指出对当前对象的引用时,才需要使用this关键字。例如,当需要返回对当前对象的引用时,就常常在return语句里这样写:

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 l = new Leaf();
l.increment().increment().increment().print();
}
}/*Output
i = 3
///:~

  由于increment()通过this关键字返回了对当前对象的引用,所以很容易在一条语句里对同一个对象执行多次操作。

  this关键字对于将当前对象传递给其他方法也很有用:

class Person
{
public void eat(Apple apple)
{
Apple peeled = apple.getPeeled();
System.out.println("Yummy");
}
} class Peeler
{
static Apple peel(Apple apple)
{
//...remove peel
return apple;
}
} class Apple
{
Apple getPeeled()
{
return Peeler.peel(this);
}
} public class PassingThis
{
public static void main(String[] args)
{
new Person().eat(new Apple());
}
}/*Output
Yummy
*///:~

  Apple需要调用Peeler.peel()方法,它是一个外部的工具方法,将执行由于某种原因而必须放在Apple外部的操作(也许是因为该外部方法要应用于许多不同的类,而你却不想重复这些代码)。为了将其自身传递给外部方法,Apple必须使用this关键字。

  • 练习8:(1)编写具有两个方法的类,在第一个方法内调用第二个方法两次:第一次调用时不使用this关键字,第二次调用时使用this关键字——这里只是为了验证它是起作用的,你不应该在实践中使用这种方式。

5.4.1 在构造器中调用构造器

  可能为一个类写了多个构造器,有时可能想在一个构造器中调用另一个构造器,以避免重复代码。可用this关键字做到达一点。

  通常写this的时候,都是指“这个对象”或者“当前对象”,而且它本身表示对当前对象的引用。在构造器中,如果为this添加了参数列表,那么就有了不同的含义。这将产生对符合此参数列表的某个构造器的明确调用,这样,调用其他构造器就有了直接的途径:

class Flower
{
int petalCount = 0;
String s = "initial value"; Flower(int petals)
{
petalCount = petals;
System.out.println("构造器 w/ int arg only,petalCount =" + petalCount);
} Flower(String ss)
{
s = ss;
System.out.println("构造器 w/ String arg only,s =" + s);
} Flower(String s, int petals)
{
this(petals);
//! this(s);//不能调用两次构造器
this.s = s;
System.out.println("String and int arg");
} Flower()
{
this("hi", 47);
System.out.println("默认构造器(无参)");
} void printPetalCount()
{
//! this(11); //不要在非构造方法里使用
System.out.println("petalCount=" + petalCount + " s=" + s);
} public static void main(String[] args)
{
Flower f = new Flower();
f.printPetalCount();
}
}/*Output
构造器 w/ int arg only,petalCount =47
String and int arg
默认构造器(无参)
petalCount=47 s=hi
*///:~

  构造器Flower(String s,int petals)表明:尽管可以用this调用一个构造器,但却不能调用两个。此外,必须将构造器调用置于最起始处,否则编译器会报错。 这个例子也展示了this的另一种用法。由于参数s的名称和数据成员s的名字相同,所以会产生歧义。使用this.s来代表数据成员就能解决这个问题。在Java程序代码中经常出现这种写法,本书中也常这么写。 printPetalCount()方法表明,除构造器之外,编译器禁止在其他任何方法中调用构造器。

  • 练习9:(1)编写具有两个(重载)构造器的类,并在第一个构造器中通过this调用第二个构造器。

5.4.2 static的含义

  在static方法的内部不能调用非静态方法,反过来倒是可以的。(只会创建一次)

5.5 清理:终结处理和垃圾回收

  Java有垃圾回收器负责回收无用对象占据的内存资源。但也有特殊情况:假定你的对象(并非使用new)获得了一块“特殊”的内存区域,由于垃圾回收器只知道释放那些经由new分配的内存,所以它不知道该如何释放该对象的这块“特殊”内存。为了应对这种情况,Java允许在类中定义一个名为finalize()的方法。它的工作原理“假定”是这样的:一旦垃圾回收器准备好释放对象占用的存储空间,将首先调用其finalize()方法,并且在下一次垃圾回收动作发生时,才会真正回收对象占用的内存。所以要是你打算用finalize(),就能在垃圾回收时刻做一些重要的清理工作。

  在C++中,对象一定会被销毁(如果程序中没有缺陷的话);而Java里的对象却并非总是被垃圾回收。或者换句话说:

  1. 对象可能不被垃圾回收。
  2. 垃圾回收并不等于“析构”。
  3. 垃圾回收只与内存有关。

  Java并未提供“析构函数”或相似的概念,要做类似的清理工作,必须自己动手创建一个执行清理工作的普通方法。

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

5.5.1 finalize()的用途何在

  读者或许已经明白了不要过多地使用finalize()的道理了 。对,它确实不是进行普通的清理工作的合适场所。那么,普通的清理工作应该在哪里执行呢?

5.5.2 你必须实施清理

  Java不允许创建局部对象,必须使用new创建对象。在Java中,也没有用于释放对象的delete,因为垃圾回收器会帮助你释放存储空间。甚至可以肤浅地认为,正是由于垃圾收集机制的存在,使得Java没有析构函数。

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

5.5.3 终结条件

  以下是个简单的例子,示范了fifinalize()可能的使用方式:

 class Book
{
boolean checkedOut = false; Book(boolean checkedOut)
{
this.checkedOut = checkedOut;
} void checkIn()
{
checkedOut = false;
} @Override
protected void finalize() throws Throwable
{
if (checkedOut)
System.out.println("ERROR: checked out");
//通常情况下,你也会这么做:
super.finalize(); //调用基类方法
} public static void main(String[] args)
{
Book novel = new Book(true);
//适当的清理
novel.checkIn();
//作为参考,故意忘了清理
new Book(true);
//垃圾收集和终结
System.gc();
}
}/*Output
ERROR: checked out
*///:~

  System.gc()用于强制进行终结动作。即使不这么做,通过重复地执行程序(假设程序将分配大量的存储空间而导致垃圾回收动作的执行),最终也能找出错误的book对象。

  • 练习10:(2)编写具有finalize()方法的类,并在方法中打印消息。在main()中为该类创建一个对象。试解释这个程序的行为。
  • 练习11:(4)修改前一个练习的程序,让你的finalize()总会被调用。
  • 练习12:(4)编写名为Tank的类,此类的状态可以是“满的”或“空的”。其终结条件是:对象被清理时必须处于空状态。请编写finalize()以检验终结条件是否成立。在main()中测试Tank可能发生的几种使用方式。

5.5.4 垃圾回收器如何工作

  在以前所用过的程序语言中,在堆上分配对象的代价十分高昂,因此读者自然会觉得Java中所有对象(基本类型除外),都在堆上分配的方式也非常高昂。然而,垃圾回收器对于提高对象的创建速度,却具有明显的效果。听起来很奇怪——存储空间的释放竟然会影响存储空间的分配,但这确实是某些Java虚拟机的工作方式。这也意味着,Java从堆分配空间的速度,可以和其他语言从堆栈上分配空间的速度相媲美。

  打个比方,你可以把C++里的堆想像成一个院子,里面每个对象都负责管理自己的地盘。一段时间以后,对象可能被销毁,但地盘必须加以重用。在某些Java虚拟机中,堆的实现截然不同:它更像一个传送带,每分配一个新对象,它就往前移动一格。这意味着对象存储空间的分配速度非常快。Java的“堆指针”只是简单地移动到尚未分配的区域,其效率比得上C++在堆栈上分配空间的效率。当然,实际过程中在簿记工作方面还有少量额外开销,但比不上查找可用空间开销大。

  读者也许已经意识到了,Java中的堆未必完全像传送带那样工作。要真是那样的话,势必会导致频繁的内存页面调度——将其移进移出硬盘,因此会显得需要拥有比实际需要更多的内存。页面调度会显著地影响性能,最终,在创建了足够多的对象之后,内存资源将耗尽。其中的秘密在于垃圾回收器的介入。当它工作时,将一面回收空间,一面使堆中的对象紧凑排列,这样“堆指针”就可以很容易移动到更靠近传送带的开始处,也就尽量避免了页面错误。通过垃圾回收器对对象重新排列,实现了一种高速的、有无限空间可供分配的堆模型。

  要想更好地理解Java中的垃圾回收,先了解其他系统中的垃圾回收机制将会很有帮助。引用记数是一种简单但速度很慢的垃圾回收技术。每个对象都含有一个引用记数器,当有引用连接至对象时,引用计数加1。当引用离开作用域或被置为null时,引用计数减1。虽然管理引用记数的开销不大,但这项开销在整个程序生命周期中将持续发生。垃圾回收器会在含有全部对象的列表上遍历,当发现某个对象的引用计数为0时,就释放其占用的空间(但是,引用记数模式经常会在记数值变为0时立即释放对象)。这种方法有个缺陷,如果对象之间存在循环引用,可能会出现“对象应该被回收,但引用计数却不为零”的情况。对垃圾回收器而言,定位这样的交互自引用的对象组所需的工作量极大。引用记数常用来说明垃圾收集的工作方式,但似乎从未被应用于任何一种Java虚拟机实现中。

  在一些更快的模式中,垃圾回收器并非基于引用记数技术。它们依据的思想是:对任何“活”的对象,一定能最终追溯到其存活在堆栈或静态存储区之中的引用。这个引用链条可能会穿过数个对象层次。由此,如果从堆栈和静态存储区开始,遍历所有的引用,就能找到所有“活”的对象。对于发现的每个引用,必须追踪它所引用的对象,然后是此对象包含的所有引用,如此反复进行,直到“根源于堆栈和静态存储区的引用”所形成的网络全部被访问为止。你所访问过的对象必须都是“活”的。注意,这就解决了“交互自引用的对象组”的问题——这种现象根本不会被发现,因此也就被自动回收了。

  在这种方式下,Java虚拟机将采用一种自适应的垃圾回收技术。至于如何处理找到的存活对象,取决于不同的Java虚拟机实现。有一种做法名为停止一复制(stop-and-copy)。显然这意味着,先暂停程序的运行(所以它不属于后台回收模式),然后将所有存活的对象从当前堆复制到另一个堆,没有被复制的全部都是垃圾。当对象被复制到新堆时,它们是一个挨着一个的,所以新堆保持紧凑排列,然后就可以按前述方法简单、直接地分配新空间了。

  当把对象从一处搬到另一处时,所有指向它的那些引用都必须修正。位于堆或静态存储区的引用可以直接被修正,但可能还有其他指向这些对象的引用,它们在遍历的过程中才能被找到(可以想像成有个表格,将旧地址映射至新地址)。

  对于这种所谓的“复制式回收器”而言,效率会降低,这有两个原因。首先,得有两个堆,然后得在这两个分离的堆之间来回捣腾,从而得维护比实际需要多一倍的空间。某些Java虚拟机对此问题的处理方式是,按需从堆中分配几块较大的内存,复制动作发生在这些大块内存之间。

  第二个问题在于复制。程序进入稳定状态之后,可能只会产生少量垃圾,甚至没有垃圾。尽管如此,复制式回收器仍然会将所有内存自一处复制到另一处,这很浪费。为了避免这种情形,一些Java虚拟机会进行检查:要是没有新垃圾产生,就会转换到另一种工作模式(即“自适应”)。这种模式称为标记一清扫(mark-and-sweep),Sun公司早期版本的Java虚拟机使用了这种技术。对一般用途而言,“标记一清扫”方式速度相当慢,但是当你知道只会产生少量垃圾甚至不会产生垃圾时,它的速度就很快了。

  “标记一清扫”所依据的思路同样是从堆栈和静态存储区出发,遍历所有的引用,进而找出所有存活的对象。每当它找到一个存活对象,就会给对象设一个标记,这个过程中不会回收任何对象。只有全部标记工作完成的时候,清理动作才会开始。在清理过程中,没有标记的对象将被释放,不会发生任何复制动作。所以剩下的堆空间是不连续的,垃圾回收器要是希望得到连续空间的话,就得重新整理剩下的对象。

  “停止一复制”的意思是这种垃圾回收动作不是在后台进行的。相反,垃圾回收动作发生的同时,程序将会被暂停。在Sun公司的文档中会发现,许多参考文献将垃圾回收视为低优先级的后台进程,但事实上垃圾回收器在Sun公司早期版本的Java虚拟机中并非以这种方式实现的。当可用内存数量较低时,Sun版本的垃圾回收器会暂停运行程序,同样,“标记一清扫”工作也必须在程序暂停的情况下才能进行。

  如前文所述,在这里所讨论的Java虚拟机中,内存分配以较大的“块”为单位。如果对象较大,它会占用单独的块。严格来说,“停止一复制”要求在释放旧有对象之前,必须先把所有存活对象从旧堆复制到新堆,这将导致大量内存复制行为。有了块之后,垃圾回收器在回收的时候就可以往废弃的块里拷贝对象了。每个块都用相应的代数(generation count)来记录它是否还存活。通常,如果块在某处被引用,其代数会增加。垃圾回收器将对上次回收动作之后新分配的块进行整理。这对处理大量短命的临时对象很有帮助。垃圾回收器会定期进行完整的清理动作——大型对象仍然不会被复制(只是其代数会增加),内含小型对象的那些块则被复制并整理。Java虚拟机会进行监视,如果所有对象都很稳定,垃圾回收器的效率降低的话,就切换到 “标记一清扫”方式;同样,Java虚拟机会跟踪“标记一清扫”的效果,要是堆空间出现很多碎片,就会切换回“停止一复制”方式。这就是“自适应”技术,你可以给它个罗嗦的称呼:“自适应的、分代的、停止一复制、标记一清扫”式垃圾回收器。

  Java虚拟机中有许多附加技术用以提升速度。尤其是与加载器操作有关的,被称为“即时”(Just-In-Time,JIT)编译器的技术。这种技术可以把程序全部或部分翻译成本地机器码(这本来是Java虚拟机的工作),程序运行速度因此得以提升。当需要装载某个类(通常是在为该类创建第一个对象)时,编译器会先找到其.class文件,然后将该类的字节码装入内存。此时,有两种方案可供选择。一种是就让即时编译器编译所有代码。但这种做法有两个缺陷:这种加载动作散落在整个程序生命周期内,累加起来要花更多时间.并且会增加可执行代码的长度(字节码要比即时编译器展开后的本地机器码小很多),这将导致页面调度,从而降低程序速度。另一种做法称为惰性评估(lazy evaluation),意思是即时编译器只在必要的时候才编译代码。这样,从不会被执行的代码也许就压根不会被JIT所编译。新版JDK中的Java HotSpot技术就采用了类似方法,代码每次被执行的时候都会做一些优化,所以执行的次数越多,它的速度就越快。

5.6 成员初始化

  Java尽力保证:所有变量在使用前都能得到恰当的初始化。对于方法的局部变量,Java以编译时错误的形式来贯彻这种保证。所以如果写成:

void f()
{
int i;
i++;//错误,变量i可能没有被初始化
}

  就会得到一条出错消息,告诉你i可能尚未初始化。当然,编译器也可以为i赋一个默认值,但是未初始化的局部变量更有可能是程序员的疏忽,所以采用默认值反而会掩盖这种失误。因此强制程序员提供一个初始值,往往能够帮助找出程序里的缺陷。

  要是类的数据成员(即字段)是基本类型,情况就会变得有些不同。正如在“一切都是对象”一章中所看到的,类的每个基本类型数据成员保证都会有一个初始值。下面的程序可以验证这类情况,并显示它们的值:

public class InitialValues
{
boolean t;
char c;
byte b;
short s;
int i;
long l;
float f;
double d;
InitialValues iv; void printInitialValues()
{
print("Data type Initial Value");
print("boolean " + t);
print("char " + c);
print("byte " + b);
print("short " + s);
print("int " + i);
print("long " + l);
print("float " + f);
print("double " + d);
print("iv " + iv);
} public static void main(String[] args)
{
new InitialValues().printInitialValues();
}
}/*Output
Data type Initial Value
boolean false
char
byte 0
short 0
int 0
long 0
float 0.0
double 0.0
iv null
*///:~

  可见尽管数据成员的初值没有给出,但它们确实有初值(char值为0,所以显示为空白)。这样至少不会冒“未初始化变量”的风险了。

  在类里定义一个对象引用时,如果不将其初始化,此引用就会获得一个特殊值null。

5.6.1 指定初始化

  如果想为某个变量赋初值,该怎么做呢?有一种很直接的办法,就是在定义类成员变量的地方为其赋值(注意在C++里不能这样做,尽管C++的新手们总想这样做)。以下代码片段修改了InitialValues类成员变量的定义,直接提供了初值。

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

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

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

  如果没有为d指定初始值就尝试使用它,就会出现运行时错误,告诉你产生了一个异常(这在第12章中详述)。

  甚至可以通过调用某个方法来提供初值:

public class MethodInit
{
int i = f();
int f()
{
return 11;
}
}

  这个方法也可以带有参数,但这些参数必须是已经被初始化了的。因此,可以这样写:

public class MethodInit2
{
int i = f();
int j = g(i);
int f()
{
return 11;
}
int g(int n)
{
return n*10;
}
}

  但像下面这样写就不对了:

public class MethodInit3
{
//! int j = g(i); //非法向前引用
int i = f();
int f()
{
return 11;
}
int g(int n)
{
return n*10;
}
}

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

  这种初始化方法既简单又直观。但有个限制:类InitialValues的每个对象都会具有相同的初值。有时,这正是所希望的,但有时却需要更大的灵活性。

5.7 构造器初始化

  可以用构造器来进行初始化。在运行时刻,可以调用方法或执行某些动作来确定初值,这为编程带来了更大的灵活性。但要牢记:无法阻止自动初始化的进行,它将在构造器被调用之前发生。因此,假如使用下述代码:

public class Counter
{
int i;
Counter()
{
i = 7;
}
}

  那么i首先会被置0,然后变成7。对于所有基本类型和对象引用,包括在定义时已经指定初值的变量,这种情况都是成立的.因此,编译器不会强制你一定要在构造器的某个地方或在使用它们之前对元素进行初始化——因为初始化早已得到了保证。

5.7.1 初始化顺序

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

class Window
{
Window(int marker)
{
print("Window(" + marker + ")");
}
} class House
{
Window w1 = new Window(1);//在构造器之前 House()
{
print("House()");
w3 = new Window(33);//重新赋值
} Window w2 = new Window(2);//在构造器之后 void f()
{
print("f()");
} Window w3 = new Window(3);//在末尾
}
public class OrderOfInitialization
{
public static void main(String[] args)
{
new House().f();
}
}/*Output
Window(1)
Window(2)
Window(3)
House()
Window(33)
f()
*///:~

  在House类中,故意把几个Window对象的定义散布到各处,以证明它们全都会在调用构造器或其他方法之前得到初始化。此外,w3在构造器内再次被初始化。

  由输出可见,w3这个引用会被初始化两次:一次在调用构造器前,一次在调用期间(第一次引用的对象将被丢弃,并作为垃圾回收)。试想,如果定义了一个重载构造器,它没有初始化w3;同时在w3的定义里也没有指定默认值,那会产生什么后果呢?所以尽管这种方法似乎效率不高,但它的确能使初始化得到保证。

5.7.2 静态数据的初始化

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

  如果想在定义处进行初始化,采取的方法与非静态数据没什么不同。

  要想了解静态存储区域是何时初始化的,就请看下面这个例子:

class Bowl
{
Bowl(int marker)
{
print("Bowl(" + marker + ")");
} void f1(int marker)
{
print("f1(" + marker + ")");
}
} class Table
{
static Bowl b1 = new Bowl(1); Table()
{
print("Table()");
b2.f1(1);
} void f2(int marker)
{
print("f2(" + marker + ")");
} static Bowl b2 = new Bowl(2);
} class Cupboard
{
Bowl b3 = new Bowl(3);
static Bowl b4 = new Bowl(4); Cupboard()
{
print("Cupboard()");
b4.f1(2);
} void f3(int marker)
{
print("f3(" + marker + ")");
} static Bowl b5 = new Bowl(5);
}
public class Staticlnitialization
{
public static void main(String[] args)
{
print("在main()中新建一个Cupboard");
new Cupboard(); print("在main()中再建一个Cupboard");
new Cupboard(); table.f2(1);
cupboard.f3(1);
} static Table table = new Table();
static Cupboard cupboard = new Cupboard(); }/*Output
Bowl(1)
Bowl(2)
Table()
f1(1) Bowl(4)
Bowl(5)
Bowl(3)
Cupboard()
f1(2) 在main()中新建一个Cupboard
Bowl(3)
Cupboard()
f1(2) 在main()中再建一个Cupboard
Bowl(3)
Cupboard()
f1(2) f2(1)
f3(1)
*///:~

  Bowl类使得看到类的创建,而Table类和Cupboard类在它们的类定义中加入了Bowl类型的静态数据成员。注意,在静态数据成员定义之前,Cupboard类先定义了一个Bowl类型的非静态数据成员b3。

  由输出可见,静态初始化只有在必要时刻才会进行。如果不创建Table对象,也不引用Table.b1Table.b2,那么静态的Bowl b1b2永远都不会被创建。只有在第一个Table对象被创建(或者第一次访问静态数据)的时候,它们才会被初始化。此后,静态对象不会再次被初始化。

  初始化的顺序是先静态对象(如果它们尚未因前面的对象创建过程而被初始化),而后是“非静态”对象。从输出结果中可以观察到这一点。要执行main()(静态方法),必须加载Staticlnitialization类,然后其静态域table和cupboard被初始化,这将导致它们对应的类也被加载,并且由于它们也都包含静态的Bowl对象,因此Bowl随后也被加载。这样,在这个特殊的程序中的所有类在main()开始之前就都被加载了。实际情况通常并非如此,因为在典型的程序中,不会像在本例中所做的那样,将所有的事物都通过static联系起来。

总结一下对象的创建过程,假设有个名为Dog的类:

  1. 即使没有显式地使用static关键字,构造器实际上也是静态方法。因此,当首次创建类型为Dog的对象时(构造器可以看成静态方法),或者Dog类的静态方法/静态域首次被访问时,Java解释器必须查找类路径,以定位Dog.class文件。
  2. 然后载入Dog.class(后面会学到,这将创建一个Class对象),有关静态初始化的所有动作都会执行。因此,静态初始化只在Class对象首次加载的时候进行一次。
  3. 当用new Dog()创建对象的时候,首先将在堆上为Dog对象分配足够的存储空间。
  4. 这块存储空间会被清零,这就自动地将Dog对象中的所有基本类型数据都设置成了默认值(对数字来说就是0,对布尔型和字符型也相同),而引用则被设置成了null。
  5. 执行所有出现于字段定义处的初始化动作。
  6. 执行构造器。正如将在第7章所看到的,这可能会牵涉到很多动作,尤其是涉及继承的时候。

5.7.3 显式的静态初始化

  Java允许将多个静态初始化动作组织成一个特殊的“静态子句”(有时也叫做“静态块”)。就像下面这样:

public class Spoon
{
static int i;
static
{
i = 47;
}
}

  尽管上面的代码看起来像个方法,但它实际只是一段跟在static关键字后面的代码。与其他静态初始化动作一样,这段代码仅执行一次:当首次生成这个类的一个对象时,或者首次访问属于那个类的静态数据成员时(即便从未生成过那个类的对象)。例如:

class Cup
{
Cup(int marker)
{
print("Cup(" + marker + ")");
} void f(int marker)
{
print("f(" + marker + ")");
}
} class Cups
{
static Cup cup1;
static Cup cup2; static
{
cup1 = new Cup(1);
cup2 = new Cup(2);
} Cups()
{
print("Cups()");
}
} public class ExplicitStatic
{
public static void main(String[] args)
{
print("Inside main()");
Cups.cup1.f(99); //(1)
} static Cups cups1 = new Cups(); //(2)
static Cups cups2 = new Cups(); //(2)
}/*Output
Inside main()
Cup(1)
Cup(2)
f(99)
*///:~

  无论是通过标为(1)的那行代码访问静态的cup1对象,还是把标为(1)的行注释掉,让它去运行标为(2)的那行代码(即解除标为(2)的行的注释),Cups的静态初始化动作都会得到执行。如果把标为(1和(2)的行同时注释掉,Cups的静态初始化动作就不会进行,就像在输出中看到的那样。此外,激活一行还是两行标为(2)的代码(即解除注释)都无关紧要,静态初始化动作只进行一次。

  • 练习13:(1)验证前面段落中的语句。
  • 练习14:(1)编写一个类,拥有两个静态字符串域,其中一个在定义处初始化,另一个在静态块中初始化。现在,加入一个静态方法用以打印出两个字段值。请证明它们都会在被使用之前完成初始化动作。

5.7.4 非静态实例初始化

  Java中也有被称为实例初始化的类似语法,用来初始化每一个对象的非静态变量。例如:

class Mug
{
Mug(int marker)
{
print("Mug(" + marker + ")");
} void f(int marker)
{
print("f(" + marker + ")");
}
} public class Mugs
{
Mug mug1;
Mug mug2; {
mug1 = new Mug(1);
mug2 = new Mug(2);
print("mug1和mug2 初始化了");
} Mugs()
{
print("Mugs()");
} Mugs(int i)
{
print("Mugs(int)");
} public static void main(String[] args)
{
print("Inside main()");
new Mugs();
print("new Mugs() completed");
new Mugs(1);
print("new Mugs(1) completed");
}
}/*Output
Inside main()
Mug(1)
Mug(2)
mug1和mug2 初始化了
Mugs()
new Mugs() completed
Mug(1)
Mug(2)
mug1和mug2 初始化了
Mugs(int)
new Mugs(1) completed
*///:~

  你可以看到实例初始化子句

{
mug1 = new Mug(1);
mug2 = new Mug(2);
print("mug1和mug2 初始化了");
}

  看起来它与静态初始化子句一模一样,只不过少了static关键字。这种语法对于支持“匿名内部类”(参见第10章)的初始化是必须的,但是它也使得你可以保证无论调用了哪个显式构造器,某些操作都会发生。从输出中可以看到实例初始化子句是在两个构造器之前执行的。

  • 练习1 5:(1)编写一个含有字符串域的类,并采用实例初始化方式进行初始化。

5.8 数组初始化

  数组只是相同类型的、用一个标识符名称封装到一起的一个对象序列或基本类型数据序列。数组是通过方括号下标操作符[]来定义和使用的。要定义一个数组,只需在类型名后加上一对空方括号即可:

int [] a1;

  方括号也可以置于标识符后面

  int a1[] ;

  两种格式的含义是一样的,后一种格式符合C和C++程序员的习惯。不过,前一种格式或许更合理,毕竟它表明类型是“一个int型数组”。本书将采用这种格式。

  编译器不允许指定数组的大小。这就又把我们带回到有关“引用”的问题上。现在拥有的只是对数组的一个引用(你已经为该引用分配了足够的存储空间),而且也没给数组对象本身分配任何空间。为了给数组创建相应的存储空间,必须写初始化表达式。对于数组,初始化动作可以出现在代码的任何地方,但也可以使用一种特殊的初始化表达式,它必须在创建数组的地方出现。这种特殊的初始化是由一对花括号括起来的值组成的。在这种情况下,存储空间的分配(等价于使用new)将由编译器负责。例如:

  在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] = a2[i] + 1;
} for (int i = 0; i < a1.length; i++)
{
print("a1[" + i + "] = " + a1[i]);
}
}
}/*Output
a1[0] = 2
a1[1] = 3
a1[2] = 4
a1[3] = 5
a1[4] = 6
*///:~

  可以看到代码中给出了a1的初始值,但a2却没有。在本例中,a2是在后面被赋给另一个数组的。由于a2和a1是相同数组的别名,因此通过a2所做的修改在a1中可以看到。

  所有数组(无论它们的元素是对象还是基本类型)都有一个固有成员,可以通过它获知数组内包含了多少个元素,但不能对其修改。这个成员就是length。与C和C++类似,Java数组计数也是从第0个元素开始,所以能使用的最大下标数是length-1。要是超出这个边界,c和C++会“默默”地接受,并允许你访问所有内存,许多声名狼藉的程序错误由此而生。 Java则能保护你免受这一问题的困扰,一旦访问下标过界,就会出现运行时错误(即异常)。

  当然,每次访问数组的时候都要检查边界的做法在时间和代码上都是需要开销的,但是无法禁用这个功能。这意味着如果数组访问发生在一些关键节点上,它们有可能会成为导致程序效率低下的原因之一。但是基于因特网的安全以及提高程序员生产力”的理由,Java的设计者认为这种权衡是值得的。尽管你可能会受到诱惑,去编写你认为可以使得数组访问效率提高的代码,但是这一切都是在浪费时间,因为自动的编译期错误和运行时优化都可以提高致组访问的速度。

  如果在编写程序时,并不能确定在数组里需要多少个元素,那么该怎么办呢?可以直接用new在数组里创建元素。尽管创建的是基本类型数组,new仍然可以工作(不能用new创建单个的基本类型数据)。

public class ArrayNew
{
public static void main(String[] args)
{
int[] a;
Random random = new Random(47); a = new int[random.nextInt(20)];
print("length of a =" + a.length);
print(Arrays.toString(a));
}
}/*Output
length of a =18
[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]
*///:~

  数组的大小是通过Random.nextInt()方法随机决定的,这个方法会返回0到输入参数之间的一个值。这表明数组的创建确实是在运行时刻进行的。此外,程序输出表明:数组元素中的基本数据类型值会自动初始化成空值(对于数字和字符,就是0;对于布尔型,是false)。

  Arrays.toString0方法属于java.util标准类库,它将产生一维数组的可打印版本。

  当然,在本例中,数组也可以在定义的同时进行初始化:

int[] a = new int [rand.nextInt(20)] ;

  如果可能的话,应该尽量这么做。

  如果你创建了一个非基本类型的数组,那么你就创建了一个引用数组。以整型的包装器类Integer为例,它是一个类而不是基本类型:

public class ArrayNew
{
public static void main(String[] args)
{
Random random = new Random(47);
Integer[] a = new Integer[random.nextInt(20)];
print("length of a =" + a.length); for (int i = 0; i < a.length; i++)
a[i] = random.nextInt(500); print(Arrays.toString(a));
}
}/*Output
length of a =18
[55, 193, 361, 461, 429, 368, 200, 22, 207, 288, 128, 51, 89, 309, 278, 498, 361, 20]
*///:~

  这里,即便使用new创建数组之后:

Integer[] a = new Integer[rand.nextlnt(20)];

  它还只是一个引用数组,并且直到通过创建新的Integer对象(在本例中是通过自动包装机制创建的),并把对象赋值给引用,初始化进程才算结束:

a[i] = random.nextInt(500);

  如果忘记了创建对象,并且试图使用数组中的空引用,就会在运行时产生异常。

  也可以用花括号括起来的列表来初始化对象数组。有两种形式:

public class ArrayNew
{
public static void main(String[] args)
{
Integer[] a = {new Integer(1), new Integer(2), 3,};
Integer[] b = new Integer[]{new Integer(1), new Integer(2), 3,};
print(Arrays.toString(a));
print(Arrays.toString(b));
}
}/*Output
[1, 2, 3]
[1, 2, 3]
*///:~

  在这两种形式中,初始化列表的最后一个逗号都是可选的(这一特性使维护长列表变得更容易)。

  尽管第一种形式很有用,但是它也更加受限,因为它只能用于数组被定义之处。你可以在任何地方使用第二种和第三种形式,甚至是在方法调用的内部。例如,你可以创建一个String对象数组,将其传递给另一个main()方法,以提供参数,用来替换传递给该main()方法的命令行参数。

class Other
{
public static void main(String[] args)
{
for (String s : args)
System.out.print(s + " ");
}
} public class DynamicArray
{
public static void main(String[] args)
{
print("Inside main()");
Other.main(new String[]{"fiddle", "de", "dum",});
}
}/*Output
fiddle de dum
*///:~

  为Other.main()的参数而创建的数组是在方法调用处创建的,因此你甚至可以在调用时提供可替换的参数。

  • 练习16:(1)创建一个String对象数据,并为每一个元素都赋值一个String。用for循环来打印该数组。
  • 练习17:(2)创建一个类,它有一个接受一个String参数的构造器。在构造阶段,打印该参数。创建一个该类的对象引用数组,但是不实际去创建对象赋值给该数组。当运行程序时,请注意来自对该构造器的调用中的初始化消息是否打印了出来。
  • 练习18:(1)通过创建对象赋值给引用数组,从而完成前一个练习。

5.8.1 可变参数列表

class A
{
}
public class VarArgs
{
static void printArray(Object[] args)
{
for (Object o : args)
System.out.print(o + " ");
System.out.println();
} public static void main(String[] args)
{
printArray(new Object[]{new Integer(47), new Float(3.14), new Double(11.11),});
printArray(new Object[]{"one", "two", "three",});
printArray(new Object[]{new A(), new A(), new A(),});
}
}/*Output
47 3.14 11.11
one two three
A@4aa298b7 A@7d4991ad A@28d93b30
*///:~

  标准Java库中的类能输出有意义的内容,但这里建立的类的对象,打印出的内容只是类的名称以及后面紧跟着的一个@符号以及多个十六进制数字。于是,默认行为(如果没有定义toString()方法的话,后面会讲这个方法的)就是打印类的名字和对象的地址。

class A
{
}
public class VarArgs
{
static void printArray(Object[] args)
{
for (Object o : args)
System.out.print(o + " ");
System.out.println();
} public static void main(String[] args)
{
printArray(new Object[]{new Integer(47), new Float(3.14), new Double(11.11),});
printArray(new Object[]{"one", "two", "three",});
printArray(new Object[]{new A(), new A(), new A(),}); printArray(new Integer[]{1, 2, 3, 4,});
printArray();
}
}/*Output
47 3.14 11.11
one two three
A@4aa298b7 A@7d4991ad A@28d93b30
1 2 3 4 *///:~

  如果你有一组事物,可以把它们当作列表传递,而如果你已经有了一个数组,该方法可以把它们当作可变参数列表来接受。

public class AutoboxingVarargs
{
static void f(Integer... args)
{
for (Integer i : args)
System.out.print(i + " ");
System.out.println();
} public static void main(String[] args)
{
f(new Integer(1), new Integer(2));
f(4, 5, 6, 7, 8, 9);
f(10, new Integer(11), 12);
}
}/*Output
1 2
4 5 6 7 8 9
10 11 12
*///:~

  你可以在单一的参数列表中将类型混合在一起,而自动包装机制将有选择地将Int参数提升为Integer。

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.print("third");
} public static void main(String[] args)
{
f('a', 'b', 'c');
f(1);
f(2,1);
f(0);
f(0L);
//! f();//这个无法调用,因为编译器不知道调哪个
}
}/*Output
first a b c
second 1
second 2 1
second 0
third
*///:~

  在每一种情况中,编译器都会使用自动包装机制来匹配重载的方法,然后调用最明确匹配的方法。

  但是在不使用参数调用f()时,编译器就无法知道应该调用哪一个方法了。尽管这个错误可以弄清楚,但是它可能会使客户端程序员大感意外。

  你可能会通过在某个方法中增加一个非可变参数来解决该问题:

public class OverloadingVarargs
{
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');//这个无法调用,因为编译器不知道调哪个
}
}/*Output
*///:~

  如果你给这两个方法都添加一个非可变参数,就可以解决问题了:

public class OverloadingVarargs
{
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');
}
}/*Output
first
second
*///:~

  你应该总是只在重载方法的一个版本上使用可变参数列表,或者压根就不是用它。

  • 练习19:(2)写一个类,它接受一个可变参数的String数组。验证你可以向该方法传递一个用逗号分隔的String列表,或是一个String[]。
  • 练习20:(1)创建一个使用可变参数列表而不是普通的main()语法的main()。打印所产生的args数组的所有元素,并用各种不同数量的命令行参数来测试它。

5.9 枚举类型

  在Java SE5中添加了一个看似很小的特性,即enum关键字,它使得我们在需要群组并使用枚举类型集时,可以很方便地处理。在此之前,你需要创建一个整型常量集,但是这些枚举值并不会必然地将其自身的取值限制在这个常量集的范围之内,因此它们显得更有风险,且更难以使用。枚举类型属于非常普遍的需求,C、C++和其他许多语言都已经拥有它了。在Java SE5之前,Java程序员在需要使用枚举类型时,必须了解很多细节并需要格外仔细,以正确地产生enum的效果。现在Java也有了enum,并且它的功能比C/C++中的枚举类型要完备得多。下面是一个简单的例子:

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

  这里创建了一个名为Spiciness的枚举类型,它具有5个具名值。由于枚举类型的实例是常量,因此按照命名惯例它们都用大写字母表示(如果在一个名字中有多个单词,用下划线将它们隔开)。

  为了使用enum,需要创建一个该类型的引用,并将其赋值给某个实例:

public class SimpleEnumUse
{
enum Spiciness
{
NOT, MILD, MEDIUM, HOT, FLAMING
} public static void main(String[] args)
{
Spiciness howHot = Spiciness.MEDIUM;
print(howHot);
}
}/*Output
MEDIUM
*///:~

  在你创建enum时,编译器会自动添加一些有用的特性。例如,它会创建toString()方法,以便你可以很方便地显示某个enum实例的名字,这正是上面的打印语句如何产生其输出的答案。编译器还会创建ordinal()方法,用来表示某个特定enum常量的声明顺序,以及static values()方法,用来按照enum常量的声明顺序,产生由这些常量值构成的数组:

public class EnumOrder
{
enum Spiciness
{
NOT, MILD, MEDIUM, HOT, FLAMING
} public static void main(String[] args)
{
for (Spiciness s:Spiciness.values())
print(s+",ordinal "+s.ordinal());
}
}/*Output
NOT,ordinal 0
MILD,ordinal 1
MEDIUM,ordinal 2
HOT,ordinal 3
FLAMING,ordinal 4
*///:~

  尽管enum看起来像是一种新的数据类型,但是这个关键字只是为enum生成对应的类时,产生了某些编译器行为,因此在很大程度上,你可以将enum当作其他任何类来处理。事实上,enum确实是类,并且具有自己的方法。

  enum有一个特别实用的特性,即它可以在switch语句内使用:

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:
print("not spicy at all");
break;
case MILD:
case MEDIUM:
print("a little hot");
break;
case FLAMING:
case HOT:
default:
print("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();
}
}/*Output
This burrito is not spicy at all
This burrito is a little hot
This burrito is maybe too hot
*///:~

  由于switch是要在有限的可能值集合中进行选择,因此它与enum正是绝佳的组合。请注意enum的名字是如何能够倍加清楚地表明程序意欲何为的。

  大体上,你可以将enum用作另外一种创建数据类型的方式,然后直接将所得到的类型拿来使用。这正是关键所在,因此你不必过多地考虑它们。在Java SE5引进enum之前,你必须花费大量的精力去保证与其等价的枚举类型是安全可用的。

  这些介绍对于你理解和使用基本的enum已经足够了,但是我们将在第19章中更加深入地探 
讨它们。

    • 练习21:(1)创建一个enum,它包含纸币中最小面值的6种类型。通过values()循环并打印每一个值及其ordinal() 。
    • 练习22:(2)在前面的例子中,为enum写一个switch语句,对于每一个case,输出该特定货币的描述。

5.10 总结

  构造器,这种精巧的初始化机制,应该给了读者很强的暗示:初始化在Java中占有至关重要的地位。 C++的发明人Bjame Stroustrup在设计C++期间,在针对C语言的生产效率所进行的最初调查中发现,大量编程错误都源于不正确的初始化。这种错误很难发现,并且不恰当的清理也会导致类似问题。构造器能保证正确的初始化和清理(没有正确的构造器调用,编译器就不允许创建对象),所以有了完全的控制,也很安全。

  在C++中,“析构”相当重要,因为用new创建的对象必须明确被销毁。在Java中,垃圾回收器会自动为对象释放内存,所以在很多场合下,类似的清理方法在Java中就不太需要了(不过当要用到的时候,你就只能自己动手了)。在不需要类似析构函数的行为的时候,Java的垃圾回收器可以极大地简化编程工作,而且在处理内存的时候也更安全。有些垃圾回收器甚至能清理其他资源,比如图形和文件句柄。然而,垃圾回收器确实也增加了运行时的开销。而且Java解释器从来就很慢,所以这种开销到底造成了多大的影响也很难看出。随着时间的 推移,Java在性能方面已经取得了长足的进步,但速度问题仍然是它涉足某些特定编程领域的障碍。

  由于要保证所有对象都被创建,构造器实际上要比这里讨论的更复杂。特别当通过组合或继承生成新类的时候,这种保证仍然成立,并且需要一些附加的语法提供支持。在后面的章节中,读者将学习到有关组合、继承以及它们对构造器造成的影响等方面的知识。

练习1:

class StringTest1{
protected String s;
StringTest1(){
System.out.println("s: " + s);
}
}
@Test
public void exercises1() {
new StringTest1();
}

练习2:

class StringTest2{
protected String s;
StringTest2(String str){
System.out.println(s);
s = str;
System.out.println(s);
}
}
class StringTest3{
protected String s = "123";
StringTest3(String str){
System.out.println(s + " : " + str);
System.out.println(s == str);
s = str;
System.out.println(s + " : " + str);
System.out.println(s == str);
}
}
@Test
public void exercises2() {
new StringTest2("456");
new StringTest3("123");
new StringTest3(new String("123"));
}/* Output:
null
456
123 : 123
true
123 : 123
true
123 : 123
false
123 : 123
true*///:~

练习3:

class StringTest1{
protected String s;
StringTest1(){
System.out.println("s: " + s);
}
}
@Test
public void exercises3() {
new StringTest1();
}

练习4:

class StringTest1{
protected String s;
StringTest1(){
System.out.println("s: " + s);
}
StringTest1(String str){
System.out.println("str: " + str);
}
}
@Test
public void exercises4() {
new StringTest1();
new StringTest1("123");
}

练习5:

public class Dog {
public void bark(int i) {
System.out.println("barking");
}
public void bark(String s) {
System.out.println("barking");
}
}
@Test
public void exercises5() {
Dog dog = new Dog();
dog.bark(0);
dog.bark("0");
}/* Output
barking
howling
*///:~

练习6:

public class Dog {
public void bark(int i,String s) {
System.out.println("barking");
}
public void bark(String s,int i) {
System.out.println("howling");
}
}
@Test
public void exercises6() {
Dog dog = new Dog();
dog.bark(0,"0");
dog.bark("0",0);
}/* Output
barking
howling
*///:~

练习7:

class StringTest7{
void info() {
System.out.println("123");
}
}
@Test
public void exercises7() {
StringTest7 stringTest7 = new StringTest7();
stringTest7.info();
}/* Output
123
*///:~

练习13:

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(int i){
cup1.f(i);
System.out.println("Cups");
}
} public class ExplicitStatic { public static void main(String[] args) {
System.out.println("Inside main()");
Cups.cup1.f(1);
}
static Cups cups1 = new Cups(1);
static Cups cups2 = new Cups(2); @Test
public void explicitStaticTest1() {
System.out.println("explicitStaticTest1");
Cups.cup1.f(1);
}
}

练习14:

class Cup {
Cup(int marker){
System.out.println("Cup(" + marker + ")");
}
void f(int marker) {
System.out.println("f(" + marker + ")");
}
} class Cups{
static Cup cup2;
static {
cup2 = new Cup(2);
}
static Cup cup1 = new Cup(1);
Cups(int i){
cup1.f(i);
System.out.println("Cups");
}
} public class ExplicitStatic { public static void main(String[] args) {
System.out.println("Inside main()");
Cups.cup1.f(1);
}
static Cups cups1 = new Cups(1);
static Cups cups2 = new Cups(2); @Test
public void explicitStaticTest1() {
System.out.println("explicitStaticTest1");
Cups.cup1.f(1);
}
}
上一篇:2018-2019-2 20175329许钰玮 实验二《Java面向对象程序设计》实验报告


下一篇:php设计模式-------(1)策略模式