Философия Java

         

Immutable строки


Ознакомьтесь со следующим кодом: 

//: Приложение А:Stringer.java

public class Stringer { static String upcase(String s) { return s.toUpperCase(); } public static void main(String[] args) { String q = new String("howdy"); System.out.println(q); // howdy

String qq = upcase(q); System.out.println(qq); // HOWDY

System.out.println(q); // howdy

} } ///:~

Когда q передается в качестве параметра методу upcase() на самом деле передается копия ссылки на q. Объект на который указывает эта ссылка физически не меняет своего положения. При передаче в качестве параметров копируются только сами ссылки.

Теперь посмотрим на содержание метода upcase(), как вы видите, ссылка полученная в качестве параметра носит имя s и существует только на время работы метода upcase(). Когда работа метода upcase() завершена, локальная ссылка уничтожается. upcase() возвращает результат - оригинальную строку с заменой всех прописных символов на заглавные. Разумеется, на самом деле возвращается лишь ссылка на этот результат. Но эта ссылка указывает на новый объект, а объект-оригинал q остается в одиночестве. Каким же образом это происходит?

Неявные константы

Записав:

String s = "asdf";

String x = Stringer.upcase(s);

действительно ли вы хотите чтобы uppercase() изменял параметр? Как правило нет, поскольку читающий код воспринимает параметр как информацию, передаваемую методу не предназначенную для модификации. Это важный момент для тех кто стремится сделать код своих программ более удобочитаемым.

В Си++ это сочли настолько важным, что ввели специальное ключевое слово const, дабы гарантировать программисту что ссылка (или, для Си++, указатель или ссылка) не могут быть использованы для модификации объекта-оригинала. Но это требует от программиста Си++ прилежности, чтобы он не забывал повсеместно вставлять const. Об этом легко забыть и это вносит лишнюю путаницу. 

Перегруженный '+' и StringBuffer

Объекты String созданы чтобы быть неизменными с применением технологии, рассмотренной выше. Если вы ознакомитесь с документацией по классу String (которая рассмотрена далее в этом приложении), вы увидите что все методы этого класса, которые изменяют объект String на самом деле лишь создают и возвращают абсолютно новый объект String содержащий изменения. При этом объект-оригинал String остается неизменным. В Java нет таких средств как const в Си++ для обеспечения неизменности объектов на уровне компиляции и если вы хотите то вам придется обеспечивать ее самостоятельно, как это реализовано в String.


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



Также представляется возможным обработка всех случаях, при которых вам необходимо вносить изменения в объект. С этой целью создается совершенно новый вариант объекта с уже внесенными изменениями, как это реализовано в String. Однако, в некоторых случаях это не эффективно. Примером является использование оператора '+', перегруженного для объектов String. Термин "перегруженный" означает что при использовании с классом определенного типа оператор выполняет специфические функции. (Операторы '+' и '+=' для String - единственные перегруженные операторы в Java и в Java программист не имеет возможности перегружать какие-либо иные операторы) [83]

Когда '+' используется с объектами String, он выполняет операцию объединения двух и более объектов String:

String s = "abc" + foo + "def" + Integer.toString(47);

Вы можете предположить то как это может работать: у объекта String "abc" есть метод append(), который создает объект String, содержащий "abc", объединенный с содержимым foo. Новый объект String в свою очередь создает новый объект String, в который добавляется "def" и так далее.

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



Решением является использование модифицируемого класса-компаньона, согласно рассмотренному ранее принципу. Для объекта String классом-компаньоном является StringBuffer, и компилятор автоматически создает StringBuffer для обработки некоторых выражений, в частности при использовании операторов '+' и '+=' применительно к объектам String. Вот пример того как это происходит:

//: Приложение А:ImmutableStrings.java

// Демонстрация StringBuffer.

public class ImmutableStrings { public static void main(String[] args) { String foo = "foo"; String s = "abc" + foo + "def" + Integer.toString(47); System.out.println(s); // "Равенство" с использованием StringBuffer:

StringBuffer sb = new StringBuffer("abc"); // Создает String!

sb.append(foo); sb.append("def"); // Создает String!

sb.append(Integer.toString(47)); System.out.println(sb); } } ///:~

При создании строки String s компилятор создает грубую копию последующего кода, который использует sb: создается StringBuffer и используется append() для добавления новых символов непосредственно в объект StringBuffer (это лучше чем каждый раз создавать новые копии) При том что это более эффективно, следует отметить что каждый раз при создании строк заключенных в кавычки, таких как "abc" или "def", компилятор превращает их в объекты String. Поэтому на самом деле создается больше объектов чем вам могло показаться, несмотря на эффективность StringBuffer.


Содержание раздела