От компиляции до времени выполнения, как действительно работает кодировка Java String
Недавно я понял, что я не полностью понимаю процесс кодирования строки Java.
Рассмотрим следующий код:
public class Main
{
public static void main(String[] args)
{
System.out.println(java.nio.charset.Charset.defaultCharset().name());
System.out.println("ack char: ^"); /* where ^ = 0x06, the ack char */
}
}
Поскольку управляющие символы интерпретируются по-разному между окнами-1252 и ISO-8859-1, я выбрал ack
char для тестирование.
Теперь я скомпилирую его с различными кодировками файлов, UTF-8, windows-1252 и ISO-8859-1. Оба скомпилируются в одну и ту же вещь, байт за байт, как проверено md5sum
.
Затем я запускаю программу:
$ java Main | hexdump -C
00000000 55 54 46 2d 38 0a 61 63 6b 20 63 68 61 72 3a 20 |UTF-8.ack char: |
00000010 06 0a |..|
00000012
$ java -Dfile.encoding=iso-8859-1 Main | hexdump -C
00000000 49 53 4f 2d 38 38 35 39 2d 31 0a 61 63 6b 20 63 |ISO-8859-1.ack c|
00000010 68 61 72 3a 20 06 0a |har: ..|
00000017
$ java -Dfile.encoding=windows-1252 Main | hexdump -C
00000000 77 69 6e 64 6f 77 73 2d 31 32 35 32 0a 61 63 6b |windows-1252.ack|
00000010 20 63 68 61 72 3a 20 06 0a | char: ..|
00000019
Он правильно выводит 0x06
независимо от того, какая кодировка используется.
Хорошо, он по-прежнему выводит те же 0x06
, которые будут интерпретироваться как печатные [ACK] char кодами-окнами windows-1252.
Это приводит меня к нескольким вопросам:
- Является ли кодовая страница/кодировка скомпилированного файла Java ожидаемой идентичной кодировке по умолчанию системы, в которой она компилируется? Являются ли эти два синонимами?
- Скомпилированное представление, похоже, не зависит от кодировки времени компиляции, действительно ли это так?
- Означает ли это, что строки в файлах Java могут интерпретироваться по-разному во время выполнения, если они не используют стандартные символы для текущей кодировки/локали?
- Что еще я должен знать о кодировке строк и символов в Java?
Ответы
Ответ 1
- Исходные файлы могут быть в любой кодировке
- Вам нужно указать компилятору кодировку исходных файлов (например,
javac -encoding...
); в противном случае предполагается кодирование платформы.
- В двоичных файлах файлов классов строковые литералы хранятся как (измененные) UTF-8, но если вы не работаете с байт-кодом, это не имеет значения (см. JVM спецификации)
- Строки в Java - это UTF-16, всегда (см. Спецификация языка Java)
-
System.out
PrintStream
преобразует ваши строки из UTF-16 в байты в системном кодировании до их записи в stdout
Примечания:
Ответ 2
Резюме "что знать" о строковых кодировках в Java:
- A
String
экземпляр в памяти представляет собой последовательность из 16-разрядных "блоков кода", которые Java обрабатывает как значения char
. Концептуально эти кодовые единицы кодируют последовательность "кодовых точек", где кодовая точка - это "номер, приписываемый заданному символу в соответствии со стандартом Unicode". Кодовые точки варьируются от 0 до бит более одного миллиона, хотя до сих пор было определено только 100 тысяч или около того. Кодовые точки от 0 до 65535 кодируются в единый блок кода, в то время как другие кодовые точки используют два блока кода. Этот процесс называется UTF-16 (он же UCS-2). Есть несколько тонкостей (некоторые кодовые точки недействительны, например 65535, и в первом 65536 содержится всего 2048 кодовых точек, зарезервированных именно для кодирования других кодовых точек).
- Кодовые страницы и тому подобное не влияют на то, как Java хранит строки в ОЗУ. Поэтому "Unicode" начинается с "Uni". Пока вы не выполняете ввод-вывод со своими строками, вы находитесь в мире Unicode, где все используют одинаковое сопоставление символов с кодовыми точками.
- Шрифты вступают в действие при кодировании строк в байтах или декодировании строк из байтов. Если явно не указано, Java будет использовать кодировку по умолчанию, которая зависит от пользователя "locale", нечеткое совокупное представление о том, что делает компьютер в Японии японским. Когда вы печатаете строку с помощью
System.out.println()
, JVM преобразует строку в нечто подходящее для любых символов, что часто означает преобразование их в байты, используя кодировку, которая зависит от текущей локали (или то, что JVM догадывается о текущий язык).
- Одно Java-приложение - это компилятор Java. Компилятору Java необходимо интерпретировать содержимое исходных файлов, которые на системном уровне представляют собой только пучок байтов. Компилятор Java затем выбирает кодировку по умолчанию для этого, и он делает это в зависимости от текущей локали, как это делает Java, потому что компилятор Java сам написан на Java. Компилятор Java (
javac
) принимает флаг командной строки (-encoding
), который может использоваться для переопределения этого выбора по умолчанию.
- Компилятор Java создает файлы классов, которые не зависят от языка. Строковые литералы попадают в эти файлы классов с кодировкой (вроде) UTF-8, независимо от кодировки, которую компилятор Java использовал для интерпретации исходных файлов. Локаль в системе, на которой работает Java-компилятор, влияет на интерпретацию исходного кода, но как только компилятор Java понял, что ваша строка содержит кодовую точку 6, то эта кодовая точка - это то, что проделает путь к файлам классов, и никто другой. Обратите внимание, что коды с 0 по 127 имеют одинаковую кодировку в UTF-8, CP-1252 и ISO-8859-1, поэтому вы не удивитесь.
- Даже при этом экземпляры
String
не зависят от какого-либо кодирования, если они остаются в ОЗУ, некоторые из операций, которые вы, возможно, захотите выполнить в строках, зависят от языка. Это не вопрос кодирования; но локаль также определяет "язык", и бывает так, что понятия верхнего и нижнего регистров зависят от используемого языка. Обычный Подозреваемый вызывает "unicode".toUpperCase()
: это дает "UNICODE"
, за исключением того, что текущая локаль является турецкой, и в этом случае вы получаете "UNİCODE"
( "I
" имеет точку). Основное предположение здесь состоит в том, что если текущий язык является турецким, то данные, которыми управляет приложение, вероятно, являются турецким текстом; лично я нахожу это предположение в лучшем случае сомнительным. Но так оно и есть.
В практическом плане вы должны явно указывать кодировки в своем коде, по крайней мере, большую часть времени. Не вызывайте String.getBytes()
, звоните String.getBytes("UTF-8")
. Использование кодировки по умолчанию, зависящей от локали, отлично, когда она применяется к некоторым данным, обмениваемым с пользователем, таким как файл конфигурации или сообщение для немедленного отображения; но в других местах, избегайте локально зависимых методов, когда это возможно.
Среди других языковых зависимостей Java есть календари. Существует целый часовой бизнес, который зависит от "часового пояса", который должен относиться к географическому положению компьютера (и это не является частью "локали" stricto sensu...). Кроме того, бесчисленные приложения Java таинственно терпят неудачу при запуске в Бангкоке, потому что в тайском языке Java по умолчанию использует буддийский календарь, согласно которому текущий год равен 2553.
Как правило, предположим, что мир огромен (он есть!) и сохраняет вещи родовыми (не делайте ничего, что зависит от набора символов до самого последнего момента, когда фактически должен выполняться ввод-вывод).
Ответ 3
При компиляции с различными кодировками эти кодировки влияют только на исходные файлы. Если у вас нет специальных символов внутри ваших источников, не будет никакой разницы в полученном байтовом коде.
Для времени выполнения используется кодировка по умолчанию для операционной системы. Это не зависит от кодировки, которую вы использовали для компиляции.
Ответ 4
Erm на основе this и this Управляющий символ ACK в обоих кодировках точно такой же. Разница, о которой вы указали, говорит о том, как у DOS/Windows на самом деле есть символы для большинства управляющих символов в Windows-1252 (например, символы Heart/Club/Spade/Diamond и simileys), а ISO-8859 - нет.