Pattern Matching for instanceof, finalized in Java 16 and included in Java 17 through JEP 394, revolutionizes how we perform type checks and casts in Java. This feature eliminates the redundant casting that traditionally follows an instanceof check, making your code more concise and less error-prone.
The Old Way vs. The New Way
Before Java 17 (Traditional Approach)
public void processObject(Object obj) {
if (obj instanceof String) {
String str = (String) obj; // Redundant cast
System.out.println("String length: " + str.length());
}
}
With Java 17 (Pattern Matching)
public void processObject(Object obj) {
if (obj instanceof String str) { // Type test + binding in one
System.out.println("String length: " + str.length());
}
}
The pattern variable str is automatically created and scoped to the if block, eliminating the need for an explicit cast.
How It Works
When you use pattern matching with instanceof, Java performs two operations simultaneously:
- Type Test: Checks if the object is an instance of the specified type
- Binding: If the test succeeds, creates a new variable of that type
if (obj instanceof String str) {
// 'str' is available here and is of type String
System.out.println(str.toUpperCase());
}
// 'str' is NOT available here (out of scope)
Scope Rules
The pattern variable is only in scope where the compiler can guarantee the instanceof check succeeded:
public void demonstrateScope(Object obj) {
// Pattern variable in if statement
if (obj instanceof String str) {
System.out.println(str.length()); // ✓ str is in scope
}
// System.out.println(str.length()); // ✗ Compile error: str out of scope
// Pattern variable with logical AND
if (obj instanceof String str && str.length() > 5) {
System.out.println("Long string: " + str); // ✓ str is in scope
}
// Pattern variable with logical OR
if (obj instanceof String str || str.isEmpty()) { // ✗ Compile error
// str might not be initialized if first condition is false
}
// Pattern variable with negation
if (!(obj instanceof String str)) {
// System.out.println(str); // ✗ str is NOT in scope here
} else {
System.out.println(str.toUpperCase()); // ✓ str IS in scope here
}
}
Real-World Example: Processing Different Message Types
public sealed interface Message permits TextMessage, ImageMessage, VideoMessage {}
public record TextMessage(String content, String sender) implements Message {}
public record ImageMessage(byte[] data, String caption, String sender) implements Message {}
public record VideoMessage(byte[] data, int duration, String sender) implements Message {}
public class MessageProcessor {
public void processMessage(Message message) {
if (message instanceof TextMessage txt) {
System.out.println("Text from " + txt.sender() + ": " + txt.content());
} else if (message instanceof ImageMessage img) {
System.out.println("Image from " + img.sender());
System.out.println("Caption: " + img.caption());
System.out.println("Size: " + img.data().length + " bytes");
} else if (message instanceof VideoMessage vid) {
System.out.println("Video from " + vid.sender());
System.out.println("Duration: " + vid.duration() + " seconds");
System.out.println("Size: " + vid.data().length + " bytes");
}
}
public String getMessageSummary(Message message) {
if (message instanceof TextMessage txt && txt.content().length() > 100) {
return "Long text message from " + txt.sender();
} else if (message instanceof TextMessage txt) {
return "Text: " + txt.content();
} else if (message instanceof ImageMessage img) {
return "Image: " + img.caption();
} else if (message instanceof VideoMessage vid) {
return "Video (" + vid.duration() + "s)";
}
return "Unknown message type";
}
}
Combining with Logical Operators
Pattern matching works seamlessly with logical operators:
public void validateInput(Object input) {
// Using AND to add additional conditions
if (input instanceof String str && !str.isEmpty() && str.length() < 100) {
System.out.println("Valid string: " + str);
}
// Using AND with multiple pattern variables
if (input instanceof String str && str.matches("\\d+")) {
int number = Integer.parseInt(str);
System.out.println("Parsed number: " + number);
}
}
Practical Example: Shape Area Calculator
public abstract class Shape {
public abstract double area();
}
public class Circle extends Shape {
private final double radius;
public Circle(double radius) {
this.radius = radius;
}
public double getRadius() {
return radius;
}
@Override
public double area() {
return Math.PI * radius * radius;
}
}
public class Rectangle extends Shape {
private final double width;
private final double height;
public Rectangle(double width, double height) {
this.width = width;
this.height = height;
}
public double getWidth() {
return width;
}
public double getHeight() {
return height;
}
@Override
public double area() {
return width * height;
}
}
public class ShapeAnalyzer {
public String analyzeShape(Shape shape) {
if (shape instanceof Circle c && c.getRadius() > 10) {
return "Large circle with area: " + c.area();
} else if (shape instanceof Circle c) {
return "Small circle with area: " + c.area();
} else if (shape instanceof Rectangle r && r.getWidth() == r.getHeight()) {
return "Square with area: " + r.area();
} else if (shape instanceof Rectangle r) {
return "Rectangle with area: " + r.area();
}
return "Unknown shape";
}
public double calculatePerimeter(Shape shape) {
if (shape instanceof Circle c) {
return 2 * Math.PI * c.getRadius();
} else if (shape instanceof Rectangle r) {
return 2 * (r.getWidth() + r.getHeight());
}
return 0;
}
}
Benefits
1. Reduced Boilerplate
No more redundant casting after instanceof checks.
2. Improved Readability
The intent is clearer when the type test and binding happen together.
3. Fewer Errors
Eliminates the possibility of casting to the wrong type after an instanceof check.
4. Better Performance
The JVM only performs the type check once, not twice (once for instanceof and once for the cast).
Common Patterns
Null-Safe Processing
public int getStringLength(Object obj) {
if (obj instanceof String str) {
return str.length();
}
return 0; // null or not a String
}
Nested Conditions
public void processData(Object data) {
if (data instanceof List<?> list && !list.isEmpty()) {
if (list.get(0) instanceof String firstStr) {
System.out.println("First element: " + firstStr);
}
}
}
Early Returns
public String formatValue(Object value) {
if (!(value instanceof Number num)) {
return "Not a number";
}
// num is in scope for the rest of the method
return String.format("%.2f", num.doubleValue());
}
Conclusion
Pattern Matching for instanceof is a simple yet powerful feature that makes Java code more concise and readable. By combining type testing with variable binding, it eliminates redundant casts and reduces the potential for errors. This feature works particularly well with sealed classes and modern Java patterns, making your code both safer and more expressive.