ChatGPT解决这个技术问题 Extra ChatGPT

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

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

public abstract class BasicPage extends WebPage {

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

    protected abstract String getTitle();

}

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

我是一名 .NET 开发人员,但看到了这个并且对它为什么会警告这一点很感兴趣,因为我有时在 C# 中做类似的事情。这篇文章似乎给出了关于为什么它是一个警告的指示:javapractices.com/topic/TopicAction.do?Id=215所以这与初始化对象层次结构的时间和顺序有关。
在 C# 中我们有同样的问题:msdn.microsoft.com/en-us/library/ms182331.aspx
这提醒我检查 IntelliJ 是否发出此警告...

M
MC Emperor

从构造函数调用可覆盖的方法

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

引自 Effective Java 2nd Edition,Item 17: Design and document for inheritance,或者禁止它:

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

这里有一个例子来说明:

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中基类构造函数调用重写方法时派生类对象的状态

在抽象类的构造函数中使用抽象 init() 函数

也可以看看

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

多参数对象构造

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

这是来自 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);

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

如您所见,这不是一个令人愉快的编写模式,使用起来更不愉快(这里的“true”是什么意思?13 是多少?)。

Bloch 建议使用构建器模式,它允许您编写类似这样的东西:

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

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

也可以看看

维基百科/生成器模式

Effective Java 第 2 版,第 2 项:在面对许多构造函数参数时考虑构建器模式(在线摘录)

相关问题

你什么时候会使用建造者模式?

这是众所周知的设计模式吗?它叫什么名字?


+1。有趣的。我想知道 C# 中的对象初始化器是否使伸缩构造函数和 Builder 模式都变得不必要。
@Johannes:在 Java 中,实例初始化程序在第 4 步执行,在第 3 步的超类构造函数之后,在创建新实例 java.sun.com/docs/books/jls/third_edition/html/… 时执行;不过,我不确定这是否能解决您的评论。
也就是说,Java 没有进行 2 阶段初始化太糟糕了:第 1 遍用于方法定义,第 2 遍用于执行构造函数。现在我必须为一些工厂模式或其他模式编写更多代码。呜呜。我想要的只是从一个纯函数中设置一些默认数据,这些数据可以在子类中交换,或者在构造和使用之间更新。
Android 工程师注意:android 视图的可覆盖方法 invalidate() 有时会在视图的构造函数中调用。
仅供参考:引用的句子“如果您违反此规则,将导致程序失败。”是彻头彻尾的谎言。然而,它更有可能在未来产生。
p
palacsint

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

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() 期望什么,因此您会收到警告。


K
KeatsPeeks

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

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

要“修复”这个问题,您可以使用 factory method 而不是构造函数,这是对象实例化的常见模式。


返回 null 是破坏许多接口的一般问题。
当它发生在由超级构造函数调用的重写方法中时,返回 null 是一个特殊问题。
W
Wrench

这是一个示例,它揭示了在超级构造函数中调用可覆盖方法时可能出现的逻辑问题。

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(程序员可能认为它会是)。想象一下,如果计算逻辑扭曲十倍,跟踪错误将是多么困难。

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


M
Manuel Selva

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

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


V
Volksman

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

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

关于是否有必要(恕我直言,这完全是必要的)存在相当激烈的辩论,因为此链接显示http://apache-wicket.1842946.n4.nabble.com/VOTE-WICKET-3218-Component-onInitialize-is-broken-for-Pages-td3341090i20.html

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


s
sanluck

我猜对于 Wicket,最好在 onInitialize() 中调用 add 方法(参见 components lifecycle):

public abstract class BasicPage extends WebPage {

    public BasicPage() {
    }

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

    protected abstract String getTitle();
}

b
bvdb

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

将它们设为私有可以消除所有疑问:"You shall not pass"

但是,如果您确实想保持开放状态怎么办。

不只是访问修饰符才是真正的问题,正如我试图解释的 here。老实说,private 是一个明显的阻碍,其中 protected 通常仍然允许(有害的)解决方法。

更一般的建议:

不要从你的构造函数启动线程

不要从构造函数中读取文件

不要从构造函数调用 API 或服务

不要从构造函数的数据库中加载数据

不要从构造函数中解析 json 或 xml 文档

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

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

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

简而言之,您想将“准备”时刻与“点火”时刻分开。

如果可以扩展构造函数,则它不应自燃。

如果它自燃,那么它就有可能在完全建造之前发射。

毕竟,有朝一日可以在子类的构造函数中添加更多准备工作。而且您无法控制超类的构造函数的执行顺序。

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


关注公众号,不定期副业成功案例分享
关注公众号

不定期副业成功案例分享

领先一步获取最新的外包任务吗?

立即订阅