ООП на собесе проверяют не определениями, а последствиями: что вызовется при перегрузке, почему объект «потерялся» в 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?
- Почему композицию часто предпочитают наследованию?