What Is Project Amber in Java?
Curious about Project Amber? Take a look at the ongoing effort to enhance the productivity of Java developers everywhere.
Join the DZone community and get the full member experience.
Join For FreeIn this post, we’re going to delve into some details of the features being targeted in Project Amber, which was introduced in early 2017 by its lead and language architect Brian Goetz. This project aims to add some really cool beans to the Java programming language that improve the developer’s productivity when writing Java code.
As this is still work in progress, other features could be added to the project in the future. Even though these features may seem to have been addressed late in the Java timeline, it’s worth considering that the Java team has historically been rather cautious in introducing new features to evolve the language, as Goetz explains in a talk about the project.
Local Variable Type Inference
In Java 5, generics were introduced with the possibility for the compiler to infer type arguments during generic method calls, as shown in the below example:
private <T> void foo(T t) {
...
}
public void bar() {
this.foo(1); // with implicit type, compiler infers type argument
this.<Integer>foo(1); // with explicit typing
}
Further enhancements to type inference were done over the next releases, including the diamond operator in Java 7, enhancements in Java 8 along with lambda and stream support, and in Java 9 allowing the diamond operator in anonymous classes when the inferred type is denotable. With this feature (specified in JEP 286), the compiler will be able to infer declaration types of local variables, subject to certain limitations. The main requirement for the compiler is that the initializer needs to be included with the variable declaration. With this enhancement, a statement like:
File inputFile = new File("input.txt");
Could be written as:
var inputFile = new File("input.txt");
Like any other form of type inference, the main benefit is avoiding the redundant typing of the variable type when it can be easily known from the right hand side of the assignment. It is important to remember here that the var
keyword does not mean that the variable is dynamically typed. It is just a syntax to avoid writing the manifest type of the local variable; the static typing nature of Java remains intact. Strictly speaking, var
is a reserved type name that gets desugared to the variable initializer type by the compiler.
Not every form of local variable declaration can use var
to infer the declaration or manifest type of the variable. The following cases do not allow the use of var:
- Local variables without initializers, such as File inputFile;
- Local variables initialized to null.
- Initializers that expect a target type, such as a lambda, a method reference or an array initializer.
As such, below are examples where type inference is not allowed:
// not allowed, lambda expression needs an explicit target-type
var func = s -> Integer.valueOf(s);
// not allowed, method reference needs an explicit target-type
var biConsumer = LogProcessor::process;
// not allowed, array initializer needs an explicit target-type
var array = { 1, 2 };
The majority of local variable declarations in typical code could benefit from this feature. For example, in the OpenJDK codebase, only 13% of local variables cannot be re-written using var
. Therefore, the cost of broadening local type inference to include cases like the above may be too high compared to the amount of applicable code that can further benefit from it.
This feature is included in the planned Java 10 release, which is expected to be available in March 2018.
Enhanced Enums
This feature enhances enums in two aspects. First, it allows declaring a generic enum, which combines the flexibility and type safety of generics with the simplicity and powerful semantics of an enum. Second, it enhances enums so that an enum constant that is declared as generic or overrides behavior via a class body gets its own type information, along with its own state and behavior.
In some use cases, we may need to define enum constants where each is bound to a certain type. A typical example is an enum that contains mappings to Java types, where a generic enum can be used shown in the below JsonType
example:
public enum JsonType<T> {
STRING<String>(String.class),
LONG<Long>(Long.class),
DOUBLE<>(Double.class), // can use a diamond operator to infer
BOOLEAN<>(Boolean.class),
...
final Class<T> mappedClass;
JsonType(Class<T> mappedClass) {
this.mappedClass = mappedClass;
}
public T convert(Object o) {
...
}
}
In this case, the enum constant STRING
has a sharper type JsonType<String>
, enum constant LONG
is of type JsonType<Long>
, and so on. One could further customize each enum constant with additional state and/or methods:
public enum JsonType<T> {
STRING<>(String.class),
// LONG is has an anonymous class as type now,
// we can use a diamond as long as inferred type is denotable
LONG<>(Long.class) {
public String desc = "Long JSON type";
public boolean isLongValue(JSONValue value) {
...
}
},
DOUBLE<>(Double.class),
BOOLEAN<>(Boolean.class),
...
}
Since the enum constant LONG
has a class body, its type is an anonymous class whose supertype is JsonType<Long>
.
This feature is discussed more in JEP 301 which still has a “Candidate” status, so not all risks may have been addressed and is not expected to reach JDK 10.
Enhancements to Lambda Expressions
These are a couple of additional features added to lambda along with improving type inference for methods involving lambdas as arguments. The first feature is the ability to use an underscore to denote an unused parameter in a lambda:
BiFunction<Integer, String, String> biss = (i, _) -> String.valueOf(i);
As of Java 9, an underscore can no longer be used as an identifier, and with this feature, it now carries a special meaning in the context of lambdas.
The second feature introduced in this JEP for lambdas is the ability to shadow variables declared in the enclosing scope of a lambda by re-using the same variable names to declare the lambda parameters:
int i = 0;
// can declare lambda parameter named i, shadowing the local variable i
BiFunction<Integer, String, String> biss = (i, _) -> String.valueOf(i);
Currently, it is not allowed to re-use i
in the lambda expression because lambdas are lexically scoped and generally do not allow shadowing variables. Last but not least, improving overload resolution for methods invoked with either a lambda or a method reference as an argument is optionally targeted in this project. This should fix false compilation errors that may be commonly encountered when writing methods that accept functional interfaces:
m(Predicate<String> ps) { ... }
m(Function<String, String> fss) { ... }
m(s -> false) // error due to ambiguity, although Predicate
// should have been inferred
class Foo {
static boolean g(String s) { return false }
static boolean g(Integer i) { return false }
}
m(Foo::g) // error due to ambiguity, although boolean g(String s)
// should have been selected
Pattern Matching
The next feature in the Amber project introduces a powerful construct called a pattern. The motivation behind this feature is the commonly used boilerplate code shown below:
String content = null;
if (msg instanceof JsonMessage) {
content = unmarshalJson((JsonMessage) msg);
} else if (msg instanceof XmlMessage) {
content = unmarshalXml((XmlMessage) msg);
} else if (msg instanceof PlainTextMessage) {
content = ((PlainTextMessage) msg).getText();
} ...
Each condition branch checks if the object is of a certain type, then casts it to that type and extracts some information from it. Pattern matching is a generalization of this “test-extract-bind” technique and can be defined as:
- a predicate that can be applied to a target
- a set of binding variables that are extracted from the object matching the predicate
Instead of the above, we could apply a type-test pattern on the object msg
using a new matches operator. As a first step, this would remove the redundant cast:
String content = null;
if (msg matches JsonMessage json) {
content = unmarshalJson(json);
} else if (msg matches XmlMessage xml) {
content = unmarshalXml(xml);
} else if (msg matches PlainTextMessage text) {
content = text.getText();
} ...
The existing switch statement already makes use of the simplest form of patterns: the constant pattern. Given an object whose type is allowed in a switch statement, we test the object if it matches any of several constant expressions. Now the switch statement would also benefit from type-test patterns:
String content;
switch (msg) {
case JsonMessage json: content = unmarshalJson(json); break;
case XmlMessage xml: content = unmarshalXml(xml); break;
case PlainTextMessage text: content = text.getText(); break;
...
default: content = msg.toString();
}
This switch could be now be considered a “type switch” but it’s actually a generalized switch that can take other types of patterns; in this example, type-test patterns. In addition to generalizing a switch statement, this feature suggests a further improvement by allowing a switch to be also used as an expression instead of a statement, making the above look even more readable:
String content = switch (msg) {
case JsonMessage json -> unmarshalJson(json);
case XmlMessage xml -> unmarshalXml(xml);
case PlainTextMessage text -> text.getText();
...
default -> msg.toString();
}
Another kind of a pattern that can be used for matching and that is further proposed in this JEP is a deconstruction pattern. It matches an expression against a certain type, and extracts variables based on the signature of an existing constructor in the matched type:
// Using a deconstruction pattern with nested type patterns
if (item matches Book(String isbn, int year)) {
// do something with isbn and published
}
What’s really nice about this is that the component String isbn
is itself a type-test pattern that matches the isbn
property of the Book
object against a String
and extracts it into the isbn
variable, and the same for the int year
pattern. This means patterns may be nested within each other. Another example is nesting a constant pattern in a deconstruction pattern:
// Using a deconstruction pattern with a nested constant pattern
if (item matches Book("978-0321349606")) {
...
}
Finally, within a future work scope of this JEP are sealed types, which allow defining types whose subtypes can be limited by the programmer. With this feature, a type could be marked as “sealed” to mean that the subtypes are restricted to a known set. It is similar to a final type, but instead gives the programmer a way to restrict the hierarchy of child types. For example, in an bookstore application we may only need to handle certain items like books, DVDs, etc. In this case we could seal our BookstoreItem
parent class. This can be very useful because it removes the burden of handling default cases whenever we are switching over the possible subtypes.
Data Classes
A lot of times all that a class is responsible of is holding data. And with the current way to writing such classes, we usually end up writing too much boilerplate to customize methods based on the state of the object, e.g. by overriding equals()
, hashCode()
, toString()
, etc. Furthermore, there is nothing in the language that just tells the compiler or the reader that this class is a simple data holder class.
In order to define such semantics, this feature seeks to introduce data classes of the following form (syntax and semantics still under discussion; for a comprehensive discussion see this document on the OpenJDK site):
__data class Point(int x, int y) { }
which would be translated into the following at compile time:
final class Point extends java.lang.DataClass {
final int x;
final int y;
public Point(int x, int y) {
this.x = x;
this.y = y;
}
// destructuring pattern for Point(int x, int y)
// state-based equals, hashCode, and toString
// public read accessors for x and y
}
The general idea is to have the compiler generate all that boilerplate for the programmer (similar to what is done with enums), and have the programmer still able to override methods like equals()
or implementing interfaces. At this point, some design decisions are in progress, for example it is undecided whether immutability will be enforced or if mutability could be allowed, which would definitely impact the implementation of this feature along with the thread-safety of such classes.
Summary
Project Amber aims to bring features that can make writing Java code more readable and concise, and target specific use cases such as using generic enums or data classes. Local variable type inference enables the programmer to defer thinking about the variable type to whenever it is initialized. Enhanced enums allows a more flexible approach to solving specific problems with less code. Lambda leftovers improves lambda support with a couple of small changes. Pattern matching provides a powerful construct to writing conditional logic and reduces boilerplate coding. And finally, data classes allow the programmer to segregate plain data carriers from other classes. So far, only local variable type inference is planned in the Java 10 release, but with the accelerated release timeline of Java, the others can be rolled out as soon as they are ready.
Published at DZone with permission of Mahmoud Anouti, DZone MVB. See the original article here.
Opinions expressed by DZone contributors are their own.
Comments