При разработке мне всегда приходится переписывать одно и то же лямбда-выражение снова и снова, что довольно избыточно, и в большинстве случаев политика форматирования кода, навязанная моей компанией, не помогает. Поэтому я переместил эти общие лямбды в служебный класс как статические методы и использую их как ссылки на методы. Лучший пример, который у меня есть, — слияние 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))
Мои вопросы следующие:
Что из вышеперечисленного является правильным подходом и почему?
Предлагает ли какой-либо из них преимущество в производительности или ставит под угрозу производительность?
Обновлено: Игнорируйте мои замечания о том, чтобы избежать ненужных выделений, см. Ответ Хольгера о том, почему.
Между ними не будет заметной разницы в производительности — хотя первый вариант позволяет избежать ненужных распределений. Я бы предпочел ссылку на метод, поскольку функция не получает никакого значения и, следовательно, в этом контексте не нуждается в лямбда-выражении. По сравнению с созданием IllegalArgumentException
, который должен заполнить свою трассировку стека перед тем, как быть брошенным (что довольно дорого), разница в производительности совершенно незначительна.
Помните: речь идет больше о читабельности и передаче того, что делает ваш код, чем о производительности. Если вы когда-нибудь упираетесь в стену производительности из-за такого кода, лямбда-выражения и потоки просто не подходят, поскольку они представляют собой довольно сложную абстракцию со многими косвенными действиями.
Я немного покопался и ничего не нашел по этой теме, отличный комментарий - поищу ответ. Все, что я нашел, подразумевает, что ссылки на методы рассматриваются как особая форма лямбды, которая не требует синтеза метода. И ссылки на методы не загрязняют трассировку стека. Вернусь к вам, если найду что-нибудь еще
Как вы думаете, почему первый вариант позволяет избежать ненужных выделений? Как вы думаете, какие выделения дает второй вариант, которого можно было бы избежать?
Мне придется пригласить сюда Хольгера, точно, о каких ассигнованиях вы говорите?
Я исправлен, как указано в ответе Хольгера. Я не думал, что JVM будет создавать экземпляр класса для каждой ссылки на функцию. Даже если бы это было не так, оба варианта выделили бы ровно один экземпляр, что аннулирует мой аргумент о том, что ссылка на функцию выделяет меньше.
Ни один из вариантов не является более правильным, чем другой.
Кроме того, нет существенной разницы в производительности, поскольку соответствующий байт-код даже идентичен. В любом случае в вашем классе будет метод, содержащий оператор 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
.
Если ссылки на методы лучше, то почему в исходном коде Java используется вторая форма, например: java.util.function.Function.identity() или java.util.stream.Collectors.throwingMerger()