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

У меня следующие вопросы:

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

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

0
Mate Szilard 27 Май 2019 в 22:16

2 ответа

Лучший ответ

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

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

3
Holger 28 Май 2019 в 08:27

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

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

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

1
roookeee 28 Май 2019 в 08:58