根据传递的参数选择接口的实现
Selecting Implementation of Interface based on the Parameter passed
我有一个Parent接口。
public interface Parent{...}
还有两个 class 实现了这个接口。
public class ChildA implements Parent {...}
public class ChildB implements Parent {...}
现在我想根据传递的参数创建 child 的 object。像这样,
Parent instance1 = Parent.getChild("Key1");
目前,我正在通过使用 hashmaps 来实现这一点。
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();
}
但这里的问题是每次我实现一个新的 child 时,我都必须将它添加到 Parent 初始化方法中。那么有没有更清洁的方法来做到这一点?比如将密钥的信息添加到 child 本身,
public class ChildA implements Parent {
private String key = "Key1";
...
所以当我从 parent class 调用 getChild 时,它指的是相应的 Child.
基本上我是在寻求一种方法来根据传递的参数动态引用 parent 的 child。
选项 1:我建议您使用像这样的简单工厂 class:
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();
}
}
原因如下:
- 简单且易于扩展:class 只做一件事,创建对象
- 不要使用已弃用的 newInstance()
- 从静态“工厂”方法中清除接口 Parent
选项 2:定义一个函数,给它 returns 键 class 名称并使用它来创建实例:
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");
可以使用service provider mechanism。这在积极使用 Java 的模块系统时效果最好,必要的部分甚至集成到 Java 语言中。
包含 Parent
接口的模块必须导出其包,如果在其他模块中的实现应该是可能的。然后,想要查找接口实现的模块必须有一个
uses yourpackage.Parent;
指令在其模块信息中。提供实现的模块必须有一个像
这样的指令
provides yourpackage.Parent with yourpackage.ChildA, yourpackage.ChildB;
在其模块信息中。可以使用接口并在同一模块中提供实现。这也可能是包含接口声明的模块。在那种情况下,甚至可以只在单个模块中使用该机制,根本不导出接口。
编译器已经检查指定的实现 类 是 public
,具有 public
无参数构造函数,并真正实现服务接口。这些实现不需要驻留在导出的包中;接口可能是从另一个模块访问实现的唯一方法。
由于编译器会预先检查所需的不变量,因此您无需处理反射固有的问题。例如,您不需要声明或处理 InstantiationException
或 IllegalAccessException
.
一个简单的设置可以是
模块信息
module ExampleApp {
uses yourpackage.Parent;
provides yourpackage.Parent with yourpackage.ChildA, yourpackage.ChildB;
}
yourpackage/Parent
package yourpackage;
public interface Parent {
}
yourpackage/TheKey
package yourpackage;
import java.lang.annotation.*;
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface TheKey {
String value();
}
yourpackage/ChildA
package yourpackage;
@TheKey("Key1")
public class ChildA implements Parent {
}
yourpackage/ChildB
package yourpackage;
@TheKey("Key2")
public class ChildB implements Parent {
}
independentpackage/UsingTheInterfaces
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 支持。您只能使用已经实例化 类 和 return 实例的 Iterator
。
避免旧 API 不必要开销的一种方法是将接口拆分为服务提供者接口和实际服务接口。提供者接口将提供元信息并充当实际服务实现的工厂。比较CharsetProvider
and Charset
or FileSystemProvider
and FileSystem
. These examples also demonstrate that the service provider mechanism is already widely used. More examples can be found in the java.base
module documentation.
的关系
这只是一个概述; API documentation of ServiceLoader
包含有关该机制的更多详细信息。
我有一个Parent接口。
public interface Parent{...}
还有两个 class 实现了这个接口。
public class ChildA implements Parent {...}
public class ChildB implements Parent {...}
现在我想根据传递的参数创建 child 的 object。像这样,
Parent instance1 = Parent.getChild("Key1");
目前,我正在通过使用 hashmaps 来实现这一点。
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();
}
但这里的问题是每次我实现一个新的 child 时,我都必须将它添加到 Parent 初始化方法中。那么有没有更清洁的方法来做到这一点?比如将密钥的信息添加到 child 本身,
public class ChildA implements Parent {
private String key = "Key1";
...
所以当我从 parent class 调用 getChild 时,它指的是相应的 Child.
基本上我是在寻求一种方法来根据传递的参数动态引用 parent 的 child。
选项 1:我建议您使用像这样的简单工厂 class:
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();
}
}
原因如下:
- 简单且易于扩展:class 只做一件事,创建对象
- 不要使用已弃用的 newInstance()
- 从静态“工厂”方法中清除接口 Parent
选项 2:定义一个函数,给它 returns 键 class 名称并使用它来创建实例:
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");
可以使用service provider mechanism。这在积极使用 Java 的模块系统时效果最好,必要的部分甚至集成到 Java 语言中。
包含 Parent
接口的模块必须导出其包,如果在其他模块中的实现应该是可能的。然后,想要查找接口实现的模块必须有一个
uses yourpackage.Parent;
指令在其模块信息中。提供实现的模块必须有一个像
这样的指令provides yourpackage.Parent with yourpackage.ChildA, yourpackage.ChildB;
在其模块信息中。可以使用接口并在同一模块中提供实现。这也可能是包含接口声明的模块。在那种情况下,甚至可以只在单个模块中使用该机制,根本不导出接口。
编译器已经检查指定的实现 类 是 public
,具有 public
无参数构造函数,并真正实现服务接口。这些实现不需要驻留在导出的包中;接口可能是从另一个模块访问实现的唯一方法。
由于编译器会预先检查所需的不变量,因此您无需处理反射固有的问题。例如,您不需要声明或处理 InstantiationException
或 IllegalAccessException
.
一个简单的设置可以是
模块信息module ExampleApp {
uses yourpackage.Parent;
provides yourpackage.Parent with yourpackage.ChildA, yourpackage.ChildB;
}
yourpackage/Parent
package yourpackage;
public interface Parent {
}
yourpackage/TheKey
package yourpackage;
import java.lang.annotation.*;
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface TheKey {
String value();
}
yourpackage/ChildA
package yourpackage;
@TheKey("Key1")
public class ChildA implements Parent {
}
yourpackage/ChildB
package yourpackage;
@TheKey("Key2")
public class ChildB implements Parent {
}
independentpackage/UsingTheInterfaces
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 支持。您只能使用已经实例化 类 和 return 实例的 Iterator
。
避免旧 API 不必要开销的一种方法是将接口拆分为服务提供者接口和实际服务接口。提供者接口将提供元信息并充当实际服务实现的工厂。比较CharsetProvider
and Charset
or FileSystemProvider
and FileSystem
. These examples also demonstrate that the service provider mechanism is already widely used. More examples can be found in the java.base
module documentation.
这只是一个概述; API documentation of ServiceLoader
包含有关该机制的更多详细信息。