【问题标题】:Avoiding an unchecked cast for cast to a collection of a generic interface in Java for an event publisher避免未经检查的强制强制强制强制强制强制转换为事件发布者的 Java 中的通用接口集合
【发布时间】:2012-05-10 21:06:38
【问题描述】:

我正在尝试为我正在构建的 Android 应用创建一个轻量级、线程安全的应用内发布/订阅机制。我的基本方法是跟踪每个事件类型 T 的 IEventSubscriber<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);
      }
    }
  }
}

【问题讨论】:

  • Java 同时使用编译时和运行时类型检查。对于泛型,它在很大程度上依赖于编译时检查,以确保您不会分配错误类型的泛型实例等。但是如果您碰巧这样做,运行时最终仍会检测到错误,尽管可能直到您re 实际上是在尝试使用泛型实例。

标签: java generics


【解决方案1】:

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

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

要实际检查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 很好,全面的答案 - 您还指出了堆污染传播到 subscribe 的可能性,我没有提到。
【解决方案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&lt;T&gt; 实例的泛型参数类型如果泛型类型 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>();

在上面的示例中,对于ClassAString 在编译时绑定到参数TClassA 的所有实例都将具有 String 用于 IEventSubscriber&lt;T&gt; 中的 T。但在ClassB 中,String 在运行时绑定到TClassB 的实例可以具有 T 的任何值。如果您的IEventSubscriber&lt;T&gt; 实现在编译时绑定参数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 时检查类型,让您更轻松地追踪错误代码,而不是在发布事件时才检查类型。但是,它确实做了一些 hokey 反射,并且只能在编译时绑定 T 时检查类型。如果您可以信任将订阅者传递给 EventManager 的代码,我会保持原样,因为它更简单。但是,如上所述使用反射检查类型将使您的 IMO 更安全一点

另外一点,您可能需要重构初始化CopyOnWriteArraySets 的方式,因为subscribe 方法当前在每次调用时都会创建一个新集合,无论是否需要。试试这个:

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

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

【讨论】:

    【解决方案3】:

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

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

    另请参阅这些相关帖子:

    Generic Map of Generic key/values with related types

    Java map with values limited by key's type parameter

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 2014-01-16
      • 1970-01-01
      • 2013-04-17
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多