openjdk.ruOpenJDK на русском

JEP 400: UTF-8 by Default

UTF-8 по умолчанию

AuthorsAlan Bateman, Naoto Sato
ОтветственныйNaoto Sato
ТипFeature
ОбластьSE
СтатусClosed / Delivered
Выпуск18
Компонентcore-libs / java.nio.charsets
Обсуждениеcore dash libs dash dev at openjdk dot java dot net
ТрудоёмкостьXS
ДлительностьXS
РецензентыAlex Buckley, Brian Goetz
ОдобренBrian Goetz
Создан2017/08/31 13:16
Обновлён2025/04/14 07:28
Задача8187041

Аннотация

Указать в спецификации UTF-8 как кодировку по умолчанию для стандартных API Java. После этого изменения API, зависящие от кодировки по умолчанию, будут вести себя одинаково во всех реализациях, операционных системах, локалях и конфигурациях.

Цели

  • Сделать программы на Java более предсказуемыми и переносимыми, если их код полагается на кодировку по умолчанию.

  • Уточнить, где стандартные API Java используют кодировку по умолчанию.

  • Перейти на UTF-8 во всех стандартных API Java, за исключением консольного ввода-вывода.

Что не является целью

  • Определение новых стандартных API Java или поддерживаемых API JDK не является целью, хотя в ходе этой работы могут обнаружиться места, где новые вспомогательные методы могли бы сделать существующие API понятнее или удобнее в использовании.

  • Мы не намерены объявлять устаревшими или удалять стандартные API Java, которые полагаются на кодировку по умолчанию, а не принимают явный параметр с кодировкой.

Мотивация

Стандартные API Java для чтения и записи файлов и для обработки текста позволяют передавать в качестве аргумента кодировку (charset). Кодировка управляет преобразованием между «сырыми» байтами и 16-битовыми значениями char языка программирования Java. К поддерживаемым кодировкам относятся, например, US-ASCII, UTF-8 и ISO-8859-1.

Если аргумент с кодировкой не передан, стандартные API Java обычно используют кодировку по умолчанию. JDK выбирает кодировку по умолчанию при запуске, исходя из среды выполнения: операционной системы, локали пользователя и других факторов.

Поскольку кодировка по умолчанию не везде одинакова, API, которые её используют, таят в себе множество неочевидных опасностей даже для опытных разработчиков.

Рассмотрим приложение, которое создаёт java.io.FileWriter, не передавая кодировку, и затем с его помощью записывает текст в файл. Полученный файл будет содержать последовательность байтов, закодированную в кодировке по умолчанию того JDK, на котором работает приложение. Второе приложение, запущенное на другой машине или другим пользователем на той же машине, создаёт java.io.FileReader, не передавая кодировку, и с его помощью читает байты из этого файла. Полученный текст содержит последовательность символов, декодированную в кодировке по умолчанию того JDK, на котором работает второе приложение. Если кодировка по умолчанию в JDK первого приложения отличается от кодировки по умолчанию в JDK второго приложения, то полученный текст может оказаться незаметно искажённым или неполным, поскольку FileReader не может определить, что декодировал текст в неправильной по отношению к FileWriter кодировке. Вот пример такой опасности: японский текстовый файл в кодировке UTF-8, созданный в macOS, искажается при чтении в Windows с американской английской или японской локалью:

java.io.FileReader(“hello.txt”) -> “こんにちは” (macOS)
java.io.FileReader(“hello.txt”) -> “ã?“ã‚“ã?«ã?¡ã? ” (Windows (en-US))
java.io.FileReader(“hello.txt”) -> “縺ォ縺。縺ッ” (Windows (ja-JP)

Разработчики, знакомые с такими опасностями, могут использовать методы и конструкторы, явно принимающие аргумент с кодировкой. Однако из-за необходимости передавать аргумент такие методы и конструкторы нельзя использовать через ссылки на методы (::) в потоковых конвейерах (stream pipelines).

Иногда разработчики пытаются настроить кодировку по умолчанию, задавая системное свойство file.encoding в командной строке (т. е. java -Dfile.encoding=...), но это никогда не поддерживалось. Кроме того, попытка задать это свойство программно (т. е. System.setProperty(...)) после запуска среды выполнения Java не работает.

Не все стандартные API Java полагаются на кодировку по умолчанию, выбранную JDK. Например, методы в java.nio.file.Files, которые читают или записывают файлы без аргумента Charset, по спецификации всегда используют UTF-8. То, что новые API по умолчанию используют UTF-8, а старые API по умолчанию используют кодировку по умолчанию, создаёт опасность для приложений, в которых используются и те и другие API.

Вся экосистема Java выиграла бы, если бы кодировка по умолчанию была задана в спецификации одинаковой везде. Приложения, которых не заботит переносимость, почти не почувствуют изменений, а приложения, которые заботятся о переносимости и передают аргументы с кодировкой, не почувствуют их вовсе. UTF-8 уже давно является самой распространённой кодировкой во Всемирной паутине. UTF-8 — стандарт для файлов XML и JSON, которые обрабатывает огромное количество программ на Java, и собственные API Java всё больше отдают предпочтение UTF-8, например в NIO API и для файлов свойств. Поэтому имеет смысл указать в спецификации UTF-8 как кодировку по умолчанию для всех API Java.

Мы понимаем, что это изменение может широко затронуть совместимость программ, переходящих на JDK 18. Поэтому всегда будет возможно вернуть поведение, существовавшее до JDK 18, при котором кодировка по умолчанию зависит от среды.

Описание

В JDK 17 и более ранних версиях кодировка по умолчанию определяется при запуске среды выполнения Java. В macOS это UTF-8, кроме локали POSIX C. В других операционных системах она зависит от локали пользователя и кодировки по умолчанию, например в Windows это кодировка на основе кодовой страницы, такая как windows-1252 или windows-31j. Метод java.nio.charsets.Charset.defaultCharset() возвращает кодировку по умолчанию. Быстро узнать кодировку по умолчанию текущего JDK можно следующей командой:

java -XshowSettings:properties -version 2>&1 | grep file.encoding

Кодировку по умолчанию используют несколько стандартных API Java, в том числе:

  • В пакете java.io классы InputStreamReader, FileReader, OutputStreamWriter, FileWriter и PrintStream определяют конструкторы для создания объектов чтения, объектов записи и потоков печати, которые кодируют или декодируют данные с помощью кодировки по умолчанию.

  • В пакете java.util классы Formatter и Scanner определяют конструкторы, результаты которых используют кодировку по умолчанию.

  • В пакете java.net классы URLEncoder и URLDecoder определяют устаревшие методы, которые используют кодировку по умолчанию.

Мы предлагаем изменить спецификацию Charset.defaultCharset() так, чтобы в ней говорилось, что кодировка по умолчанию — UTF-8, если иное не настроено способом, зависящим от реализации. (Ниже описано, как настроить JDK.) Кодировка UTF-8 определена в RFC 2279; формат преобразования, на котором она основана, определён в Amendment 2 к ISO 10646-1 и также описан в стандарте Unicode. Её не следует путать с Modified UTF-8.

Мы обновим спецификации всех стандартных API Java, использующих кодировку по умолчанию, добавив в них перекрёстные ссылки на Charset.defaultCharset(). К этим API относятся перечисленные выше, но не System.out и System.err, кодировка которых будет такой, как указано в Console.charset().

Системные свойства file.encoding и native.encoding

Как и предусмотрено спецификацией Charset.defaultCharset(), JDK позволит настроить в качестве кодировки по умолчанию не UTF-8, а другую кодировку. Мы пересмотрим обработку системного свойства file.encoding так, чтобы поддерживаемым способом настройки кодировки по умолчанию стало задание этого свойства в командной строке. Мы укажем это в примечании о реализации к System.getProperties() следующим образом:

  • Если file.encoding имеет значение "COMPAT" (т. е. java -Dfile.encoding=COMPAT), то кодировкой по умолчанию будет кодировка, выбранная алгоритмом JDK 17 и более ранних версий на основе операционной системы пользователя, локали и других факторов. Значением file.encoding будет имя этой кодировки.

  • Если file.encoding имеет значение "UTF-8" (т. е. java -Dfile.encoding=UTF-8), то кодировкой по умолчанию будет UTF-8. Это значение ничего не меняет и определено для того, чтобы сохранить поведение существующих командных строк.

  • Обработка значений, отличных от "COMPAT" и "UTF-8", не специфицирована. Такие значения не поддерживаются, но если значение работало в JDK 17, то, скорее всего, оно продолжит работать в JDK 18.

Перед развёртыванием на JDK, в котором кодировкой по умолчанию является UTF-8, разработчикам настоятельно рекомендуется проверить наличие проблем с кодировкой, запустив среду выполнения Java с java -Dfile.encoding=UTF-8 ... на текущем JDK (8–17).

В JDK 17 появилось системное свойство native.encoding — стандартный способ, которым программы могут получить кодировку, выбранную алгоритмом JDK, независимо от того, настроена ли на самом деле эта кодировка в качестве кодировки по умолчанию. В JDK 18, если в командной строке file.encoding имеет значение COMPAT, то значение file.encoding во время выполнения будет совпадать со значением native.encoding во время выполнения; если в командной строке file.encoding имеет значение UTF-8, то значение file.encoding во время выполнения может отличаться от значения native.encoding во время выполнения.

Ниже, в разделе Риски и допущения, мы обсуждаем, как смягчить возможные несовместимости, возникающие из-за этого изменения file.encoding, а также системное свойство native.encoding и рекомендации для приложений.

JDK использует для внутренних целей три системных свойства, связанных с кодировками. Они по-прежнему не специфицированы и не поддерживаются, но для полноты описаны здесь:

  • sun.stdout.encoding и sun.stderr.encoding — имена кодировок, используемых для стандартного потока вывода (System.out) и стандартного потока ошибок (System.err), а также в API java.io.Console.

  • sun.jnu.encoding — имя кодировки, которую реализация java.nio.file использует при кодировании и декодировании путей к файлам, в отличие от содержимого файлов. В macOS её значение — "UTF-8"; на других платформах это обычно кодировка по умолчанию.

Кодировка исходных файлов

Язык Java позволяет исходному коду выражать символы Unicode в кодировке UTF-16, и выбор UTF-8 в качестве кодировки по умолчанию на это не влияет. Однако он влияет на компилятор javac, поскольку тот предполагает, что исходные файлы .java закодированы в кодировке по умолчанию, если иное не настроено параметром -encoding. Если исходные файлы были сохранены в кодировке, отличной от UTF-8, и скомпилированы более ранним JDK, то повторная компиляция на JDK 18 или более поздних версиях может вызвать проблемы. Например, если исходный файл не в UTF-8 содержит строковые литералы с не-ASCII-символами, то javac в JDK 18 и более поздних версиях может неверно интерпретировать эти литералы, если не использовать -encoding.

Перед компиляцией на JDK, в котором кодировкой по умолчанию является UTF-8, разработчикам настоятельно рекомендуется проверить наличие проблем с кодировкой, выполнив компиляцию с javac -encoding UTF-8 ... на текущем JDK (8–17). В качестве альтернативы разработчики, которые предпочитают сохранять исходные файлы в кодировке, отличной от UTF-8, могут не дать javac предполагать UTF-8, задав для параметра -encoding значение системного свойства native.encoding в JDK 17 и более поздних версиях.

Устаревшая кодировка default

В JDK 17 и более ранних версиях имя default распознаётся как псевдоним кодировки US-ASCII. То есть Charset.forName("default") даёт тот же результат, что и Charset.forName("US-ASCII"). Псевдоним default появился в JDK 1.5, чтобы унаследованный код, использовавший конвертеры sun.io, можно было перенести на фреймворк java.nio.charset, появившийся в JDK 1.4.

Было бы крайне запутанно, если бы JDK 18 сохранил default как псевдоним US-ASCII, когда кодировкой по умолчанию по спецификации является UTF-8. Запутанно было бы и то, что default означало бы US-ASCII, когда пользователь возвращает кодировке по умолчанию значение, действовавшее до JDK 18, задав -Dfile.encoding=COMPAT в командной строке. Если переопределить default как псевдоним не US-ASCII, а кодировки по умолчанию (будь то UTF-8 или настроенная пользователем), это приведёт к незаметным изменениям поведения в (немногих) программах, которые вызывают Charset.forName("default").

Мы считаем, что продолжать распознавать default в JDK 18 означало бы продлевать неудачное решение. Это имя не определено в платформе Java SE и не признано IANA ни как имя, ни как псевдоним какой-либо кодировки. Более того, для сетевых протоколов на основе ASCII IANA рекомендует использовать каноническое имя US-ASCII, а не просто ASCII или малоизвестные псевдонимы вроде ANSI_X3.4-1968 — очевидно, использование специфичного для JDK псевдонима default противоречит этой рекомендации. Программы на Java могут использовать константу перечисления StandardCharsets.US_ASCII, чтобы ясно выразить своё намерение, вместо того чтобы передавать строку в Charset.forName(...).

Поэтому в JDK 18 вызов Charset.forName("default") будет выбрасывать UnsupportedCharsetException. Так разработчики смогут обнаружить использование этой идиомы и перейти либо на US-ASCII, либо на результат Charset.defaultCharset().

Тестирование

  • Чтобы понять, насколько сильно это изменение повлияет на совместимость, требуется серьёзное тестирование. Понадобится тестирование силами разработчиков или организаций, чьи пользователи находятся в разных частях мира.

  • Разработчики могут проверить наличие проблем на существующем выпуске JDK, запустив его с -Dfile.encoding=UTF-8, ещё до выхода любого выпуска Early Access (ранний доступ) или GA с этим изменением.

Риски и допущения

Мы предполагаем, что во многих средах выбор UTF-8 в Java никак не скажется на приложениях:

  • В macOS кодировкой по умолчанию уже несколько выпусков является UTF-8, кроме случаев, когда настроена локаль POSIX C.

  • Во многих дистрибутивах Linux, хотя и не во всех, кодировкой по умолчанию является UTF-8, поэтому в этих средах никаких изменений заметно не будет.

  • Многие серверные приложения уже запускаются с -Dfile.encoding=UTF-8, поэтому для них ничего не изменится.

В других средах риск, связанный со сменой кодировки по умолчанию на UTF-8 спустя более чем 20 лет, может быть значительным. Самый очевидный риск в том, что приложения, неявно зависящие от кодировки по умолчанию (например, не передающие API явный аргумент с кодировкой), будут работать неправильно при обработке данных, созданных в то время, когда кодировка по умолчанию не была задана спецификацией. Ещё один риск в том, что данные могут незаметно повреждаться. Мы ожидаем, что в основном это затронет пользователей Windows с азиатскими локалями и, возможно, некоторые серверные среды с азиатскими и другими локалями. Возможные сценарии:

  • Если приложение, которое годами работало с windows-31j в качестве кодировки по умолчанию, обновить до выпуска JDK, где кодировкой по умолчанию является UTF-8, то у него возникнут проблемы при чтении файлов в кодировке windows-31j. В этом случае код приложения можно изменить так, чтобы при открытии таких файлов он передавал кодировку windows-31j. Если код изменить нельзя, то запуск среды выполнения Java с -Dfile.encoding=COMPAT принудительно сделает кодировкой по умолчанию windows-31j, пока приложение не будет обновлено или файлы не будут преобразованы в UTF-8.

  • В средах, где используется несколько версий JDK, пользователи могут оказаться не в состоянии обмениваться файлами. Если, например, один пользователь работает со старым выпуском JDK, где по умолчанию используется windows-31j, а другой — с более новым JDK, где по умолчанию используется UTF-8, то второй пользователь может не суметь прочитать текстовые файлы, созданные первым. В этом случае пользователь старого выпуска JDK может указывать -Dfile.encoding=UTF-8 при запуске приложений, или же пользователь нового выпуска может указывать -Dfile.encoding=COMPAT.

Если код приложения можно изменить, мы рекомендуем изменить его так, чтобы он передавал конструкторам аргумент с кодировкой. Если у приложения нет особых предпочтений в выборе кодировки и его устраивает традиционный выбор кодировки по умолчанию в зависимости от окружения, то для получения кодировки, определяемой окружением, во всех выпусках Java можно использовать следующий код:

String encoding = System.getProperty("native.encoding");  // Populated on Java 18 and later
Charset cs = (encoding != null) ? Charset.forName(encoding) : Charset.defaultCharset();
var reader = new FileReader("file.txt", cs);

Если нельзя изменить ни код приложения, ни параметры запуска Java, то придётся вручную изучить код приложения и определить, будет ли оно совместимо работать на JDK 18.

Альтернативы

  • Оставить всё как есть — это не устраняет описанные выше опасности.

  • Объявить устаревшими все методы Java API, использующие кодировку по умолчанию — это побудило бы разработчиков использовать конструкторы и методы, принимающие параметр с кодировкой, но код от этого стал бы более многословным.

  • Сделать UTF-8 кодировкой по умолчанию по спецификации, не предоставив никакого способа её изменить — такое изменение слишком сильно повлияло бы на совместимость.