IntelliJ IDEA Java

Drop the Baggage: Use ‘_’ for Unnamed Local Variables and Patterns in Java 22

How would you feel if you tried to understand a code snippet, only to realize that some of the variables you were trying to make sense of, were never used? This could be due to the programming syntax constraints, or an oversight as a result of changes to a codebase over time. Would you like it if there was a way to mark such unused variables in the code, not via comments, but by using a language constraint (because real developers don’t write comments)? Also, what happens if a maintainer uses these unused variables in a way they were not supposed to be? Errors, maybe?

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. I’ll answer all these questions in the blog post, including how to spot unused local variables.

Before diving deep into the details, let’s take a quick look at one of the examples.

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.

Before we discuss any further details, the next section includes basic IntelliJ IDEA Project Configuration you’d need for working with this feature in IntelliJ IDEA. If you are already familiar with it, feel free to skip to the next section.

IntelliJ IDEA Configuration

Java 22 support is available in IntelliJ IDEA 2024.1 EAP. The final release of this version is planned for March 2024.

In your Project Settings, set the SDK to Java 22. For the language level, select ‘22 – Unnamed variables and patterns’ on both the Project and Modules tab, as shown in the below settings screenshot:

Let’s start by talking about situations where an unused variable can’t be deleted from your codebase for some reason.

Unused variables that can’t be dropped

Local variables remain unused in some code snippets; but you might not be able to delete them. This could happen due to the language constraints or you might be using the side effect of those variables. In this section, I’ll talk about the use cases where deleting an unused variable is either not possible or not recommended. In such cases, replacing them with an unnamed variable, that is, _, would be a good way to make it obvious and clear. Unnamed variables can’t be used – they can’t be passed to methods or assigned values.

Let’s talk about a sample code in which pattern variables are unused in a switch expression.

1. 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.

2. 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.

3. Requirements change, but you need side effects of constructs like an enhanced for-loop

Below is an example of sample code that prints elements of an array to the console in an animated manner (you don’t need to focus on the code logic. Just note that the local variable num is used within the for loop via a call to System.out.println()):

    void animateArray() {
        int[] numbers = {9, 7, 3, 8, 5, 2, 5};
        int totalTimePerLine = 1000;
        int count = 0;

        // array element at first position is printed once, 
 // second is printed twice and so on
        for (int num : numbers) {
            count++;

	     // each line takes a total of ‘totalTimePerLine’ to print
            int delayPerNumber = totalTimePerLine / count;

            for (int i = 0; i < count; i++) { 
                System.out.print(num);    
                try {
                    Thread.sleep(delayPerNumber);  
                } catch (InterruptedException e) {
                    System.out.println(e);
                }
            }

            System.out.println();
        }
    }

Imagine, after some time, the author of the previous code modifies it to print a fixed number, say, 0, instead of the array values. In this case, the code might no longer use the local loop variable num and it might change to the following (showing only the changed line of code for conciseness):

    void animateArray() {
	//..code
                System.out.print(0); // Remove the only usage of the variable num
	//..code
    }

Anyone reading the method animateArray() might not be able to know that at the first glance or read. However, the variable num can’t be deleted, because it is defined as a loop variable in the enhanced for loop.

Shortening the variable name to n, or using an implicit type, that is, var, instead of num, won’t help much. A human must read a code snippet at least once to determine that a variable is unused. Time gone that would never come back!

Also, it doesn’t help much if you replace this enhanced for loop with the indexed loop, since the indexed loop variable wouldn’t be used in the code too. In this case, the code is more interested in executing the code multiple times in a loop, rather than using the loop index. One of the simplest ways would be to announce that a variable is unused, so it could be ignored by folks reading this code.

IDEs like IntelliJ IDEA were always able to determine this issue via their static code analysis (unused variables). With this new language feature, it can also suggest that you could replace an unused variable with an _. Here’s a gif that shows this:

The next example talks about another approach to address the issue of an unused local variable.

4. Unused parameters in exception handlers; whose signature can’t be modified

What happens when you use a method, say, Thread.sleep() that throws a checked exception, such as, InterruptedException? You must either handle the exception by enclosing the call to Thread.sleep() in an exception handler, or, add the throws clause to the method in which Thread.sleep() is called. In short, either you handle the exception yourself, or request the caller of this method to handle it.

Let’s consider handling an exception via a catch handler which receives an instance of the thrown exception. Though the information passed via the exception instance is useful, imagine a developer doesn’t use it, and writes their code as follows:

try {
    Thread.sleep(delayPerNumber);
} catch (InterruptedException e) {
    System.out.println("Sleep mode interrupted");
}

In this case, the local variable e is unused. IntelliJ IDEA can determine it and suggest it to be replaced using _. You could also use the problems window to access the issues in your code base and apply the suggestions, as shown below:

5. Unused auto-closeable resources in try-with-resources statements

It is common to see code similar to the following which checks DB connection parameters, database server availability, or network issues (for hosted databases) by establishing a connection to a database. It logs success or warning messages, but doesn’t use the Connection instance, connection:

void checkConnection(String url, String user, String pwd) {
    try (Connection connection = DriverManager.getConnection(url, user, pwd)) {
        logger.info(STR."""
                        DB Connection successful
                        URL = \{url}
                        usr = \{user}
                        pwd = \{pwd}""");
    } catch (SQLException e) {
        logger.warning(e.toString());
    }
}

The preceeding code is a good candidate to replace the local variable connection within the try-with-resources, with an underscore, as follows:

void checkConnection(String url, String user, String pwd) {
    try (Connection _ = DriverManager.getConnection(url, user, pwd)) {
        logger.info(STR."""
                        DB Connection successful
                        URL = \{url}
                        usr = \{user}
                        pwd = \{pwd}""");
    } catch (SQLException e) {
        logger.warning(e.toString());
    }
}

With the examples in this section, I highlighted how to address the unused local variables, patterns and variables in your code, when they can’t be dropped from your code.

You could apply a similar approach to other unused local variables that are defined within methods, including constructors, instance and static initialisers, if, for or while constructs, and passed as method parameters.

Now, let’s walk through a few examples where unused variables could imply a code smell.

When unused variables could be a code smell

You might need tests or experience to determine if the unused variables should be addressed in a way other than replacing their name using an underscore, that is, _.

1. Unused lambda parameter

Imagine you define the following code in which one of the lambda parameters, that is, x, is used and y is not:

BiFunction<Integer, Integer, Integer> add = (x, y) -> x + 0;

The previous code seems to be a code smell. The name of the method seems to suggest that it returns the sum of the values passed to it. In this case the corrective step should be to modify it as follows:

BiFunction<Integer, Integer, Integer> add = (x, y) -> x + y;

The preceding code is a simple example for demo purposes. When you refactor any code in your code, back them up using sufficient tests, so that the refactored code generates the same output.

Also, the preceding code example doesn’t imply that all unused method parameters are a code smell (as discussed previously in this blog post).

2. Methods with multiple responsibilities

As a good programming practice, a method should have a single responsibility. However, a method might not adhere to this practice. For instance, in the following code, the method generateRandomNumbers() prints a random number to the console, before returning it:

public class RandomNumber {
    public static void main(String[] args) {
        var randomNumber = generateRandomNumber();
        System.out.println(randomNumber);
    }
    
    public static int generateRandomNumber() {
        int random = new Random().nextInt(100);
        System.out.println(random);
        return random;
    }
}
<code></code>

On execution, a user might see two outputs for the same random number, due to the two println() method calls – one in the method generateRandomNumbers() and the other in method main(). What if another developer deletes the call to the println() statement in method main(). In this case, the local variable radomNumber becomes unused, as follows:

public static void main(String[] args) {
    var randomNumber = generateRandomNumber();
}

Should you replace the name of variable randomNumber using an _? Nah! This looks like a code smell since the method generateRandomNumber() includes code to accomplish unrelated tasks – returning a random number and printing a random number to the console. A better solution would be to refactor your code, extracting the code to print a number to another method, so that generateRandomNumber() only generates a random number:

Here’s the final code for your reference:

public class RandomNumber {
    public static void main(String[] args) {
        printNumberToConsole(generateRandomNumber());
    }
    
    public static int generateRandomNumber() {
        return new Random().nextInt(100);
    }

    private static void printNumberToConsole(int random) {
        System.out.println(random);
    }
}

Having test methods to ensure that your method continues to serve their primary purpose is a good programming practice. I didn’t include that part specifically since it would take the blog post in a tangent.

In this blog post, I didn’t cover an extensive list of use cases where an unused local variable could be a code smell.

Summary

As the name suggests, 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.

A production feature in Java 22, you could use this feature in your production code, without any fears of it being changed in one of the future versions.

image description