7

Not sure if this is even possible; I admit I am not as good at generics as I would like to be.

Basically, I want to create a map of class -> function where the class used for the key is the class of the input to the function, like such (not legal syntax):

public static Map<Class<T>,Function<T,Expression>> STUFF = new HashMap<>();

{
    STUFF.put(List.class, ListExpression::new);
    STUFF.put(String.class, StringExpression::new);// this constructor must take string
}

so that if I do:

Function<String,Expression> e = STUFF.get(o.getClass());
Expression ex = e.apply(o);

It gets the types right for me.

Radiodef
  • 37,180
  • 14
  • 90
  • 125
Christian Bongiorno
  • 5,150
  • 3
  • 38
  • 76

3 Answers3

8

It can't be done. Every time you want Map<Class<T>, SomeType<T>>, ie associating a class with a parameterized type somehow related to the class in the key, it can't be done. This is because the key type, Class<T> must be shared among all entries as per the Map<K, V> definition.

What remains is the practical alternative to have a Map<Class<?>, SomeType<?>>, encapsulate this map in a private field and check constraints when putting items in the map. Something like

public class StuffManager {

  private final Map<Class<?>, Consumer<?>> stuff = new HashMap<>();

  public <T> void register(Class<T> key, Consumer<? super T> val) {
    stuff.put(key, val);
  }
}
Raffaele
  • 20,627
  • 6
  • 47
  • 86
  • Is there a reason I couldn't just class StuffManager implements Map, Function then use delegation? – Christian Bongiorno May 06 '15 at 23:51
  • @Christian Bongiorno you can't have a type parameter in the manager because each entry in the map has its own, different class – Raffaele May 07 '15 at 03:38
  • @Radiodef I don't think so. When you get your expression via Object.class, that expression evaluates to String as per your example, which is Object-compatible, hence no ClassCastException is thrown. By the way, PECS is just stupid: it was invented solely to explain collections to people who couldn't understand generics, but there are no collections here (map is irrelevant). I think "super" may make sense too, but it all depends on the actual types of the OP program – Raffaele May 07 '15 at 04:04
  • If T is the type of the input the code needs to be fixed. I was not particularly interested in it since it does not change the matter of the question (you may notice I used a fictionary Function type with just one type parameter, not the Java 8 one), and don't know where your StringExpression comes from - but the name implies it builds a String somehow. PECS is not a technical argument: you can't say "this is wrong because PECS" . This is wrong because OP explicitly stated it was input while I assumed output in my answer and that was misleading. Thanks for pointing it out – Raffaele May 07 '15 at 05:28
  • @Radiodef btw, I obviously did **not** mean "PECS stupid = you stupid". I dislike approaches like PECS because they are non technical and encourage a "don't tell me why, just show me how" development – Raffaele May 07 '15 at 06:03
3

If you use something like Guava's TypeToken then you can do this in a type-safe way but still unchecked.

class ExpressionMap {
    private final Map<TypeToken<?>, Function<?, Expression>> m =
        new HashMap<>();

    <T> void put(TypeToken<T> type, Function<T, Expression> f) {
        m.put(type, f);
    }

    <T> Function<T, Expression> get(TypeToken<T> type) {
        @SuppressWarnings("unchecked")
        final Function<T, Expression> f =
            (Function<T, Expression>) m.get(type);
        return f;
    }
}

static ExpressionMap stuff = ExpressionMap();
static {
    stuff.put(new TypeToken<List>() {}, ListExpression::new);
    stuff.put(new TypeToken<String>() {}, StringExpression::new);
}

You can use a Class instead of TypeToken but the problem is that it breaks down with generic types.

If you had a

ListStringExpression extends Expression {
    ListStringExpression(List<String> l) {}
}

you can't have a Class<List<String>> so all ListOfSomeTypeExpression::new get lumped together as Function<List, Expression>. It's not safe.

You could do this:

ExpressionMap em = new ExpressionMap();
em.put(List.class, ListStringExpression::new);
// causing heap pollution
// subtly passing List<Integer> to List<String>
Expression ex =
    em.get(List.class).apply(Arrays.asList(1, 2, 3));

So it's possible but beware the caveats.


Also see What is a raw type and why shouldn't we use it?

Community
  • 1
  • 1
Radiodef
  • 37,180
  • 14
  • 90
  • 125
1

No, you can't do this with the default Map interface.

But you can of course 'hide' the map behind a facade - a custom interface or class that provides specific methods to the user:

public <T> Function<T, Expression> getFunction(Class<T> key) {
    // implement using generic Map<Class<?>, Function<?, Expression>
}

or even:

public Expression apply(Object param);
isnot2bad
  • 24,105
  • 2
  • 29
  • 50