Caelum | Ensino e Inovação - Cursos de Java, Scrum, Ruby on Rails


Entendendo Unicode e os Character Encodings

Por Paulo Silveira em 22/10/06

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 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[k256256);
    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?

  • Share/Bookmark

11 Comments »

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

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

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

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

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

  6. so estudante universitario quero estar atualizado em codigo

    Comment by Alberto Matias — April 22, 2008 @ 6:49 am

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

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

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

    Comment by Jean Landim — December 12, 2009 @ 12:52 pm

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

    Comment by Paulo Silveira — December 19, 2009 @ 2:25 am

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

    Comment by Paulo Silveira — March 8, 2010 @ 8:04 pm

RSS feed for comments on this post. TrackBack URL

Leave a comment




Caelum | Ensino e Inovação
São Paulo: Rua Vergueiro, 3185, cj. 87, próximo ao Metrô Vila Mariana   |   Tel. (11) 5571-2751
Rio de Janeiro: Rua Senador Dantas, 80, cj. 307/308 - Centro   |   Tel. (21) 2220-4156 ou 2297-0033
Brasília: SCS Qd. 8 Bl. B-50, Sala 521 - Ed. Venâncio 2000   |   Tel. (61) 3039-4222