Pattern Matching
Guarded Patterns
When validating input or applying business rules, you often need both type matching and additional conditions. Guarded patterns combine type checking with boolean constraints using the when keyword, eliminating nested if statements inside case blocks.
Basic Syntax
Basic.java
public class Basic {
public static void main(String[] args) {
Object obj = ;
switch (obj) {
case String s when s.length() > 5 ->
System.out.println("Long string: " + s);
case String s ->
System.out.println("Short string: " + s);
case Integer i when i > 0 ->
System.out.println("Positive: " + i);
case Integer i ->
System.out.println("Non-positive: " + i);
default ->
System.out.println("Other type");
}
}
}
public class Basic {
public static void main(String[] args) {
Object obj = ;
switch (obj) {
case String s when s.length() > 5 ->
System.out.println("Long string: " + s);
case String s ->
System.out.println("Short string: " + s);
case Integer i when i > 0 ->
System.out.println("Positive: " + i);
case Integer i ->
System.out.println("Non-positive: " + i);
default ->
System.out.println("Other type");
}
}
}
public class Basic {
public static void main(String[] args) {
Object obj = ;
switch (obj) {
case String s when s.length() > 5 ->
System.out.println("Long string: " + s);
case String s ->
System.out.println("Short string: " + s);
case Integer i when i > 0 ->
System.out.println("Positive: " + i);
case Integer i ->
System.out.println("Non-positive: " + i);
default ->
System.out.println("Other type");
}
}
}
public class Basic {
public static void main(String[] args) {
Object obj = ;
switch (obj) {
case String s when s.length() > 5 ->
System.out.println("Long string: " + s);
case String s ->
System.out.println("Short string: " + s);
case Integer i when i > 0 ->
System.out.println("Positive: " + i);
case Integer i ->
System.out.println("Non-positive: " + i);
default ->
System.out.println("Other type");
}
}
}
when_clause
A `when` clause adds a boolean condition that must be true for the pattern to match, evaluated after the type pattern succeeds.
Refinement Patterns
Refinement.java
public class Refinement {
static String describeNumber(Object obj) {
return switch (obj) {
case Integer i when i == 0 -> "Zero";
case Integer i when i > 0 && i < 10 -> "Small positive";
case Integer i when i >= 10 && i < 100 -> "Medium positive";
case Integer i when i >= 100 -> "Large positive";
case Integer i when i < 0 && i > -10 -> "Small negative";
case Integer i when i <= -10 -> "Large negative";
case Double d when d > 0 -> "Positive decimal";
case Double d when d < 0 -> "Negative decimal";
case Double d -> "Zero decimal";
default -> "Not a number";
};
}
public static void main(String[] args) {
System.out.println(describeNumber(0));
System.out.println(describeNumber(5));
System.out.println(describeNumber(50));
System.out.println(describeNumber(500));
System.out.println(describeNumber(-5));
System.out.println(describeNumber(-50));
System.out.println(describeNumber(3.14));
}
}
refinement
Multiple guarded cases for the same type create refinement patterns, checked in order from most to least specific.
Evaluation Order
- Pattern matching (type check + bind)
- Guard evaluation (
whencondition) - If guard is true, execute the case
- Otherwise, try the next case
Null-Safe Guards
NullSafe.java
public class NullSafe {
static String process(String s) {
return switch (s) {
case null -> "null value";
case String str when str.isEmpty() -> "empty string";
case String str when str.isBlank() -> "blank string";
case String str when str.length() < 5 -> "short: " + str;
case String str -> "normal: " + str;
};
}
public static void main(String[] args) {
System.out.println(process(null));
System.out.println(process(""));
System.out.println(process(" "));
System.out.println(process("hi"));
System.out.println(process("hello world"));
}
}
Guards with Record Patterns
Records.java
public class Records {
record Point(int x, int y) {}
static String classify(Point p) {
return switch (p) {
case Point(int x, int y) when x == 0 && y == 0 -> "Origin";
case Point(int x, int y) when x == y -> "On diagonal (y=x)";
case Point(int x, int y) when x == -y -> "On anti-diagonal (y=-x)";
case Point(int x, int y) when x == 0 -> "On Y-axis";
case Point(int x, int y) when y == 0 -> "On X-axis";
case Point(int x, int y) when x > 0 && y > 0 -> "Quadrant I";
case Point(int x, int y) when x < 0 && y > 0 -> "Quadrant II";
case Point(int x, int y) when x < 0 && y < 0 -> "Quadrant III";
case Point(int x, int y) -> "Quadrant IV";
};
}
public static void main(String[] args) {
Point sample = ;
System.out.println(classify(sample));
System.out.println(classify(new Point(3, 3)));
System.out.println(classify(new Point(3, -3)));
System.out.println(classify(new Point(0, 5)));
System.out.println(classify(new Point(3, 4)));
}
}
public class Records {
record Point(int x, int y) {}
static String classify(Point p) {
return switch (p) {
case Point(int x, int y) when x == 0 && y == 0 -> "Origin";
case Point(int x, int y) when x == y -> "On diagonal (y=x)";
case Point(int x, int y) when x == -y -> "On anti-diagonal (y=-x)";
case Point(int x, int y) when x == 0 -> "On Y-axis";
case Point(int x, int y) when y == 0 -> "On X-axis";
case Point(int x, int y) when x > 0 && y > 0 -> "Quadrant I";
case Point(int x, int y) when x < 0 && y > 0 -> "Quadrant II";
case Point(int x, int y) when x < 0 && y < 0 -> "Quadrant III";
case Point(int x, int y) -> "Quadrant IV";
};
}
public static void main(String[] args) {
Point sample = ;
System.out.println(classify(sample));
System.out.println(classify(new Point(3, 3)));
System.out.println(classify(new Point(3, -3)));
System.out.println(classify(new Point(0, 5)));
System.out.println(classify(new Point(3, 4)));
}
}
public class Records {
record Point(int x, int y) {}
static String classify(Point p) {
return switch (p) {
case Point(int x, int y) when x == 0 && y == 0 -> "Origin";
case Point(int x, int y) when x == y -> "On diagonal (y=x)";
case Point(int x, int y) when x == -y -> "On anti-diagonal (y=-x)";
case Point(int x, int y) when x == 0 -> "On Y-axis";
case Point(int x, int y) when y == 0 -> "On X-axis";
case Point(int x, int y) when x > 0 && y > 0 -> "Quadrant I";
case Point(int x, int y) when x < 0 && y > 0 -> "Quadrant II";
case Point(int x, int y) when x < 0 && y < 0 -> "Quadrant III";
case Point(int x, int y) -> "Quadrant IV";
};
}
public static void main(String[] args) {
Point sample = ;
System.out.println(classify(sample));
System.out.println(classify(new Point(3, 3)));
System.out.println(classify(new Point(3, -3)));
System.out.println(classify(new Point(0, 5)));
System.out.println(classify(new Point(3, 4)));
}
}
Benefits
- Precise matching: combine type and value constraints
- Cleaner code: avoid nested ifs
- Readable: conditions are explicit in the pattern
Using Methods in Guards
Methods.java
public class Methods {
static boolean isPalindrome(String s) {
return s.equals(new StringBuilder(s).reverse().toString());
}
static String analyze(Object obj) {
return switch (obj) {
case String s when s.isEmpty() -> "Empty string";
case String s when isPalindrome(s) -> "Palindrome: " + s;
case String s when s.matches("[0-9]+") -> "Numeric string";
case String s when s.matches("[a-zA-Z]+") -> "Alphabetic string";
case String s -> "Other string: " + s;
default -> "Not a string";
};
}
public static void main(String[] args) {
System.out.println(analyze(""));
System.out.println(analyze("racecar"));
System.out.println(analyze("12345"));
System.out.println(analyze("hello"));
System.out.println(analyze("hello123"));
}
}
method_guard
Guards can call methods on the pattern variable, enabling complex validation logic.
Exercise: Practical.java
Build a request validator that uses guards to check authentication, rate limits, and payload validation