20 Reasons to Move On from Java 8
Tags: java, news
Published: 2019-11-23 11:51:25

> _"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](http://sundayadelajablog.com/the-demand-and-supply-of-life-part-4/)

---

> _**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](https://javapapers.com/java/java-8-features/) to the Java ecosystem in the API, the garbage collector, the compiler, and more. [Mark Reinhold](https://twitter.com/mreinhold), 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](https://www.reddit.com/r/java/comments/8y82dn/is_the_upcoming_situation_with_the_new_release/) (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](https://en.wikipedia.org/wiki/History_of_Python#Version_release_dates) \*_retired_\* [version of Python last year](https://www.jetbrains.com/lp/devecosystem-2019/python/), we've so far been [slow to move on from Java 8](https://www.jetbrains.com/lp/devecosystem-2019/java/), 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...

---

<a name="toc"></a>
## Table of Contents

1.  [Oracle no longer provides free support for Java 8](#step01)
2.  [`jshell`, the Java REPL (Java 9)](#step02)
3.  [modules and linking (Java 9)](#step03)
4.  [improved Javadoc (Java 9)](#step04)
5.  [`Collection` immutable factory methods (Java 9)](#step05)
6.  [`Stream` improvements (Java 9)](#step06)
7.  [multi-release `jar`s (Java 9)](#step07)
8.  [`private` `interface` methods (Java 9)](#step08)
9.  [GraalVM, a new Java Virtual Machine (Java 9/10)](#step09)
10. [local variable type inference (Java 10)](#step10)
11. [unmodifiable `Collection` enhancements (Java 10)](#step11)
12. [container awareness (Java 10)](#step12)
13. [single source file launch (Java 11)](#step13)
14. [`switch` expressions -- a step toward pattern matching (Java 12)](#step14)
15. [`teeing` `Collectors` (Java 12)](#step15)
16. [multiline text blocks (Java 13 (preview))](#step16)
17. [Java-on-Java compiler with Project Metropolis (Java 14+)](#step17)
18. [flow typing, anonymous variables, data classes, and sealed types in Project Amber (Java 14+)](#step18)
19. [coroutines, tail-call optimisation, and lightweight user-mode `Fiber`s with Project Loom (Java 14+)](#step19)
20. [value types, generic specialisations, and reified generics in Project Valhalla (Java 14+)](#step20)

---

<a name="step01"></a>
## \#1. Oracle no longer provides free support for Java 8

Although [AdoptOpenJDK](https://adoptopenjdk.net/) will provide free public updates for their version of Java 8 until [at least September 2023](https://adoptopenjdk.net/support.html), 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](https://www.oracle.com/JAVA/JAVA-SE-SUBSCRIPTION.HTML).

> [Learn about some of the differences between Oracle's JDK, OpenJDK, and AdoptOpenJDK here.](https://stackoverflow.com/a/52431765/2925434)

[[ back to Table of Contents ]](#toc)

<a name="step02"></a>
## \#2. `jshell`, the Java REPL (Java 9)

A [Read-Evaluate-Print Loop (REPL)](https://en.wikipedia.org/wiki/Read%E2%80%93eval%E2%80%93print_loop) 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:

```java
$ 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 ]](#toc)

<a name="step03"></a>
## \#3. modules and linking (Java 9)

Java 9 also introduces [_modules_, which are a level of organisation _above_ packages](https://www.baeldung.com/java-9-modularity). If you have Java 9+ installed, [you're already using modules](https://www.oracle.com/corporate/features/understanding-java-9-modules.html), even if you don't know it. You can check which modules are available with the command:

```java
$ 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](http://tutorials.jenkov.com/java/modules.html), including [reducing the size of Java applications](https://www.pluralsight.com/blog/software-development/java-9-new-features) when coupled with [the new Java linker](https://openjdk.java.net/jeps/282) (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](https://www.baeldung.com/java-9-modularity) available online which explain how to construct and configure modules. Go forth and modularise!

[[ back to Table of Contents ]](#toc)

<a name="step04"></a>
## \#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](https://docs.oracle.com/javase/9/whatsnew/toc.htm#JSNEW-GUID-DB9EB298-4944-4BF9-9CE0-B4A884F8294F) is searchable, HTML5 compliant, and compatible with the new module hierarchy. Check out the difference between [JDK 8](https://docs.oracle.com/javase/8/docs/api/java/lang/String.html) and [JDK 9](https://docs.oracle.com/javase/9/docs/api/java/lang/String.html) Javadocs here.

> Make searching Oracle's Javadoc easier than ever with [this little Chrome plugin I made](https://chrome.google.com/webstore/detail/java-api-search-jdk-9-13/nlgnliljjenkennnfnllgcolafcipoab) to search multiple versions of the API directly from the omnibox.

[[ back to Table of Contents ]](#toc)

<a name="step05"></a>
## \#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:

```java
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](https://www.baeldung.com/java-9-collections-factory-methods) by introducing immutable factory methods `Map.of()`, `List.of()` (see above), and `Set.of()`:

```java
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.

[[ back to Table of Contents ]](#toc)

<a name="step06"></a>
## \#6. `Stream` improvements (Java 9)

The [`Stream` API](https://www.baeldung.com/java-8-streams) has provided (arguably) the largest increase in Java's scope as a programming language over the past decade. `Stream`s brought [functional programming](https://winterbe.com/posts/2014/07/31/java8-stream-tutorial-examples/) 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()`](https://www.journaldev.com/13204/javase9-stream-api-improvements):

```java
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:

```java
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:

```java
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](https://rosettacode.org/wiki/List_comprehensions#Java) have [worked themselves](https://medium.com/@farolfo/list-comprehensions-in-java-230e6a9d3a0c) into [knots](https://www.teamten.com/lawrence/writings/list_comprehensions_for_java.html) 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](https://dev.to/awwsmm/relearn-you-a-haskell-part-2-list-comprehensions-tuples-and-types-g29):

```java
List.comprehension(
  Stream<T> input,
  Predicate<? super T>... filters)
```

```java
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()`](https://docs.oracle.com/javase/8/docs/api/java/util/stream/Stream.html#iterate-T-java.util.function.UnaryOperator-) 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 ]](#toc)

<a name="step07"></a>
## \#7. multi-release `jar`s (Java 9)

Another exciting feature of Java 9 is the ability to create [multi-release `jar` files](https://www.javaworld.com/article/3184029/java-9s-other-new-enhancements-part-4-multi-release-jar-files.html). 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](https://docs.oracle.com/javase/7/docs/technotes/guides/jar/jar.html#The_META-INF_directory) of the `jar`, then [include the different class versions](https://www.logicbig.com/tutorials/core-java-tutorial/java-9-changes/multi-release-jars.html) 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!

[[ back to Table of Contents ]](#toc)

<a name="step08"></a>
## \#8. `private` `interface` methods (Java 9)

Java 8 introduced [`default` methods in `interface`s](https://www.geeksforgeeks.org/default-methods-java/), 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`.

```java
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:

```java
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:

```java
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 ]](#toc)

<a name="step09"></a>
## \#9. GraalVM, a new Java Virtual Machine (Java 9/10)

[GraalVM](https://www.graalvm.org/) (pronounced like "crawl" with a hard 'g' instead of a 'c') is [a new Java virtual machine and development kit](https://en.wikipedia.org/wiki/GraalVM), 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](https://en.wikipedia.org/wiki/Ahead-of-time_compilation) (AOT) compilation
1. 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"](https://en.wikipedia.org/wiki/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:

```js
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](https://www.graalvm.org/docs/reference-manual/polyglot/) between Java, JavaScript, R, Python, Ruby, and C, thanks to the [Truffle Language Implementation Framework](https://github.com/oracle/graal/blob/master/truffle/README.md). 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 ]](#toc)

<a name="step10"></a>
## \#10. local variable type inference (Java 10)

Java 10 continues the war against boilerplate with the [`var` keyword](https://developers.redhat.com/blog/2018/05/25/simplify-local-variable-type-definition-using-the-java-10-var-keyword/):

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

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

jshell> x
x ==> [42]
```

The new `var` [type](https://stackify.com/whats-new-in-java-10/) 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](https://stackoverflow.com/q/4166966/2925434)) makes variable definition just a bit less verbose.

> _Local_ type inference means that `var` [can only be used inside of method bodies](https://dzone.com/articles/finally-java-10-has-var-to-declare-local-variables) 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`:

```java
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_](https://stackoverflow.com/q/32476680/2925434) and can change from line to line.

[[ back to Table of Contents ]](#toc)

<a name="step11"></a>
## \#11. unmodifiable `Collection` enhancements (Java 10)

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

```java
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:

```java
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](https://www.geeksforgeeks.org/final-arrays-in-java/), so we can't do something like:

```java
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](https://docs.oracle.com/javase/7/docs/api/java/util/Collections.html), 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.

```java
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:

```java
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"](#step05) 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:

```java
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:

```java
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 ]](#toc)

<a name="step12"></a>
## \#12. container awareness (Java 10)

Between 2006 and 2008, Google engineers added a cool new feature [known as cgroups](https://en.wikipedia.org/wiki/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](https://medium.com/@jnsrikanth/docker-support-in-java-10-fbff28a31827) and [respects the resource limits placed on it by that container](https://jaxenter.com/better-containerized-jvms-jdk-10-140593.html) 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](https://aboullaite.me/docker-java-10/).

[[ back to Table of Contents ]](#toc)

<a name="step13"></a>
## \#13. single source file launch (Java 11)

Starting with Java 11, [you no longer need to compile a single source file before running it](https://openjdk.java.net/jeps/330) -- `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:

```java
// 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] + "!");

  }
}
```

```shell
$ 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 ]](#toc)

<a name="step14"></a>
## \#14. `switch` expressions -- a step toward pattern matching (Java 12)

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

```java
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](https://blog.codefx.org/java/switch-expressions/#Expression-vs-Statement) 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:

```java
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](https://bentolor.github.io/java9to13/#/_switch_expression_jep_325_12_jep_354_13_preview) 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`](https://docs.oracle.com/javase/tutorial/java/javaOO/enum.html) 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:

```java
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](https://docs.scala-lang.org/tour/pattern-matching.html) support in Java. This is being developed through [Project Amber, which I'll cover in more detail below](#step18). Note that switch expressions are a "preview" feature in Java 12 and 13, but are slated to be upgraded to finalised for [Java 14](https://openjdk.java.net/jeps/361).

[[ back to Table of Contents ]](#toc)

<a name="step15"></a>
## \#15. `teeing` `Collectors` (Java 12)

> Note that `DoubleStream` does have an [`average()`](https://docs.oracle.com/javase/8/docs/api/java/util/stream/DoubleStream.html#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 `Stream`s can only be iterated over a single time:

```java
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`](https://en.wikipedia.org/wiki/Tee_(command))), 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](https://www.logicbig.com/tutorials/core-java-tutorial/java-12-changes/collectors-changes.html)...

```java
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](https://www.pythonforbeginners.com/basics/list-comprehensions-in-python)?) in Java.

[[ back to Table of Contents ]](#toc)

<a name="step16"></a>
## \#16. multiline text blocks (Java 13 (preview))

[Another cool feature](https://jaxenter.com/java-13-jdk-deep-dive-new-features-162272.html) 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](http://openjdk.java.net/jeps/368)), 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.

```java
$ 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](https://blogs.oracle.com/javamagazine/text-blocks-come-to-java), 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

```java
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

```java
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:

```java
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:

```java
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 ]](#toc)

<a name="step17"></a>
## \#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](https://www.reddit.com/r/java/comments/7sf6p7/project_metropolis_is_here/), where it's currently written (partially) in other languages like C and C++ (depending on [_which_ JVM you're working with](https://en.wikipedia.org/wiki/List_of_Java_virtual_machines)). 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"](https://stackoverflow.com/q/7854808/2925434) 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"](https://www.quora.com/What-does-it-mean-for-a-programming-language-to-be-closer-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](#step20)) may have to be added to the Java language. This is [a huge project](https://jaxenter.com/openjdk-project-metropolis-137318.html) with lots of ins and outs (and [what-have-yous](https://www.youtube.com/watch?v=Jw36ivnRHRQ)), so don't expect it anytime soon. Still... there are many exciting things happening in the JVM world!

[[ back to Table of Contents ]](#toc)

<a name="step18"></a>
## \#18. flow typing, anonymous variables, data classes, and sealed types in Project Amber (Java 14+)

[Project Amber](https://cr.openjdk.java.net/~briangoetz/amber/datum.html) is the codename for a huge number of improvements to the Java API which all aim to [_simplify syntax_](https://bentolor.github.io/java9to13/#/_simplyfing_syntax_amber). These improvements include...

### [flow typing](https://en.wikipedia.org/wiki/Flow-sensitive_typing) with [`instanceof`](https://openjdk.java.net/jeps/305)

Java's `instanceof` keyword checks [if an object is an instance of a particular class or interface](https://www.javatpoint.com/downcasting-with-instanceof-operator) and returns a `boolean` to that effect:

```java
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:

```java
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:

```java
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](https://en.wikipedia.org/wiki/Pattern_matching) in Java.

### anonymous lambda variables

[Some languages](https://stackoverflow.com/a/8001065/2925434) 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"](https://openjdk.java.net/jeps/302) 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:

```java
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:

```java
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](https://docs.oracle.com/javase/tutorial/java/generics/wildcards.html). 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`](https://kotlinlang.org/docs/reference/data-classes.html) in Kotlin, a [`case` class](https://docs.scala-lang.org/tour/case-classes.html) in Scala, or (as-yet-unimplemented) [`record`s](https://github.com/dotnet/csharplang/issues/39) 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...

```java
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

```java
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):

```java
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:

```java
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](https://www.w3schools.com/java/java_enums.asp) -- we have a specified number of implementations of `Car` and that's it.

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

[[ back to Table of Contents ]](#toc)

<a name="step19"></a>
## \#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"](https://www.baeldung.com/openjdk-project-loom). The [`java.util.concurrent` API](https://www.baeldung.com/java-util-concurrent) 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](https://stackoverflow.com/q/344203/2925434) 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](https://dzone.com/articles/a-new-java-with-a-stronger-fiber), 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](https://wiki.openjdk.java.net/display/loom/Main) (similar to Kotlin's [coroutines](https://kotlinlang.org/docs/reference/coroutines-overview.html)), opening new approaches for concurrent programming in Java.

[[ back to Table of Contents ]](#toc)

<a name="step20"></a>
## \#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](https://en.wikipedia.org/wiki/Project_Valhalla_(Java_language)) to Java:

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

__Value types__ are best [understood in contrast to _reference types_](https://en.wikipedia.org/wiki/Value_type_and_reference_type), 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](https://dzone.com/articles/what-is-project-valhalla). 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:

```c
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](https://en.wikipedia.org/wiki/Struct_(C_programming_language)) 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](https://www.baeldung.com/java-valhalla-project) 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

```java
List<Integer>
```

we could also have

```java
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](https://dzone.com/articles/what-is-project-valhalla) 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:

```java
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_](https://en.wikipedia.org/wiki/Reification_(computer_science)) in Java after Project Valhalla? Maybe, at least in part.

While it's [highly unlikely](https://www.oracle.com/technetwork/java/jvmls2016-goetz-3126134.pdf) 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 ]](#toc)

---

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](https://dev.to/awwsmm) | [Twitter.com](https://twitter.com/_awwsmm)
Support me: [Ko-Fi.com](https://ko-fi.com/awwsmm)

Thanks for reading!