Entendendo Unicode e os Character Encodings
Por Paulo Silveira em 22/10/06Todo mundo já passou por problemas com character encodings. Quem nunca abriu uma conexão JDBC com o MySQL e puxou do banco um monte de caracteres onde em vez de acentos só se viam pontos de interrogação e caracteres estranhos?
O blog do Joel Spolsky já publicou um post sobre esse assunto, que é bem simples e direto. O fato importante é mostar que Unicode não é um encoding. Unicode define codepoints (um número) para cada letra (ou símbolo). Por exemplo, a letra ´A´ é o codepoint 65. A partir do Unicode 3.1, o codepoint pode até mesmo ser maior que 2^16: o fatídico 65536 (atualmente vai até 16*65536, ou seja 0×10FFFF). Sim! Unicode nada mais é que um tabelão! Nas palavras do wikipedia, “… (Unicode) assign a unique number to each character used in the written languages of the world“, traduzindo, Unicode associa um número único para cada caractere usado nas línguas escritas de todo o mundo.
Pois é, unicode não é uma maneira de se representar caracteres com 2 bytes. Aliás, Unicode não é um encoding. A pergunta “Você está usando unicode ou latin1?” está completamente errada. Quem é responsável por codificar um codepoint em bytes é o encoding. Aqui estamos falando de UTF-8, o ISO-8859-1 (vulgo latin1), entre outros. Alguns encodings podem não suportar todos os codepoints possíveis, outros podem tentar economizar alguns bytes quando codificar alguns caracteres (caso do UTF-8).
Mas não quero ficar na teoria, quero passar para o código. Vamos então codificar o ‘ç‘ em diferentes encodings: ISO-8859-1, UTF-8 e UTF-16. Basta colocar o seguinte código no main:
String[] codes = { "ISO-8859-1", "UTF-8", "UTF-16" };
String palavra = "ç";
for (String encoding : codes) {
byte[] b = palavra.getBytes(encoding);
System.out.printf("%10s\t%d\t", encoding, b.length);
for (int k = 0; k < b.length; k++) {
String hex = Integer.toHexString((b[k] + 256) % 256);
if (hex.length() == 1)
hex = "0" + hex;
System.out.print(hex);
}
System.out.println();
}
E ao rodar, teremos em cada linha o encoding, a quantidade de bytes utilizada para codificar o ‘ç‘ e sua representação em hexadecimal codificada.
ISO-8859-1 1 e7
UTF-8 2 c3a7
UTF-16 4 feff00e7
Vamos tentar o mesmo para a letra ‘a‘:
ISO-8859-1 1 61
UTF-8 1 61
UTF-16 4 feff0061
É interessante reparar aqui que o UTF-8 gastou 2 bytes em um caso, e 1 byte no outro. Outro fato importante é que UTF-8 codifica diversos caracteres da mesma forma que o ISO-8859-1 (e este por sua vez tem uma estrita relação com o US-ASCII). Você pode incrementar esse código e testar com outros encodings, tais como US-ASCII, Cp1252 e UTF-16. Você pode ver quais encodings a sua JVM suporta com Charset.availableCharsets().
Como falei anteriormente, existem caracteres que extrapolam o índice do 65535. Então como ficam esses caracteres no java, já que o char tem apenas 2 bytes? É aí que entram os surrogate pairs: alguns caracteres agora são utilizados para indicar que o restante do caractere ainda está por vir! Alguns problemas surgem com isso: o método length() da String não funciona mais tão bem: ele apenas diz quantos chars aquela String possui.
Para resolver esses problemas novos métodos e classes foram adicionados ao java 5 (através da JSR-204), como o codePointCount, na String. Esse artigo da Sun discute bem esse assunto.
Mas quando ocorrem os problemas de encoding que citamos no começo do post? Um caso em potencial é quando tentamos ler uma sequência de bytes usando um encoding que não foi o que utilizamos para codificar aquela String. Vamos simular isso escrevendo o ‘ç’ em UTF-8 e lendo como ISO-8859-1, e vice-versa:
// ç escrito em UTF-8 mas lido em ISO-8859-1
System.out.println(new String("ç".getBytes("UTF-8"), "ISO-8859-1"));
// ç escrito em ISO-8859-1 mas lido em UTF-8
System.out.println(new String("ç".getBytes("ISO-8859-1"), "UTF-8"));
E o resultado:
ç
?
Cadê o c cedilha? Você pode não ver, mas ele está por aí! Esses caracteres lhe trazem algumas lembranças?
Outro artigo interessante sobre esse assunto:
USING CHARSETS AND ENCODINGS
http://java.sun.com/developer/JDCTechTips/2003/tt0110.html#1
Comment by Fernando Boaglio — October 24, 2006 @ 9:04 am
Gostei muito de sua explicação. Mas agora eu te faço a pergunta que não quer calar:
Qual deles eu, como brasileiro e desejando que as pessoas leiam as palavras por inteiro, devo usar???
Abraços.
Comment by Rangel Viotti — December 2, 2006 @ 10:30 am
O que eu entendi é que não adianta você pegar uma String escrita em UTF-8 e tentar ler ela como ISO ou vice-versa. O certo é definir um charset e usá-lo do início ao fim sem excessões. Desde seus arquivos .html, .jsp, .css, .js, .java, solicitações ajax, no banco de dados e o que mais você utilizar.
Exemplificando:
A situação do ç só daria correto se você tentar algo assim:
System.out.println(new String(”ç”.getBytes(”UTF-8″), “UTF-8″));
ou
System.out.println(new String(”ç”.getBytes(”ISO-8859-1″), “ISO-8859-1″));
Eu já tive muita dor de cabeça com uma situação onde acontecia o seguinte. O usuário entrava com os dados em um textarea no meu formulário web que estava configurado com o charset UTF-8. Quando ele enviava era o ajax que “submetia” logo o XmlHttpRequest utiliza o padrao UTF-8. Infelizmente quando pegava o requisição o meu servlet fazia o decode automático (penso eu) e convertia para ISO-8859-1, isso porque o servlet estava configurado para pegar o padrão do sistema operacional entao a situação era a seguinte.
System.out.println(new String(”ç”.getBytes(”UTF-8″), “ISO-8859-1″));
Resultado = ç;
Não consegui perceber onde estava errado porque quando dava um request.getParameter(”") o decode rolava automático e me descabelei todo.
Portanto, Nunca misture as coisas trabalhe com um charset do ínicio ao fim. Prefira o UTF-8 porque é uma tentativa de padronizar tudo e como é feita pelo W3C você tem a garantia de que empresas como Microsoft, Sun, e demais o utilizam (ou pelo menos é o que se espera).
Comment by Eliezer Reis — February 15, 2007 @ 7:22 am
Outro motivo para utilizar UTF-8 é que ele aceita muitos mais caracters que o ISO-8859-1.
Exemplo:
O caracter &rdquo que é parecido com o " (aspas duplas) aparece como ? no ISO-8859-1.
Comment by Eliezer Reis — February 15, 2007 @ 7:31 am
Acabo de descobrir que quando executamos aplicações de linha de comando no Windows o encode usado pelo prompt do DOS é o Cp850.
Assim, a única maneira de apresentar corretamente caracteres com acento é usar:
OutputStreamWriter o = new OutputStreamWriter(System.out, “Cp850″); PrintWriter pw = new PrintWriter(o, true);
pw.println(”àáçãé”);
Comment by Gabriel Corrêa de Oliveira — May 16, 2007 @ 12:57 pm
so estudante universitario quero estar atualizado em codigo
Comment by Alberto Matias — April 22, 2008 @ 6:49 am
O Unicode tornou-se o esquema predominante para o processamento interno de texto, e por vezes também para armazenamento
Comment by Alberto Matias — April 22, 2008 @ 6:53 am
Muito bom esse artigo, mas tenho uma duvida.
Estou utilizando utf-8 em tudo .html,xml,servlet,css e banco Postgresql.
Só que quando mando algo via ajax pelo Firefox tudo funciona maravilhosamente, mas quando mando pelo nosso querido IE eis que ele manda sei lá o que.
A minha pergunta é, como faço para saber qual charset foi enviado?
Abraços
Comment by Jomello — November 3, 2008 @ 2:18 am