IntelliJ IDEA

Java 22 and IntelliJ IDEA

Java 22 is here, fully supported by IntelliJ IDEA 2024.1, allowing you to use these features now!

Java 22 has something for all – from new developers to Java experts, features related to performance and security for big organizations to those who like working with bleeding edge technology, from additions to the Java language to improvements in the JVM platform, and more.

It is also great to see how all these Java features, release after release, work together to create more possibilities and have a bigger impact on how developers create their applications that address existing pain points, perform better and are more secure.

This blog post doesn’t include a comprehensive coverage of all the Java 22 features. If you are interested in that, I’d recommend you to check out this link to know everything about what’s new and changing in Java 22, including the bugs.

In this blog post, I’ll cover how IntelliJ IDEA helps you get started, up and running with some of the Java 22 features, such as, String Templates, Implicitly Declared Classes and Instance Main Methods, Statements before super(), and Unnamed variables and patterns.

Over the past month, I published separate blog posts to cover each of these topics in detail. If you are new to these topics, I’d highly recommend you check out those detailed blog posts (I’ve included their links in the relevant subsections in this blog post). In this blog post, I’ll cover some sections from those blog posts, especially how IntelliJ IDEA supports them. Let’s start by configuring IntelliJ IDEA to work with the Java 22 features.

IntelliJ IDEA Configuration

Java 22 support is available in IntelliJ IDEA 2024.1 Beta. The final version will release soon in March 2024.

In your Project Settings, set the SDK to Java 22. For the language level, select ‘22 (Preview) – Statements before super(), string templates (2nd preview etc.)’ on both the Project and Modules tab, as shown in the below settings screenshot:

If you do not have Java 22 downloaded to your system yet, don’t worry; IntelliJ IDEA has your back! You could use the same Project settings window, select ‘Download JDK’, after you click on the drop down next to SDK. You’ll see the below popup that would enable you to choose from a list of vendors (such as Oracle OpenJDK, GraalVM, Azul Zulu and others):

With the configuration under our belt, let’s get started with covering one of my favorite new features, that is, String Templates.

String Templates (Preview Language feature)


The existing String concatenation options are difficult to work with and could be error prone; String templates offer a better alternative, that is, String interpolation with additional benefits such as validation, security and transformations via template processors.

Please check out my detailed blog post on this topic: String Templates in Java – why should you care? if you are new to this topic. It covers all the basics, including why you need String Templates, with multiple hands-on examples on built-in and user defined String processors.

IntelliJ IDEA can highlight code that could be replaced with String Templates


Let’s assume you defined the following code to log a message that combines string literals and variable values using the concatenation operator:

public void processOrder(int orderId, String product, int qty, LocalDate orderDate) {
   if (quantity <= 0) {
       String errorMessage = "Invalid order quantity: " + qty + " for product " + product + ", order ID " + orderId;
       logger.error(errorMessage);
       return;
   }
   //.. Remaining code
}

The output from the preceding code could be an issue if you miss adding spaces in the String literals. The code isn’t quite easy to read or understand due to multiple opening and closing double quotes, that is, " and the + operator, and it would get worse if you add more literals, or variable values to it.

You could replace the preceding code with either StringBuilder.append(), String.format() or String.formatted() method or by using the class MessageFormat (as shown in my detailed blog post on this topic), but each of these methods have their own issues.

Don’t worry; IntelliJ IDEA could detect such code, suggest that you could replace it with String template, and do that for you, as shown below. It doesn’t matter if you are not even aware of the syntax of the String templates, IntelliJ IDEA has your back 🙂

Embedded expressions in String Templates and IntelliJ IDEA


The syntax to embed a remplate expression (variable, expressible or a method call) is still new to what Java developers are used to and could be challenging to use without help. Don’t worry, IntelliJ IDEA has your back!

Each embedded expression must be enclosed within \{}. When you type \{, IntelliJ IDEA adds the closing ‘}’ for you. It also offers code completion to help you select a variable in scope, or any methods on it. If the code that you insert doesn’t compile, IntelliJ IDEA will highlight that too (as a compilation error), as shown in the following gif:

Using String Templates with Text Blocks


Text blocks are quite helpful when working with string values that span multiple lines, such as, JSON, XML, HTML, SQL or other values that are usually processed by external environments. It is common for us Java developers to create such string values using a combination of string literals and variable values (variables, expressions or method calls).

The example below shows how IntelliJ IDEA could detect and create a text block using String templates for multiline string values that concatenates string literals with variable values. It also shows how IntelliJ IDEA provides code completion for variable names within such blocks. When you type in \{, IntelliJ IDEA adds }. As you start typing the variable name countryName, it shows the available variables in that context:

Language injection and String Templates


You could also inject a language or a reference in string values that spans single line or multiple lines, such as, a text block. By doing so, you get comprehensive coding assistance to edit the literal value. You could avail of this feature temporarily or permanently by using the @Language annotation, as shown below:

You can check out this link for detailed information on the benefits and usage of injecting language or reference in IntelliJ IDEA.

Predefined Template Processors


With the String templates, you get access to predefined processors like the STR, FMT and RAW. I’d highly recommend you to check out my detailed blog post on String templates for multiple hands-on examples on it.

Custom Template Processor

Let’s work with a custom String template that isn’t covered in my previous blog post.

Imagine you’d like to create an instance of a record, say, WeatherData, that stores the details of the JSON we used in the previous section. Assume you define the following records to store this weather data represented by the JSON in the previous section:

public record WeatherData (String cod, City city) { }
public record City (int id, String name, String country, Coord coord) {}
public record Coord (double lat, double lon) { }

You could create a method to return a custom String template that would process interpolated string, accept a class name (WeatherData for this example) and return its instance:

public <T> StringTemplate.Processor<T, RuntimeException> getJSONProcessorFor(Class<T> classType) {
        return StringTemplate.Processor.of(
                (StringTemplate st) -> {

                    List<Object> sanitizedLst = new ArrayList<>();
                    for (Object templateExpression : st.values()) {
                        switch (templateExpression) {
                            case String str -> sanitizeStr(str, sanitizedLst);
                            case Number _, Boolean _ -> sanitizedLst.add(templateExpression);
                            case null -> sanitizedLst.add("");
                            default -> throw new IllegalArgumentException("Invalid value");
                        }
                    }
                    String jsonSource = StringTemplate.interpolate(st.fragments(), sanitizedLst);
                    System.out.println(jsonSource);

                    try {
                        ObjectMapper objectMapper = new ObjectMapper();
                        return objectMapper.readValue(jsonSource, classType);
                    } catch (JsonProcessingException e) {
                        throw new RuntimeException(e);
                    }
                });
    }

Depending on the logic of your application, you might want to escape, delete or throw errors for the special characters that you encounter in the the JSON values interpolated via template expressions, as follows (the following method chooses to escape the special characters and include them as part of the JSON value):

    private void sanitizeStr(String str, List<Object> sanitizedLst) {
        String sanitizedStr = str.replace("\\", "\\\\")
                                 .replace("\"", "\\\"")
                                 .replace("/", "\\/")
                                 .replace("\b", "\\b")
                                 .replace("\f", "\\f")
                                 .replace("\n", "\\n")
                                 .replace("\r", "\\r")
                                 .replace("\t", "\\t");
        sanitizedLst.add("\"" + sanitizedStr + "\"");
    }

You could initialize and use this custom JSON template processor as below. Note how elegant and concise the solution is with a combination of textblocks and String templates. The JSON is easy to read, write and understand (thanks to text blocks). The template expressions make it clear and obvious about the sections that are not constants and would be injected by the variables. At the end, the custom template processor WEATHER_JSON would ensure the resultant JSON is validated according to the logic you defined and returns an instance of WeatherData (doesn’t it sound magical?) :

StringTemplate.Processor<WeatherData, RuntimeException> WEATHER_JSON = getJSONProcessorFor(WeatherData.class);

String cod = null;
String name = "Amazing City";
Double lat = 55.7522;
    
WeatherData weatherData = WEATHER_JSON."""
                  {
                    "cod": \{cod},
                    "city": {
                      "id": 524901,,,
                      "name": \{name},
                      "country": "XYZ",
                      "coord": {
                        "lat": \{lat},
                        "lon": 37.6156
                      }
                    }
                  }""";

Do not miss to check out my detailed blog post on this topic: String Templates in Java – why should you care? to discover how you could use the predefined String templates like FMT, to generate properly formatted receipts for, say, your neighborhood stationery store, or, say encode and decode combinations like :) or :( to emojis like 🙂 or ☹️. Does that sound fun to you?

Implicitly Declared Classes and Instance Main Methods (Preview language feature)

Introduced as a preview language feature in Java 21, this feature is in its second preview in Java 22.

It would revolutionize 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 blog post, I’ll include a few of the sections from it.

Class ‘HelloWorld’ before and after Java 21

Before Java 21, you would need to define a class, say, HelloWorld, that defined a main() method with a specific list of keywords, to print any text, say, ‘Hello World’ to the console, as follows:

public class HelloWorld {
    public static void main(String[] args) {
        System.out.println("Hello World");
    }
}

With Java 21, this initial step has been shortened. You can define a source code file, say, HelloWorld.java, with the following code, to print a message to the console (it doesn’t need to define a class; it has a shorter signature for method main()):

void main() {
    System.out.println("Hello World");
}

The preceding code is simpler than what was required earlier. Let’s see how this change could help you focus on what you need, rather than what you don’t.

Compiling and executing your code

Once you are done writing your code, the next step is to execute it.

On the command prompt, you could use the javac and java commands to compile and execute your code. Assuming you have defined your code in a source code file HelloWorld.java, you could use the following commands to run and execute it:

C:\code\MyHelloWorldProject\javac HelloWorld.java
C:\code\MyHelloWorldProject\java HelloWorld

Since Java 11, it is possible to skip the compilation process for code defined in a single source code file, so you could use just the second command (by specifying the name of the source code file, as follows):

C:\code\MyHelloWorldProject\java HelloWorld.java

However, since instance main methods and implicit classes is a preview language feature, you should add the flag --enable-preview with --source 22 with these commands, as follows:

C:\code\MyHelloWorldProject\java --enable-preview --source 22 HelloWorld.java

Sooner or later, you might switch to using an IDE to write your code. If you wish to use IntelliJ IDEA for creating instance main methods, here’s a quick list of steps to follow. Create a new Java project, select the build system as IntelliJ (so you could use Java compiler and runtime tools), create a new file, say, HelloWorld.java with your instance main method and set the properties to use Java 22, before you run your code, as shown in the following gif (It could save you from typing out the compilation/ execution commands on the command prompt each time you want to execute your code):

Are you wondering if it would be better to create a ‘Java class’ instead of a ‘File’ in the ‘src’ folder? The option of selecting a Java class would generate the body of a bare minimum class, say, public class HelloWorld { }. Since we are trying to avoid unnecessary keywords in the beginning, I recommended creating a new ‘File’ which wouldn’t include any code.

What else can main() do apart from printing messages to the console?

As included in my detailed post on this topic, I included multiple hand-on examples to show what you could achieve via just the main() method:

  • Example 1. Variable declarations, assignments and simple calculations
  • Example 2. Print patterns, such as, big letters using a specified character
  • Example 3. Animating multiline text – one word at a time
  • Example 4. Data structure problems
  • Example 5. Text based Hangman game
  • The idea to include multiple examples as listed above is to demonstrate the power of sequence, condition and iteration all of which can be included in the main() method, to build good programming foundations with problem solving skills. By using the run command or the icon to run and execute their code in IntelliJ IDEA, new programmers reduce another step when getting started.

    Changing an implicit class to a regular class

    When you are ready to level up and work with other concepts like user defined classes, you could also covert the implicit classes and code that we used in the previous examples, to regular classes, as shown below:

    What happens when you create a source code file with method main(), but no class declaration?

    Behind the scenes, the Java compiler creates an implicit top level class, with a no-argument constructor, so that these classes don’t need to be treated in a way that is different to the regular classes.

    Here’s a gif that shows a decompiled class for you for the source code file AnimateText.java:

    Variations of the main method in the implicit class

    As we are aware, a method can be overloaded. Does that imply an implicit class can define multiple main methods? If yes, which one of them qualifies as the ‘main’ method?
    This is an interesting question. First of all, know that you can’t define a static and non-static main method with the same signature, that is, with the same method parameters. The following method are considered valid main() methods in an implicit class:

    public static void main(String args[]) {}
    public void main(String args[]) {}
    public static void main() {}
    static void main() {}
    public void main() {}
    void main() {}
    

    If there is no valid main method detected, IntelliJ IDEA could add one for you, as shown in the following gif:

    Educators could use this feature to introduce other concepts to the students in an incremental way

    If you are an educator, you could introduce your students to other commonly used programming practices, such as creating methods- that is delegating part of your code to another method and calling it from the main method. You could also talk about passing values vs. variables to these methods.

    The following gif shows how to do it:

    Statements before super() – a preview language feature

    Typically, we create alternative solutions for tasks that are necessary, but not officially permitted. For instance, executing statements before super() in a derived class constructor was not officially allowed, even though it was important for, say, validating values being passed to the base class constructor. A popular workaround involved creating static methods to validate values and then calling these methods on the arguments of super(). Though this approach worked well, it could make the code look complicated. This is changing with Statements before super(), a preview language feature in Java 22.

    By using this feature, you can opt for a more direct approach, that is, drop the workaround of creating static methods, and execute code that validates arguments, just before calling super(). Terms and conditions still apply, such as, not accessing instance members of a derived class before execution of super() completes.

    An example – Validating values passed to super() in a derived class constructor


    Imagine you need to create a class, say, IndustryElement, that extends class Element, which is defined as follows:

    public class Element {
       int atomicNumber;
       Color color;
    
       public Element(int atomicNumber, Color color) {
           if (color == null)
               throw new IllegalArgumentException("color is null");
           this.atomicNumber = atomicNumber;
           this.color = color;
       }
        // rest of the code
    }

    The constructor of the class Element misses checking if the atomicNumber is in the range of 1-118 (all known elements have atomic numbers between 1 to 118). Often the source code of a base class is not accessible or open for modification. How would you validate the values passed to atomicNumber in the constructor of class IndustryElement?

    Until Java 21, no statements were allowed to execute before super(). Here’s one of the ways we developers found a workaround by defining and calling static methods (static methods belong to a class and not to instances and can be executed before any instance of a class exists):

    public class IndustryElement extends Element{
       private static final int MIN_ATOMIC_NUMBER = 1;
       private static final int MAX_ATOMIC_NUMBER = 118;
      
       public IndustryElement(int atomicNumber, Color color) {
           super(checkRange(atomicNumber, MIN_ATOMIC_NUMBER , MAX_ATOMIC_NUMBER), color);
       }
    
       private static int checkRange(int value, int lowerBound, int upperBound) {
           if (value < lowerBound || value > upperBound)
               throw new IllegalArgumentException("Atomic number out of range");
           return value;
       }
    }

    Starting Java 22, you could inline the contents of your static method in the constructor for your derived class, as shown in the following gif:

    Here’s the resultant code for your reference:

    public class IndustryElement extends Element{
        private static final int MIN_ATOMIC_NUMBER = 1;
        private static final int MAX_ATOMIC_NUMBER = 118;
    
        public IndustryElement(int atomicNumber, Color color) {
            if (atomicNumber < MIN_ATOMIC_NUMBER || atomicNumber > MAX_ATOMIC_NUMBER)
                throw new IllegalArgumentException("Atomic number out of range");
            super(atomicNumber, color);
        }
    }

    Where else would you use this feature?


    If you are new to this feature, I’d recommend that you check out my detailed blog post, Constructor Makeover in Java 22, in which I’ve covered this feature in detail using the following examples:

    How does it work behind the scenes?


    The language syntax has been relaxed but it doesn’t change or impact the internal JVM instructions. There are no changes to the JVM instructions for this new feature because the order of execution of the constructors still remains unchanged – from base class to a derived class. Also, this feature still doesn’t allow you to use members of a derived class instance, until super() executes.

    Let’s access and compare the instruction set of the constructor of class IndustryElement, before and after its modification – one that can execute statements before super() and the one that doesn’t. To do so, use the following command:

    javap -c IndustryElement.class

    Here’s the instruction set for the constructor that doesn’t explicitly execute statements before super() and calls static methods to validate range of atomic number:

    Here’s instruction set for the constructor that explicitly executes statements before super() to validate range of atomic number:

    The most important point to note here is that in both the cases, the constructor of the base class, that is, Element is called, after the execution of all other statements. Essentially, it means, you are still doing the same thing, it is just packaged in a way that makes things easier for you.

    I understand it is difficult to remember what each of these instruction codes means. Access the following link and search for the instruction code and following the above instructions set would be a breeze:

    https://docs.oracle.com/javase/specs/jvms/se21/html/jvms-6.html#jvms-6.5.aload_n

    Can you execute ‘any’ statements before calling super()?

    No. If the statements before super() try to access instance variables or execute methods of your derived class, your code won’t compile. For example, if you change the static checkRange() method to an instance method, your code won’t compile, as shown below:

    Unnamed Variables and Patterns

    Starting Java 22, using Unnamed Variables & Patterns you can mark unused local variables, patterns and pattern variables to be ignored, by replacing their names (or their types and names) with an underscore, that is, _. Since such variables and patterns no longer have a name, they are referred to as Unnamed variables and patterns. Ignoring unused variables would reduce the time and energy anyone would need to understand a code snippet. In the future, this could prevent errors :-). This language feature doesn’t apply to instance or class variables.

    Are you wondering if replacing unused variables with _ is always a good idea, or do they imply code smells and should you consider refactoring your codebase to remove them? Those are good questions to ask. If you are new to this topic, I’d recommend you to check out my detailed blog post: Drop the Baggage: Use ‘_’ for Unnamed Local Variables and Patterns in Java 22 to find out answer to this question.

    IntelliJ IDEA Configuration

    Since this is not a preview language feature, set Language Level in your Project Settings to ‘22 – Unnamed variables and patterns’ on both the Project and Modules tab, as shown in the below settings screenshot:

    A quick example

    The following gif gives a sneak peek into how an unused local variable, connection, is detected by IntelliJ IDEA, and could be replaced with an underscore, that is, _.

    The modified code shown in the preceding gif makes it clear that the local variable defined in the try-with-resources statement is unused, making it concise and easier to understand.

    Unused Patterns and Pattern variables in switch constructs

    Imagine you defined a sealed interface, say, GeometricShape, and records to represent shapes, such as, Point, Line, Triangle, Square, as shown in the following code:

    sealed interface GeometricShape {}
    record Point   ( int x,
                     int y)         implements GeometricShape { }
    record Line    ( Point start,
                     Point end)     implements GeometricShape { }
    record Triangle( Point pointA,
                     Point pointB,
                     Point PointC)  implements GeometricShape { }
    record Square  ( Point pointA,
                     Point pointB,
                     Point PointC,
                     Point pointD)  implements GeometricShape { }

    Now assume you need a method that accepts an instance of GeometricShape and returns its area. Since Point and a Line are considered one-dimensional shapes, they wouldn’t have an area. Following is one of the ways to define such method that calculates and returns area:

    int calcArea(GeometricShape figure) {
        return switch (figure) {
            case Point    (int x, int y)                        -> 0;
            case Line     (Point a, Point b)                    -> 0;
            case Triangle (Point a, Point b, Point c)           -> areaTriangle(a, b, c);
            case Square   (Point a, Point b, Point c, Point d)  -> areaSquare  (a, b, c, d);
        };
    }

    In the previous example, the patterns int x, int y, Point a and Point B (for case label Line) remain unused as detected by IntelliJ IDEA. These could be replaced by an _. Also, since all the record components of the case Point remain unused, it could be replaced as Point _. This could also allow us to merge the first and second case labels. All of these steps are shown in the following gif:

    Here’s the modified code for your reference:

    int calcArea(GeometricShape figure) {
        return switch (figure) {
            case Point _, Line _                                -> 0;  
            case Triangle (Point a, Point b, Point c)           -> areaTriangle(a, b, c);
            case Square   (Point a, Point b, Point c, Point d)  -> areaSquare  (a, b, c, d);
        };
    }

    In the preceding example, you can’t delete the pattern variables even if they are unused. The code must include the cases when the instance passed to the method calcArea() is of type Point and Line, so that it could return 0 for them.

    Unused Patterns or variables with nested records

    This feature also comes in quite handy for nested records with multiple unused patterns or pattern variables, as demonstrated using the following example code:

    record Name       (String fName, String lName) { }
    record PhoneNumber(String areaCode, String number) { }
    record Country    (String countryCode, String countryName) { }
    record Passenger  (Name name,
                       PhoneNumber phoneNumber,
                       Country from,
                       Country destination) { }
    
    public class GeoMaps {
        boolean checkFirstNameAndCountryCodeAgain (Object obj) {
            if (obj instanceof Passenger(Name (String fName, _),
                                         _,
                                         _,
                                         Country (String countryCode, _) )) {
    
                if (fName != null && countryCode != null) {
                    return fName.startsWith("Simo") && countryCode.equals("PRG");
                }
            }
            return false;
        }
    }

    In the preceding code, since the if condition in the method checkFirstNameAndCountryCodeAgain uses only two pattern variables, others could be replaced using _; it reduced the noise in the code too.

    Where else can you use this feature?


    Checkout my detailed detailed blog post: Drop the Baggage: Use ‘_’ for Unnamed Local Variables and Patterns in Java 22 to learn more about other use cases where this feature can be used:

    It isn’t advisable to use this feature without realising if an unused variable or pattern is a code smell or not. I used these examples to show that at times it might be better to refactor your code to get rid of the unused variable instead of just replacing it with an underscore, that is, _.

    Preview Features

    ‘String Templates’, ‘Implicitly Declared Classes and Instance Main Methods’ and ‘Statements before super()’ are preview language features in Java 22. 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 four Java 22 features – String Templates, Implicitly Declared Classes and Instance Main Methods, Statements before super(), and Unnamed variable and patterns.

    String Templates is a great addition to Java. Apart from helping developers to work with strings that combine string constants and variables, they provide a layer of security. Custom String templates can be created with ease to accomplish multiple tasks, such as, to decipher letter combinations, either ignoring them or replacing them for added security.

    Java language designers are reducing the ceremony that is required to write the first HelloWorld code for Java students, by introducing implicitly declared classes and instance main methods. New students can start with bare minimum main() method, such as, void main() and build strong programming foundation by polishing their skills with basics like sequence, selection and iteration.

    In Java 22, the feature Statements before super() lets you execute code before calling super() in your derived class constructors, this() in your records or enums, so that you could validate the method parameters, or transform values, as required. This avoids creating workarounds like creating static methods and makes your code easier to read and understand. This feature doesn’t change how constructors would operate now vs. how they operated earlier – the JVM instructions remain the same.

    Unnamed variables are local to a code construct, they don’t have a name, and they are represented by using an underscore, that is, _. They can’t be passed to methods, or used in expressions. By replacing unused local variables in a code base with _ their intention is conveyed very clearly. It clearly communicates to anyone reading a code snippet that the variable is not used elsewhere. Until now, this intention could only be communicated via comments, which, unfortunately, all developers don’t write.

    Happy Coding!

    image description