About two years ago, we upgraded to Java 8. Over this time, I’ve discovered many things in the new version that I’ve come to appreciate. Some of the Java 8 features may seem minor, but I find myself using nearly every day. Going deep and learning these new features have made me more efficient and my code easier to understand and follow.
Let’s look at a few of my favorite features in Java 8.
Lambdas
The first thing most people hear about from Java 8 is Lambdas, and for good reason. If nothing else, lambdas make your code much more compact and easier to read. If you’ve been programming Java for any length of time, you’re probably familiar with anonymous inner classes. While not exactly the same under the covers, lambdas are effectively a way to write these anonymous inner classes in a much more compact and concise manner.
For example, prior to Java 8, you might have written something like this:
1 | someComponent.addChangeListener(new FooChangeListener() { |
Here, an anonymous implementation of FooChangeListener
is passed to some component. FooChangeListener
is what is known as a functional interface. This means that it is an interface with a single method. In Java 8, a functional interface is marked as such by adding the @FunctionalInterface
annotation. Not only does this annotation provide some level of documentation, the Java compiler uses this annotation to verify that no additional methods sneak into the interface definition.
Anywhere you would normally pass a functional interface, a lambda expression can be used instead. This replaces all the boiler plate code and make things much more concise.
Here is the same code as above, implemented using a lambda expression.
1 | someComponent.addChangeListener((event) -> processEvent(event)); |
The part to the left of the arrow (->
) are the arguments required by the interface’s method. Java is usually able to infer the types from the method definition, so you don’t have to include the type (FooEvent
, in this example). The part to the right of the arrow is the method body. If the method only has a single expression, as in this example, no curly braces are necessary. But if you need a more complex implementation, you can certainly enclose the lambda body in curly braces.
In this case, we’re just passing the lambda method parameters to another method, so we can simplify the code even further by using a lambda shorthand called a method reference:
1 | someComponent.addChangeListener(this::processEvent); |
Lambdas are a great way to make your code much more concise, removing much of the redundant, boilerplate code. This leaves only the meat of the functionality, making things much easier to read.
Streams
Lambdas are useful constructs all by themselves, but when used in combination with streams, things get very interesting. Streams are a way of declaring a set of operations to perform on a sequence of elements. These operations are done as lazily as possible, meaning that the fewest number of operations are executed and as late as possible.
What the heck does all that mean?
Let’s use an example to make this more concrete. Suppose your system has a Person class with these attributes:
- First Name
- Last Name
- Gender
- Age
- Nationality
Now suppose you have a list of these objects and you’d like to convert this list into a list of Strings. Each element of the resulting list should contain the first and last name. The list should be sorted by last name and only include people over 21 years of age from the USA. Prior to Java 8, this might have been written like this:
1 | List<String> nameList = new ArrayList<>(personList.size()); |
First we sort this list by last name (assume PersonLastNameComparator
implements the Comparator
interface and compares the last names of two Person
objects.) Then, for each person, we have to check their age and nationality, build the string and add the result to a new list. Fairly standard procedural code.
Let’s see how Streams help us:
1 | personList.stream() |
One of the first things that might stand out in this version, is that it’s much more declarative than the original. While the order is still important, it’s more about what to do less about how to do it.
First, we generate a Stream from the List by calling the stream()
method. The first two operations are filters. This means that we’re declaring which items we want to include in the stream and which should be excluded. Each takes a functional interface: Predicate
. This takes an object and returns either true or false.
Next we sort the list using a Java 8 comparator (more on this later) and then build up the full name string with the map operation. The map operation takes another one of the built-in functional interfaces: Function
. This takes one value and produces another. That is, we’re mapping one value to another.
The final operation is to collect the results into a new List
. Up to this point, everything we’ve defined has not yet occurred. All of the calls to map, filter and sorted define a recipe for what should be done, but nothing has been executed yet. The collect operation is an example of a terminal operation. Calling a terminal operation executes the recipe and produces a final result. In this case, the final result is a List
of the filtered stream values. Other terminal operations include:
forEach
— Perform an operation on each element of the streamreduce
— Starts with an initial value and combines the elements of the stream into this initial value. For example, adding a stream of numbers together.collect
— The javadoc for this operation calls this a multiple reduction operation. But in practice, it usually is used to combine the elements of the stream into a collection (List, Map, Set, etc.)min
&max
— Find the minimum or maximum value, respectively in the stream.count
— Determines how many elements are in the stream, after filtering.findFirst
— Get the first element of the streamfindAny
— Any element of the stream will do.
Streams are a very powerful mechanism for processing data. This is a fairly simple example. More complex usages can group data by a key value (i.e. creating a Map), reduce a set of values down into a single number or other complex object, and more.
Once you start using streams for the simple stuff, you’ll want to start using them for everything!
Optional
In Java, as in other languages, null values are the bane of many a developer’s existence. It doesn’t take long for beginner Java developers to see their first NPE (NullPointerException). Dealing with null has long been a source of frustration and bugs in software, not just Java. In fact Tony Hoare has publicly apologized for inventing the null reference.
At this point, it would be all but impossible to remove the concept of null from the language. However, Java 8 attempts to make amends by defining a new object that can be used to represent the presence or absence of an object. Optional
is a wrapper for any object, null or not, which provides methods to work with the underlying value if the value is present. Combined with lambdas, Optional
becomes a powerful way to replace a lot of if/else logic with a simple, declarative description of what to do with a value if it’s present (or not)
Using our Person example again, suppose you’ve looked up a Person by name and you want to return a String containing the combination of first and last name. If nobody was found return “
1 | String nameResult = Optional.ofNullable(findPerson(query)) |
If you squint, this looks a lot like the earlier Stream example. Optional
has a few of the same operations as Stream, namely map and filter. These operations allow you to define what should happen only if the Optional
has a value (i.e. Optional.isPresent()
returns “true”).
Map Enhancements
How often have you written something like the following?
1 | Person value = someMap.get(key); |
With Java 8, the Map
interface received some love and added several useful methods that take advantage of the lambda support. Now you can reduce the above code to the following:
1 | someMap.computeIfAbsent(key, key -> new Person()) |
This uses a Function
lambda to generate a new value to place into the map if it isn’t already present. This greatly simplifies the code, making it easier to read and follow. The Map
interface has a few other small, but extremely useful enhancements:
getOrDefault
– No more checking for null and returning a default value, simply provide the default value and the value will be returned if the key isn’t in the map.replaceAll
– Provide afFunction, and theMap
implementation will call that function to obtain a replacement value for every entry in the mapmerge
– If a key exists in the map, run a function to determine how to combine the existing value and a new value. Combined with streams, this allows you to combine the contents of two Map instances relatively easily:
1 | map1.forEach(entry -> map2.merge( |
Comparator Enhancements
Last, but not least, one of my favorite Java 8 features is the ability to create Comparator
s using lambda functions. Prior to Java 8, you had to create implementations of Comparator
, anonymous or otherwise, to pass into things like Collections.sort
, or TreeMap
. Writing these implementations can be error prone, requiring extra testing effort. Do I return -1, 0 or 1? What happens if I want to reverse the order? What about nulls?
Java 8 makes this a lot easier. The Comparator
interface provides static methods (another new feature of Java 8) that create new Comparator
instances that use a Function
to determine what value to compare in your objects. These comparators can be easily strung together to define complex sort rules in an easy to read manner. That is, with the new Comparator
methods, you define what values to extract from your objects, and how to combine the comparisons.
For example, suppose you want to sort a list of people by age, oldest to youngest, with a secondary sort based on last and first name. With Java 8, this can be expressed as follows:
1 | Collections.sort( |
Notice that I don’t have a complicated anonymous inner class with lots of if conditions returning magic numbers. This code just declares that I want to sort by age, reversed, and then compare last name, followed by first name.
Conclusion
Java 8 has many enhancements, but the examples I’ve provided are my current favorites. They’re the features I find myself using every day. As with anything in software development, it’s worth taking time to learn the nuances of the tools and languages you use to write your applications. Hopefully you’ll see the utility of these Java 8 features and start to incorporate them into your own code.
Discussion Question****: What are your favorite features introduced in Java 8?