IntelliJ IDEA
IntelliJ IDEA – the Leading Java and Kotlin IDE, by JetBrains
Pattern Matching in Java – 5 Examples for Busy Developers
As a busy developer, it is difficult to keep up with new features and deeply understand where and how you can use them.
In this blog post, I’ll cover 5 places where you can use pattern matching in Java without diving into the finer details. When you think you are ready to explore further, check out the links included in this blog post.
Let’s get started!
1. Improve code readability by converting long if-else constructs to switch
First, let’s address the most important question – why do we care about this conversion?
One of the main benefits is that the code is more concise and easier to read and understand. Since long if-else statements which usually don’t fit in a single screen and might involve vertical scrolling, it is difficult to understand the code that executes for all of the if comparisons. Also, the syntax of the if conditions can be unclear, because each if condition could have another set of conditions.
Often when you browse a code base, you notice code that is similar to the code shown below. For a long if-else construct, that conditionally assigns value to a local variable. Take a look at the code below. I’ll help you navigate it by highlighting certain sections a bit later:
private static String getValueText(Object value) { final String newExpression; if (value instanceof String) { final String string = (String)value; newExpression = '"' + StringUtil.escapeStringCharacters(string) + '"'; } else if (value instanceof Character) { newExpression = '\'' + StringUtil.escapeStringCharacters(value.toString()) + '\''; } else if (value instanceof Long) { newExpression = value.toString() + 'L'; } else if (value instanceof Double) { final double v = (Double)value; if (Double.isNaN(v)) { newExpression = "java.lang.Double.NaN"; } else if (Double.isInfinite(v)) { if (v > 0.0) { newExpression = "java.lang.Double.POSITIVE_INFINITY"; } else { newExpression = "java.lang.Double.NEGATIVE_INFINITY"; } } else { newExpression = Double.toString(v); } } else if (value instanceof Float) { final float v = (Float) value; if (Float.isNaN(v)) { newExpression = "java.lang.Float.NaN"; } else if (Float.isInfinite(v)) { if (v > 0.0F) { newExpression = "java.lang.Float.POSITIVE_INFINITY"; } else { newExpression = "java.lang.Float.NEGATIVE_INFINITY"; } } else { newExpression = Float.toString(v) + 'f'; } } else if (value == null) { newExpression = "null"; } else { newExpression = String.valueOf(value); } return newExpression; }
Let’s highlight the code to focus on. In the following image, notice that method getValueText
compares whether the value of the variable value
it is of a certain data type, such as String
, Character
, Long
, Double
or others:
To understand the other parts of this if-else construct, let’s focus on the variable, newExpression
. Notice that this variable is being assigned a value for all the possible values of the variable value
:
Interestingly, all but two code blocks corresponding to the if-conditions are longer than the other if blocks, which are usually just a single line of code:
Let’s extract these two longer code blocks to separate methods and then proceed with converting the if-else construct to switch.
To extract code to another method, select the code, invoke Context actions using Alt+Enter or (Option+Enter for macOS), and select the ‘extract method’ option. You can choose from the suggested names for the new method, or enter a name of your choice. To select logical pieces of code, my favorite shortcut is Ctrl+W (or Ctrl+Shift+W to shrink selection). After the method extraction, follow IntelliJ IDEA’s leads by noticing the keywords with yellow backgrounds and invoke context actions (Alt+enter). To convert this if-else to switch, I invoked context actions on ‘if’ and selected ‘convert ‘if’ to ‘switch’:
Here’s the switch construct in the method getValueText
that is concise and easier to understand:
private static String getValueText(Object value) { final String newExpression = switch (value) { case String string -> '"' + StringUtil.escapeStringCharacters(string) + '"'; case Character character -> '\'' + StringUtil.escapeStringCharacters(value.toString()) + '\''; case Long aLong -> value.toString() + 'L'; case Double aDouble -> getNewExpression(aDouble); case Float aFloat -> getNewExpression(aFloat); case null -> "null"; default -> String.valueOf(value); }; return newExpression; }
Does this make you wonder why you didn’t use switch expressions as often in your code as if-else constructs? There are several reasons for it. The switch construct has been enhanced in recent Java releases – they can return values (switch expressions) and they are no longer limited to comparing values for a limited primitive data type, wrapper classes, and others like String
or enum. Also, their case labels can include patterns and conditions.
With pattern matching and switch, you can also handle null values by using null as a case label. Also, each case label declares a pattern variable regardless of whether they are used in the corresponding code block. If you are concerned about the missing break labels, they are not required when you use the arrow styles with switch.
However, this feature is still in the preview stage, which implies you shouldn’t use it in your production code because it might change in a future Java version. Please follow this link to check on the configurations if you are not familiar with them.
Not all if-else statements can be converted to switch constructs. You can use the if-else constructs to define complex conditions that might use a combination of variables, constants, or method calls. Such complex comparisons are not yet supported by switch constructs.
For a detailed coverage on Pattern Matching, please check out this blog post.
Running the “if can be replaced with switch” inspection on your code base
It can be time-consuming to look for if-else constructs in your code and check if they can be replaced with switch. You can run the inspection ‘if can be replaced with switch’ on all the classes in your codebase or its subset, as covered in this blog post.
Let’s work with our next example, which uses pattern matching for instanceof, a production feature in Java.
2. Write concise code using PatternMatching with instanceof
Using pattern matching for the instanceof
operator has been available as a production feature since Java version 16 and is usable in production code.
To use this feature, I’ll just follow IntelliJ IDEA’s lead and invoke context actions on the if
keyword, which is highlighted with a yellow background.
Imagine you have a class, say, Monitor
. Here’s is one of the common examples you can find across codebases to implement its equals
method:
public class Monitor { String model; double price; @Override public boolean equals(Object object) { if (object instanceof Monitor) { Monitor other = (Monitor) object; return model.equals(other.model) && price == other.price; } return false; } }
The following gif shows how you could use pattern matching by invoking context actions on the variable named other
, highlighted using a yellow background, and then selecting the option ‘replace ‘other’ with pattern variable’. Refactoring the resultant code by invoking context actions on the if statement can make this code even more concise. The final code is easier to read and understand – return true if all three mentioned conditions are true.
What happens if instead of a regular class, you are working with an instance of a Record? For the Records, pattern matching for instanceof can deconstruct a Record instance by defining pattern variables for the Record components. In the following example, Citizen(String name, int age)
used with the instanceof
operator is a Record pattern:
It’s easy to miss the power of such features when we start with simple code examples like the two previous ones. Let’s quickly look at another example of using Pattern matching with the instanceof operator, where removing the declaration of a local variable leads to other refactoring or improvement possibilities. In short, combining this feature with other refactoring or code improvement techniques can help you write better code (just follow IntelliJ IDEA’s leads!):
3. Ignore states that don’t make sense
An if-else construct might not be the best choice to iterate over the values of a type that has an exhaustive set of values, like an enum or subtypes of a sealed class. For example, imagine you have an enum that defines a fix set of values, as follows:
enum SingleUsePlastic { BOTTLE, SPOON, CARRY_BAG; }
Even though you know an instance of the type SingleUsePlastic
can have either of the three values, that is, BOTTLE
, SPOON
and CARRY_BAG
, the following code wouldn’t compile for final local variable replacement
:
public class Citizen { String getReplacements(SingleUsePlastic plastic) { final String replacement; if (plastic == SingleUsePlastic.BOTTLE) { replacement = "Booth 4: Pick up a glass bottle"; } else if (plastic == SingleUsePlastic.SPOON) { replacement = "Pantry: Pick up a steel spoon"; } else if (plastic == SingleUsePlastic.CARRY_BAG) { replacement = "Booth 5: Pick up a cloth bag"; } return replacement; } }
To make it compile, you’ll need to add an else clause at the end that doesn’t make sense.
public class Citizen { String getReplacements(SingleUsePlastic plastic) { final String replacement; if (plastic == SingleUsePlastic.BOTTLE) { replacement = "Booth 4: Pick up a glass bottle"; } else if (plastic == SingleUsePlastic.SPOON) { replacement = "Pantry: Pick up a steel spoon"; } else if (plastic == SingleUsePlastic.CARRY_BAG) { replacement = "Booth 5: Pick up a cloth bag"; } else { replacement = ""; } return replacement; } }
With a switch construct, you don’t need to code a default
part for values that don’t exist:
public class Citizen { String getReplacements(SingleUsePlastic plastic) { final String replacement = switch (plastic) { case BOTTLE -> "Booth 4: Pick up a glass bottle"; case SPOON -> "Pantry: Pick up a steel spoon"; case CARRY_BAG -> "Booth 5: Pick up a cloth bag"; }; return replacement; } }
Similarly, if you define a sealed class, you can use a switch construct to iterate over its exhaustive list of subclasses without defining a default clause:
sealed interface Lego {} final class SquareLego implements Lego {} non-sealed class RectangleLogo implements Lego {} sealed class CharacterLego implements Lego permits PandaLego {} final class PandaLego extends CharacterLego {} public class MyLegoGame { int processLego(Lego lego) { return switch (lego) { case SquareLego squareLego -> 100; case RectangleLego rectangleLego-> 200; case CharacterLego characterLego -> 300; }; } }
If you are not familiar with sealed classes and wish to deep dive on this topic, you can access this blog post.
4. Powerful and concise data processing
You can create powerful, yet concise and expressive code to process your data by using a combination of record patterns, switch expressions, and sealed classes. Here’s an example of a sealed interface TwoDimensional
, which is implemented by Records Point
, Line
, Triangle
and Square
:
sealed interface TwoDimensional {} record Point (int x, int y) implements TwoDimensional { } record Line (Point start, Point end) implements TwoDimensional { } record Triangle (Point pointA, Point pointB, Point PointC) implements TwoDimensional { } record Square (Point pointA, Point pointB, Point PointC, Point pointD) implements TwoDimensional { }
The following method defines a recursive method process that uses a switch construct to return the sum of x and y coordinates of all the points in a two dimensional figure like a Line
, Triangle
or Square
:
static int process(TwoDimensional twoDim) { return switch (twoDim) { case Point(int x, int y) -> x + y; case Line(Point a, Point b) -> process(a) + process(b); case Triangle(Point a, Point b, Point c) -> process(a) + process(b) + process(c); case Square(Point a, Point b, Point c, Point d) -> process(a) + process(b) + process(c) + process(d); }; }
IntelliJ IDEA also displays the recursive call icon in the gutter for this method:
5. Separation of computation and side effect
You’d often notice code that combines a computation and a side effect (such as printing to console) in the same code block. For example, the following code uses an if-else block and instanceof
operator to determine the type of a variable and outputs a value conditionally in each if code block.
void printObject(Object obj) { if (obj instanceof String s) { System.out.println("String: \"" + s + "\""); } else if (obj instanceof Collection<?> c) { System.out.println("Collection (size = " + c.size() + ")"); } else { System.out.println("Other object: " + obj); } }
The following gif shows, how you can convert this if-else block to a switch construct and then use a new inspection on switch – Push down for ‘switch’ expressions, followed by extraction of a variable to separate computation and its side effects:
Summary
In this blog post, we covered 5 places where busy developers can use Pattern Matching in Java. If you are interested in learning more about these features or how IntelliJ IDEA helps you use them, refer to the links that I included while covering these examples.
Let me know what other topics you’d like me to cover in my next blog post.
Until then, happy coding!