Избегание неконтролируемого приведения к приведениям в коллекцию универсального интерфейса на Java для издателя событий

Я пытаюсь создать легкий, поточно-ориентированный механизм публикации / подписки в приложении для создаваемого приложения для 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);
      }
    }
  }
}

3 ответа

Решение

Действительно ли незарегистрированный актерский состав здесь безопасен?

Довольно. Ваш код не вызовет загрязнения кучи, поскольку подпись подписки гарантирует, что вы добавляете в карту только IEventSubscribeers соответствующего типа времени компиляции. Это может привести к загрязнению кучи, вызванному небезопасным неконтролируемым приведением в другом месте, но с этим мало что можно поделать.

Как я могу на самом деле проверить, реализуют ли элементы в списке подписчиков 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> { ... }

следующее выражение всегда будет истинным:

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 класс всегда использует дженерики и никогда не использует типы; то есть, если ваш клиентский код компилируется без предупреждений, связанных с генериками.

Однако клиентскому коду не сложно игнорировать их и вставить 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>();

В приведенном выше примере для ClassA, String привязан к параметру T во время компиляции. Все случаи ClassA буду иметь String для T в IEventSubscriber<T>, Но в ClassB, String связан с T во время выполнения. Экземпляры ClassB может иметь любое значение для T, Если ваши реализации IEventSubscriber<T> привязать параметр 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что позволяет вам легче отслеживать плохой код, а не просто проверять типы гораздо позже при публикации события. Тем не менее, он делает некоторые отражения и может проверять типы, только если T связан во время компиляции. Если вы можете доверять коду, который будет передавать подписчиков в EventManager, я бы просто оставил код как есть, потому что он намного проще. Однако проверка типа с помощью отражения, как указано выше, сделает вашу IMO немного безопаснее.

Еще одно примечание, вы можете изменить способ инициализации вашего CopyOnWriteArraySetс, потому что subscribe В настоящее время метод создает новый набор при каждом вызове, независимо от того, нужен он или нет. Попробуй это:

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

Это позволяет избежать создания нового CopyOnWriteArraySet при каждом вызове метода, но если у вас есть условие гонки и два потока пытаются поместить в набор сразу, putIfAbsent будет по-прежнему возвращать первый набор, созданный во второй поток, поэтому нет опасности перезаписать его.

Так как ваш subscribe реализация гарантирует, что каждый Class<?> ключ в ConcurrentMap карты на правильные IEventSubscriber<?>, это безопасно использовать @SuppressWarnings("unchecked") при получении с карты в publish,

Просто убедитесь, что вы правильно документировали причину подавления предупреждения, чтобы любой будущий разработчик, вносящий изменения в класс, знал о происходящем.

Смотрите также эти похожие сообщения:

Универсальная карта универсального ключа / значений со связанными типами

Карта Java со значениями, ограниченными параметром типа ключа

Другие вопросы по тегам