Skip to content

A char is not a character

Level: 101 · start here

One line: char is a 16-bit number, Unicode has characters that need 21 bits, and Java's answer is to store those as two chars — so charAt() can hand you half a character, and will do it without complaining.

The arithmetic

A char holds 0 to 65535. Unicode's highest code point is U+10FFFF, which is 1,114,111. The gap is not small: about 94% of the code space does not fit in a char.

Characters above U+FFFF — emoji, historic scripts, many CJK extensions, most mathematical alphabets — are stored as a surrogate pair: two char values from a reserved block, which mean nothing on their own and are only text when read together. That encoding is UTF-16, and in Java it is not a file format you opt into. It is what a String is, in memory, always.

What that looks like

Character.MAX_VALUE  = U+FFFF
highest code point   = U+10FFFF

the string           = 🎯
length()             = 2   <- UTF-16 code units, not characters
charAt(0)            = U+D83C  isHighSurrogate=true
charAt(1)            = U+DFAF  isLowSurrogate=true
codePointAt(0)       = U+1F3AF
fits in a char?      = false

charAt(0) printed    = [?]  <- a lone surrogate: valid Java, invalid text

The last line is the important one. charAt(0) returned a value, printed without error, and produced something that is not a character in any font. Java has no "half a character" type to refuse you with, because char is the half.

What to use instead

Instead of Use
s.charAt(i) s.codePointAt(i) — returns an int, the whole character
for (char c : s.toCharArray()) s.codePoints().forEach(...)
(char) n to build text Character.toString(n) — takes a code point, returns a String
s.length() see Length is three different numbers

The int in codePointAt is not a widening for safety's sake. It is the actual type of a Unicode character in Java. There is no 21-bit primitive, so int is the character type and char is a storage unit with a misleading name.

The one place char is still right

ASCII. If you are parsing a protocol, a number, or a file format whose grammar is defined in ASCII, every character you care about is below 128, a surrogate can never be one of them, and charAt is exact and fast. The bug arrives when the same loop meets a user's name.

See also