Оглавление
Введение
1. Классы и Объекты
2. Инкапсуляция
3. Наследование
4. Полиморфизм
5. Абстракция
Проблемы ООП
Заключение
Введение
Привет! Эта статья написана для улучшения понимания принципов объектно-ориентированного программирования для начинающих автоматизаторов. Эта тема является важной и часто её спрашивают на собеседованиях. Понимание ООП поможет тебе писать более структурированный, поддерживаемый и переиспользуемый код.
Когда я впервые столкнулся с ООП, меня встретили сухие определения: "инкапсуляция", "наследование", "полиморфизм". Звучало сложно и оторвано от реальности. Но на самом деле ООП — это способ мышления, который отражает то, как мы воспринимаем мир вокруг себя.

Представьте, что вы заходите в фруктовый магазин. Вы видите яблоки, бананы, апельсины. Каждый фрукт:
Имеет свои свойства (цвет, вес, спелость)
Имеет поведение (может созревать, может портиться)
Относится к какой-то категории (цитрусовые, косточковые)
Именно так работает ООП! Давайте разберёмся на конкретных примерах.
1. Классы и Объекты: Чертёж и реальная вещь
Теория:
Класс — это чертёж, описание того, какими свойствами и поведением будет обладать объект. Объект — это конкретный экземпляр, созданный по этому чертежу.
Главная мысль: класс — это описание, объект — это конкретный экземпляр с собственным состоянием.

Жизненный пример:
Представьте, что у вас есть чертёж "Автомобиль" с описанием: 4 колеса, руль, двигатель, возможность ехать и сигналить. Это класс. А конкретная машина во дворе — красная Toyota с номером А123ВС — это объект, созданный по этому чертежу.
Термин:
Процесс создания объекта из класса называется инстанцированием (instantiation).
Когда мы пишем new Car("Toyota", "красный", "А123ВС"), — мы инстанцируем класс Car, то есть создаём конкретный экземпляр на его основе.
Код на Java:
// Класс - чертёж для автомобиля class Car { // Состояние (поля) - что автомобиль "имеет" private String brand; private String color; private String licensePlate; private boolean isEngineOn; private int currentSpeed; // Конструктор - специальный метод для создания объектов public Car(String brand, String color, String licensePlate) { this.brand = brand; this.color = color; this.licensePlate = licensePlate; this.isEngineOn = false; this.currentSpeed = 0; } // Поведение (методы) - что автомобиль "умеет делать" public void startEngine() { this.isEngineOn = true; System.out.println("Двигатель автомобиля " + brand + " завёлся!"); } public void stopEngine() { this.isEngineOn = false; this.currentSpeed = 0; System.out.println("Двигатель автомобиля " + brand + " заглушен"); } public void accelerate(int speed) { if (isEngineOn) { this.currentSpeed += speed; System.out.println(brand + " разгоняется до " + currentSpeed + " км/ч"); } else { System.out.println("Сначала заведите двигатель!"); } } public void honk() { System.out.println("Автомобиль " + brand + " сигналит: Би-бип!"); } // Геттеры - чтобы узнать состояние public String getBrand() { return brand; } public String getColor() { return color; } public int getCurrentSpeed() { return currentSpeed; } public boolean isEngineRunning() { return isEngineOn; } } // Использование в программе public class CarDemo { public static void main(String[] args) { // Создаём ОБЪЕКТЫ по ЧЕРТЕЖУ Car Car toyota = new Car("Toyota", "красный", "А123ВС"); Car bmw = new Car("BMW", "чёрный", "В777ХХ"); // Вызываем поведение объектов toyota.startEngine(); // Заводим Toyota toyota.accelerate(50); // Разгоняемся toyota.honk(); // Сигналим System.out.println("Текущая скорость Toyota: " + toyota.getCurrentSpeed() + " км/ч"); // BMW пока не заведён System.out.println("Двигатель BMW заведён: " + bmw.isEngineRunning()); bmw.accelerate(30); // Не сработает - двигатель заглушен } }
Что происходит в коде:
Car- это класс (чертёж)toyotaиbmw- это объекты (конкретные машины)Каждый объект имеет своё состояние: разный цвет, разные номера, разная скорость
Поведение (методы) одинаковое для всех объектов, но результат зависит от их состояния
2. Инкапсуляция: Контроль доступа к состоянию
Теория:
Инкапсуляция — это принцип, согласно которому внутреннее состояние объекта защищено от прямого доступа извне. Взаимодействие происходит только через публичные методы.
Коротко
Инкапсуляция — сокрытие реализации.

Жизненный пример:
Представьте банкомат. У него есть внутреннее состояние: количество денег, журнал операций. Но вы не можете просто открыть его и взять деньги. Вы взаимодействуете через интерфейс: вставляете карту, вводите PIN, выбираете операцию. Банкомат сам проверяет корректность запроса и выдает деньги, если всё правильно.
Реализация в Java:
Модификаторы доступа:
private,protected,publicдля контроля видимости полей и методовГеттеры: методы для безопасного чтения приватных полей
Сеттеры: методы для безопасного изменения полей с валидацией
Конструкторы: обеспечение корректного начального состояния объекта
Код на Java:
class BankAccount { // private - значит, напрямую извне к этим полям обратиться нельзя private String ownerName; private double balance; private String accountNumber; public BankAccount(String ownerName, double initialBalance, String accountNumber) { this.ownerName = ownerName; this.balance = initialBalance; this.accountNumber = accountNumber; } // Публичные методы - наш "интерфейс" для взаимодействия public void deposit(double amount) { if (amount > 0) { balance += amount; System.out.println("Пополнение на " + amount + ". Новый баланс: " + balance); } else { System.out.println("Сумма пополнения должна быть положительной"); } } public boolean withdraw(double amount) { if (amount > 0 && amount <= balance) { balance -= amount; System.out.println("Снято " + amount + ". Остаток: " + balance); return true; } else { System.out.println("Недостаточно средств или неверная сумма"); return false; } } // Геттеры - только для чтения информации public double getBalance() { return balance; } public String getOwnerName() { return ownerName; } } // Использование public class BankDemo { public static void main(String[] args) { BankAccount account = new BankAccount("Иван Иванов", 1000.0, "123456789"); // Так нельзя! Будет ошибка компиляции: // account.balance = 1000000; // ОШИБКА! Поле private // А так можно - через контролируемые методы account.deposit(500.0); // Пополняем account.withdraw(200.0); // Снимаем account.withdraw(2000.0); // Не получится - недостаточно средств System.out.println("Текущий баланс: " + account.getBalance()); } }
Почему это важно: Инкапсуляция защищает целостность данных. Мы гарантируем, что баланс не может стать отрицательным, потому что все операции проходят через наши проверки.
3. Наследование
Теория:
Наследование позволяет создавать новый класс на основе существующего, перенимая его свойства и поведение, и добавляя что-то новое.
Коротко
Наследование — создание нового на основе существующего.

Жизненный пример:
Представьте транспортные средства. Есть общее понятие "Транспорт" - он может ехать и останавливаться. "Автомобиль" является транспортом, но добавляет возможность сигналить. "Велосипед" тоже является транспортом, но добавляет возможность крутить педали. "Самолёт" является транспортом, но добавляет возможность летать.
Реализация в Java:
extends: создание иерархии классов (один родитель)super: обращение к конструкторам и методам родительского классаНаследование методов: дочерние классы получают функциональность родителя
Доступ к protected: наследники видят protected-поля и методы
Код на Java:
// Базовый класс - Транспорт class Transport { private String name; public Transport(String name) { this.name = name; } public void move() { System.out.println(name + " начинает движение"); } public void stop() { System.out.println(name + " остановился"); } } // Класс Car наследует от Transport class Car extends Transport { public Car() { super("Автомобиль"); // Вызываем конструктор родителя } // Новый метод, специфичный для автомобилей public void honk() { System.out.println("Би-бип! Сигналит автомобиль"); } } // Класс Bicycle наследует от Transport class Bicycle extends Transport { public Bicycle() { super("Велосипед"); } // Новый метод, специфичный для велосипедов public void ringBell() { System.out.println("Дзинь-дзинь! Звонит велосипед"); } } // Класс Airplane наследует от Transport class Airplane extends Transport { public Airplane() { super("Самолёт"); } // Новый метод, специфичный для самолётов public void takeOff() { System.out.println("Самолёт взлетает в небо"); } } // Использование public class InheritanceDemo { public static void main(String[] args) { // Создаём объекты Car car = new Car(); Bicycle bike = new Bicycle(); Airplane plane = new Airplane(); // Все объекты имеют унаследованные методы System.out.println("=== Унаследованные методы ==="); car.move(); // Унаследовано от Transport bike.move(); // Унаследовано от Transport plane.move(); // Унаследовано от Transport System.out.println("\n=== Уникальные методы ==="); // Каждый класс имеет свои уникальные методы car.honk(); // Только у Car bike.ringBell(); // Только у Bicycle plane.takeOff(); // Только у Airplane System.out.println("\n=== Общие действия ==="); car.stop(); // Унаследовано от Transport bike.stop(); // Унаследовано от Transport plane.stop(); // Унаследовано от Transport } }
Что демонстрирует этот пример наследования:
Наследование структуры — все дочерние классы технически содержат поле
nameот родителяTransport, но не имеют к нему прямого доступа из-за модификатораprivate. Взаимодействие с этим полем происходит через унаследованные публичные методы родителя.Наследование методов - все классы могут использовать
move()иstop()от родителяРасширение функциональности - каждый дочерний класс добавил свои уникальные методы:
Car:
honk()- сигналитBicycle:
ringBell()- звонит в звонокAirplane:
takeOff()- взлетает
Вызов конструктора родителя - каждый класс вызывает конструктор родителя через
super(), передавая своё название
4. Полиморфизм: Одно действие - разные реализации
Теория:
Полиморфизм позволяет объектам разных классов отвечать на один и тот же вызов метода по-разному.
Коротко
Полиморфизм — одно действие, разные реализации.

Жизненный пример:
Представьте кнопки на пульте управления. Есть кнопка "Включить":
На телевизоре она включает экран
На кондиционере - запускает охлаждение
На кофеварке - начинает готовить кофе
Одна и та же команда "включить" выполняется по-разному в зависимости от устройства.
Реализация в Java:
Перегрузка (overloading): один класс, разные параметры (статический полиморфизм)
Переопределение (overriding): наследование + одинаковые сигнатуры (динамический полиморфизм)
Интерфейсы (
interface): контракты для реализации разными классамиАбстрактные классы: частичная реализация + обязательные для переопределения методы
Аннотация @Override явно указывает, что мы переопределяем метод родителя —
это переопределение (overriding) является основой полиморфизма.
Код на Java:
Полиморфизм через наследование:
// Базовый класс для устройств class Device { protected String name; public Device(String name) { this.name = name; } // Этот метод будет по-разному работать для каждого устройства public void turnOn() { System.out.println("Включаем " + name); } } // Конкретные устройства class TV extends Device { public TV() { super("телевизор"); } @Override public void turnOn() { System.out.println("Телевизор: загорается экран, показывается программа"); } } class AC extends Device { public AC() { super("кондиционер"); } @Override public void turnOn() { System.out.println("Кондиционер: запускается вентилятор, начинает дуть холодный воздух"); } } class CoffeeMachine extends Device { public CoffeeMachine() { super("кофеварка"); } @Override public void turnOn() { System.out.println("Кофеварка: начинает готовить кофе, слышно шипение"); } } // Демонстрация полиморфизма public class PolymorphismDemo { public static void main(String[] args) { // Создаём устройства Device tv = new TV(); Device ac = new AC(); Device coffee = new CoffeeMachine(); // Массив устройств Device[] devices = {tv, ac, coffee}; System.out.println("Нажимаем кнопку 'Включить' на всех устройствах:"); // ПОЛИМОРФИЗМ: один метод, разное поведение! for (Device device : devices) { device.turnOn(); // Каждое устройство включается по-своему } } }
Полиморфизм через интерфейсы:
// интерфейс вместо базового класса interface Switchable { void turnOn(); // Контракт - все реализации должны иметь этот метод } // Конкретные устройства реализуют интерфейс class TV implements Switchable { @Override public void turnOn() { System.out.println("Телевизор: загорается экран, показывается программа"); } } class AC implements Switchable { @Override public void turnOn() { System.out.println("Кондиционер: запускается вентилятор, начинает дуть холодный воздух"); } } class CoffeeMachine implements Switchable { @Override public void turnOn() { System.out.println("Кофеварка: начинает готовить кофе, слышно шипение"); } } // Демонстрация полиморфизма через интерфейсы public class InterfacePolymorphismDemo { public static void main(String[] args) { // Создаём устройства через интерфейсный тип Switchable tv = new TV(); Switchable ac = new AC(); Switchable coffee = new CoffeeMachine(); // Массив устройств Switchable[] devices = {tv, ac, coffee}; System.out.println("Нажимаем кнопку 'Включить' на всех устройствах:"); // ПОЛИМОРФИЗМ ЧЕРЕЗ ИНТЕРФЕЙСЫ: один метод, разное поведение! for (Switchable device : devices) { device.turnOn(); // Каждое устройство включается по-своему } } }
Сравнение подходов: наследование vs интерфейсы
Критерий | Полиморфизм через наследование | Полиморфизм через интерфейсы |
|---|---|---|
Связность | Жёсткая связь с родительским классом | Слабая связь, только контракт |
Гибкость | Один родитель, нельзя наследовать от нескольких классов | Можно реализовать несколько интерфейсов |
Переиспользование | Наследуется всё: и методы, и поля, и поведение | Реализуется только контракт, внутренняя реализация свободна |
Расширяемость | Для добавления функциональности нужно менять иерархию | Новые интерфейсы добавляются без изменения существующих классов |
Пример в коде |
|
|
Когда использовать | Когда есть чёткая иерархия "is-a" и общее поведение | Когда нужна гибкость и разные объекты должны поддерживать одинаковые операции |
5. Абстракция: Скрытие сложности
Теория:
Абстракция позволяет скрыть сложную реализацию и показать только необходимые детали, работая на уровне концепций, а не конкретной реализации.
Коротко
Абстракция — выделение главного, сокрытие деталей реализации.

Жизненный пример:
Когда вы ведете автомобиль, вам не нужно знать как работает двигатель, система впрыска топлива или электроника. Вы используете руль, педали и рычаги - это абстракция, скрывающая сложность механизмов под капотом.
Реализация в Java:
Интерфейсы (
interface): определение ЧТО должно делать, без реализации КАКАбстрактные классы (
abstract class): общая логика + абстрактные методы для реализацииАбстрактные методы: методы без реализации, которые должны быть реализованы в наследниках
Сокрытие сложности: публичные методы скрывают внутреннюю реализацию
Код на Java:
// Абстракция - определяем ЧТО должно делать, без реализации interface Vehicle { void start(); // Абстрактный метод - без реализации void stop(); // Абстрактный метод - без реализации int getMaxSpeed(); // Абстрактный метод - без реализации } // Конкретная реализация - скрываем сложность двигателя class Car implements Vehicle { private String engineType; private int currentSpeed; public Car(String engineType) { this.engineType = engineType; this.currentSpeed = 0; } @Override public void start() { // Сложная логика запуска двигателя скрыта от пользователя System.out.println("Запускаем " + engineType + " двигатель"); System.out.println("Проверяем системы..."); System.out.println("Двигатель запущен!"); } @Override public void stop() { // Сложная логика остановки скрыта System.out.println("Останавливаем двигатель..."); this.currentSpeed = 0; System.out.println("Транспорт остановлен"); } @Override public int getMaxSpeed() { return 220; // Конкретная реализация } } // Другая реализация - скрываем сложность электрической системы class ElectricScooter implements Vehicle { private int batteryLevel; public ElectricScooter() { this.batteryLevel = 100; } @Override public void start() { // Сложность электрической системы скрыта System.out.println("Активируем батарею..."); System.out.println("Запускаем электромотор..."); System.out.println("Самокат готов к поездке!"); } @Override public void stop() { // Сложность остановки электромотора скрыта System.out.println("Отключаем электромотор..."); System.out.println("Самокат остановлен"); } @Override public int getMaxSpeed() { return 25; // Конкретная реализация } } // Использование абстракции public class AbstractionDemo { public static void main(String[] args) { // Работаем через абстракцию, не зная деталей реализации Vehicle car = new Car("бензиновый"); Vehicle scooter = new ElectricScooter(); System.out.println("=== Используем транспорт через абстракцию ==="); // Нам не важно КАК именно запускается транспорт - важно ЧТО он запускается car.start(); // Сложность запуска ДВС скрыта scooter.start(); // Сложность запуска электромотора скрыта System.out.println("\nМаксимальная скорость:"); System.out.println("Автомобиль: " + car.getMaxSpeed() + " км/ч"); System.out.println("Самокат: " + scooter.getMaxSpeed() + " км/ч"); System.out.println("\nОстанавливаем транспорт:"); car.stop(); // Сложность остановки скрыта scooter.stop(); // Сложность остановки скрыта } }
Преимущество абстракции: Мы можем работать с транспортом на высоком уровне, не зная деталей реализации. Если завтра мы добавим новый вид транспорта (например, электромобиль), код, использующий абстракцию Vehicle, не нужно будет менять - мы просто реализуем интерфейс в новом классе.
Проблемы ООП
Проблема наследования: Хрупкая базовая функциональность

Представьте, что мы хотим добавить всем транспортным средствам возможность рассчитывать стоимость поездки:
class Transport { // ... остальные методы ... public double calculateTripCost(int distance) { // Базовая реализация return 0; } } // Теперь ВСЕ классы-наследники должны переопределить этот метод class Car extends Transport { @Override public double calculateTripCost(int distance) { return distance * 0.15; // 15 рублей за км } } class Bicycle extends Transport { @Override public double calculateTripCost(int distance) { return 0; // На велосипеде бесплатно } } class Airplane extends Transport { @Override public double calculateTripCost(int distance) { return distance * 5.0; // 5 рублей за км (условно) } } // И так для КАЖДОГО транспорта!
Чем больше классов-наследников, тем сложнее вносить изменения в базовый класс.
Проблема выражения: Добавление нового поведения
Допустим, мы хотим добавить операцию "помыть транспорт". В ООП-стиле нам придётся добавить метод wash() в базовый класс Transport и реализовать его во всех наследниках:
class Transport { public void wash() { // Базовая реализация - может быть не подходит для всех } } class Car extends Transport { @Override public void wash() { System.out.println("Моем автомобиль на автомойке"); } } class Bicycle extends Transport { @Override public void wash() { System.out.println("Моем велосипед из шланга"); } } class Airplane extends Transport { @Override public void wash() { System.out.println("Самолёт моет специальная бригада с подъёмниками"); // Сложная реализация! } }
Опасный пример: Нарушение инкапсуляции через возвращение mutable-объектов
Вот как можно незаметно "выстрелить себе в ногу" в ООП:
class ShoppingCart { private List<String> items; public ShoppingCart() { this.items = new ArrayList<>(); } // ОПАСНОСТЬ: возвращаем mutable-коллекцию! public List<String> getItems() { return items; // Теперь внешний код может изменить нашу внутреннюю коллекцию! } public void addItem(String item) { items.add(item); System.out.println("Добавлен товар: " + item); } } public class EncapsulationDangerDemo { public static void main(String[] args) { ShoppingCart cart = new ShoppingCart(); cart.addItem("Книга"); cart.addItem("Ноутбук"); // Получаем "защищённую" коллекцию List<String> items = cart.getItems(); System.out.println("Товаров в корзине: " + items.size()); // 2 // НЕОЖИДАННОСТЬ: внешний код меняет внутреннее состояние! items.clear(); // Удалили все товары! items.add("Взломанный товар"); // Добавили что-то без валидации System.out.println("Товаров в корзине после 'взлома': " + items.size()); // 1 System.out.println("Корзина сломана: " + cart.getItems()); // [Взломанный товар] } }
Почему это проблема: Мы думали, что защитили данные через private, но вернули mutable-объект, который позволяет обойти все защиты.
Как исправить:
// Защищённые геттеры: public List<String> getItems() { return new ArrayList<>(items); // Возвращаем копию } // Или лучше - возвращаем неизменяемую коллекцию: public List<String> getItems() { return Collections.unmodifiableList(items); } // Или предоставляем только read-only операции: public int getItemsCount() { return items.size(); } public boolean containsItem(String item) { return items.contains(item); }
Наследование вместо композиции (нарушение LSP)
class Rectangle { protected int width; protected int height; public Rectangle(int width, int height) { this.width = width; this.height = height; } public void setWidth(int width) { this.width = width; } public void setHeight(int height) { this.height = height; } public int getArea() { return width * height; } } // ОПАСНОСТЬ: Square "является" Rectangle? Не всегда! class Square extends Rectangle { public Square(int size) { super(size, size); } @Override public void setWidth(int width) { super.setWidth(width); super.setHeight(width); // Ломаем контракт Rectangle! } @Override public void setHeight(int height) { super.setHeight(height); super.setWidth(height); // Ломаем контракт Rectangle! } } public class LSPViolationDemo { static void resizeRectangle(Rectangle rectangle) { rectangle.setWidth(5); rectangle.setHeight(4); System.out.println("Ожидаемая площадь: 20, фактическая: " + rectangle.getArea()); } public static void main(String[] args) { Rectangle rect = new Rectangle(2, 3); resizeRectangle(rect); // Ожидаемая площадь: 20, фактическая: 20 ✅ Rectangle square = new Square(2); resizeRectangle(square); // Ожидаемая площадь: 20, фактическая: 16 ❌ // Нарушение принципа подстановки Лисков: // Square не может быть заменой Rectangle! } }
Почему это проблема: Наследование должно сохранять поведение базового класса. Если наследник нарушает контракт родителя - это приводит к тонким багам.
Как исправить:
// Лучше использовать композицию: class Square { private Rectangle rectangle; public Square(int size) { this.rectangle = new Rectangle(size, size); } public void setSize(int size) { // Используем делегирование, а не наследование rectangle = new Rectangle(size, size); } public int getArea() { return rectangle.getArea(); } }
Скрытые зависимости и побочные эффекты
class UserService { private UserRepository repository; private EmailService emailService; private Logger logger; public UserService(UserRepository repository, EmailService emailService) { this.repository = repository; this.emailService = emailService; this.logger = Logger.getLogger(); // СКРЫТАЯ ЗАВИСИМОСТЬ! } public void registerUser(String email, String password) { // Валидация... User user = new User(email, password); // Ожидаемые действия: repository.save(user); emailService.sendWelcomeEmail(email); // Это логично и предсказуемо // А ЭТО — СКРЫТЫЕ ПОБОЧНЫЕ ЭФФЕКТЫ: logger.log("User registered: " + email); // Логирование не заявлено в контракте Metrics.incrementCounter("registrations"); // Метрики не очевидны из названия // И ещё хуже: if (email.endsWith("@admin.com")) { SecurityNotifier.notifyAdmins(); // Глобальное состояние! } } } public class HiddenDependenciesDemo { public static void main(String[] args) { // Тестируем регистрацию пользователя UserRepository repo = new TestUserRepository(); EmailService email = new MockEmailService(); UserService service = new UserService(repo, email); service.registerUser("test@example.com", "password"); // Внезапно: наш тест падает из-за: // - глобального логгера, который не инициализирован // - метрик, которые требуют конфигурации // - SecurityNotifier, который пытается отправить реальные уведомления! } }
Почему это проблема: Метод делает больше, чем заявлено в его контракте. Это усложняет тестирование и приводит к неожиданному поведению.
Как исправить:
// Явные зависимости через конструктор: class UserService { private final UserRepository repository; private final EmailService emailService; private final Logger logger; private final Metrics metrics; // Все зависимости явные и обязательные public UserService(UserRepository repository, EmailService emailService, Logger logger, Metrics metrics) { this.repository = repository; this.emailService = emailService; this.logger = logger; this.metrics = metrics; } // Метод делает только то, что заявлено: public RegistrationResult registerUser(String email, String password) { User user = new User(email, password); repository.save(user); emailService.sendWelcomeEmail(email); logger.log("User registered: " + email); metrics.incrementCounter("registrations"); return new RegistrationResult(user, SUCCESS); } }
Заключение
ООП — это мощный инструмент, который помогает организовать сложный код, но требует понимания и осторожности:
Сильные стороны:
Естественное моделирование предметной области
Инкапсуляция защищает целостность данных
Полиморфизм делает код расширяемым
Опасности:
Слишком глубокие иерархии наследования
Скрытые побочные эффекты в методах
Сложность добавления нового поведения
Ключевой вывод: используйте ООП там, где оно действительно упрощает код, а не усложняет его. Комбинируйте ООП с другими подходами, и всегда помните о принципе KISS (Keep It Simple, Stupid) — самое простое решение часто оказывается лучшим.
