Introduction au projet Amber

1. Qu'est-ce que le projet Amber

Project Amber est une initiative actuelle des développeurs de Java et d'OpenJDK, visant à apporter des changements mineurs mais essentiels au JDK pour rendre le processus de développement plus agréable . Cela se poursuit depuis 2017 et a déjà apporté certaines modifications à Java 10 et 11, d'autres devant être incluses dans Java 12 et encore d'autres à venir dans les versions futures.

Ces mises à jour sont toutes conditionnées sous la forme de JEP - le schéma de proposition d'amélioration JDK.

2. Mises à jour fournies

À ce jour, Project Amber a apporté avec succès certaines modifications dans les versions actuellement publiées du JDK - JEP-286 et JEP-323.

2.1. Inférence de type de variable locale

Java 7 a introduit Diamond Operator comme un moyen de faciliter l'utilisation des génériques . Cette fonctionnalité signifie que nous n'avons plus besoin d'écrire des informations génériques plusieurs fois dans la même instruction lorsque nous définissons des variables:

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

Java 10 incluait le travail terminé sur JEP-286, permettant à notre code Java de définir des variables locales sans avoir besoin de dupliquer les informations de type là où le compilateur les a déjà disponibles . Ceci est appelé dans la communauté plus large le mot-clé var et apporte à Java des fonctionnalités similaires à celles disponibles dans de nombreux autres langages.

Avec ce travail, chaque fois que nous définissons une variable locale, nous pouvons utiliser le mot-clé var au lieu de la définition de type complète , et le compilateur trouvera automatiquement les informations de type correctes à utiliser:

var strings = new ArrayList();

Dans ce qui précède, les chaînes de variables sont déterminées comme étant de type ArrayList () , mais sans avoir besoin de dupliquer les informations sur la même ligne.

Nous pouvons l'utiliser partout où nous utilisons des variables locales , quelle que soit la façon dont la valeur est déterminée. Cela inclut les types et expressions de retour, ainsi que les affectations simples comme ci-dessus.

Le mot var est un cas particulier, dans la mesure où ce n'est pas un mot réservé. Au lieu de cela, c'est un nom de type spécial. Cela signifie qu'il est possible d'utiliser le mot pour d'autres parties du code - y compris les noms de variables. Il est fortement recommandé de ne pas le faire pour éviter toute confusion.

Nous pouvons utiliser l'inférence de type local uniquement lorsque nous fournissons un type réel dans le cadre de la déclaration . Il est délibérément conçu pour ne pas fonctionner lorsque la valeur est explicitement nulle, lorsqu'aucune valeur n'est fournie du tout, ou lorsque la valeur fournie ne peut pas déterminer un type exact - par exemple, une définition Lambda:

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

Cependant, la valeur peut être nulle si c'est une valeur de retour d'un autre appel car l'appel lui-même fournit des informations de type:

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

Dans ce cas, nullName déduira le type String car c'est le type de retour de name.orElse () .

Les variables définies de cette manière peuvent avoir n'importe quel autre modificateur de la même manière que n'importe quelle autre variable - par exemple, transitive, synchronisée et finale .

2.2. Inférence de type de variable locale pour Lambdas

Le travail ci-dessus nous permet de déclarer des variables locales sans avoir besoin de dupliquer les informations de type. Cependant, cela ne fonctionne pas sur les listes de paramètres, et en particulier, pas sur les paramètres des fonctions lambda, ce qui peut paraître surprenant.

Dans Java 10, nous pouvons définir les fonctions Lambda de deux manières: soit en déclarant explicitement les types, soit en les omettant complètement:

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

Ici, la deuxième ligne a une déclaration de type explicite - String - alors que la troisième ligne l'omet complètement et le compilateur détermine le type correct. Ce que nous ne pouvons pas faire est d'utiliser le type var ici .

Java 11 permet que cela se produise , nous pouvons donc écrire à la place:

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

Ceci est alors cohérent avec l'utilisation du type var ailleurs dans notre code .

Lambdas nous a toujours limité à l'utilisation de noms de type complets soit pour chaque paramètre, soit pour aucun d'entre eux. Cela n'a pas changé et l'utilisation de var doit être pour chaque paramètre ou pour aucun d'entre eux :

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

Ici, le premier exemple est parfaitement valide - car les deux paramètres lambda utilisent tous deux var . Les deuxième et troisième sont cependant illégaux, car un seul paramètre utilise var , même si dans le troisième cas nous avons également un nom de type explicite.

3. Mises à jour imminentes

En plus des mises à jour déjà disponibles dans les JDK publiés, la prochaine version du JDK 12 comprend une mise à jour: JEP-325.

3.1. Changer d'expressions

JEP-325 apporte un support pour simplifier le fonctionnement des instructions switch et pour leur permettre d'être utilisées comme expressions pour simplifier encore davantage le code qui les utilise.

À l'heure actuelle, l' instruction switch fonctionne d'une manière très similaire à celles des langages tels que C ou C ++. Ces modifications la rendent beaucoup plus similaire à l' instruction when dans Kotlin ou à l' instruction match dans Scala .

Avec ces modifications, la syntaxe de définition d'une instruction switch ressemble à celle de lambdas , avec l'utilisation du symbole -> . Cela se situe entre la correspondance de casse et le code à exécuter:

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); }

Notez que le mot-clé break n'est pas nécessaire, et de plus, nous ne pouvons pas l'utiliser ici . Cela implique automatiquement que chaque correspondance est distincte et que le basculement n'est pas une option. Au lieu de cela, nous pouvons continuer à utiliser l'ancien style lorsque nous en avons besoin.

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

Nous avons couvert de nombreuses fonctionnalités différentes ici. Certains d'entre eux sont déjà disponibles, d'autres le seront bientôt, et encore d'autres sont prévus pour les prochaines versions. Comment peuvent-ils améliorer vos projets actuels et futurs?