Java编程思想 Thinking in Java 学习笔记 第六章 划重点

Thinking in Java

原书中文翻译有点烂,进行了一些精简。

并且对原书程序进行了一些改动,增加了一部分输出结果,使得程序更容易理解。

关键词:合成、继承、上溯造型、final、类装载、初始化等

上一篇:第一至五章 https://blog.csdn.net/mooe1011/article/details/88033723

 

第六章 类再生

p139

代码再生或者重复使用的机制。

合成:在新类里简单地创建原有类的对象。

继承:它创建一个新类,将其作为现有类的一个“类型”。同时可以在其中加入新代码,不会对现有的类产生影响。

6.1 合成

举例来说,假定需要在一个对象里容纳几个 String对象、两种基本数据类型以及属于另一个类的一个对象。对于非基本类型的对象来说,只需将句柄置于新类即可;而对于基本数据类型来说,则需定义它们。

//: SprinklerSystem.java

package c06;
class WaterSource {
	private String s;

	WaterSource() {
		System.out.println("WaterSource()");
		s = new String("Constructed");
	}

	public String toString() {
		return s;
	}
}

public class SprinklerSystem {
	private String valve1, valve2, valve3, valve4;
	WaterSource source;
	int i;
	float f;

	void print() {
		System.out.println("valve1 = " + valve1);
		System.out.println("valve2 = " + valve2);
		System.out.println("valve3 = " + valve3);
		System.out.println("valve4 = " + valve4);
		System.out.println("i = " + i);
		System.out.println("f = " + f);
		System.out.println("source = " + source);
	}

	public static void main(String[] args) {
		SprinklerSystem x = new SprinklerSystem();
		x.print();
	}
}

WaterSource内定义的一个方法是比较特别的:toString()。每种非基本类型的对象都有一个toString()方法。下面这个表达式中:

System.out.println("source = " + source) ;

编译器会试图向一个WaterSource 添加一个String 对象("source =")。这对它来说是不可接受的,所以它会调用toString(),把source 转换成字串!如果不深究,可能会草率地认为编译器会为上述代码中的每个句柄都自动构造对象(由于Java 的安全和谨慎的形象)。例如,可能以为它会为 WaterSource调用默认构建器,以便初始化 source。输出事实上是:

valve1 = null

valve2 = null

valve3 = null

valve4 = null

i = 0

f = 0.0

source = null

在类内作为字段使用的基本数据会初始化成零,但对象句柄会初始化成null。而且假若试图为它们中的任何一个调用方法,就会产生一次“违例”。这种结果很有用,我们可在不丢弃违例的前提下,仍然把它们打印出来。

编译器并不只是为每个句柄创建一个默认对象,因为那样会招致不必要的开销。如希望句柄得到初始化,可进行:

(1) 在对象定义的时候。这意味着它们在构建器调用之前肯定能得到初始化。

(2) 在那个类的构建器中。

(3) 紧靠在要求实际使用那个对象之前。这样做可减少不必要的开销——假如对象并不需要创建的话。下面例子:

//: Bath.java
class Soap {
	private String s;

	Soap() {
		System.out.println("Soap()");
		s = new String("Constructed");
	}

	public String toString() {
		return s;
	}
}

public class Bath {
	private String
	// Initializing at point of definition:
	s1 = new String("Happy"), s2 = "Happy", s3, s4;
	Soap castille;
	int i;
	float toy;

	Bath() {
		System.out.println("Inside Bath()");
		s3 = new String("Joy");
		i = 47;
		toy = 3.14f;
		castille = new Soap();
	}

	void print() {
		// Delayed initialization:
		if (s4 == null)
			s4 = new String("Joy");
		System.out.println("s1 = " + s1);
		System.out.println("s2 = " + s2);
		System.out.println("s3 = " + s3);
		System.out.println("s4 = " + s4);
		System.out.println("i = " + i);
		System.out.println("toy = " + toy);
		System.out.println("castille = " + castille);
	}

	public static void main(String[] args) {
		Bath b = new Bath();
		b.print();
	}
}

在Bath 构建器中,在所有初始化开始之前执行了一个语句。如果不在定义时进行初始化,仍然不能保证能在将一条消息发给一个对象句柄之前会执行任何初始化——除非出现不可避免的运行期违例。下面是该程序的输出:

Inside Bath()

Soap()

s1 = Happy

s2 = Happy

s3 = Joy

s4 = Joy

i = 47

toy = 3.14

castille = Constructed

调用print()时,它会填充 s4,使所有字段在使用之前都获得正确的初始化。

6.2 继承

p141

//: Detergent.java

class Cleanser {

private String s = new String("Cleanser");

public void append(String a) { s += a; }

public void dilute() { append(" dilute()"); }

public void apply() { append(" apply()"); }

public void scrub() { append(" scrub()"); }

public void print() { System.out.println(s); }

public static void main(String[] args) {

Cleanser x = new Cleanser();

x.dilute(); x.apply(); x.scrub();x.print();

}

}

public class Detergent extends Cleanser {

public void scrub() {  // Change a method:

       append(" Detergent.scrub()");

       super.scrub(); // Call base-class version

}

// Add methods to the interface:

public void foam() { append(" foam()"); }

// Test the new class:

public static void main(String[] args) {

       Detergent x = new Detergent();

       x.dilute();x.apply();x.scrub();

       x.foam();x.print();

       System.out.println("Testing base class:");

       Cleanser.main(args); //测试Cleanser

}

}

输出:

Cleanser dilute() apply() Detergent.scrub() scrub() foam()

Testing base class:

Cleanser dilute() apply() scrub()

在Cleanser append()方法里,字串同一个s 连接起来。同“+”一样,“+=”被Java 用于对字串进行“过载”处理。其次,无论 Cleanser 还是Detergent 都包含了一个main()方法。通常建议大家象这样进行编写代码,为自己的每个类都创建一个main()。对于在命令行请求的public 类,只有main()才会得到调用。所以当我们使用“java Detergent”的时候,调用的是 Degergent.main()——即使Cleanser 并非一个public 类。采用这种将main()置入每个类的做法,可方便地进行单元测试。而且毋需将main()删去,可用于以后的测试。

需要强调的是Cleanser 中的所有类都是public属性。在这个包内,任何人都可使用那些没有访问指示符的方法。然而,另外某个包的类准备继承Cleanser,它就只能访问那些public 成员。所以在计划继承的时候,一个比较好的规则是将所有字段都设为private,并将所有方法都设为public(protected 成员也允许衍生出来的类访问它;以后还会深入探讨这一问题)。当然,在一些特殊的场合,我们仍然必须作出一些调整。

正如在scrub()里看到的那样,通常想在新版本里调用来自基础类的方法。但在 scrub()里,不可只是简单地发出对scrub()的调用。那样便造成了递归调用,为解决这个问题,Java 提供了一个 super 关键字,它引用当前类已从中继承的一个“超类”(Superclass)。

进行继承时,我们并不限于只能使用基础类的方法。亦可在衍生出来的类里加入自己的新方法。这时只需简单地定义它即可。foam()便是这种做法的产物。

在Detergent.main()里,我们可看到对于Detergent 对象,可调用Cleanser 以及Detergent 内所有可用的方法(如foam())。

6.2.1 初始化基础类

创建衍生类的一个对象时,它包含了基础类的一个“子对象”,就象我们根据基础类本身创建了它的一个对象

当然,基础类子对象应该正确地初始化,而且只有一种方法:在构建器中执行初始化,通过调用基础类构建器,后者便能执行对基础类的初始化。在衍生类的构建器中,Java 会自动插入对基础类构建器的调用。下面这个例子展示了对这种三级继承的应用:

//: Cartoon.java

class Art {

Art() {

       System.out.println("Art constructor");

}

}

class Drawing extends Art {

Drawing() {

       System.out.println("Drawing constructor");

}

}

public class Cartoon extends Drawing {

Cartoon() {

       System.out.println("Cartoon constructor");

}

public static void main(String[] args) {

       Cartoon x = new Cartoon();

}

}

输出显示了自动调用:

Art constructor

Drawing constructor

Cartoon constructor

1. 含有自变量的构建器

如果类没有默认的自变量,或者想调用含有一个自变量的某个基础类构建器,必须明确地编写对基础类的调用代码。这是用 super 关键字以及适当的自变量列表实现的,如下所示:

//: Chess.java

class Game {

Game(int i) {

       System.out.println("Game constructor");

}

}

class BoardGame extends Game {

BoardGame(int i) {

       super(i);

       System.out.println("BoardGame constructor");

}

}

public class Chess extends BoardGame {

Chess() {

       super(11);

       System.out.println("Chess constructor");

}

public static void main(String[] args) {

       Chess x = new Chess();

}

}

如果不调用 BoardGames()内的基础类构建器,编译器就会报告自己找不到Games()形式的一个构建器。除此以外,在衍生类构建器中,对基础类构建器的调用是必须做的第一件事情(如操作失当,编译器会向我们指出)。

2. 捕获基本构建器的违例

正如刚才指出,编译器会强迫我们在衍生类构建器的主体中首先设置对基础类构建器的调用。这意味

着会防止衍生类构建器捕获来自一个基础类的任何违例事件。显然,这有时会为我们造成不便。

6.3 合成与继承的结合

许多时候都要求将合成与继承两种技术结合起来使用,从而创建一个更复杂的类,同时进行必要的构建器初始化工作:

//: PlaceSetting.java

class Plate {

Plate(int i) {

       System.out.println("Plate constructor");

}

}

class DinnerPlate extends Plate {

DinnerPlate(int i) {

       super(i);

       System.out.println("DinnerPlate constructor");

}

}

class Utensil {

Utensil(int i) {

       System.out.println("Utensil constructor");

}

}

class Spoon extends Utensil {

Spoon(int i) {

       super(i);

       System.out.println("Spoon constructor");

}

}

class Fork extends Utensil {

Fork(int i) {

super(i);

System.out.println("Fork constructor");

}

}

class Knife extends Utensil {

Knife(int i) {

super(i);

System.out.println("Knife constructor");

}

}

// A cultural way of doing something:

class Custom {

Custom(int i) {

       System.out.println("Custom constructor");

}

}

public class PlaceSetting extends Custom {

Spoon sp;

Fork frk;

Knife kn;

DinnerPlate pl;

PlaceSetting(int i) {

super(i + 1);

sp = new Spoon(i + 2);

frk = new Fork(i + 3);

kn = new Knife(i + 4);

pl = new DinnerPlate(i + 5);

System.out.println("PlaceSetting constructor");

}

public static void main(String[] args) {

PlaceSetting x = new PlaceSetting(9);

}

}

输出:

Custom constructor

Utensil constructor

Spoon constructor

Utensil constructor

Fork constructor

Utensil constructor

Knife constructor

Plate constructor

DinnerPlate constructor

PlaceSetting constructor

尽管编译器会强迫我们对基础类进行初始化,但它并不会监视我们是否正确初始化了成员对象。因此要特别留意。

6.3.1 确保正确的清除

在 C++中,一旦破坏(清除)一个对象,就会自动调用破坏器方法。Java之所以将其省略,大概是垃圾收集器会在必要的时候自动回收内存。

垃圾收集器大多数时候都能很好地工作,但在某些情况下,我们的类可能要求必须进行明确的清除工作。正如第 4章已经指出的那样,我们并不知道垃圾收集器什么时候才会调用。所以我们必须写一个特别的方法。同时,还要让客户程序员知道必须调用这个方法。就如第9 章(违例控制)说的那样,必须将这样的清除代码置于一个 finally从句中,从而防范所有可能出现的违例事件。

下面介绍的是一个计算机辅助设计系统的例子:

//: CADSystem.java

class Shape {

Shape(int i) {

       System.out.println("Shape constructor");

}

void cleanup() {

       System.out.println("Shape cleanup");

}

}

class Circle extends Shape {

Circle(int i) {

super(i);

System.out.println("Drawing a Circle");

}

void cleanup() {

System.out.println("Erasing a Circle");

super.cleanup();

}

}

class Triangle extends Shape {

Triangle(int i) {

super(i);

System.out.println("Drawing a Triangle");

}

void cleanup() {

System.out.println("Erasing a Triangle");

super.cleanup();

}

}

class Line extends Shape {

private int start, end;

Line(int start, int end) {

super(start);

this.start = start;

this.end = end;

System.out.println("Drawing a Line: " + start + ", " + end);

}

void cleanup() {

System.out.println("Erasing a Line: " + start + ", " + end);

super.cleanup();

}

}

public class CADSystem extends Shape {

private Circle c;

private Triangle t;

private Line[] lines = new Line[3];  //原书new Line[10],改小便于观测

CADSystem(int i) {

super(i + 1);

for (int j = 0; j < 3; j++) //原书j < 10,同上

lines[j] = new Line(j, j * j);

c = new Circle(1);

t = new Triangle(1);

System.out.println("Combined constructor");

}

void cleanup() {

System.out.println("CADSystem.cleanup()");

t.cleanup();

c.cleanup();

for (int i = 0; i < lines.length; i++)

lines[i].cleanup();

super.cleanup();

}

public static void main(String[] args) {

CADSystem x = new CADSystem(47);

try {

// Code and exception handling...

} finally {

x.cleanup();

}

}

}

输出:

Shape constructor

Shape constructor

Drawing a Line: 0, 0

Shape constructor

Drawing a Line: 1, 1

Shape constructor

Drawing a Line: 2, 4

Shape constructor

Drawing a Circle

Shape constructor

Drawing a Triangle

Combined constructor

CADSystem.cleanup()

Erasing a Triangle

Shape cleanup

Erasing a Circle

Shape cleanup

Erasing a Line: 0, 0

Shape cleanup

Erasing a Line: 1, 1

Shape cleanup

Erasing a Line: 2, 4

Shape cleanup

Shape cleanup

所有东西都属于某种 Shape。Shape本身是从根类明确继承的。每个类都重新定义了Shape 的cleanup()方法,同时还要用super 调用那个方法的基础类版本。尽管对象存在期间调用的所有方法都可负责做一些要求清除的工作,但对于特定的Shape 类——Circle(圆)、Triangle(三角形)以及Line(直线),它们都拥有自己的构建器,能完成“作图”(draw)任务。每个类都有它们自己的cleanup()方法,用于将非内存的东西恢复回对象存在之前的景象。

在main()中,可看到两个新关键字:try和 finally。其中,try关键字指出后面跟随的块(由花括号定界)是一个“警戒区”。该警戒区后面跟随的finally从句的代码肯定会得以执行——不管try块到底存不存在(通过违例控制技术,try 块可有多种不寻常的应用)。在这里,finally的意思是“无论发生什么,总是为 x 调用cleanup()”。

在自己的清除方法中,必须注意对基础类以及成员对象清除方法的调用顺序。通常,应采取与C++“破坏器”采取的同样的形式:首先完成与类有关的所有特殊工作(可能要求基础类元素仍然可见),然后调用基础类清除方法,就象这儿演示的那样。

许多情况下,清除可能并不是个问题。但一旦必须由自己明确清除,就必须特别谨慎。

垃圾收集的顺序

我们不能确切知道何时会开始垃圾收集,而且可能永远不会得到调用。即使得到调用,也可能会以任何顺序回收对象。除此以外,Java 1.0实现的垃圾收集器机制通常不会调用 finalize()方法。除内存的回收以外,其他东西都最好不要依赖垃圾收集器。若明确清除什么,请制作自己的清除方法,而且不要依赖finalize()。然而正如以前指出的那样,可强迫Java1.1 调用所有收尾模块(finalizer)。

6.3.2 名字的隐藏

只有C++程序员可能才会惊讶于名字的隐藏。如果 Java 基础类有一个方法名被“过载”使用多次,在衍生类里对那个方法名的重新定义就不会隐藏任何基础类的版本。所以无论方法在这一级还是在一个基础类中定义,过载都会生效:

//: Hide.java

class Homer {

char doh(char c) {

System.out.println("doh(char)");

return 'd';

}

float doh(float f) {

System.out.println("doh(float)");

return 1.0f;

}

}

class Milhouse {

}

class Bart extends Homer {

void doh(Milhouse m) {

}

}

class Hide {

public static void main(String[] args) {

Bart b = new Bart();

b.doh(1); // doh(float) used

b.doh('x');

b.doh(1.0f);

b.doh(new Milhouse());

}

}

输出(class Hide改成public):

doh(float)

doh(char)

doh(float)

很少会用与基础类里完全一致的签名和返回类型来覆盖同名的方法(这正是C++不允许那样做的原因,能够防止产生错误)。

6.4 到底选择合成还是继承

如果想利用新类内部一个现有类的特性,而不想使用它的接口,通常应选择合成。也就是说,我们可嵌入一个对象,使自己能用它实现新类的特性。但新类的用户会看到我们已定义的接口,而不是来自嵌入对象的接口。所以我们需在新类里嵌入现有类的private 对象。

有时候,我们想让类用户直接访问新类的合成。也就是说,需要将成员对象的属性变为public。成员对象会将自身隐藏起来,所以这是安全的做法。而且在用户知道我们准备合成一系列组件时,接口就更容易理解。car(汽车)对象便是一个例子:

//: Car.java

class Engine {

public void start() {}

public void rev() {}

public void stop() {}

}

class Wheel {

public void inflate(int psi) {}

}

class Window {

public void rollup() {}

public void rolldown() {}

}

class Door {

public Window window = new Window();

public void open() {}

public void close() {}

}

public class Car {

public Engine engine = new Engine();

public Wheel[] wheel = new Wheel[4];

public Door left = new Door(), right = new Door(); // 2-door

Car() {

for (int i = 0; i < 4; i++)

wheel[i] = new Wheel();

}

public static void main(String[] args) {

Car car = new Car();

car.left.window.rollup();

car.wheel[0].inflate(72);

}

}

由于汽车的装配是故障分析时需要考虑的一项因素(并非只是基础设计简单的一部分),所以有助于客户程序员理解如何使用类,而且类创建者的编程复杂程度也会大幅度降低。

如选择继承,这意味着我们准备使用一个常规用途的类,并根据特定的需求对其进行定制。只需稍加想象,就知道自己不能用一个车辆对象来合成一辆汽车;相反,它“属于”车辆的一种类别。“属于”关系是用继承来表达的,而“包含”关系是用合成来表达的。

6.5 protected

p150

在实际应用中,经常想把某些东西深深地藏起来,但同时允许访问衍生类的成员。protected 关键字可帮助我们做到这一点。

//: Orc.java

class Villain {

private int i;

protected int read() {

return i;

}

protected void set(int ii) {

i = ii;

}

public Villain(int ii) {

i = ii;

}

public int value(int m) {

return m * i;

}

}

public class Orc extends Villain {

private int j;

public Orc(int jj) {

super(jj);

j = jj;

}

public void change(int x) {

set(x);

}

}

可以看到,change()拥有对 set()的访问权限,因为它的属性是protected。

6.7 上溯造型

“新类属于现有类的一种类型”。这种表达是对继承的一种形象化解释。作为一个例子,大家可考虑一个名为Instrument 的基础类;另一个衍生类叫作Wind。由于继承意味着基础类的所有方法亦可在衍生出来的类中使用,所以我们发给基础类的任何消息亦可发给衍生类。若Instrument 类有一个play()方法,则 Wind 设备也会有这个方法。

//: Wind.java

class Instrument {

public void play() {

}

static void tune(Instrument i) {

// ...

i.play();

}

}

// Wind objects are instruments

class Wind extends Instrument {

public static void main(String[] args) {

Wind flute = new Wind();

Instrument.tune(flute); // Upcasting

}

}

这个例子中最有趣的无疑是tune()方法,在 Wind.main()中,tune()方法是通过为其赋予一个Wind 句柄来调用的。大家可能会感到很奇怪,为什么接收一种类型(Instrument)的方法也能接收另一种类型(Wind)呢?我们一定要认识到一个Wind 对象也是一个Instrument对象。tune()中适用于 Instrument以及从中衍生出来的任何东西。在这里,我们将从一个Wind 句柄转换成一个Instrument 句柄的行为叫作“上溯造型”。

6.7.1 何谓“上溯造型”?

之所以叫作这个名字,除了历史原因外,类继承图的画法是根位于最顶部,再逐渐向下扩展。因此,Wind.java 的继承图就象下面这个样子:

由于造型的方向是从衍生类到基础类,箭头朝上,所以通常把它叫作“上溯造型”,即Upcasting。上溯造型肯定是安全的,因为我们是从一个更特殊的类型到一个更常规的类型。衍生类是基础类的一个超集。它可以包含比基础类更多的方法,但它至少包含了基础类的方法。进行上溯造型的时候,唯一一个问题是它可能丢失方法,而不是赢得这些方法。这便是在没有任何明确的造型或者其他特殊标注的情况下,编译器为什么允许上溯造型的原因所在。

也可以执行下溯造型,但这时会面临第11章要详细讲述的一种困境。

1. 再论合成与继承

在面向对象的程序设计中,最可能采取的一种做法是:将数据和方法统一封装到一个类里,并且使用那个类的对象。有些时候,需通过“合成”技术用现成的类来构造新类。而继承是最少见的一种做法。使用继承时要特别慎重。为判断到底应该选用合成还是继承,一个最简单的办法就是考虑是否需要从新类上溯造型回基础类。如果不需要上溯造型,就应提醒自己防止继承的滥用。在下一章里(多形性),会向大家介绍必须进行上溯造型的一种场合。

6.8 final 关键字

最一般的意思就是声明“这个东西不能改变”。

6.8.1 final 数据

常数主要应用于下述两个方面:

(1) 编译期常数,它永远不会改变

(2) 在运行期初始化的一个值,我们不希望它发生变化

对于编译期的常数,计算可在编译期间提前执行,从而节省运行时的一些开销。在 Java 中,这些形式的常数必须属于基本数据类型(Primitives),而且要用 final关键字进行表达。在对这样的一个常数进行定义的时候,必须给出一个值。

无论static还是 final字段,都只能存储一个数据,而且不得改变。

对于基本数据类型,final 会将值变成一个常数;但对于对象句柄,final 会将句柄变成一个常数。进行声明时,必须将句柄初始化到一个具体的对象。而且永远不能将句柄变成指向另一个对象。然而,对象本身是可以修改的。Java对此未提供任何手段,可将一个对象直接变成一个常数(但是,我们可自己编写一个类,使其中的对象具有“常数”效果)。这一限制也适用于数组。

//: FinalData.java
class Value {
	int i = 1;
}

public class FinalData {
	// Can be compile-time constants
	final int i1 = 9;
	static final int I2 = 99;
	// Typical public constant:
	public static final int I3 = 39;
	// Cannot be compile-time constants:
	final int i4 = (int) (Math.random() * 20);
	static final int i5 = (int) (Math.random() * 20);
	Value v1 = new Value();
	final Value v2 = new Value();
	static final Value v3 = new Value();
	// ! final Value v4; // Pre-Java 1.1 Error:
	// no initializer
 
	final int[] a = { 1, 2, 3, 4, 5, 6 };

	public void print(String id) {
		System.out.println(id + ": " + "i4 = " + i4 + ", i5 = " + i5);
	}

	public static void main(String[] args) {
		FinalData fd1 = new FinalData();
		// ! fd1.i1++; // Error: can't change value
		System.out.println("before:" + fd1.v2.i); //观测v2
		fd1.v2.i++; // Object isn't constant!
		System.out.println("after:" + fd1.v2.i);
		fd1.v1 = new Value(); // OK -- not final
		for (int i = 0; i < fd1.a.length; i++)
			fd1.a[i]++; // Object isn't constant!
		// ! fd1.v2 = new Value(); // Error: Can't
		// ! fd1.v3 = new Value(); // change handle
		// ! fd1.a = new int[3];
		fd1.print("fd1");
		System.out.println("Creating new FinalData");
		FinalData fd2 = new FinalData();
		fd1.print("fd1");
		fd2.print("fd2");
	}
}

由于i1和 I2都是具有 final属性的基本数据类型,并含有编译期的值,所以它们除了能作为编译期的常数使用外,在任何导入方式中也不会出现任何不同。I3是我们体验此类常数定义时更典型的一种方式:public表示它们可在包外使用;Static 强调它们只有一个;而 final 表明它是一个常数。注意对于含有固定初始化值(即编译期常数)的 fianl static基本数据类型,它们的名字根据规则要全部采用大写。也要注意i5 在编译期间是未知的,所以它没有大写。

不能由于某样东西的属性是final,就认定它的值能在编译时期知道。i4 和i5 向大家证明了这一点。例子的这一部分也向大家揭示出将final 值设为static 和非static 之间的差异。这种差异可从输出结果中看出:

before:1 //原书没有

after:2

fd1: i4 = 15, i5 = 9

Creating new FinalData

fd1: i4 = 15, i5 = 9

fd2: i4 = 10, i5 = 9

注意对于fd1和 fd2来说,i4、i5的值是唯一的,但 i5的值不会由于创建了另一个FinalData 对象而发生改变。那是因为它的属性是static,在载入时初始化。

从v1 到v4 的变量向我们揭示出final 句柄的含义。不能认为由于v2属于final,所以就不能再改变它的值。然而,我们确实不能再将v2绑定到一个新对象,因为它的属性是final。这便是final 对于一个句柄的确切含义。我们会发现同样的含义亦适用于数组,后者只不过是另一种类型的句柄而已。

2. 空白final

尽管被声明成 final,但却未得到一个初始值。无论在哪种情况下,空白 final都必须在实际使用前得到正确的初始化。对于final关键字的各种应用,具有最大的灵活性。举个例子来说,位

于类内部的一个final 字段现在对每个对象都可以有所不同,同时依然保持其“不变”的本质。

//: BlankFinal.java
class Poppet {
}

class BlankFinal {
	final int i = 0; // Initialized final
	final int j; // Blank final
	final Poppet p; // Blank final handle
	// Blank finals MUST be initialized in the constructor:

	BlankFinal() {
		j = 1; // Initialize blank final
		p = new Poppet();
	}

	BlankFinal(int x) {
		j = x; // Initialize blank final
		p = new Poppet();
	}

	public static void main(String[] args) {
		BlankFinal bf = new BlankFinal();
	}
}

现在强行要求我们对final 进行赋值处理——要么在定义字段时使用一个表达式,要么在每个构建器中。这样就可以确保final 字段在使用前获得正确的初始化。

3. final 自变量

Java 1.1 允许我们将自变量设成 final 属性,方法是在自变量列表中对它们进行适当的声明。这意味着在一个方法的内部,我们不能改变自变量句柄指向的东西。

//: FinalArguments.java
class Gizmo {
	public void spin() {
	}
}

public class FinalArguments {
	void with(final Gizmo g) {
		// ! g = new Gizmo(); // Illegal -- g is final
		g.spin();
	}

	void without(Gizmo g) {
		g = new Gizmo(); // OK -- g not final
		g.spin();
	}

	// void f(final int i) { i++; } // Can't change
	// You can only read from a final primitive:
	int g(final int i) {
		return i + 1;
	}

	public static void main(String[] args) {
		FinalArguments bf = new FinalArguments();
		bf.without(null);
		bf.with(null);
	}
}

注意此时仍然能为final 自变量分配一个null(空)句柄,同时编译器不会捕获它。这与我们对非 final 自变量采取的操作是一样的。

方法f()和 g()向我们展示出基本类型的自变量为 final 时会发生什么情况:我们只能读取自变量,不可改变。

6.8.2 final 方法

之所以要使用final 方法,可能是出于对两方面理由的考虑。第一个是为方法“上锁”,防止任何继承类改变它的本来含义。第二个理由是程序执行的效率。将一个方法设成 final 后,编译器对那个方法的所有调用都置入“嵌入”调用里。编译器会忽略为执行方法调用机制而采取的常规代码插入方法(将自变量压入堆栈;跳至方法代码并执行它;跳回来;清除堆栈自变量;最后对返回值进行处理)。相反,它会用方法主体内实际代码的一个副本来替换方法调用。这样做可避免方法调用时的系统开销。当然,若方法体积太大,可能不会有任何性能提升。编译器能自动侦测这些情况,并颇为“明智”地决定是否嵌入一个final 方法。然而,最好还是不要相信编译器的判断。通常,只有在方法的代码量非常少,或者想明确禁止方法被覆盖的时候,才应考虑将一个方法设为final。

类内所有private方法都自动成为final。由于我们不能访问一个 private方法,所以它绝对不会被其他方法覆盖。可为一个 private方法添加final 指示符,但却不能为那个方法提供任何额外的含义。

6.8.3 final 类

如果说整个类都是final,就表明不希望从这个类继承。除此以外,我们或许还考虑到执行效率的问题,并想确保涉及这个类各对象的所有行动都要尽可能地有效。

//: Jurassic.java
class SmallBrain {
}

final class Dinosaur {
	int i = 7;
	int j = 1;
	SmallBrain x = new SmallBrain();

	void f() {
	}
}

// ! class Further extends Dinosaur {} Cannot extend final class
public class Jurassic {
	public static void main(String[] args) {
		Dinosaur n = new Dinosaur();
		n.f();
		n.i = 40;
		n.j++;
		System.out.println(n.i + " " + n.j);//查看输出
	}
}

输出:40 2

 final类中的所有方法都默认为 final。因为此时再也无法覆盖它们。可为final 类内的一个方法添加final 指示符,但这样做没有任何意义。

6.8.4 final 的注意事项

p157

Stack(堆栈)是从Vector 继承来的,亦即Stack“是”一个 Vector,这种说法是不准确的。对于Vector 许多重要的方法,如addElement()以及 elementAt()等,它们都变成了 synchronized(同步的)。正如在第14章要讲到的那样,这会造成显著的性能开销,可能会把final 提供的性能改善抵销得一干二净。在标准库里居然采用了如此笨拙的设计,真不敢想象会惹恼多少程序员里引发什么样的情绪。

另一个值得注意的是Hashtable(散列表),它是另一个重要的标准类。该类没有采用任何final 方法。显然一些类的设计人员与其他设计人员有着全然不同的素质(注意比较Hashtable 极短的方法名与Vecor 的方法名)。一个产品的设计变得不一致后,会加大用户的工作量。这也从另一个侧面强调了代码设计与检查时需要很强的责任心。

6.9 初始化和类装载

在其他语言中,必须对初始化过程进行控制。比如C++在一个static 数据获得初始化之前,就有另一个 static数据希望它是一个有效值,那么就会造成问题。

Java 则没有这样的问题。由于 Java 中的一切东西都是对象,所以许多东西变得更加简单。正如下一章会讲到的那样,每个对象的代码都存在于独立的文件中。除非真的需要代码,否则那个文件是不会载入的。通常,除非那个类的一个对象构造完毕,否则代码不会真的载入。由于static 方法存在一些细微的歧义,所以也能认为“类代码在首次使用的时候载入”。

首次使用的地方也是static 初始化发生的地方。装载的时候,所有static对象和 static代码块都会按照本来的顺序初始化(亦即它们在类定义代码里写入的顺序)。当然,static 数据只会初始化一次。

6.9.1 继承初始化

//: Beetle.java
class Insect {
	int i = 9;
	int j;
	Insect() {
		prt("i = " + i + ", j = " + j);
		j = 39;
	}
	static int x1 = prt("static Insect.x1 initialized");
	static int prt(String s) {
		System.out.println(s);
		return 47;
	}
}

public class Beetle extends Insect {
	int k = prt("Beetle.k initialized");
	Beetle() {
		prt("k = " + k);
		prt("j = " + j);
	}
	static int x2 = prt("static Beetle.x2 initialized");
	static int prt(String s) {
		System.out.println(s);
		return 63;
	}
	public static void main(String[] args) {
		prt("Beetle constructor");
		Beetle b = new Beetle();
	}
}

输出:

static Insect.x1 initialized

static Beetle.x2 initialized

Beetle constructor

i = 9, j = 0

Beetle.k initialized

k = 63

j = 39

运行Beetle 时,首先发生的事情是装载程序到外面找到那个类。在装载过程中,装载程序注意它有一个基础类(即extends ),所以随之将其载入。若该类含有另一个基础类,则另一个基础类随即也会载入,以此类推。接下来,会在根基础类(Insect)执行 static 初始化,再在下一个衍生类执行,以此类推

此时,必要的类已全部装载完毕,可以创建对象。首先,这个对象中的所有基本数据类型都会设成它们的默认值,而将对象句柄设为null。随后会调用基础类构建器。在这种情况下,调用是自动进行的。但也完全可以用super 来自行指定构建器调用(就象在Beetle()构建器中的第一个操作一样)。基础类的构建采用与衍生类构建器完全相同的处理过程。基础顺构建器完成以后,实例变量会按本来的顺序得以初始化。最后,执行构建器剩余的主体部分。

 

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值