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

public abstract class BasicPage extends WebPage {

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

    protected abstract String getTitle();

}

NetBeans用“构造函数中可重写方法调用”的消息警告我,但是它应该有什么问题呢?我能想到的唯一替代方法是将抽象方法的结果传递给子类中的超构造函数。但考虑到很多参数,这可能很难解读。


当前回答

下面的示例揭示了在超级构造函数中调用可重写方法时可能出现的逻辑问题。

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();
}

结果实际上是:

minWeeklySalary: 0

最大每周工资: 0

这是因为类B的构造函数首先调用类A的构造函数,在那里执行B内部的可重写方法。但是在方法内部,我们使用的实例变量factor还没有初始化(因为A的构造函数还没有完成),因此factor是0而不是1,也肯定不是2(程序员可能认为它会是2)。想象一下,如果计算逻辑扭曲十倍,跟踪错误将是多么困难。

我希望这能帮助到一些人。

其他回答

如果在构造函数中调用子类覆盖的方法,这意味着如果在构造函数和方法之间逻辑地划分初始化,就不太可能引用还不存在的变量。

看看这个示例链接http://www.javapractices.com/topic/TopicAction.do?Id=215

在构造函数中调用可重写方法允许子类破坏代码,因此不能保证它还能正常工作。这就是为什么你会得到警告。

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

为了“修复”这个问题,你可以使用工厂方法而不是构造函数,这是对象实例化的一种常见模式。

下面的例子有助于理解这一点:

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(); 
    }
}

如果你运行这段代码,你会得到以下输出:

Constructing A
Using C
Constructing C

你看到了什么?foo()在运行C的构造函数之前使用C。如果foo()要求C有一个定义的状态(即构造函数已经完成),那么它将在C中遇到一个未定义的状态,事情可能会破裂。由于您无法知道在A中重写的foo()期望什么,因此您将得到一个警告。

关于从构造函数调用可重写方法

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

引用Effective Java 2nd Edition, Item 17:用于继承的设计和文档,否则禁止它:

There are a few more restrictions that a class must obey to allow inheritance. Constructors must not invoke overridable methods, directly or indirectly. If you violate this rule, program failure will result. The superclass constructor runs before the subclass constructor, so the overriding method in the subclass will be invoked before the subclass constructor has run. If the overriding method depends on any initialization performed by the subclass constructor, the method will not behave as expected.

这里有一个例子来说明:

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"
    }
}

在这里,当基本构造函数调用overrideMe时,Child还没有完成最终int x的初始化,方法得到了错误的值。这几乎肯定会导致错误和错误。

相关问题

从父类构造函数调用重写方法 在Java中,基类构造函数调用重写方法时派生类对象的状态 在抽象类的构造函数中使用抽象init()函数

另请参阅

从超类的构造函数调用的字段方法的未初始化读取


有很多参数的对象构造

具有许多参数的构造函数可能导致较差的可读性,存在更好的替代方法。

下面引用Effective Java 2nd Edition, Item 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;
    }
}

现在你可以做以下任何一件事:

new Telescope("X/1999");
new Telescope("X/1999", 13);
new Telescope("X/1999", 13, true);

但是,您目前不能只设置名称和is可调,而将级别设置为默认值。您可以提供更多的构造函数重载,但显然,随着参数数量的增长,重载的数量会激增,您甚至可能有多个布尔和int参数,这真的会把事情搞得一团糟。

正如您所看到的,这不是一个令人愉快的模式,使用起来更不愉快(“true”在这里是什么意思?13是什么?)。

Bloch建议使用一个构建器模式,它允许你写这样的东西:

Telescope telly = new Telescope.Builder("X/1999").setAdjustable(true).build();

请注意,现在已对参数进行了命名,您可以按照您想要的任何顺序设置它们,并且可以跳过希望保持默认值的参数。这当然比伸缩构造函数好得多,特别是当有大量属于许多相同类型的参数时。

另请参阅

维基百科/建造者模式 Effective Java 2nd Edition,第2项:在面对许多构造函数参数时考虑一个构造器模式(在线摘录)

相关问题

什么时候使用构建器模式? 这是一个众所周知的设计模式吗?它叫什么名字?

我当然同意,有些情况下最好不要从构造函数调用某些方法。

让它们成为私人空间可以消除所有的疑虑:“你不能通过”。

然而,如果你真的想保持开放呢?

正如我在这里试图解释的那样,真正的问题不仅仅是访问修饰符。坦率地说,隐私是一个明显的阻碍,受保护通常仍然会允许一个(有害的)变通办法。

一个更普遍的建议是:

不要从构造函数启动线程 不要从构造函数中读取文件 不要从构造函数中调用api或服务 不要从构造函数中加载数据库中的数据 不要从构造函数中解析json或XML文档

不要直接从构造函数中这样做。这包括从构造函数调用的private/protected函数执行这些操作。

从构造函数调用start()方法肯定是一个危险信号。

相反,你应该提供一个公共的init(), start()或connect()方法。把责任留给消费者。

简单地说,你要把“准备”和“点火”分开。

如果构造函数可以扩展,那么它就不应该自燃。 如果它自燃,那么它就有可能在完全建造之前发射。 毕竟,将来可以在子类的构造函数中添加更多的准备。而且你不能控制超类构造函数的执行顺序。

PS:考虑同时实现Closeable接口。