Chapter 9: Strings and Character Processing
9.08Beginner / Intermediate5 topics14 min3 interactive animations

Java Unicode considerations

Java strings hold UTF-16, so correct text processing must handle surrogate pairs, iterate by code point, know about Unicode normalization, avoid locale-sensitive case conversion surprises and stop assuming that one char is one character.

Learning goals

  • Recognise and handle surrogate pairs
  • Iterate a string by code point
  • Explain normalization and locale pitfalls
  • Avoid naive char-based assumptions

Prerequisites

String fundamentals
9.08.1

Surrogate pairs

Code points above U+FFFF are stored as a surrogate pair: a high surrogate (U+D800 to U+DBFF) followed by a low surrogate (U+DC00 to U+DFFF). Neither half is a valid character alone. Character.isHighSurrogate, isLowSurrogate and Character.toCodePoint(high, low) work with them.

Step through a string with an emoji and see both units form one pair.

Java
1String s = "��";
2char hi = s.charAt(0); // '\uD83D'
3char lo = s.charAt(1); // '\uDE00'
4boolean pair = Character.isHighSurrogate(hi) && Character.isLowSurrogate(lo); // true
5int cp = Character.toCodePoint(hi, lo); // 0x1F600

UTF-16 code units vs Unicode code points

Interactive · edit the input

Your input

Try

UTF-16 code units (hex) · length 5

0048
0069
D83D
DE00
00E9
01234
One code unit = one code pointSurrogate pair = one code pointAlready read
Setup

Java strings store UTF-16 code units. s.length() counts units, not characters: here 5.

Step 1 / 6
Java
1int units = s.length(); // UTF-16 code units
2int count = s.codePointCount(0, units); // real characters
3for (int i = 0; i < units; ) {
4 int cp = s.codePointAt(i); // combines a surrogate pair into one code point
5 i += Character.charCount(cp); // 2 for supplementary characters, otherwise 1
6}

Live state

s.length()

5

No condition evaluated on this step
Execution trace (dry run table)Show 6 steps
StepLineStateWhat happens
1L1s.length()=5Java strings store UTF-16 code units. s.length() counts units, not characters: here 5.
2L4i=0 cp=U+48 charCount(cp)=1Unit 48 is a whole character (U+48).
3L4i=1 cp=U+69 charCount(cp)=1Unit 69 is a whole character (U+69).
4L4i=2 cp=U+1F600 charCount(cp)=2Units D83D and DE00 form a surrogate pair: one character, U+1F600. charAt(2) alone would return half of it.
5L4i=4 cp=U+E9 charCount(cp)=1Unit E9 is a whole character (U+E9).
6L6s.length()=5 codePointCount=4 return=5 units, 4 code points5 code units but 4 code points. Use codePointCount and codePoints() when you mean characters.
9.08.2

Code-point iteration

Iterate code points rather than chars when the text may contain supplementary characters: use s.codePoints(), or a loop with codePointAt and Character.charCount. A plain for over charAt visits each half of a surrogate pair separately.

The animation advances by one or two units per step.

Java
1String s = "a��b";
2int i = 0, chars = 0;
3while (i < s.length()) {
4 int cp = s.codePointAt(i);
5 chars++;
6 i += Character.charCount(cp);
7} // chars == 3, while s.length() == 4
8long letters = s.codePoints().filter(Character::isLetter).count(); // 2

UTF-16 code units vs Unicode code points

Interactive · edit the input

Your input

Try

UTF-16 code units (hex) · length 5

0048
0069
D83D
DE00
00E9
01234
One code unit = one code pointSurrogate pair = one code pointAlready read
Setup

Java strings store UTF-16 code units. s.length() counts units, not characters: here 5.

Step 1 / 6
Java
1int units = s.length(); // UTF-16 code units
2int count = s.codePointCount(0, units); // real characters
3for (int i = 0; i < units; ) {
4 int cp = s.codePointAt(i); // combines a surrogate pair into one code point
5 i += Character.charCount(cp); // 2 for supplementary characters, otherwise 1
6}

Live state

s.length()

5

No condition evaluated on this step
Execution trace (dry run table)Show 6 steps
StepLineStateWhat happens
1L1s.length()=5Java strings store UTF-16 code units. s.length() counts units, not characters: here 5.
2L4i=0 cp=U+48 charCount(cp)=1Unit 48 is a whole character (U+48).
3L4i=1 cp=U+69 charCount(cp)=1Unit 69 is a whole character (U+69).
4L4i=2 cp=U+1F600 charCount(cp)=2Units D83D and DE00 form a surrogate pair: one character, U+1F600. charAt(2) alone would return half of it.
5L4i=4 cp=U+E9 charCount(cp)=1Unit E9 is a whole character (U+E9).
6L6s.length()=5 codePointCount=4 return=5 units, 4 code points5 code units but 4 code points. Use codePointCount and codePoints() when you mean characters.
9.08.3

Normalization overview

Some characters can be written in more than one way: “é” is either one code point (U+00E9) or an “e” followed by a combining accent (U+0065 U+0301). The two look the same but are not equals. java.text.Normalizer converts text to a canonical form (NFC composes, NFD decomposes) so that comparisons and hashing agree. Normalize user input before comparing or storing it.

NFD followed by removing combining marks is a common way to strip accents.

Java
1String a = "\u00E9"; // é as one code point
2String b = "e\u0301"; // e + combining acute accent
3boolean raw = a.equals(b); // false
4boolean nfc = Normalizer.normalize(a, Normalizer.Form.NFC)
5 .equals(Normalizer.normalize(b, Normalizer.Form.NFC)); // true
9.08.4

Locale-sensitive case conversion

toLowerCase() and toUpperCase() without an argument use the default locale, which changes the result in some languages: in Turkish, uppercase I lowercases to dotless ı, so "TITLE".toLowerCase() is not "title". A single character can also change length (German ß uppercases to SS). Pass Locale.ROOT (or Locale.ENGLISH) for protocol text, identifiers and keys.

Use the user's locale only for text shown to people.

Java
1String t = "TITLE".toLowerCase(Locale.ROOT); // "title" everywhere
2String turkish = "TITLE".toLowerCase(new Locale("tr")); // "tıtle" (dotless i)
3String up = "straße".toUpperCase(Locale.ROOT); // "STRASSE": length changed
9.08.5

Avoiding naive char assumptions

Common wrong assumptions: one char is one character (emoji take two units); length() is the number of characters; reversing char by char keeps text valid (use StringBuilder.reverse, which keeps pairs intact); toUpperCase keeps the length; two equal-looking strings are equals (they may differ in normalization); one character always has one case form. For ASCII-only data, such as DNA or identifiers, the simple char loops are fine.

Decide up front whether your input is ASCII, and validate it if you rely on it.

AssumptionCounterexampleFix
length() = number of characters"😀".length() == 2codePointCount
Reversing chars is safereversed surrogates are invalidStringBuilder.reverse()
toUpperCase keeps length"ß" becomes "SS"do not index by old length
Same look means equalsé vs e + U+0301Normalizer

UTF-16 code units vs Unicode code points

Interactive · edit the input

Your input

Try

UTF-16 code units (hex) · length 5

0048
0069
D83D
DE00
00E9
01234
One code unit = one code pointSurrogate pair = one code pointAlready read
Setup

Java strings store UTF-16 code units. s.length() counts units, not characters: here 5.

Step 1 / 6
Java
1int units = s.length(); // UTF-16 code units
2int count = s.codePointCount(0, units); // real characters
3for (int i = 0; i < units; ) {
4 int cp = s.codePointAt(i); // combines a surrogate pair into one code point
5 i += Character.charCount(cp); // 2 for supplementary characters, otherwise 1
6}

Live state

s.length()

5

No condition evaluated on this step
Execution trace (dry run table)Show 6 steps
StepLineStateWhat happens
1L1s.length()=5Java strings store UTF-16 code units. s.length() counts units, not characters: here 5.
2L4i=0 cp=U+48 charCount(cp)=1Unit 48 is a whole character (U+48).
3L4i=1 cp=U+69 charCount(cp)=1Unit 69 is a whole character (U+69).
4L4i=2 cp=U+1F600 charCount(cp)=2Units D83D and DE00 form a surrogate pair: one character, U+1F600. charAt(2) alone would return half of it.
5L4i=4 cp=U+E9 charCount(cp)=1Unit E9 is a whole character (U+E9).
6L6s.length()=5 codePointCount=4 return=5 units, 4 code points5 code units but 4 code points. Use codePointCount and codePoints() when you mean characters.

Practice ladder

Now try it yourself

Easy

  • How many code points and how many units are in "a😀b"?
  • Why is s.charAt(0) of an emoji not a valid character?

Medium

  • Reverse a string that may contain emoji correctly
  • Compare "é" written two ways using Normalizer

Advanced

  • Write a method that truncates a string to n characters without splitting a surrogate pair

Key takeaways

  • Supplementary characters are surrogate pairs; iterate code points to see characters.
  • Normalize before comparing text that may use combining marks.
  • Use Locale.ROOT for case conversion of program text.