Урок по треку Java 7 мин

ООП в Java и контракт equals/hashCode

Наследование, полиморфизм, перегрузка и переопределение, интерфейсы, контракт equals и hashCode.

ООП на собесе проверяют не определениями, а последствиями: что вызовется при перегрузке, почему объект «потерялся» в HashSet, чем абстрактный класс отличается от интерфейса в Java 21.

Четыре принципа коротко

  • Инкапсуляция: состояние закрыто (private), наружу только методы, которые держат инварианты.
  • Наследование: class B extends A, у класса один родитель, интерфейсов сколько угодно.
  • Полиморфизм: вызов animal.sound() выбирает реализацию по реальному классу объекта во время выполнения.
  • Абстракция: код зависит от интерфейса, а не от конкретной реализации.

Перегрузка и переопределение

Перегрузка (overload)Переопределение (override)
Гдев одном классе или иерархиив наследнике
Сигнатураразные параметрыта же сигнатура
Когда выбираетсякомпилятором, по объявленным типам аргументовв рантайме, по классу объекта

При переопределении можно сузить возвращаемый тип (ковариантность), расширить доступ (protected в public), но нельзя бросать более широкие checked-исключения. @Override не обязателен, но ловит опечатки в сигнатуре.

void print(Object o) { System.out.println("Object"); }
void print(String s) { System.out.println("String"); }

Object x = "hi";
print(x); // Object: перегрузка решается по объявленному типу

Абстрактный класс или интерфейс

С Java 8 у интерфейса есть default и static методы, с Java 9 private. Разница осталась в главном: интерфейс не хранит состояние объекта (только константы) и не имеет конструктора, а абстрактный класс может иметь поля, конструкторы и protected методы. Класс наследует один абстрактный класс, но реализует много интерфейсов. Если два интерфейса дают одинаковый default-метод, класс обязан переопределить его и может выбрать A.super.method().

Композиция вместо наследования

Наследование жёстко связывает классы: изменение родителя ломает детей, а наследник получает весь его API. Если отношение не «является», а «использует», берут поле и делегирование. Классический пример: Stack extends Vector в JDK, из-за чего у стека есть add(index, e).

Класс Object

Методы, которые есть у всех: equals, hashCode, toString, getClass, clone, wait/notify/notifyAll. finalize устарел и помечен к удалению: для освобождения ресурсов есть try-with-resources и Cleaner.

Контракт equals и hashCode

equals должен быть рефлексивным, симметричным, транзитивным, согласованным и давать false для null. Главное правило: равные по equals объекты обязаны иметь одинаковый hashCode. Обратное не требуется: коллизии допустимы.

public final class Point {
    private final int x, y;
    @Override public boolean equals(Object o) {
        if (this == o) return true;
        if (!(o instanceof Point p)) return false;
        return x == p.x && y == p.y;
    }
    @Override public int hashCode() { return Objects.hash(x, y); }
}

Что ломается:

  • Переопределили equals, забыли hashCode: два равных объекта попадают в разные корзины HashMap, set.contains возвращает false.
  • Изменили поле, участвующее в hashCode, после добавления в HashSet: объект «потерян», удалить его нельзя.
  • equals(Point p) вместо equals(Object o): это перегрузка, а не переопределение, коллекции её не вызовут.

Для классов-значений с Java 16 проще взять record: equals, hashCode и toString генерируются по компонентам.

Частые ошибки

  • Путать перегрузку с полиморфизмом и ждать выбор метода по реальному типу аргумента.
  • Делать изменяемые поля частью hashCode.
  • Наследоваться ради повторного использования кода, а не ради отношения «является».

Что спросят на собесе

  • Расскажите о принципах ООП на примере из своего кода.
  • Чем перегрузка отличается от переопределения, что выбирается в рантайме?
  • Контракт equals и hashCode, что сломается, если его нарушить?
  • Абстрактный класс или интерфейс: что выбрать в Java 21?
  • Почему композицию часто предпочитают наследованию?

Закрепите за 5 минут

После урока в тренажёре 3-5 проверочных вопросов, дальше модуль уходит в повторения.