在不加载类的情况下使Java中的静态初始化块运行

9
我这里有几个类,如下所示:
public class TrueFalseQuestion implements Question{
    static{
        QuestionFactory.registerType("TrueFalse", "Question");
    }
    public TrueFalseQuestion(){}
}

...

public class QuestionFactory {

 static final HashMap<String, String > map =  new HashMap<String,String>();

 public static void registerType(String questionName, String ques ) {
     map.put(questionName, ques);
     }
 }



public class FactoryTester {
    public static void main(String[] args) {
        System.out.println(QuestionFactory.map.size());
        // This prints 0. I want it to print 1
    }
}

我该如何更改TrueFalseQuestion类,以便静态方法始终运行,这样当我运行主方法时,我得到1而不是0?我不想改变主方法。
实际上,我正在尝试实现工厂模式,其中子类向工厂注册,但我已经简化了这个问题的代码。
4个回答

6

您可以调用以下内容:

Class.forName("yourpackage.TrueFalseQuestion");

这将会在你不用手动操作的情况下加载该类,并执行静态初始化块。

我应该在哪里调用这个方法?每个类都在不同的文件中。 - randomThought
有没有办法可以在不更改主方法的情况下实现这一点,因为这会在主方法中创建这个类的某种依赖关系,而我想避免这种情况。 - randomThought
有没有什么好办法同时解决运行时依赖的问题? - randomThought
1
@TP:如果您可以枚举包中的所有类,那么您只需查询实现“Question”的所有名称并执行此操作。由于这是不可能的(请参见此处:https://dev59.com/wXRB5IYBdhLWcg3wxZ1K),因此您只能手动检查JAR文件(呃)或使用某种配置文件... - R. Martinho Fernandes
@TP,正如@Martinho Fernandes所说的那样。 - Bozho
显示剩余2条评论

5
为了将TrueFalseQuestion类注册到工厂中,需要调用其静态初始化器。要执行TrueFalseQuestion类的静态初始化器,需要引用该类或通过反射加载它,在调用QuestionFactory.map.size()之前。如果您不想改动main方法,您需要在QuestionFactory的静态初始化器中引用它或通过反射加载它。我认为这不是一个好主意,但我会回答你的问题 :) 如果您不介意QuestionFactory知道所有实现Question的类来构造它们,您可以直接引用它们或通过反射加载它们。例如:
public class QuestionFactory {

 static final HashMap<String, String > map =  new HashMap<String,String>();

 static {
    this.getClassLoader().loadClass("TrueFalseQuestion");
    this.getClassLoader().loadClass("AnotherTypeOfQuestion"); // etc.
 }

 public static void registerType(String questionName, String ques ) {
     map.put(questionName, ques);
     }
 }

请确保在static块之前声明和构造map。如果您不希望QuestionFactory了解Question的实现细节,您需要将它们列在由QuestionFactory加载的配置文件中。我能想到的唯一其他(可能疯狂的)方法是查找实现Question的所有类的整个类路径 :) 如果实现Question的所有类都必须属于同一个包,则这可能效果更好--注意:我不支持此解决方案;)
我认为不应该在QuestionFactory静态初始化程序中执行任何操作,因为像TrueFalseQuestion这样的类有自己的静态初始化程序,调用QuestionFactory,此时QuestionFactory是一个未完全构造的对象,这只会引起麻烦。拥有一个简单列出您希望QuestionFactory知道如何构造的类的配置文件,然后在其构造函数中注册它们是一个不错的解决方案,但这意味着需要更改您的main方法。

作为参考,这个设计的出现是为了避免工厂需要知道问题类(在这里:https://dev59.com/60zSa4cB1Zd3GeqPrf-A)。 - R. Martinho Fernandes
1
我之前没有看到那个问题。需要了解Question接口的实现情况,无论是直接通过工厂还是通过某种配置文件。唯一的其他方法就是,如我所说,遍历类路径上的所有类并查看它们是否实现了Question接口。还要注意我的答案中有一个关于未完全构建的工厂的警告。它现在可能可以工作,但不能保证对象在未来(甚至在当前跨平台)的状态。 - Rob Heiser

4

如果从未加载类,则无法执行该类的静态初始化程序。

因此,您需要加载所有正确的类(这将很难,因为您不知道它们在编译时都是什么),或者消除对静态初始化程序的要求。

做后者的一种方法是使用 ServiceLoader

使用 ServiceLoader,您只需将文件放置在 META-INF/services/package.Question 中并列出所有实现。您可以有多个这样的文件,一个文件对应一个 .jar 文件。这样,您可以轻松地将额外的 Question 实现与主程序分开提供。

QuestionFactory 中,您可以简单地使用 ServiceLodaer.load(Question.class) 来获取一个 ServiceLoader,它实现了 Iterable<Question> 并且可以像这样使用:

for (Question q : ServiceLoader.load(Question.class)) {
    System.out.println(q);
}

2
为了运行静态初始化程序,需要加载类。为此,您的“主”类必须依赖(直接或间接)于这些类,或直接或间接地导致它们被动态加载;例如使用Class.forName(...)
我认为您正在尝试避免源代码中嵌入的依赖项。因此,静态依赖是不可接受的,使用硬编码类名调用Class.forName(...)也是不可接受的。
这留下了两个选择:
1.编写一些混乱的代码来迭代某个包中的资源名称,然后使用Class.forName(...)来加载那些看起来像您的类的资源。如果您的类路径复杂,则此方法很棘手,如果您的有效类路径包括具有远程URL的URLClassLoader,则此方法是不可能的(例如)。
2.创建一个文件(例如一个类加载器资源),其中包含要加载的类名列表,并编写一些简单的代码来读取该文件并使用Class.forName(...)来加载每个类。

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