避免将未经检查的强制转换投射到Java中用于事件发布者的通用接口的集合

时间:2012-05-10 21:06:38

标签: java generics

我正在尝试为我正在构建的Android应用创建一个轻量级,线程安全的应用内发布/订阅机制。我的基本方法是跟踪每个事件类型T的IEventSubscriber<T>列表,然后通过传递类型为T的有效负载将事件发布到订阅对象。

我使用泛型方法参数(我认为)确保以类型安全的方式创建订阅。因此,我很确定当我从订阅地图中获取订阅者列表时,发布一个我可以将其转换为IEventSubscriber<T>列表的事件,然而,这会生成未选中状态投下警告。

我的问题:

  1. 未经检查的演员在这里真的安全吗?
  2. 如何实际检查订阅者列表中的项目是否实现IEventSubscriber<T>
  3. 假设(2)涉及一些讨厌的反思,你会在这做什么?
  4. 代码(Java 1.6):

    import java.util.concurrent.ConcurrentHashMap;
    import java.util.concurrent.ConcurrentMap;
    import java.util.concurrent.CopyOnWriteArraySet;
    
    public class EventManager {
      private ConcurrentMap<Class, CopyOnWriteArraySet<IEventSubscriber>> subscriptions = 
          new ConcurrentHashMap<Class, CopyOnWriteArraySet<IEventSubscriber>>();
    
      public <T> boolean subscribe(IEventSubscriber<T> subscriber,
          Class<T> eventClass) {
        CopyOnWriteArraySet<IEventSubscriber> existingSubscribers = subscriptions.
            putIfAbsent(eventClass, new CopyOnWriteArraySet<IEventSubscriber>());
        return existingSubscribers.add(subscriber);
      }
    
      public <T> boolean removeSubscription(IEventSubscriber<T> subscriber, 
          Class<T> eventClass) {
        CopyOnWriteArraySet<IEventSubscriber> existingSubscribers = 
            subscriptions.get(eventClass);
        return existingSubscribers == null || !existingSubscribers.remove(subscriber);
      }
    
      public <T> void publish(T message, Class<T> eventClass) {
        @SuppressWarnings("unchecked")
        CopyOnWriteArraySet<IEventSubscriber<T>> existingSubscribers =
            (CopyOnWriteArraySet<IEventSubscriber<T>>) subscriptions.get(eventClass);
        if (existingSubscribers != null) {
          for (IEventSubscriber<T> subscriber: existingSubscribers) {
            subscriber.trigger(message);
          }
        }
      }
    }
    

3 个答案:

答案 0 :(得分:4)

  

未经检查的演员在这里真的安全吗?

相当。您的代码不会导致堆污染,因为subcribe的签名确保您只将适当编译时类型的IEventSubscribers放入映射中。它可能会传播由其他地方不安全的未经检查的演员造成的堆污染,但你几乎无能为力。

  

如何实际检查订阅者列表中的项目是否实现IEventSubscriber?

将每个项目投放到IEventSubscriber。您的代码已在以下行中执行此操作:

for (IEventSubscriber<T> subscriber: existingSubscribers) {

如果existingSubscribers包含无法分配给IEventSubscriber的对象,则此行将抛出ClassCastException。在迭代未知类型参数列表时避免警告的标准做法是显式地转换每个项目:

List<?> list = ...
for (Object item : list) {
    IEventSubscriber<T> subscriber = (IEventSubscriber<T>) item;
}

该代码明确检查每个项目是IEventSubscriber,但无法检查它是IEventSubscriber<T>

要实际检查IEventSubscriber的类型参数,IEventSubscriber需要帮助您。这是由于擦除,特别是在声明

的情况下
class MyEventSubscriber<T> implements IEventSubscriber<T> { ... }

以下表达式将始终为真:

new MyEventSubscriber<String>.getClass() == new MyEventSubscriber<Integer>.getClass()
  

假设(2)涉及一些讨厌的反思,你会在这做什么?

我会保留代码原样。很容易推断出演员是正确的,我觉得不值得花时间把它改写成没有警告的编译。如果您确实希望重写它,可能会使用以下想法:

class SubscriberList<E> extends CopyOnWriteArrayList<E> {
    final Class<E> eventClass;

    public void trigger(Object event) {
        E event = eventClass.cast(event);
        for (IEventSubscriber<E> subscriber : this) {
            subscriber.trigger(event);
        }
    }
}

SubscriberList<?> subscribers = (SubscriberList<?>) subscriptions.get(eventClass);
subscribers.trigger(message);

答案 1 :(得分:2)

不完全是。如果 EventManager类的所有客户端始终使用泛型而且从不使用rawtypes,那将是安全的;即,如果您的客户端代码编译时没有与泛型相关的警告。

但是,客户端代码忽略这些并插入期望错误类型的IEventSubscriber并不困难:

EventManager manager = ...;
IEventSubscriber<Integer> integerSubscriber = ...; // subscriber expecting integers

// casting to a rawtype generates a warning, but will compile:
manager.subscribe((IEventSubscriber) integerSubscriber, String.class);
// the integer subscriber is now subscribed to string messages
// this will cause a ClassCastException when the integer subscriber tries to use "test" as an Integer:
manager.publish("test", String.class);

我不知道一种编译时方法来阻止这种情况,但你可以在运行时检查IEventSubscriber<T>的实例的泛型参数类型,如果泛型类型{{ 1}}在编译时绑定到类 。考虑:

T

在上面的示例中,对于public class ClassA implements IEventSubscriber<String> { ... } public class ClassB<T> implements IEventSubscriber<T> { ... } IEventSubscriber<String> a = new ClassA(); IEventSubscriber<String> b = new ClassB<String>(); ClassA在编译时绑定到参数String。对于T中的ClassAString的所有实例都将T。但是在IEventSubscriber<T>中,ClassB在运行时绑定到StringT的实例可以包含ClassB的任何值。如果T的实现在编译时绑定参数IEventSubscriber<T>和上面的T一样,那么您可以通过以下方式在运行时获取该类型:

ClassA

这将导致在订阅者注册public <T> boolean subscribe(IEventSubscriber<T> subscriber, Class<T> eventClass) { Class<? extends IEventSubscriber<T>> subscriberClass = subscriber.getClass(); // get generic interfaces implemented by subscriber class for (Type type: subscriberClass.getGenericInterfaces()) { ParameterizedType ptype = (ParameterizedType) type; // is this interface IEventSubscriber? if (IEventSubscriber.class.equals(ptype.getRawType())) { // make sure T matches eventClass if (!ptype.getActualTypeArguments()[0].equals(eventClass)) { throw new ClassCastException("subscriber class does not match eventClass parameter"); } } } CopyOnWriteArraySet<IEventSubscriber> existingSubscribers = subscriptions.putIfAbsent(eventClass, new CopyOnWriteArraySet<IEventSubscriber>()); return existingSubscribers.add(subscriber); } 时检查类型,从而允许您更轻松地跟踪错误代码,而不是在发布事件时稍后检查类型。但是,它确实会进行一些反射,并且只能在编译时绑定EventManager时检查类型。如果您可以信任将订阅者传递给EventManager的代码,那么我只需保留代码,因为它更简单。但是,如上所述使用反射检查类型将使您成为 little 更安全的IMO。

另外请注意,您可能希望重构初始化T的方式,因为CopyOnWriteArraySet方法当前正在每次调用时创建一个新集,无论是否需要。试试这个:

subscribe

这可以避免在每个方法调用上创建一个新的CopyOnWriteArraySet<IEventSubscriber> existingSubscribers = subscriptions.get(eventClass); if (existingSubscribers == null) { existingSubscribers = subscriptions.putIfAbsent(eventClass, new CopyOnWriteArraySet<IEventSubscriber>()); } ,但是如果你有一个竞争条件并且两个线程试图一次放入一个集合,CopyOnWriteArraySet仍将返回创建的第一个集合第二个线程,所以没有覆盖它的危险。

答案 2 :(得分:1)

由于您的subscribe实施确保Class<?>中的每个ConcurrentMap键都映射到正确的IEventSubscriber<?>,因此从@SuppressWarnings("unchecked")检索时可以安全使用publish {{1}}中的地图。

只需确保正确记录警告被抑制的原因,以便任何未来对该类进行更改的开发人员都知道正在发生的事情。

另见这些相关帖子:

Generic Map of Generic key/values with related types

Java map with values limited by key's type parameter