如何使用超类的构造函数创建子类实例

18
我想创建一个注册表,用于存储超类的子类。这些类存储在充当注册表的映射中。根据一个键从注册表中选择一个类,并通过反射创建该类的实例。
我希望根据超类的构造函数(带有1个参数),来实例化一个类。只有在子类中声明构造函数时才能正常工作。
是否有一种方法可以使用超类的构造函数来实例化类?是否有一种方法可以使代码具有类型安全性?
示例代码:
public class ReflectionTest {

    /**
     * Base class with no-args constructor and another constructor with 1 parameter
     */
    public static class BaseClass {

        Object object;

        public BaseClass() {
            System.out.println("Constructor with no args");
        }

        public BaseClass( Object object) {
            this.object = object;
            System.out.println("Constructor with parameter= " + object);
        }

        public String toString() {
            return "Object = " + object;
        }
    }

    /**
     * Subclass with 1 parameter constructor
     */
    public static class SubClass1 extends BaseClass {
        public SubClass1( Object object) {
            super(object);
        }
    }

    /**
     * Subclass with no-args constructor
     */
    public static class SubClass2 extends BaseClass {

    }

    public static void main(String[] args) {

        // registry for classes
        Map<Integer,Class<?>> registry = new HashMap<>();
        registry.put(0, SubClass1.class);
        registry.put(1, SubClass2.class);

        // iterate through classes and create instances
        for( Integer key: registry.keySet()) {

            // get class from registry
            Class<?> clazz = registry.get(key);

            try {

                // get constructor with parameter
                Constructor constructor = clazz.getDeclaredConstructor( Object.class);

                // instantiate class
                BaseClass instance = (BaseClass) constructor.newInstance(key);

                // logging
                System.out.println("Instance for key " + key + ", " + instance);

            } catch (NoSuchMethodException | SecurityException | InstantiationException | IllegalAccessException | IllegalArgumentException | InvocationTargetException e) {
                e.printStackTrace();
            }

        }

        System.exit(0);
    }
}

示例给出以下控制台输出:

Constructor with parameter= 0
Instance for key 0, Object = 0
java.lang.NoSuchMethodException: swing.table.ReflectionTest$SubClass2.<init>(java.lang.Object)
    at java.lang.Class.getConstructor0(Class.java:3082)
    at java.lang.Class.getConstructor(Class.java:1825)
    at swing.table.ReflectionTest.main(ReflectionTest.java:63)

如果一个特定的子类同时具有零个和一个参数的构造函数,您想要做什么? - Andy Turner
4个回答

12
  1. 一个超类没有关于它的子类的知识。
  2. 构造函数不会被继承。

因此,如果不对子类的构造函数做出任何假设,您就无法编写所需的代码。

那么,该怎么办呢?使用“抽象工厂”模式。

我们可以创建一个“接口工厂”:

@FunctionalInterface
public interface SuperclassFactory {
    Superclass newInstance(Object o);
}

你可以在 Factory 上创建多个方法,但这会使 lambda 表达式不太简洁。

现在你有一个 Map<Integer,SuperclassFactory>,并将其填充:

Map<Integer,SuperclassFactory> registry = new HashMap<>();
registry.put(0, SubClass1::new);
registry.put(1, SubClass2::new);

所以,要使用此 Map,您只需执行以下操作:
for(final Map.Entry<Integer,SuperclassFactory> e: registry.entrySet()) {
    //...
    final BaseClass instance = e.getValue().newInstance(e.getKey());
    //...
}

如果你的子类没有适当的构造函数,那么这段代码将无法编译,因为没有可以使用的构造函数引用。这是一件好事(TM)。为了使用当前的Subclass2进行编译,你需要使用:

registry.put(1, obj -> new SubClass2());

现在我们有:

  1. 失去了反射功能
  2. 获得了编译时类型安全性
  3. 失去了丑陋的转换(尽管这是通过误用反射实现的)

N.B. 遍历 MapentrySet() 而不是它的 keySet()


1
e.getValue().apply(e.getKey()) 必须改为 e.getValue().newInstance(e.getKey()) - user4910279
非常好的答案,肯定以一种干净和安全的方式解决了问题(我希望在SO上有更多这样的答案)。这让我想起了这个问题:https://dev59.com/0JTfa4cB1Zd3GeqPQ3Xb。 - Spotted

1
在子类中需要显式定义构造函数。如果超类中定义了构造函数,那么这并不意味着可以使用该构造函数来创建子类的实例,无论您是否使用反射。
由于您的SubClass2没有带一个参数的构造函数,因此当您尝试使用一个参数创建它的实例时,会抛出NoSuchMethodException异常。

1
  • 使用clazz.getDeclaredConstructors()获取类的所有构造函数;
  • 遍历它们以找到最佳适用的构造函数,例如,如果不存在单个Object参数构造函数,则选择零参数构造函数;
  • 使用适当的参数调用该构造函数。

这样做完全没有绝对的安全性,因为您无法预先知道给定类是否存在任何适用的公共构造函数,例如,构造函数可能是私有的,或者可用的构造函数可能不接受您想要的类型的参数(例如需要一个字符串,但您只有一个对象)。


1
有人可能会认为,通过反射机制,“private”并不是一个障碍。但这样做只会导致疯狂... - Boris the Spider

0

你正在使用这行代码获取构造函数

clazz.getDeclaredConstructor( Object.class);

但是你的Subclass2没有单参数构造函数,因此会抛出异常。

使用clazz.getDeclaredConstructors()方法,根据参数数量调用构造函数。

            BaseClass instance;
            // get constructor with parameter
            Constructor constructor = clazz.getDeclaredConstructors()[0];
            if (constructor.getParameterCount() == 1) {
                instance = (BaseClass) constructor.newInstance(key);
            } else {
                instance = (BaseClass) constructor.newInstance();
            }  

1
如果我有一个接受String和一个接受number的构造函数,该怎么办?您需要检查这些参数的类型。 - Boris the Spider
这不是他代码中的问题,对吧?BaseClass实例 =(BaseClass)constructor.newInstance(key); 他想要通过显式传递参数来创建一个实例。如果他想传递一个字符串,他就不会在构造函数中传递KEY。 - mirmdasif
1
当然,不是这个具体实例。但是OP正在寻找一般解决方案-我相信OP完全有能力以某种方式解决这个问题。该问题要求如何在一般情况下完成-而这不是一个完整的答案。 - Boris the Spider

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