2 июн. 2015 г.

Строки. Часть 1 – Введение.

Со строками мы столкнулись в самой первой программе "Hello World" и постоянно их затем использовали практически во всех программах и вот сейчас начнем с ними знакомиться поближе.

Без преувеличения можно сказать, что работа со строками является одной из самых распространённых задач в программировании, поэтому мы постараемся рассмотреть работу с ними как можно подробнее и глубже. Тем более, что сами разработчики Java тоже по считают работу со строками важным моментом и хотя в Java нет перегрузки операторов, верней разработчики посчитали что это является не нужным, но для строк они все же сделали исключение и ввели перегрузку оператора (+) который выполняет конкатенацию (сцепление) строк.

И начнем мы изучение строк с рассмотрения класса String (или если быть более точным java.lang.String). Работу со строками в Java кроме класса String реализуют еще и некоторые другие классы, например StringBuilder и StringBuffer, которые мы также рассмотрим.

Поскольку работа с классом String фундаментальна очень важно хорошо понимать как он устроен и что можно с ним делать.

Объект String является последовательностью символов Unicode в кодировке UTF-16. Символы в строках хранятся в кодировке Unicode, в которой каждый символ занимает два байта. Тип каждого символа — char. Эта последовательность может быть произвольной длины (по факту ограничена 2 миллиардами символов, что согласитесь тоже не мало).

Внутри класса String символы строки хранятся в простом массиве, но класс ревностно оберегает этот массив и доступ к нему возможен только через API класса, то есть через его методы. Это необходимо для поддержки идеи о том, что объекты класса String являются неизменяемыми (immutable). Если вы посмотрите код класса String в JDK, то увидите, что методы которые казалось бы изменяют объект String, в действительности создают и возвращают новый объект String с включенными изменениями. Хотя объекты класса String внутри него и размещены в массиве символов, сам объект String не является массивом Char[], хотя и есть методы конвертирования объекта String в массив Char[] и наоборот.

И так следует уяснить, что любая создаваемая строка в действительности представляет собой объект типа String. Даже строковые литералы в действительности являются объектами String. Например следующий код является вполне корректным:

int t = "123".length();

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

Кроме того, в данном примере следует обратить внимание что используется метод length(), который возвращает длину строки, в то время как с массивами используется поле lenght, в котором содержится длина массива и оно используется без скобок. Опять же пока это все может быть не понятно, так как мы еще не изучали классы, но просто стоит намотать себе это на ус :).

Что еще следует уяснить про строки так это то, что никаких нулевых символов в конце строки нет, длина хранится отдельно. Это так, к слову, для тех кто кодил на C/C++. Строки могут быть нулевой длины, то есть не содержать символов. Например:

String str="";

В данном случае длина строки будет равна нулю.

Хотя объект класса String внешне не является массивом, через методы класса можно работать с отдельными символами строки. Символы в строке нумеруются начиная с нуля. Что собственно и не удивительно, так как мы уже говорили, что внутри себя класс String хранит строку как массив символов.

Поскольку строки в Java являются объектами, то и создаваться они могут так же как объекты, то есть через оператор new. До этого мы все время создавали строковые переменные при помощи оператора присваивания (=), так как это самый короткий и понятный способ создания строковых переменных, но для улучшения понимания рассмотрим создание строк при помощи оператора new.

String s = new String("Пример создания строки");

Почти то же самое можно сделать более простым и уже известным нам способом:

String s = "Пример создания строки";

Между этими двумя способами создания строк есть разница, которую мы обсудим чуть позже. Этот пример я привел чтобы было более полное понимание того, как создаются строки и что они могут создаваться так же, как и объекты других классов – через оператор new, хотя для них в компиляторе Java существует более короткий синтаксис создания через оператор присваивания (=).

При помощи оператора new так же можно создать и  пустую строку, то есть строку нулевой длины:

String s = new String();

Теперь, обладая этим пониманием будет легко понять синтаксис создания строки из массива символов.

char charsArray[] = { 'a', 'b', 'c' };
String s = new String(charsArray);

В данном случае переменная экземпляра класса String s будет указывать на объект String со значением "abc".

Вы можете задать поддиапазон символьного массива в качестве инициализирующей строки с помощью следующего конструктора:

String(char chars[ ], int startIndex, int numChars)

Здесь startIndex указывает начало диапазона, а numChars — количество символов, которые нужно использовать. Вот пример:

char chars[] = { 'a', 'b', 'c', 'd', 'e', 'f' };
String s = new String(chars, 2, 3);

Это инициализирует строку s символами "cde". Это я просто для краткости так написал, и далее буду писать уже только так, но понимать под этом следует что переменной класса String с именем s присваивается ссылка на вновь созданный объект класса String.

Вы можете сконструировать объект String, который содержит ту же последовательность символов, что и другой объект String, используя конструктор:

String(String strObj)

Здесь strObj — объект String.  Например:

String str = "Эта строка является объектом";
String s = new String(str);

В данном случае переменная s будет равна "Эта строка является объектом". Но что самое важно это уже будет другой объект, то есть переменные str и s указывают на разные объекты (в данном случае строки) и изменение значения одной из них не повлияет на значение другой. По этому поводу следует напомнить о том, что мы сейчас имеем дело со ссылочными типами данных и что они передаются по ссылке. Это мы обсуждали в самом начале изучения Java.

Казалось бы что эти две строки можно было записать более коротким образом:

String str = "Эта строка является объектом";
String s = str;

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

Теперь разберемся почему же объекты класса String неизменяемые, почему так было сделано и как с этим жить :).

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

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

Хоть это и несколько неожиданно,  что когда вы создаете объект типа String, то вы создаете строку, которая не может быть изменена. То есть, как только объект String создан, вы не можете изменить символы, образующие строку. На первый взгляд это может показаться серьезным ограничением. Однако на самом деле это не так уж важно. Вы можете осуществлять любые операции над строками. Особенность в том, что всякий раз, когда вам нужна измененная версия существующей строки, создается новый объект String, включающий все модификации. Оригинальная строка остается неизменной. Этот подход используется потому, что фиксированная, неизменная строка может быть реализована более эффективно, нежели изменяемая. Неизменяемые строки имеют одно большое преимущество: компилятор может делать строки совместно используемыми.

Чтобы понять, как это работает, представьте, что в совместно используемом пуленаходятся разные строки. Строковые переменные указывают объекты в этом пуле. При копировании строки переменной и оригинал, и копия содержат одну и ту же последовательность символов. Логично не прибегать к дублированию строк, а поместить в переменные ссылки одну и ту же область памяти. Одним словом, разработчики языка Java решили, что эффективность совместного использования памяти перевешивает неэффективность редактирования строк путем выделения подстрок и конкатенации.

Для тех случаев, когда нужны модифицируемые строки, Java предлагает два выбора: StringBuffer и StringBuilder. Оба содержат строки, которые могут быть изменены после того, как созданы.

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

Зачем в язык введены три класса для хранения строк? В объектах класса String хранятся строки-константы неизменной длины и содержания, так сказать, отлитые в бронзе. Как уже говорилось ускоряет обработку строк и позволяет экономить память. Компилятор создает только один экземпляр строки класса String и направляет все ссылки на него. Длину строк, хранящихся в объектах классов StringBuilder и StringBuffer, можно менять, вставляя и добавляя строки и символы, удаляя подстроки или сцепляя несколько строк в одну. Во многих случаях, когда надо изменить длину строки типа String, компилятор Java неявно преобразует ее к типу StringBuilder или StringBuffer, меняет длину, потом преобразует обратно в тип String. Например, следующее действие:

String s = "Это" + " одна " + "строка";

компилятор выполнит примерно так:

String s = new StringBuffer().append("Это").append(" одна ").append("строка").toString();

Будет создан объект класса StringBuffer или класса StringBuilder, в него методом append() последовательно будут добавлены строки "Это", " одна ", "строка", и получившийся объект класса StringBuffer или StringBuilder будет приведен к типу String методом toString() . Постарайтесь понять и запомнить этот момент. Мы к нему еще вернемся когда будем более подробно рассматривать конкатенацию строк.

Классы String, StringBuffer и StringBuilder определены в пакете java.lang. Поэтому они доступны всем программистам автоматически. Все они объявлены с модификатором final, что означает, что ни от одного из них нельзя порождать подклассы. Это позволяет осуществить некоторую оптимизацию, которая повышает производительность общих операций со строками. Все три класса реализуют интерфейс CharSequence, в котором описаны общие методы работы со строками любого типа. Таких методов немного, приведем некоторые из них:

  • length() — возвращает количество символов в строке;
  • charAt(int pos) — возвращает символ, стоящий в позиции pos строки;
  • subSequence(int start, int end) — возвращает подстроку, начинающуюся с позиции start и заканчивающуюся перед позицией end исходной строки.

В Java 8 были добавлены еще два метода chars() и codePoints(), возможно мы их тоже как-нибудь обсудим, но не сейчас точно :).

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

Ну вот мы вкратце рассмотрели строки в Java, что это такое и с чем это едят. Теперь немного попрактикуемся, чтобы закрепить теорию на практике.

Str00000

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

Str000000

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

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

  1. "Но этот и предыдущий код не полностью эквиваленты. В чем разница между ними мы рассмотрим чуть позже, когда будем подробно разбирать класс String." - написано два раза.

    ОтветитьУдалить