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.

2

3 Answers 3

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);
  }
}
Sign up to request clarification or add additional context in comments.

5 Comments

Is there a reason I couldn't just class StuffManager<T> implements Map<Class<T>, Function<T,Expression> then use delegation?
@Christian Bongiorno you can't have a type parameter in the manager because each entry in the map has its own, different class
@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
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
@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
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?

Comments

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

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.