Transforming Text with Matcher.replaceAll
Introduction
Welcome to the final lesson of Extracting Data with Capture Groups in Java! You've come a long way through this course, building skills that let you capture structured data with named groups, enforce consistency with backreferences, and extract practical information like emails and prices from messy text. In each lesson, you've focused on finding and extracting data, which is powerful, but there's another equally important skill: transforming the data you find into a different format.
In this lesson, we'll learn to use Matcher.replaceAll(), Java's regular expression substitution method, to search for patterns and replace them with new text. This is far more powerful than simple string replacement because we can transform what we find rather than just replacing it wholesale. For example, we might need to standardize phone numbers from various formats into a single consistent format or redact sensitive information while preserving parts of it for context. These transformations require us to understand what we captured and intelligently modify it.
We'll start by exploring how Matcher.replaceAll() works at a basic level, then introduce numbered backreferences that let us reuse captured groups in our replacement strings. You'll see how to normalize messy phone numbers into a standard format by rearranging their captured components. Next, we'll tackle more complex scenarios where simple replacements aren't enough: you'll learn to use lambda functions that execute custom logic on each match. By the end of this lesson, you'll be able to transform text patterns in sophisticated ways, completing your toolkit for both extracting and modifying data with regular expressions.
Understanding Text Transformation
Before diving into code, let's understand what makes Matcher.replaceAll() fundamentally different from Java's standard String.replace() method. With String.replace(), you specify an exact string to find and an exact string to replace it with. Every occurrence of "hello" becomes "goodbye," for instance. This works well for fixed text, but it falls apart when data varies: not all phone numbers look the same, and not all prices follow identical formatting.
The Matcher.replaceAll() method solves this by accepting a pattern rather than a fixed string. You define what to look for using all the regex tools you've learned: character classes, quantifiers, capture groups, and anchors. The method finds every match of your pattern, then replaces each match with new text. The replacement can be a simple string, but here's where it gets interesting: you can reference the captured groups from your pattern, letting you reorganize, reformat, or selectively modify the matched content.
This capability transforms Matcher.replaceAll() from a simple find-and-replace tool into a powerful data transformation engine. Instead of just changing text, you're restructuring it. A phone number like "(415) 555-2671" contains all the information needed to create "+1-415-555-2671," but those pieces need to be rearranged and reformatted. Similarly, "alice.smith@example.com" can become "a***@example.com" by keeping the first character, hiding the rest, and preserving the domain. These aren't simple replacements; they're intelligent transformations based on what was captured.
Basic Replacement with Matcher.replaceAll
Let's start with the simplest form of Matcher.replaceAll() to understand its syntax before adding complexity. The method works on a Matcher object that has already been created from a Pattern.
Here, the pattern "\\s+" matches one or more whitespace characters: spaces, tabs, newlines, anything classified as whitespace. The replacement string is a single space " ". The Matcher.replaceAll() method scans through text, finds every match of the pattern, and replaces each match with the replacement string. The effect is normalizing all whitespace to single spaces.
Notice that three different types of whitespace in the original text (multiple spaces, tabs, mixed) all became single spaces in the result. This demonstrates the pattern-based nature of Matcher.replaceAll(): we didn't need to know the exact whitespace characters to replace them. The pattern matched them all, and the replacement was applied uniformly. Now let's see how to incorporate captured data into our replacements.
Numbered Backreferences in Replacement Strings
Normalizing Phone Numbers with Backreferences
Testing Phone Number Normalization
Let's test our normalization method with a variety of phone number formats to verify it handles them all correctly:
The test string contains three phone numbers in different formats: "(415) 555-2671" with parentheses and spaces, "415-555-8899" with hyphens, and "2125550000" with no separators at all. Our pattern must recognize all three as valid phone numbers and transform them identically.
Perfect! All three numbers have been normalized to the "+1-AAA-BBB-CCCC" format. The parentheses, spaces, and hyphens from the original text are gone, replaced by a consistent structure. Even the ten-digit string "2125550000" was correctly parsed into area code "212," prefix "555," and line number "0000." The surrounding text ("Call," "or," "alt") remained unchanged because it didn't match the pattern. This demonstrates how Matcher.replaceAll() with backreferences transforms only the matched portions while leaving everything else intact.
Lambda Functions for Complex Logic
Numbered backreferences are powerful, but they have limitations: you can only rearrange and insert captured text, adding literal strings around it. What if you need to perform calculations on the captured data, apply conditional logic, or use standard Java methods? For these scenarios, Matcher.replaceAll() accepts a lambda function instead of a replacement string.
When you provide a lambda function, Matcher.replaceAll() calls that function once for each match it finds. The function receives a MatchResult object as its argument, and it must return a string that will replace the match. Inside your lambda, you can access captured groups with mr.group(1), mr.group(2), and so on. You can also use any Java code: string methods, arithmetic, conditionals, and external method calls.
This approach is particularly useful when the transformation depends on the captured content. For instance, you might want to redact email addresses but preserve the domain and the first character of the username for context. You can't do this with a simple replacement string because you need to compute which characters to keep and which to replace with asterisks. A lambda function gives you the flexibility to implement this logic in Java, then return the transformed string. Let's see this in action.
Redacting Emails with a Lambda
Now let's implement a more complex transformation: redacting email addresses to protect privacy while keeping enough information for context. We want "alice.smith@example.com" to become "a***@example.com": just the first character of the username is visible, the rest is replaced with asterisks, and the domain remains unchanged.
The pattern "([\\w\\.-])[\\w\\.-]*@([\\w\\.-]+\\.\\w+)" is cleverly designed. The first part ([\\w\\.-]) captures exactly one character from the username: a word character, dot, or hyphen. Then [\\w\\.-]* matches the rest of the username (zero or more characters) without capturing it. This distinction is crucial: we capture the first character because we need it, but we don't capture the rest because we're going to replace it with asterisks anyway. After the @ symbol, ([\\w\\.-]+\\.\\w+) captures the entire domain as group 2.
The lambda function takes the MatchResult object mr and constructs the redacted email. It accesses the first character with mr.group(1), adds three asterisks literally, adds the @ symbol, and then appends the domain from mr.group(2). This string concatenation becomes the replacement text for that particular match. Notice how the logic is expressed naturally in Java: we couldn't do this with numbered backreferences alone because we're not just rearranging captured groups; we're adding asterisks based on what we found.
Testing Email Redaction
Let's test the redaction method with several email formats to ensure it handles different username styles correctly:
The test string contains two different email formats: "alice.smith@example.com" with a dot in the username and a standard domain, and "Bob-B@example.co.uk" with a hyphen in the username and a country-specific domain. Both should be redacted while preserving the first character and full domain.
Excellent! Both emails were successfully redacted. "alice.smith@example.com" became "a***@example.com," preserving the lowercase 'a' and the full domain. "Bob-B@example.co.uk" became "B***@example.co.uk," preserving the uppercase 'B' and the multi-part domain. The lambda function handled each match individually, extracting the first character (whether lowercase or uppercase, letter or allowed special character) and building the appropriate redacted version. The surrounding text remained unchanged, and the domains stayed fully visible for context.
Putting It All Together
Now let's see both methods in action together, demonstrating how different transformation approaches work on different types of data:
These two methods represent the two main approaches to text transformation with Matcher.replaceAll(). The phone number normalizer uses numbered backreferences: it captures three groups (area code, prefix, line number) and rearranges them with new formatting. This approach is perfect when you need to reorganize existing data into a new structure. The email redactor uses a lambda function: it captures two groups (first character and domain) and applies custom logic to construct a new string. This approach is ideal when the transformation requires computation or conditional logic beyond simple rearrangement.
Both methods successfully transformed their respective data while leaving all other text untouched. The phone numbers now follow a consistent international format, making them easier to process, store, or display. The emails are redacted for privacy yet remain recognizable by their domains and first characters. These transformations demonstrate the practical power of Matcher.replaceAll(): you're not just finding patterns, you're intelligently modifying them to meet specific requirements. Combined with everything you've learned about capture groups, character classes, and quantifiers, you now have complete control over both extracting and transforming text data.
Conclusion and Next Steps
