(проект лабораторной, по итогам принятия лабораторной 2 может меняться) Лабораторная работа № 3 — «Наследование, интерфейсы и полиморфизм»
Здесь происходит ключевой переход: в первой лабораторной студент видел готовую иерархию Delivery; теперь он должен самостоятельно понять, когда и зачем строить иерархию.
Класс предыдущей работы рефакторится в:
Transaction
▲
┌───────────┼───────────┐
│ │ │
IncomeTransaction ExpenseTransaction TransferTransaction
interfaces
│
Commissionable
Базовый класс:
public abstract class Transaction { private final UUID id; private final LocalDate date; private final BigDecimal amount; protected Transaction( UUID id, LocalDate date, BigDecimal amount ) { // validation } public final UUID getId() { return id; } public final BigDecimal getAmount() { return amount; } public abstract BigDecimal balanceImpact(); }
Конкретные классы должны самостоятельно реализовать balanceImpact():
доход → +amount расход → -amount перевод → поведение определяется стороной операции
Через массив или обычный цикл, без instanceof и без switch по типу:
Transaction[] transactions = { ... };
for (Transaction transaction : transactions) {
total = total.add(transaction.balanceImpact());
}
студент должен увидеть позднее связывание в коде, который уже написал сам. Java позволяет классу иметь один непосредственный суперкласс, а интерфейсы используются как отдельные контракты типа; это хорошо подходит для явного сравнения «abstract class vs interface».
Для интерфейса можно использовать:
public interface Commissionable { BigDecimal commission(); }
и реализовать его только там, где комиссия действительно существует. Это даёт естественный вопрос: почему Commissionable не нужно делать родителем всей иерархии?
Вторая часть — overloading vs overriding. Студент должен показать в одном проекте:
// overloading print(Transaction transaction) print(Transaction[] transactions) // overriding @Override public BigDecimal balanceImpact()
и объяснить, какое решение принимает компилятор, а какое определяется фактическим типом объекта во время выполнения.
Третья часть — Object. В Java Object является корнем иерархии классов. Для Transaction требуется реализовать:
@Override public boolean equals(Object obj) { ... } @Override public int hashCode() { ... } @Override public String toString() { ... }
Рекомендуемое правило лабораторной: идентичность транзакции определяется UUID id. Затем студент проверяет:
t1.equals(t2) t1.hashCode() == t2.hashCode()
и отвечает, почему при переопределении equals() необходимо согласованно реализовывать hashCode(). Контракт Java прямо требует, чтобы равные согласно equals() объекты имели одинаковый hash code.
Индивидуальное расширение: добавить новый подтип транзакции из варианта преподавателя — CashbackTransaction, InterestTransaction, TaxTransaction, RefundTransaction — не изменяя полиморфный алгоритм вычисления общего влияния на баланс.
Запрещено: if (transaction instanceof IncomeTransaction), поле type для выбора поведения, switch по имени класса.
Автопроверка: вызов balanceImpact() для каждого подкласса; полиморфный расчёт; корректность equals/hashCode; невозможность прямого создания new Transaction(...); проверка новой реализации без изменения агрегирующего алгоритма.
Защита: extends; super; перегрузка vs переопределение; @Override; статический и динамический тип ссылки; late binding; abstract class vs interface; почему Java не требует, чтобы каждый общий контракт был абстрактным классом; роль Object; назначение toString, equals, hashCode.
Вопросы для углубления: Что такое наследование и как используется
extends?Чем отличается перегрузка от
переопределения методов?Что такое полиморфизм
времени выполнения и позднее связывание?
Абстрактные классы и интерфейсы: в чем
различия и когда что применять?Какую роль
играет класс Object и методы toString(), equals(),
hashCode()?
