Что такое immutable (неизменяемые) классы в Java?

Ответ

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

Признаки immutable класса:

  1. Все поля объявлены как final
  2. Класс объявлен как final (запрещает наследование)
  3. Отсутствуют сеттеры (setter methods)
  4. Поля инициализируются только в конструкторе
  5. Методы не изменяют состояние объекта, а возвращают новые экземпляры
  6. Защита от изменения mutable объектов (если они есть в полях)

Пример реализации:

public final class ImmutablePerson {
    private final String name;
    private final int age;
    private final List<String> hobbies;

    public ImmutablePerson(String name, int age, List<String> hobbies) {
        this.name = name;
        this.age = age;
        // Защитная копия для mutable поля
        this.hobbies = new ArrayList<>(hobbies);
    }

    public String getName() { return name; }
    public int getAge() { return age; }

    // Возвращаем защитную копию, а не оригинальный список
    public List<String> getHobbies() {
        return new ArrayList<>(hobbies);
    }

    // Метод для "изменения" возвращает новый объект
    public ImmutablePerson withAge(int newAge) {
        return new ImmutablePerson(this.name, newAge, this.hobbies);
    }
}

Преимущества:

  • Потокобезопасность — объекты можно безопасно использовать в многопоточных средах без синхронизации
  • Простота тестирования — состояние объекта предсказуемо
  • Безопасное кэширование — объекты можно кэшировать, так как они не изменятся
  • Упрощённый код — отсутствуют побочные эффекты от изменения состояния

Типичные примеры в Java: String, Integer, LocalDate, BigDecimal

Ответ 18+ 🔞

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

Представь себе, блядь, кирпич. Ну, обычный кирпич. Ты его слепил, обжёг — и всё, пиздец. Он уже кирпич. Ты его не можешь взять и пальцем в нём дырку просверлить, чтобы он стал пористее. Хочешь другой кирпич — лепи и жги новый. Вот immutable объект — это такой программный кирпич, ёпта. Создали — и он застыл, как мумия. Ни тебе сеттеров, ни изменения полей, ни хуя.

Как его, этого мутанта, опознать, блядь?

  1. Все поля — final. То есть после того, как их в конструкторе запихнули, они там и остаются до скончания времён. Попробуй изменить — компилятор тебе ебальник набьёт.
  2. Сам класс — final. Чтобы какой-нибудь хитрожопый наследник не пришёл и не наделал делов, переопределив методы так, что они начнут менять состояние. Нахуй такие сюрпризы.
  3. Сеттеров — ноль. Вообще. Ни одного. Если видишь setName() — это уже не immutable, а просто какая-то шлюха mutable.
  4. Всё инициализируется в конструкторе. Один раз и навсегда. Как в ЗАГСе, только без разводов.
  5. Методы — святые. Они не портят объект. Если нужно что-то «поменять» — они тебе новый объект отштампуют и отдадут. Старый останется лежать, как в мавзолее.
  6. Самое важное, блядь! Если внутри класса есть какое-то изменяемое поле, например, List или Map — ты должен его защитить, как зеницу ока. В конструкторе делаешь копию, в геттере тоже возвращаешь копию. Иначе какой-то ушлый пидорас снаружи получит ссылку на твой внутренний список и начнёт там хуйню творить, а твой «неизменяемый» объект будет тихо срать кирпичи.

Вот, смотри, как это выглядит в коде, сука:

public final class ImmutablePerson {
    private final String name;
    private final int age;
    private final List<String> hobbies;

    public ImmutablePerson(String name, int age, List<String> hobbies) {
        this.name = name;
        this.age = age;
        // Вот тут, блядь, ключевой момент! Не присваиваем список как есть!
        // Делаем защитную копию, чтобы если нам передали список, а потом его изменили — у нас внутри всё осталось как было.
        this.hobbies = new ArrayList<>(hobbies);
    }

    public String getName() { return name; }
    public int getAge() { return age; }

    // И тут тоже! Не возвращаем ссылку на свой внутренний список, а новую копию.
    // Иначе получивший её чувак начнёт в нём ковыряться.
    public List<String> getHobbies() {
        return new ArrayList<>(hobbies);
    }

    // Хочешь «изменить» возраст? Блядь, никаких изменений!
    // Вот тебе новый объект со старым именем, новым возрастом и старыми же увлечениями.
    public ImmutablePerson withAge(int newAge) {
        return new ImmutablePerson(this.name, newAge, this.hobbies);
    }
}

А нахуя это всё, спрашивается?

  • Потокобезопасность, ебать её в сраку! Объект, который не меняется, можно таскать по всем потокам, хоть тысяче. Никаких блокировок, синхронизаций, volatile — нихуя не нужно. Он же мёртвый уже. Его только читать.
  • Тестировать — одно удовольствие. Состояние известно раз и навсегда. Не будет такого, что ты передал объект в метод, а он тебе его там испоганил, и ты потом три дня ищешь, где же жопа.
  • Кэшировать можно на раз. Создал один раз — и клади хоть в статик, хоть в кэш приложения. Он не протухнет, не изменится. Как консервы.
  • Код проще. Нет этих вечных танцев с бубном: «а что если его изменили тут, а что если там». Создали — и забыли. Живёт себе.

В самой джаве этого добра — овердохуища: String, все обёртки типа Integer, LocalDate, BigDecimal. Они все такие, немые и неизменные. Попробуй изменить строку — нихуя, только новую получишь. Вот и вся философия.