我现在搜索并尝试了一天以上,无法找到解决Java中常见问题的解决方案。原因很明显 - 类型擦除。但我的问题是:在Java中这个问题真的没有好的解决方案吗?我愿意调查更多的时间,因为这种问题每次都会弹出一次。
我得到的错误是:
方法doStrategy(捕获#2-of?extends I)类型为IStrategy< capture#2-of?扩展我>不适用于参数(I)
所以我将问题简化为以下示例。
想象一下这个模型:
package model;
public interface I {
//there are actual 30 classes implementing I...
}
public class A implements I {
public void someSpecificMagicForA(){
System.out.println("A");
}
}
public class B implements I {
public void someSpecificMagicForB() {
System.out.println("B");
}
}
和选择逻辑
package strategy;
import model.A;
public interface IStrategy<T> {
public void doStrategy(T t);
}
public class AStrategy implements IStrategy<A> {
@Override
public void doStrategy(A a) {
a.someSpecificMagicForA();
}
}
public class BStrategy implements IStrategy<B> {
@Override
public void doStrategy(B b) {
b.someSpecificMagicForB();
}
}
和通用战略工厂
package strategy;
import java.util.HashMap;
import java.util.Map;
import model.A;
import model.B;
public class StrategyFactory {
static {
strategies.put(A.class, AStrategy.class);
strategies.put(B.class, BStrategy.class);
}
private static final Map<Class<?>, Class<? extends IStrategy<?>>> strategies = new HashMap<>();
@SuppressWarnings("unchecked") // I am fine with that suppress warning
public <T> IStrategy<T> createStategy(Class<T> clazz){
Class<? extends IStrategy<?>> strategyClass = strategies.get(clazz);
assert(strategyClass != null);
try {
return (IStrategy<T>) strategyClass.newInstance();
} catch (InstantiationException | IllegalAccessException e) {
e.printStackTrace();
return null;
}
}
}
这是测试
import java.util.ArrayList;
import java.util.List;
import junit.framework.TestCase;
import model.A;
import model.B;
import model.I;
import strategy.IStrategy;
import strategy.StrategyFactory;
public class TestCases extends TestCase {
public void testWithConcreteType(){
B b = new B();
StrategyFactory factory = new StrategyFactory();
IStrategy<B> createStategy = factory.createStategy(B.class);
createStategy.doStrategy(b); //awesome
}
public void testWithGenericType(){
List<I> instances = createTestData(); // image this is the business data
StrategyFactory factory = new StrategyFactory();
for (I current : instances){
IStrategy<? extends I> createStategy = factory.createStategy(current.getClass());
createStategy.doStrategy(current); //meh
//The method doStrategy(capture#2-of ? extends I) in the type IStrategy<capture#2-of ? extends I>
//is not applicable for the arguments (I)
}
}
private List<I> createTestData(){
A a = new A();
B b = new B();
List<I> instances = new ArrayList<>();
instances.add(a);
instances.add(b);
return instances;
}
}
我尝试过使用番石榴TypeTokens(https://github.com/google/guava/wiki/ReflectionExplained)的另一种方法。但我没有设法让这个工作,因为我真的没有&lt; T&gt;因为我得到的只是实现该接口的实例集合。
虽然使用访问者模式,但我有一个有效但不太糟糕的解决方案。因为我有真正的课程
...visitor class
public void visit(A a){
doVisit(A.class, a); //private generic method now works of course
}
在编译时一切都很好。但是在这个特殊情况下我花了相当长的时间来实现这个访问者超过30个子类的I。所以我真的想为未来找到更好的解决方案。
非常感谢任何评论。
答案 0 :(得分:2)
问题与擦除无关。 Java的类型系统不够强大,无法在没有帮助的情况下静态推理current
和current.getClass()
之间的关系。
在您的代码中:
for (I current : instances){
IStrategy<? extends I> createStategy = factory.createStategy(current.getClass());
createStategy.doStrategy(current);
}
current.getClass()
的结果是Class<? extends I>
类型的对象;也就是说,我们不知道静态知道的I
的某个子类型。我们程序员知道无论它是什么类型,这也是current
的具体类型,因为我们已经阅读了getClass
的文档,但类型系统并不知道。因此,当我们得到IStrategy<? extends I>
时,我们所知道的是,这是一种适用于I
的某些子类型的策略,不一定是I
本身。除此之外,通配符类型(具有?
的类型)被设计为丢失更多信息,因此类型系统甚至不知道我们的策略接受与{{1的结果相同的类型}}
因此,为了使程序类型检查,我们需要(a)为类型系统提供getClass()
I
的特定子类型的非通配符名称,并且(b)说服它current
实际的值该类型。好消息是,我们可以做到这两点。
为了给通配符类型命名,我们可以使用一种名为“通配符捕获”的技术,我们在其中创建一个私有帮助函数,其唯一的工作是将特定类型的变量名称赋给一个本来是通配符的类型。我们将测试循环的主体拉出到它自己的函数中,该函数严格地采用类型参数:
current
此函数的作用是有效地引入类型参数private <T> void genericTestHelperDraft1(Class<T> currentClass, T current) {
StrategyFactory factory = new StrategyFactory();
IStrategy<T> createStrategy = factory.createStategy(t);
createStrategy.doStrategy(current); // works
}
,这使得Java知道我们打算在我们使用的任何地方引用相同的未知类型T
它。有了这些信息,它可以理解我们从工厂获得的策略与我们的输入类具有的类型相同,这与类型签名中的当前T
类型相同。
不幸的是,当我们去调用这个方法时,我们仍然会遇到编译错误:
current
这里的问题是类型系统不知道for (I current : instances){
genericTestHelperDraft1(current.getClass(), current);
// Type error because current is not of type "capture of ? extends I"
}
有自己的类型! Java的类型系统不理解current
和current
之间的关系,因此它不知道current.getClass()
返回的类型,我们可以将current.getClass()
视为值{那种类型。幸运的是,我们可以通过简单的向下转换来解决这个问题,因为我们(程序员)做知道current
有自己的类型。我们必须在我们的帮助器中执行此操作,因为在帮助程序之外,我们没有任何名称用于我们要声明current
的子类型。我们可以像这样更改代码:
current
现在我们可以将测试中的循环更改为:
private <T> void genericTestHelperDraft2(Class<T> t, Object current) {
T currentDowncast = t.cast(current);
StrategyFactory factory = new StrategyFactory();
IStrategy<T> createStrategy = factory.createStategy(t);
createStrategy.doStrategy(currentDowncast);
}
一切正常。
答案 1 :(得分:1)
Generic是一个编译时功能,您只能使用编译器可以确定为安全的内容。
请注意,在所有情况下都不会删除该类型。例如,您可以获得AStrategy
和BStrategy
的类型,因为这些是非动态的具体类型。
AStrategy as = new AStrategy();
for(AnnotatedType asc : as.getClass().getAnnotatedInterfaces()) {
Type type = asc.getType();
System.out.println(type);
if (type instanceof ParameterizedType) {
ParameterizedType pt = (ParameterizedType) type;
for (Type t : pt.getActualTypeArguments()){
System.out.println(t); // class A
}
}
}
打印
IStrategy<A>
class A
是否所有这些代码都使您的解决方案更简单,我不知道但您可以获得正在实施的IStrategy类型,前提是它在编译类时已知。
答案 2 :(得分:1)
在彼得的回答和更多的研究之后,我很确定如果不以这种或那种方式增强模型,这在Java中是不可能的。我决定继续使用访问者模式,因为我认为编译时检查值得额外的代码。
所以这就是我最终实现的内容(我也相信你们都知道访客模式 - 只是为了完整)。
public interface I {
public void acceptVisitor(IVisitor visitor);
//there are actual 30 classes implementing I...
}
public interface IVisitor {
public void visit(A a);
public void visit(B b);
}
public void testWithGenericType(){
List<I> instances = createTestData(); // image this is the business data
StrategyFactory factory = new StrategyFactory();
Visitor visitor = new Visitor(factory);
for (I current : instances){
current.acceptVisitor(visitor);
}
}
class Visitor implements IVisitor {
private final StrategyFactory factory;
public Visitor(StrategyFactory factory) {
this.factory = factory;
}
private <T> void doVisit(Class<T> clazz, T t){
IStrategy<T> createStategy = factory.createStategy(clazz);
createStategy.doStrategy(t);
}
@Override
public void visit(A a) {
doVisit(A.class, a);
}
@Override
public void visit(B b) {
doVisit(B.class, b);
}
}
希望这可能有助于其他人。
此致 赖