Объекты Java

Класс и объект

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

final class Counter {
    private int value;
 
    void increment() {
        value++;
    }
 
    int value() {
        return value;
    }
}
 
Counter first = new Counter();
Counter second = new Counter();

first и second имеют одинаковый класс, но разную идентичность и независимое состояние.

У объекта удобно различать:

  • идентичность — это тот же экземпляр или другой;
  • состояние — значения полей;
  • поведение — доступные операции и поддерживаемые инварианты.

Создание

Выражение new выделяет объект, инициализирует его поля и вызывает конструктор:

Order order = new Order(id, items);

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

Ссылки и aliasing

List<String> left = new ArrayList<>();
List<String> right = left;
right.add("A");
 
System.out.println(left); // [A]

Две ссылки указывают на один mutable-объект — это aliasing. Он нужен, но усложняет рассуждение о состоянии. Защита границ объекта:

final class Team {
    private final List<String> members;
 
    Team(List<String> members) {
        this.members = List.copyOf(members);
    }
 
    List<String> members() {
        return members;
    }
}

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

Идентичность и равенство

== сравнивает ссылки. equals может задавать логическое равенство:

record UserId(long value) {}
 
UserId a = new UserId(7);
UserId b = new UserId(7);
 
a == b;      // false
a.equals(b); // true

Для entity идентичность обычно определяется стабильным идентификатором, для value object — совокупностью значимых полей. Контракт должен быть согласован с hashCode.

Жизненный цикл

Объект становится кандидатом на сборку мусора, когда до него нельзя добраться из GC roots по цепочке сильных ссылок. Сборка не означает немедленного удаления и не должна использоваться как механизм закрытия файлов, сокетов или соединений.

Ресурсный объект закрывают явно:

try (InputStream input = Files.newInputStream(path)) {
    return input.readAllBytes();
}

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

Неизменяемый объект не меняет наблюдаемое состояние после создания. Для этого обычно:

  • валидируют состояние в конструкторе;
  • не предоставляют mutator-методы;
  • не отдают mutable-внутренности;
  • делают defensive copy;
  • используют final для полей и, при необходимости, класса.

Один final не обеспечивает глубокую неизменяемость.

Ловушки

  • Путать ссылку и объект.
  • Ожидать копию объекта при обычном присваивании.
  • Сравнивать value objects через ==.
  • Отдавать наружу внутреннюю mutable-коллекцию.
  • Полагаться на сборщик мусора для закрытия ресурсов.
  • Вызывать переопределяемые методы из конструктора и публиковать частично созданный объект.

Вопросы на собеседовании

  1. Чем класс отличается от объекта?
  2. Что копируется при присваивании ссылочной переменной?
  3. Чем идентичность отличается от логического равенства?
  4. Когда объект становится достижимым для GC?
  5. Что нужно для практической неизменяемости класса?
  6. Почему final-поле со списком ещё не делает объект immutable?

Связанные заметки

Основы ООП · Класс Object · Конструкторы Java · Equals и hashCode · Immutable классы и данные · Garbage collector