Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

1.10. Интерфейсы и абстрактные классы

interface, abstract class, default methods

Материалы

ТипСсылка
Документссылка
Видеоссылка

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

Добро пожаловать в третью часть ООП! Здесь мы изучим мощные инструменты абстракции: абстрактные классы для неполной реализации и интерфейсы для контрактов поведения.

Абстракция в программировании

Абстракция - это сокрытие деталей реализации и показ только важной информации. Это четвёртый столп ООП!

Аналогия из жизни

Когда вы водите машину:

  • Видите: руль, педали, приборную панель
  • НЕ видите: работу двигателя, трансмиссии, электроники

Абстракция скрывает “как это работает”, показывая только “что это делает”.

Зачем нужна абстракция?

1. Упрощение Работаем с простым интерфейсом, не вникая в детали.

2. Гибкость Можем менять реализацию, не трогая код, который её использует.

3. Организация Определяем общую структуру без конкретики.

Абстрактные классы

Абстрактный класс - это класс, который:

  • Не может быть создан напрямую (нельзя new AbstractClass())
  • Может содержать абстрактные методы (без реализации)
  • Может содержать обычные методы (с реализацией)
  • Служит шаблоном для подклассов

Синтаксис

public abstract class AbstractClassName {
    // Обычные поля
    private int field;
    
    // Конструктор
    public AbstractClassName() { }
    
    // Абстрактный метод (нет реализации!)
    public abstract void abstractMethod();
    
    // Обычный метод (есть реализация)
    public void normalMethod() {
        System.out.println("Обычный метод");
    }
}

Первый пример

// Абстрактный класс Shape
public abstract class Shape {
    protected String color;
    
    public Shape(String color) {
        this.color = color;
    }
    
    // Абстрактные методы - ОБЯЗАТЕЛЬНЫ к реализации
    public abstract double getArea();
    public abstract double getPerimeter();
    
    // Обычный метод - общий для всех фигур
    public void displayColor() {
        System.out.println("Цвет: " + color);
    }
    
    public void displayInfo() {
        System.out.println("Фигура цвета " + color);
        System.out.println("Площадь: " + getArea());
        System.out.println("Периметр: " + getPerimeter());
    }
}

// Конкретный класс Circle
public class Circle extends Shape {
    private double radius;
    
    public Circle(String color, double radius) {
        super(color);
        this.radius = radius;
    }
    
    // ОБЯЗАНЫ реализовать абстрактные методы!
    @Override
    public double getArea() {
        return Math.PI * radius * radius;
    }
    
    @Override
    public double getPerimeter() {
        return 2 * Math.PI * radius;
    }
}

// Конкретный класс Rectangle
public class Rectangle extends Shape {
    private double width;
    private double height;
    
    public Rectangle(String color, double width, double height) {
        super(color);
        this.width = width;
        this.height = height;
    }
    
    @Override
    public double getArea() {
        return width * height;
    }
    
    @Override
    public double getPerimeter() {
        return 2 * (width + height);
    }
}

// Использование
public class Main {
    public static void main(String[] args) {
        // Shape shape = new Shape("red");  // ОШИБКА! Абстрактный класс
        
        Shape circle = new Circle("красный", 5);
        Shape rectangle = new Rectangle("синий", 4, 6);
        
        circle.displayInfo();
        System.out.println();
        rectangle.displayInfo();
        
        // Полиморфизм!
        Shape[] shapes = {circle, rectangle};
        for (Shape shape : shapes) {
            System.out.println("Площадь: " + shape.getArea());
        }
    }
}

Вывод:

Фигура цвета красный
Площадь: 78.53981633974483
Периметр: 31.41592653589793

Фигура цвета синий
Площадь: 24.0
Периметр: 20.0

Площадь: 78.53981633974483
Площадь: 24.0

Правила абстрактных классов

✅ Могут:

  • Иметь абстрактные и обычные методы
  • Иметь поля (любые: public, private, protected)
  • Иметь конструкторы
  • Иметь static методы и поля
  • Иметь final методы

❌ Нельзя:

  • Создать экземпляр напрямую
  • Объявить abstract final (противоречие!)
  • Объявить abstract static (бессмысленно)

⚠️ Наследники должны:

  • Реализовать ВСЕ абстрактные методы
  • ИЛИ сами быть абстрактными
abstract class Parent {
    abstract void method1();
    abstract void method2();
}

// Вариант 1: Реализуем все методы
class Child1 extends Parent {
    @Override
    void method1() { }
    
    @Override
    void method2() { }
}

// Вариант 2: Не реализуем всё - класс тоже абстрактный
abstract class Child2 extends Parent {
    @Override
    void method1() { }
    
    // method2() не реализован - Child2 тоже abstract
}

Когда использовать абстрактные классы?

Используйте когда:

  • Есть общий код для подклассов
  • Нужны поля и конструкторы
  • Часть методов реализована, часть - нет
  • Классы тесно связаны (IS-A отношение)
// ✅ Хорошо - общая логика животных
abstract class Animal {
    protected String name;  // общее поле
    
    public Animal(String name) {  // конструктор
        this.name = name;
    }
    
    public void breathe() {  // общая реализация
        System.out.println(name + " дышит");
    }
    
    public abstract void makeSound();  // специфично для каждого
}

Интерфейсы

Интерфейс - это контракт, который определяет что класс должен уметь делать, но не как.

Базовый синтаксис

public interface InterfaceName {
    // Константы (public static final автоматически)
    int CONSTANT = 10;
    
    // Абстрактные методы (public abstract автоматически)
    void method1();
    int method2(String param);
    
    // Default методы (Java 8+)
    default void defaultMethod() {
        System.out.println("Default implementation");
    }
    
    // Static методы (Java 8+)
    static void staticMethod() {
        System.out.println("Static method");
    }
}

Первый пример

// Интерфейс - контракт поведения
public interface Flyable {
    // Абстрактные методы (public abstract автоматически)
    void fly();
    void land();
    
    // Константа (public static final автоматически)
    int MAX_ALTITUDE = 10000;
}

public interface Swimmable {
    void swim();
    void dive();
}

// Класс может реализовать НЕСКОЛЬКО интерфейсов!
public class Duck implements Flyable, Swimmable {
    private String name;
    
    public Duck(String name) {
        this.name = name;
    }
    
    // Реализуем методы Flyable
    @Override
    public void fly() {
        System.out.println(name + " летит на высоте " + Flyable.MAX_ALTITUDE);
    }
    
    @Override
    public void land() {
        System.out.println(name + " приземляется на воду");
    }
    
    // Реализуем методы Swimmable
    @Override
    public void swim() {
        System.out.println(name + " плывёт");
    }
    
    @Override
    public void dive() {
        System.out.println(name + " ныряет за рыбой");
    }
}

public class Airplane implements Flyable {
    private String model;
    
    public Airplane(String model) {
        this.model = model;
    }
    
    @Override
    public void fly() {
        System.out.println(model + " взлетает");
    }
    
    @Override
    public void land() {
        System.out.println(model + " садится на взлётную полосу");
    }
}

public class Fish implements Swimmable {
    private String species;
    
    public Fish(String species) {
        this.species = species;
    }
    
    @Override
    public void swim() {
        System.out.println(species + " плывёт в воде");
    }
    
    @Override
    public void dive() {
        System.out.println(species + " ныряет глубже");
    }
}

Использование

public class Main {
    public static void main(String[] args) {
        Duck duck = new Duck("Дональд");
        Airplane plane = new Airplane("Boeing 747");
        Fish fish = new Fish("Карп");
        
        System.out.println("=== Летающие ===");
        // Полиморфизм с интерфейсами!
        Flyable[] flyers = {duck, plane};
        for (Flyable flyer : flyers) {
            flyer.fly();
            flyer.land();
        }
        
        System.out.println("\n=== Плавающие ===");
        Swimmable[] swimmers = {duck, fish};
        for (Swimmable swimmer : swimmers) {
            swimmer.swim();
            swimmer.dive();
        }
    }
}

Вывод:

=== Летающие ===
Дональд летит на высоте 10000
Дональд приземляется на воду
Boeing 747 взлетает
Boeing 747 садится на взлётную полосу

=== Плавающие ===
Дональд плывёт
Дональд ныряет за рыбой
Карп плывёт в воде
Карп ныряет глубже

Множественная реализация

В отличие от классов (одиночное наследование), можно реализовать НЕСКОЛЬКО интерфейсов:

// ✅ OK - много интерфейсов
class Duck implements Flyable, Swimmable, Audible {
    // ...
}

// ❌ ОШИБКА - только один родительский класс
class C extends A, B {  // ОШИБКА!
}

// ✅ OK - один класс + много интерфейсов
class Duck extends Bird implements Flyable, Swimmable {
    // ...
}

Правила интерфейсов

Все поля:

  • Автоматически public static final (константы)
interface Constants {
    int MAX = 100;  // = public static final int MAX = 100;
}

Все методы (до Java 8):

  • Автоматически public abstract
interface Old {
    void method();  // = public abstract void method();
}

Java 8+ добавила:

  • Default методы (с реализацией)
  • Static методы

Java 9+ добавила:

  • Private методы (для default методов)

❌ Нельзя:

  • Создать экземпляр (new Interface())
  • Иметь конструкторы
  • Иметь instance поля (кроме констант)
  • Объявить final методы

Default методы (Java 8+)

Default методы позволяют добавлять новые методы в интерфейсы без поломки существующего кода.

Синтаксис

public interface Vehicle {
    // Обычные абстрактные методы
    void start();
    void stop();
    
    // Default метод - уже реализован!
    default void honk() {
        System.out.println("Бип-бип!");
    }
    
    default void displayInfo() {
        System.out.println("Это транспортное средство");
    }
}

public class Car implements Vehicle {
    @Override
    public void start() {
        System.out.println("Машина заводится");
    }
    
    @Override
    public void stop() {
        System.out.println("Машина останавливается");
    }
    
    // honk() уже реализован в интерфейсе!
    // Можем использовать как есть или переопределить:
    
    @Override
    public void honk() {
        System.out.println("Машина сигналит: БИП-БИП!");
    }
}

Car car = new Car();
car.start();         // Машина заводится
car.honk();          // Машина сигналит: БИП-БИП!
car.displayInfo();   // Это транспортное средство (из интерфейса)

Зачем нужны default методы?

1. Эволюция интерфейсов

Добавляем новый метод без поломки существующих реализаций:

// До Java 8
interface List<E> {
    void add(E element);
    E get(int index);
    // Всё, добавить метод = сломать все реализации
}

// Java 8+
interface List<E> {
    void add(E element);
    E get(int index);
    
    // Новый метод - не ломает старый код!
    default void sort(Comparator<E> c) {
        // реализация
    }
}

2. Общая реализация

Предоставляем реализацию по умолчанию, которую можно переопределить при необходимости.

Конфликт default методов

Что если класс реализует два интерфейса с одинаковыми default методами?

interface A {
    default void method() {
        System.out.println("A");
    }
}

interface B {
    default void method() {
        System.out.println("B");
    }
}

class C implements A, B {
    // ОШИБКА КОМПИЛЯЦИИ! Неоднозначность
    
    // Должны явно разрешить конфликт:
    @Override
    public void method() {
        A.super.method();  // вызываем метод из A
        // или
        B.super.method();  // вызываем метод из B
        // или
        System.out.println("C");  // своя реализация
    }
}

Static методы в интерфейсах (Java 8+)

Интерфейсы могут иметь static методы:

public interface MathUtils {
    // Static методы - вызываются через имя интерфейса
    static int add(int a, int b) {
        return a + b;
    }
    
    static int multiply(int a, int b) {
        return a * b;
    }
    
    static double average(int... numbers) {
        return Arrays.stream(numbers).average().orElse(0);
    }
}

// Использование - через имя интерфейса
int sum = MathUtils.add(5, 3);
int product = MathUtils.multiply(4, 7);
double avg = MathUtils.average(1, 2, 3, 4, 5);

Зачем static методы в интерфейсах?

1. Утилитные методы

interface StringUtils {
    static boolean isEmpty(String str) {
        return str == null || str.isEmpty();
    }
    
    static String reverse(String str) {
        return new StringBuilder(str).reverse().toString();
    }
}

2. Фабричные методы

interface Animal {
    void makeSound();
    
    static Animal createDog(String name) {
        return new Dog(name);
    }
    
    static Animal createCat(String name) {
        return new Cat(name);
    }
}

Private методы в интерфейсах (Java 9+)

Private методы для переиспользования кода внутри default методов:

public interface Logger {
    // Private helper метод
    private String getTimestamp() {
        return LocalDateTime.now().toString();
    }
    
    // Default методы используют private
    default void logInfo(String message) {
        System.out.println("[INFO] " + getTimestamp() + ": " + message);
    }
    
    default void logError(String message) {
        System.err.println("[ERROR] " + getTimestamp() + ": " + message);
    }
}

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

Сравнительная таблица

ПризнакАбстрактный классИнтерфейс
Ключевое словоabstract classinterface
Наследованиеextends (один)implements (много)
ПоляЛюбыеТолько константы
КонструкторыЕстьНет
МетодыЛюбыеabstract, default, static
МодификаторыЛюбыеТолько public
МножественноеНетДа

Когда что использовать?

Используйте абстрактный класс:

Когда классы тесно связаны (IS-A)

// ✅ Хорошо - общая база для животных
abstract class Animal {
    protected String name;
    protected int age;
    
    public Animal(String name, int age) {
        this.name = name;
        this.age = age;
    }
    
    public void breathe() {
        System.out.println("Дышит");
    }
    
    public abstract void makeSound();
}

Когда нужны поля и конструкторы

Когда нужны protected/private члены

Когда большая часть реализации общая

Используйте интерфейс:

Для определения контракта/способностей

// ✅ Хорошо - способность летать
interface Flyable {
    void fly();
    void land();
}

// Разные классы могут летать: Bird, Plane, Drone

Когда нужна множественная реализация

class Duck implements Flyable, Swimmable, Audible {
    // утка может летать, плавать и издавать звуки
}

Для слабой связанности

Для API и плагинов

Правило большого пальца

  • Интерфейс = CAN-DO (может делать, способности)

    • Flyable, Comparable, Serializable, Runnable
  • Абстрактный класс = IS-A (является чем-то, сущность)

    • Animal, Shape, Vehicle, Document

Пример: Правильное использование

// Сущность - абстрактный класс
abstract class Employee {
    protected String name;
    protected String id;
    protected double baseSalary;
    
    public Employee(String name, String id, double baseSalary) {
        this.name = name;
        this.id = id;
        this.baseSalary = baseSalary;
    }
    
    public abstract double calculateSalary();
}

// Способности - интерфейсы
interface Bonusable {
    void awardBonus(double amount);
    double getTotalBonus();
}

interface Promotable {
    void promote(String newPosition);
    String getPosition();
}

// Конкретная реализация
class Manager extends Employee implements Bonusable, Promotable {
    private double totalBonus;
    private String position;
    
    public Manager(String name, String id, double baseSalary) {
        super(name, id, baseSalary);
        this.position = "Manager";
    }
    
    @Override
    public double calculateSalary() {
        return baseSalary + totalBonus;
    }
    
    @Override
    public void awardBonus(double amount) {
        totalBonus += amount;
    }
    
    @Override
    public double getTotalBonus() {
        return totalBonus;
    }
    
    @Override
    public void promote(String newPosition) {
        this.position = newPosition;
    }
    
    @Override
    public String getPosition() {
        return position;
    }
}

Практический пример: Платёжная система

Применим всё изученное:

// Интерфейс - контракт для оплаты
public interface Payable {
    boolean processPayment(double amount);
    String getPaymentMethod();
}

// Интерфейс - можно вернуть деньги
public interface Refundable {
    boolean refund(double amount);
}

// Абстрактный класс - базовая логика
public abstract class Payment implements Payable {
    protected double balance;
    protected String owner;
    
    public Payment(String owner, double balance) {
        this.owner = owner;
        this.balance = balance;
    }
    
    // Общая логика проверки
    protected boolean hasEnoughBalance(double amount) {
        return balance >= amount;
    }
    
    protected void logTransaction(String operation, double amount) {
        System.out.println("[" + getPaymentMethod() + "] " + 
                          owner + ": " + operation + " " + amount);
    }
    
    public double getBalance() {
        return balance;
    }
}

// Конкретная реализация - банковская карта
public class CreditCard extends Payment implements Refundable {
    private String cardNumber;
    
    public CreditCard(String owner, String cardNumber, double balance) {
        super(owner, balance);
        this.cardNumber = maskCardNumber(cardNumber);
    }
    
    @Override
    public boolean processPayment(double amount) {
        if (amount <= 0) {
            System.out.println("Некорректная сумма");
            return false;
        }
        
        if (!hasEnoughBalance(amount)) {
            System.out.println("Недостаточно средств");
            return false;
        }
        
        balance -= amount;
        logTransaction("Оплата", amount);
        return true;
    }
    
    @Override
    public boolean refund(double amount) {
        balance += amount;
        logTransaction("Возврат", amount);
        return true;
    }
    
    @Override
    public String getPaymentMethod() {
        return "CreditCard " + cardNumber;
    }
    
    private String maskCardNumber(String number) {
        if (number.length() < 4) return number;
        return "**** **** **** " + number.substring(number.length() - 4);
    }
}

// Конкретная реализация - электронный кошелёк
public class EWallet extends Payment implements Refundable {
    private String email;
    
    public EWallet(String owner, String email, double balance) {
        super(owner, balance);
        this.email = email;
    }
    
    @Override
    public boolean processPayment(double amount) {
        if (amount <= 0) {
            System.out.println("Некорректная сумма");
            return false;
        }
        
        // E-wallet может уйти в минус (кредит)
        balance -= amount;
        logTransaction("Оплата", amount);
        return true;
    }
    
    @Override
    public boolean refund(double amount) {
        balance += amount;
        logTransaction("Возврат", amount);
        return true;
    }
    
    @Override
    public String getPaymentMethod() {
        return "E-Wallet (" + email + ")";
    }
}

// Конкретная реализация - наличные (без возврата)
public class Cash extends Payment {
    public Cash(String owner, double balance) {
        super(owner, balance);
    }
    
    @Override
    public boolean processPayment(double amount) {
        if (!hasEnoughBalance(amount)) {
            System.out.println("Недостаточно наличных");
            return false;
        }
        
        balance -= amount;
        logTransaction("Оплата наличными", amount);
        return true;
    }
    
    @Override
    public String getPaymentMethod() {
        return "Cash";
    }
}

// Использование
public class Shop {
    public void checkout(Payable payment, double amount) {
        System.out.println("\n=== Оформление заказа на " + amount + " руб. ===");
        System.out.println("Метод оплаты: " + payment.getPaymentMethod());
        
        if (payment.processPayment(amount)) {
            System.out.println("✓ Оплата успешна!");
            
            // Если поддерживается возврат
            if (payment instanceof Refundable) {
                System.out.println("(Возврат возможен)");
            }
        } else {
            System.out.println("✗ Оплата не прошла");
        }
    }
    
    public static void main(String[] args) {
        Shop shop = new Shop();
        
        Payable card = new CreditCard("Анна", "1234567890123456", 5000);
        Payable wallet = new EWallet("Боб", "bob@example.com", 3000);
        Payable cash = new Cash("Карл", 1000);
        
        shop.checkout(card, 2000);
        shop.checkout(wallet, 4000);
        shop.checkout(cash, 500);
        shop.checkout(cash, 1000);  // недостаточно средств
        
        // Возврат
        if (card instanceof Refundable) {
            ((Refundable) card).refund(1000);
        }
    }
}

Лучшие практики

1. Интерфейсы для контрактов

// ✅ Хорошо - чёткий контракт
interface Repository<T> {
    void save(T entity);
    T findById(int id);
    void delete(int id);
}

2. Абстрактные классы для общей базы

// ✅ Хорошо - общая реализация
abstract class BaseRepository<T> {
    protected Connection connection;
    
    public BaseRepository(Connection connection) {
        this.connection = connection;
    }
    
    protected void log(String message) {
        System.out.println("[DB] " + message);
    }
}

3. Называйте интерфейсы по способностям

// ✅ Хорошо
interface Serializable { }
interface Comparable<T> { }
interface Runnable { }
interface Flyable { }

// ❌ Плохо - не отражает способность
interface AnimalInterface { }

4. Не создавайте пустые маркерные интерфейсы

// ❌ Плохо (устарело)
interface Marker { }

// ✅ Лучше - используйте аннотации
@Marker
class MyClass { }

5. Используйте default методы осторожно

Default методы удобны, но могут усложнить иерархию. Используйте когда действительно нужна общая реализация.

6. Проектируйте для расширения или запрещайте его

// Либо final
public final class Utility { }

// Либо документируем для расширения
/**
 * This class is designed for inheritance.
 * Override hook() method to customize behavior.
 */
public abstract class Extensible {
    protected void hook() { }
}

Итоги

Вы освоили интерфейсы и абстрактные классы!

Ключевые концепции:

  • Абстрактные классы - неполная реализация, шаблон для потомков
  • Интерфейсы - контракты поведения
  • Default методы - реализация в интерфейсах
  • Множественная реализация - через интерфейсы
  • Абстракция - сокрытие деталей, показ только важного
  • Правильный выбор - когда класс, когда интерфейс

Почему это важно:

  • Определяем контракты без привязки к реализации
  • Слабая связанность через интерфейсы
  • Организация кода через абстракцию
  • Множественное “наследование” через интерфейсы

Золотое правило:

  • Интерфейс для способностей (CAN-DO)
  • Абстрактный класс для сущностей (IS-A)
  • Предпочитайте интерфейсы для большей гибкости

Задания для практики

  1. Медиаплеер:

    • Интерфейс Playable
    • Абстрактный класс MediaFile
    • Классы: AudioFile, VideoFile, StreamFile
    • Методы: play(), pause(), stop()
  2. Система уведомлений:

    • Интерфейс Notifiable
    • Классы: EmailNotifier, SMSNotifier, PushNotifier
    • Default методы для форматирования
    • Static методы-фабрики
  3. Коллекция фигур:

    • Абстрактный Shape с площадью
    • Интерфейсы: Drawable, Resizable, Rotatable
    • Разные фигуры реализуют разные интерфейсы
    • Полиморфная обработка
  4. Игровые персонажи:

    • Абстрактный Character
    • Интерфейсы: Attackable, Healable, Movable
    • Разные персонажи с разными способностями

Удачи в программировании! Теперь вы знаете все основы ООП.


Источники: