【问题标题】:How do I force any subclasses of my class to always call a parent's implementation method they are overriding?如何强制我的类的任何子类始终调用它们覆盖的父实现方法?
【发布时间】:2015-07-15 16:45:16
【问题描述】:

假设我有一个类,它实现了一个方法 (addThings())。它是子类树的基础:

ParentClass {
    protected void addThings() {
        map.add(thing1);
        map.add(thing2);
    }
}

现在,假设我们实现了一个子类(其中也包含事物 3),并且还需要将事物 3 添加到在事物 1 和事物 2 之上

显而易见的Java解决方案似乎是让子类的方法实现调用超类的方法:

ChildClass extends ParentClass {
    protected void addThings() {
        super.addThings();
        map.add(thing3);
    }
}

问题是实现子类的人很可能忘记这样做,并且有一个错误

ChildClassBad extends ParentClass {
    protected void addThings() {
        // BUG!!! Forgot to call super.addThings(); !!!
        map.add(thing3); 
    }
}

在 Java 中是否有办法强制任何扩展的子(和孙)类在覆盖父类的方法时始终调用它? (类似于如何使方法抽象总是迫使他们实现它)。

  • 请注意,我需要解决方案可以沿继承树传播。

    换句话说,如果有人实现了需要添加 Thing4 的 GrandChildClass,他们将遭受与 ChildClass 相关的相同错误可能性。

    这意味着在 ParentClass 中具有单独的“addParentThings()”然后调用 addParentThings() 和子级可覆盖的空 addThings() 的简单修复(当您只有 1 级继承时适用)是不够的(因为孙子必须覆盖非空的 addThings)。

【问题讨论】:

  • 据我所知,您无法强化对超级方法的调用。您能否详细说明为什么要使用这种机制?也许你有一个XY-Problem
  • @Turing85 - 我正在实现一个父类,它会存在一段时间,并且可能会被子类化很多。我发现了一个明显可能是任何子类实现者都会遇到的错误,我想要一个基于 Java 编译器的解决方案来解决这个问题(而不是“写一条评论告诉人们这样做并希望他们阅读了评论”)
  • 我明白你的一般想法,但我认为你可以通过另一种方式来防止这个问题。在给定的示例中,您可以通过构造函数来加强您的条件,但我认为您的情况并不那么简单。
  • @Turing85 - 必须在对象上多次调用该方法,因此不幸的是,构造函数不是可行的方法(我的同事已经建议 :)

标签: java oop inheritance design-patterns overriding


【解决方案1】:

也许尝试使用一个调用另一个可覆盖方法的最终方法?

class ParentClass {

    public final void doStuff() {
        // Do stuff
        onPostDoStuff();
    }

    protected void onPostDoStuff() {
        // Override this!
    }
}

然后在子类中:

class ChildClass extends ParentClass {

    @Override
    protected void onPostDoStuff() {
        // Do extra stuff
    }
}

您甚至可以将 onPostDoStuff() 方法抽象化,以便孩子覆盖它。

【讨论】:

  • 这似乎根本没有帮助孙子类覆盖子类的方法? (您似乎只是采用了我在帖子后半部分明确拒绝的解决方案,并对其进行了“最终”处理,尽管这没有任何帮助)
  • 我认为这仍然是最好的方法。如果您需要将其进一步应用于大子班,请onPostDoStuff() final 并再次应用相同的原则。
  • @DidierL - 如果我需要一个孙子,再做一个方法???作为一种方法,这似乎令人难以置信。此外,它根本没有回答问题,因为问题非常明确地拒绝了这种方法,因为它不合适
【解决方案2】:

如果您愿意为每个类设置doStuff-Methods 静态,这扩展了您的ParentClass 并为您的ParentClass 提供final public void doAllStuff()-Method,您可以使用反射解决问题:

import java.lang.reflect.Method;

import java.util.ArrayList;
import java.util.List;

public class Main
{
    public static void main(String[] args) throws InterruptedException
    {
        A a = new C();
        a.doAllStuff();
    }
}

class A
{
    protected List<String> list = new ArrayList<String>();

    @SuppressWarnings("unused")
    private static void doStuff(A a)
    {
        a.list.add("I am A");
    }

    final public void doAllStuff()
    {
        List<Class<?>> list = new ArrayList<Class<?>>();
        Class<?> clazz = this.getClass();
        while (A.class.getSuperclass() != clazz)
        {
            list.add(clazz);
            clazz = clazz.getSuperclass();
        }
        System.out.println(list);
        for (Class<?> myClass : list)
        {
            try
            {
                Method method = myClass.getDeclaredMethod("doStuff"
                                                          , myClass);
                // Method is private? Make it accessible anyway.
                method.setAccessible(true);
                method.invoke(this, this);
            }
            catch (NoSuchMethodException e)
            {
                // Method not found, continue with next class.
                continue;
            }
            catch (Exception e)
            {
                e.printStackTrace();
            }
        }
        System.out.println(this.list);
    }
}

class B extends A
{
    @SuppressWarnings("unused")
    private static void doStuff(B b)
    {
        b.list.add("I am B");
    }
}

class C extends B {}

如果只需要调用属性,可以使用getDeclaredField,此时字段可能不是static

【讨论】:

  • 到目前为止,这是所有答案中最好的方法。到目前为止唯一的小缺点是(1)它是静态限制的(坦率地说,我不明白为什么它不能轻易扩展到对象方法?你可以通过使用来获取底层调用者对象的类getClass()); (2) 它打开了一个反向错误,其中 B 类的实现者不小心在他的实现中添加了对 super.doStuff() 的调用,而他们不应该这样做。我认为这个也可以解决。
  • 由于不利因素,我推迟接受答案,但除非提出更好的建议,否则很可能会接受这个答案。
  • @DVK 对象方法的问题在于动态绑定:调用doStuff 类型为B 的对象必须始终导致调用B.doStuff()(这就是动态的本质捆绑)。我通过将doStuff-methods 设置为私有来取消了“意外”调用。这样,它们只在类内可见,不能从外部访问。另一种可能的方法是Decorator Pattern,但为此,您必须将每个对象包装在一个新对象中。
  • 在运行时追踪问题看起来很痛苦。
【解决方案3】:

以下方法强制调用超类的 setup 方法。子类中的缺点或可能的错误是实现者可能忘记提供合适的构造函数来提供对 setup 方法的扩展。这意味着子不能被孙子扩展。

这也很丑陋,因为子类不能将特定于子类的设置放在它们的扩展中;他们必须接受Parent 作为参数。

总体而言,这里的尴尬困难表明,最好在静态分析器中执行(如果有的话),而不是 javac

public class Parent
{

  private final Consumer<Parent> setup;

  protected final Collection<Object> x = new ArrayList<>();

  public Parent()
  {
    setup = Parent::setupImpl;
  }

  protected Parent(Consumer<Parent> extension)
  {
    setup = ((Consumer<Parent>) Parent::setupImpl).andThen(extension);
  }

  public final void setup()
  {
    setup.accept(this);
  }

  private static void setupImpl(Parent it)
  {
    it.x.add("thing1");
    it.x.add("thing2");
  }

}

public class Child
  extends Parent
{

  public Child()
  {
    super(Child::setupImpl);
  }

  protected Child(Consumer<Parent> extension)
  {
    super(((Consumer<Parent>) Child::setupImpl).andThen(extension));
  }

  private static void setupImpl(Parent it)
  {
    it.x.add("thing3");
  }

}

【讨论】:

    【解决方案4】:

    这是解决您的问题的一种略有不同的方法,正如所选答案的一个 cmets 所说,您可以使用装饰器模式(它与传统的装饰器有点不同,适应了这个问题),它在我看来是一个更清洁的解决方案。我添加了 2 个类,分别添加了事物 3 和事物 4 以显示用法。

    public interface ThingAdder {
      void addThings();
    }
    
    public abstract class AbstractAdder implements ThingAdder {
      protected List<String> map = new ArrayList<>(); // or your map impl
    }
    
    public class DefaultAdderDecorator implements ThingAdder {
      AbstractAdder decoratedThingAdder;
    
      public DefaultAdderDecorator(AbstractAdder decoratedThingAdder) {
        this.decoratedThingAdder = decoratedThingAdder;
      }
    
      @Override
      public void addThings() {
          decoratedThingAdder.map.add("thing 1");
          decoratedThingAdder.map.add("thing 2");
          decoratedThingAdder.addThings();
      }
    }
    
    public class Thing3Adder extends AbstractAdder {
      @Override
      public void addThings() {
        map.add("thing 3");
      }
    }
    
    public class Thing4Adder extends AbstractAdder {
      @Override
      public void addThings() {
        map.add("thing 4");
      }
    }
    
    public class AdderApp {
      public static void main(String args[]) {
        Thing3Adder thing3Adder = new Thing3Adder();
        Thing4Adder thing4Adder = new Thing4Adder();
        ThingAdder decoratedAdder = new DefaultAdderDecorator(thing3Adder);
    
        decoratedAdder.addThings();
        System.out.println("Decorated Thing3Adder map:"+thing3Adder.map);
    
        decoratedAdder = new DefaultAdderDecorator(thing4Adder);
    
        decoratedAdder.addThings();
        System.out.println("Decorated Thing4Adder map:"+thing4Adder.map);
      }
    }
    

    运行 AdderApp 后会打印:

    Decorated Thing3Adder map:[thing 1, thing 2, thing 3]
    Decorated Thing4Adder map:[thing 1, thing 2, thing 4]
    

    装饰器模式背后的想法是增强现有功能,在这种情况下,我们通过使用默认装饰来增强 addThings 方法,该装饰在调用装饰对象自己的 addThings 方法之前添加事物 1 和事物 2,然后在需要时添加要拥有一个需要首先插入默认值的新加法器,开发人员只需创建一个扩展 AbstractAdder 的新 ThingXAdder。

    【讨论】:

      【解决方案5】:

      我想不出任何可以满足您在未来子类中仍然可以执行的条件。

      Android sdk 在其许多核心生命周期方法中都有“super has not been called”异常,但它是严格的单级继承。

      【讨论】:

        猜你喜欢
        • 2017-11-18
        • 1970-01-01
        • 1970-01-01
        • 2020-11-04
        • 2014-05-11
        • 2018-03-18
        • 1970-01-01
        • 2017-11-11
        • 2011-05-21
        相关资源
        最近更新 更多