The default charset¶
Level: 201 · for anyone who has met mojibake
One line: Since Java 18 the default charset is UTF-8 on every platform, which ended a twenty-year class of bug — but the JVM still knows your locale, reports it as native.encoding, and the two are now allowed to disagree.
What JEP 400 changed¶
Before Java 18, Charset.defaultCharset() was derived from the operating system's locale. The same program, the same file, the same JVM version produced different text on a Polish Windows box (windows-1250), a US Mac (UTF-8), and a Linux server started by cron with no locale (US-ASCII). Code that used new String(bytes) or new FileReader(f) — the overloads with no charset argument — was silently non-portable, and the symptom appeared in production rather than on the developer's machine.
JEP 400 made UTF-8 the default everywhere, in Java 18. This is the single biggest improvement to Java text handling in the language's history, and it is worth being clear that Java got there late: Python 3 had made the same decision a decade earlier.
What it did not change¶
Charset.defaultCharset() = UTF-8
file.encoding = UTF-8
native.encoding = US-ASCII (canonical name)
^ the RAW property string is platform-specific:
macOS says US-ASCII, Linux says ANSI_X3.4-1968.
One charset, two IANA aliases. Canonicalise before
you ever compare a charset name to a literal.
stdout.encoding = UTF-8
Locale.getDefault() = en_US
This run is under LC_ALL=C. file.encoding is UTF-8 anyway;
native.encoding followed the locale. Since Java 18 those two
can disagree, and before Java 18 they could not.
That run was under LC_ALL=C. file.encoding is UTF-8 anyway — that is JEP 400 working. native.encoding reported US-ASCII, because it is a new property whose whole job is to tell you what the environment says, now that file.encoding no longer does.
Before Java 18 those two could not disagree. Now they can, and each answers a different question:
| Property | Question it answers |
|---|---|
file.encoding |
what does Java use when you don't say? — UTF-8 |
native.encoding |
what does the host environment claim? — locale-derived |
stdout.encoding |
what will System.out encode to? — console-derived |
The trap that is left, and it bit this repo¶
stdout.encoding follows the console, not file.encoding. In a terminal or a CI job with LC_ALL=C, System.out encodes to US-ASCII — and Java transliterates anything it cannot encode to a literal ? rather than failing. Your data is fine; your output is not, and nothing says so.
The first run of this library's own test suite recorded za???? as the correct answer for zażółć. The fix is in tools/run_examples.py: every example is launched with -Dstdout.encoding=UTF-8 -Dstderr.encoding=UTF-8.
A charset has more than one name¶
The output above canonicalises native.encoding before printing it, and that is not tidiness. The raw property string is platform-specific: macOS reports US-ASCII, Linux reports ANSI_X3.4-1968. Those are the same charset — the second is its official IANA registry name and the first is an alias — but they are different strings, and CI caught this library asserting one of them as the answer.
So: never compare a charset name to a string literal. Charset.forName(name).name() canonicalises, and Charset.forName(a).equals(Charset.forName(b)) is the comparison you actually meant.
Two flags worth knowing¶
-Dfile.encoding=COMPATrestores the pre-18, locale-derived behaviour. It exists for old code that depended on it, and it is the honest way to reproduce a legacy bug.-Dfile.encoding=ISO-8859-1and friends still work on Java 25, though JEP 400 documents onlyUTF-8andCOMPATas supported values. Measured, not assumed — but do not build on it.