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

发布于 2024-09-13 04:05:22 字数 338 浏览 7 评论 0原文

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

public abstract class BasicPage extends WebPage {

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

    protected abstract String getTitle();

}

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

I have a Wicket page class that sets the page title depending on the result of an abstract method.

public abstract class BasicPage extends WebPage {

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

    protected abstract String getTitle();

}

NetBeans warns me with the message "Overridable method call in constructor", but what should be wrong with it? The only alternative I can imagine is to pass the results of otherwise abstract methods to the super constructor in subclasses. But that could be hard to read with many parameters.

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(9

夜夜流光相皎洁 2024-09-20 04:05:22

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

简单地说,这是错误的,因为它不必要地为许多错误带来了可能性。当调用@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"
    }
}

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

相关问题

另请参阅


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

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

下面引用了《Effective Java 第二版》第 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);

但是,当前您不能仅设置 nameisAdjustable,然后离开默认的级别。您可以提供更多的构造函数重载,但显然,随着参数数量的增加,数量会爆炸,您甚至可能有多个 boolean 和 int 参数,这确实会导致把事情搞乱。

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

Bloch 建议使用构建器模式,它允许您编写如下所示的内容:

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

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

另请参阅

相关问题

On invoking overridable method from constructors

Simply put, this is wrong because it unnecessarily opens up possibilities to MANY bugs. When the @Override is invoked, the state of the object may be inconsistent and/or incomplete.

A quote from Effective Java 2nd Edition, Item 17: Design and document for inheritance, or else prohibit it:

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.

Here's an example to illustrate:

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

Here, when Base constructor calls overrideMe, Child has not finished initializing the final int x, and the method gets the wrong value. This will almost certainly lead to bugs and errors.

Related questions

See also


On object construction with many parameters

Constructors with many parameters can lead to poor readability, and better alternatives exist.

Here's a quote from Effective Java 2nd Edition, Item 2: Consider a builder pattern when faced with many constructor parameters:

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...

The telescoping constructor pattern is essentially something like this:

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

And now you can do any of the following:

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

You can't, however, currently set only the name and isAdjustable, and leaving levels at default. You can provide more constructor overloads, but obviously the number would explode as the number of parameters grow, and you may even have multiple boolean and int arguments, which would really make a mess out of things.

As you can see, this isn't a pleasant pattern to write, and even less pleasant to use (What does "true" mean here? What's 13?).

Bloch recommends using a builder pattern, which would allow you to write something like this instead:

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

Note that now the parameters are named, and you can set them in any order you want, and you can skip the ones that you want to keep at default values. This is certainly much better than telescoping constructors, especially when there's a huge number of parameters that belong to many of the same types.

See also

Related questions

洛阳烟雨空心柳 2024-09-20 04:05: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(); 
    }
}

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

Constructing A
Using C
Constructing C

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

Here's an example which helps to understand this:

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

If you run this code, you get the following output:

Constructing A
Using C
Constructing C

You see? foo() makes use of C before C's constructor has been run. If foo() requires C to have a defined state (i.e. the constructor has finished), then it will encounter an undefined state in C and things might break. And since you can't know in A what the overwritten foo() expects, you get a warning.

滿滿的愛 2024-09-20 04:05:22

在构造函数中调用可重写的方法允许子类破坏代码,因此您不能保证它不再有效。这就是您收到警告的原因。

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

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

Invoking an overridable method in the constructor allows subclasses to subvert the code, so you can't guarantee that it works anymore. That's why you get a warning.

In your example, what happens if a subclass overrides getTitle() and returns null ?

To "fix" this, you can use a factory method instead of a constructor, it's a common pattern of objects instanciation.

記柔刀 2024-09-20 04:05:22

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

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

maxWeeklySalary: 0

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

我希望这会对某人有所帮助。

Here is an example that reveals the logical problems that can occur when calling an overridable method in the super constructor.

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

The result would actually be:

minWeeklySalary: 0

maxWeeklySalary: 0

This is because the constructor of class B first calls the constructor of class A, where the overridable method inside B gets executed. But inside the method we are using the instance variable factor which has not yet been initialized (because the constructor of A has not yet finished), thus factor is 0 and not 1 and definitely not 2 (the thing that the programmer might think it will be). Imagine how hard would be to track an error if the calculation logic was ten times more twisted.

I hope that would help someone.

橘味果▽酱 2024-09-20 04:05:22

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

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

If you call methods in your constructor that subclasses override, it means you are less likely to be referencing variables that don’t exist yet if you divide your initialization logically between the constructor and the method.

Have a look on this sample link http://www.javapractices.com/topic/TopicAction.do?Id=215

天荒地未老 2024-09-20 04:05:22

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

让它们私有消除了所有疑虑:“你不能通过”

但是,如果您确实想让事情保持开放怎么办?

正如我试图解释的那样,不仅仅是访问修饰符才是真正的问题此处。说实话,private 是一个明显的阻碍,而 protected 通常仍然会允许(有害的)解决方法。

更一般的建议:

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

不要直接从你的构造函数中这样做。这包括从构造函数调用的私有/受保护函数执行任何这些操作。

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

相反,您应该提供 public init()start()connect() 方法。并将责任留给消费者。

简单地说,您想要将“准备”时刻与“点火”时刻分开

  • 如果构造函数可以扩展,那么它就不会自燃。
  • 如果它自燃,那么它就有在完全建造之前被发射的风险。
  • 毕竟,有一天可以在子类的构造函数中添加更多准备工作。并且您无法控制超类构造函数的执行顺序。

PS:考虑实现 Closeable 与之相关的接口。

I certainly agree that there are cases where it is better not to call some methods from a constructor.

Making them private takes away all doubt: "You shall not pass".

However, what if you DO want to keep things open.

It's not just the access modifier that is the real problem, as I tried to explain here. To be completely honest, private is a clear showstopper where protected usually will still allow a (harmful) workaround.

A more general advice:

  • don't start threads from your constructor
  • don't read files from your constructor
  • don't call APIs or services from your constructor
  • don't load data from a database from your constructor
  • don't parse json or xml documents from your constructor

Don't do so (in)directly from your constructor. That includes doing any of these actions from a private/protected function which is called by the constructor.

Calling an start() method from your constructor could certainly be a red flag.

Instead, you should provide a public init(), start() or connect() method. And leave the responsibility to the consumer.

Simply put, you want to separate the moment of "preparation" from the "ignition".

  • if a constructor can be extended then it shouldn't self-ignite.
  • If it self-ignites then it risks being launched before being fully constructed.
  • After all, some day more preparation could be added in the constructor of a subclass. And you don't have any control over the order of execution of the constructor of a super class.

PS: consider implementing the Closeable interface along with it.

不甘平庸 2024-09-20 04:05:22

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

  1. 构建 - 通过构造函数
  2. 初始化 - 通过 onInitilize(在虚拟方法工作时构建之后!)

对于是否有必要或是否有必要进行了相当激烈的争论不是(恕我直言,这完全是必要的),因为此链接演示了 http://apache-wicket.1842946.n4.nabble.com/VOTE-WICKET-3218-Component-onInitialize-is-broken-for-Pages-td3341090i20.html< /a>)

好消息是,Wicket 的优秀开发人员最终引入了两阶段初始化(以使最棒的 Java UI 框架变得更加棒!),因此使用 Wicket,您可以在 onInitialize 方法中完成所有构建后初始化如果您覆盖它,框架会自动调用它 - 此时在组件生命周期中,其构造函数已完成其工作,因此虚拟方法按预期工作。

In the specific case of Wicket: This is the very reason why I asked the Wicket
devs to add support for an explicit two phase component initialization process in the framework's lifecycle of constructing a component i.e.

  1. Construction - via constructor
  2. Initialization - via onInitilize (after construction when virtual methods work!)

There was quite an active debate about whether it was necessary or not (it fully is necessary IMHO) as this link demonstrates http://apache-wicket.1842946.n4.nabble.com/VOTE-WICKET-3218-Component-onInitialize-is-broken-for-Pages-td3341090i20.html)

The good news is that the excellent devs at Wicket did end up introducing two phase initialization (to make the most aweseome Java UI framework even more awesome!) so with Wicket you can do all your post construction initialization in the onInitialize method that is called by the framework automatically if you override it - at this point in the lifecycle of your component its constructor has completed its work so virtual methods work as expected.

趴在窗边数星星i 2024-09-20 04:05:22

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

public abstract class BasicPage extends WebPage {

    public BasicPage() {
    }

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

    protected abstract String getTitle();
}

I guess for Wicket it's better to call add method in the onInitialize() (see components lifecycle) :

public abstract class BasicPage extends WebPage {

    public BasicPage() {
    }

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

    protected abstract String getTitle();
}
故事还在继续 2024-09-20 04:05:22

可重写的方法应包含在构造函数上调用它们的文档。继承者应该考虑到这一点,并且只使用重写来更改超类上分配的值的验证,并且始终调用超类方法来更改实际值(如果它是setter)。

我确实在构造函数上使用了很多可重写的方法,并且没有导致程序失败。这是因为我确实遵循这个原则 - 覆盖仅允许检查值,或者可能用子类上不同的合适值替换它。通常,重写的方法是不使用子类的内部状态的值检查器,但允许更改允许的值。

The overridable methods should include documentation that they are called on constructor. The inheritors should take this into account and only use overriding to change the validation of the values assigned on the super class, and always call superclass method to alter the actual value, if it is setter.

I do have used overridable methods on constructors a lot, and no program failure has resulted. This is due the fact I do follow this principle - overriding only allows checking the value, or possibly replacing it with different suitable value on sub classes. Usually the overridden method is checker of a value without using internal state of the subclass, but allows changing the allowed values.

~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文