«1. Обзор

В этом коротком уроке мы покажем, как мы можем получить ошибку Cannot reference «X» до вызова конструктора супертипа и как ее избежать.

2. Цепочка конструкторов

Конструктор может вызывать ровно один другой конструктор. Этот вызов должен находиться в первой строке его тела.

Мы можем вызвать конструктор того же класса с ключевым словом this, или мы можем вызвать конструктор суперкласса с ключевым словом super.

Когда конструктор не вызывает другой конструктор, компилятор добавляет вызов конструктору суперкласса без аргументов.

3. Наша ошибка компиляции

Эта ошибка сводится к попытке доступа к элементам уровня экземпляра до того, как мы вызовем цепочку конструкторов.

Давайте посмотрим, как мы можем столкнуться с этим.

3.1. Ссылка на метод экземпляра

В следующем примере мы увидим ошибку компиляции. Невозможно ссылаться на «X» до вызова конструктора супертипа в строке 5. Обратите внимание, что конструктор пытается использовать метод экземпляра getErrorCode() слишком рано. :

public class MyException extends RuntimeException {
    private int errorCode = 0;
    
    public MyException(String message) {
        super(message + getErrorCode()); // compilation error
    }

    public int getErrorCode() {
        return errorCode;
    }
}

Это ошибка, потому что до тех пор, пока super() не завершится, не существует экземпляра класса MyException. Поэтому мы пока не можем вызвать метод экземпляра getErrorCode().

3.2. Обращение к полю экземпляра

В следующем примере мы видим наше исключение с полем экземпляра вместо метода экземпляра. Давайте посмотрим, как первый конструктор пытается использовать член экземпляра до того, как сам экземпляр будет готов:

public class MyClass {

    private int myField1 = 10;
    private int myField2;

    public MyClass() {
        this(myField1); // compilation error
    }

    public MyClass(int i) {
        myField2 = i;
    }
}

Ссылка на поле экземпляра может быть сделана только после инициализации его класса, то есть после любого вызова это() или супер().

Итак, почему во втором конструкторе, который также использует поле экземпляра, нет ошибки компиляции?

Помните, что все классы являются неявными производными от класса Object, поэтому компилятор добавляет неявный вызов super():

public MyClass(int i) {
    super(); // added by compiler
    myField2 = i;
}

Здесь конструктор Object вызывается до того, как мы получим доступ к myField2, что означает мы в порядке.

4. Решения

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

В этом случае мы скопировали бы значение myField1 в myField2:

public class MyClass {

    private int myField1 = 10;
    private int myField2;

    public MyClass() {
        myField2 = myField1;
    }

    public MyClass(int i) {
        myField2 = i;
    }
}

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

Но если мы вызываем второй конструктор по уважительной причине, например, чтобы избежать повторения кода, мы можем переместить код в метод:

public class MyClass {

    private int myField1 = 10;
    private int myField2;

    public MyClass() {
        setupMyFields(myField1);
    }

    public MyClass(int i) {
        setupMyFields(i);
    }

    private void setupMyFields(int i) {
        myField2 = i;
    }
}

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

Третьим решением может быть использование статических полей или методов. Если мы изменим myField1 на статическую константу, то компилятор также будет счастлив:

public class MyClass {

    private static final int SOME_CONSTANT = 10;
    private int myField2;

    public MyClass() {
        this(SOME_CONSTANT);
    }

    public MyClass(int i) {
        myField2 = i;
    }
}

Следует отметить, что статическое поле означает, что оно становится общим для всех экземпляров этого объекта, поэтому это не изменение слишком легко.

Чтобы статика была правильным ответом, нам нужна веская причина. Например, возможно, значение на самом деле не поле, а константа, поэтому имеет смысл сделать его статическим и окончательным. Возможно, метод построения, который мы хотели вызвать, не нуждается в доступе к членам экземпляра класса, то есть он должен быть статическим.

5. Заключение

В этой статье мы видели, как ссылка на члены экземпляра перед вызовом super() или this() приводит к ошибке компиляции. Мы видели, как это происходит с явно объявленным базовым классом, а также с неявным базовым классом Object.

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

Как всегда, исходный код этого примера можно найти на GitHub.