Upcoming Java Language Features that I Look Forward To

Upcoming Java Language Features that I Look Forward To

Ever since Java 8's introduction of lambda statements, Java has been releasing more language updates on every major version release. From Java 9's Jigsaw (Java Platform Module System), Java 10's local variable inference using var, to the preview features in the current Java 14. More and more language features are coming to Java to promote better coding and architecture designs.

With all of these feature that was introduced in the previous Java versions, there are still a ton of feature yet to come in future releases!

Here are the upcoming Java Language features that I look forward to!

Just some disclaimers: Remember that these features are not yet released. Some of the examples shown here are subject to change when the feature is actually released. Additionally, some of the features here are only available as JDK Enhancement Proposal (JEP) or hidden in the JDK that can only be enabled by using --enable-preview --source 14  JVM flag in javac and java commands. At the time of writing, Java 14 is the latest JDK release and I can only test language features that came with it.

Records

Value-based classes or record is planned to be released in Java 15, but already usable in Java 14 by enabling the preview flag.

Record is another way to define a type in addition to class, interface, and enum. This type represents a data class that cannot be extended by a subclass.

To create a record, we need to use the new record syntax:

public record Keyboard(
    String name,
    String switchName
) {}
A simple Keyboard record. The Java record syntax kinda looks like Kotlin's data class syntax.

A record instance can be created just like how we create objects. The difference of a record from a data class is that records are always immutable and its accessors does not start with the word get.

var keeb = new Keyboard("End Game Keyboard", "Cherry MX Blue");

System.out.println("Name: " + keeb.name());
System.out.println("Switch: " + keeb.switchName());
The Keyboard record from the previous example is now instantiated and used to print its content.

In the above example, you can notice that the tuple used to declare the fields of the record becomes the constructor of the record. The field name also became the accessor name. Based on my experience with records, this is a problem when using frameworks that expects a get- style accessors. This problem can be worked-around by creating a custom getter method in the record:

public record Keyboard(
    String name,
    String switchName
) {
    public String getName() {
      return name;
    }

    public String getSwitchName() {
      return switchName;
    }
}

That does the job for backwards compatibility, but I think doing so loses the record's purpose to reduce boilerplate code for data classes.

Pattern Matching

This is another preview feature that can only be used by enabling the preview flag. Pattern matching simplifies data type checking. This is not really a big syntax update, but it helps a lot. This language feature is particularly useful when defining your own equals() method or when making a factory that is based from data types.

Object obj = 1;
int sum = 0;

if (obj instanceof Integer) {
    sum += (Integer) obj;
}
Old style type checking. After a successful check, the variable can now be safely cast to the wanted data type.
Object obj = 1;
int sum = 0;

if (obj instanceof Integer i) {
    sum += i;
}
The pattern matching syntax in the language previews. There is no need to cast since the variable i is created on successful type check.

Switch Expressions

Actually this feature is already available in Java 14, but I haven't used it yet in a project. Also, since Java 14 is not an LTS release, we expect that this will replaced in upcoming Java versions and will be production ready only on Java 17, which is the next LTS release.

What's more fun in switch expressions is that there are more incoming Java features that will extend the power of this langauge feature.

For now, let's see what does this do.

As the name suggest, the switch expression syntax makes the classic switch statement into an expression. This works much like SQL's case...when statement.

KeyboardSwitchType type = keyboard.switchType();

switch (type) {
  case CLICKY:
      System.out.println("Click click!");
      break;
  case LINEAR:
      System.out.println("Smooth keystrokes...");
      break;
  case TACTILE:
      System.out.println("I feel the keyboard's kiss!");
      break;
  default:
      throw new AssertionError("Value is not in the enum: " + type);
}
The classic switch statement testing through different values of the enum KeyboardSwitchType.

In the classic switch statement, each case block holds their own set of statements and ends with the break; statement to prevent statements from the next case blocks from executing.

KeyboardSwitchType type = keyboard.switchType();

String message = switch (type) {
  case CLICKY -> "Click click!";
  case LINEAR -> "Smooth keystrokes...";
  case TACTILE -> "I feel the keyboard's kiss!";
}
System.out.println(message);
The same code as above but refactored to the new switch expression syntax.

In the example above, we can see that the swtich statement has gone smaller due to the disappearance of some of the statements from the classic switch statement.

Differences between classic switch statement and the new switch expression

  • The symbol to stop the test value has been changed from colon : to arrow ->. When using the arrow symbol, it tells the compiler that you are doing a switch expression.
  • No need for break statement. The result of a switch expression is, of course, the evaulation of the expression at the right side of the -> symbol. This language feature is not intended to execute multiple statements per case.
  • The default case is not always required. When switching between enum values, the default case is not needed when all of the enum values are exhausted from the existing cases.
  • The evaluated value can be assigned to a variable. No need to declare an uninitialized variable above the switch statement only to be initialized in one of the cases in the switch.

Pattern Matching in Switch Expression

I haven't tried this language feature yet, but I have already seen it in its JEP and in this YouTube video.

As the name suggest, this language features enables us to test multiple data types in a switch statement.

String formatted = switch (obj) {
    case Integer i -> String.format("int %d", i);
    case Byte b    -> String.format("byte %d", b);
    case Long l    -> String.format("long %d", l); 
    case Double d  -> String.format("double %f", d); 
    case String s  -> String.format("String %s", s); 
    default        -> obj.toString();
};
Testing for the dataype of obj to be formatted. The example is copied directly from its JEP page

This language feature will be very useful when doing a type-specific action.

private UserEntity modelToEntity(User user) {
    return switch (user) {
        case DelegatedUser du -> du.getEntity();
        default -> mapper.map(user, UserEntity.class);
    }
}
An example method that tries to convert a User class to an entity class. No mapping will happen if the user is a type of DelegatedUser since delegates can just unpack the delegated entity.

Helpful NPE Message

Ok, this is not really a Java language feature but I think this is one of my favorite updates in Java 14. This feature is disabled by default and can be enabled by adding the -XX:+ShowCodeDetailsInExceptionMessages argument. In Java 15, this feature will be enabled by default.

When this feature is enabled, the error message for NPE's will change to a more natural, english-like sentence that actually tells you which field is the cause of the NullPointerException.

Consider the following source code:

class HelpfulNpe {
    public static void main(String[] args) {
        var keeb = new Keyboard("Best Keyboard", new User(null));
        System.out.println(keeb.owner().name().toUpperCase());
    }
}

record User(String name) {}
record Keyboard(String name, User owner) {}
Can you guess where the NPE will be thrown from?

When you run that program, this is the error message you will get:

Note: ./HelpfulNpe.java uses preview language features.
Note: Recompile with -Xlint:preview for details.
Exception in thread "main" java.lang.NullPointerException
	at HelpfulNpe.main(HelpfulNpe.java:4)

This is the classic NPE message that only shows us the line number the error came from.

Now, this is the NPE message when the -XX:+ShowCodeDetailsInExceptionMessages is enabled.

Note: ./HelpfulNpe.java uses preview language features.
Note: Recompile with -Xlint:preview for details.
Exception in thread "main" java.lang.NullPointerException: Cannot invoke "String.toUpperCase()" because the return value of "User.name()" is null
	at HelpfulNpe.main(HelpfulNpe.java:4)

Notice that the message now shows what method call caused the NPE and additional explaination on why that happened.

Honorable Mentions

Record Destructuring

This is a language feature that enables us to assign members of a record to multiple variables. Since Java is a strongly-typed language, this feature is only usable with pattern matching using if statement or switch statement.

record Keyboard(String name, KeyboardSwitchType switchType) {}

// ...

var keeb = fetchKeyboard();
if (keeb instanceof Keyboard(var name, var type)) {
    System.out.println("Name: " + name);
    System.out.println("Type: " + type);
}

Multiline Strings / Text Blocks

This is a preview feature available in Java 14. It enables us to write multiline strings using the triple-double quote symbol """. This is very useful when writing an embedded code in your Java code (like SQL or HTML). Unlike the multiline strings in other languages like Groovy, this language feature is smart enough to detect indentation in the Java source code and ignore them when evaluating the string value.

String sql = """select *
                from users
                where deleted = false
                    and name like :filter
             """

When the string from the example above is evaluated, the word from will be aligned from the word select from the first line. It will not have 16 spaces before it just because it has 16 spaces in the raw Java code.

In other words, the resulting string will be like this:

select *
from users
where deleted = false
    and name like :filter

and not like this:

select *
                from users
                where deleted = false
                    and name like :filter

Sealed Interfaces and Abstract Classes

This is a preview feature that will be enabled on Java 15 release. Using sealed and permits keywords to an interface or abstract class declaration prevents implementing or extending the sealed type.

public sealed interface PointingDevice
        permits Mouse, Touchpad, Trackball {
    
    // interface contents...
}

public class Mouse implements PointingDevice { }
public class Touchpad implements PointingDevice { }
public class Trackball implements PointingDevice { }

In the above example, the interface PointingDevice can only be implemented by Mouse, Touchpad, and Trackball. One of the biggest use case of this language feature is its exhaustiveness when used in a switch statement, just like enums!

PointingDevice pointingDevice = fetchPointingDevice();

String message = switch (pointingDevice) {
    case Mouse m -> "I have " + m.buttonCount() + " buttons!";
    case Touchpad tp -> "Am I tactile? It is " + tp.tactile();
    case Trackball tb -> "My ball is located at " + tb.ballLocation() + " of the device.";
    // No need for default.
}
System.out.println(message);
A switch statement that exhausts all the possible implementations of the sealed interface PointingDevice from the example above. The default case is not needed since all the possible implementations has their own case block. 

Local Methods

This is a preview feature that might be enabled on Java 15 or 16 release. Basically, this language feature enables us to declare methods inside a method. Just like local variables, this method is not known outside the method it's declared from. I'm not really a fan of this since private method already does the job right with proper code architecture. Nevertheless, this is an interesting feature to watch out for!

public Node search(T value) {
    Node preOrderSearch(T value, Node<T> node) {
        if (node == null) {
            return null;
        }

        int comparison = value.compareTo(node.value);
        if (comparison == 0) {
            return node;
        } else if (comparison < 0) {
            return preOrderSearch(value, node.left);
        } else if (comparison > 0) {
            return preOrderSearch(value, node.right);
        }
    }
    return preOrderSearch(value, root);
}
The method preOrderSearch is defined inside the public search method.

Conclusion

The Java language is evolving fast! Since the process to improve Java is open, we can see the language features that is yet to come.

Since some of the feature are almost complete, and some are already hidden in the current JDK distributions, we can now test and see the incoming language features. Here are some of my favorites:

  • Data class – The new record class enables us to write concise data classes, just like lombok's @Data annotation. The only difference is record is a native syntax and no 3rd party JAR files required.
  • Pattern Matching – Very useful on architecture that uses a lot of inheritance. This makes type checking more concise.
  • Switch Expression – A simple but powerful update to the classic switch statement. Now this switch statement allows us to assign its result to a variable. In future versions of Java, switch expressions will also allow pattern matching, destructuring, and exhaustive evaluation of sealed types.
  • Helpful NPE Message – This is the only non-language feature that I included in this list. I think this is very powerful debugging tool since it points directly where the NPE exactly came from instead of telling us just the line number.

Other than these, I also look forward to other incoming Java language features such as destructuring, multiline strings, sealed types, and local methods.

Show Comments