Pengantar Proyek Amber

1. Apa Proyek Amber

Project Amber adalah inisiatif terkini dari pengembang Java dan OpenJDK, yang bertujuan untuk memberikan beberapa perubahan kecil namun penting pada JDK untuk membuat proses pengembangan lebih baik . Ini telah berlangsung sejak 2017 dan telah mengirimkan beberapa perubahan ke dalam Java 10 dan 11, dengan yang lain dijadwalkan untuk disertakan di Java 12 dan lebih banyak lagi yang akan datang di rilis mendatang.

Pembaruan ini semuanya dikemas dalam bentuk JEP - skema Proposal Peningkatan JDK.

2. Pembaruan yang Disampaikan

Hingga saat ini, Project Amber telah berhasil menyampaikan beberapa perubahan ke dalam versi JDK yang saat ini dirilis - JEP-286 dan JEP-323.

2.1. Inferensi Jenis Variabel Lokal

Java 7 memperkenalkan Diamond Operator sebagai cara untuk membuat obat generik lebih mudah digunakan . Fitur ini berarti bahwa kita tidak perlu lagi menulis informasi umum beberapa kali dalam pernyataan yang sama saat kita mendefinisikan variabel:

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

Java 10 menyertakan pekerjaan yang telah selesai pada JEP-286, yang memungkinkan kode Java kita menentukan variabel lokal tanpa perlu menduplikasi informasi tipe di mana pun kompilator memilikinya sudah tersedia . Ini disebut di komunitas yang lebih luas sebagai kata kunci var dan menghadirkan fungsionalitas yang mirip dengan Java seperti yang tersedia di banyak bahasa lain.

Dengan pekerjaan ini, setiap kali kita mendefinisikan variabel lokal, kita dapat menggunakan kata kunci var daripada definisi tipe lengkap , dan kompilator akan secara otomatis mengerjakan informasi tipe yang benar untuk digunakan:

var strings = new ArrayList();

Di atas, string variabel ditentukan menjadi tipe ArrayList () , tetapi tanpa perlu menduplikasi informasi pada baris yang sama.

Kita dapat menggunakan ini di mana pun kita menggunakan variabel lokal , terlepas dari bagaimana nilai ditentukan. Ini termasuk tipe dan ekspresi yang dikembalikan, serta tugas sederhana seperti di atas.

Kata var adalah kasus khusus, yang bukan merupakan kata khusus. Sebaliknya, ini adalah nama tipe khusus. Ini berarti bahwa dimungkinkan untuk menggunakan kata tersebut untuk bagian lain dari kode - termasuk nama variabel. Sangat disarankan untuk tidak melakukan ini untuk menghindari kebingungan.

Kita bisa menggunakan inferensi tipe lokal hanya jika kita menyediakan tipe aktual sebagai bagian dari deklarasi . Ini sengaja dirancang untuk tidak berfungsi ketika nilainya secara eksplisit null, ketika tidak ada nilai yang diberikan 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

Namun, nilainya bisa null jika itu adalah nilai yang dikembalikan dari beberapa panggilan lain karena panggilan itu sendiri menyediakan informasi tipe:

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

Dalam kasus ini, nullName akan menyimpulkan tipe String karena itulah tipe kembalian dari name.orElse () .

Variabel yang ditentukan dengan cara ini dapat memiliki pengubah lain dengan cara yang sama seperti variabel lainnya - misalnya, transitif, tersinkronisasi, dan final .

2.2. Inferensi Jenis Variabel Lokal untuk Lambdas

Pekerjaan di atas memungkinkan kita untuk mendeklarasikan variabel lokal tanpa perlu menduplikasi informasi tipe. Namun, ini tidak berfungsi pada daftar parameter, dan khususnya, tidak pada parameter untuk fungsi lambda, yang mungkin tampak mengejutkan.

Di Java 10, kita dapat mendefinisikan fungsi Lambda dengan salah satu dari dua cara - baik dengan mendeklarasikan tipe secara eksplisit atau dengan menghilangkannya sama sekali:

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

Di sini, baris kedua memiliki deklarasi tipe eksplisit - String - sedangkan baris ketiga menghilangkannya sepenuhnya, dan kompilator menentukan tipe yang benar. Yang tidak dapat kita lakukan adalah menggunakan tipe var di sini .

Java 11 memungkinkan ini terjadi , jadi kita bisa menulis:

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

Ini kemudian konsisten dengan penggunaan tipe var di tempat lain dalam kode kita .

Lambdas selalu membatasi kami untuk menggunakan nama tipe lengkap baik untuk setiap parameter, atau tidak satupun dari mereka. Ini tidak berubah, dan penggunaan var harus untuk setiap parameter atau tidak satu pun dari mereka :

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-benar valid - karena dua parameter lambda sama-sama menggunakan var . Yang kedua dan ketiga adalah ilegal, karena hanya satu parameter yang menggunakan var , meskipun dalam kasus ketiga kami juga memiliki nama tipe eksplisit.

3. Pembaruan Segera

Selain pembaruan yang sudah tersedia di JDK yang dirilis, rilis JDK 12 yang akan datang menyertakan satu pembaruan - JEP-325.

3.1. Ganti Ekspresi

JEP-325 memberikan dukungan untuk menyederhanakan cara kerja pernyataan switch , dan untuk memungkinkannya digunakan sebagai ekspresi untuk lebih menyederhanakan kode yang menggunakannya.

Saat ini, pernyataan switch bekerja dengan cara yang sangat mirip dengan bahasa seperti C atau C ++. Perubahan ini membuatnya jauh lebih mirip dengan pernyataan when di Kotlin atau pernyataan match di Scala .

Dengan perubahan ini, sintaks untuk mendefinisikan pernyataan switch terlihat mirip dengan lambda , dengan penggunaan simbol -> . Ini berada di antara kasus yang cocok dan kode yang akan dieksekusi:

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 bahwa kata kunci break tidak diperlukan, dan terlebih lagi, kami tidak dapat menggunakannya di sini . Secara otomatis tersirat bahwa setiap pertandingan berbeda dan fallthrough bukanlah sebuah pilihan. Sebaliknya, kita dapat terus menggunakan gaya lama saat kita membutuhkannya.

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 membahas banyak fitur berbeda di sini. Beberapa di antaranya sudah tersedia, yang lain akan segera tersedia, dan lebih banyak lagi yang direncanakan untuk rilis mendatang. Bagaimana ini dapat meningkatkan proyek Anda saat ini dan masa depan?