【问题标题】:Using Java Reflection to determine which class to instantiate使用 Java 反射来确定要实例化哪个类
【发布时间】:2015-01-30 23:13:48
【问题描述】:

有一个 Message 超类和各种 Message 子类,如 WeddingMessage、GreetingMessage、FarewellMessage、Birthday Message。

Message 超类有一个构造函数:

public Message(String messageType){
        this.messageType = messageType;
}

消息子类都有不同的构造函数,但是它们都调用超类,并在其中将 messageType 作为参数传递 例如:

public BirthdayMessage( String name, int age){
    super("birthday");
    System.out.println("Happy birthday " + name + "You are " + age " years old");

public FareWellMessage(String name, String message){
    super("farewell");
    System.out.println(message + " " + name);
}

创建的消息类型由用户传入的参数决定。例如,如果用户插入“birthday John 12”,那么将使用参数 John 和 12 创建 BirthdayMessage。如果用户输入“Farewell Grace take care”,则使用这些参数创建 FarewellMessage 的实例。

而不是一堆 if/else 语句或 switch case,以类似的形式 -

words[] = userinput.slice(' ');
word1 = words[0];
if (word1 == birthday)
     create new BirthdayMessage(parameters here)
if (word1 == wedding)
    create new weddingMessage(parameters here)

我如何使用反射来确定要创建哪种类型的 Message 类。 我目前的想法是使用 File 类来获取包中包含消息子类的所有文件。然后使用反射来获取它们的每个构造函数参数类型,并查看它们是否与用户输入给出的参数匹配。然后用随机参数创建那些匹配类的实例。完成后,子类将使用其 messageType 调用其超类构造函数。然后我可以检查 messageType 变量是否与用户输入匹配。

所以如果用户输入“生日约翰 23” 我发现包中的所有构造函数都将 String 和 int 作为参数并具有字段 messageType(继承自 Message)。然后我创建该类的一个实例并检查 messageType 是否 == 到用户输入中的第一个单词(在本例中为生日)。如果是,那么我使用用户提供的参数创建该类的实例。

有没有更好的方法通过反射来做到这一点?

【问题讨论】:

  • 它会比 if/else 或 switch 语句更复杂。
  • 每个消息类型构造函数的parameters here 会不同吗?
  • @JoseMartinez,我相信是的。 OP 在问题中显示了 2 个似乎不同的示例(BirthdayMessage 和 FarewellMessage)。
  • 如果参数不同,问题就会变得更难解决。 if else 语句没有任何问题,只要它们被很好地封装在工厂类中。
  • 如果消息空间有限,您可以使用 Enum,但我同意 @Pshemo 的观点,您可能应该只使用不同的格式字符串,而不是疯狂上课。

标签: java design-patterns reflection dynamic-class-creation


【解决方案1】:

如果你想走这条路(我讨厌反射,但它有它的用途),请确保将它隔离在工厂类中。我建议查看 @Annotations 并使用特定注释标记要扫描的类。

类似:(必须承认,写这个例子真的很有趣)

注释:

@Target(value = {ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface GreetingcardInstance {
    public String value();
}

你的 2 个消息类和基类

public abstract class Message {
    private String name;
    public Message(String name) {
        this.name = name; // not used, but to keep it in line with your example
    }
}

@GreetingcardInstance("birthday")
public class BirthdayMessage extends Message {
    public BirthdayMessage(Integer i) {
        super("birthday");
        // this line prints if this works.
        System.out.println("Birthdaymessage created: " +i);
    }
}

@GreetingcardInstance("other")
public class OtherMessage extends Message{
    public OtherMessage(Integer i, Integer j) {
        super("other");
    }
}

还有隐藏讨厌的反射代码的工厂

public class CardFactory {
    private final Map<String, Class> messageClasses;
    public CardFactory() {
        // I had all my implementations of Message in the package instances
        Reflections reflections = new Reflections("instances");
        Set<Class<?>> greetingCardAnnotations = reflections.getTypesAnnotatedWith(GreetingcardInstance.class);
        Map<String, Class> result = new HashMap<String, Class>();
        for (Class c : greetingCardAnnotations) {
            if (Message.class.isAssignableFrom(c)) {
                GreetingcardInstance annotation = (GreetingcardInstance) c.getAnnotation(GreetingcardInstance.class);
                result.put(annotation.value(), c);
            }
        }
        messageClasses = result;
    }

    public Message createMessage(String messageType, Object[] arguments) throws NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException {
        Class clazz = messageClasses.get(messageType);
        if (clazz == null) {
            throw new IllegalArgumentException("MessageType: " + messageType + " not supported");
        }
        Class[] argumentTypes = new Class[arguments.length];
        for (int i = 0; i < arguments.length; ++i) {
            argumentTypes[i] = arguments[i].getClass();
        }
        Constructor constructor = clazz.getConstructor(argumentTypes);
        return (Message) constructor.newInstance(arguments);
    }
}

您可以使用springgoogle's library 或手动扫描它们,尽管您会发现这很麻烦。在此示例中,我使用了运行良好的 google 库。

在这个特定的实现中,所有类都存在于同一个包中。我不认为这太糟糕,但可能不适合你。

我也没有处理基本类型,在这种情况下,构造函数采用整数,而不是我最初打算的 int。

解析字符串时,只需将参数解析为String、INteger等,并将它们作为Object[]传递,将用作构造函数args。

public static void main(String[] argv) throws InvocationTargetException, NoSuchMethodException, InstantiationException, IllegalAccessException {
    CardFactory cf = new CardFactory();
    System.out.println(cf.toString());

    cf.createMessage("birthday", new Object[] { new Integer(0) });
}

输出:

Birthdaymessage created: 0

【讨论】:

  • 注意,这不是线程安全的。这个工厂应该在某个地方的堆栈上。 (虽然地图不会主动写入,因为它只会在施工期间完成,所以从技术上讲它可能是安全的)我仍然觉得有必要指出这一点。如果您想让它成为单例,请用线程安全映射替换。另外,spring 有很好的扫描能力,但是我不想为这个例子设置一个 spring 项目
【解决方案2】:

不是为每种消息类型创建不同的类,而是使用可以存储在Map&lt;String,String&gt;之类的不同格式的不同格式,这不是更简单吗?

我的意思是像

Map<String,String> formats = new HashMap<>();
formats.put("birthday","Happy birthday %s. You are %d years old%n");//name, age
formats.put("farewell","%s %s%n");//message, name       

Object[] data = {"Dany", 5};
System.out.printf(formats.get("birthday"),data);

data = new Object[]{"Ferwell Jack.","We will miss you"};
System.out.printf(formats.get("farewell"),data);

如果您不想在每次更改格式后重新编译代码,您可以将它们存储在文件中,并在应用程序启动或需要时加载。
简单的方法是通过java.util.Properties 类。

您可以创建formats.properties 文件,其内容类似于

birthday=%s 生日快乐。你已经 %d 岁了%n
告别=%s %s%n

使用它的代码可能看起来像

Properties formatProp = new Properties();
formatProp.load(new FileReader("formats.properties"));//        

Object[] data = {"Dany", 5};
System.out.printf(formatProp.getProperty("birthday"),data);

data = new Object[]{"Ferwell Jack.","We will miss you"};
System.out.printf(formatProp.getProperty("farewell"),data);

【讨论】:

  • 您假设 Message 只是一个字符串,而不是更复杂的对象。对于字符串,这可以工作,但是对于(作为 OP 发布的)类层次结构来说,这将如何工作?
  • @Joeblade 我不假设任何事情,我提出了其他方法,它可以让我们用更简单的代码拥有类似的功能(无需创建类层次结构)。
  • 好吧,我可能误解了你的意图。按照我的阅读方式,您假设作者只想执行 out.printf。仅此而已。如果这是他唯一想要的,那么是的,这比反射/类层次结构更可取。但如果作者想做其他事情,这种方法将不起作用。 (例如,如果 age = 10 则显示消息 B,用于问候,或基于性别的更改,或存储在 db 表中,或任何其他可能隐藏在类层次结构中的东西。
  • @Joeblade "看来你假设作者只想做 out.printf" 我不是假设 OP only 想要这个功能,但是这个问题中的例子只描述了这个功能,所以我把我的答案集中在它上面。如果 OP 将提供更多关于为什么我的解决方案不够的信息,那没关系,我将删除我的答案或将其升级到将考虑到 OP 的其他需求的东西,但在那之前我将保持原样(我喜欢简单的解决方案,为什么不展示其中之一呢?)。
  • 很公平 :) 如果 OP 可以避免反思,我会认为这是所有人的胜利。
【解决方案3】:

有很多方法可以做你想做的事。一种方法是学习如何使用像Google Guice 这样的注入库。从长远来看,您可能会从中获得最大的收益。另一种选择是学习像Clojure edit Clojure 示例这样的语言。

如果您想看一个看起来像 Java 的最小示例,以下类的 main 将向您展示如何做到这一点。基本上,它需要一个String->Classnames(字符串)的映射,并将其转换为String->Class(对象)的映射,然后一个超级简单的构建器方法在映射中查找代码字并构造一个新的实例该类并返回它。

主要构建其中两个并打印它们的输出。例如

I am a bar.
I'm a baz!

这是 Java 程序。如果更改包,则必须更改 textConfig 变量中的类名。下面是等效的 Clojure 代码。

package foo;

import java.util.HashMap;
import java.util.Map;
import java.util.stream.Collectors;

public abstract class Foo {

    public abstract String something(); // the abstract method

    static class Bar extends Foo {  // one subclass
        @Override public String something() {
            return "I am a bar.";
        }
    }

    static class Baz extends Foo { // another subclass
        @Override public String something() {
            return "I'm a baz!";
        }
    }

    public static Class loadClass(String classname) {        
        try { // wrapper for Class.forName that doesn't throw checked exception
            return Class.forName(classname);
        } catch (ClassNotFoundException ex) { 
            throw new IllegalArgumentException(ex);
        }
    }

    public static Map<String, Class> buildConfig(Map<String, String> textConfig) {
        // turn {codeword, classname} into {codeword, class} entries
        // java 8 voodoo follows...
        return textConfig.entrySet().stream().collect(Collectors.toMap(
                Map.Entry::getKey,
                e -> loadClass(e.getValue())));
    }

    public static Foo construct(Map<String, Class> config, String codeword) {
        try { // lookup codeword and return new instance of class
            return (Foo)config.get(codeword).newInstance();
        }
        catch(InstantiationException | IllegalAccessException ex) {
            throw new IllegalArgumentException(ex);
        }
    }

    public static void main(String[] args) {
        // some configuration, you could hardcode this, or even put the
        // data in annoations if you want to be fancy
        Map<String, String> textConfig = new HashMap<>();
        textConfig.put("codeword-bar", "foo.Foo$Bar");
        textConfig.put("codeword-baz", "foo.Foo$Baz");

        // turn your text config into something more immediately useful
        Map<String, Class> config = buildConfig(textConfig);

        // show that it works.
        System.out.println(construct(config, "codeword-bar").something());
        System.out.println(construct(config, "codeword-baz").something());
    }
}

编辑

上面代码的冗长让我发疯了。所以,如果你有兴趣,这里是 Clojure 中的等效代码。

它将两个函数放入带有键 :bar:baz 的映射中,然后查找、调用它们并打印返回值。

user=> (def config {:bar (fn [] "I am a bar.") :baz (fn [] "I'm a bar!")})
#'user/config
user=> (println ((:bar config)))
I am a bar.
nil
user=> (println ((:baz config)))
I'm a bar!
nil

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2013-01-18
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2012-12-16
    • 1970-01-01
    • 2013-07-03
    相关资源
    最近更新 更多