Ссылки на методы для статических или статических методов, которые возвращают лямбда-выражения - PullRequest
0 голосов
/ 27 мая 2019

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

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

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

  • Один из них дает преимущество в производительности или снижает производительность?

Ответы [ 2 ]

3 голосов
/ 28 мая 2019

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

Кроме того, нет существенной разницы в производительности, поскольку соответствующий байт-код даже идентичен.В любом случае, в вашем классе будет метод, содержащий оператор 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.

1 голос
/ 27 мая 2019

РЕДАКТИРОВАТЬ: игнорировать мои замечания о том, как избежать ненужных ассигнований, см. Ответ Хольгерса о том, почему.

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

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

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...