Deepen PracticeOrdered learning track

Unicode, Charset, Encoding & Text Boundaries

Learn Java Data Types, Type Semantics, Object Model & Data Representation - Part 022

Deep dive into Unicode, UTF-16, code points, surrogate pairs, grapheme clusters, charsets, UTF-8, encoders/decoders, normalization, locale-sensitive text, and byte-text boundary correctness in Java.

11 min read2151 words
PrevNext
Lesson 2234 lesson track1928 Deepen Practice
#java#data-types#unicode#charset+8 more

Part 022 — Unicode, Charset, Encoding & Text Boundaries

Target part ini: memahami perbedaan text, character, code point, code unit, glyph, grapheme cluster, charset, encoding, bytes, dan normalization. Fokusnya bukan teori Unicode murni, tetapi correctness di boundary Java enterprise: HTTP, JSON, file, database, message broker, logs, search, identity, dan audit.

1. Masalah Fundamental

Banyak engineer memperlakukan text seperti ini:

text == array of char == array of bytes

Itu salah.

Model yang lebih benar:

Di Java:

  • String merepresentasikan sequence of UTF-16 code units.
  • char adalah 16-bit code unit, bukan selalu “satu karakter manusia”.
  • byte boundary membutuhkan charset eksplisit.
  • Unicode equality tidak selalu sama dengan Java String.equals jika normalization berbeda.

Core rule:

Text correctness fails when we confuse user-perceived characters,
Unicode code points, UTF-16 code units, and encoded bytes.

2. Positioning dalam Framework Kaufman

Subskill yang harus dikuasai:

SubskillKemampuan Praktis
Unicode mental modelMenjelaskan perbedaan code point, code unit, grapheme cluster
Java string indexingMenghindari bug karena length() dan charAt() berbasis UTF-16 code unit
Charset boundarySelalu encode/decode dengan charset eksplisit
Error handlingMenentukan apakah invalid byte harus reject, replace, atau report
NormalizationMenentukan kapan text harus NFC/NFD/canonicalized
Locale correctnessMemisahkan domain canonicalization dari human language behavior
Enterprise boundaryMendesain kontrak API/DB/file/message yang tidak merusak text

3. Vocabulary yang Harus Rapi

IstilahArti Praktis
CharacterIstilah ambigu; bisa berarti huruf manusia, code point, atau code unit tergantung konteks
Code pointNomor Unicode abstract, misalnya U+0041 untuk A
Code unitUnit storage encoding; di UTF-16 ukurannya 16 bit
char Java16-bit unsigned value, satu UTF-16 code unit
Surrogate pairDua UTF-16 code unit untuk merepresentasikan satu code point di luar BMP
Grapheme clusterSatu karakter yang dirasakan user, bisa terdiri dari beberapa code point
GlyphBentuk visual yang dirender font
CharsetMapping antara Unicode characters/code units dan bytes
EncodingProses merepresentasikan text sebagai bytes dengan charset tertentu
DecodingProses membaca bytes menjadi text dengan charset tertentu
NormalizationMengubah representasi Unicode menjadi bentuk canonical/compatible tertentu

Jika vocabulary ini kacau, API design ikut kacau.

4. Java char Bukan “Karakter Manusia”

String s = "A";
System.out.println(s.length());  // 1
System.out.println(s.charAt(0)); // A

Ini terlihat sederhana.

Sekarang gunakan emoji:

String s = "😀";
System.out.println(s.length()); // 2
System.out.println(s.codePointCount(0, s.length())); // 1

Kenapa length() bernilai 2? Karena emoji tersebut direpresentasikan sebagai surrogate pair di UTF-16.

Rule:

String.length() counts UTF-16 code units, not user-perceived characters.

5. charAt dan Surrogate Pair

Anti-pattern:

String s = "😀";
char first = s.charAt(0);
System.out.println(first); // bukan emoji lengkap

Gunakan code point API:

String s = "😀";
int cp = s.codePointAt(0);
System.out.println(Integer.toHexString(cp)); // 1f600

Iterasi code point:

String text = "A😀B";
text.codePoints().forEach(cp -> {
    System.out.println(Integer.toHexString(cp));
});

Atau manual:

for (int i = 0; i < text.length(); ) {
    int cp = text.codePointAt(i);
    System.out.println(Integer.toHexString(cp));
    i += Character.charCount(cp);
}

Rule:

Use char-level APIs only when you intentionally operate on UTF-16 code units.

6. Code Point Bukan Grapheme Cluster

Code point juga belum tentu sama dengan “satu karakter yang dilihat user”.

Contoh:

String e1 = "é";        // precomposed: U+00E9
String e2 = "e\u0301"; // e + combining acute accent

System.out.println(e1.equals(e2)); // false
System.out.println(e1.length());   // 1
System.out.println(e2.length());   // 2

Secara visual keduanya bisa tampak sama: é.

Model:

Untuk user-perceived character boundaries, problemnya lebih kompleks:

  • emoji dengan skin tone modifier.
  • family emoji dengan zero-width joiner.
  • huruf + combining marks.
  • flag emoji yang terdiri dari regional indicators.

Jika domain membutuhkan grapheme-aware counting/truncation/display, jangan hanya pakai length() atau substring().

7. Safe Truncation

Anti-pattern:

String shortText = text.substring(0, 10);

Risiko:

  • memotong surrogate pair.
  • memotong combining mark.
  • menghasilkan tampilan rusak.
  • membuat text invalid secara semantik walau Java String masih object valid.

Lebih baik untuk code point boundary:

static String truncateByCodePoints(String text, int maxCodePoints) {
    Objects.requireNonNull(text, "text");
    if (maxCodePoints < 0) {
        throw new IllegalArgumentException("maxCodePoints must be >= 0");
    }
    int count = text.codePointCount(0, text.length());
    if (count <= maxCodePoints) {
        return text;
    }
    int end = text.offsetByCodePoints(0, maxCodePoints);
    return text.substring(0, end);
}

Untuk grapheme cluster boundary, gunakan library atau API text boundary yang memang dirancang untuk internationalized text. Jangan membuat parser Unicode sendiri kecuali memang domain library Anda.

8. Normalization

Unicode memungkinkan beberapa representasi untuk text yang tampak sama.

String composed = "é";
String decomposed = "e\u0301";

System.out.println(composed.equals(decomposed)); // false

Gunakan java.text.Normalizer:

String a = Normalizer.normalize(composed, Normalizer.Form.NFC);
String b = Normalizer.normalize(decomposed, Normalizer.Form.NFC);

System.out.println(a.equals(b)); // true

Empat bentuk umum:

FormMeaning Praktis
NFCCanonical composition; sering cocok untuk storage/search equality umum
NFDCanonical decomposition; sering muncul di beberapa filesystem/input
NFKCCompatibility composition; lebih agresif, bisa mengubah semantic appearance
NFKDCompatibility decomposition; lebih agresif lagi

Jangan sembarang memakai NFKC/NFKD untuk data legal atau identifier tanpa threat model, karena compatibility normalization dapat menyamakan karakter yang secara domain mungkin harus dibedakan.

Rule:

Normalize intentionally at boundary. Do not normalize blindly inside core domain.

9. Case Folding, Case Conversion, dan Locale

String key = input.toLowerCase();

Ini memakai default locale.

Untuk machine/domain identifier:

String key = input.toLowerCase(Locale.ROOT);

Untuk user-facing display:

String title = input.toUpperCase(userLocale);

Case-insensitive comparison:

boolean same = a.equalsIgnoreCase(b);

Namun untuk security-sensitive identifier, lebih baik canonicalize sekali dengan aturan eksplisit lalu bandingkan canonical representation.

record UsernameKey(String value) {
    UsernameKey {
        Objects.requireNonNull(value, "value");
        value = Normalizer.normalize(value.strip(), Normalizer.Form.NFC)
                .toLowerCase(Locale.ROOT);
        if (value.isBlank()) {
            throw new IllegalArgumentException("blank username");
        }
    }
}

Untuk username global production, ini masih belum cukup. Anda perlu policy tentang allowed scripts, confusables, spoofing, dan display name vs login key.

10. Confusable Characters dan Spoofing

Unicode punya banyak karakter yang terlihat mirip.

Contoh konseptual:

A  Latin capital A
А  Cyrillic capital A

Keduanya bisa terlihat mirip tetapi code point berbeda.

Risiko:

  • user impersonation.
  • phishing identifier.
  • duplicate account yang terlihat sama.
  • policy code yang tampak valid tapi bukan ASCII.

Untuk machine identifiers, gunakan whitelist ketat.

static String canonicalAsciiCode(String raw) {
    String value = Objects.requireNonNull(raw, "raw")
            .strip()
            .toUpperCase(Locale.ROOT);

    if (!value.matches("[A-Z0-9_]+")) {
        throw new IllegalArgumentException("Invalid code");
    }
    return value;
}

Rule:

Human names can be Unicode-rich. Machine codes should usually be restricted and canonicalized.

11. Charset: Boundary antara Text dan Bytes

String adalah text. byte[] adalah bytes. Untuk berpindah, butuh charset.

Anti-pattern:

byte[] bytes = text.getBytes();
String decoded = new String(bytes);

Ini memakai default charset environment. Jangan bergantung pada default untuk kontrak data.

Gunakan charset eksplisit:

byte[] bytes = text.getBytes(StandardCharsets.UTF_8);
String decoded = new String(bytes, StandardCharsets.UTF_8);

Rule:

Every text-byte boundary must name its charset explicitly.

12. StandardCharsets

Gunakan constants:

StandardCharsets.UTF_8
StandardCharsets.UTF_16
StandardCharsets.ISO_8859_1
StandardCharsets.US_ASCII

Untuk sistem modern, default kontrak eksternal biasanya sebaiknya UTF-8, kecuali protokol/legacy system menyatakan lain.

Contoh file write:

Files.writeString(path, content, StandardCharsets.UTF_8);

Contoh file read:

String content = Files.readString(path, StandardCharsets.UTF_8);

Contoh stream:

try (Reader reader = new InputStreamReader(inputStream, StandardCharsets.UTF_8)) {
    // read text
}

13. Encoding dan Decoding Error

Tidak semua byte sequence valid untuk charset tertentu.

Naive:

String text = new String(bytes, StandardCharsets.UTF_8);

Constructor ini dapat mengganti malformed input dengan replacement character, tergantung mekanisme decode. Untuk data yang harus strict, gunakan decoder dengan error action.

CharsetDecoder decoder = StandardCharsets.UTF_8.newDecoder()
        .onMalformedInput(CodingErrorAction.REPORT)
        .onUnmappableCharacter(CodingErrorAction.REPORT);

try {
    String text = decoder.decode(ByteBuffer.wrap(bytes)).toString();
} catch (CharacterCodingException e) {
    throw new IllegalArgumentException("Invalid UTF-8 payload", e);
}

Pilih policy:

BoundarySuggested Policy
Regulated audit inputReject/report invalid bytes
User free-text importReport with row/field error; jangan silent corruption
Logging fallbackReplacement bisa diterima jika original bytes tidak critical
Security tokenReject strict
Legacy migrationTrack replacement count dan source record

Rule:

Replacement is a data-loss policy. Use it only when data loss is acceptable and observable.

14. Byte Order Mark dan UTF-8

Beberapa file text memiliki BOM. UTF-8 tidak membutuhkan BOM, tetapi file dari beberapa tools bisa menyertakannya.

Gejala:

first header becomes "\uFEFFid" instead of "id"

Contoh handling sederhana:

static String removeUtf8BomIfPresent(String text) {
    if (!text.isEmpty() && text.charAt(0) == '\uFEFF') {
        return text.substring(1);
    }
    return text;
}

Jangan hapus U+FEFF secara global di seluruh text tanpa alasan. Treat sebagai boundary concern di awal file/payload.

15. HTTP, JSON, dan Charset Contract

Untuk HTTP text payload, kontrak harus eksplisit.

Contoh response header:

Content-Type: application/json; charset=utf-8

JSON modern secara praktik sangat erat dengan Unicode/UTF-8, tetapi boundary implementation tetap harus konsisten:

  • request body dibaca dengan charset yang benar.
  • response body di-encode dengan charset yang benar.
  • signature/hash dihitung atas bytes canonical yang disepakati, bukan atas String ambigu.

Anti-pattern:

String payload = requestBodyAsString();
byte[] signed = payload.getBytes(); // default charset

Perbaikan:

byte[] signed = payload.getBytes(StandardCharsets.UTF_8);

Atau lebih baik, sign original bytes jika protokol mensyaratkan byte-exact signature.

16. Database Boundary

Database text correctness bergantung pada:

  • column type.
  • database encoding/collation.
  • JDBC driver behavior.
  • normalization policy.
  • case sensitivity/collation.
  • index length limit.

Java side saja tidak cukup.

Contoh bug:

String a = "é";
String b = "e\u0301";

Java equals false. Database collation tertentu bisa menganggap sama atau berbeda. Index unique bisa behave berbeda dari service validation.

Rule:

Service equality, database equality, and search equality must be intentionally aligned.

Checklist DB:

  • Apakah DB memakai UTF-8 compatible encoding?
  • Apakah collation case-sensitive atau case-insensitive?
  • Apakah accent-sensitive atau accent-insensitive?
  • Apakah service melakukan normalization sebelum persist?
  • Apakah unique constraint mengikuti canonical form yang sama?
  • Apakah max length dihitung bytes, chars, code points, atau DB-specific units?

17. Length: Character Count, Code Point Count, Byte Count

String.length() bukan byte count.

String s = "é😀";

int codeUnits = s.length();
int codePoints = s.codePointCount(0, s.length());
int utf8Bytes = s.getBytes(StandardCharsets.UTF_8).length;

Ketiganya bisa berbeda.

RequirementUkur Dengan
Java internal indexUTF-16 code unit index
Unicode scalar countcodePointCount
Payload/network/storage limitencoded byte length
User-visible character limitgrapheme cluster aware logic
DB varchar limitsesuai DB semantics

Anti-pattern:

if (comment.length() <= 255) {
    save(comment);
}

Pertanyaan yang benar:

255 what? UTF-16 code units? code points? bytes? grapheme clusters? DB characters?

18. Designing Length Constraints

Untuk comment field:

record CaseComment(String value) {
    private static final int MAX_CODE_POINTS = 2_000;
    private static final int MAX_UTF8_BYTES = 8_000;

    CaseComment {
        Objects.requireNonNull(value, "value");
        value = Normalizer.normalize(value.strip(), Normalizer.Form.NFC);

        if (value.isBlank()) {
            throw new IllegalArgumentException("blank comment");
        }
        if (value.codePointCount(0, value.length()) > MAX_CODE_POINTS) {
            throw new IllegalArgumentException("comment too long");
        }
        if (value.getBytes(StandardCharsets.UTF_8).length > MAX_UTF8_BYTES) {
            throw new IllegalArgumentException("comment exceeds byte limit");
        }
    }
}

Ini lebih defensible daripada hanya value.length() <= 2000.

19. Filesystem Boundary

File name dan path punya Unicode/normalization/case behavior yang berbeda antar OS/filesystem.

Risiko:

  • file yang terlihat sama tetapi berbeda Unicode form.
  • case-insensitive filesystem vs case-sensitive server.
  • path traversal dengan karakter aneh.
  • log sulit dibaca karena control characters.

Guideline:

  • Jangan gunakan raw user text langsung sebagai file name.
  • Generate server-side safe name.
  • Simpan original display name sebagai metadata.
  • Normalize dan validate path segment.
  • Gunakan Path API, bukan string concatenation untuk path.

Contoh:

Path target = baseDir.resolve(safeGeneratedName).normalize();
if (!target.startsWith(baseDir)) {
    throw new SecurityException("Invalid path");
}

20. Control Characters

Text input bisa mengandung control characters:

  • newline.
  • carriage return.
  • tab.
  • escape.
  • null-like character.
  • bidirectional control characters.

Risiko:

  • log injection.
  • CSV injection.
  • terminal escape injection.
  • misleading source/display.
  • audit trail confusion.

Contoh log injection:

String user = "alice\nrole=admin";
log.info("user={}", user);

Log bisa terlihat seperti dua field berbeda.

Mitigation:

static String singleLineForLog(String raw) {
    return raw
            .replace("\r", "\\r")
            .replace("\n", "\\n")
            .replace("\t", "\\t");
}

Untuk high-security logs, gunakan structured logging encoder yang melakukan escaping otomatis.

21. CSV, XML, HTML, SQL: Encoding Bukan Escaping

Charset encoding menjawab:

How do we turn text into bytes?

Escaping menjawab:

How do we represent text safely inside another syntax?

Jangan campur.

Contoh HTML:

String html = "<p>" + userInput + "</p>"; // unsafe

Masalahnya bukan UTF-8. Masalahnya HTML escaping.

Contoh SQL:

String sql = "SELECT * FROM users WHERE name = '" + name + "'"; // unsafe

Masalahnya bukan charset. Masalahnya query construction dan parameter binding.

Rule:

Encoding protects byte representation. Escaping/binding protects syntax context.

22. Search dan Canonical Text

Search biasanya membutuhkan canonicalization berbeda dari storage.

Storage:

Preserve original user text as entered, possibly normalized minimally.

Search key:

Normalize + case fold + remove accents? Depends on product/domain.

Contoh:

record PersonName(String display, String searchKey) {
    static PersonName of(String raw) {
        String display = Normalizer.normalize(raw.strip(), Normalizer.Form.NFC);
        String searchKey = Normalizer.normalize(display, Normalizer.Form.NFD)
                .replaceAll("\\p{M}+", "")
                .toLowerCase(Locale.ROOT);
        return new PersonName(display, searchKey);
    }
}

Catatan: remove diacritics bisa salah untuk beberapa bahasa/domain. Gunakan hanya jika requirement jelas.

23. Regulatory/Audit Text Boundary

Untuk sistem regulasi, text sering menjadi evidence.

Prinsip:

  • Jangan silently mutate user statement.
  • Simpan raw input jika dibutuhkan forensik.
  • Simpan normalized/canonical form untuk indexing jika perlu.
  • Log transformation policy.
  • Pastikan rendering audit tidak mengubah makna.

Model:

record CapturedText(
        String raw,
        String normalized,
        Instant capturedAt,
        String source,
        String normalizationPolicyVersion
) { }

Dengan model ini, Anda bisa menjawab:

  • Apa yang dikirim user?
  • Apa yang dipakai sistem untuk search/matching?
  • Policy normalisasi versi berapa yang digunakan?
  • Apakah ada perubahan perilaku setelah policy berubah?

24. Hashing dan Signature Boundary

Hash/signature harus jelas menghitung apa:

  • raw bytes dari request?
  • decoded string lalu UTF-8 encoded lagi?
  • normalized canonical string?
  • JSON canonical form?

Contoh bug:

String body = decode(bytes, UTF_8);
String normalized = Normalizer.normalize(body, NFC);
byte[] signed = normalized.getBytes(UTF_8);

Ini tidak sama dengan menandatangani original bytes. Untuk signature protocol, gunakan exact byte sequence yang didefinisikan protokol.

Rule:

Sign bytes when protocol is byte-exact.
Sign canonical representation only when canonicalization is part of the protocol.

25. Designing a Text Boundary Object

Contoh value object untuk external text payload:

record Utf8Text(String value) {
    Utf8Text {
        Objects.requireNonNull(value, "value");
    }

    static Utf8Text decodeStrict(byte[] bytes) {
        Objects.requireNonNull(bytes, "bytes");
        CharsetDecoder decoder = StandardCharsets.UTF_8.newDecoder()
                .onMalformedInput(CodingErrorAction.REPORT)
                .onUnmappableCharacter(CodingErrorAction.REPORT);
        try {
            return new Utf8Text(decoder.decode(ByteBuffer.wrap(bytes)).toString());
        } catch (CharacterCodingException e) {
            throw new IllegalArgumentException("Invalid UTF-8", e);
        }
    }

    byte[] encode() {
        return value.getBytes(StandardCharsets.UTF_8);
    }
}

Ini membuat charset policy explicit dan testable.

26. Failure Mode Catalog

Failure ModeRoot CausePrevention
Emoji terpotongsubstring berbasis code unitTruncate code point/grapheme-aware
Wrong length validationlength() dianggap karakter/byteTentukan unit limit
MojibakeDecode dengan charset salahCharset eksplisit
Silent replacementInvalid bytes diganti diam-diamDecoder REPORT untuk boundary strict
Duplicate visible keyNormalization berbedaNormalize canonical key
Locale bugDefault locale untuk domain codeLocale.ROOT
Confusable spoofingUnicode unrestricted identifierWhitelist/script policy
Signature mismatchRe-encode string bukan original bytesSign agreed byte/canonical form
Log injectionControl characters di textStructured escaping
DB equality mismatchCollation berbeda dari serviceAlign service/DB/search equality

27. Review Checklist

Gunakan checklist ini saat code review:

  • Apakah semua getBytes() memakai charset eksplisit?
  • Apakah semua new String(bytes) memakai charset eksplisit?
  • Apakah invalid bytes harus reject, bukan replacement?
  • Apakah String.length() digunakan untuk limit yang benar?
  • Apakah substring/truncate aman untuk surrogate pair?
  • Apakah equality perlu normalization?
  • Apakah canonicalization memakai Locale.ROOT untuk machine key?
  • Apakah user-facing text memakai locale user?
  • Apakah machine identifier membatasi allowed character set?
  • Apakah DB collation/equality sejalan dengan service equality?
  • Apakah log/output melakukan escaping control characters?
  • Apakah hashing/signature menghitung representation yang benar?
  • Apakah raw text dan normalized text perlu disimpan terpisah untuk audit?

28. Latihan Deliberate Practice

Latihan 1 — Detect Length Confusion

Buat test untuk string berikut:

List<String> samples = List.of(
        "A",
        "é",
        "e\u0301",
        "😀",
        "👨‍👩‍👧‍👦"
);

Cetak:

  • length().
  • codePointCount.
  • UTF-8 byte length.

Jelaskan perbedaannya.

Latihan 2 — Strict UTF-8 Decoder

Implementasikan:

String decodeUtf8Strict(byte[] bytes)

Kontrak:

  • reject null.
  • reject malformed input.
  • tidak silently replace invalid bytes.

Latihan 3 — Canonical Domain Code

Implementasikan:

record DomainCode(String value) { }

Kontrak:

  • strip.
  • uppercase Locale.ROOT.
  • normalize NFC.
  • hanya allow [A-Z0-9_]+.
  • reject blank.

Latihan 4 — Audit Captured Text

Modelkan object:

CapturedText(raw, normalized, policyVersion, capturedAt)

Buat method factory yang tidak menghilangkan raw input.

29. Mini Case Study: Import CSV Regulasi

Scenario:

  • File CSV dikirim oleh lembaga eksternal.
  • Harus UTF-8 strict.
  • Header pertama kadang punya BOM.
  • Field reason_code harus ASCII uppercase.
  • Field comment boleh Unicode dan harus disimpan untuk audit.

Boundary design:

record ImportedRow(
        ReasonCode reasonCode,
        CapturedText comment
) { }

Decoder:

String fileText = decodeUtf8Strict(bytes);
fileText = removeUtf8BomIfPresent(fileText);

Reason code:

record ReasonCode(String value) {
    ReasonCode {
        Objects.requireNonNull(value, "value");
        value = Normalizer.normalize(value.strip(), Normalizer.Form.NFC)
                .toUpperCase(Locale.ROOT);
        if (!value.matches("[A-Z0-9_]+")) {
            throw new IllegalArgumentException("Invalid reason code");
        }
    }
}

Comment:

record CapturedText(String raw, String normalized, String policyVersion) {
    static CapturedText capture(String raw) {
        Objects.requireNonNull(raw, "raw");
        String normalized = Normalizer.normalize(raw, Normalizer.Form.NFC);
        return new CapturedText(raw, normalized, "unicode-nfc-v1");
    }
}

Keuntungan:

  • CSV bytes tidak silently rusak.
  • reason code aman sebagai machine identifier.
  • comment tetap mempertahankan raw evidence.
  • normalization policy auditable.

30. Kesimpulan

Java text correctness membutuhkan pemisahan mental model:

human text != Unicode code point != UTF-16 code unit != encoded byte

Engineer top-tier tidak hanya memakai String, tetapi bisa menjawab:

  • unit length apa yang sedang diukur?
  • representation apa yang sedang dibandingkan?
  • charset apa yang dipakai di boundary?
  • invalid bytes harus ditolak atau diganti?
  • normalization dilakukan kapan dan untuk field apa?
  • apakah identifier ini boleh Unicode penuh atau harus restricted?
  • apakah audit membutuhkan raw text dan normalized text terpisah?

Rule final:

Text is data with representation layers. Make every layer explicit at system boundaries.

31. Referensi Resmi

  • Java SE 25 API — String
  • Java SE 25 API — Character
  • Java SE 25 API — java.nio.charset
  • Java SE 25 API — StandardCharsets
  • Java SE 25 API — CharsetDecoder, CharsetEncoder, CodingErrorAction
  • Java SE 25 API — java.text.Normalizer
  • JLS Java SE 25 — Lexical Structure and Unicode input
  • Unicode Standard Annex #15 — Unicode Normalization Forms
Lesson Recap

You just completed lesson 22 in deepen practice. Use the series map if you want to review the broader track, or continue directly into the next lesson while the context is still warm.

Continue The Track

Keep the momentum while the lesson is still fresh. Move backward for review or continue forward into the next concept.