Martin Ahrer

Thinking outside the box

Java 9 major language and API improvements

2024-07-16 5 min read Martin

Java ⑨ has been released in September 2017 and is the first release starting a new release cycle with a new release every 6 months.

The following content is summarizing the most important changes to the Java API and the Java language introduced with Java 9.

For more complete overview follow the links in the following sections referring to the official Oracle release documents.

In addition, Nicolai Parlog has a quite comprehensive guide to Java 9 in his "Ultimate Guide To Java 9".

Java 9 Language improvements

The following describes selected improvements of the Java language. See the Java 9 release notes.

Try With Resource

Before Java9
FileReader fileReader = new FileReader("Try.txt");
try (FileReader reader = fileReader) {
  reader.read();
}

Before Java 9 a variable referencing a resource had to be redeclared (and assigned) in the try block in order to be automatically closed.

With Java9
// reader is an effectively final variable
// as it is not assigned after the initial assignment!
FileReader reader = new FileReader("Try.txt");
try (reader) { (1)
  reader.read();
}

Variables that are effectively final can be used in a try with resource block (and don’t have to be redeclared).

Private Interface Method

An interface can now have private methods. These can simplify implementing default methods.

Say Hello
interface Hello {

  default String sayHello(String firstName, String lastName) {
    return formatGreeting(firstName, lastName);
  }

  default String sayGoodMorning(String firstName, String lastName) {
    return formatGreeting(firstName, lastName);
  }

  private String formatGreeting(String firstName, String lastName) {
    return String.format("Hello %s %s", firstName, lastName);
  }
}

Java 9 API improvements

The following describes selected improvements of the Java API. See the Java 9 release notes.

Objects

Methods for convenient null checking have been added to Objects class.

Require Non Null Else
return Objects.requireNonNullElse(arg1, 0) *
    Objects.requireNonNullElse(arg2, 0);
Require Non Null Else Get
return Objects.requireNonNullElse(arg1, 0) *
    Objects.requireNonNullElseGet(arg2, () -> {
      throw new IllegalArgumentException();
    });

Immutable List Factory

Factory methods have been added to the Collections class for simplifying list creation.

Empty list with Java ⑧
List<Number> empty = new ArrayList<>();
List<Number> unmodifiable = Collections
    .unmodifiableList(empty);
Empty list with Java ⑨
List<Number> unmodifiable = List.of();
List with Java ⑧
List<Number> withItems = new ArrayList<>();
List<Number> unmodifiable = Collections
    .unmodifiableList(withItems);

withItems.add(1);
withItems.add(2);
List with Java ⑨
List<Number> unmodifiable = List.of(1, 2);

Immutable Map Entry Factory

Factory methods have been added to the Collections class for simplifying map entry creation.

Immutable Empty Map
Map<Number, String> unmodifiable = Map.ofEntries();
Immutable Map
Map<Number, String> unmodifiable = Map.ofEntries(
    Map.entry(1, "one"),
    Map.entry(2, "two"));

Optional

Optional Or
Path name = Paths.get("file.txt");
Optional<File> file = search
    .onLocalDisk(name)
    .or(() -> search.onNetworkShare(name))
    .or(() -> search.onCloudService(name));
Optional If Present Or Else
file.ifPresentOrElse(
    f -> System.out.format("File at %s", f.getName()),
    () -> System.out.format("File %s not found", name.getFileName()));
Optional Stream
long count = Optional
    .ofNullable(null)
    .stream()
    .count();

Collectors

By applying a filter predicate we can adapt a collector to accept only elements matching a predicate.

Filtering
List<Integer> list = IntStream.of(2, 4, 6, 8, 10, 12)
    .boxed()
    .collect(Collectors.filtering(i -> i % 3 == 0,
        Collectors.toList()));

With the new flatMapping method a stream of streams or stream of collections a collector is adapted to return a flattened result.

Flat Mapping
List<Integer> list = Stream.of(List.of(1, 3, 5), List.of(2, 4, 6))
    .collect(Collectors.flatMapping(
        l -> l.stream()
            .filter(i -> i % 3 == 0),
        Collectors.toList()
    ));

Immutable Set Factory

Factory methods have been added to the Collections class for simplifying set creation.

Empty Set with Java ⑧
Set<Number> empty = new HashSet<>();
Set<Number> unmodifiable = Collections
    .unmodifiableSet(empty);
Empty Set with Java ⑨
Set<Number> unmodifiable = Set.of();
Set with Java ⑧
Set<Number> withItems = new HashSet<>();
Set<Number> unmodifiable = Collections
    .unmodifiableSet(withItems);
withItems.add(1);
withItems.add(2);
Set with Java ⑨
Set<Number> unmodifiable = Set.of(1, 2);

Immutable Map Factory

Factory methods have been added to the Collections class for simplifying map creation.

Empty map with Java ⑧
Map<Number, String> empty = new HashMap<>();
Map<Number, String> unmodifiable = Collections
    .unmodifiableMap(empty);
Empty map with Java ⑨
Map<Number, String> unmodifiable = Map
    .ofEntries();
Map with Java ⑧
Map<Number, String> withItems = new HashMap<>();
Map<Number, String> unmodifiable = Collections
    .unmodifiableMap(withItems);
withItems.put(1, "one");
withItems.put(2, "two");
Map with Java ⑨
Map<Number, String> unmodifiable = Map
    .of(1, "one", 2, "two");

Matcher

With Matcher.results we get a stream of all matched elements

Results
Pattern pattern = Pattern.compile("\\d+");

Matcher matcher = pattern.matcher("11 2 3 4 5");
int result = matcher.results()
    .map(MatchResult::group)
    .mapToInt(Integer::parseInt)
    .sum();

With Matcher.replaceAll we can perform substitution of matches.

Replace All
String WHITE_SPACE = "\\s+";
Pattern pattern = Pattern.compile(WHITE_SPACE);

Matcher matcher = pattern.matcher("snake case notation");
String result = matcher.replaceAll(matchResult -> "_");

Stream

Take While
List<Integer> subset = stream
    .takeWhile(n -> n < 5)
    .collect(Collectors.toList());

We can take (accept) stream elements matching a predicate.

Drop While
List<Integer> subset = stream
    .dropWhile(n -> n < 11)
    .collect(Collectors.toList());

We can drop stream elements matching a predicate.

Iterate
List<Integer> subset = Stream
    .iterate(10, n -> n < 15, n -> n + 1)
    .collect(Collectors.toList());

Generate a stream using an iteration.

Stream Of Nullable Java8
Collection<Integer> collection = null;

List<Integer> list = Optional
    .ofNullable(collection)
    .stream()
    .flatMap(Collection::stream)
    .collect(Collectors.toList());

With Java ⑧ and before we have to use a conditional statement of Optional.ofNullable to work with potentially null collections.

Stream Of Nullable Java9
Collection<Integer> collection = null;

List<Integer> list = Stream
    .ofNullable(collection)
    .flatMap(Collection::stream)
    .collect(Collectors.toList());

With Stream.ofNullable we can work with a stream that is potentially null and avoid testing using a conditional statement or Optional.ofNullable.