2 июн. 2015 г.

Строки. Часть 2 – создание строк, класс String.

Создание строк класса String. Разница между оператором присваивания (=) и new.

Теперь рассмотрим подробнее создание строк в Java. Как я говорил в предыдущей статье есть два способа создания строки (объекта класса String):

  • при помощи оператора присваивания (=)
  • при помощи оператора new

Между этими двумя способами есть разница и она заключается в том, каким образом выделяется память для этих строк и как присваиваются ссылки на эти переменным класса String.

Для строк созданных при помощи оператора присваивания и строкового литерала в Java существует специальный механизм хранения в "отдельной" области памяти называемой строковый пул (string pool). До JDK 7 это были разные области памяти. Начиная с Oracle JDK 7, string pool хранится в общем heap'е.

Если существует два или более одинаковых строковых литерала, то для них в памяти выделается место только для одного (так как нет смысла хранить несколько одинаковых строк), но ссылки на этот объект могут быть присвоены любому количеству строковых переменных. Это позволяет уменьшить использование памяти виртуальной машины и в какой-то степени оптимизировать работу с часто используемыми строками (как вы помните строки в объектах класса String неизменяемые).

Когда же строка создается при помощи оператора new, то для каждой строки, даже если они одинаковые, выделяется своя область памяти в куче (heap), так как оператор new создает новый объект и выделяет место для него в памяти виртуальной машины.

Для того чтобы разобраться с этим получше рассмотрим простую программу:

Данная программа генерирует следующий вывод:

Str00002

Статический метод iHash это просто обертка для System.identityHashCode. Я его создал только для того, чтобы код стал более наглядным. Этот метод возвращает хэш объекта, который не следует путать с хэшем строки хранящейся в объекте, возвращает который метод hashCode.

И так мы видим что строки на которые ссылаются s1, s2 и s3 это один и тот же объект в памяти, что видно по его хэшу.  s1 и s2 созданы при помощи строковых литералов, которые у нас в данном случае одинаковые – это слово "Hello".  s3 создан при помощи присвоения ссылки и как видите, опять же, указывает на тот же объект в памяти.

А вот s4 и s5 хотя и создают строку с одинаковым содержанием, но ссылаются на разные объекты в памяти, то есть оператор new породил два новых объекта в памяти. s6 создан присвоением ссылки и поэтому указывает на тот же объект, что и s5, то есть новый объект не был создан.

Чтобы стало еще более понятно приведу следующую диаграмму:

Str00003

Важно понять, что оператор new порождает новые объекты в памяти в любом случае, есть там такая же строка или нет, в то время как оператор присваивания (=) создает новый объект в string pool, только в том случае если там нет такой же строки. Хотя при помощи метода intern() класса String можно сделать, что оператор new будет работать так же, как и оператор присваивания.

Строки с 12 по 30 создают в памяти строки показанные на рисунке и выводят их на консоль.

Далее в строке 32 присваиванием s3 = "world"; мы создаем новый обект в string pool, что собственно видно по выводу программы. И хотя строки относятся к ссылочным типам данных изменения строк s1 и s2 не произошло, а вместо этого был создан новый объект (строка).

Когда мы в 38 строке присваиваем s6 строку "Hello", то мы таким образом переопределяем эту ссылку так, что она начинает указывать на "Hello" в string pool. Создания нового объекта в памяти опять не произошло.

Вызов метода intern() для s4 в строке 43 организует поиск строки содержащейся в s4 в string pool и при положительном результате возвращает ссылку на найденную строку, а при отрицательном – заносит значение (строку) в пул и возвращает ссылку на него. В нашем случае s4 указывает на строку "Hello", которая так же уже есть в string pool. Поэтому просто произошло перенаправление ссылки, то есть s4 теперь ссылается на тот же объект (строку) что и s1 к примеру.

Далее в строке 48 мы сперва создали строковый литерал «World» и сразу же вывели его на консоль, не присваивая ссылку на эту строку ни какой переменной, и все же, даже не смотря на это строка была размещена в пуле. Затем мы эту же строку присвоили переменной s7 и как видите это один и тот же объект, что мы выводили на консоль (сравните хэши объектов).

Затем в строке 53 мы изменили поведение оператора new, т.е. не был создан новый объект (строка) в следствии применения метода intern(). Метод intern() перед созданием объекта String смотрит есть ли этот объект в string pool и возвращает его. Иначе создается новый объект в string pool.

Ну и в конце у нас небольшой простенький и наивный тест производительности создания большой строки расположенной в string pool и heap. Как видно string pool работает помедленней чем heap. Но это на разных машинах может быть по разному и бенчмарки лучше делать несколько по другому, например использовать инструмент JMH: http://openjdk.java.net/projects/code-tools/jmh/

Вообще так, в цикле, делать конкатенацию строк категорически не рекомендуется, так как это порождает множество объектов в памяти, что очень не очень. Данный пример был приведен просто чтобы сравнить производительность.

Создание строк при помощи оператора new.

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

  • String() — создается объект с пустой строкой;
  • String(String str) — конструктор копирования: из одного объекта создается его точная копия, поэтому данный конструктор используется редко;
  • String(StringBuffer str) — преобразованная копия объекта класса StringBuffer;
  • String(StringBuilder str) — преобразованная копия объекта класса StringBuilder;
  • String(byte[] byteArray) — объект создается из массива байтов byteArray;
  • String(char[] charArray) — объект создается из массива charArray символов Unicode;
  • String(byte[] byteArray, int offset, int count) — объект создается из части массива байтов byteArray, начинающейся с индекса offset и содержащей count байтов;
  • String(char[] charArray, int offset, int count) — то же, но массив состоит из символов Unicode;
  • String(int[] intArray, int offset, int count) — то же, но массив состоит из символов Unicode, записанных в массив целого типа, что позволяет использовать символы Unicode, занимающие больше двух байтов;
  • String(byte[] byteArray, String encoding) — символы, записанные в массиве байтов, задаются в Unicode-строке с учетом кодировки encoding;
  • String(byte[] byteArray, int offset, int count, String encoding) — то же самое, но только для части массива;
  • String(byte[] byteArray, Charset charset) — символы, записанные в массиве байтов, задаются в Unicode-строке с учетом кодировки, заданной объектом charset;
  • String(byte[] byteArray, int offset, int count, Charset charset) — то же самое, но только для части массива.

При неправильном задании индексов offset, count или кодировки encoding возникает исключительная ситуация.

Конструкторы, использующие массив байтов byteArray, предназначены для создания Unicode-строки из массива байтовых ASCII-кодировок символов. Такая ситуация возникает при чтении ASCII-файлов, извлечении информации из базы данных или при передаче информации по сети.

В самом простом случае компилятор для получения двухбайтовых символов Unicode добавит к каждому байту старший нулевой байт. Получится диапазон ‘ \u0000′ — ‘ \u00FF’ кодировки Unicode, соответствующий кодам Latin1. Тексты, записанные кириллицей, будут выведены неправильно.

Если же на компьютере сделаны местные установки, как говорят на жаргоне «установлена локаль» (locale) (в MS Windows это выполняется утилитой Regional Options (Язык и стандарты) в окне Control Panel (Панель управления)), то компилятор, прочитав эти установки, создаст символы Unicode, соответствующие местной кодовой странице. В русифицированном варианте MS Windows это обычно кодовая страница CP1251.

Если исходный массив с кириллическим ASCII-текстом был в кодировке CP1251, то строка Java будет создана правильно. Кириллица попадет в свой диапазон ‘ \u0400′ — ‘ \u04FF’ кодировки Unicode.

Но у кириллицы есть еще по меньшей мере четыре кодировки:

  • в MS-DOS применяется кодировка CP866;
  • в UNIX обычно применяется кодировка KOI8-R;
  • на компьютерах Apple Macintosh используется кодировка MacCyrillic;
  • есть еще и международная кодировка кириллицы ISO8859-5.

Например, байт 11100011 (0xE3 — в шестнадцатеричной форме) в кодировке CP1251 представляет кириллическую букву Г, в кодировке CP866 — букву у, в кодировке KOI8-R — букву Ц, в ISO8859-5 — букву у, в MacCyrillic — букву г. Если исходный кириллический ASCII-текст был в одной из этих кодировок, а местная кодировка — CP1251, то Unicode-символы строки Java не будут соответствовать кириллице.

В этих случаях применяются последние четыре конструктора, в которых параметром encoding или charset указывается, какую кодовую таблицу использовать конструктору при создании строки.

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

Эта программа генерирует следующий вывод:

Str00004

Строка в кодировке 1251 в обоих случаях выводится правильно, так как в первом случае компилятор использовал кодировку ОС Windows, а во втором мы ему указали ее явно.

Строки же созданные без указания кодировки и содержащие слово "Россия" в кодировках отличных от 1251 вывелись не правильно, так как компилятор преобразовал их в строки Unicode, считая что это кодировка 1251, хотя это было не так. Собственно поэтому они вывелись не правильно. Если до сих пор не понятно почему это так, то рекомендую освежить в памяти эту статью.

В случае же когда мы указали кодировку все строки вывелись правильно.

Для пущего понимания рекомендую, так же, освежить в памяти и эту статью. Тем более она нам сейчас пригодится.

Далее рассмотрим создание строки из массива символов char. Хотя мы это уже делали несколько раз, но повторенье – мать ученья.

Str00005

Эта простая программа генерирует следующий вывод:

Str00006

Строка s1 была создана из массива char c, а строка s2 из части этого массива.

При создании строки использовался синтаксис указания первого символа (offset) с которого надо начать создавать строку и затем указано количество символов (count) которые надо скопировать, включая начальный и конечный.

А теперь перейдем к самому интересному, созданию строк, которые состоят из символов расширенного Unicode.

Напомню, что примитивный тип char имеет размер два байта. И этими двумя байтами можно представить 65536 символов.

Для того чтобы понимать тип char, надо иметь представление о принципах кодировки Unicode. Кодировка Unicode была изобретена для преодоления ограничений традиционных символьных схем. До появления Unicode существовало несколько различных стандартов: ASCII, ISO 8859-1, KOI-8, GB18030, BIG-5 и т.д. При этом возникали две проблемы. Во-первых, один и тот же код в разных кодировках соответствовал различным символам. Во-вторых, в языках с большим набором символов использовался код различной длины: часто употребляющиеся символы представлялись одним байтом, другие знаки — двумя, тремя и большим количеством байтов.

Для решения этих проблем была разработана кодировка Unicode. В результате исследований, начавшихся в 80-х годах, выяснилось, что двухбайтового кода более чем достаточно для представления всех символов, использующихся во всех языках; при этом оставался достаточный резерв для любых мыслимых расширений. В 1991 г. была выпущена спецификация Unicode 1.0, в которой было использовано меньше половины из возможных 65536 кодов. В Java изначально были приняты 16-битовые символы Unicode, что стало еще одним преимуществом перед другими языками, использующими 8-битовые символы.

Однако впоследствии случилось непредвиденное: количество символов превысило допустимые 65536. Причиной тому стали чрезвычайно большие наборы иероглифов китайского, японского и корейского языков. Поэтому в настоящее время 16-битового типа char недостаточно для описания всех символов Unicode.

Чтобы понять, как эта проблема решается в Java, начиная с Java SE 5.0, надо ввести несколько терминов. Назовем кодовой точкой (code point) значение, связанное с символом в схеме кодирования. Согласно стандарту Unicode, кодовые точки записываются в шестнадцатеричном формате и предваряются символами U+. Например, для буквы A кодовая точка равна U+0041. В Unicode кодовые точки объединяются в 17 кодовых плоскостей (code plane). Первая кодовая плоскость, называемая основной многоязыковой плоскостью (basic multilingual plane), состоит из “классических” символов Unicode с кодовыми точками от U+0000 до U+FFFF. Шестнадцать дополнительных плоскостей с кодовыми точками от U+10000 до U+10FFFF содержат дополнительные символы (supplementary character). Если сказать по простому, то кодовая точка (code point) представляет один символ в расширенной кодировке Unicode.

Кодировка UTF-16 — это способ представления всех кодов Unicode последовательностью переменной длины. Символы из основной многоязыковой плоскости представляются 16-битовыми значениями, называемыми кодовыми единицами (code unit). Дополнительные символы обозначаются последовательными парами кодовых единиц. Каждое из значений пары попадает на используемую 2048-байтовую область основной многоязыковой плоскости, называемой областью подстановки (surrogates area); от U+D800 до U+DBFF для первой кодовой единицы и от U+DC00 до U+DFFF для второй кодовой единицы. Такой подход позволяет сразу определить, соответствует ли значение коду конкретного символа или является ли частью кода дополнительного символа. Например, математическому коду символов, обозначающему множество целых чисел, соответствует кодовая точка U+1D56B и две кодовых единицы, U+D835 и U+DD6B (описание алгоритма кодирования можно найти по адресу http://en.wikipedia.org/wiki/UTF-16 и на русском https://ru.wikipedia.org/wiki/UTF-16).

Собственно от сюда и пошло понятие суррогатных пар в Java, которыми представляются символы расширенного Unicode. А теперь потанцуем попрактикуемся :)

Str00008

Теперь мы на практике реализовали то, что было описано в пяти абзацах выше.

Данная программа генерирует следующий вывод:

Str00009

Обратите внимание, что для того чтобы увидеть это у вас консоль должна поддерживать Unicode, а так же должна быть переключена в эту кодировку. Ну и кроме того надо при запуске использовать ключ -Dfile.encoding=UTF-8.

В первых двух строках мы задаем парой кодовых единиц (суррогатной парой)код математического символа, обозначающего множество целых чисел (Z). Затем мы по отдельности выводим эти символы на консоль, но сами по себе они не представляют ни какого символа, а только вместе. Поэтому для того чтобы вывести этот символ, эту сладкую парочку надо преобразовать кодовую точку в массиве типа int, так как int в два раза больше char, и уж затем, этот массив загнать в строку :). Да вот такой кордебалет :). Чтобы стало все чуть более понятно посмотрите сюда:

Str00007

Далее в программе, просто для примера, я создал еще строчку из символов планет и вывел ее на консоль.

Ну вот мы рассмотрели создание строк из массива byte, char и int. Остались только StringBuffer и StringBuilder, но с ними все просто, хотя для порядка мы рассмотрим и их тоже, но чуть позже.

Создание строк на основе различных типов.

Примитивные типы и объекты в Java могут быть превращены в строки. Какие строки будут для примитивных типов это достаточно очевидно, а вот для объектов это находится под контролем самих объектов. Мы можем получить строковое представление примитивного типа или объекта при помощи статического метода String.valueOf(). Например:

String iS = String.valueOf(1);      // строка из int
String dS = String.valueOf(2.22); // строка из double
String fS = String.valueOf(3.33f); // строка из foat
String bS = String.valueOf(true); // строка из boolean

Все объекты в Java имеют метод toString(), который наследуется от класса Object. Для некоторых объектов этот метод возвращает полезный результат, который показывает содержимое объекта. Для объектов не предоставляющих такой результат, использование их в качестве аргумента метода valueOf() даст строку, которая просто является уникальным идентификатором этого объекта, которую можно использовать для отладки. Метод String.valueOf() при вызове для объекта вызывает метод toString() этого объекта и возвращает результат. Одним из реальных отличий использования этого метода является то, что если вы передадите в него объект с нулевой ссылкой, то он вернет вам "null" (строку)  класса String, вместо создания исключения NullPointerExeption при использовании метода toString() для этого объекта.

Конкатенация (сцепление) строк, так же, не явно, использует метод valueOf(), поэтому вы можете "добавлять" к строке объект или примитив:

Date today = new Date();
System.out.print("Сегодня: "+ today);

Иногда вы можете увидеть, как некоторые программисты, для сокращения, используют пустую строку и оператор сложения (+) для получения строкового значения объекта:

String st = ""+2.22;

Ну и немножко практики, дабы все стало понятнее.

Str00010

Данная программа генерирует следующий вывод:

Str00011

Она достаточно простая и я думаю не требует особых объяснений.

Единственное на что можно обратить внимание так это на то, каким образом выводятся ссылки на массивы.

А так же на вывод ссылки символьного массива и его содержимого.

Ну и еще рекомендую посмотреть исходный код этих методов класса String.

На этом создание строк класса String закончим.

3 комментария:

  1. Я так понимаю, что в статье не хватает первой картинки с программой.
    А так просто супер! Спасибо большое за труд!

    ОтветитьУдалить
    Ответы
    1. ```import static pro.java.util.Print.*;
      import static pro.java.util.Sys.*;

      public class String002 {

      private static final int SIZE_OF_STRING = 10_000;
      private static long time;

      public static void main(String[] args) {
      // пример создания строк в Java

      String s1 = "Hello"; // Строковый литерал
      String s2 = "Hello"; // Строковый литерал
      String s3 = s1; // одинаковые ссылки
      String s4 = new String("Hello"); // Строковый объект
      String s5 = new String("Hello"); // Строковый объект
      String s6 = s5; // одинаковые ссылки

      println("хэш объекта s1 = " + iHash(s1) + " хэш строки " + s1 + " = "
      + s1.hashCode());
      println("хэш объекта s2 = " + iHash(s2) + " хэш строки " + s2 + " = "
      + s2.hashCode());
      println("хэш объекта s3 = " + iHash(s3) + " хэш строки " + s3 + " = "
      + s3.hashCode());
      println("хэш объекта s4 = " + iHash(s4) + " хэш строки " + s4 + " = "
      + s4.hashCode());
      println("хэш объекта s5 = " + iHash(s5) + " хэш строки " + s5 + " = "
      + s5.hashCode());
      println("хэш объекта s6 = " + iHash(s6) + " хэш строки " + s6 + " = "
      + s6.hashCode());

      s3 = "world"; // создался новый объект в string pool
      println("\nстрока s1 = "+s1); // строка s1 осталась неизмененной
      println("хэш объекта s3 = " + iHash(s3) + " строка = " + s3);

      println();

      s6 = "Hello"; // ссылка на существующий объект в string pool
      println("хэш объекта s6 = " + iHash(s6) + " строка = " + s6);

      println();

      s4 = s4.intern(); //ищется ссылка на существующий объект в string pool
      println("хэш объекта s4 = " + iHash(s4) + " строка = " + s4);

      println();

      println("World" + " = " + System.identityHashCode("World"));
      String s7 = "World";
      println("s7 = " + System.identityHashCode(s7));

      // метод intern() ищет есть ли создаваемая строка в пуле
      String s8 = new String("World").intern();
      println("s8 = " + System.identityHashCode(s8));

      String sL = "";
      String sO = new String();

      print("\nНа создание строки sL ушло ");
      startBenchmark();
      for (int i = 0; i < SIZE_OF_STRING; ++i)
      sL += "1";
      stopBenchmark();

      print("\nНа создание строки sO ушло ");
      startBenchmark();
      for (int i = 0; i < SIZE_OF_STRING; ++i)
      sO += "1";
      stopBenchmark();

      }

      private static void startBenchmark() {
      time = System.currentTimeMillis();
      }

      private static void stopBenchmark() {
      time = System.currentTimeMillis() - time;
      println(time + "мс");
      }

      }
      ```

      Удалить