Masking a string except the last 4 characters in Java means that we replace every character before the last 4 with a mask character such as *, using “*”.repeat(length – 4) followed by substring(length – 4). The result keeps the same length, so the reader still sees how long the value is.
We mask values such as card numbers, account numbers and phone numbers before we show them on a receipt, in an email or on a profile page. The customer can recognize their own value by the last 4 characters, while anyone looking over their shoulder sees nothing useful.
The following example masks a card number and leaves a value of 4 characters or less unchanged.
String card = "4111222233334444";
int visible = Math.min(4, card.length());
String masked = "*".repeat(card.length() - visible) + card.substring(card.length() - visible); // "************4444"
String pin = "123";
String maskedPin = pin.length() <= 4 ? pin : "*".repeat(pin.length() - 4) + pin.substring(pin.length() - 4); // "123"
The repeat() call builds the whole mask in one step. Next, we turn the code into a null-safe helper, write the same rule as a one-line regex, keep spaces and dashes in formatted numbers, and look at emoji, logs and email addresses.
1. A maskExceptLast() Helper With repeat()
The rule has two parts. The mask covers length – 4 characters, and the last 4 characters are copied as they are. The method String.repeat(count), added in Java 11, builds the mask, and repeating a string with a count of 0 returns the empty string.

A helper makes the number of visible characters and the mask character parameters. It returns null for null so that callers can still tell a missing value from a real one, and returns short values unchanged.
static String maskExceptLast(String value, int visible, char mask) {
if (value == null) {
return null;
}
int maskLength = value.length() - visible;
if (maskLength <= 0) {
return value;
}
return String.valueOf(mask).repeat(maskLength) + value.substring(maskLength);
}
String cardMask = maskExceptLast("4111222233334444", 4, '*'); // "************4444"
String account = maskExceptLast("987654321", 4, 'x'); // "xxxxx4321"
String exact = maskExceptLast("6789", 4, '*'); // "6789"
String tiny = maskExceptLast("789", 4, '*'); // "789"
String empty = maskExceptLast("", 4, '*'); // ""
String absent = maskExceptLast(null, 4, '*'); // null
Returning the input for a short value can show the whole value. A card number of 3 characters is invalid anyway, but a short secret such as a 4-digit PIN should be masked completely, so for secrets we check the length and return “****” instead.
2. Masking With a Regex Lookahead
The regex .(?=.{4}) matches one character when at least 4 more characters follow it. The part in parentheses is a lookahead, which checks the text after the match without consuming it. Every character except the last 4 has 4 characters after it, so replaceAll() masks the right ones.
String byRegex = "4111222233334444".replaceAll(".(?=.{4})", "*"); // "************4444"
String shortRegex = "789".replaceAll(".(?=.{4})", "*"); // "789"
The regex version is one line, but it scans the string with a pattern match at every position. It also needs a null check, and . does not match line terminators, so a value with a newline needs the (?s) flag. We use it when masking is one rule among other regex rules and the plain helper otherwise.
3. Keeping Spaces and Dashes in Formatted Numbers
Formatted values such as 4111 2222 3333 4444 or 555-010-7788 should keep their separators, so the masked value has the same shape. The idea is to mask only digits and to count only digits when deciding which ones stay visible.
The regex \d(?=(?:\D*\d){4}) does both. It matches a digit only when at least 4 more digits follow it, with any number of non-digits, \D*, allowed between them.
String spacedCard = "4111 2222 3333 4444".replaceAll("\\d(?=(?:\\D*\\d){4})", "*"); // "**** **** **** 4444"
String dashedPhone = "555-010-7788".replaceAll("\\d(?=(?:\\D*\\d){4})", "*"); // "***-***-7788"
String splitTail = "12-34-56".replaceAll("\\d(?=(?:\\D*\\d){4})", "*"); // "**-34-56"
A simpler pattern such as [^-](?=.{4}) counts characters, not digits. It masks the spaces of a card number and, when a separator falls inside the last 4 positions, it shows fewer than 4 digits.
String charCount = "4111 2222 3333 4444".replaceAll("[^-](?=.{4})", "*"); // "***************4444"
String fewDigits = "12-34-56".replaceAll("[^-](?=.{4})", "*"); // "**-*4-56"
4. Masking Text That Contains Emoji
The helper from section 1 counts UTF-16 char values. Card and phone numbers contain only ASCII digits, so that is fine for them. A username or a display name can contain emoji, which take two char values each, and the helper can cut an emoji in half.
The Java Pattern engine matches . against whole code points, so the lookahead regex keeps emoji intact. Each emoji counts as one visible character and becomes one *.
String handle = "ab" + Character.toString(0x1F600) + "cdef";
String regexMasked = handle.replaceAll(".(?=.{4})", "*"); // "***cdef"
String helperMasked = maskExceptLast("x" + Character.toString(0x1F600) + "cde", 4, '*');
boolean splitEmoji = Character.isLowSurrogate(helperMasked.charAt(2)); // true
For text typed by people, either use the regex or build the helper on code points with codePointCount() and offsetByCodePoints(), as the guide to get the last 4 characters of a string shows for the tail.
5. Masking Card Numbers and Log Output in a Real App
An online shop shows the order confirmation page with the card number as ************4444. Payment card rules, such as the PCI DSS standard, require the card number to be masked when it is displayed to people without a business need to see it, and showing only the last 4 digits is the common choice in payment software, for example in Oracle Retail EFTLink, which shows only the last four digits by default.
Masking for display is not a way to store data safely. The full card number should never reach our database or logs, and when a value needs protection at rest we encrypt or tokenize it. Masking is the last step before the value goes to a screen, an email or a log line.
For log files, masking every call site by hand misses some of them. Logback can mask values with a pattern for the whole application, as the guide to masking sensitive data in Logback shows, and our helper stays useful for the values we show to users.
| Approach | null input | Keeps separators | Emoji safe | Use it for |
|---|---|---|---|---|
| maskExceptLast() with repeat() | Returns null | No | No | Plain digits, clearest code |
| replaceAll(“.(?=.{4})”, “*”) | NullPointerException | No | Yes | Short one-off code, user text |
| replaceAll(“\\d(?=(?:\\D*\\d){4})”, “*”) | NullPointerException | Yes | n/a (digits only) | Formatted card and phone numbers |
| char[] with Arrays.fill() | NullPointerException | No | No | Code that must avoid new strings |
The Java String tutorial lists repeat() together with the other modern String methods.
6. String Masking FAQs
Card numbers are rarely the only value to hide, so the next questions cover email addresses, partial masks, Java 8 and the length of the mask.
6.1. How do we mask an email address in Java?
We keep the first character of the local part and the full domain, and mask the rest of the local part. The regex (?<=.)[^@](?=[^@]*@) matches every character after the first one and before the @.
String email = "lokesh@example.com";
String maskedEmail = email.replaceAll("(?<=.)[^@](?=[^@]*@)", "*"); // "l*****@example.com"
6.2. How do we mask all characters except the first and last 4?
We combine a prefix and a suffix with a mask in the middle, and return the value unchanged when it is too short to hide anything.
String number = "4111222233334444";
String middle = number.length() > 8
? number.substring(0, 4) + "*".repeat(number.length() - 8) + number.substring(number.length() - 4)
: number;
String both = middle; // "4111********4444"
6.3. How do we mask a string without String.repeat() on Java 8?
We fill a char[] with Arrays.fill() and create a string from it. The same code also works on Java 25 when we want to avoid building intermediate strings.
char[] chars = "987654321".toCharArray();
Arrays.fill(chars, 0, Math.max(0, chars.length - 4), '*');
String filled = new String(chars); // "*****4321"
6.4. Should the masked value have the same length as the original?
Not always. A same-length mask shows how long the value is, which is fine for a card number. For passwords and secrets, we show a fixed mask such as ********, so the length is not revealed.
7. Conclusion
The repeat() plus substring() helper masks everything except the last 4 characters, returns null for null and leaves short values alone. The lookahead regex .(?=.{4}) does the same in one line and keeps emoji whole.
For formatted numbers, the digit-counting regex keeps spaces and dashes and always shows 4 digits. Masking belongs at the point where data goes to a screen or a log, and it never replaces encryption for stored values.
8. References
- String.repeat() (Java SE 25)
- Pattern (Java SE 25)
- Arrays.fill() (Java SE 25)
- Oracle Retail EFTLink, PAN Masking Rules
Happy Learning !!