3

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

我使用泛型方法参数(我认为)确保以类型安全的方式创建订阅。因此,我很确定,当我从订阅映射中获取订阅者列表时,需要发布一个我可以将其转换为 列表的事件IEventSubscriber<T>,但是,这会生成未经检查的转换警告。

我的问题:

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

代码(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);
      }
    }
  }
}
4

3 回答 3

5

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

相当。您的代码不会造成堆污染,因为订阅的签名确保您只将正确编译时类型的 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>

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

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);
于 2012-05-10T21:40:47.060 回答
2

不完全是。如果类的所有客户端EventManager总是使用泛型而不是原始类型,那将是安全的;即,如果您的客户端代码在没有泛型相关警告的情况下编译。

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>在运行时检查实例的泛型参数类型。考虑: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>();

在上面的示例中, for ClassA,在编译时String绑定到参数。T的所有实例都ClassAString具有Tin IEventSubscriber<T>。但是在ClassB,在运行时String是必然的。T的实例ClassB可能对 有任何价值T。如果您在编译时IEventSubscriber<T>绑定参数的实现与上面一样,那么您可以通过以下方式在运行时获取该类型:TClassA

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,使您可以更轻松地跟踪错误代码,而不是在发布事件时才检查类型。但是,它确实做了一些 hokey 反射,并且只能检查类型是否T在编译时绑定。如果您可以信任将订阅者传递给 EventManager 的代码,我会保持原样,因为它更简单。但是,如上所述使用反射检查类型将使您在IMO 中更安全一些。

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

CopyOnWriteArraySet<IEventSubscriber> existingSubscribers = subscriptions.get(eventClass);
if (existingSubscribers == null) {
    existingSubscribers = subscriptions.putIfAbsent(eventClass, new CopyOnWriteArraySet<IEventSubscriber>());
}

这避免了CopyOnWriteArraySet在每个方法调用上创建一个新的,但是如果你有一个竞争条件并且两个线程尝试一次放入一个集合,putIfAbsent仍然会将创建的第一个集合返回给第二个线程,因此没有覆盖它的危险。

于 2012-05-10T22:12:25.717 回答
1

由于您的subscribe实现确保映射中的每个Class<?>键都ConcurrentMap正确,因此在从映射中检索时IEventSubscriber<?>使用它是安全的。@SuppressWarnings("unchecked")publish

只需确保正确记录警告被禁止的原因,以便将来对类进行更改的任何开发人员都知道发生了什么。

另请参阅这些相关帖子:

具有相关类型的通用键/值的通用映射

值受键的类型参数限制的 Java 映射

于 2012-05-10T21:32:57.047 回答