【问题标题】:What's wrong with overridable method calls in constructors?构造函数中的可覆盖方法调用有什么问题?
【发布时间】:2022-11-13 19:35:22
【问题描述】:

我有一个 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 是否发出此警告...

标签: java oop inheritance constructor overriding


【解决方案1】:

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

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

引用自Effective Java 第 2 版,第 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 版,第 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();

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

也可以看看

相关问题

【讨论】:

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

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

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

【讨论】:

    【解决方案3】:

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

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

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

    【讨论】:

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

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

    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 内部的可覆盖方法被执行。但是在方法内部我们使用的是实例变量因素其中有尚未初始化(因为 A 的构造函数还没有完成),因此 factor 是 0 而不是 1 并且绝对不是 2(程序员可能认为它会是)。想象一下,如果计算逻辑扭曲十倍,跟踪错误将是多么困难。

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

    【讨论】:

      【解决方案5】:

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

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

      【讨论】:

        【解决方案6】:

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

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

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

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

        【讨论】:

          【解决方案7】:

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

          制作它们私人的消除所有疑问:"You shall not pass"

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

          它是不仅仅是访问修饰符这是真正的问题,因为我试图解释here。老实说,private 是一个明显的亮点,protected 通常仍然允许(有害的)解决方法。

          更一般的建议:

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

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

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

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

          简单地说,你想分离“的时刻准备“ 来自 ”点火”。

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

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

          【讨论】:

            【解决方案8】:

            我猜对于 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();
            }
            

            【讨论】:

              猜你喜欢
              • 2011-03-25
              • 2014-01-18
              • 2013-03-01
              • 1970-01-01
              • 1970-01-01
              • 2011-08-31
              • 1970-01-01
              • 2014-05-22
              相关资源
              最近更新 更多