首页 文章

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

提问于
浏览
342

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

public abstract class BasicPage extends WebPage {

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

    protected abstract String getTitle();

}

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

7 回答

  • 2

    在构造函数中调用可覆盖的方法允许子类破坏代码,因此您无法保证它再次起作用 . 这就是你得到警告的原因 .

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

    要"fix"这个,你可以使用factory method而不是构造函数,它是对象instanciation的常见模式 .

  • 439

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

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

  • 4

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

    简单地说,这是错误的,因为它不必要地为许多错误开辟了可能性 . 调用 @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 2nd Edition的引用,第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 参数,这实际上会让事情搞得一团糟 .

    正如你所看到的,这不是一个令人愉快的写作模式,使用起来也不那么令人愉快(“真实”在这里意味着什么?13是什么?) .

    Bloch建议使用构建器模式,这样可以编写类似这样的内容:

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

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

    另见

    相关问题

  • 11

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

    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() 期望什么,你会收到警告 .

  • 0

    下面是一个示例,显示在超级构造函数中调用可覆盖方法时可能出现的 logical problems .

    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中的可覆盖方法被执行 . 但是在方法中我们正在使用实例变量 factornot yet been initialized (因为A的构造函数还没有完成),因此因子是0而不是1,绝对不是2(程序员可能认为它会是这样) . 想象一下,如果计算逻辑的扭曲度是十倍,那么跟踪错误会有多难 .

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

  • 4

    在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方法中执行所有的post构造初始化如果覆盖它,框架会自动生成 - 在组件生命周期的这一点上,构造函数已完成其工作,因此虚拟方法按预期工作 .

  • 51

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

相关问题