☕ Java

ООП: Классы и объекты в Java: подробное руководство

Что это такое и зачем? (аналогия из реальной жизни)

Представь, что ты решил построить город из конструктора LEGO. У тебя есть коробка с деталями, но нет инструкции. Ты можешь собрать дом, но каждый следующий дом придётся собирать заново, подбирая детали. Это похоже на программирование без ООП — каждый раз ты пишешь код с нуля, даже если он очень похож на предыдущий.

А теперь представь, что у тебя есть чертёж дома. На этом чертеже указано: у дома есть стены, крыша, окна и дверь. Чертеж — это класс. А когда ты берёшь этот чертёж и строишь по нему настоящий дом — это объект. Ты можешь построить 10, 100 или 1000 одинаковых домов по одному чертежу. Более того, ты можешь взять чертёж дома и добавить к нему гараж — это называется наследование.

В программировании всё точно так же. Класс — это чертёж, шаблон или форма для создания объектов. Объект — это конкретный экземпляр, созданный по этому шаблону. У каждого объекта есть состояние (данные, поля) и поведение (методы, действия).

💡 Важно запомнить: Класс — это тип данных, который ты сам придумываешь. Точно так же, как в Java есть тип int для целых чисел или String для строк, ты можешь создать свой тип — например, Car, Student или BankAccount.

Зачем это нужно? Представь, что ты пишешь программу для библиотеки. Без ООП тебе пришлось бы хранить имя книги в одном массиве, автора — в другом, год издания — в третьем. А с ООП ты просто создаёшь класс Book, и каждая книга — это один объект, в котором уже есть всё необходимое. Удобно, правда?

Теория: как это работает изнутри

Давай заглянем под капот Java и разберёмся, что происходит в памяти компьютера, когда мы работаем с классами и объектами.

Память: Stack и Heap

Когда программа запускается, Java делит оперативную память на две основные области:

  • Stack (стек) — здесь хранятся примитивные типы (int, double, boolean) и ссылки на объекты. Работает по принципу «первым зашёл — последним вышел». Очень быстрый, но маленький.
  • Heap (куча) — здесь живут сами объекты. Места много, но работать с ним чуть медленнее.

Когда ты пишешь Book myBook = new Book();, происходит вот что:

  1. В стеке создаётся переменная myBook (это просто ссылка, как адрес дома на конверте).
  2. В куче выделяется память под новый объект Book.
  3. Вызывается конструктор, который инициализирует поля объекта.
  4. Ссылка на этот объект (адрес в куче) записывается в переменную myBook.

Жизненный цикл объекта

  1. Объявление: Book myBook; — просто говорим, что такая переменная будет.
  2. Создание: new Book(); — выделяем память.
  3. Инициализация: конструктор заполняет поля начальными значениями.
  4. Использование: мы вызываем методы, меняем поля.
  5. Уничтожение: когда объект больше не нужен, Java сама удаляет его из памяти (это называется сборка мусора).

⚠️ Важно: В Java ты не можешь сам удалить объект из памяти. За тебя это делает Garbage Collector (сборщик мусора), когда видит, что на объект больше нет ссылок.

Что такое конструктор?

Конструктор — это специальный метод, который вызывается при создании объекта. У него нет возвращаемого типа, и его имя совпадает с именем класса. Конструктор нужен, чтобы задать начальное состояние объекта.

Если ты не напишешь ни одного конструктора, Java добавит конструктор по умолчанию — пустой, без параметров. Но как только ты создаёшь свой конструктор, «дефолтный» исчезает.

Синтаксис в Java: все формы и варианты

Давай разберём синтаксис классов в Java от простого к сложному. Вот полная структура класса:

// Модификаторы доступа: public, private, protected, default
// Ключевое слово class
// Имя класса (с большой буквы)
public class Dog {

    // 1. ПОЛЯ (состояние объекта)
    // Могут быть примитивными или ссылочными типами
    private String name;      // имя собаки
    private int age;          // возраст
    private String breed;     // порода
    private boolean isHungry; // голодна ли?

    // 2. КОНСТРУКТОРЫ
    // Конструктор по умолчанию (если не написать свой)
    public Dog() {
        this.name = "Бобик";
        this.age = 1;
        this.breed = "Дворняга";
        this.isHungry = true;
    }

    // Конструктор с параметрами
    public Dog(String name, int age, String breed) {
        this.name = name;     // this.name — поле класса, name — параметр
        this.age = age;
        this.breed = breed;
        this.isHungry = true; // по умолчанию собака голодна
    }

    // 3. МЕТОДЫ (поведение объекта)
    // Геттеры (получить значение поля)
    public String getName() {
        return name;
    }

    // Сеттеры (установить значение поля)
    public void setName(String name) {
        this.name = name;
    }

    // Обычные методы
    public void bark() {
        System.out.println(name + " говорит: Гав-гав!");
    }

    public void feed() {
        if (isHungry) {
            System.out.println(name + " с удовольствием ест!");
            isHungry = false;
        } else {
            System.out.println(name + " не голоден.");
        }
    }

    // 4. СТАТИЧЕСКИЕ ПОЛЯ И МЕТОДЫ (принадлежат классу, а не объекту)
    public static int dogCount = 0; // сколько всего собак создано

    public static void showDogCount() {
        System.out.println("Всего собак: " + dogCount);
    }
}

Модификаторы доступа: кто может видеть?

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

Ключевое слово this

this — это ссылка на текущий объект. Она используется, чтобы:

  • Различать поля класса и параметры метода (как в конструкторе выше).
  • Вызывать один конструктор из другого: this("Бобик", 1, "Дворняга");
  • Передать текущий объект в другой метод: someMethod(this);

Перегрузка методов и конструкторов

В Java можно создать несколько методов или конструкторов с одинаковым именем, но разными параметрами. Это называется перегрузка.

public class Calculator {
    // Перегрузка метода add
    public int add(int a, int b) {
        return a + b;
    }

    public double add(double a, double b) {
        return a + b;
    }

    public int add(int a, int b, int c) {
        return a + b + c;
    }
}

Примеры кода — от простого к сложному

Пример 1: Самый простой класс

// Создаём класс "Студент"
public class Student {
    // Поля класса
    String name;
    int age;

    // Метод для вывода информации
    void sayHello() {
        System.out.println("Привет! Меня зовут " + name + ", мне " + age + " лет.");
    }
}

// Использование в main
public class Main {
    public static void main(String[] args) {
        // Создаём объект класса Student
        Student student1 = new Student();

        // Заполняем поля
        student1.name = "Анна";
        student1.age = 20;

        // Вызываем метод
        student1.sayHello(); // Вывод: Привет! Меня зовут Анна, мне 20 лет.
    }
}

Пример 2: Конструктор с параметрами

public class Car {
    String model;
    int year;
    String color;

    // Конструктор с параметрами
    public Car(String model, int year, String color) {
        this.model = model;
        this.year = year;
        this.color = color;
    }

    void displayInfo() {
        System.out.println(year + " " + color + " " + model);
    }
}

public class Main {
    public static void main(String[] args) {
        // Создаём машину сразу с характеристиками
        Car myCar = new Car("Toyota Camry", 2022, "Синий");
        myCar.displayInfo(); // Вывод: 2022 Синий Toyota Camry
    }
}

Пример 3: Геттеры и сеттеры (инкапсуляция)

public class BankAccount {
    // Приватные поля — никто не может их изменить напрямую
    private String accountNumber;
    private double balance;

    // Конструктор
    public BankAccount(String accountNumber, double initialBalance) {
        this.accountNumber = accountNumber;
        // Проверяем, что баланс не отрицательный
        if (initialBalance >= 0) {
            this.balance = initialBalance;
        } else {
            this.balance = 0;
            System.out.println("Начальный баланс не может быть отрицательным!");
        }
    }

    // Геттер для баланса (только чтение)
    public double getBalance() {
        return balance;
    }

    // Геттер для номера счёта
    public String getAccountNumber() {
        return accountNumber;
    }

    // Сеттера для баланса нет — вместо него методы пополнения и снятия
    public void deposit(double amount) {
        if (amount > 0) {
            balance += amount;
            System.out.println("Счёт пополнен на " + amount + ". Баланс: " + balance);
        } else {
            System.out.println("Сумма пополнения должна быть положительной!");
        }
    }

    public void withdraw(double amount) {
        if (amount > 0 && amount <= balance) {
            balance -= amount;
            System.out.println("Снято " + amount + ". Баланс: " + balance);
        } else {
            System.out.println("Недостаточно средств или неверная сумма!");
        }
    }
}

Пример 4: Статические поля и методы

public class Employee {
    private String name;
    private int id;
    private static int nextId = 1; // Статическое поле — общее для всех
    private static int totalEmployees = 0;

    public Employee(String name) {
        this.name = name;
        this.id = nextId++; // Каждому новому сотруднику даём следующий ID
        totalEmployees++;
    }

    public void displayInfo() {
        System.out.println("ID: " + id + ", Имя: " + name);
    }

    // Статический метод — можно вызвать без создания объекта
    public static void showTotalEmployees() {
        System.out.println("Всего сотрудников: " + totalEmployees);
    }
}

public class Main {
    public static void main(String[] args) {
        Employee emp1 = new Employee("Иван");
        Employee emp2 = new Employee("Мария");
        Employee emp3 = new Employee("Пётр");

        emp1.displayInfo(); // ID: 1, Имя: Иван
        emp2.displayInfo(); // ID: 2, Имя: Мария
        emp3.displayInfo(); // ID: 3, Имя: Пётр

        // Вызываем статический метод через имя класса
        Employee.showTotalEmployees(); // Всего сотрудников: 3
    }
}

Пример 5: Массив объектов

public class Book {
    String title;
    String author;
    int pages;

    public Book(String title, String author, int pages) {
        this.title = title;
        this.author = author;
        this.pages = pages;
    }

    void read() {
        System.out.println("Читаю книгу "" + title + "" автора " + author);
    }
}

public class Main {
    public static void main(String[] args) {
        // Создаём массив из 3 книг
        Book[] library = new Book[3];

        // Заполняем массив объектами
        library[0] = new Book("Война и мир", "Лев Толстой", 1225);
        library[1] = new Book("Преступление и наказание", "Фёдор Достоевский", 671);
        library[2] = new Book("Мастер и Маргарита", "Михаил Булгаков", 480);

        // Проходим по всем книгам в массиве
        for (int i = 0; i < library.length; i++) {
            library[i].read();
        }

        // Или через foreach (упрощённый цикл)
        for (Book book : library) {
            System.out.println("Книга: " + book.title + " (" + book.pages + " стр.)");
        }
    }
}

Пример 6: Композиция (класс внутри класса)

public class Engine {
    private int horsepower;
    private String type; // бензин, дизель, электрический

    public Engine(int horsepower, String type) {
        this.horsepower = horsepower;
        this.type = type;
    }

    public void start() {
        System.out.println("Двигатель " + type + " мощностью " + horsepower + " л.с. запущен.");
    }
}

public class Motorcycle {
    private String model;
    private Engine engine; // Композиция: мотоцикл содержит двигатель

    public Motorcycle(String model, int horsepower, String engineType) {
        this.model = model;
        // Создаём двигатель прямо в конструкторе мотоцикла
        this.engine = new Engine(horsepower, engineType);
    }

    public void startMotorcycle() {
        System.out.print("Мотоцикл " + model + ": ");
        engine.start(); // Запускаем двигатель через его метод
    }
}

public class Main {
    public static void main(String[] args) {
        Motorcycle myBike = new Motorcycle("Harley-Davidson", 100, "бензин");
        myBike.startMotorcycle();
        // Вывод: Мотоцикл Harley-Davidson: Двигатель бензин мощностью 100 л.с. запущен.
    }
}

Пример 7: Перегрузка конструкторов

public class Pizza {
    private String size;
    private boolean cheese;
    private boolean pepperoni;
    private boolean mushrooms;

    // Конструктор по умолчанию — маленькая пицца только с сыром
    public Pizza() {
        this("Маленькая", true, false, false);
    }

    // Конструктор с размером
    public Pizza(String size) {
        this(size, true, false, false);
    }

    // Конструктор со всеми параметрами
    public Pizza(String size, boolean cheese, boolean pepperoni, boolean mushrooms) {
        this.size = size;
        this.cheese = cheese;
        this.pepperoni = pepperoni;
        this.mushrooms = mushrooms;
    }

    public void describe() {
        System.out.println("Пицца " + size + ":");
        System.out.println("  Сыр: " + (cheese ? "да" : "нет"));
        System.out.println("  Пепперони: " + (pepperoni ? "да" : "нет"));
        System.out.println("  Грибы: " + (mushrooms ? "да" : "нет"));
    }
}

public class Main {
    public static void main(String[] args) {
        Pizza defaultPizza = new Pizza();
        Pizza mediumPizza = new Pizza("Средняя");
        Pizza customPizza = new Pizza("Большая", true, true, true);

        defaultPizza.describe();
        mediumPizza.describe();
        customPizza.describe();
    }
}

Сравнение с Python: чем похоже и чем отличается

Характеристика Java Python
Объявление класса public class MyClass { } class MyClass:
Конструктор public MyClass() { } def __init__(self):
Ссылка на текущий объект this self
Модификаторы доступа public, private, protected Нет ключевых слов, используется _ и __ (соглашение)
Типизация Строгая, статическая (тип указывается явно) Динамическая (тип определяется во время выполнения)
Геттеры и сеттеры Пишутся вручную Можно использовать @property
Статические методы static @staticmethod
Множественное наследование Нет (только интерфейсы) Да
Сборка мусора Автоматическая (Garbage Collector) Автоматическая (счётчик ссылок)

💡 Главное отличие: Java — это язык со строгой типизацией. Ты должен указать тип каждой переменной, и компилятор проверит, что ты не пытаешься положить строку в числовую переменную. Python — язык с динамической типизацией: ты можешь сначала присвоить переменной число, а потом строку, и это будет работать (но может привести к ошибкам).

Типичные ошибки и подводные камни

Ошибка 1: Забыть ключевое слово new

// НЕПРАВИЛЬНО:
Student student1 = Student(); // Ошибка компиляции!

// ПРАВИЛЬНО:
Student student1 = new Student();

Ошибка 2: Путаница между = и ==

// НЕПРАВИЛЬНО:
if (student1.name = "Анна") { // Ошибка! = это присваивание

// ПРАВИЛЬНО:
if (student1.name.equals("Анна")) { // Для строк используем equals()
// ИЛИ
if (student1.name == "Анна") { // Работает, но не всегда (см. пулы строк)

Ошибка 3: Сравнение объектов через ==

Student s1 = new Student("Анна", 20);
Student s2 = new Student("Анна", 20);

if (s1 == s2) { // Сравниваются ссылки, а не содержимое!
    System.out.println("Одинаковые");
} else {
    System.out.println("Разные"); // Выведет "Разные"
}

// Правильно: переопределить метод equals()
if (s1.equals(s2)) {
    System.out.println("Одинаковые");
}

Ошибка 4: Забыть инициализировать поля

public class Person {
    String name;
    int age;
}

public class Main {
    public static void main(String[] args) {
        Person p = new Person();
        System.out.println(p.name); // null (не ошибка, но может быть неожиданно)
        System.out.println(p.age);  // 0 (значение по умолчанию)
    }
}

Ошибка 5: Неправильное использование this

public class Example {
    int value;

    public void setValue(int value) {
        value = value; // Ошибка! Параметр перекрывает поле
        // Правильно: this.value = value;
    }
}

Ошибка 6: Создание слишком большого количества объектов

// ПЛОХО: создаём объект в цикле без необходимости
for (int i = 0; i < 1000; i++) {
    String temp = new String("Привет"); // Лишний объект!
}

// ХОРОШО:
String temp = "Привет"; // Используем строковый литерал
for (int i = 0; i < 1000; i++) {
    // Используем temp
}

Ошибка 7: Игнорирование принципов инкапсуляции

// ПЛОХО: все поля public
public class User {
    public String password; // Любой может изменить пароль!
}

// ХОРОШО: поля private, доступ через методы
public class User {
    private String password;

    public void setPassword(String password) {
        // Можно добавить проверку сложности пароля
        if (password.length() >= 8) {
            this.password = password;
        }
    }
}

Лучшие практики и советы профессионалов

1. Принцип единственной ответственности

Каждый класс должен делать только одну вещь. Если класс «Студент» умеет и учиться, и печатать диплом, и считать зарплату — это плохо. Раздели на классы Student, Printer, SalaryCalculator.

2. Всегда используйте модификаторы доступа

Поля делайте private. Доступ к ним — через геттеры и сеттеры. Это защитит данные от случайного изменения.

3. Именование

  • Классы — существительные с большой буквы: Car, Student, BankAccount
  • Методы — глаголы с маленькой буквы: run(), getName(), calculateSalary()
  • Поля — существительные с маленькой буквы: name, age, accountNumber
  • Константы — ВСЕ_БОЛЬШИЕ: MAX_SPEED, DEFAULT_NAME

4. Не бойтесь создавать много классов

Лучше 10 маленьких классов, чем 1 огромный. Это упрощает тестирование, чтение и поддержку кода.

5. Используйте конструкторы для обязательных полей

Если объект не может существовать без каких-то данных — передавайте их через конструктор. Не оставляйте объект в «полусозданном» состоянии.

6. Переопределяйте toString()

@Override
public String toString() {
    return "Student{name='" + name + "', age=" + age + "}";
}

Это поможет при отладке — ты всегда сможешь распечатать объект и увидеть его состояние.

7. Избегайте дублирования кода

Если два класса делают похожие вещи — подумай о наследовании или создании общего родительского класса.

Вопросы для самопроверки

  1. Вопрос: В чём разница между классом и объектом?

    Ответ: Класс — это шаблон (чертёж), а объект — конкретный экземпляр, созданный по этому шаблону. Класс описывает, какие поля и методы будут у объекта, а объект — это реальная сущность в памяти с конкретными значениями полей.

  2. Вопрос: Что такое конструктор и зачем он нужен?

    Ответ: Конструктор — это специальный метод, который вызывается при создании объекта. Он нужен для инициализации полей объекта начальными значениями. Имя конструктора совпадает с именем класса, и у него нет возвращаемого типа.

  3. Вопрос: Что произойдёт, если не написать ни одного конструктора в классе?

    Ответ: Java автоматически создаст конструктор по умолчанию — без параметров, который просто вызывает конструктор родительского класса. Но как только ты создашь свой конструктор, «дефолтный» исчезнет.

  4. Вопрос: Для чего нужны модификаторы доступа private и public?

    Ответ: private скрывает данные внутри класса — к ним нельзя обратиться из других классов. public делает данные доступными отовсюду. Это основа инкапсуляции — защиты данных от некорректного использования.

  5. Вопрос: Что такое статическое поле? Чем оно отличается от обычного?

    Ответ: Статическое поле принадлежит классу, а не объекту. Оно одно на все объекты этого класса. Обычное поле — у каждого объекта своё значение. Статические поля можно использовать без создания объекта.

  6. Вопрос: Можно ли вызвать нестатический метод из статического?

    Ответ: Нет, нельзя напрямую. Для вызова нестатического метода нужен объект. Из статического метода можно вызвать только другие статические методы и поля.

  7. Вопрос: Что такое перегрузка методов?

    Ответ: Это создание нескольких методов с одинаковым именем, но разными параметрами (количество, тип или порядок параметров). Java сама определяет, какой метод вызвать, по переданным аргументам.

  8. Вопрос: Как сравнить два объекта на равенство по содержимому?

    Ответ: Нужно переопределить метод equals() в классе. Оператор == сравнивает ссылки (адреса в памяти), а не содержимое объектов.

  9. Вопрос: Что такое сборка мусора (Garbage Collection) в Java?

    Ответ: Это автоматический процесс удаления объектов, на которые больше нет ссылок. Java сама отслеживает, какие объекты больше не используются, и освобождает память. Программисту не нужно вручную удалять объекты.

  10. Вопрос: Можно ли создать объект класса, у которого все поля private и нет сеттеров?

    Ответ: Да, можно. Такой объект называется неизменяемым (immutable). Его состояние задаётся через конструктор и не может быть изменено после создания. Пример — класс String в Java.

Итог и что изучить дальше

Мы разобрали фундамент объектно-ориентированного программирования в Java. Ты узнал:

  • Что такое классы и объекты, и как они связаны с реальным миром
  • Как устроена память Java: стек и куча
  • Как создавать классы, конструкторы, методы
  • Что такое инкапсуляция и зачем нужны модификаторы доступа
  • Как работают статические поля и методы
  • Как избегать типичных ошибок новичков

Это только начало пути в ООП. Вот что стоит изучить дальше:

  1. Наследование — создание классов на основе других классов. Ключевое слово extends.
  2. Полиморфиз