【问题标题】:How to automatically include the execution of a parent's method in all child constructors?如何在所有子构造函数中自动包含父方法的执行?
【发布时间】:2016-04-11 15:18:48
【问题描述】:

我有一个抽象类(Parent),它有一个抽象方法(doSetup)和一个调用doSetup 方法的成员方法。我需要的是子类(实现Parent)在构造时应该自动调用doSetup 方法,不管子类可能有多少构造函数。是否有 Java 机制或设计模式可以帮助我解决这个问题?

public abstract class Parent {
  abstract protected void sayHi();
  protected void doSetup() {
    sayHi();
  }
}

public class Child1 extends Parent {
  @Override
  protected void sayHi() {
    System.out.println("hi");
  }
  public Child1() {
    // Construction needs to automatically include exec of doSetup()
  }
  public Child1(String string) {
    // Construction needs to automatically include exec of doSetup()
    System.out.println("another constructor");
  }
}

【问题讨论】:

  • 只需在基类的构造函数中调用 doSetup() 方法。如果您希望此后包含特定方法的基类功能,只需使用super.methodYouWishToCall()。
  • 正如我所说,在你的基类的构造函数中强制执行。然后,当实例化任何子类时,必须调用基类的构造函数,强制调用该方法。如果您不确定我在这里的意思,我可以将其写下来作为您的答案。
  • @yamori 这听起来是个好主意,但实际上并非如此。你永远不应该从构造函数中调用可覆盖的方法。这在 Effective Java 中有很好的解释。
  • 同意。它不会被覆盖。在我看来,它必须是私有的或最终的。如果它的代码必须是通用的并且不被子类覆盖。
  • 我会为你写一个答案。

标签: java oop design-patterns java-8


【解决方案1】:

这是实现通用构造代码的一种方式:

Parent.java

public abstract class Parent {
    public Parent() {
        this("Default Value Goes Here");
    }

    // Funneling everything through this main constructor.
    public Parent(String value) {
        this.doSetup(value);
    }

    // I've made this method private, as it shouldn't really
    // be accessed from sub classes, but if you require that, then
    // mark this method as protected & final instead.
    private void doSetup(String value) {
        System.out.println(value);
    }
}

Child.java

public class Child extends Parent {
    // Deliberately not implementing constructors here,
    // but if I did, the first call would be to a super()
    // constructor to retain parent's construction functionality.
}

MainApp.java

public class MainApp {
    public static void main(String[] args) {
        Child child = new Child();
    }
}

运行上面的MainApp,你会看到默认值构造函数运行,它会输出“Default Value Goes Here”,因为它是由父构造函数强制执行的。

【讨论】:

    【解决方案2】:

    一个好的 IDE 可能会警告不要在构造函数中使用可覆盖的方法。

    原因可以用下面的代码来证明,这可能会产生令人惊讶的结果。

    class Base {
        Base() {
            init();
        }
    
        protected void init() {
        }
    }
    class Child extends base {
        String a = "a";
        String b;
        String c = "c";
        String d;
    
        public Child() {
            // 1. Fields are nulled
            // 2. super() called
            // 2.1. init() called
            // 3. Field initialisations done (a, c)
            // 4. Rest of constructor:
            System.out.printf("EndConstr a: %s, b: %s, c: %s%n", a, b, c);
        }
    
        @Overridable
        protected void init() {
            System.out.printf("Init a: %s, b: %s, c: %s%n", a, b, c);
            c = "cc";
            d = "dd";
        }
    }
    

    控制行为的一种解决方案是提供一个最终的不​​可覆盖方法,该方法以指定的方式调用可覆盖的受保护方法:

    class Base {
        public final void f() {
            X x = ...;
            onF(x);
        }
        protected /*abstract*/ void onF(X x) {
        }
    }
    class Child extends Base {
        @Overridable
        protected void onF(X x) {
            ...
        }
    }
    

    【讨论】:

    • 还有另一个方面。在构造函数中调用init() 确保它会在构造时被调用,但允许它被覆盖打开了子类使用不调用super.init() 的实现覆盖它的可能性,因此它的调用不是强制的没有了……
    • 这个答案说服我不要在构造函数中调用可覆盖的方法。对于那些想知道的人,上面的第一个示例演示了构造/实例化的顺序获得了覆盖的方法,但是 Child's 实例变量的值没有及时设置为构造函数调用 init()
    • @Joop Eggen ,只是确认您的标题中没有双重否定,应该阅读“......警告不要使用可覆盖的方法......”与“......警告不要使用可覆盖的方法..."
    【解决方案3】:

    为什么不在父构造函数中包含doSetup()?为了避免从构造函数调用可覆盖的方法,你可以稍微改变你的模式:

    public abstract class Parent {
      final String greeting;
    
      public Parent(String greeting) {
        this.greeting = greeting;
        doSetup();
      }
    
      final void doSetup() {
        System.out.println(greeting);
      }
    }
    

    那么你的每个子类的构造函数都需要显式调用超级构造函数,例如:

    public class Child1 extends Parent {
      private static String default_greeting = "hi";
    
      public Child1() {
        super(default_greeting); // prints "hi"
      }
    
      public Child1(String string) {
        super(string); // print a different greeting
      }
    }
    

    【讨论】:

      【解决方案4】:

      正如其他人所说,您不应该从超类的构造函数中调用可覆盖的方法。这是为了避免this to escape 并最终产​​生难以调试的竞争条件,或者只是避免在从覆盖的方法中访问尚未初始化的字段时抛出NullPointerExceptions。

      现在,关于这个问题,如果你想对所有构造函数运行公共代码,你需要一个initializer block:

      public abstract class Parent {
          {
               // this is an initializer block that is inherited 
               // by all subclasses and runs for every constructor
               // in the hierarchy
              this.doSetup();
          }
      
          protected final void sayHi() { // final to avoid this to escape
              System.out.println("hi");
          }
      
          protected final void doSetup() { // final to avoid this to escape
              sayHi();
          }
      }
      
      public class Child1 extends Parent {
      
          public Child1() {
              // initilizer block automatically called
          }
      
          public Child1(String string) {
              // initilizer block automatically called
              System.out.println("another constructor");
          }
      }
      

      运行此测试:

      new Child1();
      new Child1("something");
      

      产生以下输出:

      hi
      hi
      another constructor
      

      【讨论】:

        猜你喜欢
        • 2016-04-02
        • 2013-12-27
        • 2021-10-17
        • 1970-01-01
        • 1970-01-01
        • 2018-01-31
        • 1970-01-01
        • 2015-12-03
        • 2018-03-17
        相关资源
        最近更新 更多