sobota, 6 czerwca 2009

java encoding

Wpis został stworzony na podstawie prezentacji Dawida Weissa "The beauty of debugging".
  • Kodowanie plików źródłowych.
    javac zamienia kod źródłowy na bytecode rozumiany przez JVM. W ten sposób z pliku .java powstaje nam plik .class. W ramach tego procesu literały zawarte w pliku źródłowym trafiają do pliku wynikowego, a dokładnie do constant pool table i zostają zakodowane za pomocą UTF-8 (nie jest to 100% prawdą ale możemy tak przyjąć). javac domyślnie zakłada, że nasz plik źródłowy ma kodowanie zgodne z domyślnym kodowaniem platformy - czyli na moim Windows jest to cp1250. Jeśli mamy w takim razie plik źródłowy a w nim następujący kod:
    String str="żółć";

    to w przypadku gdy kodowanie tego pliku jest cp1250 (czyli zgodne z domyślnym kodowaniem platformy)to podglądając ten plik w edytorze hex, żółć to następujący łańcuch bajtów (hex): BF F3 B3 E6. Kompilacji pliku źródłowego, tworzy plik .class, w którym to łańcuch żółć zostaje zakodowany do postaci UTF-8(hex) : C5BC C3B3 C582 C487. Wszystko super, ale gdy nasz plik źródłowy zostanie zakodowany w UTF8 i skompilujemy go tak jak poprzednio, plik wynikowy będzie tym razem zawierał "bezsensowne krzaki". W pliku źródłowym żółć zostanie zakodowana w UTF8 jako:C5BC C3B3 C582 C487, ale javac domyślnie potraktuje ten plik jako zakodowany w Cp1250 i zostanie przeprowadzona konwersja literałów do UTF8. W ten sposób żółć zakodowana w UTF8 zostaje potraktowana jako ciąg bajtów w cp1250, dla którego będzie robiona konwersja do UTF8. W wyniku tego plik wynikowy zakoduje żółć jako następujący ciąg bajtów(hex):C4B9 C4BD C482 C582 C4B9 E280 9AC3 84E2 80A1. Mechanizm jest następujący: C4B9 w UTF8 jest to kod znaku, który odpowiada znakowi C5 w cp1250, C4BD w UTF8 odpowiada BC z cp120 itd. Trzeba pamiętać aby przy kompilacji wskazać kodowanie plików źródłowych: javac -encoding ... , ponieważ nie zawsze musi być/jest zgodne z kodowaniem domyślnym platformy.
  • Kodowanie znaków dla operacji I/O
    W Java typ char jest reprezentowany jako znak Unicode (Unicode code point). Nie oznacza to jednak, że java działa tylko na systemach z charset Unicode. Java domyślnie dokonuje konwersji do i z Unicode, ale ta domyślna konwersja zadziała poprawnie gdy to co chcemy skonwertować lub to, na co chcemy skonwertować jest kodowane za pomocą domyślnego kodowania znaków na naszym systemie. W Java API jest wiele fragmentów, których działanie jest uzależnione od domyślnego kodowania, np. używanie FileWriter, FileReader, wywołanie getBytes() na obiekcie typu String. Oznacza to, że używanie tych konstrukcji może zupełnie inaczej się zachowywać w zależności od systemu, na którym uruchomimy aplikacje. Następujący kod:
    String str = "żółć";
    FileWriter fileWriter = new FileWriter("plik.txt");
    fileWriter.write(str);
    fileWriter.flush();
    fileWriter.close);

    zapisuje do pliku plik.txt tekst "żółć". Uruchamiam aplikacje pod Windows i za pomocą edytora hex oglądam zawartość pliku plik.txt:AF F3 B3 E6. Domyślne kodowanie na moim komputerze to Cp1250. Można to sprawdzić odpytując system property: file.encoding. W przypadku uruchomienia aplikacji na Linux (tam file.encoding to UTF-8) plik plik.txt zawiera tekst "żółć", ale zakodowany w UTF8 czyli w edytorze hex zobaczymy następujący łańcuch bajtów: C5 BC C3 B3 C5 82 C4 87. Jak widać wynik działania zależy od platformy, na której uruchomimy aplikacje ...Przy uruchomieniu aplikacji można wymusić domyślne kodowanie znaków za pomocą -Dfile.encoding=, lub też (co wydaje się lepsze) unikać korzystania z elementów API, które wykorzystują domyślne kodowanie
W przypadku gdy będziemy chcieli "wypchnąć" znak Unicode, dla którego nie istnieje reprezentacja w kodowaniu, w którym będziemy chcieli ten znak reprezentować, to zobaczymy znak '?'.
char s = '\u01fe';
FileWriter fileWriter = new FileWriter("foo.bar");
fileWriter.write(s);
fileWriter.flush();
fileWriter.close();
System.out.println(s);

Uruchomienie powyższego kodu na moim komputerze z Windows (bez dodawania -Dfile.encoding) skutkuje utworzeniem pliku foo.bar oraz wypisaniem znaku na konsole.
Z racji tego, że znak 'Ǿ' nie jest w żaden sposób reprezentowany w moim domyślnym kodowaniu(cp1250) na konsoli oraz w pliku znajdzie się znak '?' (w hex 3f).
W przypadku gdy przy uruchomieniu aplikacji dodam -Dfile.encoding=UTF8 w pliku zobaczę poprawną reprezentację znaku w kodowaniu UTF8: C7 BE , a na konoli 2 znaczki: Çľ, czyli reprezentaja znaków o kodach odpowiednio C7 oraz BE w kodowaniu cp1250.
Więcej na ten temat tu

W przypadku gdy potrzebujemy sprawdzić dany znak, szczególnie dotyczy to znaków "wklejonych z nieznanych źródeł" najlepiej wyświetlić każdy znak jako code point oraz jego reprezentacje w wybranym kodowaniu, np UTF-8.
    
    int i = 8211;
    printRepresentation(i);  

    char c='ą'
    printRepresentation(c);  

    char c2 = '\u1FB4';
    printRepresentation(c2);  

   private void printRepresentation(int i) throws UnsupportedEncodingException {
        System.out.println((char)i + " = " +i +" " + unicodeCodePoint(i) + " '" + utf8HexRepresentation((char)i)+"'");
    }

   private String unicodeCodePoint(int i) {
        return String.format("U+%04X", i);
    }

    public static String utf8HexRepresentation(char c) throws UnsupportedEncodingException {
        byte[] bytes = (""+c).getBytes("UTF-8");
        return hexString(bytes);
    }
    private static String hexString(byte[] data) {
        StringBuffer s = new StringBuffer();
        for (int i = 0; i < data.length; i++) {
            int high_nibble = (data[i] & 0xf0) >>> 4;
            int low_nibble = (data[i] & 0x0f);
            s.append(hex_table[high_nibble]);
            s.append(hex_table[low_nibble]);
            s.append(" ");
        }
        return s.toString().trim();
    }

    private static char[] hex_table = { '0', '1', '2', '3', '4', '5', '6', '7',
            '8', '9', 'a', 'b', 'c', 'd', 'e', 'f' };

Wyszukanie "problematycznego" znaku w pliku/plikach można zrobić za pomocą grepa:
 
grep --colour $'\xe2\x80\x93' *

Jeśli chodzi o kodowanie znaków w J2EE to warto zajrzeć tu

Brak komentarzy:

Prześlij komentarz