3

構築中の Android アプリ用に、軽量でスレッドセーフなアプリ内パブリッシュ/サブスクライブ メカニズムを作成しようとしています。私の基本的なアプローチは、各イベント タイプ T のリストを追跡し、IEventSubscriber<T>タイプ T のペイロードを渡すことで、サブスクライブするオブジェクトにイベントを発行できるようにすることです。

私はジェネリック メソッド パラメーターを使用して、サブスクリプションがタイプ セーフな方法で作成されるようにしています。したがって、イベントを公開するときにサブスクリプション マップからサブスクライバーのリストを取得すると、それを のリストにキャストしても問題ないと確信していますがIEventSubscriber<T>、これにより unchecked cast 警告が生成されます。

私の質問:

  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

チェックされていないキャストは実際にここで安全ですか?

とても。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>

のタイプパラメータを実際にチェックするには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>();

上記の例では、 forClassAはコンパイル時Stringにパラメーターにバインドされます。TのすべてのインスタンスにClassAStringTin があり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、後でイベントを公開するときに型をチェックするよりも、悪いコードを簡単に追跡できるようになります。ただし、いくつかのばかげたリフレクションを実行しT、コンパイル時にバインドされている場合にのみ型を確認できます。サブスクライバーを EventManager に渡すコードを信頼できる場合は、コードをそのままにしておきます。はるかに単純だからです。ただし、上記のようにリフレクションを使用してタイプを確認すると、 IMO が少し安全になります。

もう 1 つの注意点として、CopyOnWriteArraySetsを初期化する方法をリファクタリングすることをお勧めします。subscribeメソッドは現在、必要かどうかにかかわらず、すべての呼び出しで新しいセットを作成しているためです。これを試して:

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

これにより、すべてのメソッド呼び出しで新しいセットが作成されるのを回避できCopyOnWriteArraySetますが、競合状態が発生し、2 つのスレッドが一度にセットを挿入しようとした場合putIfAbsentでも、最初に作成されたセットが 2 番目のスレッドに返されるため、上書きされる危険はありません。

于 2012-05-10T22:12:25.717 に答える
1

subscribe実装により、 内のすべてClass<?>のキーがConcurrentMap正しい にマップされることが保証されるため、内のマップから取得するときにIEventSubscriber<?>安全に使用できます。@SuppressWarnings("unchecked")publish

将来クラスに変更を加える開発者が何が起こっているのかを認識できるように、警告が抑制されている理由を適切に文書化してください。

これらの関連記事も参照してください。

関連する型を持つ汎用キー/値の汎用マップ

キーの型パラメーターによって値が制限された Java マップ

于 2012-05-10T21:32:57.047 に答える