Java Lambda Expressions vs Method References
Join the DZone community and get the full member experience.
Join For FreeNow we can use lambda expressions to implement functional interfaces as we have seen in previous posts, but the lambda expressions are not the only mechanism we can use for this purpose. Particularly where there are preexisting code that we would like to reuse we can simplify the implementation of the functional interface by using method references.
Static Method References
First, consider the existence of a functional interface Predicate
as follows:
public interface Predicate<T> { public void test(T t); }
And let’s say that we had a method to filter elements out of a list using this predicate, as follows:
static <T> List<T> filter(Predicate<T> predicate, List<T> source) { List<T> destiny = new ArrayList<>(); for (T item : source) { if(predicate.test(item)){ destiny.add(item); } } return destiny; }
Finally, let’s say we had a class containing a set of static method predicates which we had defined in the past, prior to the existence of the Java 8. Something as follows:
static class IntPredicates { public static boolean isOdd(Integer n) { return n % 2 != 0; } public static boolean isEven(Integer n) { return n % 2 == 0; } public static boolean isPositive(Integer n) { return n >= 0; } }
Now, one way to implement a predicate that could reuse our static methods would be through the use of lambda expressions, like this:
Predicate<Integer> isOdd = n -> IntPredicates.isOdd(n); Predicate<Integer> isEven = n -> IntPredicates.isEven(n);
However, we can clearly see that the signature of the static predicate methods corresponds perfectly with the signature of the test
method for integer predicates. So, an alternative way to implement the functional interface in this case is through a static method reference, as follows:
Predicate<Integer> isOdd = IntPredicates::isOdd; Predicate<Integer> isEven = IntPredicate::isEven;
Notice the use of double colon ::
here. We are not invoking the method, we are just referencing its name.
We could now use this technique to filter a list of numbers that satisfy any of these predicates, something like this:
List<Integer> numbers = asList(1,2,3,4,5,6,7,8,9); List<Integer> odds = filter(IntPredicates::isOdd, numbers); List<Integer> evens = filter(IntPredicates::isEven, numbers);
So, as we can see, we could implement the functional interfaces in this case using both: lambda expressions and method references, but the syntax with the static method references was more succinct.
Constructor Method References
Let’s consider now the existence of a functional interface named Function
, as follows:
public interface Function<T,R> { public R apply(T t); }
Based on it, we could define a method map
, that converts the elements from a source list from certain value T to certain value R, as follows:
static <T,R> List<R> map(Function<T,R> function, List<T> source) { List<R> destiny = new ArrayList<>(); for (T item : source) { R value = function.apply(item); destiny.add(value); } return destiny; }
Now imagine that we had a list of strings containing numbers that we
would like to transform to integer values. We could do it using a lambda
expression to provide an implementation for the Function
interface, more or less like this:
List<String> digits = asList("1","2","3","4","5"); List<Integer> numbers = map(s -> new Integer(s), digits);
However, we can clearly infer that the constructor Integer(String)
has the same signature as the apply
method in the Function
reference required here, namely, it receives a string as argument and returns an integer.
So, in this case we simplify the implementation of the functional interface by means of using a constructor reference, as follows:
List<String> digits = asList("1","2","3","4","5"); List<Integer> numbers = map(Integer::new, digits);
This conveys the same meaning: take a string and make me an integer out of it. It is the perfect task for our Integer(String)
constructor.
Instance Method Reference to Arbitrary Objects
Consider now the existence of a class named Jedi
, defined as follows:
public class Jedi { private String name; private int power; public Jedi(String name, int power){ this.name = name; this.power = power; } public String getName() { return name; } public int getPower() { return power; } //equals,hashCode,toString }
Now, consider that we had a list of jedis, and we would like to use our previous function map
to extract the name from every jedi and generate a list of names out of
the list of jedis. Somewhat like this, using lambda expressions:
List<Jedi> jedis = asList(new Jedi("Obi-wan", 80), new Jedi("Anakin", 25), new Jedi("Yoda", 500)); List<String> names = map(jedi -> jedi.getName() , jedis);
The interesting observation here is that the parameter jedi
is the argument for the apply
method in the Function
reference. And we use that reference to a jedi to invoke on it the method getName
. In other words, we invoke a method on the reference we receive as argument.
So, we could simplify this implementation by using an instance method reference as follows:
List<Jedi> jedi = asList(new Jedi("Obi-wan", 80), new Jedi("Anakin", 25), new Jedi("Yoda", 500)); List<String> names = map(Jedi::getName, jedi);
Again, the interesting aspect of this type of method reference is that the method getName
is an instance method. Therefore, the target of its invocation must be
an instance, which in this case is an arbitrary object being provided as
the argument for the method apply
in the Function
interface definition.
Instance Method Reference to a Specific Object
Let’s consider the existence of functional interface named Consumer
, as follows
public interface Consumer<T> { public void accept(T t); }
And let’s define a method capable of using a consumer to consume all the elements of a given list, like this:
static void forEach(Consumer<T> consumer, List<T> source){ for (T item : source) { consumer.accept(item); } }
Imagine that now we would like to print all the elements contained in a list, and for that purpose we could define a consumer using a lambda expressions:
List<Integer> numbers = asList(1,2,3,4,5,6,7,8,9); forEach(n -> { System.out.println(n); }, numbers);
However, we could also make the observation that the method println
has the same signature that our Consumer
has, it receives an integer and does something with it, in this case it prints it to the main output.
However, we cannot specify that this is an arbitrary instance method reference by saying PrintStream::println
, because in this case the Consumer
interface method accept
does not receive as one of its arguments the PrintStream
object on which we may want to invoke the method println
.
Conversely, we already know which is the target object on which we
would like to invoke the method: we can see that every time we would
like to invoke it on a specific reference, in this case the object System.out
.
So, we could implement our functional interface using an instance method reference to a specific object as follows:
List<Integer> numbers = asList(1,2,3,4,5,6,7,8,9); forEach(System.out::println, numbers);
In summary, there are circumstances in which we would like to use some preexisting code as the implementation for a functional interface, in those case we could use one of several variants of method references instead of a more verbose lambda expression.
Published at DZone with permission of Edwin Dalorzo. See the original article here.
Opinions expressed by DZone contributors are their own.
Comments