Java 8 and beyond, The Whistle-Stop Tour

So, what have I missed?

Java code on a computer screen



Changes to the Java release cycle

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: Lambda Expressions, Method References, Streams, Optionals, Default and Static Interface Methods, Java Date Time API

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 =
.filter(s -> !s.isEmpty())

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 = -> {
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.

.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:

.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() {

This can then be implemented in the below vehicle class

public class Car implements Vehicle {

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.

Java 9: Factory Methods for Collections, Private Methods in Interfaces, The Module System, Reactive Streams

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) {

default void printString(String s) {

private void print(String 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.

|- 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 file in order to specify exactly what we make available from Project A.

Java 10: Local Variable Types

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!

Java 11: String Methods

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.

Java 12: Switch Expressions (Preview)

private enum Weekday {

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:
resultString = "It's not Friday.";
case FRIDAY:
resultString = "It's Friday!";
resultString = "This shouldn't happen...";

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.

Java 13: Text Blocks (Preview)

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

Java 14: Records (Preview)

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) { }

Java 15: Sealed Classes

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.


Senior Software Engineer at the BBC