Java Factory using Generics, .class vs. .getClass()



我搜索并尝试了一天多,但找不到解决我在 Java 中遇到的常见问题的解决方案。原因很明显 - 键入擦除。但我的问题是:在Java中真的没有解决这个问题的好解决方案吗?我愿意调查更多的时间,因为这种问题每隔一段时间就会出现。

我得到的错误是:

类型 IStrategy<capture#2-of>> 中的方法 doStrategy(capture#2-of ? extend I) 不适用于参数 (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;
    }
}

我尝试了另一种使用番石榴类型令牌(https://github.com/google/guava/wiki/ReflectionExplained)的方法。但是我没有设法让它工作,因为我真的没有 因为我得到的只是实现该接口的实例集合。

虽然使用访问者模式,但我有一个有效且还不错的解决方案。因为我有真正的课程

...visitor class
public void visit(A a){
   doVisit(A.class, a); //private generic method now works of course
}

编译时一切正常。但是在这种特殊情况下,我花了相当长的时间来实现该访问者的 30 多个子类。所以我真的很想为未来有一个更好的解决方案。

任何意见都非常感谢。

该问题与擦除无关。而是Java的类型系统不够强大,无法在没有帮助的情况下静态地推理currentcurrent.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本身。除此之外,通配符类型(带 ? 的类型)旨在丢失更多信息,因此类型系统甚至不知道我们的策略接受相同的类型作为 getClass() 的结果。

因此,为了使程序进行类型检查,我们需要(a)为类型系统提供一些非通配符名称,用于current的特定子类型I,并且(b)说服它current的值实际上具有该类型。好消息是,我们可以两者兼而有之。

要为通配符类型命名,我们

可以使用一种称为"通配符捕获"的技术,其中我们创建一个私有帮助程序函数,其唯一工作是为本来是通配符的类型提供特定类型变量名称。我们将测试循环的主体拉出到它自己的函数中,该函数关键地接受一个类型参数:

private <T> void genericTestHelperDraft1(Class<T> currentClass, T current) {
  StrategyFactory factory = new StrategyFactory();
  IStrategy<T> createStrategy = factory.createStategy(t);
  createStrategy.doStrategy(current); // works
}

这个函数的工作是有效地引入类型参数T ,这让Java知道我们打算在任何地方引用相同的未知类型,T使用它。有了这些信息,它就可以理解我们从工厂获得的策略适用于我们的输入类具有的相同类型,即当前current在类型签名中具有的相同类型。

不幸的是,当我们调用此方法时,我们仍然会收到编译错误:

for (I current : instances){
  genericTestHelperDraft1(current.getClass(), current);
  // Type error because current is not of type "capture of ? extends I"
}

这里的问题是类型系统不知道current有自己的类型!Java的类型系统不理解currentcurrent.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);
}

现在我们可以将测试中的循环更改为:

for (I current : instances){
  genericTestHelperDraft2(current.getClass(), current);
}

一切正常。

型是一种编译时功能,您只能使用编译器可以确定为安全的功能。

请注意,在所有情况下都不会擦除该类型。 例如,您可以获取AStrategyBStrategy的类型,因为这些是非动态的具体类型。

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 类型,前提是在编译类时已知。

在Peter的回答和更多的研究之后,我很确定如果不以某种方式增强模型,这在Java中是不可能的。我决定继续使用 Visitor 模式,因为在我看来,编译时检查值得额外的代码。

所以这是我最后实现的(我也相信你们都知道访问者模式 - 只是为了完整)。

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);
        }
    }

希望这可能会对其他人有所帮助。

问候雷纳

最新更新