簡體   English   中英

避免將未經檢查的強制轉換投射到Java中用於事件發布者的通用接口的集合

[英]Avoiding an unchecked cast for cast to a collection of a generic interface in Java for an event publisher

我正在嘗試為我正在構建的Android應用程序創建一個輕量級,線程安全的應用內發布/訂閱機制。 我的基本方法是跟蹤每個事件類型T的IEventSubscriber<T>列表,然后通過傳遞類型T的有效負載將事件發布到訂閱對象。

我使用泛型方法參數(我認為)確保以類型安全的方式創建訂閱。 因此,我很確定當我從訂閱地圖中獲取訂閱者列表時,發布一個我可以將其轉換為IEventSubscriber<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);
      }
    }
  }
}

未經檢查的演員在這里真的安全嗎?

相當。 您的代碼不會導致堆污染,因為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> { ... }

以下表達式將始終為true:

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

不完全是。 如果 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);

我不知道防止這種情況的編譯時方法,但是如果泛型類型T 在編譯時綁定到類 則可以在運行時檢查IEventSubscriber<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在編譯時綁定到參數T 的所有實例ClassA將有StringTIEventSubscriber<T> 但在ClassBString在運行時綁定到T ClassB實例可以具有T任何值。 如果您的IEventSubscriber<T>實現在編譯時與上面的ClassA綁定參數T ,那么您可以通過以下方式在運行時獲取該類型:

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注冊時檢查類型,從而允許您更輕松地跟蹤錯誤代碼,而不是在發布事件時稍后檢查類型。 但是,它確實會進行一些反射,並且只能在編譯時綁定T才能檢查類型。 如果您可以信任將訂閱者傳遞給EventManager的代碼,那么我只需保留代碼,因為它更簡單。 但是使用上述反射會使你一點安全IMO檢查的類型。

另外請注意,您可能希望重構初始化CopyOnWriteArraySet的方式,因為subscribe方法當前正在每次調用時創建一個新集合,無論是否需要。 嘗試這個:

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

這避免了在每次方法調用時創建一個新的CopyOnWriteArraySet ,但是如果你有一個競爭條件並且兩個線程試圖一次放入一個set, putIfAbsent仍然會返回創建到第二個線程的第一個set,所以不存在覆蓋的危險它。

由於您的subscribe實現確保ConcurrentMap中的每個Class<?>鍵映射到正確的IEventSubscriber<?> ,因此在publish從地圖檢索時使用@SuppressWarnings("unchecked")是安全的。

只需確保正確記錄警告被抑制的原因,以便任何未來的開發人員都能知道發生了什么變化。

另見這些相關帖子:

具有相關類型的通用鍵/值的通用映射

Java映射的值受鍵的類型參數限制

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM