简体   繁体   中英

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

I searched and tried now for more than a day and could not find a solution for a common problem I have in Java. The reason is obvious - Type erasure. But the question I have is: Is there really no good solution for this problem in Java? I am willing to investigate even more time since this kind of problem pops up every once in a time.

The error I get is:

The method doStrategy(capture#2-of ? extends I) in the type IStrategy<capture#2-of ? extends I> is not applicable for the arguments (I)

So I simplified the problem to the following example.

Imagine the model:

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

}

and the selection logic

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

and a generic strategy factory

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

And here is the test

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

I have tried another approach using guava TypeTokens ( https://github.com/google/guava/wiki/ReflectionExplained ). But I did not manage to get this working since I really have no <T> since all I get is that Collection of instances implementing that interface.

I have a working and not so bad solution though using the Visitor-pattern. Since I there have the real classes

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

everything is fine again at compile time. But in this particular case it took me quite some time to implement that visitor for more than 30 sub-classes of I. So I would really like to have a better solution for the future.

Any comments are much appreciated.

The problem has nothing to do with erasure. It is that Java's type system is not strong enough to reason statically about the relationship between current and current.getClass() without some help.

In your code:

for (I current : instances){
  IStrategy<? extends I> createStategy = factory.createStategy(current.getClass());
  createStategy.doStrategy(current);
}

the result of current.getClass() is an object of type Class<? extends I> Class<? extends I> ; that is, some subtype of I that we don't know statically. We the programmers know that whatever type it is, that's also the concrete type of current because we've read the documentation for getClass , but the type system doesn't know that. So when we get an IStrategy<? extends I> IStrategy<? extends I> , all we know is that it's a strategy that works on some subtype of I , not necessarily I itself. Add to that the fact that wildcard types (the types with ? ) are designed to lose even more information, and so the type system doesn't even know that our strategy accepts the same type as the result of getClass() .

So to make the program typecheck, we need to (a) give the type system some non-wildcard name for the specific subtype of I that current is, and (b) convince it that the value of current actually has that type. The good news is, we can do both.

To give a name to a wildcard type, we can use a technique called "wildcard capture", where we create a private helper function whose only job is to give a specific type variable name to a type that would otherwise be a wildcard. We'll pull out the body of the test loop into its own function that critically takes a type parameter:

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

The job of this function is effectively to introduce the type parameter T , which lets Java know that we intend to refer to the same unknown type T everywhere we use it. With that information it can understand that the strategy we get from our factory works on the same type that our input class has, which is the same type current current has in the type signature.

Unfortunately, when we go to call this method we'll still get a compile error:

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

The problem here is that the type system doesn't know that current has its own type! Java's type system doesn't understand the relationship between current and current.getClass() , so it doesn't know that whatever type current.getClass() returns, we can treat current as a value of that type. Fortunately we can fix that with a simple downcast, since we (the programmers) do know that current has its own type. We have to do this within our helper, since outside the helper we don't have any name for the subtype we want to assert that current has. We can change the code like so:

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

Now we can change the loop within the test to:

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

and everything works.

Generic is a compile time feature and you can only work with what the compiler can determine as safe.

Note the type is not erased in all cases. You can get the type of AStrategy and BStrategy for example as these are concrete types which are not dynamic.

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

prints

IStrategy<A>
class A

whether all this code makes your solution simpler, I don't know but you can get the type of IStrategy which is being implemented provided it is known when the class was compiled.

After Peter´s answer and some more research I am pretty sure this is not possible in Java without enhancing the model in one way or another. I decided to stay with the Visitor-pattern since the compile time checks are worth the extra code in my opinion.

So here is what I implemented in the end (also I´m sure you all know the Visitor-pattern - just to be complete).

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

Hope this maybe helps someone else.

Regards, Rainer

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM