简体   繁体   中英

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

I'm trying to create a lightweight, thread-safe in-app publish/subscribe mechanism for an Android app that I'm building. My basic approach is to keep track of a list of IEventSubscriber<T> for each event type T and then be able to publish events to subscribing objects by passing along a payload of type T.

I use generic method parameters to (I think) ensure that subscriptions are created in a type safe way. Thus, I'm pretty sure that when I obtain the list of subscribers from my subscription map when it comes time to publish an event that I'm OK casting it to a list of IEventSubscriber<T> , however, this generates the unchecked cast warning.

My questions:

  1. Is the unchecked cast actually safe here?
  2. How can I actually check to see if the items in the subscriber list implement IEventSubscriber<T> ?
  3. Presuming that (2) involves some nasty reflection, what would you do here?

Code (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);
      }
    }
  }
}

Is the unchecked cast actually safe here?

Quite. Your code will not cause heap pollution because the signature of subcribe ensures that you only put IEventSubscribers of the proper compile time type into the map. It might propagate heap pollution caused by an unsafe unchecked cast elsewhere, but there is little you can do about that.

How can I actually check to see if the items in the subscriber list implement IEventSubscriber?

By casting each item to IEventSubscriber . Your code already does this in the following line:

for (IEventSubscriber<T> subscriber: existingSubscribers) {

If existingSubscribers contained an object not assignable to IEventSubscriber , this line would throw a ClassCastException. Standard practice to avoid a warning when iterating over a list of unknown type parameter is to explicitly cast each item:

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

That code explicitly checks that each item is an IEventSubscriber , but can not check that it is an IEventSubscriber<T> .

To actually check the type parameter of IEventSubscriber , the IEventSubscriber needs to help you out. That is due to erasure, specifically, given the declaration

class MyEventSubscriber<T> implements IEventSubscriber<T> { ... }

the following expression will always be true:

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

Presuming that (2) involves some nasty reflection, what would you do here?

I'd leave the code as it is. It is quite easy to reason that the cast is correct, and I would not find it worth my time to rewrite it to compile without warnings. If you do wish to rewrite it, the following idea may be of use:

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

and

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

Not exactly. It will be safe if all clients of the EventManager class always use generics and never rawtypes; ie, if your client code compiles without generics-related warnings.

However, it is not difficult for client code to ignore those and insert an IEventSubscriber that's expecting the wrong type:

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

I don't know of a compile-time way to prevent this scenario, but you can check the generic parameter types of instances of IEventSubscriber<T> at runtime if the generic type T was bound to the class at compile time . Consider:

public class ClassA implements IEventSubscriber<String> { ... }
public class ClassB<T> implements IEventSubscriber<T> { ... }

IEventSubscriber<String> a = new ClassA();
IEventSubscriber<String> b = new ClassB<String>();

In the above example, for ClassA , String is bound to the parameter T at compile time. All instances of ClassA will have String for the T in IEventSubscriber<T> . But in ClassB , String is bound to T at runtime. Instances of ClassB could have any value for T . If your implementations of IEventSubscriber<T> bind the parameter T at compile time as with ClassA above, then you can obtain that type at runtime the following way:

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

This will cause the types to be checked when the subscriber is registered with the EventManager , allowing you to track down bad code more easily, rather than if the types were to just get checked much later when publishing an event. However, it does do some hokey reflection and can only check the types if T is bound at compile time. If you can trust the code that will be passing subscribers to EventManager, I'd just leave the code as it is, because it's much simpler. However checking the type using reflection as above will make you a little safer IMO.

One other note, you may want to refactor the way you initialize your CopyOnWriteArraySet s, because the subscribe method is currently creating a new set on every invocation, whether it needs to or not. Try this:

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

This avoids creating a new CopyOnWriteArraySet on every method call, but if you have a race condition and two threads try to put in a set at once, putIfAbsent will still return the first set created to the second thread, so there is no danger of overwriting it.

Since your subscribe implementation ensures that every Class<?> key in the ConcurrentMap maps to the correct IEventSubscriber<?> , it is safe to use @SuppressWarnings("unchecked") when retrieving from the map in publish .

Just make sure to properly document the reason why the warning is being suppressed so that any future developer making changes to the class is aware of what's going on.

See also these related posts:

Generic Map of Generic key/values with related types

Java map with values limited by key's type parameter

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM