With its introduction of lambdas and the stream API, Java 8 is a very different language from its predecessors. After working on a project with Java 8, I find I use the streaming API and functional interfaces constantly.
For anyone accustomed to functional programming, Java 8 doesn't introduce anything new. Rather, it is the syntax which requires some attention.
Let's take a look at a few basic examples of what makes Java 8 so interesting. For the code, see here.
First, let's start with a List
of integers.
List<Integer> oneToFive = Arrays.asList(1, 2, 3, 4, 5);
If we wanted to map the collection and multiply each element by two, first we get a stream from the list, then we map each element passing in a lambda to describe the mapping, and finally we collect the result into a new list.
List<Integer> multipliedByTwo = oneToFive.stream()
.map((num) -> {
return num * 2;
})
.collect(toList());
System.out.println("multipliedByTwo = " + multipliedByTwo);
// multipliedByTwo = [2, 4, 6, 8, 10]
For many operations in Java 8, a common pattern is to first call stream()
, perform some transformation, and finally call collect()
with the appropriate collector. For our purposes here, let's stick to Collectors.toList()
. See the docs for more on Collectors
. There are quite a few options with the collect()
method worth considering which we won't discuss here.
Aside from the setup of stream-transform-collect, the other detail worth noting above is the lambda syntax -- a parenthetical list of arguments, an arrow to indicate the body of the lambda, and finally a pair of brackets with the actual lambda's code. Note that when a lambda's body consists of only one line, there is a shorter syntax.
Let's look at another example to see this shorter syntax. If a mapping returns the new value, then filtering returns a boolean value, which determines if the element will become part of the new collection. Let's say we want to filter our list of integers to be only even integers.
List<Integer> evensOnly = oneToFive.stream()
.filter((num) -> num % 2 == 0)
.collect(toList());
System.out.println("evensOnly = " + evensOnly);
// evensOnly = [2, 4]
The setup is just like that above with a stream-transform-collect. This time, we pass a lambda which returns a boolean value to the filter
method. Note the absence of the brackets. When a lambda is only one line, one may omit the brackets as well as the semicolon terminating the statement.
After map
and filter
, one might easily imagine what reduce
looks like. The reduce
function has two important differences. First, let's take a look at the code:
Optional<Integer> sum = oneToFive.stream()
.reduce((num, acc) -> num + acc);
System.out.println("sum = " + sum.get());
// 15
The first obvious difference is that we're not calling collect
. And, after thinking about it, we'll find that this makes sense. After all, reduce is returing a single value, rather than a collection of values. Hence, there is no need for collect
. The other difference, and a substantial one, is the return value of reduce
.
Rather than giving us a simple Integer
object, reduce
returns an Optional
. It's worth looking at the docs carefully for Optional
as it can be a powerful tool to replace painful checks for null
.
The reason reduce
returns an Optional
in this case is that reduce
has no way of knowing beforehand if the collection is empty. Look what happens when we call reduce
on an empty collection:
List<Integer> noNumbers = asList();
Optional<Integer> sumOfNothing = noNumbers.stream()
.reduce((num, acc) -> num + acc);
System.out.println("sumOfNothing = " + sumOfNothing);
// Optional.empty
There is no integer value that reduce
can return. And so reduce
always returns an Optional
to cover for the case when there is no value present. And, by returning Optional
, we have an object on which we can call additional methods (like isPresent
) whereas null
would mean additional and tedious checks.
We can use the orElse
method on Optional
to guarantee a value for our integer. For example:
Optional<Integer> empty = Optional.empty();
Integer result = empty.orElse(0);
System.out.println("result = " + result);
// result = 0
If empty
holds a value, it will return it as part of the orElse
call. Otherwise, if empty
is in fact empty (it is here), we get the value passed to orElse
.
When dealing with reduce
though, we have another option. We can pass an initial value for the accumulator to reduce
. By passing the initial value, we can be certain that reduce
will return an Integer
and so we do not need to deal with Optional
objects.
Integer knownSum = oneToFive.stream()
.reduce(0, (num, acc) -> num + acc);
System.out.println("knownSum = " + knownSum);
// 15
With just the few examples above, we have not even begun to cover the full span of Java 8's streaming API and its new functional programming features. For more, there is this tutorial. And, of course, more details can be found in the Java docs. See for instance aggregate operations or the stream API. Finally, the best resource I've found for learning more about these exciting changes is Functional Programming in Java.
Given all these changes, if you have an unfavorable opinion of Java, I think it's time to reevaluate the language.