Статьи

Неизменяемые объекты в Java

Неизменяемый объект — это объект, который не изменит своего внутреннего состояния после создания.

Неизменяемые объекты очень полезны в многопоточных приложениях, потому что они могут совместно использоваться потоками без синхронизации

Неизменяемые объекты всегда безопасны для потоков.

Темы везде

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

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

  • Веб-приложения, потому что сервлеты обрабатываются пулом потоков

  • Качайте настольные приложения, где потоки обрабатывают события GUI 

  • Экземпляры таймера создают новые потоки для обработки будущих задач

Создание неизменного объекта

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

  • Не добавляйте метод установки

  • Объявите все поля как окончательные и закрытые

  • Если поле является изменяемым объектом, создайте его защитные копии для методов получения

  • Если изменяемый объект, переданный конструктору, должен быть присвоен полю, создайте его защитную копию

  • Не позволяйте подклассам переопределять методы.

Теперь мы обнаруживаем, точка за точкой, причины пяти правил, объясненных ранее.

Не добавляйте метод установки

Если вы строите неизменный объект, его внутреннее состояние никогда не изменится. Задача метода-установщика состоит в том, чтобы изменить внутреннее значение поля, чтобы вы не могли его добавить.

Объявите все поля как окончательные и закрытые

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

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

public class DateContainer {
  private final Date date;

  public DateContainer() {
      this.date = new Date();
  }

  public Date getDate() {
    return date;
  }
}

....


  DateContainer dateContainer = new DateContainer();
  System.out.println(dateContainer.getDate());
  dateContainer.getDate().setTime(dateContainer.getDate().getTime() + 1000);
  System.out.println(dateContainer.getDate());
  // Now dateContainer date is 1 second after

Если поле является изменяемым объектом, создайте его защитные копии для методов получения

Ранее мы видели, что определения поля final и private недостаточно, потому что можно изменить его внутреннее состояние. Чтобы решить эту проблему, нам нужно создать защитную копию этого поля и возвращать это поле каждый раз, когда оно запрашивается.

Вот предыдущий класс с этой модификацией:

public class DateContainer {
  private final Date date;

  public DateContainer() {
      this.date = new Date();
  }

  public Date getDate() {
    return new Date(date.getTime());
  }
}

....


  DateContainer dateContainer = new DateContainer();
  System.out.println(dateContainer.getDate());
  dateContainer.getDate().setTime(dateContainer.getDate().getTime() + 1000);
  System.out.println(dateContainer.getDate());
  // Now dateContainer date is not changed because we changed the copy, 
  // not the original date

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

Та же проблема возникает, если вы держите ссылку, переданную конструктору, потому что ее можно изменить.

Здесь мы показываем модифицированный пример DateContainer, который принимает Date для конструктора, и мы увидим, как можно изменить его внутреннее состояние:

public class DateContainer {
  private final Date date;

  public DateContainer(Date date) {
      this.date = date;
  }

  public Date getDate() {
    return new Date(date.getTime());
  }
}

....

  Date date = new Date();
  DateContainer dateContainer = new DateContainer(date);
  System.out.println(dateContainer.getDate());
  date.setTime(date.getTime() + 1000);
  System.out.println(dateContainer.getDate());
  // Now dateContainer date is 1 second after also if the getter method
  // create a defensive copy of date. We changed the reference passed to the
  // constructor, not the copy.

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

public class DateContainer {
  private final Date date;

  public DateContainer(Date date) {
      this.date = new Date(date.getTime());
  }

  public Date getDate() {
    return new Date(date.getTime());
  }
}

....

  Date date = new Date();
  DateContainer dateContainer = new DateContainer(date);
  System.out.println(dateContainer.getDate());
  date.setTime(date.getTime() + 1000);
  System.out.println(dateContainer.getDate());
  // Now dateContainer date is not changed. We create a copy on the constructor
  // so a change to the external date will not affect the internal state of
  // DateContainer instance

Обратите внимание, что если поле является ссылкой на неизменный объект, нет необходимости создавать его защитные копии в конструкторе и в методах получения, достаточно определить поле как окончательное и закрытое. В качестве примера общих неизменяемых объектов можно привести String, все примитивные оболочки (Integer, Long, Double, Byte ….), BigDecimal, BigInteger.

Не позволяйте подклассам переопределять методы

Если подкласс переопределяет метод, он может вернуть исходное значение изменяемого поля вместо его защитной копии.

Для решения этой проблемы можно выполнить одно из следующих действий:

  • Объявите неизменный класс как final, чтобы он не мог быть расширен

  • Объявите все методы неизменяемого класса final, чтобы они не могли быть переопределены

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

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