TL;DR: conforming UTF-8, as presently defined, cannot encode some valid (to Java) Java String
s, on account of disallowing code sequences encoding surrogate code points.
In the beginning, Java chose 16-bit Unicode code points for representing characters. That was perhaps short-sighted, but 16 bits was sufficient for all of Unicode at the time. Correspondingly, Java's primitive char
type is 16-bits wide, and, uniquely among Java's primitive types, it is unsigned.
Such a representation is a bit unwieldy for a C-based JVM implementation, however, because raw Java character data tends to contain lots of null bytes, making it unsuitable for use with C string functions. To much the same degree, the Java representation also tends to be larger, which is unfortunate for storage-constrained devices. Such devices were definitely among Java's initial targets.
The UTF-8 of the time addressed both of these issues, which made it a good choice for the native-code representation of Java string data. It's unclear why Sun chose a variant encoding of code point 0 (which still contains a null byte), but there are at least two plausible explanations:
- it helps distinguish between embedded Java char's with value 0 and C string terminators; and
- it makes certain kinds of potential JVM string-handling bugs easier to troubleshoot.
These are not mutually exclusive.
And since the Unicode code point space did not provide for code points wider than 16 bits, and no code points were reserved (e.g. for surrogates), that one variant encoding was the only difference between Java's character encoding and standard UTF-8. Moreover, it's a pretty benign difference, because a UTF-8 decoder has to go out of its way to reject the Java variant-zero. A naive UTF-8 decoder will just decode it to code point 0, which is exactly what it in fact represents.
So Java pretty much chose UTF-8, not some amalgamation. And then UTF-8 was yanked out from under it.
When Unicode widened the code point space and created surrogate pairs and UTF-16, it also specified that UTF-8 encodings of surrogate code points were invalid code sequences. That made Java's (and others') existing practice non-conforming. But why should Sun have cared? They had working software, and the character representation Java used internally was, after all, internal. They stuck with it.
And Sun didn't particularly pivot on String
s, either. To this day, they are sequences of 16-bit unsigned integer values corresponding to Unicode code points in the BMP (including surrogates). Java does produce and consume UTF-16 surrogate pairs inside strings, but it does not forbid arbitrary placement of surrogate code points within. In this sense, String
s' character-data nature is not quite complete. It was much later that Java introduced code-point-based interfaces to String
s, which must effectively perform UTF-16 decoding of the character data, and the traditional char
based interfaces are still available and widely used.
So, Sun just continuing to do what they were already doing is what moved Java from slightly-quirky-if-that UTF-8 to the bona fide-variant UTF-8 now known as CESU-8. But they couldn't move to UTF-8 even if they wanted to do, because Java String
s' allowance for unpaired surrogates makes them impossible to encode in conforming UTF-8. I suppose it's a bonus that it's easier to transcode Java String
data to CESU-8 and back, exactly because there is a one-to-one correspondence between CESU-8 code sequences and Java char
s. No interpretaion of surrogate pairs is required.