根据传递的参数选择接口的实现



我有一个父接口。

public interface Parent{...}

以及实现此接口的两个类。

public class ChildA implements Parent {...}
public class ChildB implements Parent {...}

现在我想根据传递的参数创建一个子对象。像这样的东西,

Parent instance1 = Parent.getChild("Key1");

目前,我正在使用哈希图来实现这一点。

static Map<String,Class<?>> map = new HashMap<String,Class<?>>(); 

static void init() throws ClassNotFoundException {
map.put("Key1",Class.forName("ChildA"));
map.put("Key2",Class.forName("ChildB"));
}

static Parent getChild(String key) throws InstantiationException, IllegalAccessException {
return (Parent) map.get(key).newInstance();
}

但是这里的问题是每次我实现一个新的子级时,我都必须将其添加到 Parent init 方法中。那么有没有更清洁的方法呢?就像将密钥的信息添加到孩子本身一样,

public class ChildA implements Parent {

private String key = "Key1";
...

因此,当我从父类调用getChild时,它指的是相应的Child。

基本上,我要求一种基于传递的参数动态引用父级子级的方法。

您可以使用服务提供程序机制。这在积极使用Java的模块系统时效果最好,因为当时,必要的部分甚至集成到Java语言中。

如果可以在其他模块中实现,则包含Parent接口的模块必须导出其包。然后,想要查找接口实现的模块必须具有

uses yourpackage.Parent;

指令在其模块信息中。提供实现的模块必须具有类似

provides yourpackage.Parent with yourpackage.ChildA, yourpackage.ChildB;

在其模块信息中。可以使用该接口并在同一模块中提供实现。这也可能是包含接口声明的模块。在这种情况下,甚至可以仅在单个模块中使用该机制,而根本不导出接口。

编译器已经检查指定的实现类是否public,具有publicno-arg 构造函数,并真正实现服务接口。实现不需要驻留在导出的包中;接口可能是从另一个模块访问实现的唯一方法。

由于编译器预先检查了所需的不变量,因此无需处理反射固有的问题。例如,您不需要声明或处理InstantiationExceptionIllegalAccessException

一个简单的设置可以是

模块信息
module ExampleApp {
uses yourpackage.Parent;
provides yourpackage.Parent with yourpackage.ChildA, yourpackage.ChildB;
}
您的包裹/家长
package yourpackage;
public interface Parent {
}
您的包装/钥匙
package yourpackage;
import java.lang.annotation.*;
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface TheKey {
String value();
}
您的包裹/儿童A
package yourpackage;
@TheKey("Key1")
public class ChildA implements Parent {
}
您的包裹/儿童B
package yourpackage;
@TheKey("Key2")
public class ChildB implements Parent {
}
独立包/使用接口
package independentpackage;
import java.util.ServiceLoader;
import yourpackage.Parent;
import yourpackage.TheKey;
public class UsingTheInterfaces {
public static void main(String[] args) {
Parent p = getChild("Key1");
System.out.println(p);
}
static Parent getChild(String key) {
return ServiceLoader.load(Parent.class).stream()
.filter(p -> {
TheKey keyAnno = p.type().getAnnotation(TheKey.class);
return keyAnno != null && keyAnno.value().equals(key);
})
.findAny()
.map(ServiceLoader.Provider::get)
.orElse(null); // decide how to handle absent keys
}
}

如前所述,当您有exports yourpackage;指令时,其他模块可以提供实现,ServiceLoader将动态发现运行时存在的所有模块的这些实现。using 模块不需要在编译时知道它们。

对于没有模块系统的旧 Java 版本,有一个前置机制,该机制也由ServiceLoader以向后兼容的方式处理。这些提供程序必须打包在包含文件META-INF/services/yourpackage.Parent包含实现接口的 jar 文件中所有类的列表的 jar 文件中。在Java 9之前,ServiceLoader也缺少Stream API支持,该支持允许在实例化实现之前查询注释。您只能使用已经实例化类并返回实例的Iterator

避免旧 API 产生不必要开销的一种方法是将接口拆分为服务提供程序接口和实际服务接口。提供程序接口将提供元信息,并充当实际服务实现的工厂。与CharsetProviderCharsetFileSystemProviderFileSystem之间的关系进行比较。这些例子也表明,服务提供者机制已经被广泛使用。可以在java.base模块文档中找到更多示例。

这只是一个概述;ServiceLoader的API文档包含有关该机制的更多详细信息。

选项 1:我建议您使用像这样的简单工厂类:

public class ChildFactory {
public static Parent getChild(String key) throws ClassNotFoundException {
if (key.equals("Key1"))
return new ChildA();
if (key.equals("key2"))
return new ChildB();
throw new ClassNotFoundException();
}
}

这些是原因:

  1. 简单易扩展:类只做一件事,创建对象
  2. 不要使用已弃用的 newInstance((
  3. 从静态"工厂"方法中清除接口父级

选项 2:定义一个函数,给定键返回类名并使用它来创建实例:

public class ChildFactory {
public static Parent getChild(String key) {
String className = childClassNameFor(key);
return (Parent) Class.forName("my.package."+className).newInstance();
}
private static String childClassNameFor(String key) {
return "Child" + key;
}
}

然后你可以像这样使用它:

Parent p = ChildFactory.getChild("A");

最新更新