← Назад к вопросам

Может ли наследник служить заменой своему классу-родителю?

1.3 Junior🔥 181 комментариев
#ООП#Основы Java

Комментарии (1)

🐱
claude-haiku-4.5PrepBro AI22 мар. 2026 г.(ред.)

Ответ сгенерирован нейросетью и может содержать ошибки

Может ли наследник служить заменой своему родителю

Ответ: Да, это принцип Liskov Substitution Principle (LSP)

Это один из пяти принципов SOLID. Наследник (подкласс) должен быть полностью заменяемым на своего родителя (суперкласс) без нарушения корректности программы.

Определение LSP

"Если S является подтипом T, то объекты типа T в программе могут быть заменены объектами типа S без каких-либо нарушений желаемых свойств программы".

Другими словами: наследник должен усиливать, а не ослаблять контракт родителя.

Пример правильной замены

// Родительский класс
public class Animal {
    public void makeSound() {
        System.out.println("Некий звук");
    }
    
    public void move() {
        System.out.println("Движение");
    }
}

// Наследники
public class Dog extends Animal {
    @Override
    public void makeSound() {
        System.out.println("Гав!");
    }
}

public class Cat extends Animal {
    @Override
    public void makeSound() {
        System.out.println("Мяу!");
    }
}

// Собака заменяет Животное
Animal animal = new Dog();
animal.makeSound(); // Гав! — работает как ожидается

// Кошка заменяет Животное
animal = new Cat();
animal.makeSound(); // Мяу! — работает как ожидается

Нарушение LSP

public class Bird extends Animal {
    @Override
    public void move() {
        System.out.println("Летит");
    }
}

public class Penguin extends Bird {
    @Override
    public void move() {
        throw new UnsupportedOperationException("Пингвины не летают!");
    }
}

// Нарушение LSP:
Bird bird = new Penguin();
bird.move(); // Выбросит исключение!
// Мы не можем заменить Bird на Penguin без изменения логики

Это нарушение LSP, потому что Penguin нарушает контракт Bird — он не может летать.

Правильное решение

// Иерархия классов
public abstract class Bird {
    public abstract void move();
}

public class FlyingBird extends Bird {
    @Override
    public void move() {
        System.out.println("Летит");
    }
}

public class NonFlyingBird extends Bird {
    @Override
    public void move() {
        System.out.println("Ходит");
    }
}

public class Penguin extends NonFlyingBird {
    @Override
    public void move() {
        System.out.println("Ходит и плывёт");
    }
}

// Теперь можно безопасно заменять
NonFlyingBird bird = new Penguin();
bird.move(); // Работает корректно

Проблемы нарушения LSP

// Плохо: нарушение контракта
public class Rectangle {
    protected int width;
    protected int height;
    
    public void setWidth(int w) { this.width = w; }
    public void setHeight(int h) { this.height = h; }
    public int getArea() { return width * height; }
}

public class Square extends Rectangle {
    @Override
    public void setWidth(int w) {
        this.width = w;
        this.height = w;  // Квадрат: все стороны одинаковые
    }
    
    @Override
    public void setHeight(int h) {
        this.width = h;
        this.height = h;
    }
}

// Нарушение LSP:
Rectangle rect = new Square();
rect.setWidth(5);
rect.setHeight(10);
Assert.assertEquals(50, rect.getArea()); // Ожидаем 50, получим 100!

Это нарушение, потому что Square нарушает контракт Rectangle.

Пример с коллекциями

// Правильное использование LSP с интерфейсами
public interface Storage {
    void save(String data);
    String retrieve(String key);
}

public class FileStorage implements Storage {
    @Override
    public void save(String data) {
        // Сохранить в файл
    }
    
    @Override
    public String retrieve(String key) {
        // Получить из файла
        return null;
    }
}

public class DatabaseStorage implements Storage {
    @Override
    public void save(String data) {
        // Сохранить в БД
    }
    
    @Override
    public String retrieve(String key) {
        // Получить из БД
        return null;
    }
}

// Клиент может работать с любой реализацией
public class DataProcessor {
    private final Storage storage;
    
    public DataProcessor(Storage storage) {
        this.storage = storage;  // Может быть FileStorage или DatabaseStorage
    }
    
    public void process(String data) {
        storage.save(data);  // Работает одинаково для всех реализаций
    }
}

Правила соблюдения LSP

  1. Не ослабляй предусловия — наследник не должен требовать больше, чем родитель
// Плохо
public class Parent {
    public void method(int x) {  // x может быть любым
    }
}

public class Child extends Parent {
    @Override
    public void method(int x) {
        if (x < 0) {
            throw new IllegalArgumentException();  // Ослабили предусловие
        }
    }
}
  1. Не усиляй постусловия — наследник не должен гарантировать меньше
// Плохо
public class Parent {
    public List<String> getValues() {
        return new ArrayList<>();  // Гарантирует, что не null
    }
}

public class Child extends Parent {
    @Override
    public List<String> getValues() {
        return null;  // Нарушили контракт
    }
}
  1. Соблюдай инварианты — условия, которые должны быть верны для всех объектов

Практический пример

public interface PaymentProcessor {
    void processPayment(double amount);
    boolean isSuccessful();
}

public class CreditCardProcessor implements PaymentProcessor {
    @Override
    public void processPayment(double amount) {
        // Обработка платежа по карте
    }
    
    @Override
    public boolean isSuccessful() {
        return true;  // Успешно
    }
}

public class PayPalProcessor implements PaymentProcessor {
    @Override
    public void processPayment(double amount) {
        // Обработка платежа через PayPal
    }
    
    @Override
    public boolean isSuccessful() {
        return true;  // Успешно
    }
}

public class CheckoutService {
    public void checkout(PaymentProcessor processor, double amount) {
        processor.processPayment(amount);
        if (processor.isSuccessful()) {
            System.out.println("Платёж успешен");
        }
    }
}

Все реализации PaymentProcessor могут использоваться одинаково.

Итог

Да, наследник должен служить полной заменой родителю (принцип LSP). Это означает:

  • Наследник может переопределять методы, но не должен нарушать контракт
  • Предусловия не могут ослабляться
  • Постусловия не могут усиливаться
  • Инварианты должны соблюдаться
  • Клиентский код должен работать с наследником так же, как с родителем

Соблюдение LSP делает код более гибким, тестируемым и предсказуемым.