instanceof and Pattern Matching in Java
instanceof tests the runtime type, and since Java 16 it can bind a typed variable in the same step.
-
Java Basics
- Introduction to Java
- Setting Up Java and Writing Your First Program
- Variables, Data Types and Literals in Java
- Type Casting and Type Conversion in Java
- Operators and Expressions in Java
- Input and Output in Java
- Comments, Keywords and Naming Conventions in Java
- Control Flow in Java: if, else and switch
- Loops in Java: for, while and do-while
- Methods
- Arrays and Strings
-
OOP
- Classes and Objects in Java
- Constructors in Java
- The this Keyword in Java
- The static Keyword in Java
- Encapsulation in Java
- Access Modifiers in Java
- Inheritance in Java
- Method Overriding and super in Java
- Polymorphism in Java
- Abstraction, Abstract Classes and Interfaces in Java
- Composition, Aggregation and Association in Java
- The Object Lifecycle in Java
- Core Java
- Exception Handling
-
Collections
- The Java Collections Framework
- List in Java: ArrayList, LinkedList, Vector and Stack
- Set in Java: HashSet, LinkedHashSet and TreeSet
- Map in Java: HashMap, LinkedHashMap and TreeMap
- How HashMap Works Internally in Java
- Queue and Deque in Java: ArrayDeque and PriorityQueue
- Iterators in Java
- Comparable and Comparator in Java
- Collections Utilities and Choosing the Right Collection
- Generics
- Java 8+
- Stream API
- Date and Time
- File and I/O
-
Multithreading
- Threads in Java: Processes, Runnable and Thread
- Thread Lifecycle in Java
- Synchronization in Java: synchronized and volatile
- Locks and Atomic Classes in Java
- Race Conditions and Deadlocks in Java
- The Executor Framework and Thread Pools in Java
- Future and CompletableFuture in Java
- Concurrent Collections in Java
- The Java Memory Model
- JVM and Memory
- Advanced Java
- Networking
- JDBC
- Testing
The classic form
Object value = "Java notes";
if (value instanceof String) {
String text = (String) value; // the cast repeats what we just tested
System.out.println(text.length());
}instanceof asks whether the object on the left is an instance of the type on the right, including subtypes and implemented interfaces. It returns false for null, never an exception, which makes an explicit null check unnecessary.
Pattern matching for instanceof
if (value instanceof String text) { // Java 16 and later
System.out.println(text.length());
}The type pattern declares text and assigns it only when the test succeeds. The cast disappears, and so does the chance of casting to the wrong type by mistake.
Scope of the pattern variable
if (value instanceof String text && text.length() > 3) {
System.out.println(text.toUpperCase()); // in scope here
}
if (!(value instanceof String text)) {
return; // not in scope inside
}
System.out.println(text.length()); // in scope from here onwardsThe variable is in scope exactly where the compiler can prove the test passed. The second form works with an early return and keeps the main path flat.
A practical example
static String render(Object value) {
if (value instanceof Integer count) {
return "count: " + count;
}
if (value instanceof List<?> items) {
return "list of " + items.size();
}
if (value instanceof String text && !text.isBlank()) {
return "text: " + text.strip();
}
return "unsupported";
}Record patterns
record Point(int x, int y) { }
record Line(Point start, Point end) { }
static String describe(Object shape) {
if (shape instanceof Line(Point(var x1, var y1), Point(var x2, var y2))) {
return "from " + x1 + "," + y1 + " to " + x2 + "," + y2;
}
return "not a line";
}Since Java 21 a pattern can destructure a record, binding its components directly and nesting to any depth. This removes a chain of accessor calls and makes the shape of the data visible in the code.
Combining with switch
static double area(Shape shape) {
return switch (shape) {
case Circle c -> Math.PI * c.radius() * c.radius();
case Square s -> s.side() * s.side();
case Rectangle r -> r.width() * r.height();
};
}When Shape is a sealed interface, the compiler knows every permitted subtype and accepts the switch without a default. Adding a new subtype then becomes a compile error at every switch that has not been updated, which is exactly the safety you want.
instanceof compared with getClass
instanceof | getClass() == | |
|---|---|---|
| Subtypes match | Yes | No |
| Interfaces match | Yes | No |
null handling | Returns false | Throws NullPointerException |
| Typical use | General type tests | Strict equals implementations |
When type testing is a smell
// Every new type means editing this method
if (shape instanceof Circle c) { return circleArea(c); }
if (shape instanceof Square s) { return squareArea(s); }
// Better when you own the hierarchy: let each type answer for itself
interface Shape { double area(); }Prefer polymorphism when you control the types. Pattern matching is the right tool when you do not, when the hierarchy is sealed and deliberately closed, or when the logic genuinely belongs outside the types.
Generics and erasure
// if (value instanceof List<String>) // compile error
if (value instanceof List<?> items) { } // allowedGeneric type arguments are erased at runtime, so only the unbounded wildcard form can be tested.
Common mistakes
- Adding a redundant
value != nullcheck beforeinstanceof. - Testing against a supertype first, so later branches are unreachable.
- Using long
instanceofchains where an overridden method would be cleaner. - Expecting
instanceof List<String>to compile. - Casting after the test in modern Java, when the pattern variable already exists.
Best practices
- Use the pattern form; it is shorter and removes a whole class of cast errors.
- Order tests from most specific to most general.
- Combine a sealed hierarchy with a pattern switch so the compiler enforces exhaustiveness.
- Reach for polymorphism first when the types are yours to change.
Practice
- Rewrite a test and cast pair using a type pattern.
- Why does
null instanceof Stringreturn false instead of throwing? - Write a method that formats
Integer,StringandListdifferently using pattern matching. - Convert an
instanceofchain over a sealed interface into a pattern switch, and remove thedefault. - Explain why the negated form
if (!(o instanceof X x)) return;still leavesxusable afterwards.
Conclusion
Modern instanceof tests and binds in one step, record patterns destructure data, and a sealed hierarchy makes a pattern switch exhaustive. Together they replace most casting with something the compiler can verify.