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

回眸只為那壹抹淺笑 提交于 2020-02-26 11:48:13

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

public abstract class BasicPage extends WebPage {

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

    protected abstract String getTitle();

}

NetBeans用消息“构造函数中的可重写方法调用”警告我,但是这有什么问题呢? 我能想到的唯一选择是将其他方法的结果传递给子类中的超级构造函数。 但这可能很难通过许多参数来读取。


#1楼

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

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

希望对您有所帮助。


#2楼

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

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


#3楼

在构造函数中调用一个可重写的方法使子类可以破坏代码,因此您不能保证它可以正常工作。 这就是为什么您会收到警告。

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

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


#4楼

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

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

引用有效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构造函数调用overrideMeChild尚未完成对final int x初始化,并且该方法获得了错误的值。 这几乎肯定会导致错误和错误。

相关问题

也可以看看


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

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

这是来自有效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参数,这实际上会使事情变得一团糟。

如您所见,这不是一个令人愉快的模式,而且使用起来也不那么令人愉快(“ true”在这里是什么意思?13是什么?)。

Bloch建议使用构建器模式,这将使您可以编写如下代码:

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

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

也可以看看

相关问题


#5楼

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

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()发生什么,因此您会得到警告。

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!