Ссылки на статические методы или статические методы, которые возвращают лямбда-выражения

При разработке мне всегда приходится переписывать одно и то же лямбда-выражение снова и снова, что довольно избыточно, и в большинстве случаев политика форматирования кода, навязанная моей компанией, не помогает. Поэтому я переместил эти общие лямбды в служебный класс как статические методы и использую их как ссылки на методы. Лучший пример, который у меня есть, — слияние Throwing, используемое в сочетании с java.util.stream.Collectors.toMap(Function, Function, BinaryOperator, Supplier). Всегда приходится писать (a,b) -> {throw new IllegalArgumentException("Some message");}; только потому, что я хочу использовать пользовательскую реализацию карты, много хлопот.

//First Form

public static <E> E throwingMerger(E k1, E k2) {
    throw new IllegalArgumentException("Duplicate key " + k1 + " not allowed!");
  }

//Given a list of Car objects with proper getters
Map<String,Car> numberPlateToCar=cars.stream()//
   .collect(toMap(Car::getNumberPlate,identity(),StreamUtils::throwingMerger,LinkedHasMap::new))
//Second Form 

  public static <E> BinaryOperator<E> throwingMerger() {
    return (k1, k2) -> {
      throw new IllegalArgumentException("Duplicate key " + k1 + " not allowed!");
    };
  }
Map<String,Car> numberPlateToCar=cars.stream()//
   .collect(toMap(Car::getNumberPlate,identity(),StreamUtils.throwingMerger(),LinkedHasMap::new))

Мои вопросы следующие:

  • Что из вышеперечисленного является правильным подходом и почему?

  • Предлагает ли какой-либо из них преимущество в производительности или ставит под угрозу производительность?

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

Ответы 2

Обновлено: Игнорируйте мои замечания о том, чтобы избежать ненужных выделений, см. Ответ Хольгера о том, почему.

Между ними не будет заметной разницы в производительности — хотя первый вариант позволяет избежать ненужных распределений. Я бы предпочел ссылку на метод, поскольку функция не получает никакого значения и, следовательно, в этом контексте не нуждается в лямбда-выражении. По сравнению с созданием IllegalArgumentException, который должен заполнить свою трассировку стека перед тем, как быть брошенным (что довольно дорого), разница в производительности совершенно незначительна.

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

Если ссылки на методы лучше, то почему в исходном коде Java используется вторая форма, например: java.util.function.Function.identity() или java.util.stream.Collectors.throwingMerger()

Mate Szilard 27.05.2019 21:52

Я немного покопался и ничего не нашел по этой теме, отличный комментарий - поищу ответ. Все, что я нашел, подразумевает, что ссылки на методы рассматриваются как особая форма лямбды, которая не требует синтеза метода. И ссылки на методы не загрязняют трассировку стека. Вернусь к вам, если найду что-нибудь еще

roookeee 27.05.2019 23:45

Как вы думаете, почему первый вариант позволяет избежать ненужных выделений? Как вы думаете, какие выделения дает второй вариант, которого можно было бы избежать?

Holger 28.05.2019 10:32

Мне придется пригласить сюда Хольгера, точно, о каких ассигнованиях вы говорите?

Eugene 28.05.2019 10:43

Я исправлен, как указано в ответе Хольгера. Я не думал, что JVM будет создавать экземпляр класса для каждой ссылки на функцию. Даже если бы это было не так, оба варианта выделили бы ровно один экземпляр, что аннулирует мой аргумент о том, что ссылка на функцию выделяет меньше.

roookeee 28.05.2019 10:56
Ответ принят как подходящий

Ни один из вариантов не является более правильным, чем другой.

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

Обратите внимание, что вы можете найти оба шаблона в самом JDK.

  • Function.identity() и Map.Entry.comparingByKey() являются примерами фабричных методов, содержащих лямбда-выражение.
  • Double::sum, Objects::isNull или Objects::nonNull являются примерами ссылок на целевые методы, существующие исключительно для того, чтобы на них ссылались таким образом.

Как правило, если есть также варианты использования для прямого вызова методов, предпочтительно предоставлять их как методы API, на которые также могут ссылаться ссылки на методы, например. Integer::compare, Objects::requireNonNull или Math::max.

С другой стороны, предоставление фабричного метода делает метод ссылкой на деталь реализации, которую вы можете изменить, когда для этого есть причина. Например, знаете ли вы, что Comparator.naturalOrder() — это нет, реализованный как T::compareTo? В большинстве случаев вам не нужно знать.

Конечно, фабричные методы, принимающие дополнительные параметры, вообще не могут быть заменены ссылками на методы; иногда вы хотите, чтобы методы класса без параметров были симметричны методам, принимающим параметры.


Есть только крошечная разница в потреблении памяти. Учитывая текущую реализацию, каждое появление, например. Objects::isNull, приведет к созданию класса среды выполнения и экземпляра, которые затем будут повторно использоваться для определенного места кода. Напротив, реализация в Function.identity() создает только одно место кода, следовательно, один класс среды выполнения и один экземпляр. См. также этот ответ.

Но нужно подчеркнуть, что это специфика конкретной реализации, так как стратегия реализуется JRE, далее речь идет о конечном, достаточно небольшом количестве мест кода, а значит, и объектов.


Кстати, эти подходы не противоречат друг другу. Вы могли бы даже иметь оба:

// for calling directly
public static <E> E alwaysThrow(E k1, E k2) {
    // by the way, k1 is not the key, see https://stackoverflow.com/a/45210944/2711488
    throw new IllegalArgumentException("Duplicate key " + k1 + " not allowed!");
}
// when needing a shared BinaryOperator
public static <E> BinaryOperator<E> throwingMerger() {
    return ContainingClass::alwaysThrow;
}

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

DoubleBinaryOperator sum1 = Double::sum;
BinaryOperator<Double> sum2 = Double::sum;
BiFunction<Integer,Integer,Double> sum3 = Double::sum;

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

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