Andrew (he/him)
Posted on November 23, 2019
"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."
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
- Oracle no longer provides free support for Java 8
-
jshell
, the Java REPL (Java 9) - modules and linking (Java 9)
- improved Javadoc (Java 9)
-
Collection
immutable factory methods (Java 9) -
Stream
improvements (Java 9) - multi-release
jar
s (Java 9) -
private
interface
methods (Java 9) - GraalVM, a new Java Virtual Machine (Java 9/10)
- local variable type inference (Java 10)
- unmodifiable
Collection
enhancements (Java 10) - container awareness (Java 10)
- single source file launch (Java 11)
switch
expressions -- a step toward pattern matching (Java 12)teeing
Collectors
(Java 12)- multiline text blocks (Java 13 (preview))
- Java-on-Java compiler with Project Metropolis (Java 14+)
- flow typing, anonymous variables, data classes, and sealed types in Project Amber (Java 14+)
- coroutines, tail-call optimisation, and lightweight user-mode
Fiber
s with Project Loom (Java 14+) - 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.
#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
#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!
#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.
#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 Collection
s 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.
#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. Stream
s 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 Object
s (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 Stream
s 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 Stream
s 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!
#7. multi-release jar
s (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 jar
s are the way to go. You have nothing to lose by implementing them!
#8. private
interface
methods (Java 9)
Java 8 introduced default
methods in interface
s, 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 interface
s 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.
#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:
- allows for ahead-of-time (AOT) compilation
- 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.
#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.
#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 Object
s 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.
#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.
#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.
#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 break
s 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 case
s 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.
#15. teeing
Collectors
(Java 12)
Note that
DoubleStream
does have anaverage()
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 Stream
s 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 double
s using boxed()
in order to manipulate them later), but it's progress toward more flexible Stream
s (and maybe, eventually, list comprehensions?) in Java.
#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 String
s 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"
#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:
- decouple Java from dependencies on other languages, which are complicated by new versions, bug fixes, security patches, etc.
- allow the VM to optimise itself using the "hot spot" optimisation scheme that is currently applied to other compiled Java code
- 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!
#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) record
s 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 record
s 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 enum
s -- 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!
#19. coroutines, tail-call optimisation, and lightweight user-mode Fiber
s 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 Thread
s, "the core abstraction of concurrency in Java". The java.util.concurrent
API also provides lots of additional abstractions, like Locks
, Future
s and Executor
s that can make those applications a bit easier to construct.
But Java's Thread
s 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 Thread
s, 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.
#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:
- value types
- generic specialisation
- reified generics
Value types are best understood in contrast to reference types, of which Java's Collection
s 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 Object
s 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 (Object
s) 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.
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!
Posted on November 23, 2019
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.