构造函数中的可覆盖方法调用有什么问题?

dea*_*mon 366 java oop inheritance constructor overriding

我有一个Wicket页面类,它根据抽象方法的结果设置页面标题.

public abstract class BasicPage extends WebPage {

    public BasicPage() {
        add(new Label("title", getTitle()));
    }

    protected abstract String getTitle();

}
Run Code Online (Sandbox Code Playgroud)

NetBeans通过消息"构造函数中的可覆盖方法调用"警告我,但它应该有什么问题呢?我能想象的唯一选择是将其他抽象方法的结果传递给子类中的超级构造函数.但是很多参数很难读懂.

pol*_*nts 461

从构造函数调用overridable方法

简单地说,这是错误的,因为它不必要地为许多错误开辟了可能性.当@Override调用时,对象的状态可能不一致和/或不完整.

来自Effective Java 2nd Edition,第17项:设计和继承文档的引用,或者禁止它:

为了允许继承,类必须遵守一些限制.构造函数不得直接或间接调用可覆盖的方法.如果违反此规则,将导致程序失败.超类构造函数在子类构造函数之前运行,因此在子类构造函数运行之前将调用子类中的重写方法.如果重写方法依赖于子类构造函数执行的任何初始化,则该方法将不会按预期运行.

这是一个例子来说明:

public class ConstructorCallsOverride {
    public static void main(String[] args) {

        abstract class Base {
            Base() {
                overrideMe();
            }
            abstract void overrideMe(); 
        }

        class Child extends Base {

            final int x;

            Child(int x) {
                this.x = x;
            }

            @Override
            void overrideMe() {
                System.out.println(x);
            }
        }
        new Child(42); // prints "0"
    }
}
Run Code Online (Sandbox Code Playgroud)

这里,当Base构造函数调用时overrideMe,Child尚未完成初始化final int x,并且该方法获取错误的值.这几乎肯定会导致错误和错误.

相关问题

也可以看看


关于具有许多参数的对象构造

具有许多参数的构造函数可能导致可读性差,并且存在更好的替代方案.

以下是Effective Java 2nd Edition的引用,第2项:在面对许多构造函数参数时考虑构建器模式:

传统上,程序员使用了伸缩构造函数模式,其中您只为所需的参数提供构造函数,另一个使用单个可选参数,第三个具有两个可选参数,依此类推......

伸缩构造函数模式基本上是这样的:

public class Telescope {
    final String name;
    final int levels;
    final boolean isAdjustable;

    public Telescope(String name) {
        this(name, 5);
    }
    public Telescope(String name, int levels) {
        this(name, levels, false);
    }
    public Telescope(String name, int levels, boolean isAdjustable) {       
        this.name = name;
        this.levels = levels;
        this.isAdjustable = isAdjustable;
    }
}
Run Code Online (Sandbox Code Playgroud)

现在您可以执行以下任何操作:

new Telescope("X/1999");
new Telescope("X/1999", 13);
new Telescope("X/1999", 13, true);
Run Code Online (Sandbox Code Playgroud)

但是,您当前不能仅设置nameisAdjustable,并且levels默认情况下保留.您可以提供更多的构造函数重载,但显然数量将爆炸作为参数的数量增长,你甚至可以有多个booleanint参数,这将真正使惹出来的事.

正如你所看到的,这不是一个令人愉快的写作模式,使用起来也不那么令人愉快("真实"在这里意味着什么?13是什么?).

Bloch建议使用构建器模式,这样可以编写类似这样的内容:

Telescope telly = new Telescope.Builder("X/1999").setAdjustable(true).build();
Run Code Online (Sandbox Code Playgroud)

请注意,现在命名参数,您可以按任何顺序设置它们,并且可以跳过要保留默认值的参数.这肯定比伸缩构造器好得多,特别是当存在大量属于许多相同类型的参数时.

也可以看看

相关问题

  • 仅供参考:所引用的句子"如果违反此规则,将导致程序失败." 是一个彻头彻尾的谎言.然而,它更有可能导致未来. (4认同)
  • 也就是说,Java不做2阶段初始化是太糟糕了:方法*定义*的第一遍,第二遍执行构造函数.现在我要为一些工厂 - 工厂模式或其他编写*更多*代码.的Bleh.我想要的只是从纯函数中设置一些默认数据,这些数据可以在子类中交换,或者在构造和使用之间进行更新. (3认同)

Nor*_*ame 56

这是一个有助于理解这个的例子:

public class Main {
    static abstract class A {
        abstract void foo();
        A() {
            System.out.println("Constructing A");
            foo();
        }
    }

    static class C extends A {
        C() { 
            System.out.println("Constructing C");
        }
        void foo() { 
            System.out.println("Using C"); 
        }
    }

    public static void main(String[] args) {
        C c = new C(); 
    }
}
Run Code Online (Sandbox Code Playgroud)

如果您运行此代码,您将获得以下输出:

Constructing A
Using C
Constructing C
Run Code Online (Sandbox Code Playgroud)

你看?foo()在C的构造函数运行之前使用C.如果foo()要求C具有已定义的状态(即构造函数已完成),那么它将在C中遇到未定义的状态,并且事情可能会中断.而且由于你无法在A中知道被覆盖的foo()预期,你会收到警告.


Kea*_*eks 11

在构造函数中调用可覆盖的方法允许子类颠覆代码,因此您无法保证它可以再次工作.这就是你收到警告的原因.

在您的示例中,如果子类重写getTitle()并返回null 会发生什么?

要"修复"这个,你可以使用工厂方法而不是构造函数,它是对象实例的常见模式.


kir*_*ilv 6

这是一个示例,揭示了在超级构造函数中调用可重写方法时可能发生的逻辑问题

class A {

    protected int minWeeklySalary;
    protected int maxWeeklySalary;

    protected static final int MIN = 1000;
    protected static final int MAX = 2000;

    public A() {
        setSalaryRange();
    }

    protected void setSalaryRange() {
        throw new RuntimeException("not implemented");
    }

    public void pr() {
        System.out.println("minWeeklySalary: " + minWeeklySalary);
        System.out.println("maxWeeklySalary: " + maxWeeklySalary);
    }
}

class B extends A {

    private int factor = 1;

    public B(int _factor) {
        this.factor = _factor;
    }

    @Override
    protected void setSalaryRange() {
        this.minWeeklySalary = MIN * this.factor;
        this.maxWeeklySalary = MAX * this.factor;
    }
}

public static void main(String[] args) {
    B b = new B(2);
    b.pr();
}
Run Code Online (Sandbox Code Playgroud)

结果实际上是:

minWeeklySalary:0

maxWeeklySalary:0

这是因为类B的构造函数首先调用类A的构造函数,其中B内的可重写方法被执行。但是,该方法中,我们使用的是实例变量因素尚未初始化(因为A的构造尚未完成),从而系数为0而不是1,绝对不是2(程序员可能会认为这会事是)。想象一下,如果计算逻辑扭曲十倍,跟踪错误将非常困难。

希望对您有所帮助。


归档时间:

查看次数:

115135 次

最近记录:

7 年 前