Избегание неконтролируемого приведения к приведениям в коллекцию универсального интерфейса на Java для издателя событий
Я пытаюсь создать легкий, поточно-ориентированный механизм публикации / подписки в приложении для создаваемого приложения для Android. Мой основной подход - отслеживать список IEventSubscriber<T>
для каждого типа событий T и затем иметь возможность публиковать события для подписывающихся объектов, передавая полезную нагрузку типа T.
Я использую общие параметры метода, чтобы (я думаю) гарантировать, что подписки создаются безопасным способом. Таким образом, я почти уверен, что когда я получу список подписчиков с моей карты подписки, когда придет время опубликовать событие, я в порядке, приведя его к списку IEventSubscriber<T>
однако, это генерирует непроверенное предупреждение о сотворении.
Мои вопросы:
- Действительно ли незарегистрированный актерский состав здесь безопасен?
- Как я могу на самом деле проверить, реализуют ли элементы в списке подписчиков
IEventSubscriber<T>
? - Предполагая, что (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 со значениями, ограниченными параметром типа ключа