Java 8 introduced some powerful functional interfaces, and one of the most versatile is Predicate. A Predicate represents a boolean-valued function of one argument. While it’s commonly used for simple checks, combining it with regular expressions opens up a world of possibilities for more complex data validation and filtering, all within a concise and readable lambda expression.
In this post, we’ll explore how to leverage Java’s Pattern.compile() method to create efficient and reusable regex-based predicates. This approach is particularly useful when you need to perform the same regex validation multiple times, avoiding recompilation overhead for each check.
The Problem with Repeated Regex Matching
Let’s say you have a list of strings, and you want to filter them based on whether they match a specific regular expression. A naive approach might look something like this:
import java.util.Arrays;
import java.util.List;
public class BasicRegexFilter {
public static void main(String[] args) {
List emails = Arrays.asList(
"[email protected]",
"invalid-email",
"[email protected]",
"[email protected]"
);
String emailRegex = "^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,6}$";
System.out.println("Valid emails (naive approach):");
for (String email : emails) {
if (email.matches(emailRegex)) { // Compiles regex every time
System.out.println(email);
}
}
}
}
While this code works, the String.matches() method internally compiles the regular expression pattern every single time it’s called. For a small list, this overhead might be negligible, but for larger datasets or frequent validation, it can become a performance bottleneck.
Introducing Pattern.compile() and Predicate
To optimize this, we can pre-compile the regular expression into a Pattern object once and then reuse it. Combining this with a Java 8 Predicate allows for a more functional and efficient approach.
Here’s how you can do it:
import java.util.Arrays;
import java.util.List;
import java.util.function.Predicate;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
public class RegexPredicateExample {
public static void main(String[] args) {
List emails = Arrays.asList(
"[email protected]",
"invalid-email",
"[email protected]",
"[email protected]",
"another.invalid@com"
);
String emailRegex = "^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,6}$";
// 1. Compile the regex pattern once
Pattern pattern = Pattern.compile(emailRegex);
// 2. Create a Predicate using a lambda expression
// The test method of the Predicate uses the pre-compiled pattern
Predicate emailPredicate = (email) -> pattern.matcher(email).matches();
System.out.println("Valid emails (using Predicate):");
emails.stream()
.filter(emailPredicate) // Apply the predicate
.forEach(System.out::println);
// You can also chain predicates for more complex validation
Predicate startsWithT = (s) -> s.startsWith("t");
Predicate endsWithCom = (s) -> s.endsWith(".com");
System.out.println("
Emails starting with 't' AND ending with '.com':");
emails.stream()
.filter(emailPredicate) // First filter for valid email format
.filter(startsWithT.and(endsWithCom)) // Then apply chained predicates
.forEach(System.out::println);
}
}
Explanation:
Pattern pattern = Pattern.compile(emailRegex);: This line is crucial. It compiles the regular expression string into aPatternobject once. This object can then be reused many times without the overhead of recompilation.Predicate emailPredicate = (email) -> pattern.matcher(email).matches();: Here, we define ourPredicate. For each stringemailpassed to itstest()method, it creates aMatcherobject from our pre-compiledpatternand the inputemail. Thematches()method of theMatcherthen performs the actual regex comparison.emails.stream().filter(emailPredicate)...: We then use Java 8 Streams to filter our list of emails, applying our newly createdemailPredicate. This makes the code very readable and expressive.
Benefits of this Approach
- Performance: By compiling the regex once, you significantly reduce overhead when performing multiple matches.
- Readability: The functional style with
Predicatemakes the code more concise and easier to understand, especially when dealing with filtering operations. - Reusability: The
Predicatecan be easily passed around and reused in different parts of your application where the same validation logic is needed. - Flexibility: You can combine predicates using logical operations (
and(),or(),negate()) for more complex filtering criteria.
Conclusion
Integrating regular expressions with Java 8’s Predicate and Pattern.compile() provides an elegant and efficient solution for string validation and filtering. This pattern is a great example of how modern Java features can lead to cleaner, more performant, and more maintainable code. Next time you find yourself repeatedly checking strings against a regex, remember this powerful combination!