- 令人混淆的构造器案例
构造函数在学习的过程中是容易混淆的,下面这段代码现给你了两个容易令人混淆的构造器。main 方法调用了一个构造器,但是它调用的到底是哪一个呢?该程序的输出取决于这个问题的答案。那么它到底会打印出什么呢?甚至它是否是合法的呢?
public class Confusing {
private Confusing(Object o) {
System.out.println("Object");
}
private Confusing(double[] dArray) {
System.out.println("double array");
}
public static void main(String[] args) {
new Confusing(null);
}
}
传递给构造器的参数是一个空的对象引用,因此,初看起来,该程序好像应该调用参数类型为Object 的重载版本,并且将打印出Object。另一方面,数组也是引用类型,因此null 也可以应用于类型为double[ ]的重载版本。但是该程序打印的是double array。
这种行为可能显得有悖常理,但是有一个很好的理由可以解释它。Java 的重载解析过程是以两阶段运行的。第一阶段选取所有可获得并且可应用的方法或构造器。第二阶段在第一阶段选取的方法或构造器中选取最精确的一个。如果一个方法或构造器可以接受传递给另一个方法或构造器的任何参数,那么我们就说第一个方法比第二个方法缺乏精确性[JLS 15.12.2.5]。
在程序中,两个构造器都是可获得并且可应用的。构造器Confusing(Object)可以接受任何传递给Confusing(double[ ])的参数,因此Confusing(Object)相对缺乏精确性。(每一个double 数组都是一个Object,但是每一个Object 并不一定是一个double 数组。)因此,最精确的构造器就是Confusing(double[ ]),这也就解释了为什么程序会产生这样的输出。
理解本谜题的关键在于在测试哪一个方法或构造器最精确时,这些测试没有使用实际的参数:即出现在调用中的参数。这些参数只是被用来确定哪一个重载版本是可应用的。一旦编译器确定了哪些重载版本是可获得且可应用的,它就会选择最精确的一个重载版本,而此时使用的仅仅是形式参数:即出现在声明中的参数。
以这种方式来在多个重载版本中进行选择是相当令人不快的。在你的API 中,应该确保不会让客户端走这种极端。理想状态下,你应该避免使用重载:为不同的方法取不同的名称。当然,有时候这无法实现,例如,构造器就没有名称,因而也就无法被赋予不同的名称。然而,你可以通过将构造器设置为私有的并提供公有的静态工厂,以此来缓解这个问题[EJ Item 1]。如果构造器有许多参数,你可以用Builder 模式[Gamma95]来减少对重载版本的需求量。
- 不是你的类
这个谜题是测试你对Java的两个最经典的操作符:instanceof和转型的理解程度。
public class Type1 {
public static void main(String[] args) {
System.out.println(new Type1() instanceof String);
}
}
public class Type2 {
public static void main(String args[]) {
Type2 t2 = (Type2) new Object();
}
}
第一个程序,Type1,展示了instanceof 操作符在测试一个类的实例,以查看它是否是某个不相关的类的实例时所表现出来的行为。你可能会期望该程序打印出
false。毕竟,Type2 的实例不是String 的实例,因此该测试应该失败,对吗?不,instanceof 测试在编译时刻就失败了,我们只能得到下面这样的出错消息:
Type2.java:3: inconvertible types
found : Type2, required: java.lang.String
System.out.println(new Type2() instanceof String);
该程序编译失败是因为instanceof 操作符有这样的要求:如果两个操作数的类型都是类,其中一个必须是另一个的子类型[JLS 15.20.2, 15.16, 5.5]。Type2
和String 彼此都不是对方的子类型,所以instanceof 测试将导致编译期错误。这个错误有助于让你警惕instanceof 测试,它们可能并没有去做你希望它们做的事情。
第二个程序,Type2,展示了当要被转型的表达式的静态类型是转型类型的超类时,转型操作符的行为。与instanceof 操作相同,如果在一个转型操作中的两种类型都是类,那么其中一个必须是另一个的子类型。尽管对我们来说,这个转型很显然会失败,但是类型系统还没有强大到能够洞悉表达式new Object()的运行期类型不可能是Type2 的一个子类型。因此,该程序将在运行期抛出ClassCastException 异常。
- 特创论
某些时候,对于一个类来说,跟踪其创建出来的实例个数会非常用有,其典型实现是通过让它的构造器递增一个私有静态域来完成的。在下面的程序中,
Creature 类展示了这种技巧,而Creator 类对其进行了操练,将打印出已经创建的Creature 实例的数量。那么,这个程序会打印出什么呢?
public class Creator {
public static void main(String[] args) {
for (int i = 0; i < 100; i++)
Creature creature = new Creature();
System.out.println(Creature.numCreated());
}
}
class Creature {
private static long numCreated = 0;
public Creature() {
numCreated++;
}
public static long numCreated() {
return numCreated;
}
}
这是一个捉弄人的问题。该程序看起来似乎应该打印100,但是它没有打印任何东西,因为它根本就不能编译。如果你尝试着去编译它,你就会发现编译器的诊断信息基本没什么用处。
一个本地变量声明看起来像是一条语句,但是从技术上说,它不是;它应该是一个本地变量声明语句(local variable declaration statement)[JLS 14.4]。Java 语言规范不允许一个本地变量声明语句作为一条语句在for、while 或do循环中重复执行[JLS 14.12-14]。一个本地变量声明作为一条语句只能直接出现在一个语句块中。(一个语句块是由一对花括号以及包含在这对花括展中的语句和声明构成的。)
有两种方式可以订正这个问题。最显而易见的方式是将这个声明至于一个语句块中:
for (int i = 0; i < 100; i++) {
Creature creature = new Creature();
}
然而,请注意,该程序没有使用本地变量creature。因此,将该声明用一个无任何修饰的构造器调用来替代将更具实际意义,这样可以强调对新创建对象的引用正在被丢弃:
for (int i = 0; i < 100; i++)
new Creature();
无论我们做出了上面的哪种修改,该程序都将打印出我们所期望的100。
还要注意的是,本谜题中的创建计数策略并不是线程安全的。如果多个线程可以并行地创建对象,那么递增计数器的代码和读取计数器的代码都应该被同步:
// Thread-safe creation counter
class Creature {
private static long numCreated;
public Creature() {
synchronized (Creature.class) {
numCreated++;
}
}
public static synchronized long numCreated() {
return numCreated;
}
}
总之,一个本地变量声明不能被用作for、while 或do 循环中的重复执行语句,它作为一条语句只能出现在一个语句块中。另外,在使用一个变量来对实例的创
建进行计数时,要使用long 类型而不是int 类型的变量,以防止溢出。最后,如果你打算在多线程中创建实例,要么将对实例计数器的访问进行同步,要么使用一个AtomicLong 类型的计数器。