Мне интересно, что может быть хорошим способом реализации некоторого наблюдаемого в Java без особого интерфейса. Я подумал, что было бы неплохо использовать предопределенные функциональные интерфейсы. В этом примере я использую String Consumer для представления слушателя, который принимает String для уведомления.

class Subject {

  List<Consumer<String>> listeners = new ArrayList<>();
  
  void addListener(Consumer<String> listener) {listeners.add(listener);}

  void removeListener(Consumer<String> listener {listeners.remove(listener);}

  ...
}

class PrintListener{
  public void print(String s) { System.out.println(s); }
}

Subject subject = new ...
PrintListener printListener= new ...
subject.add(printListener); // works, i find it in the listener list
subject.remove(printListener); // does NOT work. I still find it in the list

Я нашел объяснение:

Consumer<String> a = printListener::print;
Consumer<String> b = printListener::print;

// it holds:
// a==b       : false
// a==a       : true
// a.equals(b): false
// a.equals(a): true

Поэтому я не могу использовать лямбды / указатели на функции в том виде, в каком они есть.

Всегда есть альтернатива вернуть старые добрые интерфейсы, s.t. мы регистрируем экземпляры объектов, но не лямбды. Но я надеялся, что есть что-то более легкое.

РЕДАКТИРОВАТЬ: из текущих ответов я вижу следующие подходы:

А) Верните дескриптор, содержащий исходную ссылку
б) сохраните исходную ссылку самостоятельно
c) Вернуть некоторый идентификатор (целое число), который можно использовать в subject.remove () вместо исходной ссылки.

Мне нравится а). Вам все еще нужно следить за ручкой.

9
markus 8 Июн 2021 в 14:15

4 ответа

Лучший ответ

В последнее время я довольно часто использую rjxs, и там они использовали настраиваемое возвращаемое значение, называемое Subscription, которое можно вызвать для повторного удаления зарегистрированного слушателя. То же самое можно сделать и в вашем случае:

public interface Subscription {
    void unsubscribe();
}

Затем измените свой метод addListener на этот:

public Subscription addListener(Consumer<String> listener) {
    listeners.add(listener);
    return () -> listeners.remove(listener);
}

Метод removeListener можно полностью удалить. А теперь это можно назвать так:

Subscription s = subject.addListener(printListener::print);
// later on when you want to remove the listener
s.unsubscribe();

Это работает, потому что возвращенная лямбда в addListener() по-прежнему использует ту же ссылку listener и, таким образом, может быть снова удалена из List. Боковое примечание: вероятно, было бы разумнее использовать Set, если вы не заботитесь о порядке итерации вашего listeners

Было бы неплохо прочитать Есть ли способ сравнить лямбды? , что более подробно объясняет, почему printListener::print != printListener::print.

6
Lino 8 Июн 2021 в 12:10

Вы предполагаете, что каждый вызов printListener::print возвращает один и тот же экземпляр print.

Subject subject = new Subject();
PrintListener printListener= new PrintListener();
subject.add(printListener::print);
subject.remove(printListener::print);

Приведенный выше код добавляет одного слушателя и пытается удалить другого слушателя, поскольку printListener::print.equals(printListener::print) == false.

Subject subject = new Subject();
PrintListener printListener= new PrintListener();
Consumer<String> listener = printListener::print;
subject.add(listener);
subject.remove(listener);

Вышеупомянутое работает, требуя, чтобы вы сохранили ссылку на слушателя, если вам нужно его удалить. Хотя, если вы хотите, чтобы он был очень легковесным, вы могли бы придерживаться только Consumer без конкретного класса PrintListener, если реализации очень просты.

Consumer<String> listener = System.out::println;
subject.add(listener);
subject.remove(listener);
6
Kayaman 8 Июн 2021 в 16:54

Вы правы, полагаться на равенство функций становится немного неловко.

Раньше я иногда решал эту проблему: метод add генерирует случайный идентификатор. Вызывающий может использовать тот же идентификатор, если он хочет удалить слушателя в будущем, или он может просто отказаться от идентификатора, если он никогда не планирует удалять слушателя.

class Subject {
    Map<String, Consumer<String>> idToListener = new HashMap<>();
  
    // Returns the ID of the listener, use it to remove 
    String addListener(Consumer<String> listener) {
       String id = generateRandomId();
       idToListeners.put(id, listener);
       return id;
    }

    void removeListener(String id) { idToListener.remove(id); }
}

generateRandomId() может быть чем угодно. Это не должно быть сложно. Вы можете использовать последовательные целые числа, если хотите.

3
Michael 8 Июн 2021 в 11:59

Чтобы это работало, PrintListener необходимо реализовать Consumer<String>, поскольку это необходимая подпись для слушателя (согласно вашему определению):

class PrintListener implements Consumer<String> {
    public void accept(String s) {
        System.out.println(s);
    }
}

Чтобы проверить это, вы можете реализовать простой метод notify в Subject:

void notify(String s) {
    listeners.forEach(listener -> listener.accept(s));
}

Теперь ваш код компилируется и добавление / удаление слушателя работает должным образом. Чтобы ответить на вторую часть вашего вопроса, я добавил слушателя, использующего лямбда, чтобы продемонстрировать, что это работает так же хорошо:

Subject subject = new Subject();

PrintListener printListener = new PrintListener();
Consumer<String> lambdaListener = System.out::println;

subject.addListener(printListener);
subject.addListener(lambdaListener);

subject.notify("abc");
subject.removeListener(printListener);
subject.removeListener(lambdaListener);
subject.notify("xyz");

Вышеупомянутый вывод abc дважды, как ожидалось.

2
Matt 8 Июн 2021 в 11:57