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.

import java.util.regex.Pattern;
import java.util.regex.Matcher;

// Replace all sequences of whitespace with a single space
String text = "Hello    world\t\tfrom   Java";
Pattern p = Pattern.compile("\\s+");
Matcher m = p.matcher(text);
String result = m.replaceAll(" ");
System.out.println(result);

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.

Hello world from Java

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:

String phonesText = "Call (415) 555-2671 or 415-555-8899, alt 2125550000.";
System.out.println(normalizePhoneNumbers(phonesText));

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.

Call +1-415-555-2671 or +1-415-555-8899, alt +1-212-555-0000.

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.

import java.util.regex.MatchResult;

// Instead of a string, pass a lambda function to replaceAll
Pattern p = Pattern.compile("(\\d+)");
Matcher m = p.matcher(text);
String result = m.replaceAll(mr -> {
    // Custom logic here
    return "transformed";
});

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.

public static String redactEmails(String text) {
    // Capture first character of username separately, rest of username, and full domain
    Pattern p = Pattern.compile("([\\w\\.-])[\\w\\.-]*@([\\w\\.-]+\\.\\w+)");
    Matcher m = p.matcher(text);
    // Define lambda function that constructs redacted email
    return m.replaceAll(mr -> {
        return mr.group(1) + "***@" + mr.group(2);
    });
}

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:

String emailsText = "Contact alice.smith@example.com and Bob-B@example.co.uk today.";
System.out.println(redactEmails(emailsText));

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.

Contact a***@example.com and B***@example.co.uk today.

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:

String phonesText = "Call (415) 555-2671 or 415-555-8899, alt 2125550000.";
String emailsText = "Contact alice.smith@example.com and Bob-B@example.co.uk today.";

System.out.println(normalizePhoneNumbers(phonesText));
System.out.println(redactEmails(emailsText));

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.

Call +1-415-555-2671 or +1-415-555-8899, alt +1-212-555-0000.
Contact a***@example.com and B***@example.co.uk today.

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

Sign up

Join the 1M+ learners on CodeSignal

Be a part of our community of 1M+ users who develop and demonstrate their skills on CodeSignal