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.8. ООП: Классы и объекты

Основы объектно-ориентированного программирования

Материалы

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

Добро пожаловать в мир объектно-ориентированного программирования! В этом разделе мы изучим основы ООП: что такое классы и объекты, как их создавать и использовать, и почему инкапсуляция так важна.

Что такое ООП?

Посмотрите вокруг прямо сейчас. Всё, что вы видите - это объекты: ваша собака, велосипед, телефон, чашка кофе. Каждый объект имеет:

  • Состояние (свойства, характеристики) - цвет собаки, скорость велосипеда, заряд батареи телефона
  • Поведение (что он умеет делать) - собака лает, велосипед едет, телефон звонит

ООП переносит эту идею в программирование! Мы создаём программные объекты, которые имеют:

  • Поля (fields) - хранят состояние
  • Методы (methods) - определяют поведение

Простой пример из жизни

Объект: Собака "Шарик"
├── Состояние (поля):
│   ├── name = "Шарик"
│   ├── breed = "Лабрадор"
│   ├── age = 3
│   └── hungry = true
└── Поведение (методы):
    ├── bark() - лаять
    ├── eat() - есть
    ├── sleep() - спать
    └── play() - играть

Преимущества ООП

1. Модульность Код организован в логические блоки (классы), которые легко понять и поддерживать.

2. Переиспользование Написали класс один раз - используем много раз. Создаём много объектов из одного класса.

3. Инкапсуляция Скрываем внутренние детали, показываем только важное.

4. Масштабируемость Легко добавлять новую функциональность без изменения существующего кода.

Классы и объекты: чертёж и дом

Класс - это чертёж, шаблон, blueprint для создания объектов. Как архитектурный план дома.

Объект - это конкретный экземпляр (instance) класса. Как настоящий дом, построенный по плану.

Аналогия

Класс "Дом"           →    Объекты (конкретные дома)
├── план               →    Мой дом на ул. Ленина
├── материалы          →    Дом соседа на ул. Мира  
└── спецификация       →    Дом друга на ул. Садовой

По одному плану (классу) можно построить много домов (объектов)!

Первый класс

Давайте создадим класс Dog (Собака):

// Класс - чертёж для создания собак
public class Dog {
    // Поля (fields) - состояние объекта
    String name;
    String breed;
    int age;
    boolean hungry;
    
    // Методы (methods) - поведение объекта
    void bark() {
        System.out.println(name + " говорит: Гав-гав!");
    }
    
    void eat(String food) {
        System.out.println(name + " ест " + food);
        hungry = false;
    }
    
    void sleep() {
        System.out.println(name + " спит... Zzz");
    }
    
    void play() {
        System.out.println(name + " играет!");
        hungry = true;
    }
}

Анатомия класса

public class Dog {
    // 1. Поля (переменные экземпляра)
    String name;
    int age;
    
    // 2. Конструкторы (скоро изучим)
    public Dog(String name, int age) {
        this.name = name;
        this.age = age;
    }
    
    // 3. Методы (функции объекта)
    void bark() {
        System.out.println("Гав!");
    }
}

Создание объектов

Чтобы создать объект, используем оператор new:

public class Main {
    public static void main(String[] args) {
        // Создаём первого пса (первый объект)
        Dog myDog = new Dog();
        myDog.name = "Шарик";
        myDog.breed = "Лабрадор";
        myDog.age = 3;
        myDog.hungry = true;
        
        // Создаём второго пса (второй объект)
        Dog herDog = new Dog();
        herDog.name = "Бобик";
        herDog.breed = "Овчарка";
        herDog.age = 5;
        herDog.hungry = false;
        
        // Используем объекты!
        myDog.bark();           // Шарик говорит: Гав-гав!
        herDog.bark();          // Бобик говорит: Гав-гав!
        
        myDog.eat("мясо");      // Шарик ест мясо
        herDog.sleep();         // Бобик спит... Zzz
        
        myDog.play();
        System.out.println(myDog.name + " голоден? " + myDog.hungry);  // true
    }
}

Что происходит при создании объекта?

Dog myDog = new Dog();
  1. Dog - тип переменной
  2. myDog - имя переменной (ссылка на объект)
  3. new - оператор создания объекта
  4. Dog() - вызов конструктора
Память:
┌──────────────┐
│ myDog (ссылка) │ ──→  ┌───────────────┐
└──────────────┘      │ Объект Dog    │
                      ├───────────────┤
                      │ name: "Шарик" │
                      │ breed: "..."  │
                      │ age: 3        │
                      │ hungry: true  │
                      └───────────────┘

Важно: myDog и herDog - это два совершенно разных объекта в памяти! У каждого своё имя, свой возраст, своё состояние!

Конструкторы: правильная инициализация

Конструктор - это специальный метод для создания и инициализации объектов.

Особенности конструктора

  • ✅ Имя совпадает с именем класса
  • ✅ НЕТ типа возвращаемого значения (даже void)
  • ✅ Вызывается автоматически при создании объекта через new
  • ✅ Может быть несколько (перегрузка)

Простой конструктор

public class Dog {
    String name;
    String breed;
    int age;
    
    // Конструктор
    public Dog(String name, String breed, int age) {
        this.name = name;
        this.breed = breed;
        this.age = age;
    }
    
    void bark() {
        System.out.println(name + " говорит: Гав-гав!");
    }
    
    void info() {
        System.out.println(name + " - " + breed + ", " + age + " лет");
    }
}

// Использование
Dog myDog = new Dog("Шарик", "Лабрадор", 3);
Dog herDog = new Dog("Бобик", "Овчарка", 5);

myDog.info();  // Шарик - Лабрадор, 3 лет
herDog.info(); // Бобик - Овчарка, 5 лет
myDog.bark();  // Шарик говорит: Гав-гав!

Почему конструкторы важны?

Без конструктора:

Dog dog = new Dog();
dog.name = "Шарик";  // можем забыть!
dog.breed = "Лабрадор";  // можем забыть!
dog.age = 3;  // можем забыть!
// У собаки могут быть неинициализированные поля!

С конструктором:

Dog dog = new Dog("Шарик", "Лабрадор", 3);
// Все поля гарантированно инициализированы!

Конструктор по умолчанию

Если вы НЕ создали конструктор, Java создаёт конструктор по умолчанию автоматически:

public class Dog {
    String name;
    int age;
    
    // Java автоматически создаёт:
    // public Dog() {}
}

Dog dog = new Dog();  // работает!

Но если вы создали ХОТЯ БЫ ОДИН конструктор, конструктор по умолчанию НЕ создаётся:

public class Dog {
    String name;
    
    public Dog(String name) {
        this.name = name;
    }
}

// Dog dog = new Dog();  // ОШИБКА! Нет конструктора без параметров
Dog dog = new Dog("Шарик");  // OK

this - ссылка на текущий объект

Ключевое слово this указывает на объект, для которого вызван метод.

Зачем нужен this?

Проблема: параметр и поле имеют одинаковое имя

public class Dog {
    String name;
    
    // name параметра "затеняет" поле name
    public Dog(String name) {
        name = name;  // ЧТО?? Это не работает!
        // Оба name - это параметр!
    }
}

Решение: используем this

public class Dog {
    String name;
    int age;
    
    public Dog(String name, int age) {
        this.name = name;   // this.name - поле объекта
                            // name - параметр конструктора
        this.age = age;
    }
}

this для вызова другого конструктора

public class Dog {
    String name;
    String breed;
    int age;
    
    // Полный конструктор
    public Dog(String name, String breed, int age) {
        this.name = name;
        this.breed = breed;
        this.age = age;
    }
    
    // Упрощённый - делегирует полному
    public Dog(String name) {
        this(name, "Дворняжка", 0);  // вызов другого конструктора!
    }
    
    // Конструктор по умолчанию
    public Dog() {
        this("Безымянный");  // вызов конструктора с одним параметром
    }
}

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

Dog dog1 = new Dog("Шарик", "Лабрадор", 3);  // полный
Dog dog2 = new Dog("Бобик");                  // упрощённый
Dog dog3 = new Dog();                         // по умолчанию

dog1.info();  // Шарик - Лабрадор, 3 лет
dog2.info();  // Бобик - Дворняжка, 0 лет
dog3.info();  // Безымянный - Дворняжка, 0 лет

Правила this():

  • ⚠️ Вызов this() должен быть ПЕРВОЙ строкой в конструкторе
  • ⚠️ Нельзя вызвать два конструктора одновременно
public Dog(String name) {
    System.out.println("Creating dog");
    this(name, "Unknown", 0);  // ОШИБКА! Должно быть первым
}

Инкапсуляция: прячем и контролируем

Инкапсуляция - это сокрытие внутренних деталей объекта и предоставление контролируемого доступа. Один из четырёх столпов ООП!

Зачем нужна инкапсуляция?

1. Защита данных Предотвращаем некорректное изменение состояния объекта.

2. Гибкость Можем изменить внутреннюю реализацию без изменения внешнего API.

3. Простота использования Пользователь класса видит только важные методы, а не внутренние детали.

Проблема без инкапсуляции

public class BankAccount {
    public double balance;  // публичное поле - ОПАСНО!
    public String owner;
}

BankAccount account = new BankAccount();
account.balance = 1000000;  // Кто угодно может изменить баланс!
account.balance = -500;     // Даже отрицательный!
account.owner = "";         // Пустое имя!

Проблемы:

  • ❌ Нет контроля над данными
  • ❌ Можно установить некорректные значения
  • ❌ Нельзя добавить логику (логирование, валидация)

Решение: private + методы доступа

public class BankAccount {
    // Поля приватные!
    private double balance;
    private String owner;
    
    public BankAccount(String owner, double initialBalance) {
        this.owner = owner;
        // Валидация при создании
        if (initialBalance >= 0) {
            this.balance = initialBalance;
        } else {
            this.balance = 0;
            System.out.println("Начальный баланс не может быть отрицательным!");
        }
    }
    
    // Контролируемые методы доступа
    public void deposit(double amount) {
        if (amount > 0) {
            balance += amount;
            System.out.println("Внесено: " + amount + " руб.");
            System.out.println("Новый баланс: " + balance + " руб.");
        } else {
            System.out.println("Сумма должна быть положительной!");
        }
    }
    
    public boolean withdraw(double amount) {
        if (amount <= 0) {
            System.out.println("Сумма должна быть положительной!");
            return false;
        }
        
        if (amount > balance) {
            System.out.println("Недостаточно средств!");
            System.out.println("Доступно: " + balance + " руб.");
            return false;
        }
        
        balance -= amount;
        System.out.println("Снято: " + amount + " руб.");
        System.out.println("Остаток: " + balance + " руб.");
        return true;
    }
    
    // Только чтение баланса (getter)
    public double getBalance() {
        return balance;
    }
    
    public String getOwner() {
        return owner;
    }
}

// Использование
BankAccount account = new BankAccount("Анна", 1000);

account.deposit(500);       // OK
account.deposit(-100);      // Ошибка: сумма должна быть положительной
account.withdraw(2000);     // Ошибка: недостаточно средств
account.withdraw(800);      // OK

System.out.println("Баланс: " + account.getBalance());

// account.balance = 1000000;  // ОШИБКА КОМПИЛЯЦИИ! Поле private

Модификаторы доступа

Java имеет 4 уровня доступа:

МодификаторВнутри классаВ пакетеВ подклассеВезде
public
protected
(default)
private

Примеры

public class Example {
    public int publicField;          // доступно везде
    protected int protectedField;    // пакет + наследники
    int defaultField;                // только в пакете
    private int privateField;        // только в классе
    
    public void publicMethod() {}         // доступен везде
    protected void protectedMethod() {}   // пакет + наследники
    void defaultMethod() {}               // только пакет
    private void privateMethod() {}       // только класс
}

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

private (по умолчанию для полей)

  • ✅ Поля класса
  • ✅ Вспомогательные методы
  • ✅ Внутренняя реализация

public (для API)

  • ✅ Методы, которые должны использовать другие классы
  • ✅ Константы
  • ✅ Интерфейс класса

protected (для наследников)

  • ✅ Методы/поля для использования в подклассах
  • ✅ Расширяемая функциональность

default (package-private)

  • ✅ Классы-помощники внутри пакета
  • ✅ Методы для внутреннего использования в пакете

Геттеры и сеттеры

Геттеры (getters) - методы для чтения приватных полей. Сеттеры (setters) - методы для изменения приватных полей.

Базовый пример

public class Person {
    private String name;
    private int age;
    
    // Getter для name
    public String getName() {
        return name;
    }
    
    // Setter для name с валидацией
    public void setName(String name) {
        if (name != null && !name.trim().isEmpty()) {
            this.name = name;
        } else {
            System.out.println("Имя не может быть пустым!");
        }
    }
    
    // Getter для age
    public int getAge() {
        return age;
    }
    
    // Setter для age с валидацией
    public void setAge(int age) {
        if (age >= 0 && age <= 150) {
            this.age = age;
        } else {
            System.out.println("Некорректный возраст!");
        }
    }
}

// Использование
Person person = new Person();
person.setName("Анна");
person.setAge(25);

System.out.println(person.getName() + ", " + person.getAge() + " лет");

person.setAge(200);   // Некорректный возраст!
person.setName("");   // Имя не может быть пустым!

Зачем геттеры и сеттеры?

1. Валидация

public void setAge(int age) {
    if (age < 0 || age > 150) {
        throw new IllegalArgumentException("Некорректный возраст");
    }
    this.age = age;
}

2. Вычисляемые свойства

private String firstName;
private String lastName;

public String getFullName() {
    return firstName + " " + lastName;
}

3. Логирование

public void setBalance(double balance) {
    System.out.println("Изменение баланса: " + this.balance + " → " + balance);
    this.balance = balance;
}

4. Ленивая инициализация

private List<String> items;

public List<String> getItems() {
    if (items == null) {
        items = new ArrayList<>();
    }
    return items;
}

5. Контроль записи

private String id;

public String getId() {
    return id;
}

// Нет setter - поле только для чтения!

Naming conventions

// Для boolean используем is/has/can
private boolean active;
public boolean isActive() { return active; }

private boolean hasLicense;
public boolean hasLicense() { return hasLicense; }

// Для остальных типов - get/set
private String name;
public String getName() { return name; }
public void setName(String name) { this.name = name; }

Практический пример: Класс Rectangle

Применим все концепции:

public class Rectangle {
    // Инкапсуляция - поля приватные
    private double width;
    private double height;
    private String color;
    
    // Конструктор с валидацией
    public Rectangle(double width, double height, String color) {
        setWidth(width);
        setHeight(height);
        setColor(color);
    }
    
    // Перегрузка конструктора
    public Rectangle(double side) {
        this(side, side, "белый");  // квадрат
    }
    
    // Геттеры
    public double getWidth() {
        return width;
    }
    
    public double getHeight() {
        return height;
    }
    
    public String getColor() {
        return color;
    }
    
    // Сеттеры с валидацией
    public void setWidth(double width) {
        if (width > 0) {
            this.width = width;
        } else {
            throw new IllegalArgumentException("Ширина должна быть положительной");
        }
    }
    
    public void setHeight(double height) {
        if (height > 0) {
            this.height = height;
        } else {
            throw new IllegalArgumentException("Высота должна быть положительной");
        }
    }
    
    public void setColor(String color) {
        if (color != null && !color.isEmpty()) {
            this.color = color;
        } else {
            this.color = "белый";
        }
    }
    
    // Методы поведения
    public double getArea() {
        return width * height;
    }
    
    public double getPerimeter() {
        return 2 * (width + height);
    }
    
    public boolean isSquare() {
        return width == height;
    }
    
    public void scale(double factor) {
        if (factor > 0) {
            width *= factor;
            height *= factor;
        }
    }
    
    @Override
    public String toString() {
        return String.format("Rectangle[%.1f x %.1f, %s, area=%.2f]",
            width, height, color, getArea());
    }
}

// Использование
public class Main {
    public static void main(String[] args) {
        Rectangle rect1 = new Rectangle(10, 5, "красный");
        Rectangle rect2 = new Rectangle(8);  // квадрат
        
        System.out.println(rect1);  // Rectangle[10.0 x 5.0, красный, area=50.00]
        System.out.println(rect2);  // Rectangle[8.0 x 8.0, белый, area=64.00]
        
        System.out.println("Площадь: " + rect1.getArea());      // 50.0
        System.out.println("Периметр: " + rect1.getPerimeter()); // 30.0
        System.out.println("Квадрат? " + rect1.isSquare());      // false
        
        rect1.scale(2);
        System.out.println(rect1);  // Rectangle[20.0 x 10.0, красный, area=200.00]
        
        // rect1.width = -5;  // ОШИБКА! Поле private
        // rect1.setWidth(-5);  // IllegalArgumentException
    }
}

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

1. Делайте поля private

// ❌ Плохо
public class Person {
    public String name;  // прямой доступ
    public int age;
}

// ✅ Хорошо
public class Person {
    private String name;
    private int age;
    
    public String getName() { return name; }
    public void setName(String name) { this.name = name; }
}

2. Используйте конструкторы для инициализации

// ❌ Плохо
Dog dog = new Dog();
dog.name = "Шарик";
dog.age = 3;

// ✅ Хорошо
Dog dog = new Dog("Шарик", 3);

3. Валидируйте в сеттерах

public void setAge(int age) {
    if (age < 0 || age > 150) {
        throw new IllegalArgumentException("Некорректный возраст: " + age);
    }
    this.age = age;
}

4. Используйте final для неизменяемых полей

public class Person {
    private final String id;  // нельзя изменить после создания
    private String name;
    
    public Person(String id, String name) {
        this.id = id;
        this.name = name;
    }
    
    public String getId() {
        return id;  // только getter, нет setter
    }
}

5. Создавайте неизменяемые классы когда возможно

public final class Point {
    private final int x;
    private final int y;
    
    public Point(int x, int y) {
        this.x = x;
        this.y = y;
    }
    
    public int getX() { return x; }
    public int getY() { return y; }
    
    // Нет setters - объект неизменяемый!
    
    // Для "изменения" создаём новый объект
    public Point move(int dx, int dy) {
        return new Point(x + dx, y + dy);
    }
}

6. Документируйте публичные методы

/**
 * Устанавливает возраст человека.
 * 
 * @param age возраст в годах (должен быть от 0 до 150)
 * @throws IllegalArgumentException если возраст вне допустимого диапазона
 */
public void setAge(int age) {
    if (age < 0 || age > 150) {
        throw new IllegalArgumentException("Некорректный возраст: " + age);
    }
    this.age = age;
}

Итоги

Вы изучили основы ООП в Java!

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

  • Классы - чертежи для создания объектов
  • Объекты - экземпляры классов со своим состоянием
  • Поля - хранят состояние объекта
  • Методы - определяют поведение объекта
  • Конструкторы - инициализируют новые объекты
  • this - ссылка на текущий объект
  • Инкапсуляция - скрытие деталей реализации
  • Модификаторы доступа - контроль видимости
  • Геттеры/сеттеры - контролируемый доступ к полям

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

  • Организация кода в логические блоки
  • Защита данных от некорректного использования
  • Переиспользование через создание множества объектов
  • Модульность и простота поддержки

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

  1. Класс Book:

    • Поля: title, author, pages, price
    • Конструкторы: полный и упрощённый
    • Геттеры/сеттеры с валидацией
    • Метод displayInfo()
  2. Класс BankAccount:

    • Поля: accountNumber, balance, owner
    • Методы: deposit(), withdraw(), transfer()
    • Валидация всех операций
    • История транзакций
  3. Класс Circle:

    • Поле: radius
    • Методы: getArea(), getCircumference(), getDiameter()
    • Сравнение двух окружностей
    • Константа PI
  4. Класс Student:

    • Поля: name, id, grades (массив)
    • Методы: addGrade(), getAverage(), isPassing()
    • Валидация оценок (от 2 до 5)

Удачи в освоении ООП! Это фундамент профессионального программирования.


Источники: