$ sudo teach IT
МОДУЛЬ 7 · УРОК 3

Конструкторы

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

~30 минут Для новичков Java ООП

Зачем нужны конструкторы

Представьте: вы создаёте объект класса Dog и забываете задать ему имя. Без конструктора объект существует, но name = null, age = 0, breed = null. Это бесполезный объект — он ничего не знает о себе.

Конструктор решает эту проблему: он автоматически вызывается при new и задаёт начальные значения. Это как «инструкция по первичной настройке» нового устройства. Вы покупаете телефон — он включается, проходит настройку языка, времени, аккаунта. Конструктор делает то же самое для объекта.

Без конструктора пришлось бы после каждого new вручную вызывать десятки сеттеров. Конструктор берёт это на себя.

Конструктор — это единственное место, где вы ГАРАНТИРОВАННО можете настроить объект. Сеттеры могут быть вызваны позже, а могут и нет. Конструктор вызывается ВСЕГДА при new.

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

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

  1. Нет возвращаемого типа — даже не void. Конструктор не «возвращает» объект — он его настраивает.
  2. Имя конструктора совпадает с именем класса — если класс называется Book, конструктор тоже Book.
  3. Вызывается автоматически при new — вы не можете вызвать конструктор вручную (ну, почти).
public class Book {
    String title;
    String author;

    Book() {
        System.out.println("Создаётся новая книга!");
    }
}

Book b = new Book(); // Выведет: "Создаётся новая книга!"

Обратите внимание: конструктор вызывается сам, без вашего участия. Написали new Book() — конструктор выполнился. Вы даже не можете написать Book b = new Book(); b.Book(); — это не обычный метод.

Вот ещё пример — класс Person:

public class Person {
    String name;
    int age;

    Person() {
        System.out.println("Родился новый человек!");
        name = "Неизвестный";
        age = 0;
    }
}

Person p = new Person(); // "Родился новый человек!"
System.out.println(p.name); // "Неизвестный"
System.out.println(p.age);  // 0

Без конструктора name был бы null, age — 0. С конструктором мы задали осмысленное значение по умолчанию.

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

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

// Вы написали:
public class Dog {
    String name;
    int age;
}

// Java тайно создала:
public class Dog {
    String name;
    int age;

    Dog() {
        // Пустой конструктор — ничего не делает
    }
}

// Поэтому это работает:
Dog d = new Dog(); // OK — конструктор по умолчанию

Конструктор по умолчанию ничего не делает — просто создаёт объект с полями, равными значениям по умолчанию (null, 0, false).

ВАЖНО: как только вы определяете ХОТЯ БЫ ОДИН конструктор, конструктор по умолчанию ИСЧЕЗАЕТ. Это самая частая ошибка новичков! Если вам нужен и конструктор по умолчанию, и параметризованный — напишите оба явно.

public class User {
    String name;

    User(String name) {
        this.name = name;
    }
}

// User u1 = new User(); // Ошибка! Пустой конструктор удалён
User u2 = new User("Иван"); // Работает — параметризованный конструктор

// Правильно — написать оба явно:
public class User {
    String name;

    User() {
        this.name = "Гость";
    }

    User(String name) {
        this.name = name;
    }
}

User u3 = new User();       // OK — "Гость"
User u4 = new User("Иван"); // OK — "Иван"

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

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

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

    Book(String title, String author, int year, int pages) {
        this.title = title;
        this.author = author;
        this.year = year;
        this.pages = pages;
        System.out.println("Создана книга: \"" + title + "\"");
    }
}

Book b = new Book("Война и мир", "Толстой", 1869, 1225);
System.out.println(b.title);  // "Война и мир"
System.out.println(b.author); // "Толстой"
System.out.println(b.year);   // 1869
System.out.println(b.pages);  // 1225

Параметризованный конструктор сразу создаёт объект в правильном состоянии. Не нужно потом вызывать сеттеры — всё задано при создании.

Вот ещё пример — класс Точка на координатной плоскости:

public class Point {
    double x;
    double y;

    Point(double x, double y) {
        this.x = x;
        this.y = y;
    }

    double distanceTo(Point other) {
        double dx = this.x - other.x;
        double dy = this.y - other.y;
        return Math.sqrt(dx * dx + dy * dy);
    }

    void display() {
        System.out.printf("(%.1f, %.1f)%n", x, y);
    }
}

Point p1 = new Point(0, 0);
Point p2 = new Point(3, 4);
p1.display(); // (0.0, 0.0)
p2.display(); // (3.0, 4.0)
System.out.println("Расстояние: " + p1.distanceTo(p2)); // 5.0

Обратите внимание: конструктор Point(x, y) принимает два параметра. Без него мы не смогли бы создать точку — пришлось бы вручную задавать x и y через сеттеры.

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

Перегрузка (overloading) — это когда класс имеет несколько конструкторов с РАЗНЫМИ списками параметров. Это позволяет создавать объекты разными способами.

Аналогия: в restaurantе можно заказать кофе: «крупно» (без параметров), «крупно с молоком» (один параметр), «крупно, молоко, два сахара» (три параметра). Один и тот же продукт, разные варианты.

public class User {
    String name;
    String email;
    int age;

    User() {
        this("Гость", "guest@mail.com", 0);
    }

    User(String name) {
        this(name, name.toLowerCase() + "@mail.com", 18);
    }

    User(String name, String email) {
        this(name, email, 18);
    }

    User(String name, String email, int age) {
        this.name = name;
        this.email = email;
        this.age = age;
    }

    void display() {
        System.out.printf("Имя: %s, Email: %s, Возраст: %d%n", name, email, age);
    }
}

Теперь можно создавать объекты разными способами:

User u1 = new User();
u1.display(); // Имя: Гость, Email: guest@mail.com, Возраст: 0

User u2 = new User("Иван");
u2.display(); // Имя: Иван, Email: ivan@mail.com, Возраст: 18

User u3 = new User("Мария", "maria@mail.com");
u3.display(); // Имя: Мария, Email: maria@mail.com, Возраст: 18

User u4 = new User("Пётр", "petr@mail.com", 25);
u4.display(); // Имя: Пётр, Email: petr@mail.com, Возраст: 25

Java сама выбирает нужный конструктор по количеству и типам параметров. Это называется разрешение перегрузки. Когда вы пишете new User("Иван"), Java ищет конструктор с одним параметром String — и находит.

Перегрузка работает для методов И конструкторов. Главное правило: сигнатуры (имя + список параметров) должны быть РАЗНЫМИ. Два конструктора с одинаковыми параметрами — ошибка компиляции.

Вызов this(): делегирование конструкторов

this() — это вызов другого конструктора того же класса. Он должен быть ПЕРВОЙ строкой в конструкторе. Это позволяет избежать дублирования кода — вся логика инициализации в одном конструкторе, остальные просто перенаправляют.

public class Point {
    int x;
    int y;

    Point() {
        this(0, 0);
    }

    Point(int x) {
        this(x, 0);
    }

    Point(int x, int y) {
        this.x = x;
        this.y = y;
    }

    void display() {
        System.out.println("Точка: (" + x + ", " + y + ")");
    }
}

Point p1 = new Point();       // (0, 0)
Point p2 = new Point(5);      // (5, 0)
Point p3 = new Point(3, 4);   // (3, 4)

Конструктор Point() делегирует работу конструктору Point(int, int). Конструктор Point(int) тоже делегирует. Вся логика инициализации — в одном конструкторе. Остальные просто перенаправляют.

Почему это важно? Потому что без делегирования пришлось бы копировать один и тот же код в каждый конструктор:

// ПЛОХО — дублирование кода:
public class Point {
    int x;
    int y;

    Point() {
        x = 0;
        y = 0;
    }

    Point(int x) {
        this.x = x;
        y = 0;
    }

    Point(int x, int y) {
        this.x = x;
        this.y = y;
    }
}

// ХОРОШо — делегирование:
public class Point {
    int x;
    int y;

    Point() {
        this(0, 0);       // делегирует
    }

    Point(int x) {
        this(x, 0);       // делегирует
    }

    Point(int x, int y) {
        this.x = x;       // вся логика здесь
        this.y = y;
    }
}

this() должен быть ПЕРВОЙ строкой конструктора. Нельзя писать ничего до него. Также нельзя использовать this() и this. (обращение к полям) одновременно в одном конструкторе — это ошибка компиляции.

Валидация в конструкторах

Конструкторы — идеальное место для проверки входных данных. Если данные некорректные — объект не создаётся. Это гарантирует, что каждый объект ВСЕГДА в правильном состоянии.

Аналогия: на заводе деталь проходит контроль качества. Бракованная деталь не отправляется на сборку. Конструктор — это контроль качества для объектов.

public class Person {
    private String name;
    private int age;

    public Person(String name, int age) {
        if (name == null || name.trim().isEmpty()) {
            throw new IllegalArgumentException("Имя не может быть пустым");
        }
        if (age < 0 || age > 150) {
            throw new IllegalArgumentException("Некорректный возраст: " + age);
        }
        this.name = name.trim();
        this.age = age;
    }

    public String getName() {
        return name;
    }

    public int getAge() {
        return age;
    }
}

Person p1 = new Person("Иван", 25); // OK
Person p2 = new Person("", 25);     // IllegalArgumentException!
Person p3 = new Person("Мария", -5); // IllegalArgumentException!

Ещё пример — валидация email:

public class Email {
    private String address;

    public Email(String address) {
        if (address == null || !address.contains("@") || !address.contains(".")) {
            throw new IllegalArgumentException("Некорректный email: " + address);
        }
        this.address = address.toLowerCase();
    }

    public String getAddress() {
        return address;
    }
}

А ещё — валидация возраста сотрудника:

public class Employee {
    private String name;
    private int age;
    private double salary;

    public Employee(String name, int age, double salary) {
        if (name == null || name.trim().isEmpty()) {
            throw new IllegalArgumentException("Имя обязательно");
        }
        if (age < 18 || age > 65) {
            throw new IllegalArgumentException("Возраст сотрудника: 18-65");
        }
        if (salary < 0) {
            throw new IllegalArgumentException("Зарплата не может быть отрицательной");
        }
        this.name = name.trim();
        this.age = age;
        this.salary = salary;
    }

    public String getName() { return name; }
    public int getAge() { return age; }
    public double getSalary() { return salary; }
}

Employee e1 = new Employee("Иванов", 30, 100000); // OK
Employee e2 = new Employee("Петров", 15, 50000);  // Ошибка! Возраст < 18
Employee e3 = new Employee("Сидоров", 40, -10000); // Ошибка! Зарплата < 0

Полный пример: класс BankAccount

Давайте создадим класс Банковский счёт, используя все концепции этого урока:

public class BankAccount {
    private String accountNumber;
    private String ownerName;
    private double balance;
    private boolean isActive;

    public BankAccount(String accountNumber, String ownerName, double initialBalance) {
        if (accountNumber == null || accountNumber.isEmpty()) {
            throw new IllegalArgumentException("Номер счёта обязателен");
        }
        if (ownerName == null || ownerName.trim().isEmpty()) {
            throw new IllegalArgumentException("Имя владельца обязательно");
        }
        if (initialBalance < 0) {
            throw new IllegalArgumentException("Начальный баланс не может быть отрицательным");
        }
        this.accountNumber = accountNumber;
        this.ownerName = ownerName.trim();
        this.balance = initialBalance;
        this.isActive = true;
    }

    public BankAccount(String accountNumber, String ownerName) {
        this(accountNumber, ownerName, 0);
    }

    public void deposit(double amount) {
        if (!isActive) {
            System.out.println("Счёт заблокирован!");
            return;
        }
        if (amount <= 0) {
            System.out.println("Сумма должна быть положительной");
            return;
        }
        balance += amount;
        System.out.printf("Зачислено %.2f руб. Баланс: %.2f руб.%n", amount, balance);
    }

    public boolean withdraw(double amount) {
        if (!isActive) {
            System.out.println("Счёт заблокирован!");
            return false;
        }
        if (amount <= 0) {
            System.out.println("Сумма должна быть положительной");
            return false;
        }
        if (amount > balance) {
            System.out.printf("Недостаточно средств. Баланс: %.2f руб.%n", balance);
            return false;
        }
        balance -= amount;
        System.out.printf("Списано %.2f руб. Баланс: %.2f руб.%n", amount, balance);
        return true;
    }

    public void close() {
        isActive = false;
        System.out.println("Счёт " + accountNumber + " закрыт");
    }

    public String getAccountNumber() { return accountNumber; }
    public String getOwnerName() { return ownerName; }
    public double getBalance() { return balance; }
    public boolean isActive() { return isActive; }

    public void displayInfo() {
        System.out.println("Счёт: " + accountNumber);
        System.out.println("Владелец: " + ownerName);
        System.out.printf("Баланс: %.2f руб.%n", balance);
        System.out.println("Статус: " + (isActive ? "активен" : "закрыт"));
    }
}
BankAccount acc1 = new BankAccount("123456", "Иванов И.И.", 10000);
acc1.deposit(5000);
acc1.withdraw(3000);
acc1.displayInfo();

System.out.println("---");

BankAccount acc2 = new BankAccount("789012", "Петров П.П.");
acc2.deposit(1000);
acc2.displayInfo();

Конструкторы и инкапсуляция

Конструкторы работают в паре с приватными полями и публичными методами. Это основа инкапсуляции:

public class Temperature {
    private double celsius;

    public Temperature(double celsius) {
        setCelsius(celsius);
    }

    public double getCelsius() {
        return celsius;
    }

    public void setCelsius(double celsius) {
        if (celsius < -273.15) {
            throw new IllegalArgumentException("Температура ниже абсолютного нуля");
        }
        this.celsius = celsius;
    }

    public double getFahrenheit() {
        return celsius * 9.0 / 5.0 + 32;
    }

    public void setFahrenheit(double fahrenheit) {
        setCelsius((fahrenheit - 32) * 5.0 / 9.0);
    }
}

Temperature t = new Temperature(37.5);
t.setFahrenheit(212);
System.out.println(t.getCelsius()); // 100.0

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

Ещё примеры перегрузки

Перегрузка конструкторов — мощный инструмент. Вот ещё примеры:

public class Rectangle {
    private double width;
    private double height;

    public Rectangle(double width, double height) {
        this.width = width;
        this.height = height;
    }

    public Rectangle(double side) {
        this(side, side); // Квадрат
    }

    public Rectangle() {
        this(1.0, 1.0); // Единичный квадрат
    }

    public double getArea() { return width * height; }
    public double getPerimeter() { return 2 * (width + height); }

    public void display() {
        System.out.printf("Прямоугольник: %.1f x %.1f%n", width, height);
    }
}

Rectangle r1 = new Rectangle(5, 3);   // 5 x 3
Rectangle r2 = new Rectangle(4);      // 4 x 4 (квадрат)
Rectangle r3 = new Rectangle();        // 1 x 1

Ещё пример — класс Circle:

public class Circle {
    private double radius;

    public Circle(double radius) {
        if (radius <= 0) {
            throw new IllegalArgumentException("Радиус должен быть положительным");
        }
        this.radius = radius;
    }

    public Circle() {
        this(1.0);
    }

    public double getArea() { return Math.PI * radius * radius; }
    public double getCircumference() { return 2 * Math.PI * radius; }
    public double getDiameter() { return 2 * radius; }

    public void display() {
        System.out.printf("Круг: r=%.1f, площадь=%.1f, длина окружности=%.1f%n",
            radius, getArea(), getCircumference());
    }
}

Circle c1 = new Circle(5);
Circle c2 = new Circle();
c1.display(); // r=5.0, площадь=78.5, длина окружности=31.4
c2.display(); // r=1.0, площадь=3.1, длина окружности=6.3

Типичные ошибки

public class Mistakes {
    private String name;

    // Ошибка 1: объявление возвращаемого типа
    void Mistakes(String name) { // Это МЕТОД, не конструктор!
        this.name = name;
    }

    // Правильно:
    Mistakes(String name) {
        this.name = name;
    }

    // Ошибка 2: this() не первая строка
    Mistakes() {
        System.out.println("Привет");
        this("Гость"); // Ошибка! this() должен быть первым
    }

    // Ошибка 3: вызов this() и this. в одном конструкторе
    Mistakes(String name, int age) {
        this(); // ОК
        this.name = name; // Ошибка! Нельзя и this(), и this.
    }

    // Ошибка 4: забыли вызвать this() или задать поля
    Mistakes(int x) {
        // name остался null!
    }
}

Итоги урока

  • Конструктор — специальный метод, нет возвращаемого типа, имя = имя класса.
  • Вызывается автоматически при new.
  • Конструктор по умолчанию существует только если нет других конструкторов.
  • Параметризованные конструкторы инициализируют поля сразу.
  • Перегрузка: несколько конструкторов с разными параметрами.
  • this() вызывает другой конструктор (должен быть первой строкой).
  • Валидация в конструкторах гарантирует правильное состояние объекта.
  • Делегирование через this() избегает дублирования кода.

Следующий урок: Модификаторы доступа — public, private, protected и инкапсуляция.

Тест: Конструкторы

10 вопросов

Конструктор с валидацией

Premium