1. What Is Project Amber
Project Amber is a current initiative from the developers of Java and OpenJDK, aiming to deliver some small but essential changes to the JDK to make the development process nicer. This has been ongoing since 2017 and has already delivered some changes into Java 10 and 11, with others scheduled for inclusion in Java 12 and yet more coming in future releases.
These updates are all packaged up in the form of JEPs – the JDK Enhancement Proposal scheme.
2. Delivered Updates
To date, Project Amber has successfully delivered some changes into currently released versions of the JDK – JEP-286 and JEP-323.
2.1. Local Variable Type Inference
Java 7 introduced the Diamond Operator as a way to make generics easier to work with. This feature means that we no longer need to write generic information multiple times in the same statement when we’re defining variables:
List<String> strings = new ArrayList<String>(); // Java 6
List<String> strings = new ArrayList<>(); // Java 7
Java 10 included the completed work on JEP-286, allowing for our Java code do define local variables without needing to duplicate the type information wherever the compiler has it already available. This is referred to in the wider community as the var keyword and brings similar functionality to Java as is available in many other languages.
With this work, whenever we’re defining a local variable, we can use the var keyword instead of the full type definition, and the compiler will automatically work out the correct type information to use:
var strings = new ArrayList<String>();
In the above, the variable strings is determined to be of type ArrayList
We can use this anywhere we use local variables, regardless of how the value is determined. This includes return types and expressions, as well as simple assignments like the above.
The word var is a special case, in that it’s not a reserved word. Instead, it’s a special type name. This means that it is possible to use the word for other parts of the code – including variable names. It is strongly recommended not to do this to avoid confusion.
We can use local type inference only when we provide an actual type as part of the declaration. It is deliberately designed not to work when the value is explicitly null, when no value is provided at all, or when the provided value can not determine an exact type – for example, a 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
However, the value can be null if it’s a return value from some other call since the call itself provides type information:
Optional<String> name = Optional.empty();
var nullName = name.orElse(null);
In this case, nullName will infer the type String because that’s what the return type of name.orElse() is.
Variables defined this way can have any other modifiers in the same way as any other variable – for example, transitive, synchronized, and final.
2.2. Local Variable Type Inference for Lambdas
The above work allows us to declare local variables without needing to duplicate type information. However, this does not work on parameter lists, and in particular, not on parameters for lambda functions, which may seem surprising.
In Java 10, we can define Lambda functions in one of two ways – either by explicitly declaring the types or by completely omitting them:
names.stream()
.filter(String name -> name.length() > 5)
.map(name -> name.toUpperCase());
Here, the second line has an explicit type declaration — String — whereas the third line omits it completely, and the compiler works out the correct type. What we can’t do is to use the var type here.
Java 11 allows this to happen, so we can instead write:
names.stream()
.filter(var name -> name.length() > 5)
.map(var name -> name.toUpperCase());
This is then consistent with the use of the var type elsewhere in our code.
Lambdas have always restricted us to using full type names either for every parameter, or for none of them. This has not changed, and the use of var must be for either every parameter or none of them:
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
Here, the first example is perfectly valid – because the two lambda parameters are both using var. The second and third ones are illegal, though, because only one parameter uses var, even though in the third case we have an explicit type name as well.
3. Imminent Updates
In addition to the updates that are already available in released JDKs, the upcoming JDK 12 release includes one update – JEP-325.
3.1. Switch Expressions
JEP-325 brings support for simplifying the way that switch statements work, and for allowing them to be used as expressions to even further simplify the code that makes use of them.
At present, the switch statement works in a very similar manner to those in languages such as C or C++. These changes make it much more similar to the when statement in Kotlin or the match statement in Scala.
With these changes, the syntax for defining a switch statement looks similar to that of lambdas, with the use of the -> symbol. This sits between the case match and the code to be executed:
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);
}
Note that the break keyword is not needed, and what’s more, we can’t use it here. It’s automatically implied that every match is distinct and fallthrough is not an option. Instead, we can continue to use the older style when we need it.
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<String> ps) { ... }
m(Function<String, String> 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<String> lenFn = (String s) -> { return s.length(); };
ToIntFunction<String> lenFn = (String s) -> s.length();
ToIntFunction<String> 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<X> {
INT<Integer>(Integer.class, 0) {
int mod(int x, int y) { return x % y; }
int add(int x, int y) { return x + y; }
},
FLOAT<Float>(Float.class, 0f) {
long add(long x, long y) { return x + y; }
}, ... ;
final Class<X> boxClass;
final X defaultValue;
Primitive(Class<X> 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
We’ve covered many different features here. Some of them are already available, others will be available soon, and yet more are planned for future releases. How can these improve your current and future projects?