Java 15 through the eyes of a Scala programmer

Adam Warski
SoftwareMill Tech Blog
9 min readOct 6, 2020

--

Time flies, and before you know it, we have another Java release. In line with the half-year release schedule, it’s time for Java 15, which paves the way for the upcoming Java 17 LTS (a year from now).

Java has seen a steady stream of improvements, many of them influenced by other JVM languages and functional programming. This includes features such as lambdas, limited local type inference or switch expressions. Scala is an especially rich source of ideas, due to its innovative object-oriented and functional programming blend.

Let’s take a look at how the (preview or final) features available in Java 15 relate to constructs known in Scala. We’ll be focusing on language features, skipping improvements to the JVM or cleaning up of the standard library. Also note that some of the described components are already available in earlier Java versions (as previews/betas).

The real Java.

Records

Let’s start with records, which are available as a (second) preview of the upcoming final version. The amount of code that has been necessary to create a simple data class has been an easy target when talking about Java’s verbosity.

When creating a data class, we typically write:

  • private final fields containing the data
  • a constructor setting the fields to given values
  • accessors to get the data
  • equals, hashCode and toString
class Person {
private final String name;
private final int age;

Person(String name, int age) {
this.name = name;
this.age = get;
}

String name() { return name; }
int age() { return age; }

public boolean equals(Object o) {
if (!(o instanceof Person)) return false;
Person other = (Person) o;
return other.name == name && other.age = age;
}

public int hashCode() {
return Objects.hash(name, age);
}

public String toString() {
return String.format("Person[name=%s, age=%d]", name, age);
}
}

There have been work-arounds, ranging from auto-generating the code using the IDE to using annotations (see project Lombok). But this always felt like a hack, not a proper way to solve this problem. Well, not anymore!

With records, the above is shortened to:

record Person(String name, int age) { }

A 21x improvement! The bytecode to which both of these compile will be similar. Record instances can be created the same way as a class:

var john = new Person("john", 76);

In Scala, there’s a very similar feature — case classes. The above example would be written as:

case class Person(name: String, age: Int)

What are the similarities between records and case classes?

  • equals, hashCode and toString methods are automatically generated (unless explicitly overridden)
  • the data fields are immutable and publicly accessible: as private final fields + no-parameter public accessor methods in Java, and as public vals in Scala
  • a constructor with all data fields is available
  • methods can be defined in the record/case class body
  • records can implement interfaces, and case classes can implement traits (which are Scala’s more powerful equivalent of Java’s interface)
  • all records extend java.lang.Record, while all case classes implement scala.Product

However, there are also some notable differences:

  • records cannot have additional state: private or public instance fields. That means that records cannot have any computed internal state; everything that is available is part of the record’s main signature. In Scala, case classes can have private or public instances fields, just like any other class.
  • records cannot extend classes, as they already implicitly extend java.lang.Record. In Scala, case classes can extend any other class, as they only implicitly implement a trait (with one exception: a case class cannot extend another case class).
  • records are always final (cannot be extended), while case classes can (though this has limited utility)
  • record constructors are very limited, as they cannot have computed state, they can be used mainly for validation, e.g.:
record Person(String name, int age) {
Person {
if (age < 0)
throw new IllegalArgumentException("Too young");
}
}

In Scala, constructors are not constrained.

Update 9th October: As pointed out by Loïc Descotte and Jarek Ratajski, an important feature of Scala’s case classes, which is missing from records, is the copy method. This allows to create a copy of the instance (we cannot modify fields due to immutability), with some fields set to new values. Copy is indeed one of most useful Scala features, and so pervasive, that it’s easy to forget it’s there!

Summing up, in Scala the case indeed behaves as a modifier for class: almost everything that is permitted in a regular class, is also permitted in a case class; the modifier generates some methods and fields for us. In Java, on the other hand, records are a separate “type of thing”, which compiles to a class, but has its own restrictions and syntax (much like enums).

Sealed classes

A very much related feature, making its debut in Java 15, is support for sealed classes and interfaces. This allows restricting the possible implementations of a class or interface. Then, any code using the abstract class or interface can safely make assumption about the possible shape of a value. Quite often we want to make our class widely accessible, but not necessarily widely extensible.

For example, the following defines an interface Animal with a closed set of implementations:

public sealed interface Animal permits Cat, Dog, Elephant {...}

An implementation then is defined as usual:

public class Cat implements Animal { ... }
public class Dog implements Animal { ... }
public class Elephant implements Animal { ... }

The implementations can either be explicitly enumerated after the permits keyword, or they can be inferred by the compiler if all implementations are in the same source file. However, the utility of the inference-style is probably limited, as each public class in Java has to be declared in a separate, top-level file.

Scala uses the same keyword (sealed) and the mechanism is very similar. However, there’s not permits keyword. The implementations are always inferred and all of them must be in the same source file as the base trait/class. This is less flexible than Java, but also Scala’s classes tend to be shorter and multiple public classes can be defined in the same source file (often named after the base trait/class):

sealed trait Animal
class Cat extends Animal { ... }
class Dog extends Animal { ... }
class Elephant extends Animal { ... }

(note that in Scala public is the default access modifier, hence it is ommited here, vs. Java’s package-private).

Both Java’s and Scala’s sealed work well with records/case classes. Using this combination, we get an implementation of Algebraic Data Types, one of the basic tools for functional programming. A record/case class is the product type, and sealed interface/trait is the sum type.

What about extending the implementations of a sealed type? In Java, we have three possibilities:

  • an implementation can be final, meaning no further subclassing is possible
  • it might be sealed itself, again enumerating the possible implementations using permits
  • or it might be non-sealed, making that particular implementation open for extension

Each implementation of a sealed type must contain exactly one of the modifiers mentioned above; however, each implementation can contain a different modifier. For example:

public sealed interface Animal permits Cat, Dog, Elephant
public final class Cat implements Animal { ... }
public sealed class Dog permits Chihuahua, Pug implements Animal {}
public non-sealed class Elephant implements Animal { ... }

Note that even if an implementation is non-sealed, code which uses the sealed type can still make assumptions about the possible implementations, due to subtyping.

In Scala, we have similar level of control, an implementation can be:

  • final
  • sealed (again all implementations must be in the same source file)
  • no modifier, making the class open for extension

The last (and default) option corresponds to non-sealed:

sealed trait Animal
final class Cat extends Animal { ... }
sealed class Dog extends Animal { ... }
class Elephant extends Animal { ... }

Pattern matching for instanceof

A small but probably very useful feature, also in “second preview”, is pattern matching for instanceof:

if (myValue instanceof String s) {
// s is in scope and is a String
} else {
// s is not in scope
}

The new syntax not only saves us from adding a var s = (String) myValue type cast in the if branch, but also eliminates the possibility of a silly mistake, where the if-condition and the typecast go out of sync (e.g. we perform the typecast in the wrong branch).

This is slightly similar to flow-typing as know from TypeScript, however here we do need to introduce a new name (in the example, s) for the cast value.

The Scala equivalent employs pattern matching:

myValue match {
case s: String => // s is in scope and is a String
case _ => // s is not in scope
}

The syntax is quite different, but the overall syntactic overhead is similar.

Java also has a shorter version, useful when writing compound conditions, e.g.:

if (myValue instanceof String s && s.length() > 42) { ... }

In Scala, we would write that as:

myValue match {
case s: String if s.length() > 42 => ...
}

One case where Java’s syntax is more concise, is when we want to store the result of the condition as a value:

var isLongString = myValue instanceof String s && s.length() > 42

Scala’s pattern matches are typically written in multiple lines (for readability), so you would have the following code:

val isLongString = myValue match {
case s: String if s.length() > 42 => true
case _ => false
}

On the other hand, Scala’s pattern matching is a much more general and powerful mechanism. For example, we can deconstruct case classes, which have been mentioned before, including arbitrary levels of nesting.

What’s next for Java’s pattern matching? As hinted in the release notes, pattern matching and sealed classes complement each other naturally. When pattern-matching on a value of a sealed type, the compiler can statically verify that we have covered all possible cases. Indeed, Scala performs such exhaustiveness checks for match expressions on a value of a sealed trait or class.

Text blocks

Multi-line strings are often a necessary evil that we have to live with. So far in Java we had to resort to concatenating multiple strings with explicit \n newline characters. Not anymore! Text blocks are out of the “preview” phase and available as a normal feature.

Text blocks are delimited with triple quotes: """, for example:

var response = """
<html>
<body>Internal server error</body>
</html>"""

The opening """ must be followed by a newline, from which the actual string starts. Text blocks are interesting because of their whitespace handling. The above example would create a string with the following content:

<html>
<body>Internal server error</body>
</html>

Note that the initial whitespace has been removed — and most probably, that’s exactly what you wanted! The rule is quite simple: leading columns of whitespace are stripped until the first non-whitespace character is encountered. Similarly, unnecessary trailing whitespace is removed as well, giving us nicely trimmed text blocks.

Scala also has text blocks, using the same triple-quote delimiter, and doesn’t require a newline after the opening quotes:

val response = """<html>
<body>Internal server error</body>
</html>"""

However, the above in Scala would include the leading whitespace — it is not automatically removed. We need to explicitly trim the margin, using an explicit separator character

val response = """<html>
| <body>Internal server error</body>
|</html>""".stripMargin)

(By default, | is used as the separator, but another character can be used and passed to stripMargin instead.)

While the Scala version is more powerful, Java’s is more convenient and addresses the “default” case better.

Scala can also do string interpolation, something that is missing from Java and has to be addressed using String::formatted. In Scala, we just need to prefix the string with s:

val message = "Internal server error"
val
response = s"""<html>
| <body>$message</body>
|</html>""".stripMargin)

Summing up

Neither Scala, nor Java are new languages. They have to evolve taking into account existing code-bases and the “spirit” of the platform. Still, Scala had the opportunity to build on top of experience gained from Java and other languages. Hence, it avoided many of Java’s traps.

The new features in recent Java versions are welcome additions, making lives of Java programmers much easier. The code can be more concise, and easier to read, as the “essence” of the problem can be made more visible.

However, these extensions are often quite irregular, as each solves a single problem, rather than providing a general way of structuring code. For example, switch expressions, introduced in Java 12, enable treating a switch as an expression (the result can be e.g. assigned to a variable). In Scala, everything is an expression, so no special syntax or support is needed.

Similarly pattern matching — it’s a general mechanism, again exposing a new type of expressions, rather than special syntax for type casts. Or records — Scala extends the existing class concept, while Java introduces a new type of a structure.

That’s why it might not be surprising that Scala is in fact a smaller language than Java — at least when it comes to comparing the size of the grammar. That said, while Scala has relatively few basic features, they are mostly general and hence interact with each other, creating a number of interesting combinations (and some uninteresting as well).

While waiting for Java 16 & beyond (more interesting features are in the pipeline), Scala has a lot more to offer beyond what we’ve covered here! If you’d like to learn more, check out our Scala-start page &&/|| subscribe to Scala Times!

--

--

Software engineer, Functional Programming and Scala enthusiast, SoftwareMill co-founder