Matching Pattern for java objects
Extracting data from objects enables us to write concise, safe, and maintainable code, making it easier for other developers to understand and modify. Java has introduced pattern matching to simplify common coding tasks, particularly when working with object types and control structures. The two main features are:
instanceof+ Pattern Matching (Java 16+)switchPattern Matching (Java 17 preview, finalized in Java 21)
1. Pattern Matching with instanceof
Before Java 16, type-checking and casting were separate steps:
if (obj instanceof String) {
String s = (String) obj; // Manual casting
System.out.println(s.length());
}With pattern matching (Java 16+):
if (obj instanceof String s) { // Auto-casting into 's'
System.out.println(s.length()); // No explicit cast needed
}2. Pattern Matching in switch (Java 21+)
Java 21 finalized pattern matching for switch, allowing type-based branching without instanceof chains:
if (shape instanceof Circle) {
Circle c = (Circle) shape;
System.out.println("Radius: " + c.getRadius());
} else if (shape instanceof Rectangle) {
Rectangle r = (Rectangle) shape;
System.out.println("Area: " + r.getWidth() * r.getHeight());
} else {
System.out.println("Unknown shape");
}Modern Approach (Java 21+)
switch (shape) {
case Circle c -> System.out.println("Radius: " + c.getRadius());
case Rectangle r -> System.out.println("Area: " + r.getWidth() * r.getHeight());
default -> System.out.println("Unknown shape");
}3. Advanced Use Cases
3.1 Matching on null (Java 21+)
switch (obj) {
case String s -> System.out.println("String: " + s);
case null -> System.out.println("Null object!"); // Explicit null handling
default -> System.out.println("Other type");
}3.2 Nested Patterns (Java 21+)
if (obj instanceof Point(int x, int y)) { // Deconstructing a record
System.out.println("X: " + x + ", Y: " + y);
}3.3 Sealed Classes + Pattern Matching
sealed interface Shape permits Circle, Rectangle { }
record Circle(double radius) implements Shape { }
record Rectangle(double w, double h) implements Shape { }
// Exhaustive pattern matching (compiler checks all cases)
double area(Shape s) {
return switch (s) {
case Circle c -> Math.PI * c.radius() * c.radius();
case Rectangle r -> r.w() * r.h();
};
}A
sealed interfaceis an interface that restricts which classes or interfaces can implement it. This provides fine-grained control over inheritance hierarchies, improving API design and enabling safer pattern matching.
4. Generic types as operands
The instanceof operator in Java can be used with generic types, but there are strict rules about which combinations are valid. These rules depend on the subtype relationship between the generic types and how they are parameterized.
Get Tadeo Aragón’s stories in your inbox
Join Medium for free to get updates from this writer.
4.1 Direct Relationship (ArrayList<String> → List<String>)
List<String> stringList = new ArrayList<>();
if (stringList instanceof ArrayList<String>) { // Valid
System.out.println("ArrayList<String> detected");
}Why it works:
ArrayList<String>is a direct subtype ofList<String>- Both the collection type (
ArrayList) and generic parameter (String) match exactly
4.2 No Type Relationship — Invariance (Invalid)
List<String> stringList = new ArrayList<>();
if (stringList instanceof List<CharSequence>) { // Compile error
System.out.println("This will never work");
}Why it fails:
- Although
StringextendsCharSequence - Generics are invariant:
List<String>is NOT a subtype ofList<CharSequence> - Prevents unsafe additions of other
CharSequencetypes
The following code demonstrates why Java’s generic type invariance is necessary, showing what could go wrong if List<String> were incorrectly treated as a subtype of List<CharSequence>
// Hypothetical dangerous scenario (doesn't actually compile)
List<String> stringList = new ArrayList<>();
stringList.add("Hello");
// If Java allowed this (which it doesn't):
List<CharSequence> charSeqList = stringList; // ACTUALLY PROHIBITED BY COMPILER
// Then we could corrupt the original list:
charSeqList.add(new StringBuilder()); // Adding non-String to List<String>
String s = stringList.get(1); // ClassCastException at runtime!
// This is why Java generics are invariant4.3 Unbounded Wildcard (Valid)
List<String> stringList = new ArrayList<>();
if (stringList instanceof List<?>) { // Valid
System.out.println("Matches any List type");
}Why it works:
List<?>is supertype of allList<T>variants- Safe because it makes no assumptions about element type
- Can only read as
Object, can't add elements
4.4 Bounded Wildcard (Valid with restrictions)
List<String> stringList = new ArrayList<>();
if (stringList instanceof List<? extends CharSequence>) { // Valid
System.out.println("Matches CharSequence subtypes");
CharSequence cs = stringList.get(0); // Can read
// stringList.add("new"); // Can't add (compile error)
}Why it works:
? extends CharSequenceincludesStringand other subtypes- Safe for reading but not writing
Use
extendsto produce (get) andsuperto consume (add).
4.4 Raw Type (Valid but dangerous)
List<String> stringList = new ArrayList<>();
if (stringList instanceof List) { // Valid but warning
System.out.println("Raw type match");
// Loses all generic type safety
}Why it works but is bad:
- Backward compatibility with pre-Java 5 code
- Bypasses generic type checking
- Generates compiler warnings
Conclusion
Pattern matching with instanceof and switch represents a natural evolution in Java, designed to:
- Reduce Boilerplate Code
- Eliminates verbose type checks and manual casting.
- Combines type testing and variable binding in a single step.
2. Enhance Code Safety & Readability
- Compile-time type verification prevents
ClassCastException. - Clear, expressive syntax aligns with modern coding practices.
3. Future-Proof Your Designs
- Seamlessly integrates with newer features like
sealedclasses andrecords - Prepares codebases for upcoming Java enhancements










