Entendendo Unicode e os Character Encodings

Todo 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 0x10FFFF). 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?

27 Comentários

  1. Fernando Boaglio 24/10/2006 at 09:04 #

    Outro artigo interessante sobre esse assunto:

    USING CHARSETS AND ENCODINGS
    http://java.sun.com/developer/JDCTechTips/2003/tt0110.html#1

  2. Rangel Viotti 02/12/2006 at 10:30 #

    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.

  3. Eliezer Reis 15/02/2007 at 07:22 #

    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).

  4. Eliezer Reis 15/02/2007 at 07:31 #

    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.

  5. Gabriel Corrêa de Oliveira 16/05/2007 at 12:57 #

    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(“àáçãé”);

  6. Alberto Matias 22/04/2008 at 06:49 #

    so estudante universitario quero estar atualizado em codigo

  7. Alberto Matias 22/04/2008 at 06:53 #

    O Unicode tornou-se o esquema predominante para o processamento interno de texto, e por vezes também para armazenamento

  8. Jomello 03/11/2008 at 02:18 #

    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

  9. Jean Landim 12/12/2009 at 12:52 #

    De fato o UTF-8 é melhor que o latin-1.

  10. Paulo Silveira 19/12/2009 at 02:25 #

    pra quem quiser algo mais prático para resolver os problemas, tem o famoso post do luca:
    http://www.guj.com.br/posts/list/12456.java

  11. Paulo Silveira 08/03/2010 at 20:04 #

    Legal ver o método DataInputStream.readUTF. Como ler um unicode, se acabamos de discutir que não há arquivos em “formato Unicode” . Na verdade ele usa algo parecido, mas nao igual, ao UTF-8: http://java.sun.com/j2se/1.4.2/docs/api/java/io/DataInputStream.html

  12. Cleyson Lago 26/05/2011 at 10:38 #

    Já tive muitos problemas com Character Encodings no php, já que ele trabalha nativamente com ISO-8859-1 , mas conseguindo intender como funciona essas codificações facilita um pouco.

    Ótimo artigo

  13. Adriano 29/05/2011 at 00:41 #

    recomendo quem for testar o codigo do exemplo testar tamben com essa lista de Charset

    String[] codes = Charset.availableCharsets().keySet().toArray(new String[0]);

    existe encodings de até 72bits.

  14. Anonimo 19/12/2011 at 06:31 #

    Como eliminar este problema???
    Como verificar qual/quais encodes estou a usar na minha aplicação???
    Java + mysql + ZK + Spring

  15. Marcos 16/06/2013 at 18:43 #

    Muito bom o post. Agradeço ao autor aí por tê-lo escrito.

  16. PH 04/12/2013 at 17:12 #

    Paulo, porque a gente soma com 256 e depois pega o resto da divisão por 256???

  17. Paulo Silveira 16/12/2013 at 11:48 #

    por uma questao de lidar com os numeros negativos e complemento da divisao

  18. Jorge Reis 05/02/2015 at 10:48 #

    Olá meu caro, gosto muito de seus textos e de todo o material da Caelum / Alura, gostaria de saber o seguinte:
    A informação de encoding fica onde no arquivo: em um “head”, ela é uma informação/atributo do arquivo, ou ela não é uma informação que se aplique a todo arquivo, ou seja, é possível misturar vários encodes no mesmo arquivo, (pelo que entendi sim), neste caso, como faço pra saber o encoding de um arquivo? tem como fazer isso de forma determinística?
    Me desculpe as perguntas.
    Grato.

  19. caio 14/05/2016 at 04:03 #

    é possível que o usuario digite “possível” na jsp, no banco grave “possível” e ao exibir na tela exiba “possível”, sem uma maracutaia louca?

  20. MAX 07/08/2016 at 01:16 #

    Não estou achando um exemplo para um exercicio de criar uma moldura usando ascii caracteres 205, 187, 210 etc que são elementos que formam um frame no Java, alguém tem uma dica ?

Deixe uma resposta