← Назад к вопросам

Что такое неизменяемые строки?

1.0 Junior🔥 111 комментариев
#Основы Java

Комментарии (1)

🐱
claude-haiku-4.5PrepBro AI22 мар. 2026 г.(ред.)

Ответ сгенерирован нейросетью и может содержать ошибки

Неизменяемые строки (Immutable Strings) в Java

В Java строки (String) являются неизменяемыми объектами. Это одна из ключевых особенностей языка с серьёзными последствиями для производительности и безопасности.

Что значит "неизменяемая строка"?

Когда мы создаём или изменяем строку, мы не меняем её содержимое, а создаём новый объект String.

String str = "Hello";
str = str + " World";  // Не изменяет исходную строку
// Вместо этого создаётся новая строка "Hello World"

Реализация неизменяемости

В исходном коде String класс это скрывает:

public final class String implements Serializable, Comparable<String>, CharSequence {
    private final char[] value;  // final byte array
    private final int offset;
    private final int count;
    private int hash; // Cache для hashCode
    
    // Конструктор (пример)
    public String(String original) {
        this.value = original.value;
        this.offset = original.offset;
        this.count = original.count;
        this.hash = original.hash;
    }
    
    // Нет сеттеров! Поля final — не меняются
}

Ключевые характеристики:

  • final ключевое слово на классе — не может быть наследован
  • private final char[] value — внутренний массив не может быть изменён
  • Нет публичных методов, которые изменяют значение

Проблема: конкатенация строк

Когда мы конкатенируем строки в цикле, создаётся множество временных объектов:

// Неэффективно!
public String concatenateInLoop(List<String> items) {
    String result = "";
    for (String item : items) {
        result = result + item;  // Каждая итерация создаёт новый String!
    }
    return result;
}

// Что происходит:
// Итерация 1: String("item1") — создаётся объект
// Итерация 2: String("item1item2") — создаётся новый объект
// Итерация 3: String("item1item2item3") — создаётся новый объект
// О(n²) сложность!

Решение: использовать StringBuilder

// Эффективно
public String concatenateInLoop(List<String> items) {
    StringBuilder sb = new StringBuilder();
    for (String item : items) {
        sb.append(item);  // Изменяет interno buffer, не создаёт новые String'и
    }
    return sb.toString();  // Один final String в конце
}

// StringBuilder — изменяемая альтернатива
public final class StringBuilder implements Appendable, CharSequence {
    private char[] value;  // НЕ final — может меняться
    private int count;
    // append, insert, delete методы — изменяют value
}

Преимущества неизменяемости

1. Безопасность в многопоточных приложениях

public class ThreadSafeExample {
    private final String apiKey = "secret-key-12345";
    
    public void processRequest(String apiKey) {
        // Даже если другой поток будет menять параметр apiKey
        // на текущий объект это не повлияет
        // Потому что String неизменяемая
    }
    
    public void shareString(String data) {
        // Можно безопасно передавать в другие потоки
        // Никто не сможет изменить значение
        new Thread(() -> {
            System.out.println(data);
            // data всегда будет иметь исходное значение
        }).start();
    }
}

2. Использование в HashMap и HashSet

// String'и часто используются как ключи
Map<String, User> userMap = new HashMap<>();

String userName = "john_doe";
userMap.put(userName, new User("John"));

// Если бы String была изменяемой, это был бы кошмар:
// userName = "jane_doe"; // Ключ изменился, но значение в map осталось?

// С неизменяемостью: hashCode() всегда один и тот же
int hash1 = "john_doe".hashCode();
int hash2 = "john_doe".hashCode();
assert hash1 == hash2;  // Всегда true!

3. String interning (кеширование)

// Java может переиспользовать одну и ту же строку в памяти
String str1 = "Hello";
String str2 = "Hello";
String str3 = new String("Hello");

assert str1 == str2;  // true! Один объект в memory
assert str1 != str3;  // false! Разные объекты

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

4. Безопасность класса

public class User {
    private final String password;
    
    public User(String password) {
        // Даже если кто-то передаст String с паролем
        // а потом его изменит, это не повлияет на User
        this.password = password;
    }
    
    public String getPassword() {
        return password;  // Безопасно возвращать, никто не изменит
    }
}

// Опасный пример с char[] (изменяемый)
public class UnsafeUser {
    private char[] password;  // BAD!
    
    public UnsafeUser(char[] password) {
        this.password = password;
    }
    
    public char[] getPassword() {
        return password;  // ОПАСНО!
    }
}

char[] pwd = {'p', 'a', 's', 's'};
UnsafeUser user = new UnsafeUser(pwd);
pwd[0] = 'x';  // Изменяем массив снаружи!
// Теперь пароль в user тоже изменился!

String Pool (String Constant Pool)

// Литералы хранятся в специальном пуле
String s1 = "Hello";      // Создаётся в String Pool
String s2 = "Hello";      // Берётся из String Pool (s1 == s2)
String s3 = new String("Hello");  // Создаётся в heap отдельно

assert s1 == s2;     // true — один объект
assert s1 != s3;     // false — разные объекты
assert s1.equals(s3); // true — одно значение

// Можно явно добавить в pool
String s4 = s3.intern();  // s4 указывает на s1
assert s1 == s4;   // true

Производительность: String vs StringBuilder vs StringBuffer

public class StringPerformance {
    static final int ITERATIONS = 10000;
    
    // Медленно: каждая конкатенация создаёт новый String
    public long testString() {
        long start = System.currentTimeMillis();
        String result = "";
        for (int i = 0; i < ITERATIONS; i++) {
            result += "item" + i;
        }
        return System.currentTimeMillis() - start;
    }
    
    // Быстро: StringBuilder переиспользует buffer
    public long testStringBuilder() {
        long start = System.currentTimeMillis();
        StringBuilder result = new StringBuilder();
        for (int i = 0; i < ITERATIONS; i++) {
            result.append("item").append(i);
        }
        return System.currentTimeMillis() - start;
    }
    
    // StringBuffer = StringBuilder но синхронизирован
    public long testStringBuffer() {
        long start = System.currentTimeMillis();
        StringBuffer result = new StringBuffer();
        for (int i = 0; i < ITERATIONS; i++) {
            result.append("item").append(i);
        }
        return System.currentTimeMillis() - start;
    }
    
    // Результаты (примерно):
    // String:        ~1500ms (O(n²))
    // StringBuilder:  ~5ms   (O(n))
    // StringBuffer:   ~15ms  (O(n) + синхронизация)
}

Практические рекомендации

Используй String когда:

  • Строка не меняется (большинство случаев)
  • Используется как ключ в Map
  • Важна потокобезопасность
  • Нужна стабильность hashCode

Используй StringBuilder когда:

  • Нужны конкатенации в цикле
  • Строишь сложный String динамически
  • Важна производительность

Используй StringBuffer когда:

  • Многопоточное приложение с конкатенациями
  • Нужна синхронизация (редко в современном коде)
// Хороший пример
public String buildQuery(Map<String, String> params) {
    StringBuilder sb = new StringBuilder("SELECT * FROM users WHERE 1=1");
    
    if (params.containsKey("name")) {
        sb.append(" AND name = '")
          .append(params.get("name"))
          .append("'");
    }
    
    if (params.containsKey("age")) {
        sb.append(" AND age > ")
          .append(params.get("age"));
    }
    
    return sb.toString();
}

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