Einführung in das Projekt Amber

1. Was ist Projekt Amber?

Project Amber ist eine aktuelle Initiative der Entwickler von Java und OpenJDK, die darauf abzielt, einige kleine, aber wesentliche Änderungen am JDK vorzunehmen, um den Entwicklungsprozess zu vereinfachen . Dies läuft seit 2017 und hat bereits einige Änderungen an Java 10 und 11 vorgenommen, andere sollen in Java 12 aufgenommen werden, weitere werden in zukünftigen Versionen folgen.

Diese Updates sind alle in Form von JEPs verpackt - dem JDK Enhancement Proposal-Schema.

2. Gelieferte Updates

Bisher hat Project Amber einige Änderungen an den derzeit veröffentlichten Versionen des JDK - JEP-286 und JEP-323 - erfolgreich durchgeführt.

2.1. Inferenz des lokalen Variablentyps

In Java 7 wurde der Diamond Operator eingeführt, um die Arbeit mit Generika zu vereinfachen . Diese Funktion bedeutet, dass wir beim Definieren von Variablen nicht mehr mehrmals generische Informationen in dieselbe Anweisung schreiben müssen:

List strings = new ArrayList(); // Java 6 List strings = new ArrayList(); // Java 7

Java 10 enthielt die abgeschlossene Arbeit an JEP-286, sodass unser Java-Code lokale Variablen definieren kann, ohne dass die Typinformationen überall dort dupliziert werden müssen, wo der Compiler sie bereits zur Verfügung hat . Dies wird in der breiteren Community als var- Schlüsselwort bezeichnet und bietet Java ähnliche Funktionen wie in vielen anderen Sprachen.

Bei dieser Arbeit können wir immer dann , wenn wir eine lokale Variable definieren, das Schlüsselwort var anstelle der vollständigen Typdefinition verwenden , und der Compiler erarbeitet automatisch die richtigen zu verwendenden Typinformationen:

var strings = new ArrayList();

Oben wird festgelegt, dass die variablen Zeichenfolgen vom Typ ArrayList () sind, ohne dass die Informationen in derselben Zeile dupliziert werden müssen.

Wir können dies überall dort verwenden , wo wir lokale Variablen verwenden , unabhängig davon, wie der Wert bestimmt wird. Dies umfasst Rückgabetypen und Ausdrücke sowie einfache Zuweisungen wie die oben genannten.

Das Wort var ist insofern ein Sonderfall, als es kein reserviertes Wort ist. Stattdessen ist es ein spezieller Typname. Dies bedeutet, dass es möglich ist, das Wort für andere Teile des Codes zu verwenden - einschließlich Variablennamen. Es wird dringend empfohlen, dies nicht zu tun, um Verwirrung zu vermeiden.

Wir können die lokale Typinferenz nur verwenden, wenn wir als Teil der Deklaration einen tatsächlichen Typ angeben . Es ist absichtlich so konzipiert, dass es nicht funktioniert, wenn der Wert explizit null ist, wenn überhaupt kein Wert angegeben wird oder wenn der angegebene Wert keinen genauen Typ bestimmen kann - zum Beispiel eine Lambda-Definition:

var unknownType; // No value provided to infer type from var nullType = null; // Explicit value provided but it's null var lambdaType = () -> System.out.println("Lambda"); // Lambda without defining the interface

Der Wert kann jedoch null sein, wenn es sich um einen Rückgabewert eines anderen Aufrufs handelt, da der Aufruf selbst Typinformationen bereitstellt:

Optional name = Optional.empty(); var nullName = name.orElse(null);

In diesem Fall leitet nullName den Typ String ab, da dies der Rückgabetyp von name.orElse () ist.

Auf diese Weise definierte Variablen können andere Modifikatoren auf dieselbe Weise wie jede andere Variable haben - z. B. transitiv, synchronisiert und endgültig .

2.2. Lokale Variablentypinferenz für Lambdas

Die obige Arbeit ermöglicht es uns, lokale Variablen zu deklarieren, ohne Typinformationen duplizieren zu müssen. Dies funktioniert jedoch nicht bei Parameterlisten und insbesondere nicht bei Parametern für Lambda-Funktionen, was überraschend erscheinen mag.

In Java 10 können wir Lambda-Funktionen auf zwei Arten definieren - entweder indem wir die Typen explizit deklarieren oder indem wir sie vollständig weglassen:

names.stream() .filter(String name -> name.length() > 5) .map(name -> name.toUpperCase());

Hier hat die zweite Zeile eine explizite Typdeklaration - String -, während die dritte Zeile diese vollständig weglässt und der Compiler den richtigen Typ ermittelt. Was wir nicht tun können, ist hier den var- Typ zu verwenden .

Java 11 ermöglicht dies , sodass wir stattdessen schreiben können:

names.stream() .filter(var name -> name.length() > 5) .map(var name -> name.toUpperCase());

Dies steht dann im Einklang mit der Verwendung des var- Typs an anderer Stelle in unserem Code .

Lambdas haben uns immer darauf beschränkt, vollständige Typnamen entweder für jeden Parameter oder für keinen von ihnen zu verwenden. Dies hat sich nicht geändert, und die Verwendung von var muss entweder für jeden Parameter oder für keinen von ihnen gelten :

numbers.stream() .reduce(0, (var a, var b) -> a + b); // Valid numbers.stream() .reduce(0, (var a, b) -> a + b); // Invalid numbers.stream() .reduce(0, (var a, int b) -> a + b); // Invalid

Hier ist das erste Beispiel vollkommen gültig - da beide Lambda-Parameter var verwenden . Der zweite und dritte sind jedoch unzulässig, da nur ein Parameter var verwendet , obwohl wir im dritten Fall auch einen expliziten Typnamen haben.

3. Bevorstehende Updates

Zusätzlich zu den Updates, die bereits in freigegebenen JDKs verfügbar sind, enthält die kommende JDK 12-Version ein Update - JEP-325.

3.1. Ausdrücke wechseln

JEP-325 bietet Unterstützung für die Vereinfachung der Funktionsweise von switch- Anweisungen und für die Verwendung als Ausdrücke , um den Code, der sie verwendet , noch weiter zu vereinfachen.

Derzeit funktioniert die switch- Anweisung sehr ähnlich wie in Sprachen wie C oder C ++. Diese Änderungen machen es der when- Anweisung in Kotlin oder der match- Anweisung in Scala viel ähnlicher .

Mit diesen Änderungen ähnelt die Syntax zum Definieren einer switch-Anweisung der von Lambdas unter Verwendung des Symbols -> . Dies liegt zwischen der Fallübereinstimmung und dem auszuführenden Code:

switch (month) { case FEBRUARY -> System.out.println(28); case APRIL -> System.out.println(30); case JUNE -> System.out.println(30); case SEPTEMBER -> System.out.println(30); case NOVEMBER -> System.out.println(30); default -> System.out.println(31); }

Beachten Sie, dass das Schlüsselwort break nicht benötigt wird. Außerdem können wir es hier nicht verwenden . Es wird automatisch impliziert, dass jedes Spiel anders ist und Fallthrough keine Option ist. Stattdessen können wir den älteren Stil weiterhin verwenden, wenn wir ihn benötigen.

The right-hand side of the arrow must be either an expression, a block, or a throws statement. Anything else is an error. This also solves the problem of defining variables inside of switch statements – that can only happen inside of a block, which means they are automatically scoped to that block:

switch (month) { case FEBRUARY -> { int days = 28; } case APRIL -> { int days = 30; } .... }

In the older style switch statement, this would be an error because of the duplicate variable days. The requirement to use a block avoids this.

The left-hand side of the arrow can be any number of comma-separated values. This is to allow some of the same functionality as fallthrough, but only for the entirety of a match and never by accident:

switch (month) { case FEBRUARY -> System.out.println(28); case APRIL, JUNE, SEPTEMBER, NOVEMBER -> System.out.println(30); default -> System.out.println(31); }

So far, all of this is possible with the current way that switch statements work and makes it tidier. However, this update also brings the ability to use a switch statement as an expression. This is a significant change for Java, but it's consistent with how many other languages — including other JVM languages — are starting to work.

This allows for the switch expression to resolve to a value, and then to use that value in other statements – for example, an assignment:

final var days = switch (month) { case FEBRUARY -> 28; case APRIL, JUNE, SEPTEMBER, NOVEMBER -> 30; default -> 31; }

Here, we're using a switch expression to generate a number, and then we're assigning that number directly to a variable.

Before, this was only possible by defining the variable days as null and then assigning it a value inside the switch cases. That meant that days couldn't be final, and could potentially be unassigned if we missed a case.

4. Upcoming Changes

So far, all of these changes are either already available or will be in the upcoming release. There are some proposed changes as part of Project Amber that are not yet scheduled for release.

4.1. Raw String Literals

At present, Java has exactly one way to define a String literal – by surrounding the content in double quotes. This is easy to use, but it suffers from problems in more complicated cases.

Specifically, it is difficult to write strings that contain certain characters – including but not limited to: new lines, double quotes, and backslash characters. This can be especially problematic in file paths and regular expressions where these characters can be more common than is typical.

JEP-326 introduces a new String literal type called Raw String Literals. These are enclosed in backtick marks instead of double quotes and can contain any characters at all inside of them.

This means that it becomes possible to write strings that span multiple lines, as well as strings that contain quotes or backslashes without needing to escape them. Thus, they become easier to read.

For example:

// File system path "C:\\Dev\\file.txt" `C:\Dev\file.txt` // Regex "\\d+\\.\\d\\d" `\d+\.\d\d` // Multi-Line "Hello\nWorld" `Hello World`

In all three cases, it's easier to see what's going on in the version with the backticks, which is also much less error-prone to type out.

The new Raw String Literals also allow us to include the backticks themselves without complication. The number of backticks used to start and end the string can be as long as desired – it needn't only be one backtick. The string ends only when we reach an equal length of backticks. So, for example:

``This string allows a single "`" because it's wrapped in two backticks``

These allow us to type in strings exactly as they are, rather than ever needing special sequences to make certain characters work.

4.2. Lambda Leftovers

JEP-302 introduces some small improvements to the way lambdas work.

The major changes are to the way that parameters are handled. Firstly, this change introduces the ability to use an underscore for an unused parameter so that we aren't generating names that are not needed. This was possible previously, but only for a single parameter, since an underscore was a valid name.

Java 8 introduced a change so that using an underscore as a name is a warning. Java 9 then progressed this to become an error instead, stopping us from using them at all. This upcoming change allows them for lambda parameters without causing any conflicts. This would allow, for example, the following code:

jdbcTemplate.queryForObject("SELECT * FROM users WHERE user_id = 1", (rs, _) -> parseUser(rs))

Under this enhancement, we defined the lambda with two parameters, but only the first is bound to a name. The second is not accessible, but equally, we have written it this way because we don't have any need to use it.

The other major change in this enhancement is to allow lambda parameters to shadow names from the current context. This is currently not allowed, which can cause us to write some less than ideal code. For example:

String key = computeSomeKey(); map.computeIfAbsent(key, key2 -> key2.length());

There is no real need, apart from the compiler, why key and key2 can't share a name. The lambda never needs to reference the variable key, and forcing us to do this makes the code uglier.

Instead, this enhancement allows us to write it in a more obvious and simple way:

String key = computeSomeKey(); map.computeIfAbsent(key, key -> key.length());

Additionally, there is a proposed change in this enhancement that could affect overload resolution when an overloaded method has a lambda argument. At present, there are cases where this can lead to ambiguity due to the rules under which overload resolution works, and this JEP may adjust these rules slightly to avoid some of this ambiguity.

For example, at present, the compiler considers the following methods to be ambiguous:

m(Predicate ps) { ... } m(Function fss) { ... }

Both of these methods take a lambda that has a single String parameter and has a non-void return type. It is obvious to the developer that they are different – one returns a String, and the other, a boolean, but the compiler will treat these as ambiguous.

This JEP may address this shortcoming and allow this overload to be treated explicitly.

4.3. Pattern Matching

JEP-305 introduces improvements on the way that we can work with the instanceof operator and automatic type coercion.

At present, when comparing types in Java, we have to use the instanceof operator to see if the value is of the correct type, and then afterwards, we need to cast the value to the correct type:

if (obj instanceof String) { String s = (String) obj; // use s }

This works and is instantly understood, but it's more complicated than is necessary. We have some very obvious repetition in our code, and therefore, a risk of allowing errors to creep in.

This enhancement makes a similar adjustment to the instanceof operator as was previously made under try-with-resources in Java 7. With this change, the comparison, cast, and variable declaration become a single statement instead:

if (obj instanceof String s) { // use s }

This gives us a single statement, with no duplication and no risk of errors creeping in, and yet performs the same as the above.

This will also work correctly across branches, allowing the following to work:

if (obj instanceof String s) { // can use s here } else { // can't use s here }

The enhancement will also work correctly across different scope boundaries as appropriate. The variable declared by the instanceof clause will correctly shadow variables defined outside of it, as expected. This will only happen in the appropriate block, though:

String s = "Hello"; if (obj instanceof String s) { // s refers to obj } else { // s refers to the variable defined before the if statement }

This also works within the same if clause, in the same way as we rely on for null checks:

if (obj instanceof String s && s.length() > 5) { // s is a String of greater than 5 characters }

At present, this is planned only for if statements, but future work will likely expand it to work with switch expressions as well.

4.4. Concise Method Bodies

JEP Draft 8209434 is a proposal to support simplified method definitions, in a way that is similar to how lambda definitions work.

Right now, we can define a Lambda in three different ways: with a body, as a single expression, or as a method reference:

ToIntFunction lenFn = (String s) -> { return s.length(); }; ToIntFunction lenFn = (String s) -> s.length(); ToIntFunction lenFn = String::length;

However, when it comes to writing actual class method bodies, we currently must write them out in full.

This proposal is to support the expression and method reference forms for these methods as well, in the cases where they are applicable. This will help to keep certain methods much simpler than they currently are.

For example, a getter method does not need a full method body, but can be replaced with a single expression:

String getName() -> name;

Equally, we can replace methods that are simply wrappers around other methods with a method reference call, including passing parameters across:

int length(String s) = String::length

These will allow for simpler methods in the cases where they make sense, which means that they will be less likely to obscure the real business logic in the rest of the class.

Note that this is still in draft status and, as such, is subject to significant change before delivery.

5. Enhanced Enums

JEP-301 was previously scheduled to be a part of Project Amber. This would've brought some improvements to enums, explicitly allowing for individual enum elements to have distinct generic type information.

For example, it would allow:

enum Primitive { INT(Integer.class, 0) { int mod(int x, int y) { return x % y; } int add(int x, int y) { return x + y; } }, FLOAT(Float.class, 0f) { long add(long x, long y) { return x + y; } }, ... ; final Class boxClass; final X defaultValue; Primitive(Class boxClass, X defaultValue) { this.boxClass = boxClass; this.defaultValue = defaultValue; } }

Unfortunately, experiments of this enhancement inside the Java compiler application have proven that it is less viable than was previously thought. Adding generic type information to enum elements made it impossible to then use those enums as generic types on other classes – for example, EnumSet. This drastically reduces the usefulness of the enhancement.

As such, this enhancement is currently on hold until these details can be worked out.

6. Summary

Wir haben hier viele verschiedene Funktionen behandelt. Einige von ihnen sind bereits verfügbar, andere werden in Kürze verfügbar sein, und weitere sind für zukünftige Versionen geplant. Wie können diese Ihre aktuellen und zukünftigen Projekte verbessern?