Java Lambda Expressions vs Method References
Now 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 { 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 List filter(Predicate predicate, List source) { List 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 isOdd = n -> IntPredicates.isOdd(n); Predicate 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 isOdd = IntPredicates::isOdd; Predicate 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 numbers = asList(1,2,3,4,5,6,7,8,9); List odds = filter(IntPredicates::isOdd, numbers); List 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 { 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 List map(Function function, List source) { List 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 digits = asList("1","2","3","4","5"); List 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 digits = asList("1","2","3","4","5"); List 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 jedis = asList(new Jedi("Obi-wan", 80), new Jedi("Anakin", 25), new Jedi("Yoda", 500)); List 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 = asList(new Jedi("Obi-wan", 80), new Jedi("Anakin", 25), new Jedi("Yoda", 500)); List 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 { 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 consumer, List 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 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 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.
April 9, 2013
by Edwin Dalorzo
·
78,073 Views
·
5 Likes