Neither variant is more correct than the other.
Further, there is no significant performance difference, as the relevant bytecode is even identical. In either case, there will be a method holding a throw statement in your class and an instance of a runtime generated class which will invoke that method.
Note that you can find both patterns within the JDK itself.
Function.identity()
and Map.Entry.comparingByKey()
are examples of factory methods containing a lambda expression
Double::sum
, Objects::isNull
, or Objects::nonNull
are examples of method references to target methods solely existing for the purpose of being referenced that way
Generally, if there are also use cases for invoking the methods directly, it’s preferable to provide them as API methods, which may also be referenced by method references, e.g. Integer::compare
, Objects::requireNonNull
, or Math::max
.
On the other hand, providing a factory method makes the method reference an implementation detail that you can change when there is a reason to do so. E.g., did you know that Comparator.naturalOrder()
is not implemented as T::compareTo
? Most of the time, you don’t need to know.
Of course, factory methods taking additional parameters can’t be replaced by method references at all; sometimes, you want the parameterless methods of a class to be symmetric to those taking parameters.
There is only a tiny difference in memory consumption. Given the current implementation, every occurrence of, e.g. Objects::isNull
, will cause the creation of a runtime class and an instance, which will then be reused for the particular code location. In contrast, the implementation within Function.identity()
makes only one code location, hence, one runtime class and instance. See also this answer.
But it must be emphasized that this is specific to a particular implementation, as the strategy is implemented by the JRE, further, we’re talking about a finite, rather small number of code locations and hence, objects.
By the way, these approaches are not contradicting. You could even have both:
// 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;
}
Note that there’s another point to consider; the factory method always returns a materialized instance of a particular interface, i.e. BinaryOperator
. For methods that need to be bound to different interfaces, depending on the context, you need method references at these places anyway. That’s why you can write
DoubleBinaryOperator sum1 = Double::sum;
BinaryOperator<Double> sum2 = Double::sum;
BiFunction<Integer,Integer,Double> sum3 = Double::sum;
which would not be possible if there was only a factory method returning a DoubleBinaryOperator
.