[an error occurred while processing this directive]
2000 г

Java: Русские буквы и не только...

Сергей Астахов (sergeya@comita.spb.ru),
http://www.comita.spb.ru/users/sergeya/java/ruschars.html

Здесь я попытался собрать информацию по поводу написания русскоязычных программ на языке Java. Если Вам известно что-либо помимо того, что я тут понаписал, напишите мне, ваши добавления будут кстати.

Содержание:

Файлы данных, потоки, БД.

Итак, как все, надеюсь, знают, в языке Java для представления символов используется Unicode, т.е. по два байта на один символ (тип char размером в 16 бит). В набор символов входят всевозможные буквы со всякими чёрточками и припендюльками, греческие, математические и символы псевдографики. В том числе и так любимые нами символы кириллицы (диапазон значений 0x0400-0x04ff). Так что с этой стороны никакой дискриминации нет.

Если Вам интересны конкретные кода символов, для их просмотра удобно использовать программу "Таблица символов" из WinNT. Вот, например, диапазон кириллицы:

С другой стороны большинство файлов данных основано на 8-битовом представлении символов. Сюда входят также текстовые файлы и большинство баз данных (окромя наиболее продвинутых). Кроме того, что самое паршивое, одни и те же байты могут представлять разные символы (в зависимости от кодовой страницы). Налицо конфликт - как преобразовать одно в другое и наоборот, причём с наименьшими потерями для данных. Для этого был придуман довольно удобный механизм использования кодовых страниц. Для каждой кодовой страницы было создано по 2 класса перекодировки (ByteToChar и CharToByte). Классы эти лежат в пакете sun.io. Если, при перекодировке из char в byte не было найдено соответствующего символа, он заменяется на символ ?.

Кстати, эти файлы кодовых страниц в некоторых ранних версиях JDK 1.1 содержат ошибки, вызывающие ошибки перекодировок, а то и вообще исключения при выполнении. Например, это касается кодировки KOI8_R. Лучшее, что можно при этом сделать - сменить версию на более позднюю. Судя по Sun-овскому описанию, большинство этих проблем было решено в версии JDK 1.1.6.

Когда и как надлежит пользоваться этой перекодировкой? Когда пользоваться, в принципе, понятно - при любом преобразовании из byte в char и наоборот. В классе String в тех местах, где есть преобразование можно указать дополнительный параметр (String enc), задающий имя кодовой страницы. Это конструктор по массиву байтов и метод getBytes(). Однако, в реальной программе, явно указывать кодовую страницу не всегда удобно. Для этого была введена кодировка по умолчанию. По умолчанию она зависит от системы (для русских виндов принята кодировка Cp1251), и в старых JDK её можно изменить установкой системного свойства file.encoding. Вообще-то, как утверждают в Sun, это свойство отражает системную кодировку, и она не должна изменяться в командной строке (см., например, комментарии к BugID 4256423) Эта кодировка используется тогда, когда явно не указанно название страницы. Т.к. эта настройка одна на все преобразования, иногда можно наткнуться на неприятности. Например, эта же настройка используется для вывода на консольный экран, что, в случае виндов, как правило, неприемлемо - там нужно использовать страницу Cp866. Было бы здорово, если бы эти кодировки указывались независимо - например, console.encoding и т.п., но, думаю, Sun-овцам пока не до таких высоких материй.

Кстати, о выводе на консоль. Есть два пути решения вышеуказанной проблемы:

  1. Использовать вместо System.out.println свой класс, а уже в нём делать преобразование. Например:
  2.  public class Msg
     {
      static String cp = System.getProperty("console.encoding","Cp866");
      public static void message(String msg)
      {
       msg += "\n";
       byte[] b;
       try { b = msg.getBytes(cp); }
       catch( UnsupportedEncodingException e )
         {
          b = msg.getBytes();       // В случае отсутствия нужной кодировки,
                                    // делаем преобразование по умолчанию
         }
       System.out.write(b);
      }
     }
     ...
     Msg.message("Сообщение");
    
  3. Написать свою версию PrintStream, поддерживающую нужную кодировку, и подставить его через System.setOut() и System.setErr(). Вот, например, обычное начало в моих программах:
  4.  ...
     public static void main(String[] args)
     {
      // Установка вывода консольных сообщений в нужной кодировке
      try
        {
         System.setOut(new CodepagePrintStream(System.out,System.getProperty("console.encoding","Cp866")) );
        }
      catch(UnsupportedEncodingException e)
        {
         System.out.println("Unable to setup console codepage: " + e);
        }
     ...
    

Куда же податься бедному российскому программисту? Не отчаивайтесь, есть множество путей прочитать и сохранить данные из файлов в нужной Вам кодировке.

  1. Читать и записывать массивы байтов (byte[]), а для перекодировки использовать упомянутые методы класса String. Этот способ особенно удобен, когда в потоке могут присутствовать данные в разных кодировках.
  2. Использовать классы InputStreamReader и OutputStreamWriter из пакета java.io, специально предназначенные для этих целей.
  3. Сделать преобразование в нужную кодировку. Если вы всё сделаете корректно, то данные не потеряются, хотя, конечно, пользоваться этим желательно только в крайнем случае. Пример:
  4.  // Чтение русских букв в кодировке Cp866 через объект,
     // поддерживающий только Cp437
     String str = o.readString();
     str = new String(str.getBytes("Cp437"),"Cp866");
     // Сохранение русских букв в кодировке Cp866 через объект,
     // поддерживающий только Cp437
     str = new String(str.getBytes("Cp866"),"Cp437");
     o.writeString(str);
    

    Более подробно о этом методе, и о возможных проблемах с ним, расписано ниже, в разделе О методе перекодировки символов.

  5. Настроить драйвер БД на нужную кодировку. Как именно - это зависит от конкретного драйвера. К сожалению, многие драйвера понятия не имеют о каких-то там кодировках. Иногда их можно пропатчить на этот счёт, но чаще всего приходится действовать обходными путями.
  6. Например, один из самых часто используемых драйверов - мост JDBC-ODBC. В версиях JDK 1.1, этот мост просто игнорировал кодировки символов, из-за чего нужно было предпринять дополнительные ухищрения, типа описанных в предыдущем пункте (это также касается и последней ихней версии, 1.1.8).

    Мост из комплекта Sun Java 2 теперь можно настроить на нужную кодировку. Это делается добавлением дополнительного свойства charSet в набор параметров, передаваемых для открытия соединения с базой. По умолчанию используется file.encoding. Делается это примерно так:

      // Параметры соединения с базой
      Properties connInfo = new Properties();
      connInfo.put("user", username);
      connInfo.put("password", password);
      connInfo.put("charSet", "Cp1251");
      // Устанавливаем соединение
      Connection db = DriverManager.getConnection(dataurl, connInfo);
    

    Другой пример - драйвер JDBC-OCI (не pure Java - тот называется thin) от Oracle 8.0.5 под Linux. При получении данных из БД, драйвер определяет "свою" кодировку при помощи переменной окружения NLS_LANG. Если эта переменная не найдена, то он считает что кодировка - ISO88591. Весь фокус в том, что NLS_LANG должна быть именно переменной окружения, а properties (типа file.encoding) здесь "не катят". В случае использования драйвера внутри servlet engine Apache+Jserv, переменную окружения можно задать в файле jserv.properties:

      wrapper.env=NLS_LANG=American_America.CL8KOI8R

    Информацию об этом прислал Сергей Безруков, за что ему отдельное спасибо.

Если же Вы свободны в формировании формата - тогда всё проще. Используйте формат Unicode или UTF8 - и проблем не будет.

В случае с БД, можно, конечно, использовать и какой-нибудь 16-ричный формат, но это не всегда приемлемо, т.к. Вы получите 2-х - 4-х кратный рост места на диске и потеряете возможность использовать стандартные программы работы с БД, например генераторы отчётов.

Русские буквы в исходниках Java-программ.

Как уже упоминалось, при выполнении программы используется Unicode. Исходные же файлы пишутся в обычных редакторах. Я пользуюсь Far-ом, у Вас, наверняка есть свой любимый редактор. Эти редакторы сохраняют файлы в 8-битовом формате, а значит, что к этим файлам также применимы рассуждения, аналогичные приведённым выше. Разные версии компиляторов немного по разному выполняют преобразование символов. В ранних версиях JDK 1.1.x используется настройка file.encoding, которую можно поменять при помощи нестандартной опции -J. В более новых (как сообщил Денис Кокарев - начиная с 1.1.4) был введён дополнительный параметр -encoding, при помощи которого можно указать используемую кодировку. В скомпилированных классах строки представлены в виде Unicode (точнее в модифицированном варианте формата UTF8), так что самое интересное происходит при компиляции. Поэтому, самое главное - выяснить, в какой кодировке у Вас исходники и указать правильное значение при компиляции. По умолчанию будет использован всё тот же пресловутый file.encoding. Пример вызова компилятора:

javac -encoding=KOI8_R ...

Кроме использования этой настройки есть ещё один метод - указывать буквы в формате "\uxxxx", где указывается код символа. Этот метод работает со всеми версиями, а для получения этих кодов можно использовать стандартную утилиту native2ascii.

Русские буквы в файлах properties.

Для чтения файлов properties используются методы загрузки ресурсов, которые работают специфичным образом. Собственно для чтения используется метод Properties.load, который не использует file.encoding (там кодировка жёстко указана), поэтому единственный способ указать русские буквы - использовать формат "\uxxxx" и утилиту native2ascii.

Метод Properties.save работает по разному в версиях JDK 1.1 и 1.2. В версиях 1.1 он просто отбрасывал старший байт, поэтому правильно работал только с англицкими буквами. В 1.2 делается обратное преобразование в "\uxxxx", так что он работает зеркально к методу load.

Русские буквы в Servlet-ах.

Ну, для чего эти самые Servlet-ы нужны, я думаю, Вы в курсе. Если нет - то лучше сначала прочитать документацию. Здесь же рассказывается только об особенностях работы с русскими буквами.

Так в чём же особенности? Когда Servlet посылает ответ клиенту, есть два способа послать этот ответ - через OutputStream (getOutputStream()) или через PrintWriter (getWriter()). В первом случае Вы записываете массивы байтов, поэтому применимы вышеописанные методы записи в файл. В случае же PrintWriter, он использует установленную кодировку. В любом случае необходимо правильно указать используемую кодировку при вызове метода setContentType(), для того, чтобы было правильное преобразование символов на стороне сервера. Это указание должно быть сделано перед вызовом getWriter() или перед первой записью в OutputStream. Пример:

 public void doPost(HttpServletRequest request,HttpServletResponse response)
    throws ServletException, IOException
 {
  response.setContentType("text/html; charset=windows-1251"));
  PrintWriter out = response.getWriter();
  // Отладочный вывод названия кодировки для проверки
  out.println( "Encoding: " + response.getCharacterEncoding() );
  ...
  out.close();

Это по поводу отдачи ответов клиенту. Со входными параметрами, к сожалению не так просто. Входные параметры кодируются броузером побайтно в соответствии с MIME-типом "application/x-www-form-urlencoded". Как рассказал Алексей Менделев русские буквы броузеры кодируют, используя текущую установленную кодировку. Ну и, разумеется, ничего о ней не сообщают. Соотвественно, например, в JSDK 2.0 и 2.1 это никак не проверяется. Собственно для раскодирования используются методы HttpUtils.parsePostData() и HttpUtils.parseQueryString(), которые просто обнуляют старший байт. Это зарегистрированная ошибка в JSDK (4154966). К сожалению, эту ошибку закрыли как "Will not be fixed", с тем оправданием, что, дескать, раз в RFC на эту тему ничего не сказанно, то и делать мы ничего не будем. Однако, после переписки наших разработчиков в майл-листе SERVLET-INTEREST дело, похоже, сдвинулось с мёртвой точки. По крайней мере на словах было обещано включить метод установки кодировки в спецификацию JSDK 2.3.

Пока же приходится обходиться своими средствами. Оригинальный способ работы с кодировками предлагает Russian Apache - здесь расписано, как именно. Судя по отзывам, не имеет проблем с русскими и система Resin.

Своё решение проблемы так же предложил Вячеслав Педак.

Ну а самый простейший вариант извлечь таки символы - передавать в комплекте параметров имя кодировки (или, если вы уверены в текущей кодировке броузера, использовать предопределённую кодировку) и использовать метод перекодировки символов:

 public void doPost(HttpServletRequest request,HttpServletResponse response)
    throws ServletException, IOException
 {
  // Кодировка сообщений
  // В связке MSIE 4.01 SP1 -> JSDK 2.0 servletrunner.exe всегда выдаёт "ISO-8859-1"
  String requestEnc = request.getCharacterEncoding();
  // Некоторые servlet engine, не мудрствуя лукаво, возвращают null
  if( requestEnc==null ) requestEnc="ISO-8859-1";
  String clientEnc = request.getParameter("charset");
  if( clientEnc==null ) clientEnc="Cp1251";
  String value = new String(request.getParameter("value").getBytes(requestEnc),clientEnc);

работает. :-)

В общем, опыт в написании Servlet-ов у меня небольшой, так что Ваши замечания будут приветствоваться.

GUI (AWT, Swing)

Многие связывают неправильный вывод русских букв с неправильной установкой шрифта. Мне кажется, это связанно с тяжким опытом программирования на Windows 3.x, где основная причина действительно была в этом. В Java всё сложнее и редко действительно связанно со шрифтами. Я не разбирался со специфическими настройками броузеров, т.к. ещё не писал апплетов, только приложения, но думаю в последних версиях в этом плане всё нормально.

Где же действительно лежат наибольшие подводные камни? В основном это связанно с неправильной перекодировкой символов. Часть этих проблем и методы их решения описаны выше. Если у Вас все преобразования выполняются корректно, и для вывода используется шрифт Unicode, то есть очень большой шанс, что Ваша программа будет работать правильно.

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

Если программа не работает нигде - значит проблема только в ней и в ваших руках. Внимательно перечитайте всё, что было написано выше, и ищите. Если же проблема проявляется только в конкретном окружении - значит дело, скорей всего в настройках. Где именно - зависит от того, какой графической библиотекой Вы пользуетесь. Если AWT - помочь может правильная настройка файла font.properties.ru. Пример корректного файла можно взять из Java 2. Если у Вас нет этой версии, можете скачать его с данного сайта: версия для Windows, версия для Linux. Если у Вас установлена русская версия OS - просто добавьте этот файл туда, где лежит файл font.properties. Если же это англицкая версия, то нужно, или дополнительно сменить текущий язык при помощи задания настройки -Duser.language=ru или переписать этот файл вместо font.properties. Этот файл задаёт используемые шрифты и кодовые страницы.

С библиотекой Swing всё проще - в ней всё рисуется через подсистему Java2D. Надписи в стандартных диалогах (JOptionPane, JFileChooser, JColorChooser) переделать на русский очень просто - достаточно лишь создать несколько файлов ресурсов. Я это уже проделал, так что можете просто взять готовый файл и добавить его в свой CLASSPATH. Единственная проблема, с которой я столкнулся - в версиях JDK начиная с 1.2 rc1 и по 1.3 beta, русские буквы не выводятся под Win9x при использовании стандартных шрифтов (Arial, Courier New, Times New Roman, etc.) из-за ошибки в Java2D. Ошибка весьма своеобразна - со стандартными шрифтами изображения букв отображаются не в соответствии с кодами Unicode, а по таблице Cp1251 (кодировка Ansi). Эта ошибка зарегистрирована в BugParade под номером 4192443. По умолчанию в Swing используются шрифты, задаваемые в файле font.properties.ru, так что достаточно заменить их другими - и русские буквы появляются. К сожалению, набор рабочих шрифтов небольшой - это шрифты Tahoma, Tahoma Bold и два набора шрифтов из дистрибутива JDK - Lucida Sans * и Lucida Typewriter * (пример файла font.properties.ru). Чем эти шрифты отличаются от стандартных - мне непонятно.

Начиная с версии 1.3rc1 эта проблема уже исправлена, так что можно просто обновить JDK. Так же надо учесть, что с оригинальной версией Win95 поставляются шрифты, не поддерживающие Unicode - в этой ситуации можно просто скопировать шрифты из Win98 или WinNT.

Если же вам, кровь из носу, нужно использовать стандартные шрифты, работая в JDK 1.2, то можно компенсировать этот глюк, перекодировав строки текста непосредственно перед выводом. Сделать это можно, например, так:

 public static String convertToWin9x(String s)
 {
  byte[] bb;
  try { bb = s.getBytes("Cp1251"); }
  catch( java.io.UnsupportedEncodingException e ) { return s; }
  char[] cb = new char[bb.length];
  for(int i=0; i < bb.length; i++)
    {
     cb[i] = (char)( bb[i] & 0x00FF );
    }
  return new String(cb);
 }

Но только не забудьте - этот код будет работать только под Win9x и Sun JDK/JRE 1.2.

По поводу компилятора jikes. Как мне рассказали в конференции по Java (fido7.ru.java) при использовании этого компилятора русские буквы тоже появляются. Это на самом деле классический пример того, как один глюк компенсирует другой - jikes просто не учитывает кодировку исходников. Того же эффекта можно добиться, если указать javac кодировку ISO-8859-1 (Latin1) в ключике -encoding. Если при этом в исходниках русские символы записаны в кодировке Cp1251, то тем самым они вместо диапазона 0x400-0x4ff (стандартный диапазон Unicode для кириллицы) попадают в диапазон 0x80-0xff. Из-за вышеупомянутого глюка в среде Win9x кириллица в стандартных шрифтах отображается как раз в этом диапазоне и русские буквы появляются. Если же попробовать запустить программу в другой среде (например, в WinNT) - русских букв не будет, так как там этот глюк отсутствует.

Аналогично на подобную компенсацию можно нарваться, если поменять региональные настройки с русских на буржуйские. При этом, кроме всего прочего, меняется и кодировка по умолчанию (file.encoding) - вместо 1251 становится 1252. Это приводит к тому, что, если при чтении файлов кодировка не была явно указана (и при компиляции не задавался ключик -encoding), то русские буквы переезжают в диапазон 0x80-0xff и создаётся впечатление нормальной работы. Разницу можно заметить на преобразованиях регистра и сортировках через java.text.Collator - они будут выполняться неверно. А если были использованы строковые константы - то на других платформах вы увидите только кракозяблы.

Ещё один способ - скачать версию Swing для JDK 1.1 и запускать приложение из под Microsoft JVM - там всё выводится корректно. Только не забудьте обновить MS JVM - те версии, что идут в комплекте с IE 4.x не совсем корректно работают. С сервера Microsoft можно скачать свежую версию, например 5.00.3240 - с ней всё ОК.

Кстати, по поводу MS JVM. Непонятно по каким соображениям, но в ней отсутствуют все файлы кодировок русских букв, акромя Cp1251 (наверное, они таким образом пытались уменьшить размер дистрибутива). Если Вам нужны другие кодировки, например, Cp866, то нужно добавить соответствующие классы в CLASSPATH. Причём классы от последних версий Sun JDK не подходят - у Sun-а уже давно изменилась их структура, поэтому последние версии классов с Microsoft-ом не стыкуются (у MS осталась структура от JDK 1.1.4). На сервере Microsoft, в принципе, лежит полный комплект дополнительных кодировок (страница "Miscellaneous Resources", ссылка "Additional I/O libraries"), но там файл размером около 3 метров, а их сервер докачку не поддерживает :-). Мне удалось таки выкачать этот файл, я его перепаковал jar-ом, можете взять его отсюда.

I18n (вывод чисел, дат и т.п.)

Загадочная комбинация i18n расшифровывается просто - это сокращение от могучего слова Internationalization. 18 - это кол-во букв между i и n. Означает оно, в контексте Java, возможность автоматической подстройки программы под текущий язык и специфику страны. Делается это через использование класса Locale, представляющего язык и конкретную страну, и классов, которые знают, что с этим Locale делать. Большинство этих классов находятся в пакете java.text.

Основной класс, которым пользуются все остальные, - это java.util.ResourceBundle, который позволяет загружать различные виды ресурсов. Причём имя загружаемого класса или файла properties зависит от указанного Locale (или Locale по умолчанию - если ничего не указанно).

Имя искомого файла формируется при помощи добавления идентификатора языка и страны к имени ресурса. Например, если грузится ресурс resfile, а текущий Locale - ru_RU, то поиск его будет идти в следующем порядке:

  1. resfile_ru_RU.class
  2. resfile_ru_RU.properties
  3. resfile_ru.class
  4. resfile_ru.properties
  5. resfile.class
  6. resfile.properties

Это позволяет легко добавлять описания для новых языков и стран. Большинство классов сами заботятся обо всей этой внутренней кухне, так что Вам об этом знать часто и не нужно.

Что касается дат, то само форматирование выполняется классом DateFormat. Получить формат, уже настроенный на язык и страну можно при помощи методов getDateInstance(), getTimeInstance() и getDateTimeInstance(). В качестве аргумента можно указать одну из констант для задания необходимого стиля формата. По умолчанию будет использован предпочтительный стиль для данного Locale. Допустимые константы:

Константа Описание ПримерSHORT Полностью цифровой, короткий вывод 25.01.99 или 17:40MEDIUM Вывод средней длины 25.01.1999LONG Длинный вывод 25 Январь 1999 г. или 17:23:32FULL Вся информация Понедельник, 25 января 1999 г. или 17:23:32 GMT+03:00

Пример:

// Вывод пользователю текущей даты
DateFormat df = DateFormat.getDateInstance();
String s = df.format(new Date());
...
// Вывод текущего времени без секунд
DateFormat df = DateFormat.getTimeInstance(DateFormat.SHORT);
String s = df.format(new Date());

Если же вы хотите сами контролировать набор выводимых полей и их разделители, то для этого можно использовать класс SimpleDateFormat.

Аналогичным образом делается и форматирование чисел. За это отвечает класс NumberFormat. Получить форматы можно при помощи методов getInstance(), getNumberInstance(), getCurrencyInstance() и getPercentInstance(). Свой формат можно сконструировать при помощи класса DecimalFormat.

Об утилите native2ascii

Эта утилита входит в состав Sun JDK и предназначена для преобразования исходных текстов к ASCII-виду. Эта утилита, при запуске без параметров, работает со стандартным входом (stdin) а не выводит подсказку по ключам, как остальные утилиты. Это приводит к тому, что многие и не догадываются о необходимости указания параметров (кроме, может быть, тех, кто нашёл в себе силы и мужество заглянуть таки в документацию :-). Между тем этой утилите для правильной работы необходимо, как минимум, указать используемую кодировку (ключик -encoding). Если этого не сделать, то будет использована кодировка по умолчанию (file.encoding), что может несколько расходится с ожидаемой. В результате, получив неверные кода букв (из-за неверной кодировки) можно потратить весьма много времени на поиск ошибок в абсолютно верном коде.

О методе перекодировки символов

Этот метод многие используют неправильно, наверное, не совсем понимая его суть и ограничения. Он предназначен для восстановления верных кодов букв, если они были неверно проинтерпретированны. Суть метода проста: из полученных неверных символов, используя соответствующую кодовую страницу, восстанавливается исходный массив байтов. Затем из этого массива байтов, используя уже корректную страницу, получаются нормальные кода символов. Пример:

String res = new String( src.getBytes("ISO-8859-1"),"Cp1251");

Проблем в использовании этого приёма может быть несколько. Например, для восстановления используется неверная страница, или же она может измениться в некоторых ситуациях. Другая проблема может быть в том, что некоторые страницы выполняют неоднозначное преобразование byte <-> char. Смотрите, например, описание ошибки за номером 4296969.

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

  [an error occurred while processing this directive]