Java 8 and beyond, The Whistle-Stop Tour

So, what have I missed?

Java code on a computer screen
Java code on a computer screen

Audience

This article is aimed at developers with a basic understanding of Java but who are looking to brush up on the changes from version 8 onwards. It isn’t intended as an in-depth guide, but more as a summary to remind you of the more exciting new features.

Argument

We will start with the changes to the Java release cycle, then move on to the key additions to each new version.

Previously Java would leave years between versions, finessing all of the features it hoped to introduce before doing a big bang release. Since Java 8, this has been eschewed in favour of smaller releases with fewer new features. However, these smaller releases also come with shorter support lifespans of six months.

In versions 8–17 there are three long term support (LTS) versions — 8, 11 and 17 (due in September 2021). In a professional context it is best to use these versions (due to the extended support). Having said that, it doesn’t stop us investigating the new features introduced by the others!

Java 8 was a big step from 7 and introduced lots of exciting new things. The first one we will cover is Lambda Expressions. A Lambda expression is a small, anonymous piece of functionality that can be passed around as a variable.

Supplier<String> stringSupplier = () -> "Here's a string";
String string = stringSupplier.get();

In the below we assign the Lambda to the stringSupplier variable, then execute it using the get method. We won’t dive too much further into how they work, as this is well beyond the scope of this overview article. Just try to conceptually understand what they are for!

Method References are a feature that allows us to simplify some of our Lambda code, and are characterised by ::. They are a shorthand way of referring to methods on an object.

Function<Integer, Long> longValueOfInt = Long::valueOf;

This is equivalent to:

Function<Integer, Long> longValueOfInt = (i) -> Long.valueOf(i);

Another addition was the Stream API, which allowed us to do filter , map , and reduce style operations on streams of data.

final List<String> strings = List.of("A", "B", "C", "D", "");String hyphenatedString = strings.stream()
.filter(s -> !s.isEmpty())
.collect(Collectors.joining("-");

In the above we create a list of strings (using a new Java 9 method we will cover below), convert it to a stream, filter the empty strings and then collect it all into a value “A-B-C-D”.

Some cool features of the Stream API include the fact that when we create a stream it doesn’t mutate the original object, meaning we would be able to reuse the list of strings above for another stream.

Additionally, there are lots of methods of stream creation.

  • Stream.generate(() -> "Item").limit(10); generates a stream of ten items, each an “Item” string.
  • Stream.iterate(0, n -> n + 1).limit(10); generates a stream from 0 to 9.
  • IntStream.range(0, 10); generates a primitive stream from 0 to 9, we cannot call object methods on the elements.

Another interesting factoid about streams is that they are lazily invoked. We can think of streams having three components:

  1. Source: This is what generates the stream
  2. Intermediate Operations: These take a stream and act on the elements (like filter above).
  3. Terminal Operations: This converts back from a stream.

None of the intermediate operations are executed until the terminal operation is called! Therefore if we have the below:

Stream<String> nonExecuted = strings.stream().filter(s -> {
System.out.println("Filtering!");
return !s.isEmpty();
});
String commaSeparatedString = nonExecuted.collect(Collectors.joining(", "));

Nothing is printed until we call the collect function (the terminal operation).

Of course, there are lots of other fascinating things about streams, but these are my main highlights.

In addition we have Optionals. These are helpful in navigating what were previously opportunities to receive null pointer exceptions. Optionals can be considered little boxes which may, or may not, contain objects.

  • Optional.of("Value") is the same as a box containing the string ‘Value’.
  • Optional.empty() is the same as an empty box.

In the same way we call methods on streams we can call methods on Optionals.

Optional.ofNullable("Value")
.ifPresent((a) -> System.out.println(String.format("This will print %s", a)));

This code will print “This will print Value”. However the below code will print nothing:

Optional.ofNullable(null)
.ifPresent((a) -> System.out.println("This won't print"));

By implementing Optionals we can better control the flow of the program. There is no idea of null, only empty or populated, therefore we circumnavigate the potential for NPEs.

Default and static methods in interfaces were also introduced. Default methods are useful if we have multiple classes that all implement the same interface and wish to share a method implementation.

interface Vehicle {

int getNumberWheels();

default void printWheels() {
System.out.println(getNumberWheels());
}
}

This can then be implemented in the below vehicle class

public class Car implements Vehicle {

@Override
public int getNumberWheels() {
return 4;
}
}

When we call Car.printWheels we will print “4” to the standard output.

Static methods were introduced to circumvent the need for utility classes.

public class UtilityClass {
static int add(int x, int y) {
return x + y;
}
}

Previously we may have something similar to the above. This would mean we could unnecessarily create instances of the object using new UtilityClass(). With the introduction of static methods in interfaces we can replace this with:

public interface UtilityInterface {
static int add(int x, int y) {
return x + y;
}
}

Where no such instantiation is possible, and we can use UtilityInterface.add directly.

Finally, there were some additions to the Java Date/Time API. These simplified interactions with dates and times through the use of LocalDate, LocalTime, LocalDateTime and ZonedDateTime. The first three are used to represent dates, times and datetimes from the context of the system clock. The final class is used when we want to consider a certain timezone, rather than just the one set on the machine.

Although Factory Methods for Collections may sound a bit dry, if you’ve programmed in pre-Java 9 you’ll know creating lists, sets and maps can be a bit of a headache. In this version we can use methods such as

List<String> list = List.of("A", "B", "C");
Set<String> set = Set.of("One", "Two", "Three");

And in order to create a map we can use the below:

Map<String, String> map = Map.of("Key1", "Value1", "Key2", "Value2", "Key3", "Value3");

Which will create a self-explanatory map of key-value pairs.

Private methods in interfaces are a continuation of the interface improvements from Java 8. If we have code we would like to share between several default methods in an interface we may hide it in a private method.

public interface InterfaceWithPrivateMethods {
default void printInt(int x) {
print(Integer.toString(x));
}

default void printString(String s) {
print(s);
}

private void print(String s) {
System.out.println(s);
}
}

The Module System is the feature with the most hype. This is a way of hiding internal APIs within a package. Let’s say we have two projects we are working on, A and B.

A
|- Package A1
|- Package A2
B (uses A as a dependency)
|- Package B1

Within Project A, Package A1 uses classes from Package A2. This means they must be declared as public. However, we don’t want Project B to be able to use these A2 classes.

Previously, we would not have had a way of limiting Project B’s access to these classes. However, now we can utilise a module-info.java file in order to specify exactly what we make available from Project A.

Java 10 introduced Local Variable Types with the var reserved type name. For Scala programmers this may be familiar! We use var when we want to infer a variable’s type.

var message = "Cool new feature alert!";

Although this reduces boilerplate code, we need to be a little bit careful in its use. Mainly this centres around code readability. For example:

var output = function();

Doesn’t tell us a whole lot about what the function is returning, and makes it tricky to use the variable further down the line!

A number of new methods were introduced into the String API. These include the fairly self-explanatory repeat , strip , and isBlank . The most interesting addition is most likely the lines method, which we can see working below:

var string = "This\nis\na\nstring\nwith\nnewlines";

var spaceString = string.lines().collect(Collectors.joining(" "));

Here we turn a string with newline characters into a stream of strings using lines. From there we collect it into a new string, replacing the newlines with spaces.

To introduce Switch Expressions we first need to introduce an enumeration:

private enum Weekday {
MONDAY,
TUESDAY,
WEDNESDAY,
THURSDAY,
FRIDAY
};

Now, let’s say we want to detect when it’s Friday. Previously we may have written something similar to the below:

String resultString;

switch (weekday) {
case MONDAY:
case TUESDAY:
case WEDNESDAY:
case THURSDAY:
resultString = "It's not Friday.";
break;
case FRIDAY:
resultString = "It's Friday!";
break;
default:
resultString = "This shouldn't happen...";
break;
}

Now, with the introduction of the new Java 12 expressions we can express the same idea in a more terse, readable manner.

var secondResultString = switch (weekday) {
case MONDAY, TUESDAY, WEDNESDAY, THURSDAY -> "It's not Friday.";
case FRIDAY -> {
System.out.println("Demonstrating yield.");
yield "It's Friday!";
}
};

Here I’ve added an extra, unnecessary line in order to demonstrate the use of yield. This can be used when we want to add more complex blocks of code, as return can only be used inside switch statements and not inside switch expressions as above.

Text Blocks are admittedly not the most thrilling addition to a programming language ever. Their purpose is to allows for multi-line string literals, whilst providing a number of methods we can call on them.

String textBlock = """
This is a text block!
"""
;

Similar to some of the previous features, Records were introduced in order to reduce boilerplate code. In pre-14 iterations we may have written something like:

final class Coordinates {
public final int x;
public final int y;

public Scale(int x, int y) {
this.x = x;
this.y = y;
}

// Getters, setters etc.
}

However, Records simplify all of the above (with some constraints) to:

record Scale(int x, int y) { }

Sealed classes are a method of restricting who may extend a class or interface.

sealed interface Vehicle permits Car, Van {
...
}

This is useful for a number of reasons. Initially, it prevents programmers inadvertently abusing classes or interfaces. Secondly, it allows us to reason exhaustively in cases such as:

if(vehicle instanceOf Car) {
...
} else if (vehicle instanceOf Van) {
...
}
... we know all cases are covered.

Conclusion

To summarise, we have covered what I believe to be the most interesting additions to Java over the versions 8–15. The next LTS release is due in 2021, so keep an eye out!

Senior Software Engineer at the BBC

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store