JAVA / VARIABLES, PRIMITIVES AND TYPES
char, Unicode code points and char versus String
Work with char as a 16-bit UTF-16 code unit, tell char apart from String, and read or count Unicode code points that need a surrogate pair.
What you will learn
- Tell char from String: single quotes give a 16-bit code unit, double quotes an object.
- Count real characters with codePointCount and read them with codePointAt.
- Predict that char + char yields an int, and cast the result back to char.
- Spot surrogate pairs with Character.isHighSurrogate and Character.charCount.
Understanding char, Unicode code points and char versus String
A char holds one 16-bit unsigned number, from 0 to 65535, and the only reason it appears on screen as a letter is that the type tells println to interpret it that way. Assigning a char to an int needs no cast, because char is a smaller integral type that widens, and that is how you see the numeric code unit sitting behind the glyph. Single quotes produce a char and double quotes produce a String object, so char c = "A"; is a type error, and so is String s = 'A';.
Sixteen bits was enough when Java was designed, because Unicode at the time promised to fit every character into 65536 slots. Unicode outgrew that limit, and by then char was frozen into the language and its libraries, so Java kept char at 16 bits and encodes strings as UTF-16: code points up to U+FFFF take one char, and everything above takes a surrogate pair, a high surrogate in U+D800 to U+DBFF followed by a low surrogate in U+DC00 to U+DFFF. The practical consequence is that String.length() counts code units rather than characters, and charAt can hand you half of a character that means nothing on its own.
In arithmetic a char behaves as a plain number: binary numeric promotion turns both operands into int, so 'a' + 'b' evaluates to 195 and not to "ab". Concatenation happens only when one operand is already a String, which is why the leading empty string in "" + 'a' + 'b' changes the result completely. Going the other way, an int result assigned back into a char needs an explicit cast, except for compile-time constants the compiler can verify fit, and for compound assignments like c += 1, which narrow implicitly. The model worth keeping is char for one UTF-16 code unit, int for a Unicode code point, String for text.
public class CharBasics {
public static void main(String[] args) {
char letter = 'A';
int code = letter; // char widens to int, no cast
System.out.println(letter + " -> " + code);
System.out.println('a' + 'b'); // numeric promotion to int
System.out.println("" + 'a' + 'b'); // String concatenation
String cafe = "caf\u00E9"; // e-acute fits in one char
String grin = "hi \uD83D\uDE00"; // U+1F600 needs two chars
System.out.println(cafe.length() + " chars, "
+ cafe.codePointCount(0, cafe.length()) + " code points");
System.out.println(grin.length() + " chars, "
+ grin.codePointCount(0, grin.length()) + " code points");
System.out.println(Character.isHighSurrogate(grin.charAt(3)));
System.out.println(grin.codePointAt(3));
}
}A char is a single 16-bit UTF-16 code unit, not a character, so any code point above U+FFFF occupies two chars and needs an int to hold it whole.
Worked examples
char in arithmetic
Shows when a char arithmetic result needs a cast and when the compiler narrows it for you.
public class CharMath {
public static void main(String[] args) {
char c = 'a' + 1; // constant expression, narrowed at compile time
System.out.println(c);
char base = 'a';
// char next = base + 1; // does not compile: int cannot become char
char next = (char) (base + 1);
System.out.println(next);
for (char digit = '0'; digit <= '9'; digit += 3) {
System.out.print(digit - '0');
}
System.out.println();
}
}Example explained
Line 1char c = 'a' + 1; compiles because 'a' + 1 is a compile-time constant whose value 98 fits in char, so the compiler inserts the narrowing itself.
Line 2base + 1 is an ordinary int expression evaluated at runtime, so the assignment to char requires the explicit (char) cast.
Line 3digit += 3 needs no cast: compound assignment carries an implicit narrowing conversion back to the left-hand type.
Line 4digit - '0' promotes both operands to int, so print receives a number and writes 0, 3, 6, 9 instead of characters.
Where a surrogate pair splits
Compares code-unit indexing against code-point iteration on a string holding one supplementary character.
public class CodePoints {
public static void main(String[] args) {
String s = "A\uD83D\uDE00B";
System.out.println("length() = " + s.length());
System.out.println("codePointCount = " + s.codePointCount(0, s.length()));
for (int i = 0; i < s.length(); i++) {
System.out.println(i + ": " + Integer.toHexString(s.charAt(i)));
}
s.codePoints().forEach(cp -> System.out.println("cp " + Integer.toHexString(cp)));
System.out.println("cut in half = " + s.substring(0, 2).codePointCount(0, 2));
}
}Example explained
Line 1length() is 4 because the single emoji occupies two of the four UTF-16 code units, while codePointCount reports the 3 characters a reader would see.
Line 2Indices 1 and 2 print d83d and de00, the high and low surrogate halves, neither of which is a usable character alone.
Line 3codePoints() rejoins the pair into 1f600, so the stream produces three values rather than four.
Line 4substring(0, 2) cuts between the halves, and the orphaned high surrogate is still counted as one code point, which is why the last line prints 2.
Important notes
Unicode escapes are decoded by the compiler before it tokenises the file, so '\u000A' is a compile error, a raw line break inside a char literal; use '\n' instead.
char is Java's only unsigned integral type, 0 to 65535, so (char) -1 is 65535. A code point is still not a user-visible character either: e plus a combining accent is two code points, and BreakIterator is what handles grapheme clusters.
Common mistakes
Expecting System.out.println('a' + 'b') to print ab. Both operands are numeric, so they promote to int and the line prints 195; write "" + 'a' + 'b' or String.valueOf(a) + b to get text.
Treating length() as a character count. For a string holding one emoji length() is 2, so charAt(0), substring(0, 1) and char-by-char reversal all yield a lone surrogate that renders as a box or question mark and can no longer be recombined.
Writing char c = "A"; or char c = 'AB';. Both fail at compile time, not at runtime: a String is never assignable to char, and a char literal holds exactly one code unit.
Try it yourself
Change, predict, then run
Declare String s = "a\uD83D\uDE00b" and print s.length() beside s.codePointCount(0, s.length()). Then write a loop that reads codePointAt(i) and advances i by Character.charCount so each code point is printed in hex exactly once.
Open the Java workspaceCheck your understanding
A String contains exactly one emoji whose Unicode code point is above U+FFFF. What do length() and charAt(0) report, and why?
- length() is 2 and charAt(0) returns only the high surrogate, because a char stores a 16-bit UTF-16 code unit
- length() is 1 and charAt(0) returns the whole code point, because char widens to 32 bits for supplementary characters
- length() is 4, because Java stores every String as UTF-32 internally
- length() is 2 but charAt(0) still returns the whole code point, because charAt reads ahead to the low surrogate
Show answer
A char is 16 bits wide and cannot hold a value above 0xFFFF, so the emoji is stored as a high surrogate followed by a low surrogate, and length() counts both units. The option claiming charAt reads ahead is tempting because codePointAt(0) genuinely does rejoin the pair, but charAt is defined to return the raw code unit at that index and knows nothing about its partner.