17. Item 6: 避免落入 隐藏变量成员 的陷阱
尽管在子类的外部无法访问父类中被覆盖的实例方法 但你也可以很容易意识到这将会
是一个产生错误的潜在根源 假如你怀疑你所使用的某个对象实际上是一个子类的实例 那
么你可以调用它的 getClass().getName()方法来判断它的真实身份 又如果你是编写增加了新
功能的子类的程序员 那你必须确保进行兼容性测试 或者保证在编写程序时 任何新增的
功能都是通过增加新的方法 而不是覆盖父类方法实现的
Item 6: 避免落入 隐藏变量成员 的陷阱
在 Java 语言中 与理解方法是如何被覆盖同等重要的就是 理解变量成员是如何被隐藏
的 假如你认为自己已经理解了方法是如何被覆盖的 依此类推变量成员是如何被隐藏的也
是同样道理的话 那么你最好仔细地读读本节 在程序中 无意地隐藏了一个变量成员或者
错误地认为已经“覆盖”了一个变量成员 都会导致错误的结果
01: public class Wealthy
02: {
03: public String answer = "Yes!";
04: public void wantMoney()
05: {
06: System.out.println("Would you like $1,000,000? > "+ answer);
07: }
08: public static void main(String[] args)
09: {
10: Wealthy w = new Wealthy();
11: w.wantMoney();
12: }
13: }
输出结果为
Would you like $1,000,000? > Yes!
在上例中 Wealthy 类具有一个名为 answer 的实例变量 一个名为 wantMoney 的方法
以及一个 main 方法 在 main 方法中 一个 Wealthy 类的实例 w 被创建 w 调用它自己的
wantMoney 方法 输出了一个问题以及作为回答的实例变量 answer 的值 上例正确地回答了
问题 现在让我们来看看一个没有正确回答这个问题的例子
u 17
18. 第 1 部分 语法
01: public class Poor
02: {
03: public String answer = "Yes!";
04: public void wantMoney()
05: {
06: String answer = "No!"; // hides instance variable answer
07: System.out.println("Would you like $1,000,000? > " + answer);
08: }
09: public static void main(String[] args)
10: {
11: Poor p = new Poor();
12: p.wantMoney();
13: }
14: }
输出结果为
Would you like $1,000,000? > No!
注意 本例输出中的回答已经变成了“No ” 局部变量 answer 隐藏了实例变量 answer
因此 回答的结果就是局部变量的值 这个例子很简单 也显而易见 局部变量 answer 隐藏
了实例变量 answer 产生了意想不到的结果 然而 在许多复杂的环境下 一个变量成员由
于意外而被覆盖所引起的问题可能就很难被发现了 为了避免“数据隐藏”所带来的问题 理
解下面的知识点就显得非常重要了
l 不同类型的 Java 变量
l 变量的作用范围
l 何种变量能被隐藏
l 变量是如何被隐藏的
l 如何访问被隐藏的变量
l 变量隐藏与方法覆盖的区别
不同类型的 Java 变量
Java 一共有 6 种变量类型 类变量 实例变量 方法参数 构造函数参数 异常处理器
参数 以及局部变量 类变量包括在类体中声明的静态数据成员以及在接口体中声明的静态
或非静态数据成员 实例变量是在类体中声明的非静态变量 术语“变量成员”指的是类变量
和实例变量 方法参数是用来传入一个方法体的 构造函数参数是用来传入一个构造函数体
18 t
19. Item 6: 避免落入 隐藏变量成员 的陷阱
的 异常处理器参数是用来传入一个 try 语句的 catch 块中的 最后 局部变量是在一个代码
块中或一个 for 语句中声明的变量
下面的例子声明了各种类型的变量
01: public class Types
02: {
03: int x; // instance variable
04: static int y; // class variable
05: public Types(String s) // s is a constructor parameter
06: {
07: // constructor code f.
08: }
09: public createURL(String urlString) //urlString is a method parameter
10: {
11: String name = "example"; // name is a local variable
12: try
13: {
14: URL url = new URL(urlString);
15: }
16: catch(Exception e) // e is a exception-handler parameter
17: {
18: // handle exception
19: }
20: }
21: }
变量的作用范围
变量的作用范围指的其实是一个代码块 在这个代码块中 可以通过该变量的简名来引
用它 简名是一个变量的专一标识符 在上例的第 3 行中 实例变量 x 的简名就是“x” 实例
变量和类变量的作用范围就是声明它们的类或接口的整体 变量成员 x 和 y 的作用范围就是
Types 类的类体 方法参数的作用范围就是整个方法体 构造函数参数的作用范围就是整个
构造函数体 异常处理器参数的作用范围就是 catch 语句块 局部变量的作用范围就是它被
声明的所在代码块 上例的局部变量 name 它在第 11 行也就是 createURL 方法中被声明
那么它的作用范围就是 createURL 方法的整个方法体
u 19
20. 第 1 部分 语法
何种变量能被隐藏
实例变量和类变量能被隐藏 局部变量和各种参数永远不会被隐藏 假如用一个同名的
局部变量去隐藏一个参数 编译器将会报错 同样地 用一个同名的局部变量去隐藏另一个
局部变量 编译器也会报错
01: class Hidden
02: {
03: public static void main(String[] args)
04: {
05: int args = 0; // illegal – results in a compiler error
06: String s = "string";
07: int s = 10; // illegal – results in a compiler error
08: }
09: }
上例中 第 5 行的局部变量不能和方法参数 args 同名 同样 第 7 行的局部变量 s 也会
引起编译器错误 因为它不能和另一个同名的局部变量存在于相同的作用范围内
实例变量和类变量如何被隐藏
同名的局部变量或者同名的参数 可以隐藏掉变量成员的一部分作用范围 变量成员也
能被子类的同名变量成员隐藏 与一个变量成员同名的局部变量 将在其作用范围内 隐藏
掉这个变量成员 与一个变量成员同名的方法参数 将在方法体中隐藏掉这个变量成员 与
一个变量成员同名的构造函数参数 将在构造函数体中隐藏掉这个变量成员 依此类推 与
一个变量成员同名的异常处理器参数 将在 catch 语句块中隐藏掉这个变量成员
01: public class Bike
02: {
03: String type;
04: public Bike(String type)
05: {
06: System.out.println("type =" + type);
07: }
08: }
20 t
21. Item 6: 避免落入 隐藏变量成员 的陷阱
上例中 构造函数参数 type 隐藏了实例变量 type System.out.println 方法输出的 type 变
量的值将是构造函数参数 type 的值 而不是实例变量 type 的值
子类的变量成员将会隐藏掉父类中同名的变量成员
01: public class Bike
02: {
03: String type = "generic";
04: }
01: public class MountainBike extends Bike
02: {
03: String type = "All terrain";
04: }
上例中 Bike 类中的实例变量 type 被子类 MoutainByte 的实例变量 type 隐藏了 子类
的类变量将会隐藏父类中与之同名的类变量和实例变量 同理 子类的实例变量也会隐藏父
类中与之同名的类变量和实例变量
01: public interface Stretchable
02: {
03: int y
04: }
01: public class Line
02: {
03: int x;
04: }
01: public class MultiLine extends Line implements Stretchable
02: {
03: public MultiLine()
04: {
05: System.out.println("x = " + x);
06: }
07: }
上例的程序可以编译通过 假如向 Strectchable 接口中加入一个名为 x 的变量 那么
MultiLine 类将无法通过编译 因为它尝试按简名去引用一个被多重继承的变量 x
u 21
22. 第 1 部分 语法
如何访问被隐藏的变量
通过全局名 可以访问大多数的变量成员 关键字“this”可以限定一个正被局部变量隐藏
的实例变量 关键字“super”可以限定一个正被子类隐藏的实例变量 类变量也可以被限定
只要在该类的名字与该类变量的名字之间加上“.”即可
01: public class Wealthy
02: {
03: public String answer = "Yes!";
04: public void wantMoney()
05: {
06: String answer = "No!";
07: System.out.println("Do you want to give me $1,000,000? > " +
08: answer);
09: System.out.println("Would you like $1,000,000? > " +
10: this.answer);
11: }
12: public static void main(String[] args)
13: {
14: Wealthy w = new Wealthy();
15: w.wantMoney();
16: }
17: }
输出结果为
Do you want to give me $1,000,000 > No!
Would you like $1,000,000? > Yes!
上例中 Wealthy 类具有一个名为 answer 的实例变量 wantMoney 方法也声明了一个名
为 answer 的局部变量 为了对 wantMoney 方法中的每个问题都给出正确的回答 我们需要
访问局部变量 answer 也需要访问实例变量 answer 通过使用关键字“this” 我们告知编译器
我们需要的是实例变量 answer 而非局部变量 answer 正如输出结果所显示的 第一个问题
的回答是局部变量 answer 的值 第二个问题的回答是实例变量 answer 的值 它被关键字“this”
限定了
下例显示了如何限定父类中被隐藏的一个实例变量
01: public class StillWealthy extends Wealthy
02: {
03: public String answer = "No!";
22 t
23. Item 6: 避免落入 隐藏变量成员 的陷阱
04: public void wantMoney()
05: {
06: String answer = "maybe?";
07: System.out.println("Did you see that henway? > " + answer);
08: System.out.println("Do you want to give me $1,000,000? > " +
09: this.answer);
10: System.out.println("Would you like $1,000,000? > " + super.answer);
11: }
12: public static void main(String[] args)
13: {
14: Wealthy w = new Wealthy();
15: w.wantMoney();
16: }
17: }
输出结果为
Did you see that henway? > maybe?
Do you want to give me $1,000,000 > No!
Would you like $1,000,000? > Yes!
注意 上例中第 7 行问题的回答 输出的是局部变量 answer 的值 第 8 行问题的回答
输出的是 StillWealthy 子类的实例变量 它被关键字“this”限定了 第 10 行问题的回答 输出
的是父类 Wealthy 的实例变量 它被关键字“super”限定了
变量隐藏与方法覆盖的区别
隐藏变量和覆盖方法有许多区别 也许它们之间最重要的不同就是 一个类的实例 无
法通过使用全局名 或者强制转换自己为其父类的类型 以访问其父类中被覆盖的方法
01: public class Wealthier extends Wealthy
02: {
03: public void wantMoney()
04: {
05: System.out.println("Would you like $2,000,000? > " + answer);
06: }
07: public static void main(String[] args)
08: {
09: Wealthier w = new Wealthier();
10: w.wantMoney();
11: ((Wealthy)w).wantMoney();
12: }
13: }
u 23
24. 第 1 部分 语法
输出结果为
Would you like $2,000,000? > Yes!
Would you like $2,000,000? > Yes!
上例中 Wealthier 类继承了 Wealthy 类 并且覆盖了 wantMoney 方法 main 方法创建了
Wealthier 类的一个实例 w 并调用它的 wantMoney()方法 注意 它的结果就是上例输出的
第一行 接着 main 方法又强制将实例 w 转换为它父类的类型 Wealthy 并再次调用它的
wantMoney()方法 它的结果仍旧和之前一样 上面的例子说明 通过强制转换子类的实例为
父类类型 是无法访问父类中被覆盖的方法的 而下面的例子显示了 一个被隐藏的变量与
一个被覆盖的方法的区别 也就是说 强制转换子类的实例为父类类型后 我们就可以访问
父类中被隐藏的变量了
01: public class Poorer extends Wealthier
02: {
03: String answer = "No!";
04: public void wantMoney()
05: {
06: System.out.println("Would you like $3,000,000? > " + answer);
07: }
08: public static void main(String[] args)
09: {
10: Poorer p = new Poorer();
11: ((Wealthier)p).wantMoney();
12: System.out.println("Are you sure? > " + ((Wealthier)p).answer);
13: }
14: }
输出结果为
Do you want $3,000,000? ? No!
Are you sure? > Yes!
Poorer 类继承了 Wealthier 类 main 方法创建了一个 Poorer 类的实例 p 然后强制转换 p
为其父类类型 正如在前一个例子中解释的 由于 wantMoney 方法被覆盖 通过强制转换
是不能访问父类的 wantMoney 方法的 因此 上例第 11 行调用的仍旧是子类 Poorer 的
wantMoney 方法 它的回答是“No!” 接着 main 方法又问到 “Are you sure?>” 这个问题
答案就不再是子类变量的值 而是父类中被隐藏变量的值了 这种情况的出现 是由于子类
24 t
25. Item 7: 提前引用
仅仅“隐藏”了父类的变量成员 所以 只要将子类实例强制转换为父类类型 我们就可以访
问父类中被隐藏的变量成员了 数据隐藏与方法覆盖的另外一个不同 就是静态方法不能覆
盖父类的实例方法 而静态变量 却可以隐藏父类的一个同名实例变量 相同地 实例方法
也不能覆盖父类的同名静态方法 而变量成员却可以隐藏父类同名变量成员 不论父类的这
个同名变量成员是类变量还是实例变量
通过理解本节讨论的知识点 避免“隐藏变量”的陷阱 会帮助应用程序得到你所期望的
结果 并且为你节省花费在复杂程序调试上的大量时间
Item 7: 提前引用
类变量以及静态初始化块是在类被加载进 JVM 时执行初始化操作的 Java 语言规范 8.5
节指出 “静态初始化块和类变量是按照其在代码中出现的顺序依次执行初始化操作的 而不
能在类变量声明出现之前就引用它” 换句话说 这些语句被处理的顺序就是它们在代码中出
现的顺序 一般来说 编译器会捕捉到任何的提前引用 看看下面的代码
1: public class ForwardReference
2: {
3: int first = second; // this will fail to compile
4: int second = 2;
5: }
尝试去编译这个类 将会得到一个如下的错误
ForwardReference.java:3: Can't make forward reference to second in class
ForwardReference.
所以说 即使变量 first 和 second 都处在同一个作用范围内 Java 语言规范也不允许这种
类型的无效初始化 而且编译器会捕捉到这个错误
可是 绕开这个保护措施还是有可能的 Java 允许方法调用出现在类变量的初始化之前
而且方法内部对类变量的访问不会按照这个原则被检查 下面的程序将会编译通过
01: public class ForwardReferenceViaMethod
02: {
03: static int first = accessTooSoon();
04: static int second = 1;
05:
06: static int accessTooSoon()
07: {
u 25