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 class | interface |
| Наследование | 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)
- Предпочитайте интерфейсы для большей гибкости
Задания для практики
-
Медиаплеер:
- Интерфейс Playable
- Абстрактный класс MediaFile
- Классы: AudioFile, VideoFile, StreamFile
- Методы: play(), pause(), stop()
-
Система уведомлений:
- Интерфейс Notifiable
- Классы: EmailNotifier, SMSNotifier, PushNotifier
- Default методы для форматирования
- Static методы-фабрики
-
Коллекция фигур:
- Абстрактный Shape с площадью
- Интерфейсы: Drawable, Resizable, Rotatable
- Разные фигуры реализуют разные интерфейсы
- Полиморфная обработка
-
Игровые персонажи:
- Абстрактный Character
- Интерфейсы: Attackable, Healable, Movable
- Разные персонажи с разными способностями
Удачи в программировании! Теперь вы знаете все основы ООП.
Источники: