【问题标题】:Consumer<T> mapped Class<T> in HashMapConsumer<T> 在 HashMap 中映射了 Class<T>
【发布时间】:2017-11-09 09:35:01
【问题描述】:

我想创建一个IdentityHashMap&lt;Class&lt;T&gt;, Consumer&lt;T&gt;&gt;。基本上,我想用一个方法来映射一个类型,说明如何处理这个类型。

我希望能够动态地说出对象 X,执行 Y。我可以做到

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

但这很糟糕,因为我必须在使用它时将对象投射到 Lamba 中。

例子:

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

我想做的是

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

但这似乎是不允许的。有没有办法做到这一点 ?使用此类型的方法映射类型的最佳解决方法是什么?

【问题讨论】:

  • 这是一个小插曲,但请注意java.lang.Class 将相等定义为身份,因此此处无需使用IdentityHashMap
  • @ruakh 也没有必要使用HashMap。比较时多打 2 个equals 会带来什么?
  • 请参阅docs.oracle.com/javase/8/docs/api/java/util/…。只有在特别需要时才应使用IdentityHashMap

标签: java dictionary generics java-8 type-safety


【解决方案1】:

这基本上就像type-safe heterogeneous container described by Joshua Bloch,只是你不能使用Class 来转换结果。

奇怪的是,我在 SO 上找不到一个很好的例子,所以这里有一个:

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

这是类型安全的,因为键和值之间的关系由 put 方法的签名强制执行。

关于 Java 泛型限制的一个令人讨厌的事情是,其中一个容器不能为泛型值类型编写,因为没有办法,例如:

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

其他说明:

  • 我会为此使用常规的HashMapLinkedHashMapHashMap 维护得更好,并且有许多 IdentityHashMap 没有的优化。

  • 如果需要使用泛型类型,比如Consumer&lt;List&lt;String&gt;&gt;,那么你需要使用类似Guava的TypeToken作为key,因为Class只能代表一个类型的擦除。

  • Guava 有一个 ClassToInstanceMap,当您需要 Map&lt;Class&lt;T&gt;, T&gt; 时。

有时人们想做这样的事情,使用从类到消费者的映射:

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

也就是说,给定任何对象,在映射中找到绑定到该对象的类的消费者,并将该对象传递给消费者的accept 方法。

不过,该示例无法编译,因为 getClass() 实际上被指定返回 Class&lt;? extends |T|&gt;,其中 |T| 表示 T擦除。 (见JLS §4.3.2。)在上面的例子中,T的擦除是Object,所以obj.getClass()返回一个普通的Class&lt;?&gt;

这个问题可以通过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));
}

另外,如果您想要修改版本的 get 以返回任何适用的消费者,您可以使用如下内容:

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

这让我们可以将一般的超类型消费者放在地图中,如下所示:

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

然后用子类型类检索:

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

这里有一个更一般的例子,这次使用UnaryOperator 并且没有有界通配符:

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,我认为没有通配符的示例可能更易于阅读。

【讨论】:

    【解决方案2】:

    可以以类型安全的方式实现这一点无需任何未经检查的强制转换。解决方案在于将Consumer&lt;T&gt; 包装成更通用的Consumer&lt;Object&gt;,该Consumer&lt;Object&gt; 进行转换,然后委托给原始消费者:

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

    根据您的需要,get() 也可以简单地返回 Consumer&lt;Object&gt;。如果您只在运行时知道类型,这将是必要的,例如

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

    我很确定我在 2016 年比利时 Devoxx 的一次演讲中看到了这个解决方案(或类似的东西),可能来自 Venkat Subramaniam,但我肯定找不到它……

    【讨论】:

    • 这确实很好,但它并不总是一个通用的解决方案。例如,如果我们有 Map&lt;Class&lt;T&gt;, List&lt;T&gt;&gt; 或其他一些我们无法轻松为其创建适配器的值,则使用此模式会更加困难。
    • @radiodef 确实如此,这将始终视情况而定。我希望有一种更简洁的方法可以在没有警告的情况下进行一些通用类型转换。
    【解决方案3】:

    我可以让IdentityHashMap 使用通常的Class&lt;?&gt;Consumer&lt;?&gt;

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

    然后我将 put 操作包装在一个方法中。此方法接受相同泛型的类型和使用者。

    public <T> void intercept(Class<T> type, Consumer<T> consumer)
    {
        interceptor.put(type, consumer);
    }
    

    这让我可以写

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

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 2021-08-09
      • 1970-01-01
      • 2016-05-26
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多