loading...
Cover image for 20 Reasons to Move On from Java 8

20 Reasons to Move On from Java 8

awwsmm profile image Andrew (he/him) Updated on ・31 min read

"Those who don’t want to supply what life demands suffer the consequences. If you don’t want to change, you are left in the back. If you don’t want to change, you are kicked out of the technological age. If you don’t like to change, you become obsolete. You are not able to live in a fast-changing world. Life demands change."

-- Pastor Sunday Adelaja


Updates 25 Nov 2019: fixed errors in Oracle Java 8 support, Graal support in Java 8, and added warnings for preview features.

From the very first beta release of Java in 1995 until the end of 2006, new versions of the language appeared at roughly two-year intervals. But after Java 6 was released in late 2006, developers had to wait nearly five years for a new version of the language. Java 8 and 9 were similarly sluggish out of the gate, with a nearly-three and a three-and-a-half year wait, respectively for each release.

Java versions 7, 8, and 9 all introduced huge changes to the Java ecosystem in the API, the garbage collector, the compiler, and more. Mark Reinhold, Chief Architect of Java at Oracle, has committed to a 6-month rapid release cycle for future versions of Java, with long-term support (LTS) versions interspersed, and so far his team has made good on that promise -- versions 10, 11, 12, and 13 have been released (on average) every 6 months, 13.5 days (what's a fortnight among friends?).

The most recent LTS version is Java 11, released September 2018, which will have free public updates until the next LTS version is released (Sep 2021), with extended support until September 2026. Reinhold's new vision sees an LTS version of Java every three years (every six versions), so the next LTS release will be Java SE 17, tentatively scheduled for September 2021. This allows a long supported transition period from the old LTS version to the new LTS version.

Note that there are subtle but significant differences between the Oracle JDK, OpenJDK, and AdoptOpenJDK release dates and support cycles, which I will cover in a later article.

Although Java developers aren't as bad as Python users -- a significant chunk of whom were still coding with an 8-year-old *retired* version of Python last year, we've so far been slow to move on from Java 8, even though Java 9 has been around for over two years now. There are plenty of good reasons why you should consider moving from whatever version of Java you're using now to (at least) Java 11, including...


Table of Contents

  1. Oracle no longer provides free support for Java 8
  2. jshell, the Java REPL (Java 9)
  3. modules and linking (Java 9)
  4. improved Javadoc (Java 9)
  5. Collection immutable factory methods (Java 9)
  6. Stream improvements (Java 9)
  7. multi-release jars (Java 9)
  8. private interface methods (Java 9)
  9. GraalVM, a new Java Virtual Machine (Java 9/10)
  10. local variable type inference (Java 10)
  11. unmodifiable Collection enhancements (Java 10)
  12. container awareness (Java 10)
  13. single source file launch (Java 11)
  14. switch expressions -- a step toward pattern matching (Java 12)
  15. teeing Collectors (Java 12)
  16. multiline text blocks (Java 13 (preview))
  17. Java-on-Java compiler with Project Metropolis (Java 14+)
  18. flow typing, anonymous variables, data classes, and sealed types in Project Amber (Java 14+)
  19. coroutines, tail-call optimisation, and lightweight user-mode Fibers with Project Loom (Java 14+)
  20. value types, generic specialisations, and reified generics in Project Valhalla (Java 14+)

#1. Oracle no longer provides free support for Java 8

Although AdoptOpenJDK will provide free public updates for their version of Java 8 until at least September 2023, Oracle has already dropped free support for their JDK. "Extended support" will be provided until March 2025, and can be availed of by purchasing an Oracle Java subscription.

Learn about some of the differences between Oracle's JDK, OpenJDK, and AdoptOpenJDK here.

[ back to Table of Contents ]

#2. jshell, the Java REPL (Java 9)

A Read-Evaluate-Print Loop (REPL) has become an almost-mandatory feature for modern programming languages. Python has one, Ruby has one, and -- as of JDK 9 -- Java has one. The Java REPL, jshell, is a great way to try out some small pieces of code and get feedback in real-time:

$ jshell
|  Welcome to JShell -- Version 11.0.2
|  For an introduction type: /help intro

jshell> var x = List.of(1, 2, 3, 4, 5)
x ==> [1, 2, 3, 4, 5]

jshell> x.stream().map(e -> (e + " squared is " + e*e)).forEach(System.out::println)
1 squared is 1
2 squared is 4
3 squared is 9
4 squared is 16
5 squared is 25

[ back to Table of Contents ]

#3. modules and linking (Java 9)

Java 9 also introduces modules, which are a level of organisation above packages. If you have Java 9+ installed, you're already using modules, even if you don't know it. You can check which modules are available with the command:

$ java --list-modules
java.base@11.0.2
java.compiler@11.0.2
java.datatransfer@11.0.2
...
jdk.unsupported.desktop@11.0.2
jdk.xml.dom@11.0.2
jdk.zipfs@11.0.2

Modules have several benefits, including reducing the size of Java applications when coupled with the new Java linker (by including only the modules and submodules needed for that application), allowing for private packages (encapsulated within the module), and fast failure (if your application is dependent on a module and that module is unavailable on your system). This prevents runtime errors from occurring when a bit of code tries to access a method defined in a package which isn't present on your system.

There are lots of good tutorials available online which explain how to construct and configure modules. Go forth and modularise!

[ back to Table of Contents ]

#4. improved Javadoc (Java 9)

Starting with Java 9, Google searches like "java string class" are a thing of the past. The new-and-improved Javadoc is searchable, HTML5 compliant, and compatible with the new module hierarchy. Check out the difference between JDK 8 and JDK 9 Javadocs here.

Make searching Oracle's Javadoc easier than ever with this little Chrome plugin I made to search multiple versions of the API directly from the omnibox.

[ back to Table of Contents ]

#5. Collection immutable factory methods (Java 9)

Prior to Java 9, the easiest way to quickly make a small, unmodifiable Set of predefined values was something like:

Set<String> seasons = new HashSet<>();

seasons.add("winter");
seasons.add("spring");
seasons.add("summer");
seasons.add("fall");

seasons = Collections.unmofidiableSet(seasons);

A truly ridiculous amount of code for such a small task. Java 9 simplifies that notation considerably by introducing immutable factory methods Map.of(), List.of() (see above), and Set.of():

Set<String> seasons = Set.of("winter", "spring", "summer", "fall")

One caveat is that Collections created in this way cannot contain null values -- that includes values in Map entries.

Overall, this is a welcome change that helps (a bit) to push back against Java's reputation as an unnecessarily verbose language, and reduces the mental overhead required to complete minor tasks like creating a small Set of predefined values.

[ back to Table of Contents ]

#6. Stream improvements (Java 9)

The Stream API has provided (arguably) the largest increase in Java's scope as a programming language over the past decade. Streams brought functional programming to Java more-or-less singlehandedly, turning many for loops into map() pipelines.

Java 9 brings some small improvements to Stream, the iterate(), takeWhile(), dropWhile(), and ofNullable() methods.

Stream.iterate() allows us to recursively apply a function to a stream of Objects (beginning with some seed Object), and choose when to stop returning values. It's basically Java 8's Stream.iterate() plus Java 8's Stream.filter():

jshell> Stream.iterate("hey", x -> x.length() < 7, x -> x + "y").forEach(System.out::println)
hey
heyy
heyyy
heyyyy

The new takeWhile() and dropWhile() methods essentially apply a filter to a Stream, accepting or rejecting, respectively, all values until some condition is met:

jshell> IntStream.range(0, 5).takeWhile(i -> i < 3).forEach(System.out::println)
0
1
2

jshell> IntStream.range(0, 5).dropWhile(i -> i < 3).forEach(System.out::println)
3
4

Java 9 also brought Optional and Stream closer together, providing the Stream.ofNullable() and Optional.stream() methods, which make working with Streams and Optional values a breeze:

jshell> Optional.ofNullable(null).stream().forEach(System.out::println)

jshell> Optional.of(1).stream().forEach(System.out::println)
1

jshell> Stream.ofNullable(null).forEach(System.out::println)

jshell> Stream.ofNullable(1).forEach(System.out::println)
1

Java doesn't yet have a fully-fledged list comprehension interface, so developers have worked themselves into knots trying to come up with their own solutions. Hopefully the next LTS version of Java will include true list comprehensions, like those that appear in Haskell:

List.comprehension(
  Stream<T> input,
  Predicate<? super T>... filters)
jshell> // NOT ACTUAL JAVA CODE
jshell> List.comprehension(
   ...>   Stream.iterate(1, i -> ++i), // 1, 2, 3, 4, ...
   ...>   i -> i%2 == 0,               // 2, 4, 6, 8, 10, ...
   ...>   i -> i > 7,                  // 8, 10, 12, 14, ...
   ...>   i -> i < 13                  // 8, 10, 12
   ...> ).take(3).forEach(System.out::println)
8
10
12

Java already provides infinite Streams through the iterate() method (just use the Java 8 iterate() with no filter), but as soon as a single element fails the filter, the Stream is truncated. In order for true list comprehensions to be implemented, elements failing the filter need to just be removed from the output, without terminating the Stream.

We can dream!

[ back to Table of Contents ]

#7. multi-release jars (Java 9)

Another exciting feature of Java 9 is the ability to create multi-release jar files. What this means, in a nutshell, is that your packages (or modules) can contain specific implementations targeted at each Java version 9 and above. So you can have a specific version of a class which is loaded for Java 9, if it's installed on the client machine.

All you have to do is specify Multi-Release: true in the META-INF/MANIFEST.MF file of the jar, then include the different class versions in the META-INF/versions directory:

jar root /
  - Foo.class
  - Bar.class
  - META-INF
     - MANIFEST.MF
     - versions
        - 9
           - Foo.class
        - 10
           - Foo.class

In the example above, if this jar is being used on a machine with Java 10 installed, then Foo.class refers to /META-INF/versions/10/Foo.class. Otherwise, if Java 9 is available, then Foo.class refers to /META-INF/versions/9/Foo.class. If neither of these Java versions are installed on the target machine, then the default /Foo.class is used.

If you're stuck using Java 8 for now, but want to be prepared for a future switch to a newer version, multi-release jars are the way to go. You have nothing to lose by implementing them!

[ back to Table of Contents ]

#8. private interface methods (Java 9)

Java 8 introduced default methods in interfaces, which were a boon for DRY (don't repeat yourself) software development. No longer did you need to re-define the same methods across multiple implementations of a single interface. Instead, you could define the method with a default body in the interface, which would be inherited by any classes which implement that interface.

interface MyInterface {

  default void printSquared (int n) {
    System.out.println(n + " squared is " + n*n);
  }

  default void printCubed (int n) {
    System.out.println(n + " cubed is " + n*n*n);
  }
}

public class MyImplementation implements MyInterface { }

We could use this class in the jshell like:

jshell> var x = new MyImplementation()
x ==> MyImplementation@39c0f4a

jshell> x.printSquared(3)
3 squared is 9

jshell> x.printCubed(3)
3 cubed is 27

Java 9 further improves interfaces by allowing private methods within them. This means that we can further increase code reuse, particularly between these default method implementations, without the user being able to access these "helper" methods:

interface MyInterface {

  private void printHelper (String verb, int n, int pow) {
    System.out.printf("%d %s is %d%n", n, verb, (int) Math.pow(n, pow));
  }

  default void printSquared (int n) {
    printHelper("squared", n, 2);
  }

  default void printCubed (int n) {
    printHelper("cubed", n, 3);
  }
}

public class MyImplementation implements MyInterface { }

We use the methods in this re-implementation of MyInterface in exactly the same way as we used them above, but the repeated code within the method bodies is now extracted to a small "helper" method. By reducing the amount of copied-and-pasted code, we can make our interface easier to maintain.

[ back to Table of Contents ]

#9. GraalVM, a new Java Virtual Machine (Java 9/10)

GraalVM (pronounced like "crawl" with a hard 'g' instead of a 'c') is a new Java virtual machine and development kit, created by Oracle, based on HotSpot and OpenJDK.

Graal was developed in an attempt to improve the performance of Java applications by trying to match the speed that native (compiled to machine code) languages enjoy. GraalVM differs from other Java Virtual Machines in two main ways:

  1. allows for ahead-of-time (AOT) compilation
  2. supports polyglot programming

As most Java developers know, Java compiles to Java bytecode, which is later read by the Java Virtual Machine and translated into processor-specific code for the user's machine. This two-step compilation is part of the reason for Java's "write once, run anywhere" motto -- a Java programmer doesn't need to worry about specific implementations for specific machine architectures. If it works on her machine, it will work on anyone else's machine, provided the Java Runtime Environment (JRE) is installed there.

Note that this model just pushes the architecture-specific details from the Java programmer to the JVM engineer. Machine-specific code still needs to be written, but it's hidden from the run-of-the-mill Java developer. This is why there are different JDK versions for Windows, Mac, and Linux, of course.

GraalVM combines these two steps to produce machine-native images -- binary code which is created for the particular architecture on which the VM is running. This ahead-of-time compilation from bytecode to machine language means that GraalVM produces binary executables, which can be run immediately without passing through the JVM.

This is not a new concept, as languages like C have always compiled to machine-specific binary code, but it is new for the Java ecosystem. (Or new-ish, as Android Runtime has used AOT compilation since about 2013.) AOT compilation with GraalVM brings about reduced startup times and improved performance over JIT compiled-code.

But the thing that really sets GraalVM apart from any other Java VMs is that Graal is a polyglot VM:

const express = require('express');
const app = express();
app.listen(3000);

app.get('/', function(req, res) {
  var text = 'Hello World!';
  const BigInteger = Java.type('java.math.BigInteger');
  text += BigInteger.valueOf(2).pow(100).toString(16);
  text += Polyglot.eval('R', 'runif(100)')[0];
  res.send(text);
})

Graal provides zero-overhead interoperability between Java, JavaScript, R, Python, Ruby, and C, thanks to the Truffle Language Implementation Framework. Code written in any of those languages can be run in programs written in any of those languages. You can compile a Ruby program that calls Python code, or a Java program that uses C libraries. It really is a huge amount of work that's gone into getting all of these languages to communicate correctly with one another, and the result is almost unbelievable.

[ back to Table of Contents ]

#10. local variable type inference (Java 10)

Java 10 continues the war against boilerplate with the var keyword:

jshell> var x = new ArrayList<Integer>();
x ==> []

jshell> x.add(42)
$2 ==> true

jshell> x
x ==> [42]

The new var type allows for local type inference in Java versions 10 and up. This small new piece of syntax, like the diamond operator before it (<> defined in JDK 7) makes variable definition just a bit less verbose.

Local type inference means that var can only be used inside of method bodies or other similar blocks of code. It can't be used to declare instance variables or as the return type of a method, etc.

Note that the variable x above still has a type -- it's just inferred from context. This means, of course, that we can't assign a non-ArrayList<Integer> value to x:

jshell> x = "String"
|  Error:
|  incompatible types: java.lang.String cannot be converted to java.util.ArrayList<java.lang.Integer>
|  x = "String"
|      ^------^

Even with type inference, Java is still a statically-typed language. Once a variable is declared to be of a particular type, it is always that type. This is different from, for instance, JavaScript, where the type of a variable is dynamic and can change from line to line.

[ back to Table of Contents ]

#11. unmodifiable Collection enhancements (Java 10)

Working with immutable data in Java is notoriously difficult. Primitive values are immutable when declared with final...

jshell> public class Test { public static final int x = 3; }
|  created class Test

jshell> Test.x
$3 ==> 3

jshell> Test.x = 4
|  Error:
|  cannot assign a value to final variable x
|  Test.x = 4
|  ^----^

...but even something as simple as a final primitive array is not really immutable:

jshell> public class Test { public static final int[] x = new int[]{1, 2, 3}; }
|  replaced class Test

jshell> Test.x[1]
$5 ==> 2

jshell> Test.x[1] = 6
$6 ==> 6

jshell> Test.x[1]
$7 ==> 6

The final keyword, above, means that the object x is immutable, but not necessarily its contents. Immutability, in this case, means that x can only ever refer to a particular memory location, so we can't do something like:

jshell> Test.x = new int[]{8, 9, 0}
|  Error:
|  cannot assign a value to final variable x
|  Test.x = new int[]{8, 9, 0}
|  ^----^

If x were not final, the above code would run just fine (try it yourself!). Of course, this causes all sorts of issues when programmers have an object that they want to be immutable, declare it as final, and go on their merry way. final Objects are not really final at all.

When the Collections class was introducted in Java 7, it brought along a few unmodifiable...() methods, which offer "an unmodifiable view" of particular collections. What this means is that, if you only have access to the unmodifiable view, you do not have access to methods like add(), remove(), set(), put(), and so on. Any method which would modify the object or its contents is effectively hidden from your view. Out of sight, out of mind.

jshell> List<Integer> lint = new ArrayList<>();
lint ==> []

jshell> lint.addAll(List.of(1, 9, 0, 1))
$22 ==> true

jshell> lint
lint ==> [1, 9, 0, 1]

jshell> List<Integer> view = Collections.unmodifiableList(lint);
view ==> [1, 9, 0, 1]

jshell> view.add(8);
|  Exception java.lang.UnsupportedOperationException
|        at Collections$UnmodifiableCollection.add (Collections.java:1058)
|        at (#25:1)

If you retain access to the underlying object, however, you can still modify it. Anyone with access to the unmodifiable view will be able to see your changes:

jshell> lint.addAll(List.of(1, 8, 5, 5))
$26 ==> true

jshell> lint
lint ==> [1, 9, 0, 1, 1, 8, 5, 5]

jshell> view
view ==> [1, 9, 0, 1, 1, 8, 5, 5]

So even "unmodifiable views" can still be modified, kind of.

Following in Java 9's "immutable factory methods" footsteps, Java 10 introduces even more API improvements to make working with immutable data just a bit easier. The first is the new copyOf() methods added to List, Set, and Map. These create truly immutable (shallow) copies of their respective types:

jshell> List<Integer> nope = List.copyOf(lint)
nope ==> [1, 9, 0, 1, 1, 8, 5, 5]

jshell> nope.add(4)
|  Exception java.lang.UnsupportedOperationException
|        at ImmutableCollections.uoe (ImmutableCollections.java:71)
|        at ImmutableCollections$AbstractImmutableCollection.add (ImmutableCollections.java:75)
|        at (#32:1)

jshell> lint.set(3, 9)
$33 ==> 1

jshell> lint
lint ==> [1, 9, 0, 9, 1, 8, 5, 5]

jshell> nope
nope ==> [1, 9, 0, 1, 1, 8, 5, 5]

And there are new toUnmodifiable...() methods in the Collectors class, which also create truly immutable objects:

jshell> var lmod = IntStream.range(1, 6).boxed().collect(Collectors.toList())
lmod ==> [1, 2, 3, 4, 5]

jshell> lmod.add(6)
$38 ==> true

jshell> lmod
lmod ==> [1, 2, 3, 4, 5, 6]

jshell> var lunmod = IntStream.range(1, 6).boxed().collect(Collectors.toUnmodifiableList())
lunmod ==> [1, 2, 3, 4, 5]

jshell> lunmod.add(6)
|  Exception java.lang.UnsupportedOperationException
|        at ImmutableCollections.uoe (ImmutableCollections.java:71)
|        at ImmutableCollections$AbstractImmutableCollection.add (ImmutableCollections.java:75)
|        at (#41:1)

One step at a time, Java is moving toward a more comprehensive model for immutable data.

[ back to Table of Contents ]

#12. container awareness (Java 10)

Between 2006 and 2008, Google engineers added a cool new feature known as cgroups or "control groups" to the Linux kernel. This new feature "limits, accounts for, and isolates the resource usage (CPU, memory, disk I/O, network, etc.) of a collection of processes".

This concept may sound familiar to you if you use tools like Hadoop, Kubernetes, or Docker, which use control groups extensively. Without the ability to limit resources available to particular groups of processes, Docker couldn't exist.

Unfortunately, Java was created well before cgroups were implemented, so Java initially ignored this feature completely.

Starting with Java 10, however, the JVM is aware of when it's being run within a container and respects the resource limits placed on it by that container by default. This feature has also been backported to JDK 8. So if you pick a recent Java 8 version, that JVM will be container aware, as well.

In other words, with the release of Java 10, Docker and Java are finally friends.

[ back to Table of Contents ]

#13. single source file launch (Java 11)

Starting with Java 11, you no longer need to compile a single source file before running it -- java sees the main method within the main class, and will compile and run the code automatically when you call it on the command line:

// Example.java
public class Example {
  public static void main (String[] args) {

    if (args.length < 1)
      System.out.println("Hello!");

    else
      System.out.println("Hello, " + args[0] + "!");

  }
}
$ java Example.java 
Hello!

$ java Example.java Biff
Hello, Biff!

This is a small but helpful change to the java launcher which can make it easier to (among other things) teach those new to Java, without introducing the "ceremony" of explicitly compiling small introductory programs like these.

[ back to Table of Contents ]

#14. switch expressions -- a step toward pattern matching (Java 12)

Don't we already have switch expressions in Java? What's this?

jshell> int x = 2;
x ==> 2

jshell> switch(x) {
   ...>   case 1: System.out.println("one"); break;
   ...>   case 2: System.out.println("two"); break;
   ...>   case 3: System.out.println("three"); break;
   ...> }
two

...well, that's a switch statement, not a switch expression. A statement directs the flow of the program but doesn't evaluate to a value itself. (You can't, for instance, do something like y = switch(x) { ... }.) An expression, on the other hand, evaluates to a result. Expressions can therefore be assigned to variables, returned from functions, and so on.

switch expressions look slightly different from switch statements. An expression similar to the above statement might look like:

String name = switch(x) {
    case 1 -> "one";
    case 2 -> "two";
    case 3 -> "three";
    default -> throw new IllegalArgumentException("I can only count to 3.");
};

System.out.println(name);

You can see a few differences between this and the earlier snippet. First, we use arrows -> instead of colons :. This is, syntactically, how a switch statement is differentiated from a switch expression by the compiler.

Second, there are no breaks in the expression code. There is no "fall-through" with switch expressions like there are with switch statements, so there's no need to break after a case branch.

Third, we must have a default unless the cases listed are exhaustive. That is, the compiler can tell if -- for any possible value of x -- there is a case statement which would catch that value. If not, we must have a default case. As of now, the only real way to exhaust all possible cases without a default is to switch on a boolean or an enum value.

Finally, we can assign the switch expression to a variable! The difference between switch expressions and statements is similar to the difference in Java between the ternary operator ? : and the if else statement:

jshell> int what = 0;
what ==> 0

jshell> boolean flag = false;
flag ==> false

jshell> if (flag) what = 2; else what = 3;

jshell> what
what ==> 3

jshell> what = flag ? 4 : 5
what ==> 5

An if else directs the flow of code, but can't be assigned to a variable (you can't do something like y = if (flag) 3 else 4 in Java) while the ternary operator ? : defines an expression, which can be assigned to a variable.

switch expressions are a step on the path to full pattern matching support in Java. This is being developed through Project Amber, which I'll cover in more detail below. Note that switch expressions are a "preview" feature in Java 12 and 13, but are slated to be upgraded to finalised for Java 14.

[ back to Table of Contents ]

#15. teeing Collectors (Java 12)

Note that DoubleStream does have an average() method. The example below is only for illustrative purposes.

If you've ever tried to perform a complex manipulation of a Stream of values in Java, you know how annoying it can be that Streams can only be iterated over a single time:

jshell> var ints = DoubleStream.of(1, 2, 3, 4, 5)
ints ==> java.util.stream.DoublePipeline$Head@12cdcf4

jshell> var avg = ints.sum()
avg ==> 15.0

jshell> avg /= ints.count()
|  Exception java.lang.IllegalStateException: stream has already been operated upon or closed
|        at AbstractPipeline.evaluate (AbstractPipeline.java:229)
|        at DoublePipeline.count (DoublePipeline.java:486)
|        at (#19:1)

Java 12 eases your pain by introducing Collectors.teeing() (inspired by the UNIX utility tee), which "duplicates" a stream, allowing you to perform two simultaneous Stream operations, before merging the results.

A Java 12 version of the above might look something like...

jshell> import static java.util.stream.Collectors.*

jshell> var ints = DoubleStream.of(1, 2, 3, 4, 5)
ints ==> java.util.stream.DoublePipeline$Head@574caa3f

jshell> ints.boxed().collect(teeing(
   ...>   summingDouble(e -> e),
   ...>   counting(),
   ...>   (a,b) -> a/b
   ...> ))
$20 ==> 3.0

This is still far from perfect (the syntax is a bit clunky, and you can see that we need to box the primitive doubles using boxed() in order to manipulate them later), but it's progress toward more flexible Streams (and maybe, eventually, list comprehensions?) in Java.

[ back to Table of Contents ]

#16. multiline text blocks (Java 13 (preview))

Another cool feature available right now in Java 13 is multiline text blocks. This is a preview feature (with a revised preview available in the upcoming JDK 14), so you need to pass the --enable-preview flag when running java or jshell:

Note that this is a preview feature and is subject to future changes and complications. Please read the manual before attempting to seriously use this feature.

$ jshell --enable-preview
|  Welcome to JShell -- Version 13.0.1
|  For an introduction type: /help intro

jshell> String greetings = """
   ...> Hello. My name is Inigo Montoya.
   ...> You killed my father.
   ...> Prepare to die.
   ...> """
greetings ==> "Hello. My name is Inigo Montoya.\nYou killed my father.\nPrepare to die.\n"

Multiline text blocks must begin with three double-quote characters in a row """, followed by a newline character, and must similarly end with a newline character, followed by three double-quote characters in a row """.

This feature is probably not extremely useful in the above case, but it hugely improves readability when doing things like generating Strings with lots of interspersed variables, or trying to emulate indented code with String concatenation over multiple lines. What we would have written in earlier versions of Java as

jshell> String html1 = "<html>\n\t<body>\n\t\t<h1>\"I love Java and Java loves me!\"</h1>\n\t</body>\n</html>\n";
html1 ==> "<html>\n\t<body>\n\t\t<h1>\"I love Java and Java ... h1>\n\t</body>\n</html>\n"

or

jshell> String html2 =
   ...>   "<html>\n" +
   ...>     "\t<body>\n" +
   ...>       "\t\t<h1>\"I love Java and Java loves me!\"</h1>\n" +
   ...>     "\t</body>\n" +
   ...>   "</html>\n";
html2 ==> "<html>\n\t<body>\n\t\t<h1>\"I love Java and Java ... h1>\n\t</body>\n</html>\n"

...we can now write as the much more readable:

jshell> String html3 = """
   ...> <html>
   ...>     <body>
   ...>         <h1>"I love Java and Java loves me!"</h1>
   ...>     </body>
   ...> </html>
   ...> """
html3 ==> "<html>\n\t<body>\n\t\t<h1>\"I love Java and Java ... h1>\n\t</body>\n</html>\n"

jshell> html1.equals(html2); html2.equals(html1);
$16 ==> true
$17 ==> true

No need to escape single- or double-quotes, no excessive "..." + over and over, just nicely-formatted, whitespace-preserved text. The multiline string "fences" """ also respect the indentation level of your code. So if you're inside a method or a class, there's no need to have your multiline String aligned to the left-hand side of the page:

jshell> String bleh = """
   ...>        hello
   ...>        is it me you're looking for
   ...>        """
bleh ==> "hello\nis it me you're looking for\n"

jshell> String bleh = """
   ...>        hello
   ...>        is it me you're looking for
   ...> """
bleh ==> "       hello\n       is it me you're looking for\n"

[ back to Table of Contents ]

#17. Java-on-Java compiler with Project Metropolis (Java 14+)

The most exciting thing about writing Java in 2020 isn't how far it's come over the past six years, but where it might go in the next six. So for the last few points here, I'd like to discuss some ongoing Java Projects, which promise new and exciting features in the not-too-distant future.

The first of these is Project Metropolis, which aims to rewrite some (or all) of the JVM in Java itself, where it's currently written (partially) in other languages like C and C++ (depending on which JVM you're working with). The project's lead calls this approach "Java on Java" and it has several advantages:

  1. decouple Java from dependencies on other languages, which are complicated by new versions, bug fixes, security patches, etc.
  2. allow the VM to optimise itself using the "hot spot" optimisation scheme that is currently applied to other compiled Java code
  3. maintainability / simplification -- if the JVM can be rewritten entirely in Java, then JVM architects will only need to know Java itself, rather than knowing multiple languages; this will make the JVM easier to maintain

C and C++ (which parts of the JVM are currently written in) offer benefits over Java thanks to their "close to the metal" nature. To bring an entirely Java-based JVM to the same level as these current hybrid-language JVMs, new features (like value types) may have to be added to the Java language. This is a huge project with lots of ins and outs (and what-have-yous), so don't expect it anytime soon. Still... there are many exciting things happening in the JVM world!

[ back to Table of Contents ]

#18. flow typing, anonymous variables, data classes, and sealed types in Project Amber (Java 14+)

Project Amber is the codename for a huge number of improvements to the Java API which all aim to simplify syntax. These improvements include...

flow typing with instanceof

Java's instanceof keyword checks if an object is an instance of a particular class or interface and returns a boolean to that effect:

jshell> ArrayList<Integer> alist = new ArrayList<>();
alist ==> []

jshell> alist instanceof List
$5 ==> true

jshell> alist instanceof ArrayList
$6 ==> true

jshell> alist instanceof Object
$7 ==> true

In practice, when instanceof is used and returns true, the object in question is then explicitly typecasted to the desired type and used as an object of that type:

jshell> void alertNChars (Object o) {
   ...>   if (o instanceof String)
   ...>     System.out.println("String contains " + ((String)o).length() + " characters");
   ...>   else System.out.println("not a String");
   ...> }
|  created method alertNChars(Object)

jshell> String s = "I am a banana";
s ==> "I am a banana"

jshell> Integer i = 1;
i ==> 1

jshell> alertNChars(s)
String contains 13 characters

jshell> alertNChars(i)
not a String

Project Amber aims to simplify this syntax a bit with flow-sensitive typing (or "flow typing"), where the compiler can reason about instanceof blocks. Basically, if an if (x instanceof C) block executes, then the object x must be an instance of class C (or a subclass of C), so C instance methods can be used. After Project Amber, the above method should look something like:

void alertNChars (Object o) {
  if (o instanceof String s)
    System.out.println("String contains " + s.length() + " characters");
  else System.out.println("not a String");
}

A small change, but one that reduces some visual clutter and lays some of the foundational work for pattern matching in Java.

anonymous lambda variables

Some languages allow the user to ignore parameters in lambdas (and other places) by using a single underscore character _ instead of a parameter identifier. As of Java 9, using an underscore character by itself as an identifier will throw an error at compile-time, so it has been "rehabilitated" and can now be used in this "anonymous" fashion in Java, as well.

"Unnamed" or "anonymous" variables and parameters are used in cases where you care about some of the information provided, but not all of it. An example similar to the one given at the above link is a BiFunction that takes an Integer and a Double as its two arguments, but simply returns the Integer as a String. The second argument (the Double) is not needed for the BiFunction implementation:

BiFunction<Integer, Double, String> bids = (i, d) -> String.valueOf(i);

So why do we need to name this second argument (d) at all? Anonymous parameters would allow us to simply replace unwanted or unneeded variables with a _ and be done with them:

BiFunction<Integer, Double, String> bids = (i, _) -> String.valueOf(i);

The closest thing to this in Java at the moment is probably the ? wildcard generic type. We use this when we need to specify some generic type argument, but when we don't care at all what that type actually is. This means that we can't use the type elsewhere in the code. You could call this an "unnamed" or "anonymous" type, and the usage is similar to the above.

data classes

The proposed "data class" in Project Amber is similar to a data class in Kotlin, a case class in Scala, or (as-yet-unimplemented) records in C#. Essentially, their goal is to alleviate a lot of the verbosity that can plague Java code.

Once implemented, data classes should turn all of this horrible boilerplate code...

package test;

public class Boilerplate {

  public final int    myInt;
  public final double myDouble;
  public final String myString;

  public Boilerplate (int myInt, double myDouble, String myString) {
    super();
    this.myInt = myInt;
    this.myDouble = myDouble;
    this.myString = myString;
  }

  @Override
  public int hashCode() {
    final int prime = 31;
    int result = prime + myInt;
    long temp = Double.doubleToLongBits(myDouble);

    result = prime * result + (int) (temp ^ (temp >>> 32));
    result = prime * result + ((myString == null) ? 0 : myString.hashCode());

    return result;
  }

  @Override
  public boolean equals (Object obj) {
    if (this == obj) return true;
    if (obj == null) return false;
    if (getClass() != obj.getClass()) return false;

    Boilerplate other = (Boilerplate) obj;
    if (myInt != other.myInt) return false;

    if (Double.doubleToLongBits(myDouble) !=
        Double.doubleToLongBits(other.myDouble))
      return false;

    if (myString == null) {
      if (other.myString != null) return false;
    } else if (!myString.equals(other.myString))
      return false;

    return true;
  }

  @Override
  public String toString() {
    return "Boilerplate [myInt=" + myInt + ", myDouble=" + myDouble + ", myString=" + myString + "]";
  }

}

...into just

record Boilerplate (int myInt, double myDouble, String myString) { }

The new record keyword would tell the compiler that Boilerplate is a standard data class, that we want simple access to public instance variables, and that we want all of the standard methods: hashCode(), equals(), toString().

While it's true that most modern IDEs will generate all of this code for you, there's an argument that it shouldn't really be there at all. There's so much conceptual overhead with reading and understanding the non-record code above, and so many places for bugs to hide, that it's better to just get rid of it. Data classes are the future for Java.

sealed types

In Java, if a class is declared final, it cannot be extended in any way. There can be no subclasses of it of any kind, whether written by the user or the API developer. The opposite of this, of course, is a class that is not declared final. Such a class can be extended by both the user and the API developer alike, with as many subclasses as desired.

But what if we want a semi-final class? Say we have a class that we want to subclass, but only a specified number of times (I'll use records below, for brevity):

record FossilFuelCar (Make make, Model model) { }
record ElectricCar   (Make make, Model model) { }
record HybridCar     (Make make, Model model) { }
record FuelCellCar   (Make make, Model model) { }

Suppose we're certain that these are the only four kinds of cars we'll need for the foreseeable future, and we want to prevent people from creating spurious varieties of cars (SteamPoweredCar?). sealed types provide a way of doing that:

sealed interface Car (Make make, Model model) { }

record FossilFuelCar (Make make, Model model) implements Car { }
record ElectricCar   (Make make, Model model) implements Car { }
record HybridCar     (Make make, Model model) implements Car { }
record FuelCellCar   (Make make, Model model) implements Car { }

Within the source code of this file, presumably, we would be able to define as many implementations of Car as we desired. But outside this file, no new implementations are allowed at all. Think of this like a mashup between classes and enums -- we have a specified number of implementations of Car and that's it.

sealed classes and interfaces would also work nicely with the exhaustivity required for a fully-fledged pattern matching schema in Java. More steps toward modernity!

[ back to Table of Contents ]

#19. coroutines, tail-call optimisation, and lightweight user-mode Fibers with Project Loom (Java 14+)

Project Loom has one main focus: lightweight multithreading.

At present, if a user wants to implement a concurrent / multithreaded application in Java, they need to -- at some level -- use Threads, "the core abstraction of concurrency in Java". The java.util.concurrent API also provides lots of additional abstractions, like Locks, Futures and Executors that can make those applications a bit easier to construct.

But Java's Threads are implemented at the operating system level. Creating them and switching between them can be very expensive. And the OS can greatly influence the maximum number of allowed concurrent threads, putting limits on the helpfulness of this approach.

What Project Loom aims to do is create an application-level Thread-like abstraction called a Fiber. The idea of VM-level multithreading (rather than OS-level) is actually an old one for Java -- Java used to have these "green threads" in Java 1.1, but they were phased out in favor of native threads. With Loom, green threads are back with a vengeance.

Being able to create orders of magnitude more Fibers in a similar amount of time, relative to Threads, means that the JVM will be able to put multithreading front and center. Loom will allow for tail call optimisation and continuations (similar to Kotlin's coroutines), opening new approaches for concurrent programming in Java.

[ back to Table of Contents ]

#20. value types, generic specialisations, and reified generics in Project Valhalla (Java 14+)

The last Project I'd like to discuss is Project Valhalla, which I think could bring the greatest changes to the Java ecosystem of all the upcoming and proposed features discussed so far. Project Valhalla proposes three major changes to Java:

  1. value types
  2. generic specialisation
  3. reified generics

Value types are best understood in contrast to reference types, of which Java's Collections are good examples. In a List, for instance, the elements are stored in memory as a contiguous block of references. The references themselves point to their values, which may be held in totally different places in memory. Iterating over a List, then, takes a bit of work, as each address referenced needs to be navigated to in order to pull the value.

A value type aray, on the other hand, has all of its values stored in a contiguous block of memory. There are no references needed, because the next value is simply at the next position in memory. A good example of this is a C-style array, where the type of data stored in the array must be declared, along with the length of the array:

double balance[10];

The length is required so that a contiguous block of memory of the desired size can be allocated when the code is run. Java, of course, already has this construct (arrays). But value types would allow a user to create a primitive-like array like the above, even for compound, struct-like groups of data. By bypassing the referencing/dereferencing required with existing Collections, access speed and data storage efficiency would increase dramatically.

Value types in Java would behave like classes, with methods and fields, but with access speeds as fast as primitive types. Value types could also be used as generic types, without the boxing/unboxing overhead of primitives and wrapper classes. Which brings us to the next big feature of Project Valhalla...

generic specialisation

Generic specialisation sounds like an oxymoron, but what it means (in a nutshell) is that value types (and therefore primitive types, as well) can be used as the type parameter in generic methods and classes. So in addition to

List<Integer>

we could also have

List<int>

Most Java developers know that generic type arguments T, E, K, V, etc. must be classes. They cannot be primitive types. But why?

Well, at compile time, Java uses type erasure to convert all specific types to one superclass. In Java's case, that superclass is Object. Since primitive types do not inherit from Object, they cannot be used as type arguments in generic classes, methods, and so on.

This article on DZone gives a great explanation of homogeneous translation (what Java uses to convert all classes to Object at compile time) vs. heterogeneous translation (aka. generic specialisation), which would allow disjoint type hierarchies in generic types, like the Object vs. primitive types in Java. For backwards-compatibility reasons, you won't be able to just pass an int for a T type parameter anywhere in the existing Java API. Instead, methods which accept primitives as well as Objects will need to be defined with the any keyword along with the generic type:

public class Box<any T> {
  private T value;
  public T getValue() {
    return value;
  }
}

reified generics

Generic specialisation for value types means that the JVM will be aware, at runtime, of at least some of the types being passed around your application. This stands in contrast to the usual type erasure procedure used for reference types (Objects) to date. So will types be reified in Java after Project Valhalla? Maybe, at least in part.

While it's highly unlikely that type information will be available at runtime for reference types (in order to maintain backward compatibility) it's possible (likely?) that they will be available for value types. So what's the future of Java's type system? Only time will tell.

[ back to Table of Contents ]


The above list is only a small subset of the features available in Java 9-13 and upcoming features in Java 14+. Java has evolved enormously as a language and as an ecosystem since Java 8 was released over five years ago. If you haven't upgraded since then, you're really missing out!



Follow me: Dev.To | Twitter.com
Support me: Ko-Fi.com

Thanks for reading!

Discussion

pic
Editor guide
Collapse
jmfayard profile image
Jean-Michel Fayard 🇫🇷🇩🇪🇬🇧🇪🇸🇨🇴

Meanwhile Android is still blocked at Java 7.5, slowing down the whole ecosystem!

Collapse
delta456 profile image
Swastik Baranwal

They switched to Kotlin sadly.

Collapse
julianjupiter profile image
Julian Jupiter

But Java is still there. And Kotlin uses JVM. Android does not leverage on much of Java 8. People complain of Java's verbosity and yet Google does not push newer JVM. Since Android M, Android has been using OpenJDK, GPL-licensed, so the case between Oracle and Google should not matter in using newer JVM version.

Thread Thread
jmfayard profile image
Thread Thread
julianjupiter profile image
Julian Jupiter

Read my comment again as to which I'm referring to when I said "should not matter".

Collapse
jmfayard profile image
Jean-Michel Fayard 🇫🇷🇩🇪🇬🇧🇪🇸🇨🇴

Switching to kotlin is good IMHO,
But it's in no way a valid reason to slow down progress on the Java/JVM side.

Collapse
thefern profile image
Fernando B 🚀

Kotlin still runs on JVM fyi.

Thread Thread
jmfayard profile image
Jean-Michel Fayard 🇫🇷🇩🇪🇬🇧🇪🇸🇨🇴

The kotlin compiler has to target Java 8 bytecode because of Android. It would be more efficient it it could target newer JVM.

Kotlin programmers leverage Java libraries all the time, and those are stuck in Java 8 if they want to support Android.

Tldr: both Java and Kotlin devs would be better off if the Android framework team stopped slowing down progress of Java and the JVM.

Thread Thread
thefern profile image
Fernando B 🚀

Have not got caught up in the Google vs Oracle debacle but newer versions of Java now require enterprise licensing. Is the reason why we have "open jdk". Looks to me like Oracle wants to cash in big time.

The slow down could be merely a legal issue, but that's just my guess.

Collapse
edh_developer profile image
edh_developer

Important to remember that Java 9 introduced breaking changes. That's why, for instance, Jenkins on any Java version > 8 was broken for years. Much like going from Python 2 to Python 3, before you commit, be sure the things you need to work are going to continue to work.

Collapse
awwsmm profile image
Andrew (he/him) Author

Yeah, Hadoop still doesn't work on Java >8, as far as I know.

Collapse
erikpischel profile image
Erik Pischel

Wrong! There is commercial support for java 8 from Oracle! If you are commercial yourself (e
g. use it in a company) you already have to pay for it. If you're non-commercial, you get updates for free until the end of next year.

Collapse
mesadhan profile image
Sadhan Sarker

Lots of things covered! Thanks for sharing that's types of summary.
Please let it keep update.

Collapse
andevr profile image
drew

Holy chit this is huge. Book marked :)

Collapse
christopherme profile image
Christopher Elias

So what would happen If google suddently decides to upgrade the JVM?

Collapse
varunra35569603 profile image
Varun Rana

Lots of things covered! Thanks for sharing that's types of summary.
Please let it keep update.