谜题66:一件私事
在下面的程序中,子类的一个域具有与超类的一个域相同的名字。那么,这个程序会打印出什么呢?
class Base {
public String className = "Base";
}
class Derived extends Base {
private String className = "Derived";
}
public class PrivateMatter {
public static void main(String[ ] args) {
System.out.println(new Derived().className);
}
}
题目:不可编译
原因:
如果className是一个实例方法,而不是一个实例域,那么Derived.className()将覆写Base.className(),而这样的程序是非法的。一个覆写方法的访问修饰符所提供的访问权限与被覆写方法的访问修饰符所提供的访问权限相比,至少要一样多[JLS 8.4.8.3]。因为className是一个域,所以Derived.className隐藏(hide)了Base.className,而不是覆盖了它[JLS 8.3]。对一个域来说,当它要隐藏另一个域时,如果隐藏域的访问修饰符提供的访问权限比被隐藏域的少,尽管这么做不可取的,但是它确实是合法的。事实上,对于隐藏域来说,如果它具有与被隐藏域完全无关的类型,也是合法的:即使Derived.className是GregorianCalendar类型的,Derived类也是合法的。
注:
注:
覆写与隐藏之间的一个非常大的区别。一旦一个方法在子类中被覆写,你就不能在子类的实例上调用它了(除了在子类内部,通过使用
super关键字来方法)。然而,你可以通过将子类实例转型为某个超类类型来访问到被隐藏的域,在这个超类中该域未被隐藏。
谜题67:对字符串上瘾
一个名字可以被用来引用位于不同包内的多个类。下面的程序就是在探究当你重用了一个平台类的名字时,会发生什么。你认为它会做些什么呢?尽管这个程序属于那种让你通常一看到就会感到尴尬的程序,但是你还是应该继续下去,把门锁上,把百叶窗拉上,然后试试看:
public class StrungOut {
public static void main(String[] args) {
String s = new String("Hello world");
System.out.println(s);
}
}
class String {
private final java.lang.String s;
public String(java.lang.String s) {
this.s = s;
}
public java.lang.String toString() {
return s;
}
}
注:避免重用类名
谜题68:灰色的阴影
下面的程序在相同的范围内具有两个名字相同的声明,并且没有任何明显的方式可以在它们二者之间做选择。这个程序会打印Black吗?它会打印White吗?甚至,它是合法的吗?
public class ShadesOfGray {
public static void main(String[] args){
System.out.println(X.Y.Z);
}
}
class X {
static class Y {
static String Z = "Black";
}
static C Y = new C();
}
class C {
String Z = "White";
}
题目:White!
谜题69:黑色的渐隐
假设你不能修改前一个谜题(谜题68)中的X和C这两个类。你能否编写一个类,其main方法将读取X.Y类中的Z域的值,然后打印它。注意,不能使用反射。
本谜题初看起来是不可能实现的。毕竟,X.Y类被具有相同名字的一个域给遮掩了,因此对其命名的尝试将引用到该域上。
事实上,我们是可以引用到一个被遮掩的类型名的,其技巧就是在某一种特殊的语法上下文环境中使用该名字,在该语法上下文环境中允许出现一个类型但是不允许出现一个变量。在转型表达式的括号中间的部分就是这样一种上下文环境。下面的程序通过使用这种技术解决了这个谜题,并且将打印出我们所期望的Black:
public class FadeToBlack {
public static void main(String[] args){
System.out.println(((X.Y)null).Z);
}
}
请注意,我们是用一个具有X.Y类型的表达式来访问X.Y类的Z域的。就像我们在谜题48和54中所看到的,用一个表达式而不是类型名来访问一个静态成员是合法的,但却是一种有问题的用法。
不借助这种有问题的用法,而是通过在一个类声明的extends子句中使用一个被遮掩的类这种方式,你也可以解决本谜题。因为基类总是一种类型,出现在extends子句中的名字从来都不会被解析为变量名。下面的程序就展示了这项技术,它也会打印出Black:
public class FadeToBlack {
static class Xy extends X.Y{ }
public static void main(String[ ] args){
System.out.println(Xy.Z);
}
}
如果你使用的5.0或更新的版本,那么通过在一个类型变量声明的extends子句中使用X.Y这种方式,你也可以解决本谜题:
public class FadeToBlack {
public static <T extends X.Y> void main(String[] args){
System.out.println(T.Z);
}
}
总之,要解决由类型被变量遮掩而引发的问题,需要按照标准的命名习惯来重命名类型和变量,就像在谜题68中所讨论的那样。如果做不到这一点,那么你应该在只允许类型名的上下文环境中使用被遮掩的类型名。幸运的话,你将永远不需要凭借这种对程序的变形来解决问题,因为大多数的类库作者都很明智,他们都避免了必需使用这种变形的有问题的用法。然而,如果你确实发现自己身处这种境地,那么你最好是要了解这个问题需要解决。
谜题70:一揽子交易
下面这个程序设计在不同的包中的两个类的交互,main方法位于hack.TypeIt中。那么,这个程序会打印什么呢?
package hack;
import click.CodeTalk;
public class TypeIt {
private static class ClickIt extends CodeTalk {
void printMessage() {
System.out.println("Hack");
}
}
public static void main(String[ ] args) {
ClickIt clickit = new ClickIt();
clickit.doIt();
}
}
package click;
public class CodeTalk {
public void doIt() {
printMessage();
}
void printMessage() {
System.out.println("Click");
}
}
题目:打印的将是Click
解决:一个包内私有的方法不能被位于另一个包中的某个方法直接覆写[JLS 8.4.8]。
谜题71:进口税
在5.0版中,Java平台引入了大量的可以使操作数组变得更加容易的工具。下面这个谜题使用了变量参数、自动包装、静态导入(请查看http://java.sun.com/j2se/5.0/docs/guide/language [Java-5.0])以及便捷方法Arrays.toString(请查看谜题60)。那么,这个程序会打印什么呢?
import static java.util.Arrays.toString;
class ImportDuty {
public static void main(String[ ] args) {
printArgs(1, 2, 3, 4, 5);
}
static void printArgs(Object... args) {
System.out.println(toString(args));
}
}
题目:
ImportDuty.java:9:Object.toString()can't be applied to(Object[])
System.out.println(toString(args));原因:
在5.0版中,Java平台引入了大量的可以使操作数组变得更加容易的工具。下面这个谜题使用了变量参数、自动包装、静态导入(请查看http://java.sun.com/j2se/5.0/docs/guide/language [Java-5.0])以及便捷方法Arrays.toString(请查看谜题60)。那么,这个程序会打印什么呢?
import static java.util.Arrays.toString;
class ImportDuty {
public static void main(String[ ] args) {
printArgs(1, 2, 3, 4, 5);
}
static void printArgs(Object... args) {
System.out.println(toString(args));
}
}
你可能会期望该程序打印[1,2,3,4,5],实际上它确实会这么做,只要它能编译。令人沮丧的是,看起来编译器找不到恰当的toString方法:
ImportDuty.java:9:Object.toString()can't be applied to(Object[])
System.out.println(toString(args));
^
是不是编译器的理解力太差了?为什么它会尝试着去应用Object.toString()呢?它与调用参数列表并不匹配,而Arrays.toString(Object[ ])却可以完全匹配。
编译器在选择在运行期将被调用的方法时,所作的第一件事就是在肯定能找到该方法的范围内挑选[JLS 15.12.1]。编译器将在包含了具有恰当名字的方法的最小闭合范围内进行挑选,在我们的程序中,这个范围就是ImportDuty类,它包含了从Object继承而来的toString方法。在这个范围中没有任何可以应用于toString(args)调用的方法,因此编译器必须拒绝该程序。
换句话说,我们想要的toString方法没有在调用点所处的范围内。导入的toString方法被ImportDuty从Object那里继承而来的具有相同名字的方法所遮蔽(shade)了[JLS 6.3.1]。遮蔽与遮掩(谜题68)非常相像,二者的关键区别是一个声明只能遮蔽类型相同的另一个声明:一个类型声明可以遮蔽另一个类型声明,一个变量声明可以遮蔽另一个变量声明,一个方法声明可以遮蔽另一个方法声明。与其形成对照的是,变量声明可以遮掩类型和包声明,而类型声明也可以遮掩包声明。
当一个声明遮蔽了另一个声明时,简单名将引用到遮蔽声明中的实体。在本例中,toString引用的是从Object继承而来的toString方法。简单地说,本身就属于某个范围的成员在该范围内与静态导入相比具有优先权。这导致的后果之一就是与Object的方法具有相同名字的静态方法不能通过静态导入工具而得到使用。
既然你不能对Arrays.toString使用静态导入,那么你就应该用一个普通的导入声明来代替。下面就是Arrays.toString应该被正确使用的方式:
import java.util.Arrays;
class ImportDuty {
static void printArgs(Object... args) {
System.out.println(Arrays.toString(args));
}
}
如果你特别强烈地想避免显式地限定Arrays.toString调用,那么你可以编写你自己的私有静态转发方法:
private static String toString(Object[] a) {
return Arrays.toString(a);
}
静态导入工具所专门针对的情况是:程序中会重复地使用另一个类的静态元素,而每一次用到的时候都进行限定又会使程序变得乱成一锅粥。在这类情况中,静态导入工具可以显著地提高可读性。这比通过实现接口来继承其常量要安全得多,而实现接口这种做法是你从来都不应该采用的 [EJ Item 17]。然而,滥用静态导入工具也会损害可读性,因为这会使得静态成员的类在何处被使用显得非常不清晰。应该有节制地使用静态导入,只有在非常需要的情况下才应该使用它们。
对API设计者来说,要意识到当某个方法的名字已经出现在某个作用域内时,静态导入工具并不能被有效地作用于该方法上。这意味着静态导入不能用于那些与通用接口中的方法共享方法名的静态方法,而且也从来不能用于那些与Object中的方法共享方法名的静态方法。再次说明一下,本谜题所要说明的仍然是你在覆写之外的情况中使用名字重用通常都会产生混乱。我们通过重载、隐藏和遮掩看清楚了这一点,现在我们又通过遮蔽看到了同样的问题。
谜题72:终极危难
本谜题旨在检验当你试图隐藏一个final域时将要发生的事情。下面的程序将做些什么呢?
class Jeopardy {
public static final String PRIZE = "$64,000";
}
public class DoubleJeopardy extends Jeopardy {
public static final String PRIZE = "2 cents";
public static void main(String[ ] args) {
System.out.println(DoubleJeopardy.PRIZE);
}
}
打印: 2 cents
原因:final修饰符对方法和域而言,意味着某些完全不同的事情。对于方法,final意味着该方法不能被覆写(对实例方法而言)或者隐藏(对静态方法而言)[JLS 8.4.3.3]。对于域,final意味着该域不能被赋值超过一次[JLS 8.3.1.2]。
谜题73:你的隐私正在公开
略:谜题74:同一性的危机
下面的程序是不完整的,它缺乏对Enigma的声明,这个类扩展自java.lang.Object。请为Enigma提供一个声明,它可以使该程序打印false:
public class Conundrum {
public static void main(String[] args) {
Enigma e = new Enigma();
System.out.println(e.equals(e));
}
}
答案 :
final class Enigma {
// Don’t do this!
public Boolean equals(Enigma other){
return false;
}
}
尽管这个声明能够解决本谜题,但是它的做法确实非常不好的。它违反了谜题58的建议:如果同一个方法的两个重载版本都可以应用于某些参数,那么它们应该具有相同的行为。在本例中,e.equals(e)和e.equals((Object)e)将返回不同的结果,其潜在的混乱是显而易见的。
然而,有一种解谜方案是不会违反这项建议的:
final class Enigma {
public Enigma() {
System.out.println(false);
System.exit(0);
}
}
谜题75:头还是尾?
覆写(override)
一个实例方法可以覆写(override)在其超类中可访问到的具有相同签名的所有实例方法[JLS 8.4.8.1],从而使能了动态分派(dynamic dispatch);换句话说,VM将基于实例的运行期类型来选择要调用的覆写方法[JLS 15.12.4.4]。覆写是面向对象编程技术的基础,并且是唯一没有被普遍劝阻的名字重用形式:
class Base {
public void f() { }
}
class Derived extends Base {
public void f() { } // overrides Base.f()
}
隐藏(hide)
一个域、静态方法或成员类型可以分别隐藏(hide)在其超类中可访问到的具有相同名字(对方法而言就是相同的方法签名)的所有域、静态方法或成员类型。隐藏一个成员将阻止其被继承[JLS 8.3, 8.4.8.2, 8.5]:
class Base {
public static void f() { }
}
class Derived extends Base {
private static void f() { } // hides Base.f()
}
重载(overload)
在某个类中的方法可以重载(overload)另一个方法,只要它们具有相同的名字和不同的签名。由调用所指定的重载方法是在编译期选定的[JLS 8.4.9, 15.12.2]:
class CircuitBreaker {
public void f(int i) { } // int overloading
public void f(String s) { } // String overloading
}
遮蔽(shadow)
一个变量、方法或类型可以分别遮蔽(shadow)在一个闭合的文本范围内的具有相同名字的所有变量、方法或类型。如果一个实体被遮蔽了,那么你用它的简单名是无法引用到它的;根据实体的不同,有时你根本就无法引用到它[JLS 6.3.1]:
class WhoKnows {
static String sentence = "I don't know.";
public static woid main(String[ ] args) {
String sentence = “I know!”; // shadows static field
System.out.println(sentence); // prints local variable
}
}
尽管遮蔽通常是被劝阻的,但是有一种通用的惯用法确实涉及遮蔽。构造器经常将来自其所在类的某个域名重用为一个参数,以传递这个命名域的值。这种惯用法并不是没有风险,但是大多数Java程序员都认为这种风格带来的实惠要超过其风险:
class Belt {
private final int size;
public Belt(int size) { // Parameter shadows Belt.size
this.size = size;
}
}
遮掩(obscure)
一个变量可以遮掩具有相同名字的一个类型,只要它们都在同一个范围内:如果这个名字被用于变量与类型都被许可的范围,那么它将引用到变量上。相似地,一个变量或一个类型可以遮掩一个包。遮掩是唯一一种两个名字位于不同的名字空间的名字重用形式,这些名字空间包括:变量、包、方法或类型。如果一个类型或一个包被遮掩了,那么你不能通过其简单名引用到它,除非是在这样一个上下文环境中,即语法只允许在其名字空间中出现一种名字。遵守命名习惯就可以极大地消除产生遮掩的可能性[JLS 6.3.2, 6.5]:
public class Obscure {
static String System; // Obscures type java.lang.System
public static void main(String[ ] args) {
// Next line won't compile: System refers to static field
System.out.println(“hello, obscure world!”);
}
}