Ratgeber · Encoding & Engine

UTF-8 vs UTF-16 vs UTF-32: Der ehrliche Vergleich der drei Unicode-Encodings

Unicode kann auf drei Arten codiert werden: UTF-8, UTF-16 und UTF-32. Jede hat ihre Stärken und Schwächen. Wir gehen die wichtigsten Unterschiede durch, damit du weisst, welches Encoding in welcher Situation Sinn ergibt.

7 Min Lesezeit 1.633 Wörter 5 FAQs
Jan-Tristan Rudat
Jan-Tristan RudatRedakteur · Computing-Historiker
Geprüft am

Unicode ist der Zeichensatz, der praktisch alle Schriften der Welt abbildet. Aber Unicode allein sagt noch nichts darüber, wie diese Codepoints im Speicher oder in einer Datei abgelegt werden. Dafür gibt es drei verschiedene Encodings: UTF-8, UTF-16 und UTF-32. Jede hat unterschiedliche Eigenschaften, unterschiedliche Stärken und unterschiedliche Anwendungsfelder. Wir vergleichen die drei systematisch.

UTF-8: Byte-Strom mit variabler Länge

UTF-8 codiert jeden Unicode-Codepoint mit 1 bis 4 Bytes. Die Aufteilung folgt einem cleveren Bit-Muster, das den Byte-Typ aus den führenden Bits ableitbar macht. Codepoints 0 bis 127 (der ASCII-Bereich) bekommen genau 1 Byte. Codepoints 128 bis 2047 bekommen 2 Bytes. Codepoints 2048 bis 65535 bekommen 3 Bytes. Codepoints 65536 bis 1114111 bekommen 4 Bytes.

Die wichtigste Eigenschaft: ASCII-Byte-Kompatibilität. Eine reine ASCII-Datei ist gleichzeitig eine valide UTF-8-Datei mit identischen Bytes. Das ist der historische Erfolgsgrund von UTF-8: alle Legacy-Tools, die nur ASCII verstehen, können UTF-8-ASCII-Inhalte ohne Modifikation verarbeiten. Wer mit nicht-ASCII-Zeichen arbeitet, braucht aber explizite UTF-8-Awareness im Code, sonst entstehen Mojibake-Effekte.

Eine zweite wichtige Eigenschaft: Selbst-Synchronisation. Wenn ein Decoder mitten in einer UTF-8-Sequenz aufsetzt (etwa nach einem Byte-Verlust), kann er anhand der Bit-Muster der nächsten Bytes erkennen, wo das nächste Start-Byte beginnt. Innerhalb von maximal vier Bytes hat er sich wieder synchronisiert. Bei UTF-16 wäre das deutlich aufwändiger.

Keine Endianness. UTF-8 ist ein reiner Byte-Strom, die Reihenfolge der Bytes ist eindeutig durch das Encoding-Schema festgelegt. Es gibt kein UTF-8 LE oder BE. Das ist ein weiterer Grund, warum UTF-8 in der Praxis so robust ist: Es gibt keine versteckten Plattform-Inkompatibilitäten.

UTF-16: 16-Bit-Code-Units mit Surrogate-Erweiterung

UTF-16 codiert jeden Unicode-Codepoint mit 1 oder 2 16-Bit-Code-Units. Codepoints in der Basic Multilingual Plane (BMP, also U+0000 bis U+FFFF) bekommen genau eine Code-Unit, also 2 Bytes. Codepoints jenseits der BMP (U+10000 bis U+10FFFF) bekommen ein Surrogate-Pair, also zwei 16-Bit-Code-Units, also 4 Bytes.

UTF-16 hat ein Endianness-Problem, das UTF-8 nicht hat. Ein 16-Bit-Wert kann als Little-Endian (LE, niederwertiges Byte zuerst) oder Big-Endian (BE, höchstwertiges Byte zuerst) im Speicher liegen. Windows nutzt UTF-16 LE als Standard, Java und viele Unix-Tools nutzen UTF-16 BE. Beim Datenaustausch muss die Endianness explizit kommuniziert werden, üblicherweise über eine BOM (Byte Order Mark) am Datei-Anfang: FE FF für BE, FF FE für LE.

Windows hat UTF-16 seit Windows NT (1993) tief in seine APIs eingebaut. Alle Win32-APIs mit dem W-Suffix (CreateFileW, MessageBoxW etc.) erwarten und liefern UTF-16-LE-codierte Strings. .NET-Strings, SQL-Server-NVARCHAR-Felder, Windows-Registry-String-Werte: alles UTF-16. Wer mit Windows-Software interagiert, kommt um UTF-16 nicht herum.

Java hat eine ähnliche Erblast: Die String-Klasse speichert intern UTF-16-Code-Units. Das ist heute teils problematisch (string.length() für einen Smiley gibt 2 zurück), wird aber aus Kompatibilitätsgründen nicht geändert.

Zeichen UTF-8 UTF-16 UTF-32
<rect class="box" x="40" y="45" width="120" height="40" rx="6"/>
<text class="label" x="100" y="70">A (U+0041)</text>
<rect class="box" x="240" y="45" width="120" height="40" rx="6"/>
<text class="small" x="300" y="70">1 Byte: 41</text>
<rect class="box" x="440" y="45" width="120" height="40" rx="6"/>
<text class="small" x="500" y="70">2 Bytes: 00 41</text>
<rect class="box" x="640" y="45" width="120" height="40" rx="6"/>
<text class="small" x="700" y="70">4 Bytes: 00 00 00 41</text>

<rect class="box" x="40" y="100" width="120" height="40" rx="6"/>
<text class="label" x="100" y="125">ä (U+00E4)</text>
<rect class="boxA" x="240" y="100" width="120" height="40" rx="6"/>
<text class="small" x="300" y="125">2 Bytes: C3 A4</text>
<rect class="box" x="440" y="100" width="120" height="40" rx="6"/>
<text class="small" x="500" y="125">2 Bytes: 00 E4</text>
<rect class="box" x="640" y="100" width="120" height="40" rx="6"/>
<text class="small" x="700" y="125">4 Bytes: 00 00 00 E4</text>

<rect class="box" x="40" y="155" width="120" height="40" rx="6"/>
<text class="label" x="100" y="180">漢 (U+6F22)</text>
<rect class="boxA" x="240" y="155" width="120" height="40" rx="6"/>
<text class="small" x="300" y="180">3 Bytes: E6 BC A2</text>
<rect class="box" x="440" y="155" width="120" height="40" rx="6"/>
<text class="small" x="500" y="180">2 Bytes: 6F 22</text>
<rect class="box" x="640" y="155" width="120" height="40" rx="6"/>
<text class="small" x="700" y="180">4 Bytes: 00 00 6F 22</text>

<rect class="box" x="40" y="210" width="120" height="40" rx="6"/>
<text class="label" x="100" y="235">Smiley (U+1F600)</text>
<rect class="boxA" x="240" y="210" width="120" height="40" rx="6"/>
<text class="small" x="300" y="235">4 Bytes: F0 9F 98 80</text>
<rect class="boxA" x="440" y="210" width="120" height="40" rx="6"/>
<text class="small" x="500" y="235">4 Bytes: D8 3D DE 00</text>
<rect class="box" x="640" y="210" width="120" height="40" rx="6"/>
<text class="small" x="700" y="235">4 Bytes: 00 01 F6 00</text>
Wie die drei Encodings vier verschiedene Codepoint-Kategorien speichern.

UTF-32: Feste Breite, vier Bytes pro Codepoint

UTF-32 ist die einfachste Variante: Jeder Codepoint bekommt genau 4 Bytes, also 32 Bit. Das ist deutlich mehr als nötig (Unicode hat nur 21 Bit-Werte, die ersten 11 Bit sind also immer Null), aber dafür gibt es zwei Vorteile.

Erstens: Konstante Indizierung. Der n-te Codepoint sitzt an Position n*4 im Byte-Stream. Das macht Random Access trivial. Bei UTF-8 oder UTF-16 muss man den Stream sequenziell scannen, um die Codepoint-Grenzen zu finden.

Zweitens: Keine Surrogate-Komplikationen. UTF-32 deckt den gesamten Unicode-Codepoint-Raum mit einer einzigen Code-Unit ab. Es gibt keine Pairs, keine speziellen Bereiche, keine Bit-Magic. Decoder sind trivial.

Der Preis: 4 Bytes pro Codepoint, auch für ASCII. Eine englische Plaintext-Datei wird in UTF-32 viermal so gross wie in UTF-8. Für Speicher- oder Bandbreiten-sensitive Anwendungen ist das ein No-Go.

In der Praxis trifft man UTF-32 selten. CPython mit PEP 393 (seit Python 3.3) nutzt intern eine Mischung aus 1-Byte-, 2-Byte- oder 4-Byte-Repräsentation pro String, je nach maximalem Codepoint. Für Strings mit Codepoints jenseits der BMP wird UTF-32 verwendet. Linux-wchar_t ist auf den meisten Systemen 32 Bit und entspricht damit UTF-32. Als Datei-Format oder Netzwerk-Encoding wird UTF-32 fast nie eingesetzt.

Entscheidungsmatrix für die Praxis

Wann nimmst du welches Encoding? Hier eine pragmatische Tabelle:

  • Web-Server, REST-API, JSON-Payload: UTF-8 (kein anderes Encoding akzeptiert)
  • Linux-Dateien, Programmiersprachen-Quellcode: UTF-8 (System-Default)
  • macOS-Dateien, iOS-Apps: UTF-8 (Apple-Default seit Mac OS X)
  • Windows-API-Calls (Win32 W-Suffix): UTF-16 LE (Zwangs-Encoding)
  • .NET-Strings, C# / VB.NET / F#: UTF-16 (intern, transparent für den Developer)
  • Java-Strings: UTF-16 (intern, transparent)
  • SQL-Server-NVARCHAR-Felder: UTF-16
  • PostgreSQL, MySQL utf8mb4: UTF-8
  • Internal Python-String-Storage (CPython): variable Mischung, bis zu UTF-32
  • Datenaustausch zwischen Systemen: UTF-8 als sicherste Wahl

Die Faustregel: Wenn du nicht explizit weisst, was dein Ziel-System will, nimm UTF-8. Es funktioniert mit der grössten Wahrscheinlichkeit ohne Verlust.

Konvertierung zwischen den drei Encodings

Alle drei Encodings codieren den gleichen Unicode-Codepoint-Raum, also ist die Konvertierung verlustfrei möglich. In JavaScript ist das mit TextDecoder einfach:

// UTF-8 Bytes nach String
const text = new TextDecoder('utf-8').decode(utf8Bytes);
// String nach UTF-8 Bytes
const bytes = new TextEncoder().encode(text);

Für UTF-16 gibt es in JavaScript keinen direkten TextEncoder. Wer UTF-16-Bytes braucht, muss eine eigene Konvertierung schreiben oder TextDecoder mit dem entsprechenden Charset-Argument zum Decodieren nutzen:

const text = new TextDecoder('utf-16le').decode(utf16leBytes);

In Node.js gibt es das Buffer-Modul, das mit .toString('utf-16le') oder .toString('utf-32le') arbeiten kann. Für ältere Java-Anwendungen ist String-Konvertierung via String.getBytes("UTF-8") oder String.getBytes("UTF-16") der Standardweg.

binaerkonverter.de macht alle Konvertierungen intern in UTF-8, weil das die Browser-Web-API ist. Wer wirklich UTF-16-codierte Bytes braucht, sollte das Tool als Zwischenschritt nutzen (Text in UTF-8 codieren, dann mit einer Library wie iconv-lite in UTF-16 umrechnen).

Geschichtlicher Kontext

Die drei Encodings sind nicht gleichzeitig entstanden. Die historische Reihenfolge erklärt einige der Eigenheiten. UCS-2 (der Vorläufer von UTF-16) wurde 1991 mit Unicode 1.0 als das ursprünglich gedachte Encoding eingeführt. Idee war: Alle Codepoints passen in 16 Bit, also feste Breite, einfaches Indexing. Diese Annahme stellte sich 1996 als zu optimistisch heraus, woraufhin UTF-16 mit Surrogate-Pairs nachgerüstet wurde.

UTF-8 wurde 1992 von Ken Thompson und Rob Pike entworfen, also nur ein Jahr nach Unicode 1.0. Beide Männer arbeiteten an Plan 9, dem Bell-Labs-Betriebssystem, und brauchten ein Encoding, das ASCII-kompatibel war. Die Genialität: UTF-8 kann von Anfang an beliebige Unicode-Codepoints abdecken, nicht nur die BMP. Damit war UTF-8 schon 1992 zukunftssicher, während UTF-16 erst Jahre später mit Surrogates nachziehen musste.

UTF-32 ist die jüngste der drei und eher eine Reaktion auf die Komplexität von UTF-16. Spezifiziert in RFC 3629 (2003) für den Fall, dass Festbreite-Encoding gewünscht wird. In der Praxis war UTF-32 nie besonders verbreitet, weil der Speicher-Overhead zu hoch ist.

Häufige Encoding-Fallstricke

Aus der Praxis ein paar Stolpersteine, die mir immer wieder begegnen.

Erstens: Mojibake. Wenn ein UTF-8-codierter Text mit einer ISO-8859-1-Annahme dekodiert wird, sieht ein Umlaut wie ä plötzlich als ä aus. Das ist klassisches Mojibake. Die Lösung: Encoding-Annotation explizit prüfen, im HTTP-Header (Content-Type), im HTML-Meta-Tag (charset), in der Datei selbst (BOM). Wenn drei verschiedene Stellen unterschiedliche Encodings sagen, gewinnt typischerweise der HTTP-Header.

Zweitens: BOM-Konflikte. Manche Tools setzen einen UTF-8-BOM (EF BB BF) an den Datei-Anfang, was zwar erlaubt aber nicht empfohlen ist. Manche Parser haben mit dem BOM Probleme, etwa wenn er als erstes Zeichen interpretiert wird statt als Codierungs-Hinweis. PHP hatte über Jahre Probleme mit BOM in Include-Files, was zu mysteriösen Header-Already-Sent-Fehlern führte.

Drittens: Surrogate-Probleme in JavaScript. Ein einzelner Emoji-String hat in JavaScript einen length-Wert von 2, weil intern UTF-16 verwendet wird. Wer einen Twitter-ähnlichen Charakter-Counter implementiert und naiv string.length nutzt, bekommt falsche Werte. Die korrekte Methode ist […string].length, die den Spread-Operator nutzt, der Codepoints einzeln iteriert.

Viertens: Datenbank-Migrationen. Eine MySQL-Datenbank mit utf8-Encoding (3 Bytes pro Zeichen) kann keine Emoji speichern, weil die ausserhalb der BMP liegen. Die Migration zu utf8mb4 (4 Bytes pro Zeichen) ist die Lösung, war aber für viele Anwendungen ein schmerzhafter Schritt mit Index-Grössen-Problemen.

Was hängenbleibt

UTF-8 ist der Web-Standard und für die meisten Anwendungen die richtige Wahl, dank ASCII-Kompatibilität und fehlender Endianness-Probleme. UTF-16 ist Windows- und Java-intern und in einigen .NET- und Java-Kontexten unvermeidlich. UTF-32 ist eine Festbreite-Alternative mit konstanter Indizierung, aber zu speicher-ineffizient für Praxis-Einsätze ausser internem String-Storage. Alle drei codieren den gleichen Unicode-Codepoint-Raum, also ist die Konvertierung verlustfrei. binaerkonverter.de arbeitet intern mit UTF-8 als Default-Encoding.

FAQ

Häufige Fragen

Welches Encoding ist das effizienteste?

Das hängt vom Inhalt ab. Für englische und westeuropäische Sprachen ist UTF-8 mit Abstand am effizientesten, weil ein Byte pro Zeichen reicht. Für asiatische Schriften (Chinesisch, Japanisch, Koreanisch) ist UTF-16 mit zwei Bytes pro CJK-Zeichen etwas effizienter als UTF-8 (das drei Bytes braucht). UTF-32 ist immer am ineffizientesten beim Speicher-Verbrauch (vier Bytes pro Zeichen, egal ob ASCII oder Emoji), aber dafür hat es konstante Byte-zu-Codepoint-Indizierung. Für ein zufälliges deutsches Web-Dokument ist UTF-8 etwa 2 bis 5 Prozent grösser als ISO-8859-1, aber 25 bis 35 Prozent kleiner als UTF-16. Für ein typisches HTML-Dokument macht das deutlich Unterschiede in der Bandbreite.

Was ist der Unterschied zwischen UTF-16 LE und BE?

LE steht für Little-Endian, BE für Big-Endian. Bei Multi-Byte-Werten ist die Frage, in welcher Reihenfolge die einzelnen Bytes im Speicher liegen. Little-Endian bedeutet: das niederwertige Byte zuerst. Big-Endian: das höchstwertige Byte zuerst. Beispiel: Der Codepoint U+0041 (lateinisches A) ist in UTF-16 der 16-Bit-Wert 0041 hex. In UTF-16 LE wird das als 41 00 im Speicher abgelegt, in UTF-16 BE als 00 41. Windows nutzt UTF-16 LE als Standard, Java und viele Unix-Tools nutzen UTF-16 BE. Wer Daten zwischen verschiedenen Systemen austauscht, muss die Endianness explizit kennen oder am Datei-Anfang eine BOM (FE FF für BE, FF FE für LE) setzen. UTF-8 hat dieses Problem nicht, weil es ein Byte-Strom ist.

Wann ist UTF-32 wirklich sinnvoll?

Selten, aber es gibt Use-Cases. UTF-32 codiert jeden Codepoint mit genau vier Bytes, also fester Breite. Das bedeutet: Der n-te Codepoint sitzt an Position n*4 im Byte-Stream. Das ist nützlich für Anwendungen, die viel Random Access in Text brauchen (etwa Text-Editoren, die auf bestimmte Spalten springen müssen, oder Linguistische Analyse-Tools). In der Praxis sind solche Anwendungen aber selten, weil die meisten Operationen sequenziell sind und UTF-8 mit einer einfachen Indizierungs-Tabelle ähnliche Effizienz erreicht. Internal-String-Repräsentation in einigen Python-Versionen (CPython mit PEP 393) nutzt UTF-32 für Strings mit Codepoints jenseits der BMP, sonst kleinere Bit-Tiefen. Im Datenaustausch oder als Datei-Encoding ist UTF-32 fast nie zu sehen.

Wie geht UTF-16 mit Emoji um?

Über Surrogate-Pairs. Emoji wie das grinsende Gesicht (U+1F600) liegen ausserhalb der Basic Multilingual Plane (BMP), die nur die ersten 65.536 Codepoints umfasst. Für solche Codepoints nutzt UTF-16 zwei 16-Bit-Werte: ein High-Surrogate in U+D800 bis U+DBFF und ein Low-Surrogate in U+DC00 bis U+DFFF. Der Smiley wird zu D83D DE00, also vier Bytes insgesamt. Das ist genauso viel wie UTF-8 für den gleichen Codepoint braucht. Praktisch hat das zwei Konsequenzen: Erstens, .length()-Aufrufe in Java oder JavaScript zählen die Anzahl der UTF-16-Code-Units, nicht die Anzahl der visuellen Zeichen. Ein einzelner Smiley hat in JavaScript einen string.length von 2. Zweitens, Slicing-Operationen müssen Surrogate-Grenzen respektieren, sonst entstehen ungültige UTF-16-Sequenzen.

Warum ist UTF-8 ASCII-kompatibel und UTF-16 nicht?

UTF-8 wurde explizit ASCII-kompatibel entworfen. Die ersten 128 Codepoints (also der gesamte ASCII-Bereich) werden in UTF-8 mit genau einem Byte codiert, byte-identisch zur ASCII-Codierung. Das bedeutet: Eine UTF-8-Datei mit nur ASCII-Inhalt ist gleichzeitig eine valide ASCII-Datei. Alte Tools, die nur ASCII verstehen, können UTF-8-ASCII-Inhalte ohne Modifikation verarbeiten. UTF-16 hat diese Eigenschaft nicht. Das ASCII-A (Codepoint 65) ist in UTF-16 LE die Byte-Sequenz 41 00, in UTF-16 BE 00 41. Das ist zwei Bytes statt einem, und das führende oder folgende Null-Byte würde ein ASCII-Tool als String-Ende interpretieren (NULL-terminierte Strings). Diese fehlende Kompatibilität ist der Hauptgrund, warum UTF-16 im Web keine Chance hatte und nur in Microsoft- und Java-Welten lebt.

Anzeige

Quellen

Worauf dieser Ratgeber sich stützt

Verwandte Ratgeber

Weiterlesen

Veröffentlicht · zuletzt geprüft
Verantwortlich: Jan-Tristan Rudat
Anzeige
Anzeige
Anzeige
Anzeige