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

What's wrong with overridable method calls in constructors?

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

1
2
3
4
5
6
7
8
9
public abstract class BasicPage extends WebPage {

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

    protected abstract String getTitle();

}

NetBeans警告我"构造函数中的可重写方法调用",但是它应该有什么问题?我能想象的唯一选择是将抽象方法的结果传递给子类中的超级构造函数。但如果有很多参数的话,这可能很难理解。


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

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

有效Java第二版的引用,项目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.

下面是一个例子来说明:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
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"
    }
}

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

相关问题

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

也见

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

关于多参数对象构造

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

下面是有效Java第二版的一个引文,项目2:当遇到许多构造函数参数时,考虑一个构造器模式:

Traditionally, programmers have used the telescoping constructor pattern, in which you provide a constructor with only the required parameters, another with a single optional parameters, a third with two optional parameters, and so on...

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
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;
    }
}

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

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

但是,当前不能只设置nameisAdjustable,而将levels保留为默认值。您可以提供更多的构造函数重载,但是很明显,随着参数数量的增加,这个数字会爆炸,而且您甚至可能有多个booleanint参数,这会使事情变得一团糟。

正如你所看到的,这不是一个令人愉快的写作模式,甚至不太好用(这里"真"是什么意思?13是什么?).

Bloch建议使用一个构建器模式,它允许您编写类似这样的内容:

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

请注意,现在参数已命名,您可以按任意顺序设置它们,并且可以跳过要保留为默认值的参数。这当然比伸缩构造函数要好得多,尤其是当有大量属于许多相同类型的参数时。

也见

  • 维基百科/建设者模式
  • 有效的Java第二版,第2项:在遇到许多构造函数参数时考虑构造器模式(在线摘录)

相关问题

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


下面是一个有助于理解这一点的示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
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();
    }
}

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

1
2
3
Constructing A
Using C
Constructing C

你明白了吗?foo()在c的构造函数运行之前使用c。如果foo()要求c有一个定义的状态(即构造器已经完成),那么它将在c中遇到一个未定义的状态,事情可能会中断。由于您不知道覆盖的foo()所期望的是什么,所以会收到一个警告。


在构造函数中调用一个可重写的方法允许子类去破坏代码,所以你不能保证它能继续工作。这就是你得到警告的原因。

在您的示例中,如果一个子类重写getTitle()并返回空值,会发生什么情况?

要"修复"这个问题,您可以使用工厂方法而不是构造函数,这是对象声明的一种常见模式。


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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
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();
}

结果实际上是:

最小值:0

最大周工资:0

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

我希望这能帮助别人。


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

查看此示例链接http://www.javapractices.com/topic/topication.do?ID=215


在柳条的具体案例中:这就是我问柳条的原因开发人员在构建组件的框架生命周期中添加对显式两阶段组件初始化过程的支持,即

  • 施工-通过建造商
  • 初始化-通过OnInitialize(在虚拟方法工作时构造之后!)
  • 关于是否有必要(完全有必要,imho),有一个相当活跃的争论,因为这个链接显示http://apache-wicket.1842946.n4.nabble.com/vote-wicket-3218-component-oninitialize-is-break-for-pages-td3341090i20.html)

    好消息是WIKET上的优秀DEVs最终引入了两个阶段初始化(以使最优秀的Java UI框架更加出色!)因此,使用Wicket,您可以在框架调用的OnInitialize方法中进行所有的构造后初始化,如果您重写了它,该方法将自动调用——此时,在组件的生命周期中,它的构造函数已经完成了它的工作,因此虚拟方法可以按预期工作。


    我想对于wicket,最好在onInitialize()中调用add方法(参见组件生命周期):

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    public abstract class BasicPage extends WebPage {

        public BasicPage() {
        }

        @Override
        public void onInitialize() {
            add(new Label("title", getTitle()));
        }

        protected abstract String getTitle();
    }