Thinking in Java 第七章——复用类(1)
Thinking in Java 第七章——复用类(2)
Thinking in Java 第七章——复用类(3)
六、final关键字
根据上下文环境,Java的关键字final
的含义存在着细微的差别,但是通常它是指:“这是无法改变的”,不想做改变可能处于两种理由:设计或者是效率。这两个原因相差很远,所以final
可能会被误用。下面从数据,方法和类三个方面记录final
。
6.1 final数据
某一块数据区域是恒定不变的:
- 一个永不改变的编译时常量。
一个在运行时被初始化的值,而你不希望它被改变。
对于编译器常量这种情况,编译器可以将该常量值带入任何可能用到它的计算式中,也就是说,可以在编译时执行计算式,这减轻了一些运行时负担。在Java中,这类常量必须是基本数据类型,并且以关键字
final
表示。在对这个常量进行定义的时候,必须进行赋值。一个既是
static
又是final
的域值占据一段不能改变的存储空间。并且要用大写表示,用下划线分割每个单词。当对对象引用的时候,
final
使引用恒定不变,一旦引用被初始化指向一个对象没救无法再把它改为指向另一个对象,然而,对象本身却是可以被修改的。
例如:
import java.util.*;
import static net.mindview.util.Print.*;
class Value {
int i; // Package access
public Value(int i) { this.i = i; }
}
public class FinalData {
private static Random rand = new Random(47);
private String id;
public FinalData(String id) { this.id = id; }
// 编译时常量:
private final int valueOne = 9;
private static final int VALUE_TWO = 99;
// 典型的常量定义方式;public表示可用于包之外;static强调只有一份;final说明是一个常量
public static final int VALUE_THREE = 39;
// 运行时被初始化的值:
private final int i4 = rand.nextInt(20);
static final int INT_5 = rand.nextInt(20);
private Value v1 = new Value(11);
private final Value v2 = new Value(22);
private static final Value VAL_3 = new Value(33);
// Arrays:
private final int[] a = { 1, 2, 3, 4, 5, 6 };
public String toString() {
return id + ": " + "i4 = " + i4 + ", INT_5 = " + INT_5;
}
public static void main(String[] args) {
FinalData fd1 = new FinalData("fd1");
//! fd1.valueOne++; // 错误:不能修改
fd1.v2.i++; // Object isn't constant!
fd1.v1 = new Value(9); // OK -- not final
for(int i = 0; i < fd1.a.length; i++)
fd1.a[i]++; // Object isn't constant!
//! fd1.v2 = new Value(0); // Error: Can't
//! fd1.VAL_3 = new Value(1); // change reference
//! fd1.a = new int[3];
print(fd1);
print("Creating new FinalData");
FinalData fd2 = new FinalData("fd2");
print(fd1);
print(fd2);
}
} /* Output:
fd1: i4 = 15, INT_5 = 18
Creating new FinalData
fd1: i4 = 15, INT_5 = 18
fd2: i4 = 13, INT_5 = 18
*///:~
valueOne
和VAL_ONE
都是带有编译时数值的final
类型,所以它们二者均可以用作编译期常量,没有重大区别。VAL_THREE
就是一种更加典型的对常量进行定义的方式。public
表示可用于包之外;static
强调只有一份;final
说明是一个常量。不能因为某数据是
final
的就认为在编译期就可以知道它的值。在运行时生成的数值i4
和INT_5
就说明了这一点。将
final
的数值定义为静态和非静态的区别只有在数值在运行时内被初始化时才会显现。这是因为编译期对编译时数值一视同仁。v1
到VAL_3
这些变量说明了final引用的意义。
空白final
java还允许生成“空白final”即,被申明为final但是又未给定初值的域。无论什么情况,编译器都确保空白final在使用前必须被初始化。
例如:
class Poppet {
private int i;
Poppet(int ii) { i = ii; }
}
public class BlankFinal {
private final int i = 0; // Initialized final
private final int j; // Blank final
private final Poppet p; // Blank final reference
// Blank finals MUST be initialized in the constructor:
public BlankFinal() {
j = 1; // Initialize blank final
p = new Poppet(1); // Initialize blank final reference
}
public BlankFinal(int x) {
j = x; // Initialize blank final
p = new Poppet(x); // Initialize blank final reference
}
public static void main(String[] args) {
new BlankFinal();
new BlankFinal(47);
}
} ///:~
final参数
Java允许在参数列表中以声明的方式将参数指定为final的,这意味着你无法在方法中更改参数引用所指向的对象。
例如:
class Gizmo {
public void spin() {}
}
public class FinalArguments {
void with(final Gizmo g) {
//! g = new Gizmo(); // Illegal -- g is final
}
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);
}
} ///:~
方法f(),g()
展示了当基本类型的参数被指明为final时所出现的结果:你可以读参数,但是无法修改。这一特性主要用来想匿名内部类传递数据。
6.2 final方法
使用final方法的原因有两个:
- 将方法锁定,以防任何继承类修改它的含义。展示处于设计的考虑:想要确保在继承中使方法行为保持不变,并且不会被覆盖。
- 在过去建议使用final的第二个原因是效率。在Java的早期实现中,如果将一个方法指明为final,就是同意编译期将针对该方法的所有调用都转为内嵌调用。
【在最近的Java版本中,虚拟机可以探测到这些情况,并优化去掉这些效率反而降低的额外的内嵌调用,因此不需要使用final方法来进行优化。】
final和private关键字
类中所有private方法都隐式地指定为final的。由于无法取用private方法,所以也就无法覆盖它。可以对private方法添加修饰词final,但是没有任何意思。
当在子类中试图覆盖父类的一个private方法(隐含是final的)时,其实不是覆盖,只是具有相同的名称而已。
例如:
import static net.mindview.util.Print.*;
class WithFinals {
// Identical to "private" alone:
private final void f() { print("WithFinals.f()"); }
// Also automatically "final":
private void g() { print("WithFinals.g()"); }
}
class OverridingPrivate extends WithFinals {
private final void f() {
print("OverridingPrivate.f()");
}
private void g() {
print("OverridingPrivate.g()");
}
}
class OverridingPrivate2 extends OverridingPrivate {
public final void f() {
print("OverridingPrivate2.f()");
}
public void g() {
print("OverridingPrivate2.g()");
}
}
public class FinalOverridingIllusion {
public static void main(String[] args) {
OverridingPrivate2 op2 = new OverridingPrivate2();
op2.f();
op2.g();
// You can upcast:
OverridingPrivate op = op2;
// But you can't call the methods:
//! op.f();
//! op.g();
// Same here:
WithFinals wf = op2;
//! wf.f();
//! wf.g();
}
} /* Output:
OverridingPrivate2.f()
OverridingPrivate2.g()
*///:~
请注意:“覆盖”只有在某方法是基类的接口的一部分时才会出现。即,必须能将一个对象向上转型为它的基本类型并调用相同的方法。如果方法为private
,他就不是基类的接口的一部分,它只是一些隐藏于类中的程序代码,只不过是具有相同的名称而已。
认真体会上面的描述
使用@override
注解可以解决上述问题!!!
6.3 final类
当将某个类的整体定义为final时,就表明了你不打算继承该类,而且也不允许别人这样做。换句话说,出于某种考虑,你对该类的设计永远不需要做任何变动,或者处于安全的考虑,你不希望它有子类。
class SmallBrain {}
final class Dinosaur {
int i = 7;
int j = 1;
SmallBrain x = new SmallBrain();
void f() {}
}
//! class Further extends Dinosaur {}
// error: Cannot extend final class 'Dinosaur'
public class Jurassic {
public static void main(String[] args) {
Dinosaur n = new Dinosaur();
n.f();
n.i = 40;
n.j++;
}
} ///:~
注意:final类的所有方法都隐式制定为时final的,因为无法覆盖他们。
七、初始化及类的加载
每个类的编译代码都存在于它自己的独立文件中。该文件只在需要使用程序代码时才被加载。一般来说,可以说:“类的代码在初次使用时才加载。”这通常是指加载发生于创建类的第一个对象之时,但是当访问static域或者static方法时,也会发生加载。更准确的讲:类是在其任何static成员被访问时加载的。
7.1继承与初始化
通过下面例子,记录初始化全过程,对所发生的有一个全局性的把握。
import static net.mindview.util.Print.*;
class Insect {
private int i = 9;
protected int j;
Insect() {
print("i = " + i + ", j = " + j);
j = 39;
}
private static int x1 =
printInit("static Insect.x1 initialized");
static int printInit(String s) {
print(s);
return 47;
}
}
public class Beetle extends Insect {
private int k = printInit("Beetle.k initialized");
public Beetle() {
print("k = " + k);
print("j = " + j);
}
private static int x2 =
printInit("static Beetle.x2 initialized");
public static void main(String[] args) {
print("Beetle constructor");
Beetle b = new Beetle();
}
} /* Output:
static Insect.x1 initialized
static Beetle.x2 initialized
Beetle constructor
i = 9, j = 0
Beetle.k initialized
k = 47
j = 39
*///:~
运行
Beetle
时,所发生的第一件事就是试图访问Beetle.main()
(一个static
方法),于是加载器开始启动并找出Beetle
类的编译代码(在名为Beetle.class
文件之中)。在对它进行加载的过程中,编译期知道有一个基类(通过关键字
extends
得知),于是它继续加载,不管你是否打算产生一个基类的对象,这个过程都要发生。(如果此基类还有其自身的基类,那么第二个基类就会被加载,以此类推。)接下来,基类中的
static
初始化即会被执行,然后是下一个导出类,以此类推。这种方式很重要,因为导出类的static
初始化可能会依赖于基类成员能否被正确初始化。至此,必要的类都已经加载完毕,接下来就是创建对象了。
首先,对象中所有的基本类型就会被设为默认值,引用类型被设为
null
。然后基类的构造器会被调用。在本例中它是被自动调用的,也可以通过
super
关键字调用。基类构造器和导出类构造器一样,以相同是顺序来经历相同的过程。在基类构造器完成以后,实例变量按其次序被初始化。
最后,构造器的其余部分被执行。
第七章总结
尽管面向对象编程对继承极力强调,但是在开始设计一个系统的时候,一般优先选择使用组合(或者代理),只在确实必要时才使用继承。因为组合更具备灵活性。
在设计一个系统时,目标是找到或创建某些类,其中每个类都有具体的用途,而且既不会太大(包含太过功能难以服用),也不会太小(不添加其他功能就无法使用)。如果你的设计变得过于复杂,通过将现有类拆分为更小的部分而添加更多的对象,通常会有所帮助。
当你开始设计一个系统时候,应该认识到程序开发是一个增量过程,犹如我们学习一样,这一点很重要。程序开发依赖于试验,你可以尽自己所能去分析,但当你开始执行一个项目时,你任然无法知道所有的答案。如果将项目视作一种有机的,进化着的生命去培养,而不是打算想盖摩天大楼一样快速见效,就会获得更多的成功和迅速的回馈。继承和组合正是面向对象程序设计中使得你可以执行这种试验的最基本的两个工具。