length() de cadenas y Unicode
- Estándares de codificación y codificación de caracteres
- Unicode y utf
- UTF-8, UTF-16, UTF-32
- Char y code unit
- Punto de código, code point
- Longitud de una cadena con emojis
- Codificaciones de caracteres
- Diferencia entre UTF-8 y UTF-16
- Compact strings en Java 9
- Cómo contar emojis
- Pares sustitutos en cadenas Java
- ASCII y Unicode en Java
- Trabajo con codificaciones de cadenas en Java
Estándares de codificación y codificación de caracteres, Unicode y UTF
Para empezar, es necesario saber distinguir entre estándares de codificación y formatos de codificación de caracteres.
El estándar de codificación asigna un número único (punto de código, a.k.a code point) a cada carácter, independientemente de la plataforma, programa o idioma, y define qué símbolo corresponde a qué número.
Qué caracteres existen (letras, dígitos, emojis, ideogramas, etc.) y qué número único (code point) corresponde a cada símbolo – lo define el estándar.
Uno de estos estándares es Unicode.
Codificaciones – son formas concretas de representar estos puntos de código en secuencias de bytes para almacenar en la memoria de la computadora o transferir por red.
Familia de codificaciones que implementan el estándar Unicode: UTF-8, UTF-16, UTF-32.
¿ASCII – estándar o codificación?
Pero también tenemos ASCII. ¿Qué es entonces?
De hecho, ASCII es 2 en 1: ambas cosas. ASCII significa American Standard Code for Information Interchange.
ASCII como estándar
Es una tabla de caracteres, como Unicode, pero mucho más pequeña: solo 128 caracteres.
Incluye:
- Letras inglesas (A–Z, a–z)
- Dígitos (0–9)
- Signos de puntuación y símbolos especiales (!, @, #, ~, etc.)
- Caracteres de control (por ejemplo, salto de línea \n, retorno de carro \r)
Cada carácter tiene un número del 0 al 127 – esto es el code point de ASCII.
ASCII como codificación
Esos mismos números (0–127) corresponden directamente a valores de un byte.
- Es decir, el símbolo A (code point 65) → byte 01000001
- Sin transformaciones adicionales – uno a uno.
Por eso ASCII – es un conjunto de caracteres y una codificación, porque define qué codificar y cómo codificar.
ASCII es de 7 bits. El último símbolo corresponde al byte 01111111.
El bit más significativo (Most Significant Bit, MSB) – es el bit más a la izquierda en un byte. No forma parte del estándar ASCII, pero aún así se transmite completamente.
En sistemas antiguos se usaba para la verificación de paridad, pero hoy en día siempre es 0.
Versiones nacionales de ASCII
Como ya vimos, el ASCII original es de 7 bits.
Existen dos tipos de modificaciones de ASCII:
- Extensiones de 8 bits de ASCII, usando el bit más significativo del ASCII original
- La idea es añadir el alfabeto local del país (Windows-1252, KOI8-R, y otros)
- Interpretaciones de 7 bits del ASCII original
- Aquí la idea es que algunos caracteres (por ejemplo, #, @, [, , ], {, }) puedan ser reemplazados por letras nacionales.
Las extensiones de 8 bits se denominan con su propio nombre: Windows-1252, KOI8-R, etc.
Para evitar confusión con interpretaciones nacionales de 7 bits del ASCII original utilizadas en otros países, se recomienda marcar el código original como US-ASCII.
Veamos un ejemplo en Java. En la clase StandardCharsets no hay una constante ASCII, pero existe US-ASCII:
String text = "!";
byte[] bytes = text.getBytes(StandardCharsets.US_ASCII);
System.out.println(bytes.length); // 1 byte
System.out.println(new String(bytes, StandardCharsets.US_ASCII));
Code unit, code point
Como vimos, el estándar de codificación define la relación carácter <-> número, y la codificación – la representación real de ese número en la memoria del ordenador.
Code point (punto de código) - es el valor numérico de un carácter en Unicode, independientemente de su representación. Por ejemplo, el símbolo “A” tiene code point U+0041 (65 en decimal).
Code unit (unidad de código) - es la unidad mínima de almacenamiento usada en una codificación concreta (por ejemplo, 8 bits en UTF-8, 16 bits en UTF-16).
Dependiendo de la codificación y de qué tan grande puede ser el número que codifica el símbolo, en la memoria del PC puede representarse de distintas formas:
- 4 bytes fijos para todos los caracteres.
- Aquí, code point = code unit = 4 bytes.
- Dos bytes para la mayoría de los caracteres, pero a veces con pares de 2 bytes en el caso de caracteres complejos.
- Aquí, el code point es de 2 o 4 bytes, y el code unit siempre es 2 bytes.
Ahora que distinguimos claramente entre code point y code unit, pasemos a las codificaciones.
UTF-8, UTF-16 y UTF-32
UTF-8
- Codificación más popular
- Número variable de bytes para cada símbolo
- O sea, code point = code unit = 1…4 bytes.
- Los símbolos en el rango ASCII (los primeros 128 símbolos de Unicode) ocupan 1 byte.
- Esto significa que UTF-8 es un superconjunto de ASCII, y todos los caracteres ASCII son válidos en UTF-8.
- Los símbolos más complejos ocupan 2, 3 o 4 bytes en UTF-8.
Ejemplo de símbolo de 1 byte (mayúscula latina A):
- Unicode code point: U+0041 (decimal 65)
- Representación UTF-8: 01000001 (1 code point / 1 byte / 1 code unit)
Ejemplo de símbolo de dos bytes (letra cirílica minúscula я):
- Unicode code point: U+044F (decimal 1103)
- Representación UTF-8: 11010001 10001111 (1 code point / 2 bytes / 2 code units)
Ejemplo de símbolo de tres bytes (símbolo de suma ∑):
- Unicode code point: U+2211 (decimal 8721)
- Representación UTF-8: 11100010 10001000 10010001 (1 code point / 3 bytes / 3 code units)
Ejemplo de símbolo de cuatro bytes (letra gótica 𐍈):
- Unicode code point: U+10348 (decimal 66376)
- Representación UTF-8: 11110000 10010000 10001101 10001000 (1 code point / 4 bytes / 4 code units)
Note la estructura de bytes en UTF-8:
Símbolos de 1 byte: 0xxxxxxx
Símbolos de 2 bytes: 110xxxxx 10xxxxxx
Símbolos de 3 bytes: 1110xxxx 10xxxxxx 10xxxxxx
Símbolos de 4 bytes: 11110xxx 10xxxxxx 10xxxxxx 10xxxxxx
Ventaja: compatibilidad con ASCII, compacidad para la mayoría de los textos (por ejemplo, un texto en inglés ocupará el mismo espacio que en ASCII).
UTF-16
- Utiliza code units de 16 bits (2 bytes)
- La mayoría de los símbolos más usados (Basic Multilingual Plane) se codifican en un code unit (2 bytes)
- Los símbolos fuera de BMP (ejemplo: emojis, caracteres raros) se codifican mediante un par sustituto – dos code units (4 bytes)
Así, en UTF-16 un code point puede codificarse por uno o dos code units. Pero el code unit en UTF-16 siempre es = 2 bytes.
UTF-16 no es directamente compatible con ASCII. Aunque el byte bajo de UTF-16 para todos los símbolos ASCII corresponde a la codificación ASCII, UTF-16 requiere la presencia del byte alto.
Si en ASCII, el code point (y por tanto el code unit) para la letra ‘A’ será 01000001, en UTF-16 será 00000000 01000001.
UTF-32
- Utiliza code units de 32 bits (4 bytes)
- Cada símbolo Unicode ocupa exactamente 4 bytes
- Aquí siempre 1 code point = 1 code unit = 4 bytes
- Ventaja: simplicidad y previsibilidad al procesar
Desventaja: ocupa entre 2 y 4 veces más que UTF-8 o UTF-16 para la mayoría de los textos.
¿Qué ocurre con char en Java?
Java internamente usa UTF-16, donde el code unit siempre es de dos bytes, y el code point = a uno o dos code units (par sustituto).
Char siempre almacena un code unit
El tipo char (Character) en Java en realidad almacena un code unit, no un code point.
“Por feliz coincidencia”, para la mayoría de los símbolos esto es suficiente para conocer el símbolo, PERO NO SIEMPRE. Detengámonos aquí un poco.
En Java existen métodos para determinar si los dos bytes actuales son un code unit inicial (leading o high) en un par sustituto:
Character.isHighSurrogate();
O un code unit final (following o low) en un par sustituto:
Character.isLowSurrogate();
Char y emojis
Por ejemplo, un emoji en UTF-16 se representa por un par sustituto de code units. Como ya vimos que char = 1 code unit, ¡eso significa que en un char no cabe un emoji!
char ch = '😎'; // ERROR: Too many characters in character literal
Generar un par sustituto de code units
¿Cómo obtener el high surrogate y low surrogate para un emoji? Extraer el array char[] del String:
String str = "😎";
char[] arr = str.toCharArray();
char high = arr[0];
char low = arr[1];
Podemos comprobarlo así:
Character.isHighSurrogate(high); // true
Character.isLowSurrogate(low); // true
Si intentamos imprimir por pantalla high o low por separado, no veremos el resultado esperado.
Montar el símbolo (code unit) desde el par sustituto
¿Cómo “reunir” el emoji? Utilice el constructor de String:
System.out.println(new String(arr));
En las manipulaciones anteriores Java usa UTF-16 de forma implícita, ya que esa es la codificación usada de fondo en Java.
Note que al convertir un byte[] en String se puede especificar la codificación:
new String(byteArr, StandardCharsets.UTF_8);
Pero al convertir char[] en String – esto no es posible, ya que se usa UTF-16:
new String(charArr);
Nota sobre codificaciones en Java / JVM
Para entenderlo plenamente, es necesario distinguir entre la codificación por defecto (la usada en operaciones IO, que puede ser configurada) y la codificación interna (siempre UTF-16, usada por la JVM para manejar strings).
Codificación por defecto:
- Se usa para operaciones de entrada/salida, como leer de archivos o escribir en flujos.
- Depende de la configuración local del sistema operativo, o puede ser definida mediante parámetros JVM (por ejemplo,
-Dfile.encoding=UTF-8). - Puede variar según entorno y configuración.
Codificación interna:
- Se usa para representar objetos String durante la ejecución en Java.
- Siempre UTF-16, independientemente de la configuración del sistema o JVM.
- Es uniforme en cualquier entorno Java, lo que asegura un procesamiento consistente de los datos de String.
Conversión de símbolo en byte[] y BOM (Byte Order Mark)
String <-> char[] siempre es UTF-16, pero para String <-> byte[] la codificación por defecto es ¡UTF-8!
// sin especificar codificación
byte[] bytes = "abc123😎".getBytes();
// especificando UTF-8, todo funciona
System.out.println(new String(bytes, StandardCharsets.UTF_8));
Se puede convertir una cadena (símbolo) a bytes en UTF-16. Pero primero es importante notar que la clase StandardCharsets tiene tres constantes para UTF-16:
StandardCharsets.UTF_16StandardCharsets.UTF_16BEStandardCharsets.UTF_16LE
Si convertimos un carácter en array de bytes:
byte[] bytes = "b".getBytes(StandardCharsets.UTF_16);
el tamaño esperado del array byte[] es 2 bytes, porque el code unit en UTF-16 es 2 bytes.
Pero si comprobamos esto:
byte[] bytes = "b".getBytes(StandardCharsets.UTF_16);
System.out.println(bytes.length); // salida: 4
Será igual a cuatro. ¿Por qué?
Al usar StandardCharsets.UTF_16, el array de bytes resultante contendrá una marca de orden de bytes (BOM) al principio, que normalmente son 2 bytes (o bien FE FF, o FF FE según el orden de los bytes).
Por tanto, para un símbolo realmente se obtienen 4 bytes:
- 2 bytes para la especificación (BOM)
- 2 bytes para el carácter
Pero usando StandardCharsets.UTF_16BE (big endian) o StandardCharsets.UTF_16LE (little endian), el tamaño esperado del array resultante será 2 bytes:
// char (2 bytes) a 2 bytes en UTF-16BE <- big endian, el alto va primero
// usando String
byte[] bytes = new String(new char[] {ch}).getBytes(StandardCharsets.UTF_16BE);
// char (2 bytes) a 2 bytes en UTF-16LE <- little endian, el bajo va primero
// usando String
byte[] bytes = new String(new char[] {ch}).getBytes(StandardCharsets.UTF_16LE);
Pero eso no es todo
¿Cómo calcular el número de caracteres en una cadena? ¿string.length()?
Las cadenas en Java son arrays de char[]. Y string.length() en realidad – es la longitud del array char[] interno.
Por eso la longitud de esta cadena será igual a 2:
System.out.println("😎".length()); // salida: 2
Si leemos el javadoc oficial de string.length():
The length is equal to the number of Unicode code units in the string.
Todo encaja. Pero entonces ¿cómo calcular la longitud real de la cadena?
Como cada símbolo siempre se codifica con un número (code point), para contar el número de símbolos en una cadena, basta con contar los code points.
Por tanto, la respuesta correcta a la pregunta es:
String str = "😎";
System.out.println(str.codePoints().count());
Java 9+ Compact Strings
Antes de Java 9, todos los objetos String en Java guardaban sus símbolos en formato UTF-16, usando dos bytes por carácter sin tener en cuenta el contenido real.
En Java 9 apareció la optimización Compact Strings, que selecciona dinámicamente entre las codificaciones Latin-1 (un byte) y UTF-16 (dos bytes) en función del contenido de la cadena.
- La JVM analiza todos los caracteres de la cadena.
- Se pregunta: “¿Es posible representar cada carácter usando solo Latin-1?” (caracteres con code point 0-255)
- Si SÍ → Se usa la codificación Latin-1 (1 byte por carácter)
- Si NO → Se usa UTF-16 (2 bytes por carácter)
Si la cadena contiene emojis, entonces Java 9+ usará UTF-16 (code unit = 2 bytes) en vez de Latin-1 (code unit = 1 byte).
Bonus – conversión de char a un solo byte
Ya sabemos que:
- Java usa UTF-16 internamente para las cadenas
- Char almacena un code unit
- Code unit para UTF-16 siempre son 2 bytes
- Por tanto, el tamaño del char es 2 bytes
- Para algunos símbolos se necesitan dos char, no uno
- Los code points de los símbolos ASCII (letras inglesas, dígitos y símbolos especiales básicos) coinciden con los code point de Unicode
Esto significa que los símbolos básicos (ASCII) como letras (por ejemplo, b) y dígitos (por ejemplo, 3), aunque se representen en UTF-16, caben en un byte:
byte b = (byte) 'a';
Así, la representación UTF-16 del símbolo b:
0000 0000 0110 0010
Cabe en una variable de tipo byte gracias al recorte del byte alto:
0110 0010
Bonus code unit / code point / char
int -> char
// int (code unit) -> char
char ch = (char) 97;
// int (code point) -> char[]
int codePoint = 0x1F60A; // 😊
char[] chars = Character.toChars(codePoint);