With some additional APIs on the Collection classes themselves, a variety of new and more powerful approaches
and techniques open up, most often leveraging techniques drawn from the world of
functional programming. No knowledge of functional programming is necessary to use them,
fortunately, as long you can open your mind to the idea that functions are just
as valuable to manipulate and reuse as are classes and objects.
Comparisons. One of the drawbacks to the Comparator approach shown earlier is hidden inside the Comparator implementation. The code is actually doing two comparisons, one as a “dominant”
comparison
over the other, meaning that last names are compared first, and
age is compared only if the last names
are identical. If project requirements later demand that sorting be done by age
first and by last names second, a new Comparator must be written—no parts of compareLastAndAge can be reused. This is where taking a more functional approach can add
some powerful benefits. If we look at that comparison as entirely
separate Comparator instances, we can combine
them to create the precise kind of comparison needed. Historically, writing the combination by hand has been less productive, because by the time you write
the code to do the combination, it would be just as fast (if not faster) to
write the multistage comparison by hand. As a matter of fact, this “I want to
compare these two X things by comparing values
returned to me by a method on each X” approach is such a common
thing, the platform gave us that functionality out of the box. On the Comparator class, a comparing method takes a function (a
lambda) that extracts a comparison key out of the object and returns a Comparator that sorts based on that. The Person is no longer about sorting,
but just about extracting the key by which the sort should be done. This is a
good thing—Person shouldn’t have to think
about how to sort; Person should just focus on being a
Person. It gets better, though,
particularly when we want to compare based on two or more of those values.
Composition. As of Java 8, the Comparator interface comes with several methods to combine Comparator instances in various ways by stringing them together. For example, the Comparator .thenComparing() method takes a Comparator to use for comparison after the first one compares. So, re-creating the “last name then age” comparison can now be written in terms of the two Comparator instances LAST and AGE. Or, if you prefer to use
methods rather than Comparator instances . By the way, for
those who didn’t
grow up using Collections.sort(), there’s now a sort() method directly on List. This is one of the neat
things about the introduction of interface default methods: where we used to have to put that kind
of noninheritance-based reusable behavior in static methods, now it can be
hoisted up intointerfaces. Similarly, if the code needs to sort the collection
of Person objects by last name and
then by first name, no new Comparator needs to be written, because this comparison can, again, be made
of the two particular atomic comparisons as shown below.
Collections.sort(people,
Comparators.comparing(Person::getLastName)
.thenComparing(Person::getFirstName));
This combinatory “connection” of methods, known as functional composition, is common in functional programming
and at the heart of why functional programming is as powerful as it is. It’s
important to understand that the real benefit here isn’t just in the APIs that
enable us to do comparisons, but the ability to pass bits of executable code
(and then combine them in new and interesting ways) to create opportunities
for reuse and design. Comparator is just the tip of the iceberg. Lots of things can be made more flexible and powerful, particularly when combining and composing them.
No comments:
Post a Comment