Статья

Почему строки неизменяемы в Java?

В Java строки неизменяемы. Очевидный вопрос, который довольно часто встречается в интервью, звучит так: “Почему строки спроектированы как неизменяемые в Java?”

Джеймса Гослинга, создателя Java, однажды спросили в интервью, когда следует использовать неизменяемые объекты, на что он ответил: “Я бы использовал неизменяемые объекты всегда, когда это возможно”.

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

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

Что такое неизменяемый объект?

Неизменяемый объект – это объект, внутреннее состояние которого остается постоянным после того, как он был создан. Это означает, что как только объект был присвоен переменной, мы не можем ни обновить ссылку, ни каким-либо образом изменить внутреннее состояние.

Почему строки неизменяемы в Java?

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

Давайте обсудим, как это работает.

Знакомство с пулом строк

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

Java String Pool – это специальная область памяти, в которой JVM хранит строки. Поскольку строки в Java неизменяемы, JVM оптимизирует объем выделенной для них памяти, сохраняя в пуле только одну копию каждой строки литерала. Этот процесс называется интернированием:
String str1 = "Hello World";
String str2 = "Hello World";
         
// здесь str1 == str2
Из-за наличия пула строк в предыдущем примере две разные переменные указывают на один и тот же объект String из пула, тем самым экономя важный ресурс – память.

Безопасность

Строки широко используются в приложениях Java для хранения конфиденциальной информации, такой как имена пользователей, пароли, URL-адреса соединений, сетевые подключения и т.д. Строки также широко используются загрузчиками классов JVM при загрузке классов.

Следовательно, защита класса String имеет решающее значение для безопасности всего приложения в целом. Рассмотрим этот простой фрагмент кода:
void criticalMethod(String login) {
    // проверка безопасности

    if (!isAlphaNumeric(login)) {
        throw new SecurityException(); 
    }
	
    // выполняем второстепенные задачи...

    initDatabase();
	
    // важные задачи

    connection.executeUpdate("UPDATE Clients SET Status = 'Active' " +
      " WHERE login = '" + login + "'");
}
В приведенном выше фрагменте кода предположим, что мы получили объект String из ненадежного источника. Сначала мы выполняем все необходимые проверки безопасности, чтобы проверить, является ли строка только буквенно-цифровой, а затем выполняем еще несколько операций.
Помните, что наш ненадежный исходный вызывающий метод все еще имеет ссылку на объект login.
Если бы строки были изменяемыми, то к моменту выполнения обновления мы не могли быть уверены, что полученная нами строка, даже после выполнения проверок безопасности, будет безопасной. Ненадежный вызывающий метод по-прежнему имеет ссылку и может изменять строку между проверками целостности. Таким образом, в данном случае наш запрос подвержен SQL-инъекциям. Таким образом, изменяемые строки могут привести к ухудшению безопасности с течением времени.
Также может случиться так, что строка login будет видна другому потоку, который затем может изменить значение после проверки целостности.
В общем, нам на помощь в этом случае приходит неизменяемость, потому что легче работать с чувствительным кодом, когда значения не меняются, потому что существует меньше чередований операций, которые могут повлиять на результат.

Синхронизация

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

Кэширование хэш-кода

Строки широко используются в реализациях хэша, таких как HashMap, HashTable, HashSet и т.д. При работе с этими реализациями хэша довольно часто вызывается метод hashCode().
Неизменяемость строк гарантирует, что их значение не изменится. Таким образом, метод hashCode() переопределяется в классе String для облегчения кэширования, так что хэш вычисляется и кэшируется во время первого вызова hashCode(), и с тех пор возвращается одно и то же значение.
Это, в свою очередь, повышает производительность коллекций, использующих реализации хэша при работе со строковыми объектами.
С другой стороны, изменяемые строки будут генерировать два разных хэш-кода во время вставки и извлечения, если содержимое строки было изменено после операции, что может привести к потере объекта value в Map.

Производительность

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

Заключение

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