那个点是什么
程序员文章站
2022-06-19 11:00:37
...
出自《java puzzle》
下面这个程序有两个不可变的值类(value class),值类即其实例表示值的类。第一个类用整数坐标来表示平面上的一个点,第二个类在此基础上添加了一点颜色。主程序将创建和打印第二个类的一个实例。那么,下面的程序将打印出什么呢?
main方法创建并打印了一个ColorPoint实例。println方法调用了该ColorPoint实例的toString方法,这个方法是在Point中定义的。toString方法将直接返回name域的值,这个值是通过调用makeName方法在Point的构造器中被初始化的。对于一个Point实例来说,makeName方法将返回[x,y]形式的字符串。对于一个ColorPoint实例来说,makeName方法被覆写为返回[x,y]:color形式的字符串。在本例中,x是4,y是2,color的purple,因此程序将打印[4,2]:purple,对吗?不,如果你运行该程序,就会发现它打印的是[4,2]:null。这个程序出什么问题了呢?
这个程序遭遇了实例初始化顺序这一问题。要理解该程序,我们就需要详细跟踪该程序的执行过程。下面是该程序注释过的版本的列表,用来引导我们了解其执行顺序:
在下面的解释中,括号中的数字引用的就是在上述注释版本的列表中的注释标号。首先,程序通过调用ColorPoint构造器创建了一个ColorPoint实例(1)。这个构造器以链接调用其超类构造器开始,就像所有构造器所做的那样(2)。超类构造器在构造过程中对该对象的x域赋值为4,对y域赋值为2。然后该超类构造器调用makeName,该方法被子类覆写了(3)。
ColorPoint中的makeName方法(4)是在ColorPoint构造器的程序体之前执行的,这就是问题的核心所在。makeName方法首先调用super.makeName,它将返回我们所期望的[4,2],然后该方法在此基础上追加字符串“:”和由color域的值所转换成的字符串。但是此刻color域的值是什么呢?由于它仍处于待初始化状态,所以它的值仍旧是缺省值null。因此,makeName方法返回的是字符串“[4,2]:null”。超类构造器将这个值赋给name域(3),然后将控制流返回给子类的构造器。
这之后子类构造器才将“purple”赋予color域(5),但是此刻已经为时过晚了。color域已经在超类中被用来初始化name域了,并且产生了不正确的值。之后,子类构造器返回,新创建的ColorPoint实例被传递给println方法,它适时地调用了该实例的toString方法,这个方法返回的是该实例的name域的内容,即“[4,2]:null”,这也就成为了程序要打印的东西。
本谜题说明:在一个final类型的实例域被赋值之前,存在着取用其值的可能,而此时它包含的仍旧是其所属类型的缺省值。
无论何时,只要一个构造器调用了一个已经被其子类覆写了的方法,那么该问题就会出现,因为以这种方式被调用的方法总是在实例被初始化之前执行。要想避免这个问题,就千万不要在构造器中调用可覆写的方法,直接调用或间接调用都不行[EJ Item 15]。这项禁令应该扩展至实例初始器和伪构造器(pseudoconstructors)readObject与clone。(这些方法之所以被称为伪构造器,是因为它们可以在不调用构造器的情况下创建对象。)
你可以通过惰性初始化name域来订正该问题,即当它第一次被使用时初始化,以此取代积极初始化,即当Point实例被创建时初始化。
通过这种修改,该程序就可以打印出我们期望的[4,2]:purple。
尽管惰性加载可以订正这个问题,但是对于让一个值类去扩展另一个值类,并且在其中添加一个会对euqals比较方法产生影响的域的这种做法仍旧不是一个好主意。你无法在超类和子类上都提供一个基于值的equals方法,而同时又不违反Object.equals方法的通用约定,或者是不消除在超类和子类之间进行有实际意义的比较操作的可能性[EJ Item 7]。
循环实例初始化问题对语言设计者来说是问题成堆的地方。C++是通过在构造阶段将对象的类型从超类类型改变为子类类型来解决这个问题的。如果采用这种解决方法,本谜题中最开始的程序将打印[4,2]。我们发现没有任何一种流行的语言能够令人满意地解决这个问题。也许,我们值得去考虑,当超类构造器调用子类方法时,通过抛出一个不受检查的异常使循环实例初始化非法。
总之,在任何情况下,你都务必要记住:不要在构造器中调用可覆 写的方法。在实例初始化中产生的循环将是致命的。该问题的解决方案就是惰性初始化[EJ Items 13,48]。
下面这个程序有两个不可变的值类(value class),值类即其实例表示值的类。第一个类用整数坐标来表示平面上的一个点,第二个类在此基础上添加了一点颜色。主程序将创建和打印第二个类的一个实例。那么,下面的程序将打印出什么呢?
class Point {
protected final int x, y;
private String name; // Cached at construction time
Point(int x, int y) {
this.x = x;
this.y = y;
name = makeName();
}
protected String makeName() {
return "[" + x + "," + y + "]";
}
public final String toString() {
return name;
}
}
public class ColorPoint extends Point {
private final String color;
ColorPoint(int x, int y, String color) {
super(x, y);
this.color = color;
}
protected String makeName() {
return super.makeName() + ":" + color;
}
public static void main(String[] args) {
System.out.println(new ColorPoint(4, 2, "purple"));
}
}
main方法创建并打印了一个ColorPoint实例。println方法调用了该ColorPoint实例的toString方法,这个方法是在Point中定义的。toString方法将直接返回name域的值,这个值是通过调用makeName方法在Point的构造器中被初始化的。对于一个Point实例来说,makeName方法将返回[x,y]形式的字符串。对于一个ColorPoint实例来说,makeName方法被覆写为返回[x,y]:color形式的字符串。在本例中,x是4,y是2,color的purple,因此程序将打印[4,2]:purple,对吗?不,如果你运行该程序,就会发现它打印的是[4,2]:null。这个程序出什么问题了呢?
这个程序遭遇了实例初始化顺序这一问题。要理解该程序,我们就需要详细跟踪该程序的执行过程。下面是该程序注释过的版本的列表,用来引导我们了解其执行顺序:
class Point {
protected final int x, y;
private final String name; // Cached at construction time
Point(int x, int y) {
this.x = x;
this.y = y;
name = makeName(); // 3. Invoke subclass method
protected String makeName() {
return "[" + x + "," + y + "]";
}
public final String toString() {
return name;
}
}
public class ColorPoint extends Point {
private final String color;
ColorPoint(int x, int y, String color) {
super(x, y); // 2. Chain to Point constructor
this.color = color; // 5. Initialize blank final-Too late
}
protected String makeName() {
// 4. Executes before subclass constructor body!
return super.makeName() + ":" + color;
}
public static void main(String[] args) {
// 1. Invoke subclass constructor
System.out.println(new ColorPoint(4, 2, "purple"));
}
}
}
在下面的解释中,括号中的数字引用的就是在上述注释版本的列表中的注释标号。首先,程序通过调用ColorPoint构造器创建了一个ColorPoint实例(1)。这个构造器以链接调用其超类构造器开始,就像所有构造器所做的那样(2)。超类构造器在构造过程中对该对象的x域赋值为4,对y域赋值为2。然后该超类构造器调用makeName,该方法被子类覆写了(3)。
ColorPoint中的makeName方法(4)是在ColorPoint构造器的程序体之前执行的,这就是问题的核心所在。makeName方法首先调用super.makeName,它将返回我们所期望的[4,2],然后该方法在此基础上追加字符串“:”和由color域的值所转换成的字符串。但是此刻color域的值是什么呢?由于它仍处于待初始化状态,所以它的值仍旧是缺省值null。因此,makeName方法返回的是字符串“[4,2]:null”。超类构造器将这个值赋给name域(3),然后将控制流返回给子类的构造器。
这之后子类构造器才将“purple”赋予color域(5),但是此刻已经为时过晚了。color域已经在超类中被用来初始化name域了,并且产生了不正确的值。之后,子类构造器返回,新创建的ColorPoint实例被传递给println方法,它适时地调用了该实例的toString方法,这个方法返回的是该实例的name域的内容,即“[4,2]:null”,这也就成为了程序要打印的东西。
本谜题说明:在一个final类型的实例域被赋值之前,存在着取用其值的可能,而此时它包含的仍旧是其所属类型的缺省值。
无论何时,只要一个构造器调用了一个已经被其子类覆写了的方法,那么该问题就会出现,因为以这种方式被调用的方法总是在实例被初始化之前执行。要想避免这个问题,就千万不要在构造器中调用可覆写的方法,直接调用或间接调用都不行[EJ Item 15]。这项禁令应该扩展至实例初始器和伪构造器(pseudoconstructors)readObject与clone。(这些方法之所以被称为伪构造器,是因为它们可以在不调用构造器的情况下创建对象。)
你可以通过惰性初始化name域来订正该问题,即当它第一次被使用时初始化,以此取代积极初始化,即当Point实例被创建时初始化。
通过这种修改,该程序就可以打印出我们期望的[4,2]:purple。
class Point {
protected final int x, y;
private String name; // Lazily initialized
Point(int x, int y) {
this.x = x;
this.y = y;
// name initialization removed
}
protected String makeName() {
return "[" + x + "," + y + "]";
}
// Lazily computers and caches name on first use
public final synchronized String toString() {
if (name == null)
name = makeName();
return name;
}
}
尽管惰性加载可以订正这个问题,但是对于让一个值类去扩展另一个值类,并且在其中添加一个会对euqals比较方法产生影响的域的这种做法仍旧不是一个好主意。你无法在超类和子类上都提供一个基于值的equals方法,而同时又不违反Object.equals方法的通用约定,或者是不消除在超类和子类之间进行有实际意义的比较操作的可能性[EJ Item 7]。
循环实例初始化问题对语言设计者来说是问题成堆的地方。C++是通过在构造阶段将对象的类型从超类类型改变为子类类型来解决这个问题的。如果采用这种解决方法,本谜题中最开始的程序将打印[4,2]。我们发现没有任何一种流行的语言能够令人满意地解决这个问题。也许,我们值得去考虑,当超类构造器调用子类方法时,通过抛出一个不受检查的异常使循环实例初始化非法。
总之,在任何情况下,你都务必要记住:不要在构造器中调用可覆 写的方法。在实例初始化中产生的循环将是致命的。该问题的解决方案就是惰性初始化[EJ Items 13,48]。
上一篇: 闭包是什么?
推荐阅读
-
Adobe Dimension CC是什么软件? Adobe Dimension CC 2018 mac快捷键大全
-
c语言编译系统是什么软件(c语言手机编程软件推荐)
-
组策略是什么?组策略怎么打开?
-
什么是驱动程序签名 驱动程序的数字签名又是什么
-
健康感知仪是什么? 麦开here智能健康感知仪开箱测评
-
微信小程序打款验证是什么情况 微信小程序打款验证问题解析
-
索尼assist是什么意思?索尼assist键有什么功能和作用?
-
中国电信的超牛卡闲时流量使用时段是什么时候?超牛卡闲时流量转换方法是什么?
-
哔哩av号是什么?bilibili哔哩哔哩观看看av号的教程
-
电信京东体验卡是什么?电信京东体验卡资费和办理申请地址