IntelliJ IDEA Java

Java 23 and IntelliJ IDEA

New and updated Java language features, core API, and the JVM – Java 23 packs it all – for new Java developers to senior developers. IntelliJ IDEA 2024.2 is ready with its support for Java 23 features.

Keeping pace with new Java version releases could be difficult – what changed, why, and how to use new and updated features. In this blog post, I’ll cover some of the new and updated features in Java 23 – the pain points they address for you, their syntax and semantics, and how IntelliJ IDEA can help you to use them.

I’ll highlight Java 23 features, such as the inclusion of the primitive data types in pattern matching, the ability to import modules in code bases, the possibility to use Markdown in documentation comments, implicitly declared classes and instance main methods, and Flexible Constructor Bodies. If you are interested, this link includes a list of other Java 23 features.

Let’s quickly configure IntelliJ IDEA before deep diving into the details of Java 23 features.

IntelliJ IDEA Configuration

Java 23 support is available in IntelliJ IDEA 2024.2, released recently.

In your Project Settings, set the SDK to Java 23. You can both configure IntelliJ IDEA to use a downloaded version of JDK 23, or select to download it from a list of vendors, without exiting the IDE. For the language level, select ‘23(Preview) – Primitive types in patterns, implicitly declared classes, etc.’, as shown in the screenshot below:

To use production feature, such as, Markdown documentation comments, change the language level, to ‘23 – Markdown documentation comments.’, as shown in the below settings screenshot:

With the IntelliJ IDEA configuration under your belt, let’s deep dive into learning the new features. 

Primitive Types in Patterns, instanceof, and switch (Preview Feature)

Imagine you need to write a conditional construct that executes code based on whether the value of a long variable matches a few literal values, or falls in a range of values.

How would you do that? Until now, you could only use an if/else construct to do so. But, with Java 23, Primitive Types in Patterns, instanceof, and switch, a preview language feature, you could code this functionality using the more expressive and easy-to-read switch constructs, using long values in the case labels. 

What does it mean to add primitive types to Pattern Matching?

Until Java 23, switch constructs (statements and expressions) worked with reference variables and some primitives data types, such as int, byte, short (with constraints). Also, the instanceof operator couldn’t be used with any primitive data type. 

With Java 23, you will be able to use ALL the primitive data types, including boolean, long, float, double and long, with pattern matching in Switch constructs and instanceof operators. This applies to using it in nesting and top level contexts. 

Why should you care about this feature? The worth of a feature depends on how large the codebase it affects and how often. Since conditional statements are one of the basics of programming, you could expect to see usage of this feature a lot in your codebase. Even if you might not write the code, you would read code written by someone else.

Let’s understand this feature using an example.

An example (replacing long if-else statements with switch expression)

Imagine a method, say, getHTTPCodeDesc(int), accepts an HTTP server code as an int value and returns a corresponding String representation, checking it against a literal value or a range of values. 

There doesn’t seem to be any obvious issue with this code – we all have either written or read similar code. However, it might take longer to process the flow of an if-else construct because they COULD define complicated conditions that are NOT limited to just one variable. Let’s keep it simple and assume that the method getHTTPCodeDesc() is defined as follows: 

public String getHTTPCodeDesc(int code) {
   if (code == 100) {
       return "Continue";
   } 
   else if (code == 200) {
       return "OK";
   } 
   else if (code == 301) {
       return "Moved permanently";
   } 
   else if (code == 302) {
       return "Found";
   } 
   else if (code == 400) {
       return "Bad request";
   } 
   else if (code == 500) {
       return "Internal server error";
   } 
   else if (code == 502) {
       return "Bad gateway";
   } 
   else if (code > 100 && code < 200) {
       return "Informational";
   } 
   else if (code > 200 && code < 300) {
       return "Successful";
   } 
   else if (code > 302 && code < 400) {
       return "Redirection";
   } 
   else if (code > 400 && code < 500) {
       return "Client error";
   } 
   else if (code > 502 && code < 600) {
       return "Server error";
   } 
   else {
       return "Unknown error";
   }
}

In Java 23, the preceding code could be replaced using a switch expression (using primitives as patterns), as follows:

public String getHTTPCodeDesc(int code) {
    return switch(code) {
        case 100 -> "Continue";
        case 200 -> "OK";
        case 301 -> "Moved Permanently";
        case 302 -> "Found";
        case 400 -> "Bad Request";
        case 500 -> "Internal Server Error";
        case 502 -> "Bad Gateway";
        case int i when i > 100 && i < 200 -> "Informational";
        case int i when i > 200 && i < 300 -> "Successful";
        case int i when i > 302 && i < 400 -> "Redirection";
        case int i when i > 400 && i < 500 -> "Client Error";
        case int i when i > 502 && i < 600 -> "Server Error";
        default                            -> "Unknown error";
    };
}

The first obvious benefit in the preceding code is that it is much easier to read and understand than the version that uses if-else statements. You can understand the code logic at a glance. 

Another not-so-obvious benefit (but an important one) is how the preceding code decreases the cognitive load for you. Cognitive load refers to the amount of information you have in your working memory (working memory space is limited). If you try to overload your working memory with instructions or information not directly related to your end goal then your productivity drops. Code snippets that are easier to read help you focus your attention on other areas of the code. Such small wins help a lot when we talk about how often we could benefit from them.

Let’s talk about the simple parts now, I mean the syntax. As you can notice, the case labels can have both – constant values (such as 100, 200, etc.) and also a range of values specified using pattern matching, using type patterns (int i) with guards (when i > 100 && i < 200). You can also define a default clause.

In the preceding code, the method getHTTPCodeDesc() returns a value using the switch expression. Irrespective of the value that you pass to the method parameter, that is, code, the method must return a value. In other words, the switch expression must be exhaustive. If it isn’t IntelliJ IDEA can detect it and offer the addition of the default clause, as shown in the following GIF:

The preceding code switches on a variable of type int. Similarly you can switch on variables of all the other primitive types, such as, long, double, float, etc.

Are you new to Pattern matching or recent changes to the switch construct?

If you are completely new to Pattern Matching, check out the section on its basics in my blog post Java 17 and IntelliJ IDEA. If you are interested in how Pattern Matching is being used with the switch constructs, I have another detailed blogpost on this topic: Evolution of the Switch Construct in Java—Why Should you Care? It covers how switch constructs use pattern matching to check reference values against different patterns and execute code conditionally, depending on the type of variable and its attributes.

Using pattern matching with boolean values

It is common to read and write code that returns a value based on whether the value of a boolean variable is true or false. For example, in the following code, the method calculateDiscount, calculates and returns a discount value depending on whether you pass true or false to the method parameter isPremiumMember:

public class DiscountCalculator {
    private static final int PREMIUM_DISCOUNT_PERCENTAGE = 20;
    private static final int REGULAR_DISCOUNT_PERCENTAGE = 5;

    public int calculateDiscount(boolean isPremiumMember, int totalAmount) {
        int discount;
        if (isPremiumMember) {
            // Calculate discount for premium members
            discount = (totalAmount * PREMIUM_DISCOUNT_PERCENTAGE) / 100;
        } else {
            // Calculate discount for regular members
            discount = (totalAmount * REGULAR_DISCOUNT_PERCENTAGE) / 100;
        }
        return discount;
    }
}

Instead of an if-else construct, you can switch over the value of the boolean method parameter isPremiumMember, without requiring the definition of the local variable discount, as follows:

public int calculateDiscount(boolean isPremiumMember, int totalAmount) {
    return switch (isPremiumMember) {
        case true -> (totalAmount * PREMIUM_DISCOUNT_PERCENTAGE) / 100;
        case false -> (totalAmount * REGULAR_DISCOUNT_PERCENTAGE) / 100;
    };
}

Since the switch expression in the method calculateDiscount() is exhaustive, if you miss any of the true or false values, IntelliJ IDEA can detect it and suggest you to insert a default or the missing true/ false case, as shown in the following gif:

Using primitive types with the instanceof operator 

Until Java 23, none of the primitive types could be used with the instanceof operator.

The instanceof operator can be used to check the type of a variable, and execute code conditionally. With pattern matching for instanceof, you could also declare and initialize pattern variables if the type of a variable being compared matched without the type pattern without the need for an explicit casting. The instanceof variable can also use guard patterns.

With the addition of primitive types to the instanceof operator, you could define code such as the following:

import static java.io.IO.println;

void main() {
    var weight = 68;
    if (weight instanceof byte byteWeight && byteWeight <= 70) {
        println("Weight less than 70");
    }
}

Note that the feature Implicitly declared classes and instance main method in Java 23 defines java.io.IO with static methods that allows you to import it and use println() to output values to console, rather than using System.out.println() to do so.

If you plan to check for more types and conditions, you could use a switch construct with guard, as follows:

var weight = 68;

switch (weight) {
    case byte b when b <= 70                    -> println("byte: less than 70");
    case int i  when i <= 700                   -> println("int: less than 7000");
    case long l when l >= 7_000 && l <= 70_000  -> println("long range: 7_000 - 70_000");
    case double d                               -> println("double");
}

 

Safe conversion between data types 

When you use pattern matching with primitive data types, the Java compiler ensures that there is no loss of information. In the following example, the instanceof operator can convert between the double and byte data types when it detects it is safe to do so:

double height = 67;

if (height instanceof byte byteHeight)
    System.out.println(byteHeight);

Similar code wouldn’t execute without an instanceof operator. The following code won’t compile:

double height = 67;

final byte convertToByte = height;

Robust data flow analysis in IntelliJ IDEA

IntelliJ IDEA offers robust support for primitive types in switch statements, and it also integrates sophisticated data-flow analysis to help developers avoid common issues. As shown in the following example, the data-flow analysis in IntelliJ IDEA can determine that the second case label, that is, case int _ and the rest of the case labels are not reachable (since the code exits from the method if the value of the variable weight is more than 70). IntelliJ IDEA warns you about unreachable code and offers appropriate suggestions:

Records and primitives data type components

Imagine you define a record Person as follows:

record Person(String name, double weight) {}

Until now, you could decompose it to the exact data types. However, with this feature you could use other compatible data types, such as, int, long etc. Here is an example:

Person person = new Person("Java", 672);

switch (person) {
    case Person(String name, byte weight) -> println("byte:" + weight);
    case Person(String name, int weight) -> println("int:"  + weight);
    case Person(String name, double weight) -> println("double:" + weight);
    case Person(String name, long weight) -> println("long:" + weight);
    default -> throw new IllegalStateException("Unexpected value: " + person);
}

You could use it with the instanceof operator too, as follows:

 if (person instanceof Person(String name, byte weight)) {
     System.out.println("Instanceof : " + weight);
 }

What’s next in IntelliJ IDEA to support this feature

More support on the primitive data types in Pattern Matching is in works, including the ability to use the postfix operator .switch on all the primitive data types to start writing a switch construct. Support is also in works to convert an existing if-else statement to a switch construct that uses primitive data types – so that it is more easier for you to adopt this new feature.

Markdown documentation comments (Production feature)

Until now the Java documentation comments were coded using HTML and JavaDoc tags. With this new feature, that is, Documentation comments, you’ll also be able to use Markdown to write the JavaDoc comments. This is a production feature in Java 23.

Are you wondering what is the reason for this change?

One of the reasons is that HTML is no longer a popular choice with new developers (even though it was a great choice back when Java was introduced in the late 1990s). It is not simple to write HTML manually. Also, Markdown is easier to write, read, and can be easily transformed to HTML. A lot of developers are using Markdown to document their code, write books, articles, blog posts, generate website pages, and much more. 

Let’s see how you can use Markdown to write Javadoc comments in your source code file and how IntelliJ IDEA can help.

An example

The Markdown documentation comments start with ///. 

The choice for using three black slashes is interesting. Jonathan Gibbons, owner of this feature at Oracle, shared that it is not an easy task to change the syntax of features in the Java language. Multiline comments start with /* and end with */. This makes it difficult to include any code in the documentation that might include */. Hence, the Markdown documentation comments start with ///. 

The old way of writing the documentation comments, that is, HTML and JavaDoc tags are supported too. Jonathan also mentioned that it was not feasible to convert JavaDoc tags to the Markdown equivalent. So, the developers can use a combination of Markdown comments and JavaDoc tags to get the best of both worlds.

Here’s an example that uses Markdown and Javadoc tags for documenting a method:

///
/// **Getting started and having fun learning Java :)**
///
/// Prints a pattern of the letter 'P' using the specified character.
///
/// @param size the size of the pattern
/// @param charToPrint the character to use for the pattern
///
private void printP(int size, char charToPrint) {
    for (int i = 0; i < size; i++) {
        for (int j = 0; j < size; j++) {
            if (j == 0 || (i == 0 || i == size / 2) && j < size - 1 || (j == size - 1 && i <= size / 2)) {
                System.out.print(charToPrint + " ");
            } else {
                System.out.print("  ");
            }
        }
        System.out.println();
    }
}

IntelliJ IDEA can help you switch between your views in the editor and check how the documentation comments would appear to anyone reading the Java documentation comments.

Viewing Java Documentation comments in IntelliJ IDEA

Jonathan Gibbons, owner of the JEP ‘Markdown Documentation Comments’ emphasized the need for all developers to check if the Java documentation comments they added to their codebase are correct. 

IntelliJ IDEA provides a ‘Reader mode’ that enables you to view Java documentation comments in your source code. You can also switch between the Java documentation comments code and how it is viewed using the feature ‘Toggle Rendered View’, as shown in the following gif:

Writing Markdown Documentation Comments in IntelliJ IDEA

IntelliJ IDEA can detect you are using Markdown to document a method. When you start with /// and hit Enter, it will add /// on the next line too, as shown in the following GIF:

Should you convert your existing documentation comments to use Markdown?

Here’s a gif showing the documentation of the method hashCode written using markdown in IntelliJ IDEA. Using ‘Toggle Rendered View’ you can easily view the documentation in the reader view, which is much easier to read and understand. 

Ideally, I wouldn’t encourage you to convert your existing documentation to using Markdown, unless your developers or API users have major readability issues when they are viewing your (API, libraries, Framework) code base using tools that don’t offer an alternate view like IntelliJ IDEA. 

Module Import Declarations (Preview Feature)

With this feature, you can import a module library like the Java API in your class or interface using a single statement, such as, import module java.base, which will import all the packages that are exported by module java.base. 

You wouldn’t need separate import statements to import packages like java.util, or say, java.io in your class because these are exported by java.base.

An example

The following code example uses classes from the package java.io and java.util. By including the statement ‘import java.base’, you don’t need to import the java.io and java.util packages individually since they are exported by the module java.base:

import module java.base;
public class ImportModuleHelloWorld {

    public static void main(String[] args) {

        try {
            InputStream inputStream = new BufferedInputStream(
                                            new FileInputStream(
                                                   new File("abc.txt")));
        } catch (FileNotFoundException e) {
            throw new RuntimeException(e);
        }

        Map<String, String> list = new HashMap<>();
    }
}

However, if you delete the import module statement from the top, IntelliJ IDEA will import individual classes and interfaces from the packages java.io and java.util. This is shown in the following GIF:

Which packages are exported by the module java.base (or other modules)?

It is simple to answer this question when you are using IntelliJ IDEA. Click on the module name in the editor or use the relevant shortcut (Go to Declaration or Usages) and you could view the definition of this module to find out all the modules exported by this module. This is shown in the following gif:

Implicit Declared classes and Instance Main methods (Third Preview)

Introduced as a preview language feature in Java 21, this feature is in its third preview in Java 23.

It is here to change how new Java developers would get started learning Java. It simplifies the initial steps for students when they start learning basics, such as variable assignment, sequence, conditions and iteration. Students no longer need to declare an explicit class to develop their code, or write their main() method using this signature – public static void main(String []). With this feature, classes could be declared implicitly and the main() method can be created with a shorter list of keywords.

If you are new to this feature, I’d highly recommend you to check out my detailed blog post: ‘HelloWorld’ and ‘main()’ meet minimalistic on this feature. In this section, I’ll include the additions to this feature in Java 23. 

Using packages exported by module java.base without importing them explicitly

By including just one import statement, that is, import module java.base, at the top of your implicit class, you’ll be able to automatically import the packages exported by java.base. This means that your implicit class will no longer need individual import statements for these classes or packages, as shown in the following gif:

Simplifying code to write to interact with the console

Interaction with the console – printing messages, or consuming messages is one of the frequently used actions by new Java developers. This has been simplified further in this feature.  

Your implicitly declared classes can output messages to he console by using methods println() and print() and read String messages using the readln() method without explicitly importing them in your class. All these methods are declared in a new top-level class java.io.IO, which is implicitly imported by implicit classes.

Take a moment and see and how you need to explicit import them in a regular class, as shown in the following GIF:

The following gif shows you doesn’t need explicit imports when you use the same previous methods in an implicit class:

Flexible Constructor Bodies (Second Preview)

This is the second preview of a feature in JDK 22, previously called “Statements before super()”. Apart from the change in the feature name, this feature has an important change. It is now possible to initialize fields before calling super() or this().

This is useful when a superclass calls a method from its constructor and you want to override this method in a subclass and want to access a field from the subclass inside this method. Previously, the subclass field would not be initialized when the method was called from the superclass constructor. Now it is possible to initialize the field and prevent surprises. Here’s an example code to show this feature:

abstract class Action {
    public Action() {
        System.out.println("performing " + getText());
    }
    public abstract String getText();
}

class DoubleAction extends Action {
    private final String text;
    private DoubleAction(String text) {
        this.text = text; // this did not compile before Java 23 with preview features enabled.
        super();
    }

    @Override public String getText() {
        return text + text;
    }
}

If you are new to this feature, don’t miss checking out my detailed blog post on this feature, https://blog.jetbrains.com/idea/2024/02/constructor-makeover-in-java-22/, which talks about the why and how of this feature.

Preview Features

Of the features that I covered in this blog post, ‘Primitive Types in Patterns, instanceof, and switch’, ‘Modeule Import Declarations’, ‘Implicitly Declared Classes and Instance Main Methods’ and ‘Flexible constructor bodies’ are preview language features in Java 23. With Java’s new release cadence of six months, new language features are released as preview features. They may be reintroduced in later Java versions in the second or more preview, with or without changes. Once they are stable enough, they may be added to Java as a standard language feature.

Preview language features are complete but not permanent, which essentially means that these features are ready to be used by developers, although their finer details could change in future Java releases depending on developer feedback. Unlike an API, language features can’t be deprecated in the future. So, if you have feedback about any of the preview language features, feel free to share it on the JDK mailing list (free registration required).

Because of how these features work, IntelliJ IDEA is committed to only supporting preview features for the current JDK. Preview language features can change across Java versions, until they are dropped or added as a standard language feature. Code that uses a preview language feature from an older release of the Java SE Platform might not compile or run on a newer release.

Summary

In this blog post, I covered five Java 23 features – Primitive Types in Patterns, instanceof, and switch, Markdown Documentation Comments, Module Import Declarations, Implicitly Declared Classes and Instance Main Methods, and Flexible constructor bodies.

Check out these new features to find out how they can help you improve your applications.

Happy Coding!

image description