Справочник по методам Java

Я пытаюсь создать библиотеку, в которой вы можете добавлять и удалять прослушиватели событий в системе pub/sub, но столкнулся с проблемой, используя ссылки на методы:

// here, this::printMessage is being passed as an instance of Consumer<String>
pubSub.subscribe(this::printMessage);
pubSub.unsubscribe(this::printMessage);

Внутри вызов subscribe() добавит экземпляр Consumer<T> в Set<Consumer<T>>, а unsubscribe() удалит его. Эта проблема возникает из-за того, что каждое использование this::printMessage здесь фактически заставляет компилятор генерировать новую ссылку/экземпляр объекта, поэтому отказ от подписки на самом деле не работает.

Обходной путь до сих пор, который мне удалось:

final Consumer<String> consumer = this::printMessage;
pubSub.subscribe(consumer);
pubSub.unsubscribe(consumer);

Но это не совсем идеально. Меня беспокоит то, что кто-то с меньшим опытом использования этой библиотеки может предположить, что они могут использовать ссылки на методы напрямую при подписке/отмене подписки, когда это не совсем так, и в худшем случае это приводит к утечке памяти.

Итак, вопрос в том, есть ли какой-нибудь умный способ избежать этого или заставить ссылку на метод всегда разрешать одну и ту же ссылку/экземпляр объекта?

Однако тот, кто использует this::printMessage, должен знать, что компилятор генерирует новый экземпляр объекта. На самом деле это не проблема вашей библиотеки.

m0skit0 31.01.2019 18:42

Там нет никакого реального способа обойти это. Вместо этого вы можете вернуть объект Subscription с помощью метода unsubscribe(), чтобы избежать подобных ошибок в API.

Joachim Sauer 31.01.2019 18:57

вернуть дескриптор подписки и использовать его для отказа от подписки (конечно, используйте этот дескриптор в качестве ключа карты, где все ваши потребители хранятся внутри, и сделайте так, чтобы этот дескриптор последовательно реализовывал hashCode и equals)

fps 31.01.2019 19:01

Возможный дубликат Есть ли способ сравнить лямбды?

Denis Zavedeev 31.01.2019 20:06

@JoachimSauer, к вашему сведению, вы должны создать фактический ответ на вопрос, чтобы ОК мог пометить его как правильный ответ.

barclay 31.01.2019 20:35

Если один из приведенных ниже ответов решает вашу проблему, вы должны принять его (установите флажок рядом с соответствующим ответом). Это делает две вещи. Это позволяет всем узнать, что ваша проблема была решена к вашему удовлетворению, и дает человеку, который помогает вам, кредит на помощь. Глянь сюда для полного объяснения.

dpr 20.05.2019 12:38
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
Как вычислять биты и понимать побитовые операторы в Java - объяснение с примерами
Как вычислять биты и понимать побитовые операторы в Java - объяснение с примерами
В компьютерном программировании биты играют важнейшую роль в представлении и манипулировании данными на двоичном уровне. Побитовые операции...
Поднятие тревоги для долго выполняющихся методов в Spring Boot
Поднятие тревоги для долго выполняющихся методов в Spring Boot
Приходилось ли вам сталкиваться с требованиями, в которых вас могли попросить поднять тревогу или выдать ошибку, когда метод Java занимает больше...
Полный курс Java для разработчиков веб-сайтов и приложений
Полный курс Java для разработчиков веб-сайтов и приложений
Получите сертификат Java Web и Application Developer, используя наш курс.
7
6
132
2

Ответы 2

Когда вы пишете код вроде:

pubSub.subscribe(this::printMessage);
pubSub.unsubscribe(this::printMessage);

Это аналогичный код, например:

pubSub.subscribe(new Consumer() {

            @Override
            public void accept(Object t) {
              // your code here
            };
        });

pubSub.unsubscribe(new Consumer() {

            @Override
            public void accept(Object t) {
              // your code here
            };
        });

Таким образом, из приведенного выше кода ясно, что каждый раз будет создаваться новый объект.

Now why those are similar code?

Java представила инструкцию байт-кода invokedynamic для создания анонимного класса, а затем generate byte code для lambdas/метод ref. Итак, в случае lambdas/method ref java генерирует класс реализации и генерирует байтовый код во время выполнения.. После того, как байт-код сгенерирован, остальная часть шага аналогична обычному вызову метода.

Is there some clever way to avoid this or coerce the method reference to always resolve to the same object reference/instance?

-> Я не верю, что есть какой-то другой умный способ (кроме того, что вы сделали в качестве обходного пути), чтобы сделать это.

Спасибо, я понимаю, почему это происходит, мне просто интересно, есть ли способ обойти это, поскольку это неочевидное поведение для тех, кто пришел из других языков, у которых нет этого ограничения.

Gene Trog 31.01.2019 18:55

Я не верю, что есть какой-то обходной путь. Не могли бы вы сообщить мне, на каком языке он концептуально отличается от Java. Спасибо!!!

Amit Bera 31.01.2019 18:59

С# — это то, что я могу придумать навскидку: попробуйте это: dotnetfiddle.net/TOIn3s

Gene Trog 01.02.2019 16:33

Вы можете заставить subscribe возвращать фактический экземпляр Consumer или идентификатор для добавленного Consumer. Это возвращаемое значение можно использовать в unsubscribe для повторного удаления Consumer.

Может быть, что-то похожее на это:

Map<UUID, Consumer<?>> consumers = new ConcurrentHashMap<>();

public UUID subscribe(Consumer<?> consumer) {
    UUID identifier = UUID.randomUUID();
    consumers.put(identifier, consumer);
    return identifier;
}

public void unsubscribe(UUID identifier) {
    consumers.remove(identifier);
}

Использование идентификатора вместо фактического экземпляра Consumer в качестве возвращаемого значения имеет то преимущество, что пользователи вашего кода сразу увидят, что им нужно отслеживать возвращенный UUID вместо использования unsubscribe с другим «идентичным» (с точки зрения поведения) ) Consumer.

Но я бы не стал использовать для этого UUID, как обычный объект без каких-либо свойств.

Holger 18.02.2019 10:25

@Holger вам нужен простой объект с правильной реализацией равенства и хэш-кода. Можно было бы использовать строковое представление UUID, но при этом вы ничего не выиграете.

dpr 18.02.2019 10:28

Каждый объект имеет реализацию equals и hashCode. Реализация по умолчанию делает этот объект равным ничему, кроме самого себя, что и является желаемым поведением. Напротив, UUID позволяет создавать другой равный объект с помощью строкового представления, что вам нужно здесь нет. Вы хотите, чтобы подписчик сохранил возвращенный объект и передал его обратно в unsubscribe, а не выполнял с ним преобразования или преобразования.

Holger 18.02.2019 10:31

@Holger На самом деле мы точно не знаем полных требований здесь. Возможно, ОП хочет сохранить ссылку для последующего удаления, а может и нет. Да, вы можете использовать простой объект, но это излишне ограничит возможные варианты использования. Однако приведенный выше пример был только для демонстрации возможного решения. Это не означало «сделай так, или ты сделал это неправильно».

dpr 18.02.2019 10:36

OP имеет, чтобы сохранить ссылку, хотят они этого или нет. Возможность преобразовать этот UUID в String этого не меняет. Тогда вам придется сохранить строку. И вопрос был об ограничении ссылок на методы над обычными объектами. Обычные слушатели также не имеют возможности реконструировать их через строковое представление. И нет смысла предоставлять форму постоянного ключа для регистрации, которая в любом случае не длится дольше этого времени выполнения.

Holger 18.02.2019 10:45

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