Pengenalan Projek Amber

1. Apakah Projek Amber

Project Amber adalah inisiatif terkini dari pemaju Java dan OpenJDK, yang bertujuan untuk memberikan beberapa perubahan kecil tetapi penting pada JDK agar proses pembangunan lebih baik . Ini telah berlangsung sejak tahun 2017 dan telah memberikan beberapa perubahan ke dalam Java 10 dan 11, dengan yang lain dijadwalkan untuk dimasukkan ke dalam Java 12 dan masih banyak lagi yang akan datang dalam rilis mendatang.

Kemas kini ini dikemas dalam bentuk JEP - skema Cadangan Peningkatan JDK.

2. Kemas kini yang Dihantar

Sehingga kini, Project Amber telah berjaya menyampaikan beberapa perubahan pada versi JDK - JEP-286 dan JEP-323 yang sedang dikeluarkan.

2.1. Inferens Jenis Pembolehubah Tempatan

Java 7 memperkenalkan Diamond Operator sebagai cara untuk membuat generik lebih mudah digunakan . Ciri ini bermaksud bahawa kita tidak perlu lagi menulis maklumat generik berkali-kali dalam pernyataan yang sama ketika kita menentukan pemboleh ubah:

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

Java 10 menyertakan karya yang telah diselesaikan pada JEP-286, yang memungkinkan untuk kod Java kami menentukan pemboleh ubah tempatan tanpa perlu menduplikasi jenis maklumat di mana pun penyusunnya sudah tersedia . Ini disebut dalam masyarakat luas sebagai kata kunci var dan membawa fungsi yang serupa dengan Java seperti yang terdapat dalam banyak bahasa lain.

Dengan kerja ini, setiap kali kita menentukan pemboleh ubah tempatan, kita dapat menggunakan kata kunci var dan bukannya definisi jenis penuh , dan penyusun akan secara automatik mencari maklumat jenis yang betul untuk digunakan:

var strings = new ArrayList();

Di atas, rentetan pemboleh ubah ditentukan jenis ArrayList () , tetapi tanpa perlu menduplikasi maklumat pada baris yang sama.

Kita boleh menggunakannya di mana sahaja kita menggunakan pemboleh ubah tempatan , tanpa mengira bagaimana nilainya ditentukan. Ini termasuk jenis dan ungkapan pengembalian, serta tugas sederhana seperti di atas.

Perkataan var adalah kes khas, kerana itu bukan perkataan terpelihara. Sebaliknya, ia adalah nama jenis khas. Ini bermaksud bahawa mungkin menggunakan perkataan untuk bahagian lain kod - termasuk nama pemboleh ubah. Sangat digalakkan untuk tidak melakukan ini untuk mengelakkan kekeliruan.

Kami dapat menggunakan inferens jenis tempatan hanya apabila kami memberikan jenis sebenar sebagai bagian dari deklarasi . Ia sengaja dirancang untuk tidak berfungsi ketika nilainya jelas , jika tidak ada nilai sama sekali, atau ketika nilai yang diberikan tidak dapat menentukan jenis yang tepat - misalnya, definisi 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

Walau bagaimanapun, nilainya boleh menjadi nol jika itu adalah nilai kembali dari beberapa panggilan lain kerana panggilan itu sendiri memberikan maklumat jenis:

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

Dalam kes ini, nullName akan menyimpulkan jenis String kerana itulah jenis nama pengembalian.orElse () .

Pemboleh ubah yang ditentukan dengan cara ini boleh mempunyai pengubah lain dengan cara yang sama seperti pemboleh ubah lain - misalnya, transitif, disegerakkan, dan akhir .

2.2. Inferens Jenis Pembolehubah Tempatan untuk Lambdas

Karya di atas membolehkan kita menyatakan pemboleh ubah tempatan tanpa perlu mendua maklumat jenis. Walau bagaimanapun, ini tidak berfungsi pada senarai parameter, dan khususnya, tidak pada parameter untuk fungsi lambda, yang mungkin kelihatan mengejutkan.

Di Java 10, kita dapat menentukan fungsi Lambda dengan salah satu dari dua cara - baik dengan menyatakan secara eksplisit jenisnya atau dengan menghilangkannya sepenuhnya:

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

Di sini, baris kedua mempunyai deklarasi jenis eksplisit - String - sedangkan baris ketiga menghilangkannya sepenuhnya, dan penyusunnya menghasilkan jenis yang betul. Apa yang tidak dapat kita lakukan adalah menggunakan jenis var di sini .

Java 11 memungkinkan ini terjadi , jadi kita dapat menulis:

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

Ini kemudiannya selaras dengan penggunaan jenis var di tempat lain dalam kod kami .

Lambdas selalu mengehadkan kami untuk menggunakan nama jenis penuh sama ada untuk setiap parameter, atau untuk tidak satu pun dari mereka. Ini tidak berubah, dan penggunaan var harus untuk setiap parameter atau tidak ada di antaranya :

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

Di sini, contoh pertama benar - kerana kedua-dua parameter lambda menggunakan var . Walau bagaimanapun, yang kedua dan ketiga tidak sah, kerana hanya satu parameter yang menggunakan var , walaupun dalam kes ketiga kita juga mempunyai nama jenis eksplisit.

3. Kemas kini yang akan berlaku

Sebagai tambahan kepada kemas kini yang sudah tersedia dalam JDK yang dilepaskan, pelepasan JDK 12 yang akan datang termasuk satu kemas kini - JEP-325.

3.1. Tukar Ekspresi

JEP-325 memberikan sokongan untuk mempermudah cara menukar pernyataan berfungsi, dan untuk membiarkannya digunakan sebagai ungkapan untuk lebih mempermudah kod yang menggunakannya.

Pada masa ini, pernyataan beralih berfungsi dengan cara yang sangat serupa dengan bahasa dalam bahasa seperti C atau C ++. Perubahan ini menjadikannya lebih mirip dengan pernyataan ketika di Kotlin atau pernyataan perlawanan di Scala .

Dengan perubahan ini, sintaks untuk menentukan pernyataan beralih kelihatan serupa dengan lambda , dengan penggunaan simbol -> . Ini terletak antara padanan kes dan kod yang akan dijalankan:

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

Perhatikan bahawa kata kunci rehat tidak diperlukan, dan apa lagi, kami tidak dapat menggunakannya di sini . Secara automatik tersirat bahawa setiap perlawanan adalah berbeza dan kejayaan bukan pilihan. Sebagai gantinya, kita boleh terus menggunakan gaya lama apabila kita memerlukannya.

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

Kami telah merangkumi pelbagai ciri di sini. Sebahagian daripadanya sudah tersedia, yang lain akan tersedia tidak lama lagi, namun masih banyak lagi yang dirancang untuk pelepasan masa depan. Bagaimana ini dapat meningkatkan projek semasa dan masa depan anda?