如何强制要求所有抽象类的子类定义构造函数?

73

我有一个抽象类A,定义了一些抽象方法。这意味着,为了使类可以实例化,所有的抽象方法都必须被实现。

我希望所有的子类都实现一个带有两个整数参数的构造函数。

声明一个构造函数会破坏我的目的,因为我想在子类中定义构造函数,而且我不知道任何关于实现的信息。此外,我不能将构造函数声明为抽象的。

有没有办法做到这一点?

我想要的示例:

假设我正在定义一个矩阵类的API。在我的问题中,矩阵的尺寸不能更改。

为了创建一个矩阵,我需要提供其大小。

因此,我希望所有的实现者都提供带有大小参数的构造函数。这个构造函数是由问题引起的,而不是由实现问题引起的。实现可以随便做任何事情,只要所有方法的语义保持不变。

假设我想在我的抽象类中提供一个invert()方法的基本实现。这个方法将创建一个新的矩阵,尺寸与this相反。更具体地说,正如在抽象类中定义的那样,它将使用一个带有两个整数参数的构造函数创建一个与this相同的类的新实例。由于它不知道将要使用的实例,它将使用反射(getDefinedConstructor),而我希望有一种方法来保证我会得到它,并且对于实现来说是有意义的。


这里可以找到一种优雅的解决方案: https://dev59.com/P2025IYBdhLWcg3wZlLd - Kuldeep S Chauhan
实际上,这可能可以通过apt处理器完成...在此处(更新中)提到了如何对无参数构造函数执行此操作,并提供了一个链接...在链接的代码中,您可以更改visitExecutable以检查不是t.getParameterTypes().isEmpty()而是如果给定类的参数(您可能可以将其传递给注释)...它将在编译时工作。 - Selvin
8个回答

57
你无法强制子类使用特定的构造函数签名,但你可以在抽象类中定义一个接受两个整数参数的构造函数,并强制子类调用该构造函数。子类可以从不带参数的构造函数中调用该构造函数,并传递常量等参数。这是你能够做到的最接近的方式。
另外,正如你所说,你不知道实现的细节 - 那么你怎么知道需要一个要求两个整数参数的构造函数呢?如果其中一个参数需要一个字符串,或者对于其中一个整数参数使用常量是有意义的,那怎么办呢?
更重要的是,你为什么想要强制子类使用特定的构造函数签名? 如果你解释了为什么想要它,可能会出现解决方案。
一个选项是为工厂单独创建一个接口。
interface MyClassFactory
{
    MyClass newInstance(int x, int y);
}

那么您的MyClass的每个具体子类也需要一个工厂,该工厂知道如何根据两个整数构建实例。虽然不是特别方便,但仍然需要构建工厂实例本身。那么,这里的真实情况是什么呢?


2
假设我正在定义一个Matrix类的API。在我的问题中,Matrix不能改变其维度。要创建矩阵,我需要提供其大小。因此,我希望所有实现者都能提供构造函数并将大小作为参数传递。这个构造函数是由问题而来的,而不是由实现问题引起的关注。实现可以根据自己的意愿进行处理,只要保持方法的语义即可。 - dodecaplex
1
但是如果您想创建一个始终为10 x 10的FixedSizeMatrix实现呢?无论如何,您都不能多态地调用构造函数,那么为什么要尝试限制实现呢? - Jon Skeet
1
那么,实现将不符合我的API …… 如果它有很好的理由这样做,那么它将提供一个零参数构造函数和一个2个参数构造函数,如果参数不是10x10,则会引发异常。这意味着我仍然可以创建相同实现和相同大小的空矩阵(不知道有效实现),但如果我尝试为非10x10的矩阵使用此实现,则会收到异常。 - dodecaplex
3
@dodecaplex,你可以在抽象类中轻松声明一个接受两个整数的构造函数,然后为 FixedSizeMatrix 声明一个无参构造函数,它看起来像这样 public FixedSizeMatrix() { super(10, 10); },就像 Jon 所说的那样。API 更多关注的是方法(如何使用对象)而不是构造函数(如何构建对象)。 - matt b
@mattb 这样做的问题在于抽象类将不得不实现它的构造函数,而这正是 OP 试图避免的。然而,他可以将该构造函数的实现委托给一个抽象方法,但这将违反其他原则,可惜了。 - klaar
显示剩余3条评论

6
您可以尝试以下方式。如果实现类没有适当参数的构造函数,构造函数将抛出异常。
这很愚蠢。请比较OK和Bad。两个类是相同的,只是OK满足您的要求,因此通过了运行时检查。因此,强制执行要求会促进低效忙碌的工作。
更好的解决方案是某种类型的工厂。
abstract class RequiresConstructor
{
    RequiresConstructor( int x, int y ) throws NoSuchMethodException
    {
    super();
    System.out.println( this.getClass().getName() ) ;
    this.getClass(). getConstructor ( int.class , int.class ) ;
    }

    public static void main( String[] args ) throws NoSuchMethodException
    {
    Good good = new Good ( 0, 0 );
    OK ok = new OK ();
    Bad bad = new Bad ();
    }
}

class Good extends RequiresConstructor
{
    public Good( int x, int y ) throws NoSuchMethodException
    {
    super( x, y ) ;
    }
}

class OK extends RequiresConstructor
{
    public OK( int x, int y ) throws NoSuchMethodException
    {
    super( x, y ) ;
    throw new NoSuchMethodException() ;
    }

    public OK() throws NoSuchMethodException
    {
    super( 0, 0 ) ;
    }
}

class Bad extends RequiresConstructor
{
    public Bad() throws NoSuchMethodException
    {
    super( 0, 0 ) ;
    }
}

3
有点晚了,但是...
在你的类中创建一个默认构造函数,它始终作为超级构造函数调用。在这个默认构造函数中,你可以使用反射在自己的类对象上检查所有定义的构造函数(此时不是抽象超类,而是具体的子类)。如果缺少要实现的构造函数,则抛出运行时异常。
我不太喜欢使用反射,因为它有通过后门进行黑客攻击的味道,但有时会有帮助...
看一下这个例子:
import java.lang.reflect.Constructor;

public abstract class Gaga {
  public Gaga() {
    boolean found = false;
    try {
      Constructor<?>[] constructors = getClass().getConstructors();
      for (Constructor<?> c : constructors) {
        if (c.getParameterTypes().length==2) {
          Class<?> class0 = c.getParameterTypes()[0];
          Class<?> class1 = c.getParameterTypes()[1];
          if ( (class0.getName().equals("int") || class0.isAssignableFrom(Integer.class))
              &&  (class1.getName().equals("int") || class1.isAssignableFrom(Integer.class)) )
            found = true;
        }
      }
    } catch (SecurityException e)
    {
      found = false;
    }

    if (!found)
      throw new RuntimeException("Each subclass of Gaga has to implement a constructor with two integers as parameter.");

    //...
  }

}

还有一个测试类:

public class Test {
  private class Gaga1 extends Gaga {
    public Gaga1() { this(0, 0); }
    public Gaga1(int x, Integer y) { }
  }

  private class Gaga2 extends Gaga {

  }

  public static void main(String[] args)
  {
    new Gaga1();
    new Gaga1(1, 5);
    new Gaga2();
    System.exit(0);
  }
}

在主函数中将创建Gaga1对象,但创建Gaga2会抛出运行时异常。
但你不能确定是否调用了这个构造函数,甚至不能确保它正在做你想要的事情。
只有在使用反射时才有用的测试。

3
如果您需要在接口中定义实现类将使用的内部表示形式,那么您只是在错误地做事。请阅读关于封装数据抽象的内容。
如果您的抽象实现依赖于某些实现细节,则它们属于该抽象类。这意味着,抽象类应该定义一个构造函数,允许初始化抽象方法所需的内部状态。
通常,构造函数旨在通过提供对象实例的初始状态的一些详细信息来创建类的实例。这并不意味着正在构建的实例应该像我看到的大多数软件那样复制对每个单独参数的引用。因此,即使Java为子类提供了强制实施某些构造函数签名的构造函数,这些子类也可以轻松丢弃参数。

1
嗯,如果我在n维空间中定义一个点,我可以确定该点不能存在于m维空间中(m!= n)。因此,对我来说,空间的维度是点的固有属性,而不考虑点的实现方式。就我所知,这就是封装和数据抽象。如果每个实例的维度必须被知道以便创建,那么要求所有实现都提供一个接受此参数的构造函数似乎是很自然的。 - dodecaplex
@dodecaplex,啊,一个点,很好的例子。我们假设你在谈论一个二维点。当然,我可以使用x和y值来表示它。或者,我可以使用弧度/角度值和距离值来表示它。这只是一个例子。可能还有其他方法。你只是认为你知道如何实现。无论如何,根据定义,如果您试图强制使用一种表示方法,那么您就没有使用数据抽象。 - Tim Bender

1
超级类:
public abstract class SupperClass {
  protected Foo foo;

  //primary constructor
  public SupperClass(Foo foo) {
      this.foo = foo;
  }

  private SupperClass(){
    //with this private constructor, the subclass forced to implement primary constructor
  }

}

子类:
public class SubClass extends JLLayer {

  public SubClass(Foo foo) {
      super(foo);
  }
}

0
标题中的问题是“如何强制在我的抽象类的所有子类中定义构造函数”,我想回答这个问题,而不是讨论楼主的真实需求。
答案是:
你可以选择使用APT处理器。
@SupportedAnnotationTypes("pl.selvin.constructorsconstraints.apt.ConstructorConstraint")
@SupportedSourceVersion(SourceVersion.RELEASE_8)
public class ConstructorConstraintProcessor extends AbstractProcessor {
    private static final TypeVisitor<Boolean, ArrayList<String>> constraintArgsVisitor =
            new SimpleTypeVisitor7<Boolean, ArrayList<String>>() {
                public Boolean visitExecutable(ExecutableType t, ArrayList<String> args) {
                    final List<? extends TypeMirror> types = t.getParameterTypes();
                    if (args.size() != types.size()) {
                        return false;
                    }
                    for (int i = 0; i < args.size(); i++) {
                        if (!args.get(i).equals(types.get(i).toString()))
                            return false;
                    }
                    return true;
                }
            };

    @Override
    public Set<String> getSupportedOptions() {
        final HashSet<String> ret = new HashSet<>();
        ret.add("org.gradle.annotation.processing.aggregating");
        return ret;
    }

    public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment env) {
        for (final TypeElement type : annotations) {
            processConstructorConstraintClasses(env, type);
        }
        return true;
    }

        private void processConstructorConstraintClasses(final RoundEnvironment env, final TypeElement type) {
        final Element constructorConstraintElement = processingEnv.getElementUtils().getTypeElement(ConstructorConstraint.class.getName());
        final TypeMirror constructorConstraintType = constructorConstraintElement.asType();
        final HashMap<String, ArrayList<String>> constructorConstraints = new HashMap<>();
        final ArrayList<Element> elements = new ArrayList<>();
        for (final Element element : env.getElementsAnnotatedWith(type)) {
            elements.add(element);
            for (AnnotationMirror am : element.getAnnotationMirrors()) {
                if (am.getAnnotationType().equals(constructorConstraintType)) {
                    for (Map.Entry<? extends ExecutableElement, ? extends AnnotationValue> entry : am.getElementValues().entrySet()) {
                        if ("arguments".equals(entry.getKey().getSimpleName().toString()) && entry.getValue() instanceof Attribute.Array) {
                            final Attribute.Array array = (Attribute.Array) entry.getValue();
                            for (final Attribute a : array.values) {
                                final String className = element.toString();
                                final ArrayList<String> arguments;
                                if(constructorConstraints.containsKey(className)) {
                                    arguments = constructorConstraints.get(className);
                                } else {
                                    arguments = new ArrayList<>();
                                    constructorConstraints.put(className, arguments);
                                }
                                arguments.add(a.getValue().toString());
                            }
                        }
                    }
                    break;
                }
            }
        }
        for (Element element : elements) {
            final TypeMirror derived = element.asType();
            for (String className : constructorConstraints.keySet()) {
                final TypeMirror baseType = processingEnv.getElementUtils().getTypeElement(className).asType();
                if(derived.equals(baseType)) {
                    continue;
                }
                if(processingEnv.getTypeUtils().isAssignable(derived, baseType)) {
                    processClass(element, constructorConstraints.get(className));
                }
            }
        }
    }

    private void processClass(Element element, ArrayList<String> arguments) {
        if (!doesClassContainConstructorWithConstraint(element, arguments)) {
            final String needs;
            if (arguments == null || arguments.size() == 0) {
                needs = "a No-Args Constructor";
            } else {
                needs = "a Contrcutor with arguments(" + String.join(", ", arguments) + ")";
            }
            processingEnv.getMessager().printMessage(Diagnostic.Kind.ERROR, "Class " + element + " needs " + needs);
        }
    }

    private boolean doesClassContainConstructorWithConstraint(Element element, ArrayList<String> arguments) {
        for (final Element subElement : element.getEnclosedElements()) {
            if (subElement.getKind() == ElementKind.CONSTRUCTOR && subElement.getModifiers().contains(Modifier.PUBLIC)) {
                final TypeMirror mirror = subElement.asType();
                if (mirror.accept(constraintArgsVisitor, arguments))
                    return true;
            }
        }
        return false;
    }
}

优点:
- 如果派生类没有这样的构造函数,它将在编译时报错。
缺点:
  • 像泛型类一样

    class Whatever<T>{
        Whatever(T arg) {}
    }
    
  • 多个构造函数

  • 它不是增量的

Github页面: https://github.com/SelvinPL/ConstructorsConstraints - 修复了多个基类注释导致的奇怪行为
FX我想使用类似的东西
public class QueryProviders {
    private final ArrayList<AbstractQueryProvider> providers = new ArrayList<>();

    @SafeVarargs
    public QueryProviders(SomeClass arg1, ...OtherArgs... ,
                          Class<? extends AbstractQueryProvider>... providerClasses) {
        try {
            for (Class<? extends AbstractQueryProvider> providerClass : providerClasses) {
                final Constructor<? extends AbstractQueryProvider> ctor = providerClass.getConstructor(SomeClass arg1, ...OtherArgs...);
                providers.add(ctor.newInstance(arg1, ...OtherArgs...));
            }
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    }
}

和班级

public abstract class AbstractQueryProvider {
    public AbstractQueryProvider(SomeClass arg1, ...OtherArgs...) {

    }
}

和使用

QueryProviders providers = new QueryProviders(SomeClass arg1, ...OtherArgs...,
            Backup.class,
            GoodsPrices.class, ...OtherDerived...);

其中BackupGoodsPrices是从AbstractQueryProvider派生出来的。

当我忘记这样的构造函数时,我需要在编译时出现错误(换句话说,“强制一个构造函数”)

因此,使用APT处理器,您将拥有以下类:

@ConstructorConstraint(SomeClass.class, ...)
public abstract class AbstractQueryProvider {
    public AbstractQueryProvider(SomeClass arg1, ...) {

    }
}

(包: "pl.selvin.constructorsconstraints.apt") 带有这样的注释:
@Inherited
@Documented
@Retention(RetentionPolicy.SOURCE)
@Target(ElementType.TYPE)
public @interface ConstructorConstraint {
    Class<?>[] arguments() default {};
}

-2

让抽象类拥有一个抽象方法,该方法需要传入您想要的参数。例如:

public abstract void setSize(int rows,int columns);

-2

免责声明:

  • 咔嚓咔嚓“使用工厂”
  • 咔嚓咔嚓“封装和数据抽象”
  • 咔嚓咔嚓“运行时抛出”

标题中的问题是“如何强制要求在我的抽象类的所有子类中定义构造函数”,我想回答这个问题,而不是讨论OP的真实需求。

答案是:

您可以选择APT处理器

例如,我想使用以下内容:

public class QueryProviders {
    private final ArrayList<AbstractQueryProvider> providers = new ArrayList<>();

    @SafeVarargs
    public QueryProviders(SomeClass arg1, ...OtherArgs... ,
                          Class<? extends AbstractQueryProvider>... providerClasses) {
        try {
            for (Class<? extends AbstractQueryProvider> providerClass : providerClasses) {
                final Constructor<? extends AbstractQueryProvider> ctor = providerClass.getConstructor(SomeClass arg1, ...OtherArgs...);
                providers.add(ctor.newInstance(arg1, ...OtherArgs...));
            }
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    }
}

和类

public abstract class AbstractQueryProvider {
    public AbstractQueryProvider(SomeClass arg1, ...OtherArgs...) {

    }
}

和使用

QueryProviders providers = new QueryProviders(SomeClass arg1, ...OtherArgs...,
            Backup.class,
            GoodsPrices.class, ...OtherDerived...);

其中BackupGoodsPrices是从AbstractQueryProvider派生的。

当我忘记这样的构造函数时,我需要编译时错误(换句话说,“强制构造函数”)

因此,使用APT处理器,您将拥有以下类:

@ConstructorConstraint(SomeClass.class, ...)
public abstract class AbstractQueryProvider {
    public AbstractQueryProvider(SomeClass arg1, ...) {

    }
}

(包:“pl.selvin.constructorsconstraints.apt”) 带有以下注释:

@Inherited
@Documented
@Retention(RetentionPolicy.SOURCE)
@Target(ElementType.TYPE)
public @interface ConstructorConstraint {
    Class<?>[] arguments() default {};
}

像这样的处理器:

@SupportedAnnotationTypes("pl.selvin.constructorsconstraints.apt.ConstructorConstraint")
@SupportedSourceVersion(SourceVersion.RELEASE_8)
public class ConstructorConstraintProcessor extends AbstractProcessor {
    private static final TypeVisitor<Boolean, ArrayList<String>> constraintArgsVisitor =
            new SimpleTypeVisitor7<Boolean, ArrayList<String>>() {
                public Boolean visitExecutable(ExecutableType t, ArrayList<String> args) {
                    final List<? extends TypeMirror> types = t.getParameterTypes();
                    if (args.size() != types.size()) {
                        return false;
                    }
                    for (int i = 0; i < args.size(); i++) {
                        if (!args.get(i).equals(types.get(i).toString()))
                            return false;
                    }
                    return true;
                }
            };

    @Override
    public Set<String> getSupportedOptions() {
        final HashSet<String> ret = new HashSet<>();
        ret.add("org.gradle.annotation.processing.aggregating");
        return ret;
    }

    public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment env) {
        for (final TypeElement type : annotations) {
            processConstructorConstraintClasses(env, type);
        }
        return true;
    }

        private void processConstructorConstraintClasses(final RoundEnvironment env, final TypeElement type) {
        final Element constructorConstraintElement = processingEnv.getElementUtils().getTypeElement(ConstructorConstraint.class.getName());
        final TypeMirror constructorConstraintType = constructorConstraintElement.asType();
        final HashMap<String, ArrayList<String>> constructorConstraints = new HashMap<>();
        final ArrayList<Element> elements = new ArrayList<>();
        for (final Element element : env.getElementsAnnotatedWith(type)) {
            elements.add(element);
            for (AnnotationMirror am : element.getAnnotationMirrors()) {
                if (am.getAnnotationType().equals(constructorConstraintType)) {
                    for (Map.Entry<? extends ExecutableElement, ? extends AnnotationValue> entry : am.getElementValues().entrySet()) {
                        if ("arguments".equals(entry.getKey().getSimpleName().toString()) && entry.getValue() instanceof Attribute.Array) {
                            final Attribute.Array array = (Attribute.Array) entry.getValue();
                            for (final Attribute a : array.values) {
                                final String className = element.toString();
                                final ArrayList<String> arguments;
                                if(constructorConstraints.containsKey(className)) {
                                    arguments = constructorConstraints.get(className);
                                } else {
                                    arguments = new ArrayList<>();
                                    constructorConstraints.put(className, arguments);
                                }
                                arguments.add(a.getValue().toString());
                            }
                        }
                    }
                    break;
                }
            }
        }
        for (Element element : elements) {
            final TypeMirror derived = element.asType();
            for (String className : constructorConstraints.keySet()) {
                final TypeMirror baseType = processingEnv.getElementUtils().getTypeElement(className).asType();
                if(derived.equals(baseType)) {
                    continue;
                }
                if(processingEnv.getTypeUtils().isAssignable(derived, baseType)) {
                    processClass(element, constructorConstraints.get(className));
                }
            }
        }
    }

    private void processClass(Element element, ArrayList<String> arguments) {
        if (!doesClassContainConstructorWithConstraint(element, arguments)) {
            final String needs;
            if (arguments == null || arguments.size() == 0) {
                needs = "a No-Args Constructor";
            } else {
                needs = "a Contrcutor with arguments(" + String.join(", ", arguments) + ")";
            }
            processingEnv.getMessager().printMessage(Diagnostic.Kind.ERROR, "Class " + element + " needs " + needs);
        }
    }

    private boolean doesClassContainConstructorWithConstraint(Element element, ArrayList<String> arguments) {
        for (final Element subElement : element.getEnclosedElements()) {
            if (subElement.getKind() == ElementKind.CONSTRUCTOR && subElement.getModifiers().contains(Modifier.PUBLIC)) {
                final TypeMirror mirror = subElement.asType();
                if (mirror.accept(constraintArgsVisitor, arguments))
                    return true;
            }
        }
        return false;
    }
}

优点:

  • 如果派生类没有这样的构造函数,它将在编译时抛出错误

缺点:

  • 泛型类,例如

    class Whatever<T>{
        Whatever(T arg) {}
    }
    
  • 多个构造函数

  • 它不是增量的

Github页面: https://github.com/SelvinPL/ConstructorsConstraints - 修复了多个基类注释导致的奇怪行为。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接