Introduzione al progetto Amber

1. Che cos'è Project Amber

Project Amber è un'iniziativa attuale degli sviluppatori di Java e OpenJDK, con l'obiettivo di fornire alcune piccole ma essenziali modifiche al JDK per rendere più piacevole il processo di sviluppo . Questo è in corso dal 2017 e ha già apportato alcune modifiche a Java 10 e 11, con altre programmate per l'inclusione in Java 12 e altre ancora in arrivo nelle versioni future.

Questi aggiornamenti sono tutti confezionati sotto forma di PEC: lo schema di proposta di miglioramento JDK.

2. Aggiornamenti consegnati

Ad oggi, Project Amber ha fornito con successo alcune modifiche alle versioni attualmente rilasciate di JDK: JEP-286 e JEP-323.

2.1. Inferenza del tipo di variabile locale

Java 7 ha introdotto Diamond Operator come un modo per rendere più facile lavorare con i generici . Questa caratteristica significa che non abbiamo più bisogno di scrivere informazioni generiche più volte nella stessa istruzione quando definiamo le variabili:

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

Java 10 includeva il lavoro completato su JEP-286, consentendo al nostro codice Java di definire variabili locali senza la necessità di duplicare le informazioni sul tipo ovunque il compilatore le abbia già disponibili . Questa viene definita nella comunità più ampia come la parola chiave var e porta funzionalità simili a Java come è disponibile in molte altre lingue.

Con questo lavoro, ogni volta che definiamo una variabile locale, possiamo usare la parola chiave var invece della definizione completa del tipo , e il compilatore elaborerà automaticamente le informazioni di tipo corrette da usare:

var strings = new ArrayList();

In quanto sopra, si determina che le stringhe di variabili siano di tipo ArrayList () , ma senza la necessità di duplicare le informazioni sulla stessa riga.

Possiamo usarlo ovunque usiamo variabili locali , indipendentemente da come viene determinato il valore. Ciò include tipi ed espressioni restituite, nonché semplici assegnazioni come quella sopra.

La parola var è un caso speciale, in quanto non è una parola riservata. Invece, è un nome di tipo speciale. Ciò significa che è possibile utilizzare la parola per altre parti del codice, inclusi i nomi delle variabili. Si consiglia vivamente di non farlo per evitare confusione.

Possiamo usare l'inferenza del tipo locale solo quando forniamo un tipo effettivo come parte della dichiarazione . È deliberatamente progettato per non funzionare quando il valore è esplicitamente nullo, quando non viene fornito alcun valore o quando il valore fornito non può determinare un tipo esatto, ad esempio una definizione 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

Tuttavia, il valore può essere null se è un valore restituito da un'altra chiamata poiché la chiamata stessa fornisce informazioni sul tipo:

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

In questo caso, nullName dedurrà il tipo String perché questo è il tipo restituito di name.orElse () .

Le variabili definite in questo modo possono avere qualsiasi altro modificatore allo stesso modo di qualsiasi altra variabile , ad esempio transitiva, sincronizzata e finale .

2.2. Inferenza del tipo di variabile locale per Lambda

Il lavoro precedente ci consente di dichiarare le variabili locali senza la necessità di duplicare le informazioni sul tipo. Tuttavia, questo non funziona sugli elenchi di parametri e, in particolare, non sui parametri per le funzioni lambda, il che può sembrare sorprendente.

In Java 10, possiamo definire le funzioni Lambda in due modi: dichiarando esplicitamente i tipi o omettendoli completamente:

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

Qui, la seconda riga ha una dichiarazione di tipo esplicita - String - mentre la terza riga la omette completamente e il compilatore elabora il tipo corretto. Quello che non possiamo fare è usare il tipo var qui .

Java 11 consente che ciò accada , quindi possiamo invece scrivere:

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

Questo è quindi coerente con l'uso del tipo var altrove nel nostro codice .

Lambdas ci ha sempre limitato all'utilizzo di nomi di tipi completi per ogni parametro o per nessuno di essi. Questo non è cambiato e l'uso di var deve essere per ogni parametro o per nessuno di essi :

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

Qui, il primo esempio è perfettamente valido, perché i due parametri lambda utilizzano entrambi var . Il secondo e il terzo sono illegali, però, perché solo un parametro usa var , anche se nel terzo caso abbiamo anche un nome di tipo esplicito.

3. Aggiornamenti imminenti

Oltre agli aggiornamenti che sono già disponibili nei JDK rilasciati, l'imminente versione JDK 12 include un aggiornamento: JEP-325.

3.1. Cambia espressioni

JEP-325 offre supporto per semplificare il modo in cui funzionano le istruzioni switch e per consentire loro di essere utilizzate come espressioni per semplificare ulteriormente il codice che le utilizza.

Al momento, l' istruzione switch funziona in modo molto simile a quelli in linguaggi come C o C ++. Queste modifiche lo rendono molto più simile all'istruzione when in Kotlin o all'istruzione match in Scala .

Con queste modifiche, la sintassi per la definizione di un'istruzione switch è simile a quella di lambda , con l'uso del simbolo -> . Questo si trova tra la corrispondenza tra maiuscole e minuscole e il codice da eseguire:

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

Nota che la parola chiave break non è necessaria e, inoltre, non possiamo usarla qui . È automaticamente implicito che ogni corrispondenza è distinta e che il fallthrough non è un'opzione. Invece, possiamo continuare a utilizzare il vecchio stile quando ne abbiamo bisogno.

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

Abbiamo coperto molte funzionalità diverse qui. Alcuni di loro sono già disponibili, altri lo saranno presto e altri ancora sono previsti per le versioni future. Come possono migliorare i tuoi progetti attuali e futuri?