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

Пример: прямоугольник и квадрат

Предположим, мы сначала пишем класс, описывающий прямоугольник, содержащий длины двух сторон a и b, с площадью. strong> вычисляется как их произведение:

class Rectangle {
    private final int a, b;
    protected int area;

    public Rectangle(int a, int b) {
        this.a = a;
        this.b = b;
        calculateArea();
    }

    public void calculateArea() {
        area = a * b;
        System.out.println("Rectangle area = " + area);
    }
}

Поскольку у нас есть вся необходимая информация, мы вычисляем площадь уже во время строительства, распечатывая соответствующее сообщение.

Теперь предположим, что мы хотим определить Квадрат. Поскольку все квадраты являются прямоугольниками, имеет смысл расширить класс Rectangle. Далее мы можем повторно использовать поле area, избегая его дублирования в унаследованном классе.

Однако мы хотели бы адаптировать напечатанное сообщение к квадрату, чего можно добиться путем переопределения метода calculateArea. Хотя мы могли бы повторно использовать значения a и b из базового класса, не имеет смысла описывать квадрат с двумя сторонами, поскольку они всегда имеют одинаковую длину. Поэтому мы просто сохраняем значение a в классе Square:

class Square extends Rectangle {
    private final int a;

    public Square(int a) {
        super(a, a);
        this.a = a;
    }

    @Override
    public void calculateArea() {
        area = a * a;
        System.out.println("Square area = " + area);
    }
}

Наш клиентский код мог бы выглядеть так:

public class Main {
    public static void main(String[] args) {
        Rectangle rectangle = new Rectangle(2, 3);
        Square square = new Square(5);
    }
}

Мы ожидаем, что программа выведет 6 как площадь прямоугольника и 25 как площадь квадрата. Однако он дает неверный результат для площади квадрата:

Rectangle area = 6
Square area = 0

Что пошло не так?

Неверные результаты из-за неинициализированных данных

Конструктор Square должен сначала вызвать конструктор суперкласса. Затем конструктор Rectangle задает значения a и b, после чего вызывает метод computeArea, но какой именно?

Если бы Rectangle вызвал свой собственный метод calculateArea, он выдал бы правильное значение, так как две его стороны были инициализированы в этот момент. Однако Rectangle вызывает метод в унаследованном классе, работая с переменной Square a, которая еще не инициализирована. Значение целого числа по умолчанию равно 0, следовательно, результирующая квадратная площадь. Ссылка this подкласса исчезла во время построения, что привело объект в недопустимое состояние.

Если площадь вычисляется во время строительства, решение состоит в том, чтобы сделать метод непереопределяемым в классе Rectangle, сделав его закрытым, окончательным или статическим. В результате в вызовах методов из базового класса и его унаследованного класса не может возникнуть двусмысленность.

Вывод

Частично инициализированные объекты никогда не должны публиковаться в клиентском коде. Ссылка this не должна исчезать во время построения объекта. Один из способов, которым это может произойти, — вызов переопределяемых методов из конструктора. По этой причине необходимо убедиться, что конструкторы вызывают только закрытые, окончательные или статические методы.