• No results found

Composing Behaviors

In document CHAPTER. JavaFX Fundamentals (Page 171-174)

Taking Java to the Next Level

1.4 Composing Behaviors

Earlier in this chapter we saw how functionally similar lambda expressions are to anonymous inner classes. But writing them so differently leads to different ways of thinking about them. Lambda expressions look like functions, so it’s natural to ask whether we can make them behave like functions. That change of perspective will encourage us to think about working with behaviors rather than objects, and that in turn will lead in the direction of some very different programming idioms and library APIs.

For example, a core operation on functions is composition: combining together two functions to make a third, whose effect is the same as applying its two components in succession. Composition is not an idea that arises at all naturally in connection with anonymous inner classes, but in a generalized form it corresponds very well to the construction of traditional object-oriented programs. And just as object-oriented programs are broken down by decomposition, the reverse of composition will work for functions too.

Suppose, for example, that we want to sort a list ofPointinstances in order of their xcoordinate. The standard Java idiom for a “custom” sort4is to create aComparator: Comparator<Point> byX = new Comparator<Point>(){

public int compare(Point p1, Point p2) {

return Double.compare(p1.getX(), p2.getX());

} };

Substituting a lambda expression for the anonymous inner class declaration, as described in the previous section, improves the readability of the code:

Comparator<Point> byX =

(p1, p2) -> Double.compare(p1.getX(), p2.getX());

Ê

But that doesn’t help with another very signi cant problem:Comparatoris mono-lithic. If we wanted to de ne aComparatorthat compared onyinstead ofx coordi-nates, we would have to copy the entire declaration, substitutinggetYforgetX every-where. Good programming practice should lead us to look for a better solution, and a moment’s re ection shows thatComparatoris actually carrying out two functions—

extracting sort keys from its arguments and then comparing those keys. We should be able to improve the code ofÊ by building aComparatorfunction parameterized on these two components. We’ll now evolve the code to do that. The intermediate stages may seem awkward and verbose, but persist: the conclusion will be worthwhile.

4Two ways of comparing and sorting objects are standard in the Java platform: a class can have a natural order; in this case, it implements the interfaceComparableand so exposes acompareTomethod that an object can use to compare itself with another. Or aComparatorcan be created for the purpose, as in this case.

“Oracle_LaTeX/Mastering Lambdas/ Naftalin/ 182962-8” — 2014/8/27 — 16:22

16

Mastering Lambdas

To start, let’s turn the two concrete component functions that we have into lambda form. We know the type of the functional interface for the key extractor function—

Comparator—but we also need the type of the functional interface corresponding to the functionp -> p.getX(). Looking in the package devoted to the declaration of functional interfaces,java.util.function, we nd the interfaceFunction: public interface Function<T,R> {

public R apply(T t);

}

So we can now write the lambda expressions for both key extraction and key comparison:

Function<Point,Double> keyExtractor = p -> p.getX();

Comparator<Double> keyComparer = (d1, d2) -> Double.compare(d1, d2);

And our version of ComparatorăPointą can be reassembled from these two smaller functions:

Comparator<Point> compareByX = (p1, p2) -> keyComparer.compare(

keyExtractor.apply(p1), keyExtractor.apply(p2));

Ë

This matches the form of Ê but represents an important improvement (one that would be much more signi cant in a larger example): you could plug in any keyComparerorkeyExtractorthat had previously been de ned. After all, that was the whole purpose of seeking to parameterize the larger function on its smaller com-ponents.

But although recasting theComparatorin this way has improved its structure, we have lost the conciseness ofÊ. We can recover that in the special but very common case wherekeyComparerexpresses the natural ordering on the extracted keys. Then Ë can be rewritten as:

Comparator<Point> compareByX = (p1, p2) ->

keyExtractor.apply(p1).compareTo(keyExtractor.apply(p2));

Ì

And, noticing the importance of this special case, the platform library designers added a static methodcomparingto the interfaceComparator; given a key extractor, it cre-ates the correspondingComparator5using natural ordering on the keys. Here is its method declaration, in which generic type parameters have been simpli ed for this explanation:

public static <T,U extends Comparable<U>>

Comparator<T> comparing(Function<T,U> keyExtractor) {

5Other overloads ofcomparingcan createComparators for primitive types in the same way, but since natural ordering can’t be used, they instead use thecomparemethods exposed by the wrapper classes.

“Oracle_LaTeX/Mastering Lambdas/ Naftalin/ 182962-8” — 2014/8/27 — 16:22

Chapter 1: Taking Java to the Next Level

17

return (c1, c2) ->

keyExtractor.apply(c1).compareTo(keyExtractor.apply(c2));

}

Using that method allows us to write the following (assuming a static import dec-laration ofComparators.comparing) instead ofÌ:

Comparator<Point> compareByX = comparing(p -> p.getX());

Í

Compared toÊ, Í is a big improvement: more concise and more immediately understandable because it isolates and lays emphasis on the important element, the key extractor, in a way that is possible only because comparing accepts a simple behavior and uses it to build a more complex one from it.

To see the improvement in action, imagine that our problem changes slightly so that instead of nding the single point that is furthest from the origin, we decide to print all the points in ascending order of their distance. It is straightforward to capture the necessary ordering:

Comparator<Point> byDistance = comparing(p -> p.distance(0, 0));

And to implement the changed problem speci cation, the stream pipeline needs only a small corresponding change:

intList.stream()

.map(i -> new Point(i % 3, i / 3))

.sorted(comparing(p -> p.distance(0, 0)))

.forEach(p -> System.out.printf("(%f, %f)", p.getX(), p.getY()));

The change needed to accommodate the new problem statement illustrates some of the advantages that lambdas will bring. Changing theComparatorwas straightfor-ward because it is being created by composition and we needed to specify only the single component being changed. The use of the new comparator ts smoothly with the existing stream operations, and the new code is again close to the problem state-ment, with a clear correspondence between the changed part of the problem and the changed part of the code.

1.5 Conclusion

It should be clear by now why the introduction of lambda expressions has been so keenly awaited. In the earlier sections of this chapter we saw the possibilities they will create for performance improvement, by allowing library developers to enable auto-matic parallelization. Although this improvement will not be universally available—

one purpose of this book is to help you to understand exactly when your application will bene t from “going parallel”—it represents a major step in the right direction,

“Oracle_LaTeX/Mastering Lambdas/ Naftalin/ 182962-8” — 2014/8/27 — 16:22

18

Mastering Lambdas

of starting to make the improved performance of modern hardware accessible to the application programmer.

In the last section, we saw how lambdas will encourage the writing of better APIs.

The signature ofComparator.comparing is a sign of things to come: as client pro-grammers become comfortable with supplying behaviors like the key extraction func-tion that comparingaccepts, ne-grained library methods likecomparingwill be-come the norm and, with them, corresponding improvements in the style and ease of client coding.

In document CHAPTER. JavaFX Fundamentals (Page 171-174)