5

How can I use Java8 Supplier interface to rewrite this factory method to provide the proper typed instance?

I've a simple interface that extends Map:

public interface Thingy<K, V> extends Map<K, V> {}

Then I have a ThingyFactory class, which contains a list of all of the implementation classnames of Thingy:

public final class ThingyFactory {
    Map<String, Class<Thingy<?, ?>>> thingyclasses = new ConcurrentHashMap<>();
    .....

    @SuppressWarnings("unchecked")
    public <K, V> Thingy<K, V> getInstance(String classname) throws ThingyException {
        Thingy<K, V> thingy;
        try {
            thingy = (Thingy<K, V>) thingyclasses.get(classname).newInstance();
        } catch (InstantiationException | IllegalAccessException e) {
            throw new ThingyException("Something bad happened: ", e.toString());
        }
        return thingy;
    }
}

I'm pretty sure that I can do this elegantly and w/o the SuppressWarnings and classloader using the Supplier interface but I can't seem to get the pattern quite right. Any guidance appreciated!

Richard Sand
  • 642
  • 6
  • 20

2 Answers2

1

Because you're using wildcards as the generic type of Thingy within thingyclasses, you're essentially saying: The types of Thingy can be anything; however, this prohibits the compiler from inferring anything about the type, hence why you require an explicit cast. You can use Supplier to improve it slightly, but you'll still have the warning regarding an unchecked cast:

class ThingyFactory {
    private Map<String, Supplier<Thingy<?, ?>>> providers = new ConcurrentHashMap<>();

    @SuppressWarnings("unchecked")
    public <K, V> Supplier<Thingy<K, V>> getInstance(String classname) {
        return () -> (Thingy<K, V>) providers.get(classname);
    }
}

If you want it to be type-safe, you'll need to redesign it entirely and only use wildcards if the compiler is able to infer the type(s).

Jacob G.
  • 28,856
  • 5
  • 62
  • 116
  • 1
    Wildcards have nothing to do with type inference. – shmosel May 02 '18 at 04:01
  • Hi Jacob - let me try another approach - forget the "providers" map completely - if I simply have the String classname, is there a way I can create a new instance of classname Thingy? – Richard Sand May 02 '18 at 21:20
  • @RichardSand Without an explicit cast to `Thingy`, I doubt it, as your method isn't type-safe. – Jacob G. May 02 '18 at 21:23
0

Interesting challenge. I think this is what you're looking for:

public class Main {

    public static void main(String[] args) {
        ThingyFactory<String, String> factory = new ThingyFactory<>();
        Thingy<String, String> instance = factory.getInstance("ThingyImpl");
        System.out.println(instance.clazzName());
        Thingy<String, String> otherInstance = factory.getInstance(ThingyImpl.class);
        System.out.println(instance.clazzName());
    }
}

interface Thingy<K, V> extends Map<K, V> {
    //added this method for testing purpuses
    String clazzName();
}

//extending HashMap so I don't have to implement Map's methods
class ThingyImpl<K, V> extends HashMap<K, V> implements Thingy<K, V> {
    public String clazzName() {
        return "ThingyImpl";
    }
}

final class ThingyFactory<K, V> {
    private Map<String, Supplier<Thingy<K, V>>> providers = new ConcurrentHashMap<>();

    public ThingyFactory() {
        providers.put("ThingyImpl", () -> new ThingyImpl());
    }

    public Thingy<K, V> getInstance(String classname) {
        return providers.get(classname).get();
    }

    // alternative with Class. 
    // You could change providers to a Map<Class<? extends Thingy>, Supplier<Thingy<K, V>>>
    public Thingy<K, V> getInstance(Class<? extends Thingy> clazz) {
        return providers.get(clazz.getName()).get();
    }
}

Of course, you will need a ThingyFactory for every specific K, V type pair, but this is type safe.

SrThompson
  • 5,568
  • 2
  • 17
  • 25