Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Consumer<T> mapped Class<T> in HashMap

I want to create an IdentityHashMap<Class<T>, Consumer<T>>. Basically, I want to map a type with a method saying what to do with this type.

I want to dynamically be able to say with objects X, execute Y. I can do

private IdentityHashMap<Class<?>, Consumer<?>> interceptor = new IdentityHashMap<>();

but it sucks because then I have to cast the object in the lamba when using it.

Example:

interceptor.put(Train.class, train -> {
    System.out.println(((Train)train).getSpeed());
});

What I would like to do is

private <T> IdentityHashMap<Class<T>, Consumer<T>> interceptor = new IdentityHashMap<>();

But it doesn't seem to be allowed. Is there a way to do this ? What is the best workaround to map types with a method for this type ?

like image 215
Winter Avatar asked Jun 07 '17 21:06

Winter


People also ask

What is Map <> in Java?

A Map is an object that maps keys to values. A map cannot contain duplicate keys: Each key can map to at most one value. It models the mathematical function abstraction.

How do you check if a HashMap contains a key Java?

util. HashMap. containsKey() method is used to check whether a particular key is being mapped into the HashMap or not. It takes the key element as a parameter and returns True if that element is mapped in the map.

What is the load factor in HashMap?

The meaning of operational complexity of O(1) means the retrieval and insertion operations take constant time. The default load factor of a HashMap is 0.75f.

How elements are stored in HashMap?

HashMap stores entries into multiple singly linked lists, called buckets or bins. Default number of bins is 16 and it's always power of 2. HashMap uses hashCode() and equals() methods on keys for get and put operations. So HashMap key object should provide good implementation of these methods.


2 Answers

This is essentially just like the type-safe heterogeneous container described by Joshua Bloch, except you can't use the Class to cast the result.

Weirdly, I can't find a great example existing on SO, so here is one:

package mcve;
import java.util.*;
import java.util.function.*;

class ClassToConsumerMap {
    private final Map<Class<?>, Consumer<?>> map =
        new HashMap<>();

    @SuppressWarnings("unchecked")
    public <T> Consumer<? super T> put(Class<T> key, Consumer<? super T> c) {
        return (Consumer<? super T>) map.put(key, c);
    }

    @SuppressWarnings("unchecked")
    public <T> Consumer<? super T> get(Class<T> key) {
        return (Consumer<? super T>) map.get(key);
    }
}

That's type-safe, because the relation between keys and values is enforced by the signature of the put method.

One annoying thing about the limitations of Java's generics is that one of these containers can't be written for a generic value type, because there's no way to do e.g.:

class ClassToGenericValueMap<V> {
    ...
    public <T> V<T> put(Class<T> key, V<T> val) {...}
    public <T> V<T> get(Class<T> key) {...}
}

Other notes:

  • I would use a regular HashMap or a LinkedHashMap for this. HashMap is better maintained and has many optimizations that IdentityHashMap doesn't have.

  • If it's necessary to use generic types, like Consumer<List<String>>, then you need to use something like Guava TypeToken as the key, because Class can only represent the erasure of a type.

  • Guava has a ClassToInstanceMap for when you need a Map<Class<T>, T>.

Sometimes people want to do something like this, with a class-to-consumer map:

public <T> void accept(T obj) {
   Consumer<? super T> c = get(obj.getClass());
   if (c != null)
       c.accept(obj);
}

That is, given any object, find the consumer in the map bound to that object's class and pass the object to the consumer's accept method.

That example won't compile, though, because getClass() is actually specified to return a Class<? extends |T|>, where |T| means the erasure of T. (See JLS §4.3.2.) In the above example, the erasure of T is Object, so obj.getClass() returns a plain Class<?>.

This issue can be solved with a capturing helper method:

public void accept(Object obj) {
    accept(obj.getClass(), obj);
}
private <T> void accept(Class<T> key, Object obj) {
    Consumer<? super T> c = get(key);
    if (c != null)
        c.accept(key.cast(obj));
}

Also, if you want a modified version of get which returns any applicable consumer, you could use something like this:

public <T> Consumer<? super T> findApplicable(Class<T> key) {
    Consumer<? super T> c = get(key);
    if (c == null) {
        for (Map.Entry<Class<?>, Consumer<?>> e : map.entrySet()) {
            if (e.getKey().isAssignableFrom(key)) {
                @SuppressWarnings("unchecked")
                Consumer<? super T> value =
                    (Consumer<? super T>) e.getValue();
                c = value;
                break;
            }
        }
    }
    return c;
}

That lets us put general supertype consumers in the map, like this:

ctcm.put(Object.class, System.out::println);

And then retrieve with a subtype class:

Consumer<? super String> c = ctcm.findApplicable(String.class);
c.accept("hello world");

Here's a slightly more general example, this time using UnaryOperator and no bounded wildcards:

package mcve;
import java.util.*;
import java.util.function.*;

public class ClassToUnaryOpMap {
    private final Map<Class<?>, UnaryOperator<?>> map =
        new HashMap<>();

    @SuppressWarnings("unchecked")
    public <T> UnaryOperator<T> put(Class<T> key, UnaryOperator<T> op) {
        return (UnaryOperator<T>) map.put(key, op);
    }

    @SuppressWarnings("unchecked")
    public <T> UnaryOperator<T> get(Class<T> key) {
        return (UnaryOperator<T>) map.get(key);
    }
}

The ? super bounded wildcard in the first example is specific to consumers, and I thought an example without wildcards might be easier to read.

like image 80
Radiodef Avatar answered Sep 23 '22 22:09

Radiodef


It is possible to implement this in a type-safe manner without any unchecked cast. The solution resides in wrapping the Consumer<T> into a more general Consumer<Object> that casts and then delegates to the original consumer:

public class ClassToConsumerMap {
    private final Map<Class<?>, Consumer<Object>> map = new IdentityHashMap<>();

    public <T> Consumer<? super T> put(Class<T> key, Consumer<? super T> c) {
        return map.put(key, o -> c.accept(key.cast(o)));
    }

    public <T> Consumer<? super T> get(Class<T> key) {
        return map.get(key);
    }
}

Depending on your needs, get() could also simply return a Consumer<Object>. This would be necessary if you only know the type at runtime, e.g.

classToConsumerMap.get(someObject.getClass()).accept(someObject);

I am pretty sure I saw this solution (or something similar) in a talk @ Devoxx Belgium 2016, possibly from Venkat Subramaniam, but I definitively cannot find it back…

like image 25
Didier L Avatar answered Sep 23 '22 22:09

Didier L