Spec-Zone.ru › Java Language Specification 17

Глава 8. Классы

Оглавление

8.1. Объявления классов
8.1.1. Модификаторы классов
8.1.1.1. abstract классы
8.1.1.2. sealed, non-sealed и final классы
8.1.1.3. strictfp классы
8.1.1.4. static классы
8.1.2. Обобщенные классы и параметры типов
8.1.3. Вложенные классы и окружающие экземпляры
8.1.4. Суперклассы и подклассы
8.1.5. Суперинтерфейсы
8.1.6. Разрешённые непосредственные подклассы
8.1.7. Тело класса и объявления членов
8.2. Члены класса
8.3. Объявления полей
8.3.1. Модификаторы полей
8.3.1.1. static поля
8.3.1.2. final поля
8.3.1.3. transient поля
8.3.1.4. volatile поля
8.3.2. Инициализация полей
8.3.3. Ограничения на ссылки на поля в инициализаторах
8.4. Объявления методов
8.4.1. Формальные параметры
8.4.2. Подпись метода
8.4.3. Модификаторы методов
8.4.3.1. abstract методы
8.4.3.2. static методы
8.4.3.3. final методы
8.4.3.4. native методы
8.4.3.5. strictfp методы
8.4.3.6. synchronized методы
8.4.4. Обобщенные методы
8.4.5. Результат метода
8.4.6. Исключение метода
8.4.7. Тело метода
8.4.8. Наследование, переопределение и скрытие
8.4.8.1. Переопределение (методами экземпляра)
8.4.8.2. Скрытие (методами класса)
8.4.8.3. Требования при переопределении и скрытии
8.4.8.4. Наследование методов с эквивалентными подписями переопределения
8.4.9. Перегрузка
8.5. Объявления классов-членов и интерфейсов
8.6. Инициализаторы экземпляров
8.7. Статические инициализаторы
8.8. Объявления конструкторов
8.8.1. Формальные параметры
8.8.2. Подпись конструктора
8.8.3. Модификаторы конструктора
8.8.4. Обобщенные конструкторы
8.8.5. Исключение конструктора
8.8.6. Тип конструктора
8.8.7. Тело конструктора
8.8.7.1. Явные вызовы конструкторов
8.8.8. Перегрузка конструкторов
8.8.9. Конструктор по умолчанию
8.8.10. Препятствие к созданию экземпляра класса
8.9. Классы перечислений
8.9.1. Константы перечисления
8.9.2. Объявления тела класса перечисления
8.9.3. Члены перечисления
8.10. Классы записей
8.10.1. Компоненты записи
8.10.2. Объявления тела класса записи
8.10.3. Члены записи
8.10.4. Объявления конструкторов класса записи
8.10.4.1. Нормальные канонические конструкторы
8.10.4.2. Компактные канонические конструкторы

Объявление класса определяет новый класс и описывает его реализацию (§8.1).

Класс верхнего уровня (§7.6) — это класс, объявленный непосредственно в единице компиляции.

Вложенный класс — это любой класс, объявление которого находится внутри тела другого объявления класса или интерфейса. Вложенный класс может быть классом-членом (§8.5, §9.5), локальным классом (§14.3) или анонимным классом (§15.9.5).

Некоторые виды вложенных классов являются внутренними классами (§8.1.3), которые являются классами, которые могут ссылаться на экземпляры окружающих классов, локальные переменные и переменные типа.

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

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

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

Класс может быть объявлен public (§8.1.1), поэтому на него можно ссылаться из кода в любом пакете его модуля и, возможно, из кода в других модулях.

Класс может быть объявлен abstract (§8.1.1.1), и должен быть объявлен abstract, если он неполностью реализован; такой класс не может быть создан, но может быть расширен подклассами. Степень расширения класса может быть явно контролируема (§8.1.1.2): он может быть объявлен sealed для ограничения его подклассов или объявлен final для гарантии отсутствия подклассов. Каждый класс, кроме Object, является расширением (то есть подклассом) единственного существующего класса (§8.1.4) и может реализовывать интерфейсы (§8.1.5).

Класс может быть обобщенным (§8.1.2), то есть его объявление может ввести переменные типа, значения которых отличаются в разных экземплярах класса.

Объявления классов могут быть снабжены аннотациями (§9.7), как и любой другой вид объявления.

END_OF_DOCUMENT_MARKER

Тело класса объявляет члены (поля, методы, классы и интерфейсы), инициализаторы экземпляров и статические инициализаторы, а также конструкторы (§8.1.7). Область видимости (§6.3) члена (§8.2) охватывает всё тело объявления класса, к которому принадлежит член. Объявления полей, методов, вложенных классов, вложенных интерфейсов и конструкторов могут включать модификаторы доступа public, protected или private (§6.6). Члены класса включают как объявленные, так и унаследованные члены (§8.2). Новые объявленные поля могут скрывать поля, объявленные в суперклассе или суперинтерфейсе. Новые объявленные вложенные классы и вложенные интерфейсы могут скрывать вложенные классы и вложенные интерфейсы, объявленные в суперклассе или суперинтерфейсе. Новые объявленные методы могут скрывать, реализовывать или переопределять методы, объявленные в суперклассе или суперинтерфейсе.

Объявления полей (§8.3) описывают переменные класса, которые инициализируются один раз, и переменные экземпляра, которые инициализируются заново для каждого экземпляра класса. Поле может быть объявлено final (§8.3.1.2), в этом случае оно может быть присвоено только один раз. Любое объявление поля может включать инициализатор.

Объявления вложенных классов (§8.5) описывают вложенные классы, которые являются членами окружающего класса. Вложенные классы могут быть static, в этом случае они не имеют доступа к переменным экземпляра окружающего класса; или они могут быть внутренними классами.

Объявления вложенных интерфейсов (§8.5) описывают вложенные интерфейсы, которые являются членами окружающего класса.

Объявления методов (§8.4) описывают код, который может быть вызван выражениями вызова метода (§15.12). Метод класса вызывается относительно класса; метод экземпляра вызывается относительно конкретного объекта, являющегося экземпляром класса. Метод, объявление которого не указывает, как он реализован, должен быть объявлен abstract. Метод может быть объявлен final (§8.4.3.3), в этом случае он не может быть скрыт или переопределён. Метод может быть реализован платформенно-зависимым native кодом (§8.4.3.4). synchronized метод (§8.4.3.6) автоматически блокирует объект перед выполнением своего тела и автоматически разблокирует объект при возвращении, как если бы он использовал оператор synchronized (§14.19), что позволяет его действиям синхронизироваться с действиями других потоков (§17 (Threads and Locks)).

Имена методов могут быть перегружены (§8.4.9).

Инициализаторы экземпляров (§8.6) — это блоки исполняемого кода, которые могут использоваться для инициализации экземпляра при его создании (§15.9).

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

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

8.1. Объявления классов

Объявление класса определяет класс.

Существуют три вида объявлений классов: обычные объявления классов, объявления перечислений (§8.9) и объявления записей (§8.10).

ClassDeclaration:
NormalClassDeclaration
EnumDeclaration
RecordDeclaration
NormalClassDeclaration:
{МодификаторКласса} class ИдентификаторТипа [ПараметрыТипов] [НаследуемыйКласс] [РеализуемыеИнтерфейсы] [РазрешенияКласса] ТелоКласса

Класс также неявно объявляется выражением создания экземпляра класса (§15.9.5) и константой перечисления, завершающейся телом класса (§8.9.1).

ИдентификаторТипа в объявлении класса указывает имя класса.

Если класс имеет то же простое имя, что и любой из его содержащих классов или интерфейсов, возникает ошибка компиляции.

Область видимости и перекрытие объявления класса указаны в §6.3 и §6.4.1.

8.1.1. Модификаторы классов

В объявлении класса могут присутствовать модификаторы класса.

ClassModifier:
(один из)
Аннотация public protected private
abstract static final sealed non-sealed strictfp

Правила, касающиеся модификаторов аннотаций для объявления класса, указаны в §9.7.4 и §9.7.5.

Модификатор доступа public (§6.6) относится только к классам верхнего уровня (§7.6) и вложенным классам (§8.5, §9.5), а не к локальным классам (§14.3) или анонимным классам (§15.9.5).

Модификаторы доступа protected и private относятся только к вложенным классам.

Модификатор static относится только к вложенным и локальным классам.

Если один и тот же ключевое слово появляется более одного раза в качестве модификатора для объявления класса, или если объявление класса имеет более одного модификатора доступа public, protected и private, возникает ошибка компиляции.

Если объявление класса имеет более одного из модификаторов sealed, non-sealed и final, возникает ошибка компиляции.

Если в объявлении класса присутствует два или более (разных) модификатора класса, то обычно, хотя и не обязательно, они располагаются в порядке, согласованном с приведенным выше в производстве для МодификатораКласса.

8.1.1.1. Абстрактные классы

Абстрактный класс — это класс, который неполный или должен считаться неполным.

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

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

Обычный класс может иметь абстрактные методы, то есть методы, которые объявлены, но еще не реализованы (§8.4.3.1), только если это абстрактный класс. Возникает ошибка компиляции, если обычный класс, который не является абстрактным, имеет абстрактный метод.

Класс C имеет абстрактные методы, если выполняется одно из следующих условий:

  • Любой из методов членов (§8.2) класса C — объявленный или унаследованный — является абстрактным.

  • У любого из суперклассов C есть абстрактный метод, объявленный с доступом пакета, и не существует метода, который переопределяет абстрактный метод из C или из суперкласса C.

Возникает ошибка компиляции при объявлении типа абстрактного класса таким образом, что невозможно создать подкласс, который реализует все его абстрактные методы. Эта ситуация может возникнуть, если класс будет иметь два абстрактных метода с одинаковой сигнатурой метода (§8.4.2), но типы возвращаемых значений, для которых нет типа, заменяемого типом возвращаемого значения обоих (§8.4.5).

Пример 8.1.1.1-1. Объявление абстрактного класса

abstract class Point {
    int x = 1, y = 1;
    void move(int dx, int dy) {
        x += dx;
        y += dy;
        alert();
    }
    abstract void alert();
}
abstract class ColoredPoint extends Point {
    int color;
}
class SimplePoint extends Point {
    void alert() { }
}

Здесь объявлен класс Point, который должен быть объявлен абстрактным, потому что он содержит объявление абстрактного метода с именем alert. Подкласс Point с именем ColoredPoint наследует абстрактный метод alert, поэтому он также должен быть объявлен абстрактным. С другой стороны, подкласс Point с именем SimplePoint предоставляет реализацию alert, поэтому он не обязательно должен быть абстрактным.

Выражение:

Point p = new Point();

приведет к ошибке компиляции; класс Point не может быть создан, потому что он абстрактный. Однако переменная типа Point может быть правильно инициализирована ссылкой на любой подкласс Point, и класс SimplePoint не абстрактный, поэтому оператор:

Point p = new SimplePoint();

будет корректным. Создание экземпляра абстрактного класса приводит к выполнению конструктора по умолчанию и инициализаторов полей для x и y класса Point.


Пример 8.1.1.1-2. Объявление абстрактного класса, запрещающего создание подклассов

interface Colorable {
    void setColor(int color);
}
abstract class Colored implements Colorable {
    public abstract int setColor(int color);
}

Эти объявления приводят к ошибке компиляции: для любого подкласса класса Colored будет невозможно предоставить реализацию метода с именем setColor, принимающего один аргумент типа int, удовлетворяющего обеим спецификациям абстрактных методов, поскольку одна из них в интерфейсе Colorable требует, чтобы метод не возвращал никакого значения, а другая в классе Colored требует, чтобы этот же метод возвращал значение типа int (§8.4).


Тип класса должен быть объявлен абстрактным только в том случае, если предполагается, что могут быть созданы подклассы для завершения реализации. Если цель состоит просто в предотвращении создания экземпляров класса, правильный способ выразить это — объявить конструктор без аргументов (§8.8.10), сделать его абстрактным, никогда не вызывать его и не объявлять других конструкторов. Класс такого вида обычно содержит методы и переменные класса.

Класс Math является примером класса, который нельзя создать; его объявление выглядит так:


public final class Math {
    private Math() { }  // never instantiate this class
    . . . declarations of class variables and methods . . .
}

8.1.1.2. sealed, non-sealed, и final классы

Класс может быть объявлен sealed, если все его непосредственные подклассы известны на момент объявления класса (§8.1.6), и другие непосредственные подклассы не требуются или нежелательны.

Явное и исчерпывающее управление непосредственными подклассами класса полезно, когда иерархия классов используется для моделирования типов значений в области, а не как механизм наследования и повторного использования кода. Непосредственные подклассы могут быть объявлены sealed, чтобы дополнительно контролировать иерархию классов.

Класс может быть объявлен final, если его определение завершено, и подклассы не требуются или нежелательны.

Если класс объявлен одновременно final и abstract, это ошибка компиляции, поскольку реализация такого класса никогда не может быть завершена (§8.1.1.1).

Поскольку у класса final никогда нет подклассов, методы класса final никогда не переопределяются (§8.4.8.1).

Класс является свободно расширяемым, если его непосредственный суперкласс не является sealed (§8.1.4), ни один из его непосредственных суперинтерфейсов не является sealed (§8.1.5), и он сам не является sealed или final.

Класс, имеющий sealed непосредственный суперкласс или sealed непосредственный суперинтерфейс, является свободно расширяемым только тогда, когда он объявлен non-sealed.

Если класс имеет sealed непосредственный суперкласс или sealed непосредственный суперинтерфейс и не объявлен final, sealed или non-sealed явно или неявно, это ошибка компиляции.

Таким образом, ключевое слово sealed заставляет все непосредственные подклассы явно объявлять, являются ли они final, sealed или non-sealed. Это предотвращает случайное предоставление доступа к иерархии запечатанных классов для нежелательного наследования.

Класс перечисления неявно final или неявно sealed, поэтому он может реализовать интерфейс sealed. Аналогично, класс записи неявно final, поэтому он также может реализовать запечатанный интерфейс.

Если класс объявлен non-sealed, но не имеет ни sealed непосредственного суперкласса, ни sealed непосредственного суперинтерфейса, это ошибка компиляции.

Таким образом, подкласс класса non-sealed сам не может быть объявлен non-sealed.

8.1.1.3. strictfp классы

Модификатор strictfp в объявлении класса устарел и не должен использоваться в новом коде. Его присутствие или отсутствие не влияет на компиляцию или выполнение.

8.1.1.4. static классы

Модификатор static указывает, что вложенный класс не является внутренним классом (§8.1.3). Так же, как метод static класса не имеет текущего экземпляра класса в своём теле, static вложенный класс не имеет непосредственно окружающего экземпляра в своём теле.

Ссылок из static вложенного класса на параметры типа, переменные экземпляра, локальные переменные, формальные параметры, параметры исключений или методы экземпляра лексически окружающего класса, интерфейса или объявления метода запрещено (§6.5.5.1, §6.5.6.1 и §15.12.3).

Модификатор static не относится ко всем вложенным классам. Он относится только к членам классов, в объявлениях которых может использоваться модификатор static, а не к локальным классам или анонимным классам, в объявлениях которых модификатор static не может быть использован (§14.3, §15.9.5). Однако некоторые локальные классы неявно static, а именно локальные классы перечисления и локальные классы записей, поскольку все вложенные классы перечисления и вложенные классы записей неявно static (§8.9, §8.10).

8.1.2. Обобщённые классы и параметры типа

Класс является обобщённым, если объявление класса содержит одну или несколько переменных типа (§4.4).

Эти переменные типа называются параметрами типа класса. Раздел параметров типа следует за именем класса и ограничен угловыми скобками.

TypeParameters:
< Список параметров типа >
TypeParameterList:
Параметр типа {, Параметр типа}

Следующие производные из §4.4 показаны здесь для удобства:

TypeParameter:
{Модификатор параметра типа} Идентификатор типа [Граница типа]
TypeParameterModifier:
Аннотация
TypeBound:
extends Переменная типа
extends Тип класса или интерфейса {Дополнительная граница}
AdditionalBound:
& Тип интерфейса

Правила, касающиеся модификаторов аннотаций для объявления параметра типа, указаны в §9.7.4 и §9.7.5.

В разделе параметров типа класса переменная типа T непосредственно зависит от переменной типа S, если S является границей T, в то время как T зависит от S, если либо T непосредственно зависит от S, либо T непосредственно зависит от переменной типа U, которая зависит от S (используя это определение рекурсивно).

Если переменная типа в разделе параметров типа класса зависит от самой себя, это ошибка компиляции.

Область действия и перекрытие параметра типа класса определены в §6.3 и §6.4.1.

Ссылки на параметр типа класса из статического контекста или вложенного класса или интерфейса ограничены, как указано в §6.5.5.1.

Объявление обобщённого класса определяет набор параметризованных типов (§4.5), по одному для каждого возможного параметризации раздела параметров типа с помощью аргументов типа. Все эти параметризованные типы разделяют один и тот же класс во время выполнения.

Например, выполнение кода:


Vector<String>  x = new Vector<String>();
Vector<Integer> y = new Vector<Integer>();
boolean b = x.getClass() == y.getClass();

приведёт к тому, что переменная b будет содержать значение true.

Если обобщённый класс является непосредственным или косвенным подклассом Throwable (§11.1.1), это ошибка компиляции.

Это ограничение необходимо, так как механизм обработки исключений Java Virtual Machine работает только с необобщёнными классами.

Пример 8.1.2-1. Взаимно рекурсивные границы переменных типа

interface ConvertibleTo<T> {
    T convert();
}
class ReprChange<T extends ConvertibleTo<S>,
                 S extends ConvertibleTo<T>> { 
    T t; 
    void set(S s) { t = s.convert();    } 
    S get()       { return t.convert(); } 
}

Пример 8.1.2-2. Вложенные обобщённые классы

class Seq<T> { 
    T      head;
    Seq<T> tail;

    Seq() { this(null, null); } 
    Seq(T head, Seq<T> tail) {
        this.head = head;
        this.tail = tail;
    }
    boolean isEmpty() { return tail == null; }

    class Zipper<S> { 
        Seq<Pair<T,S>> zip(Seq<S> that) { 
            if (isEmpty() || that.isEmpty()) {
                return new Seq<Pair<T,S>>(); 
            } else {
                Seq<T>.Zipper<S> tailZipper =
                    tail.new Zipper<S>();
                return new Seq<Pair<T,S>>( 
                    new Pair<T,S>(head, that.head),
                    tailZipper.zip(that.tail));
            }
        }
    }
}
class Pair<T, S> {
    T fst; S snd;
    Pair(T f, S s) { fst = f; snd = s; }
}
class Test {
    public static void main(String[] args) {
        Seq<String> strs =
            new Seq<String>(
                "a",
                new Seq<String>("b",
                                new Seq<String>()));
        Seq<Number> nums =
            new Seq<Number>(
                new Integer(1),
                new Seq<Number>(new Double(1.5),
                                new Seq<Number>()));

        Seq<String>.Zipper<Number> zipper =
            strs.new Zipper<Number>();

        Seq<Pair<String,Number>> combined =
            zipper.zip(nums);
    }
}

8.1.3. Вложенные классы и окружающие экземпляры

Вложенный класс — это вложенный класс, который не явным или неявным образом static.

Вложенный класс может быть одним из следующих:

  • классом-членом, который не явным или неявным образом static (§8.5)

  • локальным классом, который не неявным образом static (§14.3)

  • анонимным классом (§15.9.5)

Следующие вложенные классы неявным образом static, поэтому не являются вложенными классами:

  • перечисление-член (§8.9)

  • локальное перечисление (§14.3)

  • класс-запись-член (§8.10)

  • локальный класс-запись (§14.3)

  • класс-член интерфейса (§9.5)

Все правила, которые применяются к вложенным классам, применяются и к вложенным классам. В частности, вложенный класс может объявлять и наследовать static члены (§8.2) и объявлять статические инициализаторы (§8.7), даже если сам вложенный класс не static.

Нет «вложенных интерфейсов», поскольку каждый вложенный интерфейс неявным образом static (§9.1.1.3).

Пример 8.1.3-1. Объявления вложенных классов и статических членов

class HasStatic {
    static int j = 100;
}

class Outer {
    class Inner extends HasStatic {
        static {
            System.out.println("Hello from Outer.Inner");
        }

        static       int x = 3;
        static final int y = 4;

        static void hello() {
            System.out.println("Hello from Outer.Inner.hello");
        }

        static class VeryNestedButNotInner
            extends NestedButNotInner {}
    }
    
    static class NestedButNotInner {
        int z = Inner.x;
    }
    
    interface NeverInner {}  // Implicitly static, so never inner
}

До Java SE 16 вложенный класс не мог объявлять статические инициализаторы и мог объявлять только static члены, которые были константами (§4.12.4).


Конструкт (выражение, объявление локальной переменной, объявление локального класса, объявление локального интерфейса или выражение) располагается в статическом контексте, если самый внутренний:

  • объявление метода,

  • объявление поля,

  • объявление конструктора,

  • инициализатор экземпляра,

  • статический инициализатор или

  • выражение явного вызова конструктора

который включает конструкт, является одним из следующих:

  • объявление static метода (§8.4.3.2, §9.4)

  • объявление static поля (§8.3.1.1, §9.3)

  • статический инициализатор (§8.7)

  • выражение явного вызова конструктора (§8.8.7.1)

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

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

  • выражения this (как неквалифицированные, так и квалифицированные) запрещены (§15.8.3, §15.8.4).

  • Доступ к полям, вызовы методов и ссылки на методы не могут быть квалифицированы с помощью super (§15.11.2, §15.12.3, §15.13.1).

  • Неквалифицированные ссылки на переменные экземпляра любого лексически окружающего класса или объявления интерфейса запрещены (§6.5.6.1).

  • Неквалифицированные вызовы методов экземпляра любого лексически окружающего класса или объявления интерфейса запрещены (§15.12.3).

  • Ссылки на параметры типа любых лексически окружающих классов или объявления интерфейсов запрещены (§6.5.5.1).

  • Ссылки на параметры типа, локальные переменные, формальные параметры и параметры исключений, объявленные методами или конструкторами любого лексически окружающего класса или объявления интерфейса , который находится за пределами непосредственно окружающего класса или объявления интерфейса, запрещены (§6.5.5.1, §6.5.6.1).

  • Объявления локальных обычных классов (в отличие от локальных перечислений) и объявления анонимных классов оба указывают на классы, которые являются внутренними, но при создании не имеют непосредственно окружающих экземпляров (§15.9.2).

  • Выражения создания экземпляров класса, которые создают экземпляры внутренних классов-членов, должны быть квалифицированы (§15.9).

Вложенный класс C является непосредственным вложенным классом класса или интерфейса O, если O — это непосредственно окружающее объявление класса или интерфейса C, а объявление C не происходит в статическом контексте.

Если вложенный класс является локальным классом или анонимным классом, он может быть объявлен в статическом контексте, и в этом случае не считается вложенным классом любого окружающего класса или интерфейса.

Класс C является вложенным классом класса или интерфейса O, если он является либо непосредственным вложенным классом O, либо вложенным классом вложенного класса O.

Хотя это не типично, но возможно, что непосредственно окружающим классом или объявлением интерфейса вложенного класса является интерфейс. Это происходит только если класс является локальным или анонимным классом, объявленным в default или static теле метода (§9.4).

Класс или интерфейс O является нулевым лексически окружающим классом или объявлением интерфейса самого себя.

Класс O является n-ым лексически окружающим классом объявления класса C, если он является непосредственно окружающим классом объявления (n-1)-го лексически окружающего класса C.

Экземпляр i непосредственного вложенного класса C класса или интерфейса O связан с экземпляром O, известным как непосредственно окружающий экземпляр i. Непосредственно окружающий экземпляр объекта, если таковой имеется, определяется при создании объекта (§15.9.2).

Объект o является нулевым лексически окружающим экземпляром самого себя.

Объект o является n-ым лексически окружающим экземпляром экземпляра i, если он является непосредственно окружающим экземпляром (n-1)-го лексически окружающего экземпляра i.

Экземпляр локального вложенного класса или анонимного класса, объявление которого происходит в статическом контексте, не имеет непосредственно окружающего экземпляра. Также экземпляр static вложенного класса (§8.1.1.4) не имеет непосредственно окружающего экземпляра.

Для каждого суперкласса S класса C, являющегося непосредственным внутренним классом класса или интерфейса SO, существует экземпляр SO, связанный с i, известный как непосредственно окружающий экземпляр i относительно S. Непосредственно окружающий экземпляр объекта относительно его непосредственного суперкласса, если таковой имеется, определяется при вызове конструктора суперкласса посредством явного оператора вызова конструктора (§8.8.7.1).

Когда внутренний класс (объявление которого не происходит в статическом контексте) ссылается на переменную экземпляра, являющуюся членом лексически окружающего класса или интерфейса, используется переменная соответствующего лексически окружающего экземпляра.

Любая локальная переменная, формальный параметр или параметр исключения, используемый, но не объявленный во внутреннем классе, должен быть либо final, либо эффективно постоянным (§4.12.4), как указано в §6.5.6.1.

Любая локальная переменная, используемая, но не объявленная во внутреннем классе, должна быть однозначно присвоена (§16 (Однозначное присвоение)) перед телом внутреннего класса, иначе произойдёт ошибка компиляции.

Аналогичные правила использования переменных применяются в теле лямбда-выражения (§15.27.2).

Пустое final поле (§4.12.4) лексически окружающего класса или интерфейса не может быть присвоено внутри внутреннего класса, иначе произойдёт ошибка компиляции.

Пример 8.1.3-2. Объявления внутренних классов

class Outer {
    int i = 100;
    static void classMethod() {
        final int l = 200;
        class LocalInStaticContext {
            int k = i;  // Compile-time error
            int m = l;  // OK
        }
    }
    void foo() {
        class Local {  // A local class
            int j = i;
        }
    }
}

Объявление класса LocalInStaticContext происходит в статическом контексте из-за нахождения внутри статического метода classMethod. Переменные экземпляра класса Outer недоступны внутри тела статического метода. В частности, переменные экземпляра Outer недоступны внутри тела LocalInStaticContext. Однако, локальные переменные из окружающего метода могут быть использованы без ошибок (при условии, что они объявлены final или эффективно постоянными).

Внутренние классы, объявления которых не происходят в статическом контексте, могут свободно ссылаться на переменные экземпляра их окружающего класса. Переменная экземпляра всегда определена относительно экземпляра. В случае переменных экземпляра окружающего класса, переменная экземпляра должна быть определена относительно окружающего экземпляра внутреннего класса. Например, класс Local выше имеет окружающий экземпляр класса Outer. Как ещё один пример:

class WithDeepNesting {
    boolean toBe;
    WithDeepNesting(boolean b) { toBe = b; }

    class Nested {
        boolean theQuestion;
        class DeeplyNested {
            DeeplyNested(){
                theQuestion = toBe || !toBe;
            }
        }
    }
}

Здесь каждый экземпляр WithDeepNesting.Nested.DeeplyNested имеет окружающий экземпляр класса WithDeepNesting.Nested (его непосредственно окружающий экземпляр) и окружающий экземпляр класса WithDeepNesting (его 2-ой лексически окружающий экземпляр).


8.1.4. Базовые и производные классы

Необязательная extends часть в объявлении обычного класса указывает тип непосредственного базового класса объявляемого класса.

ClassExtends:
extends Тип класса

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

Тип класса должен указывать доступный класс (§6.6), в противном случае возникает ошибка компиляции.

Возникает ошибка компиляции, если Тип класса указывает класс, который является sealed (§8.1.1.2), и объявляемый класс не является допустимым непосредственным подклассом указанного класса (§8.1.6).

Возникает ошибка компиляции, если Тип класса указывает класс, который является final, потому что классы final не могут иметь подклассы (§8.1.1.2).

Возникает ошибка компиляции, если Тип класса указывает класс Enum, который может быть расширен только перечислением (§8.9), или указывает класс Record, который может быть расширен только классом-записью (§8.10).

Если у Тип класса есть аргументы типа, он должен обозначать правильно сформированный параметризованный тип (§4.5), и ни один из аргументов типа не может быть аргументами типа с подстановочными знаками, в противном случае возникает ошибка компиляции.

Непосредственный базовый тип класса, в объявлении которого отсутствует extends, определяется следующим образом:

  • У класса Object нет непосредственного базового типа.

  • Для класса, отличного от Object, с объявлением обычного класса, непосредственный базовый тип — Object.

  • Для класса перечисления E непосредственный базовый тип — Enum<E>.

  • Для анонимного класса непосредственный базовый тип определяется в §15.9.5.

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

Отношение «базовый класс» — это транзитивное замыкание отношения «непосредственный базовый класс». Класс A является базовым классом класса C, если выполняется одно из следующих условий:

  • A — непосредственный базовый класс C.

  • Где класс B — непосредственный базовый класс C, A — базовый класс B, применяя это определение рекурсивно.

Говорят, что класс является непосредственным подклассом своего непосредственного базового класса и подклассом каждого из своих базовых классов.

Пример 8.1.4-1. Непосредственные базовые классы и подклассы

class Point { int x, y; }
final class ColoredPoint extends Point { int color; }
class Colored3DPoint extends ColoredPoint { int z; }  // error

Здесь отношения таковы:

  • Класс Point является непосредственным подклассом класса Object.

  • Класс Object — непосредственный базовый класс класса Point.

  • Класс ColoredPoint является непосредственным подклассом класса Point.

  • Класс Point — непосредственный базовый класс класса ColoredPoint.

Объявление класса Colored3dPoint приводит к ошибке компиляции, так как он пытается расширить конечный класс ColoredPoint.


Пример 8.1.4-2. Базовые и производные классы

class Point { int x, y; }
class ColoredPoint extends Point { int color; }
final class Colored3dPoint extends ColoredPoint { int z; }

Здесь отношения таковы:

  • Класс Point является базовым классом класса ColoredPoint.

  • Класс Point является базовым классом класса Colored3dPoint.

  • Класс ColoredPoint является производным классом класса Point.

  • Класс ColoredPoint является базовым классом класса Colored3dPoint.

  • Класс Colored3dPoint является производным классом класса ColoredPoint.

  • Класс Colored3dPoint является производным классом класса Point.


Класс C непосредственно зависит от класса или интерфейса A, если A указан в extends или implements части C в качестве базового класса или базового интерфейса, или в качестве квалификатора в полном квалифицированном имени базового класса или базового интерфейса.

Класс C зависит от класса или интерфейса A, если выполняется одно из следующих условий:

  • C непосредственно зависит от A.

  • C непосредственно зависит от интерфейса I, который зависит (§9.1.3) от A.

  • C непосредственно зависит от класса B, который зависит от A, применяя это определение рекурсивно.

Возникает ошибка компиляции, если класс зависит от самого себя.

Если при выполнении обнаруживаются циклически объявленные классы, при загрузке классов выбрасывается исключение ClassCircularityError (§12.2.1).

Пример 8.1.4-3. Класс зависит от самого себя

class Point extends ColoredPoint { int x, y; }
class ColoredPoint extends Point { int color; }

Эта программа приводит к ошибке компиляции, так как класс Point зависит от самого себя.


8.1.5. Суперинтерфейсы

Необязательная implements часть в объявлении класса указывает прямые типы суперинтерфейсов объявляемого класса.

ClassImplements:
implements Список типов интерфейсов
InterfaceTypeList:
Тип интерфейса {, Тип интерфейса}

Каждый Тип интерфейса должен указывать доступный интерфейс (§6.6), в противном случае произойдет ошибка компиляции.

Если любой Тип интерфейса указывает интерфейс, который является sealed (§9.1.1.4), и объявляемый класс не является разрешенным прямым подклассом указанного интерфейса (§9.1.4), произойдет ошибка компиляции.

Если у Тип интерфейса есть аргументы типа, он должен обозначать правильно сформированный параметризованный тип (§4.5), и ни один из аргументов типа не должен быть аргументом типа с подстановочным знаком, в противном случае произойдет ошибка компиляции.

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

Пример 8.1.5-1. Недопустимые суперинтерфейсы

class Redundant implements java.lang.Cloneable, Cloneable {
    int x;
}

Эта программа приводит к ошибке компиляции, потому что имена java.lang.Cloneable и Cloneable ссылаются на один и тот же интерфейс.


Класс, в объявлении которого отсутствует implements часть, не имеет прямых типов суперинтерфейсов, за исключением анонимных классов, которые могут иметь тип суперинтерфейса (§15.9.5).

Интерфейс является прямым суперинтерфейсом класса, если интерфейс указан в одном из прямых типов суперинтерфейсов класса.

Интерфейс I является суперинтерфейсом класса C, если выполняется хотя бы одно из следующих условий:

  • I является прямым суперинтерфейсом C.

  • C имеет некоторый прямой суперинтерфейс J, для которого I является суперинтерфейсом, согласно определению "суперинтерфейса интерфейса", приведенному в §9.1.3.

  • I является суперинтерфейсом непосредственного суперкласса C.

Класс может иметь суперинтерфейс более чем одним способом.

Говорят, что класс непосредственно реализует свои прямые суперинтерфейсы и реализует все свои суперинтерфейсы.

Говорят, что класс является непосредственным подклассом своих прямых суперинтерфейсов и подклассом всех своих суперинтерфейсов.

Класс не может объявлять прямой тип суперкласса и прямой тип суперинтерфейса или два прямых типа суперинтерфейса, которые являются или у которых супертипы (§4.10.2) являются различными параметризациями одного и того же обобщенного интерфейса (§9.1.2) или параметризацией обобщенного интерфейса и сыром типом, обозначающим тот же обобщенный интерфейс. В случае такого конфликта произойдет ошибка компиляции.

Это требование было введено для поддержки перевода путем стирания типов (§4.6).

Пример 8.1.5-2. Суперинтерфейсы

interface Colorable {
    void setColor(int color);
    int getColor();
}
enum Finish { MATTE, GLOSSY }
interface Paintable extends Colorable {
    void setFinish(Finish finish);
    Finish getFinish();
}

class Point { int x, y; }
class ColoredPoint extends Point implements Colorable {
    int color;
    public void setColor(int color) { this.color = color; }
    public int getColor() { return color; }
}
class PaintedPoint extends ColoredPoint implements Paintable {
    Finish finish;
    public void setFinish(Finish finish) {
        this.finish = finish;
    }
    public Finish getFinish() { return finish; }
}

Здесь отношения выглядят следующим образом:

  • Интерфейс Paintable является суперинтерфейсом класса PaintedPoint.

  • Интерфейс Colorable является суперинтерфейсом класса ColoredPoint и класса PaintedPoint.

  • Интерфейс Paintable является подинтерфейсом интерфейса Colorable, а Colorable является суперинтерфейсом Paintable, как определено в §9.1.3.

Класс PaintedPoint имеет Colorable в качестве суперинтерфейса как потому, что он является суперинтерфейсом для ColoredPoint, так и потому, что он является суперинтерфейсом для Paintable.


Пример 8.1.5-3. Недопустимое множественное наследование интерфейса

interface I<T> {}
class B implements I<Integer> {}
class C extends B implements I<String> {}

Класс C вызывает ошибку компиляции, так как пытается быть подтипом как I<Integer>, так и I<String>.


Если объявляемый класс не является abstract, все abstract методы каждого прямого суперинтерфейса должны быть реализованы (§8.4.8.1) либо объявлением в этом классе, либо существующим объявлением метода, унаследованным от непосредственного суперкласса или прямого суперинтерфейса, так как класс, который не является abstract, не может иметь методы abstract (§8.1.1.1).

Каждый метод по умолчанию (§9.4.3) суперинтерфейса класса может быть выборочно переопределен методом в классе; в противном случае метод по умолчанию обычно наследуется и его поведение определяется его телом по умолчанию.

Разрешается, чтобы одно объявление метода в классе реализовывало методы более чем одного суперинтерфейса.

Пример 8.1.5-4. Реализация методов суперинтерфейса

interface Colorable {
    void setColor(int color);
    int getColor();
}
class Point { int x, y; };
class ColoredPoint extends Point implements Colorable {
    int color;
}

Эта программа приводит к ошибке компиляции, так как ColoredPoint не является классом abstract, но не предоставляет реализацию методов setColor и getColor интерфейса Colorable.

В следующей программе:

interface Fish  { int getNumberOfScales(); }
interface Piano { int getNumberOfScales(); }
class Tuna implements Fish, Piano {
    // You can tune a piano, but can you tuna fish?
    public int getNumberOfScales() { return 91; }
}

метод getNumberOfScales в классе Tuna имеет имя, сигнатуру и тип возвращаемого значения, соответствующие методу, объявленному в интерфейсе Fish, а также методу, объявленному в интерфейсе Piano; он считается реализующим оба.

С другой стороны, в такой ситуации:

interface Fish       { int    getNumberOfScales(); }
interface StringBass { double getNumberOfScales(); }
class Bass implements Fish, StringBass {
    // This declaration cannot be correct,
    // no matter what type is used.
    public ?? getNumberOfScales() { return 91; }
}

невозможно объявить метод с именем getNumberOfScales, сигнатура и тип возвращаемого значения которого совместимы с методами, объявленными в интерфейсе Fish и в интерфейсе StringBass, так как класс не может иметь несколько методов с одинаковой сигнатурой и разными примитивными типами возвращаемого значения (§8.4). Поэтому класс не может реализовать одновременно оба интерфейса Fish и StringBass (§8.4.8).


8.1.6. Разрешённые непосредственные подклассы

Необязательная permits фраза в объявлении обычного класса указывает все классы, предназначенные в качестве непосредственных подклассов объявляемого класса (§8.1.1.2).

ClassPermits:
permits ТипИмени {, ТипИмени}

Если в объявлении класса присутствует permits фраза, но отсутствует модификатор sealed, то это ошибка компиляции.

Каждый ТипИмени должен называть доступный класс (§6.6), иначе произойдёт ошибка компиляции.

Если один и тот же класс указан более одного раза в permits фразе, это ошибка компиляции. Это верно даже если класс назван по-разному.

Каноническое имя класса не обязательно использовать в permits фразе, но permits фраза может указывать класс только один раз. Например, следующая программа не скомпилируется:

package p;

sealed class A     permits B, C, p.B {}  // error

non-sealed class B extends A {}  
non-sealed class C extends A {}

Если класс C sealed связан с именованным модулем (§7.3), то каждый класс, указанный в permits фразе объявления C, должен быть связан с тем же модулем, что и C; в противном случае произойдёт ошибка компиляции.

Если класс C sealed связан с безымянным модулем (§7.7.5), то каждый класс, указанный в permits фразе объявления C, должен принадлежать к тому же пакету, что и C; в противном случае произойдёт ошибка компиляции.

Класс sealed и его непосредственные подклассы должны ссылаться друг на друга циклически, в permits и extends фразах соответственно. Поэтому в модульной кодовой базе они должны быть размещены в одном модуле, так как классы в разных модулях не могут ссылаться друг на друга циклически. Совместное размещение желательно в любом случае, потому что иерархия герметичных классов всегда должна объявляться в одном домене поддержки, где за её поддержку отвечает один и тот же разработчик или группа разработчиков. Именованный модуль обычно представляет собой домен поддержки в модульной кодовой базе.

Если объявление класса sealed C содержит permits фразу, то разрешённые непосредственные подклассы C — это классы, указанные в permits фразе.

Каждый разрешённый непосредственный подкласс, указанный в permits фразе, должен быть непосредственным подклассом C (§8.1.4), иначе произойдёт ошибка компиляции.

Если объявление класса sealed C не содержит permits фразу, то разрешённые непосредственные подклассы C следующие:

  • Если C не является классом перечисления, то его разрешённые непосредственные подклассы — это те классы, объявленные в той же единице компиляции, что и C (§7.3), которые имеют каноническое имя (§6.7) и непосредственным суперклассом являются C.

    То есть, разрешённые непосредственные подклассы выводятся как классы в той же единице компиляции, которые указывают C в качестве своего непосредственного суперкласса. Требование канонического имени означает, что локальные классы или анонимные классы не будут рассматриваться.

    Если объявление класса sealed C не содержит permits фразу и C не имеет разрешённых непосредственных подклассов, то это ошибка компиляции.

  • Если C — класс перечисления, то его разрешённые непосредственные подклассы, если таковые имеются, указаны в §8.9.

8.1.7. Тело класса и объявления членов

Тело класса может содержать объявления членов класса, то есть полей (§8.3), методов (§8.4), классов и интерфейсов (§8.5).

Тело класса также может содержать инициализаторы экземпляров (§8.6), статические инициализаторы (§8.7) и объявления конструкторов (§8.8) для класса.

ClassBody:
{ {ОбъявлениеТелаКласса} }
ClassBodyDeclaration:
ОбъявлениеЧленаКласса
ИнициализаторЭкземпляра
СтатическийИнициализатор
ОбъявлениеКонструктора
ClassMemberDeclaration:
ОбъявлениеПоля
ОбъявлениеМетода
ОбъявлениеКласса
ОбъявлениеИнтерфейса
;

Область действия и затенение объявления члена m, объявленного в или унаследованного классом C, заданы в §6.3 и §6.4.1.

Если C — вложенный класс, могут быть определения того же типа (переменная, метод или тип) и имени, что и m во внешних областях видимости. (Области видимости могут быть блоками, классами или пакетами.) Во всех таких случаях член m, объявленный в или унаследованный классом C, затеняет другие определения того же типа и имени.

8.2. Члены класса

К членам класса относятся следующие элементы:

  • Члены, унаследованные от непосредственного суперкласса (§8.1.4), за исключением класса Object, у которого нет непосредственного суперкласса

  • Члены, унаследованные от любых непосредственных суперинтерфейсов (§8.1.5)

  • Члены, объявленные в теле класса (§8.1.7)

Члены класса, объявленные private, не наследуются подклассами этого класса.

Подклассы, объявленные в другом пакете, чем класс, наследуют только члены класса, объявленные protected или public.

Конструкторы, статические инициализаторы и инициализаторы экземпляров не являются членами и поэтому не наследуются.

Мы используем фразу тип члена для обозначения:

  • Для поля — его тип.

  • Для метода — упорядоченную 4-х-кортеж, состоящую из:

    • Параметры типа: объявления любых параметров типа метода-члена.

    • Типы аргументов: список типов аргументов метода-члена.

    • Тип возвращаемого значения: тип возвращаемого значения метода-члена.

    • Оператор throws: типы исключений, объявленные в операторе throws метода-члена.

Поля, методы, вложенные классы и вложенные интерфейсы класса могут иметь одинаковые имена, так как они используются в разных контекстах и различаются различными процедурами поиска (§6.5). Однако, это не рекомендуется с точки зрения стиля.

Пример 8.2-1. Использование членов класса

class Point {
    int x, y;
    private Point() { reset(); }
    Point(int x, int y) { this.x = x; this.y = y; }
    private void reset() { this.x = 0; this.y = 0; }
}
class ColoredPoint extends Point {
    int color;
    void clear() { reset(); }  // error
}
class Test {
    public static void main(String[] args) {
        ColoredPoint c = new ColoredPoint(0, 0);  // error
        c.reset();  // error
    }
}

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

Одна ошибка возникает, потому что ColoredPoint не объявляет конструктор с двумя int параметрами, как требуется при использовании в main. Это иллюстрирует тот факт, что ColoredPoint не наследует конструкторы своего суперкласса Point.

Другая ошибка возникает, потому что ColoredPoint не объявляет конструкторы, и поэтому для него неявно объявляется конструктор по умолчанию (§8.8.9), эквивалентный:

ColoredPoint() { super(); }

который вызывает конструктор без аргументов непосредственного суперкласса класса ColoredPoint. Ошибка заключается в том, что конструктор Point без аргументов private, и поэтому недоступен за пределами класса Point, даже через вызов конструктора суперкласса (§8.8.7).

Еще две ошибки возникают, потому что метод reset класса Point является private, и поэтому не наследуется классом ColoredPoint. Вызовы методов в методе clear класса ColoredPoint и в методе main класса Test, следовательно, некорректны.


Пример 8.2-2. Наследование членов класса с доступом к пакету

Рассмотрим пример, когда пакет points объявляет две единицы компиляции:

package points;
public class Point {
    int x, y;
    public void move(int dx, int dy) { x += dx; y += dy; }
}

и:

package points;
public class Point3d extends Point {
    int z;
    public void move(int dx, int dy, int dz) {
        x += dx; y += dy; z += dz;
    }
}

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

import points.Point3d;
class Point4d extends Point3d {
    int w;
    public void move(int dx, int dy, int dz, int dw) {
        x += dx; y += dy; z += dz; w += dw; // compile-time errors
    }
}

В этом случае оба класса в пакете points компилируются. Класс Point3d наследует поля x и y класса Point, так как он находится в том же пакете, что и Point. Класс Point4d, находящийся в другом пакете, не наследует поля x и y класса Point или поле z класса Point3d, и поэтому не компилируется.

Лучший способ написания третьей единицы компиляции:


import points.Point3d;
class Point4d extends Point3d {
    int w;
    public void move(int dx, int dy, int dz, int dw) {
        super.move(dx, dy, dz); w += dw;
    }
}

используя метод move суперкласса Point3d для обработки dx, dy и dz. Если Point4d будет написано таким образом, он будет компилироваться без ошибок.


Пример 8.2-3. Наследование членов классов public и protected

Учитывая класс Point:

package points;
public class Point {
    public int x, y;
    protected int useCount = 0;
    static protected int totalUseCount = 0;
    public void move(int dx, int dy) {
        x += dx; y += dy; useCount++; totalUseCount++;
    }
}

поля public и protected x, y, useCount и totalUseCount наследуются во всех подклассах Point.

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

class Test extends points.Point {
    public void moveBack(int dx, int dy) {
        x -= dx; y -= dy; useCount++; totalUseCount++;
    }
}

Пример 8.2-4. Наследование членов класса private

class Point {
    int x, y;
    void move(int dx, int dy) {
        x += dx; y += dy; totalMoves++;
    }
    private static int totalMoves;
    void printMoves() { System.out.println(totalMoves); }
}
class Point3d extends Point {
    int z;
    void move(int dx, int dy, int dz) {
        super.move(dx, dy); z += dz; totalMoves++; // error
    }
}

Здесь переменная класса totalMoves может использоваться только внутри класса Point; она не наследуется подклассом Point3d. Ошибка на этапе компиляции возникает, потому что метод move класса Point3d пытается увеличить totalMoves.


Пример 8.2-5. Доступ к членам недоступных классов

Даже если класс не объявлен public, экземпляры класса могут быть доступны во время выполнения коду вне пакета, в котором он объявлен, с помощью public суперкласса или суперинтерфейса. Экземпляр класса может быть присвоен переменной такого public типа. Вызов метода public объекта, на который ссылается такая переменная, может вызвать метод класса, если он реализует или переопределяет метод public суперкласса или суперинтерфейса. (В этой ситуации метод обязательно объявлен public, даже если он объявлен в классе, который не является public.)

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

package points;
public class Point {
    public int x, y;
    public void move(int dx, int dy) {
        x += dx; y += dy;
    }
}

и другую единицу компиляции из другого пакета:

package morePoints;
class Point3d extends points.Point {
    public int z;
    public void move(int dx, int dy, int dz) {
        super.move(dx, dy); z += dz;
    }
    public void move(int dx, int dy) {
        move(dx, dy, 0);
    }
}
public class OnePoint {
    public static points.Point getOne() { 
        return new Point3d(); 
    }
}

Вызов morePoints.OnePoint.getOne() в ещё третьем пакете вернёт Point3d, который может использоваться как Point, даже если тип Point3d недоступен вне пакета morePoints. Затем можно вызвать двухаргументную версию метода move для этого объекта, что допустимо, потому что метод move класса Point3d является public (поскольку любой метод, переопределяющий метод public, должен сам быть public, именно для того, чтобы такие ситуации работали правильно).

Поля x и y этого объекта также могут быть доступны из такого третьего пакета.

Хотя поле z класса Point3d является public, получить доступ к этому полю из кода вне пакета morePoints, имея только ссылку на экземпляр класса Point3d в переменной p типа Point, невозможно. Это потому, что выражение p.z некорректно, так как p имеет тип Point, а класс Point не имеет поля с именем z; также выражение ((Point3d)p).z некорректно, потому что тип класса Point3d не может быть использован вне пакета morePoints.

Объявление поля z как public не бесполезно, однако. Если бы в пакете morePoints существовал public подкласс Point4d класса Point3d:

package morePoints;
public class Point4d extends Point3d {
    public int w;
    public void move(int dx, int dy, int dz, int dw) {
        super.move(dx, dy, dz); w += dw;
    }
}

тогда класс Point4d унаследовал бы поле z, которое, будучи public, могло бы быть доступно кодом в пакетах, отличных от morePoints, через переменные и выражения типа public типа Point4d.


8.3. Объявления полей

Переменные класса объявляются с помощью объявлений полей.

FieldDeclaration:
{МодификаторПоля} Тип СписокИдентификаторовПеременных ;
VariableDeclaratorList:
ИдентификаторПеременной {, ИдентификаторПеременной}
VariableDeclarator:
ИдентификаторОбъявленияПеременной [= ИнициализаторПеременной]
VariableDeclaratorId:
Идентификатор [Размеры]
VariableInitializer:
Выражение
ИнициализаторМассива
UnannType:
ПримитивныйТип
СсылкаТип
UnannPrimitiveType:
ЧисловойТип
boolean
UnannReferenceType:
ТипКлассаИлиИнтерфейса
ПеременнаяТипа
ТипМассива
UnannClassOrInterfaceType:
ТипКласса
ТипИнтерфейса
UnannClassType:
ИдентификаторТипа [АргументыТипа]
ИмяПакет . {Аннотация} ИдентификаторТипа [АргументыТипа]
ТипКлассаИлиИнтерфейса . {Аннотация} ИдентификаторТипа [АргументыТипа]
UnannInterfaceType:
ТипКласса
UnannTypeVariable:
ИдентификаторТипа
UnannArrayType:
ПримитивныйТип Размеры
ТипКлассаИлиИнтерфейса Размеры
ПеременнаяТипа Размеры

Следующее производство из §4.3 показано здесь для удобства:

Dims:
{Аннотация} [ ] {{Аннотация} [ ]}

Каждый идентификатор в FieldDeclaration объявляет одно поле. Идентификатор в идентификаторе может быть использован в имени для обращения к полю.

В одном FieldDeclaration может быть объявлено несколько полей, используя несколько идентификаторов; МодификаторыПоля и Тип применяются ко всем идентификаторам в объявлении.

Раздел МодификаторПоля описан в §8.3.1.

Объявленный тип поля обозначается Тип, если в Тип и ИдентификаторОбъявленияПеременной не присутствуют квадратные скобки, и определяется в §10.2 в противном случае.

Область действия и перекрытие объявления поля задаются в §6.3 и §6.4.1.

Если тело объявления класса объявляет два поля с одинаковым именем, то это является ошибкой на этапе компиляции.

Если класс объявляет поле с определённым именем, то объявление этого поля считается скрывающим все и любые доступные объявления полей с тем же именем в суперклассах и суперинтерфейсах класса.

В этом отношении скрытие полей отличается от скрытия методов (§8.4.8.3), поскольку нет различия между static и не-static полями при скрытии полей, в то время как различие делается между static и не-static методами при скрытии методов.

К скрытому полю можно обратиться, используя полное имя (§6.5.6.2), если оно static, или используя выражение доступа к полю, которое содержит ключевое слово super (§15.11.2) или приведение к типу суперкласса.

В этом отношении скрытие полей аналогично скрытию методов.

Если объявление поля скрывает объявление другого поля, то эти два поля могут иметь разные типы.

Класс наследует от своего непосредственного суперкласса и непосредственных суперинтерфейсов все не-private поля суперкласса и суперинтерфейсов, которые доступны (§6.6) коду в классе и не скрыты объявлением в классе.

private поле суперкласса может быть доступно подклассу — например, если оба класса являются членами одного класса. Тем не менее, private поле никогда не наследуется подклассом.

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

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

Пример 8.3-1. Поля с множественным наследованием

Класс может унаследовать два или более поля с одинаковым именем, как от своего суперкласса и суперинтерфейса, так и от двух суперинтерфейсов. Любая попытка обратиться к любому унаследованному полю с неоднозначным именем приводит к ошибке компиляции. Для однозначного доступа к таким полям можно использовать полное имя или выражение доступа к полю, содержащее ключевое слово super (§15.11.2). В программе:

interface Frob  { float v = 2.0f; }
class SuperTest { int   v = 3; }
class Test extends SuperTest implements Frob {
    public static void main(String[] args) {
        new Test().printV();
    }
    void printV() { System.out.println(v); }
}

класс Test наследует два поля с именем v, одно от своего суперкласса SuperTest и одно от своего суперинтерфейса Frob. Само по себе это разрешено, но ошибка компиляции возникает из-за использования простого имени v в методе printV: невозможно определить, какое v подразумевается.

Следующий вариант использует выражение доступа к полю super.v для обращения к полю с именем v, объявленному в классе SuperTest, и полное имя Frob.v для обращения к полю с именем v, объявленному в интерфейсе Frob:

interface Frob  { float v = 2.0f; }
class SuperTest { int   v = 3; }
class Test extends SuperTest implements Frob {
    public static void main(String[] args) {
        new Test().printV();
    }
    void printV() {
        System.out.println((super.v + Frob.v)/2);
    }
}

Он компилируется и выводит:

2.5

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

interface Color        { int RED=0, GREEN=1,  BLUE=2;  }
interface TrafficLight { int RED=0, YELLOW=1, GREEN=2; }
class Test implements Color, TrafficLight {
    public static void main(String[] args) {
        System.out.println(GREEN);  // compile-time error
        System.out.println(RED);    // compile-time error
    }
}

не удивительно, что обращение к полю GREEN считается неоднозначным, так как класс Test наследует два разных объявления для GREEN с разными значениями. Суть этого примера в том, что обращение к полю RED также считается неоднозначным, так как унаследованы два разных объявления. Тот факт, что два поля с именем RED имеют одинаковый тип и одинаковое неизменяемое значение, не влияет на это суждение.


Пример 8.3-2. Повторное наследование полей

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

interface Colorable {
    int RED = 0xff0000, GREEN = 0x00ff00, BLUE = 0x0000ff;
}
interface Paintable extends Colorable {
    int MATTE = 0, GLOSSY = 1;
}
class Point { int x, y; }
class ColoredPoint extends Point implements Colorable {}
class PaintedPoint extends ColoredPoint implements Paintable {
    int p = RED;
}

поля RED, GREEN и BLUE наследуются классом PaintedPoint как через его непосредственный суперкласс ColoredPoint, так и через его непосредственный суперинтерфейс Paintable. Простые имена RED, GREEN и BLUE тем не менее могут использоваться внутри класса PaintedPoint без неоднозначности для обращения к полям, объявленным в интерфейсе Colorable.


8.3.1. Модификаторы полей

FieldModifier:
(один из)
Аннотация public protected private
static final transient volatile

Правила, касающиеся модификаторов аннотаций для объявления поля, указаны в §9.7.4 и §9.7.5.

Если одно и то же ключевое слово появляется более одного раза в качестве модификатора объявления поля, или если объявление поля имеет более одного из модификаторов доступа public, protected и private (§6.6), то это ошибка времени компиляции.

Если в объявлении поля присутствуют два или более (различных) модификаторов поля, то принято, хотя и необязательно, чтобы они располагались в порядке, согласующемся с порядком, показанным выше в выражении для FieldModifier.

8.3.1.1. Статические поля

Если поле объявлено static, то существует ровно одна его копия, независимо от того, сколько экземпляров (возможно, ноль) класса будет создано. Поле static, иногда называемое классовой переменной, инициализируется при инициализации класса (§12.4).

Поле, которое не объявлено static, называется переменной экземпляра, а иногда — полем, не являющимся static. При создании нового экземпляра класса (§12.5), для каждой переменной экземпляра, объявленной в этом классе или любом его суперклассе, создаётся новая переменная, связанная с этим экземпляром.

Объявление классовой переменной вводит статический контекст (§8.1.3), который ограничивает использование конструкций, ссылающихся на текущий объект. Отметим, что ключевые слова this и super запрещены в статическом контексте (§15.8.3, §15.11.2), как и неквалифицированные ссылки на переменные экземпляров, методы экземпляров и параметры типов лексически окружающих объявлений (§6.5.5.1, §6.5.6.1, §15.12.3).

Ссылки на переменную экземпляра из статического контекста или вложенного класса или интерфейса ограничены, как указано в §6.5.6.1.

Пример 8.3.1.1-1. Статические поля

class Point {
    int x, y, useCount;
    Point(int x, int y) { this.x = x; this.y = y; }
    static final Point origin = new Point(0, 0);
}
class Test {
    public static void main(String[] args) {
        Point p = new Point(1,1);
        Point q = new Point(2,2);
        p.x = 3;
        p.y = 3;
        p.useCount++;
        p.origin.useCount++;
        System.out.println("(" + q.x + "," + q.y + ")");
        System.out.println(q.useCount);
        System.out.println(q.origin == Point.origin);
        System.out.println(q.origin.useCount);
    }
}

Эта программа выводит:

(2,2)
0
true
1

показывающий, что изменение полей x, y и useCount в p не влияет на поля q, поскольку эти поля являются переменными экземпляров в разных объектах. В этом примере классовая переменная origin класса Point ссылается как с использованием имени класса в качестве квалификатора в Point.origin, так и с использованием переменных типа класса в выражениях доступа к полям (§15.11), как в p.origin и q.origin. Эти два способа доступа к классовой переменной origin класса обращаются к одному и тому же объекту, что подтверждается тем, что значение выражения равенства ссылок (§15.21.3):

q.origin==Point.origin

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

p.origin.useCount++;

что приводит к значению q.origin.useCount в качестве 1; это происходит потому, что p.origin и q.origin ссылаются на одну и ту же переменную.


Пример 8.3.1.1-2. Скрытие классовых переменных

class Point {
    static int x = 2;
}
class Test extends Point {
    static double x = 4.7;
    public static void main(String[] args) {
        new Test().printX();
    }
    void printX() {
        System.out.println(x + " " + super.x);
    }
}

Эта программа выводит:

4.7 2

потому что объявление x в классе Test скрывает определение x в классе Point, поэтому класс Test не наследует поле x от своего суперкласса Point. Внутри объявления класса Test простое имя x относится к полю, объявленному внутри класса Test. Код в классе Test может ссылаться на поле x класса Point как на super.x (или, поскольку x является static, как на Point.x). Если объявление Test.x удалено:

class Point {
    static int x = 2;
}
class Test extends Point {
    public static void main(String[] args) {
        new Test().printX();
    }
    void printX() {
        System.out.println(x + " " + super.x);
    }
}

то поле x класса Point больше не скрывается в классе Test; вместо этого простое имя x теперь относится к полю Point.x. Код в классе Test все еще может ссылаться на это же поле как на super.x. Следовательно, вывод этой модифицированной программы таков:

2 2

Пример 8.3.1.1-3. Скрытие переменных экземпляров

class Point {
    int x = 2;
}
class Test extends Point {
    double x = 4.7;
    void printBoth() {
        System.out.println(x + " " + super.x);
    }
    public static void main(String[] args) {
        Test sample = new Test();
        sample.printBoth();
        System.out.println(sample.x + " " + ((Point)sample).x);
    }
}

Эта программа выводит:

4.7 2
4.7 2

потому что объявление x в классе Test скрывает определение x в классе Point, поэтому класс Test не наследует поле x от своего суперкласса Point. Однако следует отметить, что, хотя поле x класса Point не наследуется классом Test, оно тем не менее реализуется экземплярами класса Test. Другими словами, каждый экземпляр класса Test содержит два поля, одно типа int и одно типа double. Оба поля имеют имя x, но внутри объявления класса Test простое имя x всегда относится к полю, объявленному внутри класса Test. Код в методах экземпляров класса Test может ссылаться на переменную экземпляра x класса Point как на super.x.

Код, использующий выражение доступа к полю для доступа к полю x, обращается к полю с именем x в классе, указанном типом выражения ссылки. Таким образом, выражение sample.x обращается к значению double, переменной экземпляра, объявленной в классе Test, потому что тип переменной sample — это Test, но выражение ((Point)sample).x обращается к значению int, переменной экземпляра, объявленной в классе Point, из-за приведения к типу Point.

Если объявление x удалено из класса Test, как в программе:

class Point {
    static int x = 2;
}
class Test extends Point {
    void printBoth() {
        System.out.println(x + " " + super.x);
    }
    public static void main(String[] args) {
        Test sample = new Test();
        sample.printBoth();
        System.out.println(sample.x + " " +	((Point)sample).x);
    }
}

то поле x класса Point больше не скрывается в классе Test. Внутри методов экземпляров в объявлении класса Test простое имя x теперь относится к полю, объявленному внутри класса Point. Код в классе Test по-прежнему может ссылаться на это же поле как на super.x. Выражение sample.x по-прежнему относится к полю x в типе Test, но это поле теперь является наследованием и, следовательно, относится к полю x, объявленному в классе Point. Вывод этой модифицированной программы таков:

2 2
2 2

8.3.1.2. Поля типа final

Поле может быть объявлено final (§4.12.4). Как классовые, так и переменные экземпляров (static и не-static поля) могут быть объявлены final.

Пустая final классовая переменная должна быть определённо присвоена статическим инициализатором класса, в котором она объявлена, в противном случае произойдёт ошибка времени компиляции (§8.7, §16.8).

Пустая final переменная экземпляра должна быть определённо присвоена и не быть определённо неприсвоенной в конце каждого конструктора класса, в котором она объявлена, в противном случае произойдёт ошибка времени компиляции (§8.8, §16.9).

8.3.1.3. transient Поля

Переменные могут быть помечены transient, чтобы указать, что они не являются частью постоянного состояния объекта.

Пример 8.3.1.3-1. Сохранение transient полей

Если экземпляр класса Point:


class Point {
    int x, y;
    transient float rho, theta;
}

был сохранён в постоянное хранилище службой системы, то будут сохранены только поля x и y. Данное описание не детализирует такие службы; см. описание java.io.Serializable для примера такой службы.


8.3.1.4. volatile Поля

Язык программирования Java позволяет потокам доступа к общим переменным (§17.1). Как правило, для обеспечения согласованного и надёжного обновления общих переменных поток должен гарантировать себе эксклюзивный доступ к ним, получив блокировку, которая, как правило, обеспечивает взаимное исключение для этих общих переменных.

Язык программирования Java предоставляет второй механизм, volatile поля, который удобнее, чем блокировка, для некоторых целей.

Поле может быть объявлено volatile, в таком случае Java Memory Model гарантирует, что все потоки видят согласованное значение переменной (§17.4).

Ошибка времени компиляции, если final переменная также объявлена volatile.

Пример 8.3.1.4-1. volatile Поля

Если в примере один поток неоднократно вызывает метод one (но не более Integer.MAX_VALUE раз в целом), а другой поток неоднократно вызывает метод two:

class Test {
    static int i = 0, j = 0;
    static void one() { i++; j++; }
    static void two() {
        System.out.println("i=" + i + " j=" + j);
    }
}

то метод two мог бы иногда вывести значение для j, которое больше значения i, потому что пример не включает синхронизацию и, согласно правилам, описанным в §17.4, общие значения i и j могли быть обновлены не в порядке.

Один из способов предотвратить такое поведение — объявить методы one и two как synchronized (§8.4.3.6):

class Test {
    static int i = 0, j = 0;
    static synchronized void one() { i++; j++; }
    static synchronized void two() {
        System.out.println("i=" + i + " j=" + j);
    }
}

Это предотвращает одновременное выполнение метода one и метода two, и гарантирует, что общие значения i и j оба обновлены до возвращения метода one. Поэтому метод two никогда не наблюдает значение j, большее, чем значение i; на самом деле, он всегда наблюдает одинаковое значение для i и j.

Другой подход — объявить i и j как volatile:

class Test {
    static volatile int i = 0, j = 0;
    static void one() { i++; j++; }
    static void two() {
        System.out.println("i=" + i + " j=" + j);
    }
}

Это позволяет методам one и two выполняться одновременно, но гарантирует, что доступ к общим значениям i и j происходит ровно столько раз, и в том же порядке, как они, по-видимому, происходят во время выполнения программы каждым потоком. Поэтому общее значение для j никогда не больше значения i, потому что каждое обновление i должно отразиться в общем значении i до обновления j. Однако возможно, что любое данное вызов метода two может наблюдать значение для j, которое значительно больше, чем значение, наблюдаемое для i, потому что метод one может выполняться много раз между моментом, когда метод two извлекает значение i, и моментом, когда метод два извлекает значение j.

См. §17.4 для более подробного обсуждения и примеров.


8.3.2. Инициализация полей

Если в объявлении поля у декларатора есть инициализатор переменной, то декларатор имеет семантику присваивания (§15.26) объявленной переменной.

Если декларатор относится к переменной класса (то есть, к static полю) (§8.3.1.1), то для его инициализатора действуют следующие правила:

  • Инициализатор не может ссылаться на текущий объект, используя ключевое слово this или ключевое слово super, как указано в §15.8.3 и §15.11.2, ни ссылаться по имени на любую переменную экземпляра или метод экземпляра, как указано в §6.5.6.1 и §15.12.3.

  • Во время выполнения инициализатор вычисляется, и присваивание выполняется ровно один раз, когда класс инициализируется (§12.4.2).

    Обратите внимание, что static поля, которые являются константами (§4.12.4), инициализируются до других static полей (§12.4.2, шаг 6). Это также относится к интерфейсам (§9.3.1). Когда такие поля ссылаются по простому имени, их никогда не наблюдают со значениями по умолчанию (§4.12.5).

Если декларатор относится к переменной экземпляра (то есть, к полю, которое не static), то для его инициализатора действуют следующие правила:

  • Инициализатор может ссылаться на текущий объект, используя ключевое слово this или ключевое слово super, и может ссылаться по имени на любую переменную класса, объявленную в или унаследованную классом, даже ту, чьё объявление находится справа от инициализатора (§3.5).

  • Во время выполнения инициализатор вычисляется и присваивание выполняется каждый раз при создании экземпляра класса (§12.5).

Ссылки из инициализаторов переменных на поля, которые могут ещё не быть инициализированы, ограничены, как указано в §8.3.3 и §16 (Определённое присваивание).

Проверка исключений для инициализатора переменной в объявлении поля указана в §11.2.3.

Инициализаторы переменных также используются в операторах объявления локальных переменных (§14.4), где инициализатор вычисляется, а присваивание выполняется каждый раз, когда выполняется оператор объявления локальной переменной.

Пример 8.3.2-1. Инициализация полей

class Point {
    int x = 1, y = 5;
}
class Test {
    public static void main(String[] args) {
        Point p = new Point();
        System.out.println(p.x + ", " + p.y);
    }
}

Эта программа выводит:

1, 5

потому что присваивания x и y выполняются всякий раз, когда создаётся новый Point.


Пример 8.3.2-2. Вперёд ссылка на переменную класса

class Test {
    float f = j;
    static int j = 1;
}

Эта программа компилируется без ошибок; она инициализирует j значением 1, когда инициализируется класс Test, и инициализирует f текущим значением j каждый раз, когда создаётся экземпляр класса Test.


8.3.3. Ограничения на ссылки на поля в инициализаторах

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

Для ссылки по простому имени на переменную класса f, объявленную в классе или интерфейсе C, это ошибка времени компиляции, если:

  • Ссылка появляется либо в инициализаторе переменной класса C, либо в статическом инициализаторе C (§8.7); и

  • Ссылка появляется либо в инициализаторе f's own declarator, либо в позиции слева от f's declarator; и

  • Ссылка не находится в левой части выражения присваивания (§15.26); и

  • Внутренний класс или интерфейс, охватывающий ссылку, — C.

Для ссылки по простому имени на переменную экземпляра f, объявленную в классе C, это ошибка времени компиляции, если:

  • Ссылка появляется либо в инициализаторе переменной экземпляра C, либо в инициализаторе экземпляра C (§8.6); и

  • Ссылка появляется в инициализаторе f's own declarator или в позиции слева от f's declarator; и

  • Ссылка не находится в левой части выражения присваивания (§15.26); и

  • Внутренний класс, охватывающий ссылку, — C.

Пример 8.3.3-1. Ограничения на ссылки на поля

Для этой программы возникает ошибка времени компиляции:

class Test1 {
    int i = j;  // compile-time error:
                // incorrect forward reference
    int j = 1;
}

в то время как следующая программа компилируется без ошибок:

class Test2 {
    Test2() { k = 2; }
    int j = 1;
    int i = j;
    int k;
}

хотя конструктор для Test2 (§8.8) ссылается на поле k, которое объявлено на три строки ниже.

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

class Z {
    static int i = j + 2; 
    static int j = 4;
}

и:

class Z {
    static { i = j + 2; }
    static int i, j;
    static { j = 4; }
}

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

class Z {
    static int peek() { return j; }
    static int i = peek();
    static int j = 1;
}
class Test {
    public static void main(String[] args) {
        System.out.println(Z.i);
    }
}

выводит:

0

потому что инициализатор переменной для i использует метод класса peek для доступа к значению переменной j до того, как j была инициализирована своим инициализатором переменной, в этот момент она ещё имеет своё значение по умолчанию (§4.12.5).

Более подробный пример:

class UseBeforeDeclaration {
    static {
        x = 100;
          // ok - assignment
        int y = x + 1;
          // error - read before declaration
        int v = x = 3;
          // ok - x at left hand side of assignment
        int z = UseBeforeDeclaration.x * 2;
          // ok - not accessed via simple name

        Object o = new Object() { 
            void foo() { x++; }
              // ok - occurs in a different class
            { x++; }
              // ok - occurs in a different class
        };
    }

    {
        j = 200;
          // ok - assignment
        j = j + 1;
          // error - right hand side reads before declaration
        int k = j = j + 1;
          // error - illegal forward reference to j
        int n = j = 300;
          // ok - j at left hand side of assignment
        int h = j++;
          // error - read before declaration
        int l = this.j * 3;
          // ok - not accessed via simple name

        Object o = new Object() { 
            void foo(){ j++; }
              // ok - occurs in a different class
            { j = j + 1; }
              // ok - occurs in a different class
        };
    }

    int w = x = 3;
      // ok - x at left hand side of assignment
    int p = x;
      // ok - instance initializers may access static fields

    static int u =
        (new Object() { int bar() { return x; } }).bar();
	    // ok - occurs in a different class

    static int x;

    int m = j = 4;
      // ok - j at left hand side of assignment
    int o =
        (new Object() { int bar() { return j; } }).bar(); 
        // ok - occurs in a different class
    int j;
}

8.4. Объявления методов

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

MethodDeclaration:
{Методический модификатор} Заголовок метода Тело метода
MethodHeader:
Результат Декларатор метода [Исключения]
Параметры типа {Аннотация} Результат Декларатор метода [Исключения]
MethodDeclarator:
Идентификатор ( [Параметр получателя ,] [Список формальных параметров] ) [Размеры]
ReceiverParameter:
{Аннотация} Тип без аннотаций [Идентификатор .] this

Следующее производство из §4.3 показано здесь для удобства:

Dims:
{Аннотация} [ ] {{Аннотация} [ ]}

Описание пункта Список формальных параметров приведено в §8.4.1, Методический модификатор – в §8.4.3, Параметры типа – в §8.4.4, Результат – в §8.4.5, Исключения – в §8.4.6, а Тело метода – в §8.4.7.

Идентификатор в Деклараторе метода может использоваться в имени для ссылки на метод (§6.5.7.1, §15.12).

Область действия и перекрытие объявления метода указаны в §6.3 и §6.4.1.

Параметр получателя — это необязательный синтаксический элемент для метода экземпляра или конструктора внутреннего класса. Для метода экземпляра параметр получателя представляет собой объект, для которого вызывается метод. Для конструктора внутреннего класса параметр получателя представляет собой непосредственный внешний экземпляр вновь созданного объекта. В обоих случаях параметр получателя существует только для того, чтобы в исходном коде можно было указать тип представленного объекта, так что тип может быть аннотирован (§9.7.4). Параметр получателя не является формальным параметром; точнее, это не объявление какой-либо переменной (§4.12.3), он никогда не связывается ни с каким значением, переданным в качестве аргумента в выражении вызова метода или выражении создания экземпляра класса, и не оказывает никакого влияния во время выполнения.

Параметр получателя может появляться либо в Деклараторе метода метода экземпляра, либо в Деклараторе конструктора конструктора внутреннего класса, где внутренний класс не объявлен в статическом контексте (§8.1.3). Если параметр получателя появляется в любом другом типе метода или конструктора, возникает ошибка компиляции.

Тип и имя параметра получателя ограничены следующим образом:

  • В методе экземпляра тип параметра получателя должен соответствовать классу или интерфейсу, в котором объявлен метод, а имя параметра получателя должно быть this; в противном случае возникает ошибка компиляции.

  • В конструкторе внутреннего класса тип параметра получателя должен соответствовать классу или интерфейсу, являющемуся непосредственным окружающим типом объявления внутреннего класса, а имя параметра получателя должно быть Идентификатор . this, где Идентификатор — простое имя класса или интерфейса, являющегося непосредственным окружающим типом объявления внутреннего класса; в противном случае возникает ошибка компиляции.

Ошибка компиляции возникает, если тело объявления класса объявляет два метода с эквивалентными по переопределению сигнатурами (§8.4.2).

Объявление метода, возвращающего массив, допускает размещение некоторых или всех пар скобок, обозначающих тип массива, после списка формальных параметров. Этот синтаксис поддерживается для совместимости с ранними версиями языка программирования Java. Настоятельно рекомендуется не использовать этот синтаксис в новом коде.

8.4.1. Формальные параметры

Формальные параметры метода или конструктора, если таковые имеются, задаются списком параметров, разделенных запятыми. Каждый параметр состоит из типа (при необходимости, предваряемого модификатором final и/или одним или несколькими аннотациями) и идентификатора (при необходимости, с последующими скобками), определяющего имя параметра.

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

FormalParameterList:
FormalParameter {, FormalParameter}
FormalParameter:
{Изменяющие переменные} UnannType VariableDeclaratorId
VariableArityParameter
VariableArityParameter:
{Изменяющие переменные} UnannType {Аннотация} ... Идентификатор
VariableModifier:
Аннотация
final

Следующие правила из §8.3 и §4.3 приведены здесь для удобства:

VariableDeclaratorId:
Идентификатор [Размеры]
Dims:
{Аннотация} [ ] {{Аннотация} [ ]}

Формальный параметр метода или конструктора может быть параметром переменной арности, указанным многоточием после типа. Для метода или конструктора разрешен не более одного параметра переменной арности. Ошибка компиляции, если параметр переменной арности появляется где-либо в списке параметров, кроме последней позиции.

В грамматике для VariableArityParameter обратите внимание, что многоточие (...) является отдельным токеном (§3.11). Возможна установка пробелов между многоточием и типом, но это не рекомендуется по стилистическим соображениям.

Если последний формальный параметр метода — параметр переменной арности, то метод является методом переменной арности. В противном случае — методом фиксированной арности.

Правила, касающиеся модификаторов аннотаций для объявления формального параметра и для параметра получателя, указаны в §9.7.4 и §9.7.5.

Ошибка компиляции, если final появляется более одного раза в качестве модификатора для объявления формального параметра.

Область действия и перекрытие формального параметра указаны в §6.3 и §6.4.

Ссылки на формальный параметр из вложенного класса или интерфейса, или выражения лямбда-функции ограничены, как указано в §6.5.6.1.

Ошибка компиляции, если метод или конструктор объявляют два формальных параметра с одинаковым именем. (То есть, в их объявлениях упоминается один и тот же Идентификатор.)

Ошибка компиляции, если формальный параметр, объявленный как final, присваивается внутри тела метода или конструктора.

Объявленный тип формального параметра зависит от того, является ли он параметром переменной арности:

  • Если формальный параметр не является параметром переменной арности, то объявленный тип обозначается как UnannType, если в UnannType и VariableDeclaratorId нет пар скобок, и определен в §10.2 в противном случае.

  • Если формальный параметр является параметром переменной арности, то объявленный тип — это тип массива, определенный в §10.2.

Если объявленный тип параметра переменной арности имеет нереализуемый элементный тип (§4.7), то для объявления метода переменной арности возникает предупреждение о неопределённой ошибке компиляции, если метод не помечен аннотацией @SafeVarargs (§9.6.4.7) или предупреждение подавляется @SuppressWarnings (§9.6.4.5).

При вызове метода или конструктора (§15.12) значения выражений фактических аргументов инициализируют только что созданные переменные параметров, каждый из объявленного типа, перед выполнением тела метода или конструктора. Идентификатор, который появляется в FormalParameter, может использоваться как простое имя в теле метода или конструктора для ссылки на формальный параметр.

Вызовы метода с переменной арностью могут содержать больше выражений фактических аргументов, чем формальных параметров. Все выражения фактических аргументов, которые не соответствуют формальным параметрам, предшествующим параметру переменной арности, будут вычислены и результаты будут сохранены в массив, который будет передан вызову метода (§15.12.4.2).

Вот несколько примеров параметров получателя в методах экземпляра и конструкторах вложенных классов:


class Test {
    Test(/* ?? ?? */) {}
      // No receiver parameter is permitted in the constructor of
      // a top level class, as there is no conceivable type or name.

    void m(Test this) {}
      // OK: receiver parameter in an instance method

    static void n(Test this) {}
      // Illegal: receiver parameter in a static method                         

    class A {
        A(Test Test.this) {}
          // OK: the receiver parameter represents the instance
          // of Test which immediately encloses the instance
          // of A being constructed.

        void m(A this) {}
          // OK: the receiver parameter represents the instance 
          // of A for which A.m() is invoked.

        class B {
            B(Test.A A.this) {}
              // OK: the receiver parameter represents the instance 
              // of A which immediately encloses the instance of B 
              // being constructed.

            void m(Test.A.B this) {}
              // OK: the receiver parameter represents the instance 
              // of B for which B.m() is invoked.
        }
    }
}

Конструктор и метод экземпляра B показывают, что тип параметра получателя может обозначаться с помощью квалифицированного TypeName, как и любой другой тип; но что имя параметра получателя в конструкторе вложенного класса должно использовать простое имя внешнего класса.

8.4.2. Подпись метода

Два метода или конструктора, M и N, имеют одинаковую подпись, если у них одинаковое имя, одинаковые параметры типа (если таковые имеются) (§8.4.4) и, после адаптации типов формальных параметров N к параметрам типа M, одинаковые типы формальных параметров.

Подпись метода m1 является подписью метода m2, если выполняется одно из следующих условий:

  • m2 имеет ту же подпись, что и m1, или

  • подпись m1 совпадает со стиранием (§4.6) подписи m2.

Две подписи методов m1 и m2 являются эквивалентными для переопределения, если либо m1 является подписью m2, либо m2 является подписью m1.

Ошибка компиляции, если в классе объявлены два метода с перекрывающимися подписями.

Пример 8.4.2-1. Эквивалентные подписи переопределения

class Point {
    int x, y;
    abstract void move(int dx, int dy);
    void move(int dx, int dy) { x += dx; y += dy; }
}

Эта программа вызывает ошибку компиляции, так как она объявляет два метода move с одинаковой (и, следовательно, перекрывающейся) подписью. Это ошибка, даже если одно из объявлений — abstract.


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

Рассмотрим пример:


class CollectionConverter {
    List toList(Collection c) {...}
}
class Overrider extends CollectionConverter {
    List toList(Collection c) {...}
}

Теперь предположим, что этот код был написан до введения обобщений, и теперь автор класса CollectionConverter решает обобщить код, таким образом:


class CollectionConverter {
    <T> List<T> toList(Collection<T> c) {...}
}

Без специальных разрешений Overrider.toList больше не переопределял бы CollectionConverter.toList. Вместо этого код был бы недопустимым. Это значительно препятствовало бы использованию обобщений, поскольку разработчики библиотек колебались бы при миграции существующего кода.

8.4.3. Модификаторы методов

MethodModifier:
(один из)
Аннотация public protected private
abstract static final synchronized native strictfp

Правила, касающиеся модификаторов аннотаций для объявления метода, указаны в §9.7.4 и §9.7.5.

Ошибка во время компиляции, если одно и то же ключевое слово используется более одного раза в качестве модификатора для объявления метода, или если объявление метода содержит более одного из модификаторов доступа public, protected и private (§6.6).

Ошибка во время компиляции, если объявление метода содержит ключевое слово abstract, а также любое из ключевых слов private, static, final, native, strictfp или synchronized.

Ошибка во время компиляции, если объявление метода содержит ключевое слово native, а также strictfp.

Если в объявлении метода используются два или более (различных) модификатора метода, то по умолчанию, хотя и не обязательно, они должны быть упорядочены так же, как показано выше в произведении для MethodModifier.

8.4.3.1. Методы abstract

Объявление метода abstract вводит метод как член, предоставляя его сигнатуру (§8.4.2), результат (§8.4.5) и раздел throws, если таковой имеется (§8.4.6), но не предоставляет реализации (§8.4.7). Метод, который не является abstract, может быть назван конкретным методом.

Объявление abstract метода m должно появляться непосредственно внутри класса abstract (назовем его A), если только это не происходит в объявлении перечисления (§8.9); в противном случае произойдет ошибка во время компиляции.

Каждый подкласс A, который не является abstract (§8.1.1.1), должен предоставить реализацию для m; в противном случае произойдет ошибка во время компиляции.

Класс abstract может переопределять abstract метод, предоставив другое объявление метода abstract.

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

Метод-инстанс, который не является abstract, может быть переопределен abstract методом.

Пример 8.4.3.1-1. Абстрактный/Переопределение абстрактного метода

class BufferEmpty extends Exception {
    BufferEmpty() { super(); }
    BufferEmpty(String s) { super(s); }
}
class BufferError extends Exception {
    BufferError() { super(); }
    BufferError(String s) { super(s); }
}
interface Buffer {
    char get() throws BufferEmpty, BufferError;
}
abstract class InfiniteBuffer implements Buffer {
    public abstract char get() throws BufferError;
}

Переопределяющее объявление метода get в классе InfiniteBuffer указывает, что метод get в любом подклассе InfiniteBuffer никогда не бросает исключение BufferEmpty, предположительно, потому что он генерирует данные в буфере и, следовательно, никогда не может исчерпать данные.


Пример 8.4.3.1-2. Абстрактное/Не-абстрактное переопределение

Мы можем объявить abstract класс Point, который требует от своих подклассов реализации toString, если они должны быть полными, экземпляризуемыми классами:

abstract class Point {
    int x, y;
    public abstract String toString();
}

Это объявление abstract метода toString переопределяет не-abstract toString метод класса Object. (Object — неявный непосредственный суперкласс класса Point.) Добавление кода:

class ColoredPoint extends Point {
    int color;
    public String toString() {
        return super.toString() + ": color " + color;  // error
    }
}

приведет к ошибке во время компиляции, потому что вызов super.toString() относится к методу toString в классе Point, который является abstract и поэтому не может быть вызван. Метод toString класса Object может быть доступен для класса ColoredPoint только если класс Point явно делает его доступным через какой-либо другой метод, как в:

abstract class Point {
    int x, y;
    public abstract String toString();
    protected String objString() { return super.toString(); }
}
class ColoredPoint extends Point {
    int color;
    public String toString() {
        return objString() + ": color " + color;  // correct
    }
}

8.4.3.2. Статические методы

Метод, объявленный static, называется методом класса.

Метод класса всегда вызывается без ссылки на конкретный объект. Объявление метода класса вводит статический контекст (§8.1.3), который ограничивает использование конструкций, ссылающихся на текущий объект. Отметим, что ключевые слова this и super запрещены в статическом контексте (§15.8.3, §15.11.2), как и неквалифицированные ссылки на переменные экземпляров, методы экземпляров и параметры типа лексически окружающих объявлений (§6.5.5.1, §6.5.6.1, §15.12.3).

Метод, который не объявлен static, называется методом экземпляра, и иногда называется не-static методом.

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

Ссылки на метод экземпляра из статического контекста или вложенного класса или интерфейса ограничены, как указано в §15.12.3.

8.4.3.3. Финальные методы

Метод может быть объявлен final, чтобы предотвратить переопределение или скрытие его подклассами.

Ошибка во время компиляции, если вы пытаетесь переопределить или скрыть final метод.

private метод и все методы, объявленные непосредственно в final классе (§8.1.1.2), ведут себя так, как если бы они были final, поскольку переопределить их невозможно.

Во время выполнения генератор или оптимизатор машинного кода может "встроить" тело final метода, заменив вызов метода кодом в его теле. Процесс встраивания должен сохранить семантику вызова метода. В частности, если целью вызова метода экземпляра является null, то должно быть брошено исключение NullPointerException, даже если метод встроен. Компилятор Java должен гарантировать, что исключение будет брошено в правильной точке, так что фактические аргументы метода будут видны, чтобы они были оценены в правильном порядке до вызова метода.

Рассмотрим пример:

final class Point {
    int x, y;
    void move(int dx, int dy) { x += dx; y += dy; }
}
class Test {
    public static void main(String[] args) {
        Point[] p = new Point[100];
        for (int i = 0; i < p.length; i++) {
            p[i] = new Point();
            p[i].move(i, p.length-1-i);
        }
    }
}

Встраивание метода move класса Point в метод main преобразовало бы цикл for в форму:


    for (int i = 0; i < p.length; i++) {
        p[i] = new Point();
        Point pi = p[i];
        int j = p.length-1-i;
        pi.x += i;
        pi.y += j;
    }

Затем цикл может быть подвергнут дальнейшей оптимизации.

Такое встраивание не может быть выполнено во время компиляции, если нет гарантии, что Test и Point будут всегда перекомпилированы вместе, так что всякий раз, когда Point — и, в частности, его метод move — изменяется, код для Test.main также обновляется.

8.4.3.4. Методы native

Метод, который является native, реализуется в платформозависимом коде, обычно написанном на другом языке программирования, таком как C. Тело native метода предоставляется как только точка с запятой, указывая, что реализация опущена, а не блок (§8.4.7).

Например, класс RandomAccessFile пакета java.io может объявить следующие native методы:


package java.io;
public class RandomAccessFile
    implements DataOutput, DataInput {
    . . .
    public native void open(String name, boolean writeable)
        throws IOException;
    public native int readBytes(byte[] b, int off, int len)
        throws IOException;
    public native void writeBytes(byte[] b, int off, int len)
        throws IOException;
    public native long getFilePointer() throws IOException;
    public native void seek(long pos) throws IOException;
    public native long length() throws IOException;
    public native void close() throws IOException;
}

8.4.3.5. strictfp методы

Модификатор strictfp в объявлении метода устарел и не должен использоваться в новом коде. Его присутствие или отсутствие не оказывает никакого влияния во время выполнения.

8.4.3.6. synchronized Методы

Метод synchronized приобретает монитор (§17.1) перед выполнением.

Для метода класса (static) используется монитор, связанный с объектом Class класса метода.

Для метода экземпляра используется монитор, связанный с объектом this (объект, для которого был вызван метод).

Пример 8.4.3.6-1. synchronized Мониторы

Это те же мониторы, которые могут использоваться оператором synchronized (§14.19).

Таким образом, код:

class Test {
    int count;
    synchronized void bump() {
        count++;
    }
    static int classCount;
    static synchronized void classBump() {
        classCount++;
    }
}

имеет точно такой же эффект, как:

class BumpTest {
    int count;
    void bump() {
        synchronized (this) { count++; }
    }
    static int classCount;
    static void classBump() {
        try {
            synchronized (Class.forName("BumpTest")) {
                classCount++;
            }
        } catch (ClassNotFoundException e) {}
    }
}

Пример 8.4.3.6-2. synchronized Методы

public class Box {
    private Object boxContents;
    public synchronized Object get() {
        Object contents = boxContents;
        boxContents = null;
        return contents;
    }
    public synchronized boolean put(Object contents) {
        if (boxContents != null) return false;
        boxContents = contents;
        return true;
    }
}

Эта программа определяет класс, предназначенный для одновременного использования. Каждый экземпляр класса Box имеет переменную экземпляра boxContents, которая может содержать ссылку на любой объект. Вы можете поместить объект в Box, вызвав put, которая возвращает false, если ящик уже заполнен. Вы можете извлечь что-то из Box, вызвав get, которая возвращает нулевую ссылку, если ящик пуст.

Если put и get не были synchronized, и две потока выполняли методы для одного экземпляра Box одновременно, то код мог бы работать некорректно. Например, он мог бы потерять отслеживание объекта, потому что два вызова put произошли одновременно.


8.4.4. Обобщенные методы

Метод является обобщенным, если он объявляет одну или несколько переменных типов (§4.4).

Эти переменные типов известны как параметры типа метода. Формат раздела параметров типа обобщенного метода идентичен разделу параметров типа обобщенного класса (§8.1.2).

Объявление обобщенного метода определяет набор методов, по одному для каждого возможного вызова раздела параметров типа с помощью аргументов типа. Аргументы типа могут не потребоваться явно при вызове обобщенного метода, так как их часто можно вывести (§18 (Выведение типов)).

Область действия и перекрытие параметров типа метода указаны в §6.3 и §6.4.1.

Ссылки на параметр типа метода из вложенного класса или интерфейса ограничены, как указано в §6.5.5.1.

Два метода или конструктора M и N имеют одинаковые параметры типа, если оба следующих утверждения верны:

  • M и N имеют одинаковое количество параметров типа (возможно, ноль).

  • Если A1, ..., An являются параметрами типа M, а B1, ..., Bn являются параметрами типа N, пусть θ=[B1:=A1, ..., Bn:=An]. Тогда для всех i (1 ≤ i ≤ n), ограничение Ai совпадает с типом, полученным при применении θ к ограничению Bi.

Если два метода или конструктора M и N имеют одинаковые параметры типа, то тип, упомянутый в N, может быть приведен к параметрам типа M путем применения θ, как определено выше, к типу.

8.4.5. Результат метода

Результат объявления метода либо объявляет тип значения, которое возвращает метод (тип возвращаемого значения), либо использует ключевое слово void, чтобы указать, что метод не возвращает значение.

Результат:
UnannType
void

Если результат не void, то тип возвращаемого значения метода обозначается UnannType, если после списка формальных параметров нет скобок, и задаётся в §10.2 в противном случае.

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

Объявление метода d1 с типом возвращаемого значения R1 является подставляемым по типу возвращаемого значения для другого метода d2 с типом возвращаемого значения R2, если выполняется любое из следующих условий:

  • Если R1 является void, то R2 является void.

  • Если R1 является примитивным типом, то R2 идентичен R1.

  • Если R1 является ссылочным типом, то выполняется одно из следующих условий:

    • R1, приведенный к параметрам типа d2 (§8.4.4), является подтипом R2.

    • R1 может быть преобразован в подтип R2 с помощью неявного преобразования (§5.1.9).

    • d1 не имеет той же подписи, что и d2 (§8.4.2), и R1 = |R2|.

Неявное преобразование разрешено в определении, несмотря на то, что оно некорректно, как специальное разрешение для плавного перехода от необобщенного к обобщенному коду. Если неявное преобразование используется для определения того, что R1 подставляется по типу возвращаемого значения для R2, то R1 обязательно не является подтипом R2, и правила переопределения (§8.4.8.3, §9.4.1) потребуют предупреждение о неявном преобразовании во время компиляции.

8.4.6. Блок throws

Блок throws используется для обозначения любых исключений с проверкой (§11.1.1), которые могут быть брошены операторами в теле метода или конструктора (§11.2.2).

Throws:
throws СписокИсключений
СписокИсключений:
ТипИсключения {, ТипИсключения}
ТипИсключения:
ТипКласса
ПеременнаяТипа

Ошибка компиляции, если ТипИсключения, упомянутый в блоке throws, не является подтипом (§4.10) типа Throwable.

Переменные типа допускаются в блоке throws, несмотря на то, что они запрещены в блоке try (§14.20).

Допускается, но не обязательно, упоминать классы исключений без проверки (§11.1.1) в блоке throws.

Связь между блоком throws и проверкой исключений для метода или конструктора задана в §11.2.3.

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

Требование объявления исключений с проверкой позволяет компилятору Java гарантировать, что в коде включены обработчики таких ошибок. Методы или конструкторы, не обрабатывающие исключения с проверкой, брошенные в их телах, обычно вызывают ошибки компиляции, если им не хватает соответствующих типов исключений в их блоках throws. Таким образом, язык программирования Java поощряет стиль программирования, в котором редкие и в ином случае действительно исключительные ситуации документируются таким образом.

Связь между блоком throws метода и блоками throws переопределенных или скрытых методов задана в §8.4.8.3.

Пример 8.4.6-1. Переменные типа в качестве типов исключений

import java.io.FileNotFoundException;
interface PrivilegedExceptionAction<E extends Exception> { 
    void run() throws E;
} 
class AccessController {
    public static <E extends Exception> 
    Object doPrivileged(PrivilegedExceptionAction<E> action) throws E {
        action.run();
        return "success";
    }
}
class Test {
    public static void main(String[] args) {
        try {
            AccessController.doPrivileged(
              new PrivilegedExceptionAction<FileNotFoundException>() {
                  public void run() throws FileNotFoundException {
                      // ... delete a file ...
                  } 
              }); 
        } catch (FileNotFoundException f) { /* Do something */ }
    }
}

8.4.7. Тело метода

Тело метода — это либо блок кода, реализующий метод, либо просто точка с запятой, указывающая на отсутствие реализации.

ТелоМетода:
Блок
;

Тело метода должно быть точкой с запятой, если метод является abstract или native (§8.4.3.1, §8.4.3.4). Точнее:

  • Ошибка компиляции, если объявление метода является либо abstract, либо native и имеет блок в качестве тела.

  • Ошибка компиляции, если объявление метода не является ни abstract, ни native и имеет точку с запятой в качестве тела.

Если для объявленного void метода требуется предоставить реализацию, но реализация не требует исполняемого кода, тело метода должно быть написано как блок, не содержащий операторов: "{ }".

Правила для операторов return в теле метода описаны в §14.17.

Если методу объявлено возвращаемое значение (§8.4.5), то происходит ошибка компиляции, если тело метода может завершиться нормально (§14.1).

Другими словами, метод с возвращаемым значением должен возвращать значение только с помощью оператора return, который предоставляет возвращаемое значение; метод не может «просто завершиться». См. §14.17 для точных правил об операторах return в теле метода.

Метод может иметь возвращаемое значение и при этом не содержать операторов return. Вот один пример:

class DizzyDean {
    int pitch() { throw new RuntimeException("90 mph?!"); }
}

8.4.8. Наследование, переопределение и скрытие

Класс C унаследует от своего непосредственного суперкласса типа D все конкретные методы m (как static, так и экземплярные), для которых выполняются все следующие условия:

  • m является членом D.

  • m является public, protected или объявлен с доступом по умолчанию в том же пакете, что и C.

  • Ни один метод, объявленный в C, не имеет сигнатуры, являющейся подсигнатурой (§8.4.2) сигнатуры m в качестве члена D.

Класс C унаследует от своего непосредственного суперкласса типа и непосредственных суперинтерфейсов все abstract и методы по умолчанию (§9.4) m, для которых выполняются все следующие условия:

  • m является членом непосредственного суперкласса типа или непосредственного суперинтерфейса типа C, известного в обоих случаях как D.

  • m является public, protected или объявлен с доступом по умолчанию в том же пакете, что и C.

  • Ни один метод, объявленный в C, не имеет сигнатуры, являющейся подсигнатурой (§8.4.2) сигнатуры m в качестве члена D.

  • Ни один конкретный метод, унаследованный C от своего непосредственного суперкласса типа, не имеет сигнатуры, являющейся подсигнатурой сигнатуры m в качестве члена D.

  • Не существует метода m', который является членом непосредственного суперкласса типа или непосредственного суперинтерфейса типа C, D' (m отличен от m', D отличен от D'), такой, что m' переопределяет из класса или интерфейса D' объявление метода m (§8.4.8.1, §9.4.1.1).

Класс не наследует private или static методы от своих суперинтерфейсов.

Обратите внимание, что методы переопределяются или скрываются на основе сигнатуры. Например, если класс объявляет два public метода с одинаковым именем (§8.4.9), и подкласс переопределяет один из них, подкласс все равно наследует другой метод.

Пример 8.4.8-1. Наследование

interface I1 {
    int foo();
}

interface I2 {
    int foo();
}

abstract class Test implements I1, I2 {} 

Здесь класс abstract Test наследует метод abstract foo от интерфейса I1 и также метод abstract foo от интерфейса I2. Ключевой вопрос при определении наследования foo из I1: переопределяет ли метод foo в I2 метод foo в I1? Нет, потому что I1 и I2 не являются подинтерфейсами друг друга. Таким образом, с точки зрения класса Test, наследование foo из I1 не ограничено; аналогично для наследования foo из I2. В соответствии с §8.4.8.4, класс Test может унаследовать оба метода foo; очевидно, он должен быть объявлен abstract, или же переопределить оба abstract foo метода с помощью конкретного метода.


Обратите внимание, что унаследованный конкретный метод может препятствовать наследованию метода по умолчанию или abstract. (Конкретный метод переопределит метод abstract или метод по умолчанию «из C», в соответствии с §8.4.8.1 и §9.4.1.1.) Также, один метод супертипа может препятствовать наследованию другого метода супертипа, если первый «уже» переопределяет второй — это то же правило, что и для интерфейсов (§9.4.1), и предотвращает конфликты, в которых унаследовано несколько методов по умолчанию и одно конкретное исполнение явно должно заменять другое.

8.4.8.1. Переопределение (методами экземпляров)

Метод экземпляра mC, объявленный в классе или унаследованный классом C, переопределяет из C другой метод mA, объявленный в классе A, если все перечисленные ниже условия выполняются:

  • C является подклассом A.

  • C не наследует mA.

  • Подпись mC является подподписью (§8.4.2) подписи mA как члена супертипа C, который называет A.

  • Выполняется одно из следующих условий:

    • mA является public.

    • mA является protected.

    • mA объявлен с доступом по умолчанию в том же пакете, что и C, и либо C объявляет mC, либо mA является членом непосредственного суперкласса типа C.

    • mA объявлен с доступом по умолчанию и mC переопределяет mA из некоторого суперкласса C.

    • mA объявлен с доступом по умолчанию и mC переопределяет метод m' из C (m' отличается от mC и mA), так что m' переопределяет mA из некоторого суперкласса C.

Если mC не является abstract и переопределяет из C метод abstract mA, то mC называется реализующим mA из C.

Если переопределяемый метод mA является методом static, то это ошибка компиляции.

В этом отношении переопределение методов отличается от скрытия полей (§8.3), так как разрешается скрытие переменной экземпляра static.

Метод экземпляра mC, объявленный в классе или унаследованный классом C, переопределяет из C другой метод mI, объявленный в интерфейсе I, если все следующие условия выполняются:

  • I является суперинтерфейсом C.

  • mI не является static.

  • C не наследует mI.

  • Подпись mC является подподписью (§8.4.2) подписи mI как члена супертипа C, который называет I.

  • mI является public.

Подпись переопределяемого метода может отличаться от переопределяемого, если формальный параметр в одном из методов имеет непараметризованный тип, а соответствующий параметр в другом методе имеет параметризованный тип. Это позволяет мигрировать существующий код для использования дженериков.

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

  • Конкретный метод в генерическом суперклассе может, при определенных параметризациях, иметь ту же подпись, что и метод abstract в этом классе. В этом случае конкретный метод наследуется, а абстрактный метод нет (как описано выше). Наследуемый метод следует тогда рассматривать как переопределяющий свой абстрактный аналог из C. (Этот сценарий усложняется доступом по умолчанию: если C находится в другом пакете, то mA не будет унаследован и не должен рассматриваться как переопределенный.)

  • Метод, унаследованный от класса, может переопределять метод суперинтерфейса. (Здесь доступ по умолчанию не вызывает проблем.)

К переопределенному методу можно обратиться, используя выражение вызова метода (§15.12), которое содержит ключевое слово super. Квалифицированное имя или приведение к типу суперкласса неэффективны при попытке доступа к переопределенному методу.

В этом отношении переопределение методов отличается от скрытия полей.

Наличие или отсутствие модификатора strictfp абсолютно не влияет на правила переопределения методов и реализации методов abstract. Например, разрешается, чтобы метод, который не является strictfp, переопределял метод strictfp, и разрешается, чтобы метод strictfp переопределял метод, который не является strictfp.

Пример 8.4.8.1-1. Переопределение

class Point {
    int x = 0, y = 0;
    void move(int dx, int dy) { x += dx; y += dy; }
}
class SlowPoint extends Point {
    int xLimit, yLimit;
    void move(int dx, int dy) {
        super.move(limit(dx, xLimit), limit(dy, yLimit));
    }
    static int limit(int d, int limit) {
        return d > limit ? limit : d < -limit ? -limit : d;
    }
}

Здесь класс SlowPoint переопределяет объявления метода move класса Point своим методом move, который ограничивает расстояние, на которое точка может перемещаться при каждом вызове метода. Когда метод move вызывается для экземпляра класса SlowPoint, переопределяющее определение в классе SlowPoint всегда будет вызываться, даже если ссылка на объект SlowPoint взята из переменной, тип которой является Point.


Пример 8.4.8.1-2. Переопределение

Переопределение упрощает подклассам расширять поведение существующего класса, как показано в этом примере:

import java.io.OutputStream;
import java.io.IOException;

class BufferOutput {
    private OutputStream o;
    BufferOutput(OutputStream o) { this.o = o; }
    protected byte[] buf = new byte[512];
    protected int pos = 0;
    public void putchar(char c) throws IOException {
        if (pos == buf.length) flush();
        buf[pos++] = (byte)c;
    }
    public void putstr(String s) throws IOException {
        for (int i = 0; i < s.length(); i++)
            putchar(s.charAt(i));
    }
    public void flush() throws IOException {
        o.write(buf, 0, pos);
        pos = 0;
    }
}
class LineBufferOutput extends BufferOutput {
    LineBufferOutput(OutputStream o) { super(o); }
    public void putchar(char c) throws IOException {
        super.putchar(c);
        if (c == '\n') flush();
    }
}
class Test {
    public static void main(String[] args) throws IOException {
        LineBufferOutput lbo = new LineBufferOutput(System.out);
        lbo.putstr("lbo\nlbo");
        System.out.print("print\n");
        lbo.putstr("\n");
    }
}

Эта программа выводит:

lbo
print
lbo

Класс BufferOutput реализует очень простую буферизованную версию OutputStream, очищая вывод, когда буфер заполнен или вызывается flush. Подкласс LineBufferOutput объявляет только конструктор и единственный метод putchar, который переопределяет метод putchar из BufferOutput. Он наследует методы putstr и flush из класса BufferOutput.

В методе putchar объекта LineBufferOutput, если аргумент символа является переводом строки, то он вызывает метод flush. Важный момент относительно переопределения в этом примере заключается в том, что метод putstr, который объявлен в классе BufferOutput, вызывает метод putchar, определенный текущим объектом this, который необязательно является методом putchar, объявленным в классе BufferOutput.

Таким образом, когда putstr вызывается в main с использованием объекта LineBufferOutput lbo, вызов putchar в теле метода putstr является вызовом putchar объекта lbo, переопределяющего объявления putchar, которое проверяет наличие перевода строки. Это позволяет подклассу BufferOutput изменить поведение метода putstr, не переопределяя его.

Документация для класса, такого как BufferOutput, предназначенного для расширения, должна ясно указывать договор между классом и его подклассами и четко указывать, что подклассы могут переопределять метод putchar таким образом. Таким образом, разработчик класса BufferOutput не захочет менять реализацию putstr в будущем варианте реализации BufferOutput, не используя метод putchar, так как это нарушит существующий договор с подклассами. См. обсуждение двоичной совместимости в §13 (Двоичная совместимость), особенно §13.2.


8.4.8.2. Скрытие (методами класса)

Если класс C объявляет или наследует метод m, то он скрывает любой метод m', объявленный в классе или интерфейсе A, если выполняются все следующие условия:

  • A является суперклассом или суперинтерфейсом C.

  • Если A является интерфейсом, то m' является методом экземпляра.

  • m' доступен для C (§6.6).

  • Подпись метода m является подписью (§8.4.2) подписи m' в качестве члена супертипа C, который называет A.

Если метод static скрывает метод экземпляра, это является ошибкой компиляции.

В этом отношении, скрытие методов отличается от скрытия полей (§8.3), так как допустимо, что переменная static скрывает переменную экземпляра. Скрытие также отличается от затемнения (§6.4.1) и маскирования (§6.4.2).

К скрытому методу можно получить доступ, используя полное имя или выражение вызова метода (§15.12), содержащее ключевое слово super или приведение к типу суперкласса.

В этом отношении скрытие методов аналогично скрытию полей.

Пример 8.4.8.2-1. Вызов скрытых методов класса

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

class Super {
    static String greeting() { return "Goodnight"; }
    String name() { return "Richard"; }
}
class Sub extends Super {
    static String greeting() { return "Hello"; }
    String name() { return "Dick"; }
}
class Test {
    public static void main(String[] args) {
        Super s = new Sub();
        System.out.println(s.greeting() + ", " + s.name());
    }
}

выводит:

Goodnight, Dick

потому что вызов greeting использует тип s, а именно Super, для определения, во время компиляции, какой метод класса вызвать, тогда как вызов name использует класс s, а именно Sub, для определения, во время выполнения, какой метод экземпляра вызвать.


8.4.8.3. Требования к переопределению и скрытию

Если объявление метода d1 с типом возвращаемого значения R1 переопределяет или скрывает объявление другого метода d2 с типом возвращаемого значения R2, то d1 должен быть совместимым с типом возвращаемого значения (§8.4.5) для d2, в противном случае произойдет ошибка компиляции.

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

Если R1 не является подтипом R2, то возникает неустранимая ошибка компиляции, если она не подавлена с помощью @SuppressWarnings (§9.6.4.5).

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

В этом отношении переопределение методов отличается от скрытия полей (§8.3), так как поле может скрыть поле другого типа.

Более точно, предположим, что B — это класс или интерфейс, а A — суперкласс или суперинтерфейс B, и объявление метода m2 в B переопределяет или скрывает объявление метода m1 в A. Тогда:

  • Если у m2 есть раздел throws, в котором упоминаются типы проверочных исключений, то у m1 должен быть раздел throws; в противном случае произойдет ошибка компиляции.

  • Для каждого типа проверочного исключения, указанного в разделе throws объявления m2, тот же класс исключения или один из его супертипов должен присутствовать в стирании (§4.6) раздела throws объявления m1; в противном случае произойдет ошибка компиляции.

  • Если нестираемый раздел throws объявления m1 не содержит супертип каждого типа исключения из раздела throws объявления m2 (при необходимости адаптированный к параметрам типа m1), то возникает неустранимая ошибка компиляции, если она не подавлена с помощью @SuppressWarnings (§9.6.4.5).

Ошибка компиляции возникает, если у класса или интерфейса C есть метод-член m1 и существует метод m2, объявленный в C или в суперклассе или суперинтерфейсе C, A, и выполняются следующие условия:

  • m1 и m2 имеют одинаковое имя.

  • m2 доступен (§6.6) из C.

  • Подпись m1 не является подписью (§8.4.2) подписи m2 как члена супертипа C, который называет A.

  • Объявленная подпись m1 или какого-либо метода m1 (прямо или косвенно) переопределяет стирание объявленной подписи m2 или какого-либо метода m2 (прямо или косвенно).

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

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

  • Если переопределяемый или скрываемый метод имеет public, то переопределяемый или скрываемый метод должен иметь public; в противном случае произойдет ошибка компиляции.

  • Если переопределяемый или скрываемый метод имеет protected, то переопределяемый или скрываемый метод должен иметь protected или public; в противном случае произойдет ошибка компиляции.

  • Если переопределяемый или скрываемый метод имеет доступ пакета, то переопределяемый или скрываемый метод не должен быть private; в противном случае произойдет ошибка компиляции.

Обратите внимание, что private метод не может быть переопределен или скрыт в техническом смысле этих терминов. Это означает, что подкласс может объявить метод с той же подписью, что и private метод в одном из его суперклассов, и нет требования, чтобы тип возвращаемого значения или throws раздел такого метода имели какую-либо связь с private методом в суперклассе.

Пример 8.4.8.3-1. Ковариативные типы возвращаемых значений

Следующие объявления допустимы в языке программирования Java начиная с Java SE 5.0:

class C implements Cloneable { 
    C copy() throws CloneNotSupportedException {
        return (C)clone();
    } 
}
class D extends C implements Cloneable { 
    D copy() throws CloneNotSupportedException {
        return (D)clone();
    } 
}

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


Пример 8.4.8.3-2. Неустранимая ошибка из-за типа возвращаемого значения

Рассмотрим:


class StringSorter {
    // turns a collection of strings into a sorted list
    List toList(Collection c) {...} 
}

и предположим, что кто-то наследует от StringSorter:


class Overrider extends StringSorter {
    List toList(Collection c) {...}
}

Теперь в какой-то момент автор StringSorter решает обобщить код:


class StringSorter {
    // turns a collection of strings into a sorted list
    List<String> toList(Collection<String> c) {...}
}

При компиляции Overrider относительно нового определения StringSorter будет выдана неустранимая ошибка, так как тип возвращаемого значения Overrider.toList есть List, который не является подтипом типа возвращаемого значения переопределяемого метода List<String>.


Пример 8.4.8.3-3. Неправильное переопределение из-за throws

Эта программа использует обычную и общепринятую форму объявления нового типа исключений в объявлении класса BadPointException:

class BadPointException extends Exception {
    BadPointException() { super(); }
    BadPointException(String s) { super(s); }
}
class Point {
    int x, y;
    void move(int dx, int dy) { x += dx; y += dy; }
}
class CheckedPoint extends Point {
    void move(int dx, int dy) throws BadPointException {
        if ((x + dx) < 0 || (y + dy) < 0)
            throw new BadPointException();
        x += dx; y += dy;
    }
}

Программа приводит к ошибке компиляции, так как переопределение метода move в классе CheckedPoint объявляет, что он будет сбрасывать проверочное исключение, которое move в классе Point не объявил. Если бы это не рассматривалось как ошибка, вызывающий метод move со ссылкой типа Point мог бы обнаружить нарушение соглашения между ним и Point, если бы это исключение было сброшено.

Удаление throws раздела не помогает:

class CheckedPoint extends Point {
    void move(int dx, int dy) {
        if ((x + dx) < 0 || (y + dy) < 0)
            throw new BadPointException();
        x += dx; y += dy;
    }
}

Теперь возникает другая ошибка компиляции, так как тело метода move не может сбросить проверочное исключение, а именно BadPointException, которое не указано в throws разделе для move.


Пример 8.4.8.3-4. Стирание влияет на переопределение

Класс не может иметь два метода-члена с одинаковым именем и стиранием типов:


class C<T> {
    T id (T x) {...}
}
class D extends C<String> {
    Object id(Object x) {...}
}

Это недопустимо, так как D.id(Object) является членом D, C<String>.id(String) объявлен в супертипе D, и:

  • Два метода имеют одинаковое имя, id

  • C<String>.id(String) доступен из D

  • Подпись D.id(Object) не является подписью C<String>.id(String)

  • Два метода имеют одинаковое стирание

Два разных метода класса не могут переопределять методы с одинаковым стиранием:


class C<T> {
    T id(T x) {...}
}
interface I<T> {
    T id(T x);
}
class D extends C<String> implements I<Integer> {
   public String  id(String x)  {...}
   public Integer id(Integer x) {...}
}

Это также недопустимо, так как D.id(String) является членом D, D.id(Integer) объявлен в D, и:

  • Два метода имеют одинаковое имя, id

  • D.id(Integer) доступен из D

  • Два метода имеют разные подписи (и ни один из них не является подписью другого)

  • D.id(String) переопределяет C<String>.id(String), а D.id(Integer) переопределяет I.id(Integer), но два переопределяемых метода имеют одинаковое стирание


8.4.8.4. Наследование методов с эквивалентными подписывающимися методами

Класс может унаследовать несколько методов с эквивалентными подписывающимися методами (§8.4.2).

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

Если класс C наследует метод по умолчанию, чья подпись эквивалентна подписи другого метода, унаследованного классом C, то это ошибка времени компиляции, за исключением случая, когда существует метод abstract, объявленный в суперклассе C и унаследованный классом C, эквивалентный по подписи двум методам.

Это исключение из строгих правил конфликтов по умолчанию-abstract и по умолчанию-по умолчанию применяется, когда метод abstract объявлен в суперклассе: утверждение об абстрактности, полученное из иерархии суперклассов, по существу, перекрывает метод по умолчанию, заставляя метод по умолчанию действовать так, как будто он был abstract. Однако метод abstract из класса не переопределяет метод(ы) по умолчанию, так как интерфейсам всё ещё разрешено уточнять подпись метода abstract, полученного из иерархии классов.

Обратите внимание, что исключение не применяется, если все эквивалентные по подписи abstract методы, унаследованные классом C, были объявлены в интерфейсах.

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

Один из унаследованных методов должен быть замещаемым по типу возвращаемого значения для каждого другого унаследованного метода; в противном случае возникает ошибка времени компиляции. (В этом случае throws-заключения не вызывают ошибок.)

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

8.4.9. Перегрузка

Если у класса два метода (либо оба объявлены в одном классе, либо оба унаследованы классом, либо один объявлен, а другой унаследован) имеют одинаковое имя, но разные подписи, не являющиеся эквивалентными по переопределению, то имя метода считается перегруженным.

Этот факт не создаёт трудностей и никогда сам по себе не приводит к ошибке времени компиляции. Нет обязательных отношений между типами возвращаемых значений или между throws-заключениями двух методов с одинаковым именем, если только их подписи не эквивалентны по переопределению.

При вызове метода (§15.12), количество фактических аргументов (и любые явные типы аргументов) и типы аргументов во время компиляции используются для определения подписи метода, который будет вызван (§15.12.2). Если вызываемый метод — метод экземпляра, фактический метод для вызова будет определён во время выполнения с использованием динамического поиска метода (§15.12.4).

Пример 8.4.9-1. Перегрузка

class Point {
    float x, y;
    void move(int dx, int dy) { x += dx; y += dy; }
    void move(float dx, float dy) { x += dx; y += dy; }
    public String toString() { return "("+x+","+y+")"; }
}

Здесь класс Point имеет два члена, которые являются методами с одинаковым именем, move. Метод move класса Point, выбранный для любого конкретного вызова метода, определяется во время компиляции процедурой разрешения перегрузки, описанной в §15.12.

В целом, членами класса Point являются float переменные экземпляра x и y, объявленные в Point, два объявленных move метода, объявленный toString метод и члены, которые Point наследует от своего неявного непосредственного суперкласса Object (§4.3.2), такой как метод hashCode. Обратите внимание, что Point не наследует метод toString класса Object, потому что этот метод переопределяется объявлением метода toString в классе Point.


Пример 8.4.9-2. Перегрузка, переопределение и скрытие

class Point {
    int x = 0, y = 0;
    void move(int dx, int dy) { x += dx; y += dy; }
    int color;
}
class RealPoint extends Point {
    float x = 0.0f, y = 0.0f;
    void move(int dx, int dy) { move((float)dx, (float)dy); }
    void move(float dx, float dy) { x += dx; y += dy; }
}

Здесь класс RealPoint скрывает объявления int переменных экземпляра x и y класса Point с помощью собственных float переменных экземпляра x и y, и переопределяет метод move класса Point с помощью собственного метода move. Он также перегружает имя move другим методом с другой подписью (§8.4.2).

В этом примере, члены класса RealPoint включают переменную экземпляра color, унаследованную от класса Point, float переменных экземпляра x и y, объявленных в RealPoint, и два move метода, объявленных в RealPoint.

Какой из перегруженных move методов класса RealPoint будет выбран для любого конкретного вызова метода, будет определено во время компиляции процедурой разрешения перегрузки, описанной в §15.12.

Следующая программа представляет собой расширенный вариант предыдущей программы:

class Point {
    int x = 0, y = 0, color;
    void move(int dx, int dy) { x += dx; y += dy; }
    int getX() { return x; }
    int getY() { return y; }
}
class RealPoint extends Point {
    float x = 0.0f, y = 0.0f;
    void move(int dx, int dy) { move((float)dx, (float)dy); }
    void move(float dx, float dy) { x += dx; y += dy; }
    float getX() { return x; }
    float getY() { return y; }
}

Здесь класс Point предоставляет методы getX и getY, возвращающие значения своих полей x и y; класс RealPoint затем переопределяет эти методы, объявляя методы с той же подписью. Результатом являются две ошибки во время компиляции, по одной на каждый метод, потому что типы возвращаемых значений не совпадают; методы в классе Point возвращают значения типа int, а методы-кандидаты для переопределения в классе RealPoint возвращают значения типа float.

Эта программа исправляет ошибки предыдущей программы:

class Point {
    int x = 0, y = 0;
    void move(int dx, int dy) { x += dx; y += dy; }
    int getX() { return x; }
    int getY() { return y; }
    int color;
}
class RealPoint extends Point {
    float x = 0.0f, y = 0.0f;
    void move(int dx, int dy) { move((float)dx, (float)dy); }
    void move(float dx, float dy) { x += dx; y += dy; }
    int getX() { return (int)Math.floor(x); }
    int getY() { return (int)Math.floor(y); }
}

Здесь переопределяющие методы getX и getY в классе RealPoint имеют те же типы возвращаемых значений, что и методы класса Point, которые они переопределяют, поэтому этот код может быть успешно скомпилирован.

Рассмотрим затем эту тестовую программу:

class Test {
    public static void main(String[] args) {
        RealPoint rp = new RealPoint();
        Point p = rp;
        rp.move(1.71828f, 4.14159f);
        p.move(1, -1);
        show(p.x, p.y);
        show(rp.x, rp.y);
        show(p.getX(), p.getY());
        show(rp.getX(), rp.getY());
    }
    static void show(int x, int y) {
        System.out.println("(" + x + ", " + y + ")");
    }
    static void show(float x, float y) {
        System.out.println("(" + x + ", " + y + ")");
    }
}

Вывод этой программы:

(0, 0)
(2.7182798, 3.14159)
(2, 3)
(2, 3)

Первая строка вывода иллюстрирует тот факт, что экземпляр RealPoint фактически содержит две целые переменные, объявленные в классе Point; просто их имена скрыты от кода, который находится внутри объявления класса RealPoint (и тех, которые могут быть у него подклассов). Когда ссылка на экземпляр класса RealPoint в переменной типа Point используется для доступа к полю x, обращается к целочисленному полю x, объявленному в классе Point. Тот факт, что его значение равно нулю, указывает на то, что вызов метода p.move(1, -1) не вызвал метод move класса Point; вместо этого он вызвал переопределяющий метод move класса RealPoint.

Вторая строка вывода показывает, что доступ к полю rp.x относится к полю x, объявленному в классе RealPoint. Это поле типа float, и эта вторая строка вывода соответственно отображает значения с плавающей точкой. Кстати, это также иллюстрирует тот факт, что имя метода show перегружено; типы аргументов в вызове метода диктуют, какая из двух определений будет вызвана.

Последние две строки вывода показывают, что вызовы методов p.getX() и rp.getX() вызывают каждый метод getX, объявленный в классе RealPoint. Действительно, нет способа вызвать метод getX класса Point для экземпляра класса RealPoint извне тела RealPoint, независимо от типа переменной, которую мы можем использовать для хранения ссылки на объект. Таким образом, мы видим, что поля и методы ведут себя по-разному: скрытие отличается от переопределения.


8.5. Объявления вложенных классов и интерфейсов

Вложенный класс — это класс, объявление которого непосредственно вложено в тело другого класса или интерфейса (§8.1.7, §9.1.5).

Вложенный интерфейс — это интерфейс, объявление которого непосредственно вложено в тело другого класса или интерфейса.

Вложенный класс может быть обычным классом (§8.1), классом-перечислением (§8.9) или классом-записью (§8.10).

Вложенный интерфейс может быть обычным интерфейсом (§9.1) или аннотационным интерфейсом (§9.6).

Доступность объявления вложенного класса или интерфейса в теле класса определяется его модификатором доступа или правилом §6.6, если модификатор отсутствует.

Правила модификаторов объявления вложенного класса в теле класса описаны в §8.1.1.

Правила модификаторов объявления вложенного интерфейса в теле класса описаны в §9.1.1.

Область видимости и перекрытие вложенного класса или интерфейса определены в §6.3 и §6.4.1.

Если класс объявляет вложенный класс или интерфейс с определенным именем, то объявление вложенного класса или интерфейса скрывает любые и все доступные объявления вложенных классов и интерфейсов с тем же именем в суперклассах и суперинтерфейсах класса.

В этом отношении скрытие вложенных классов и интерфейсов аналогично скрытию полей (§8.3).

Класс наследует от своего непосредственного суперкласса и непосредственных суперинтерфейсов все не-private вложенные классы и интерфейсы суперкласса и суперинтерфейсов, которые доступны коду в классе и не скрыты объявлением в классе.

Возможно, что класс унаследует более одного вложенного класса или интерфейса с одинаковым именем, либо от своего суперкласса и суперинтерфейсов, либо только от своих суперинтерфейсов. Такая ситуация сама по себе не приводит к ошибке компиляции. Однако любая попытка в теле класса сослаться на любой такой вложенный класс или интерфейс по его простому имени приведет к ошибке компиляции, так как ссылка является неоднозначной.

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

8.6. Инициализаторы экземпляров

Объявленный в классе инициализатор экземпляра выполняется при создании экземпляра класса (§12.5, §15.9, §8.8.7.1).

InstanceInitializer:
Блок

Ошибка компиляции, если инициализатор экземпляра не может завершиться нормально (§14.22).

Ошибка компиляции, если в инициализаторе экземпляра встречается return оператор (§14.17).

Инициализатор экземпляра может ссылаться на текущий объект с помощью ключевого слова this (§15.8.3) или ключевого слова super (§15.11.2, §15.12), а также использовать любые переменные типов в области видимости.

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

Проверка исключений для инициализатора экземпляра указана в §11.2.3.

8.7. Статические инициализаторы

Объявленный в классе статический инициализатор выполняется при инициализации класса (§12.4.2). Вместе с любыми инициализаторами полей для статических переменных (§8.3.2), статические инициализаторы могут использоваться для инициализации статических переменных класса.

StaticInitializer:
static Блок

Ошибка компиляции, если статический инициализатор не может завершиться нормально (§14.22).

Ошибка компиляции, если в статическом инициализаторе встречается return оператор (§14.17).

Статический инициализатор вводит статический контекст (§8.1.3), который ограничивает использование конструкций, ссылающихся на текущий объект. В частности, ключевые слова this и super запрещены в статическом контексте (§15.8.3, §15.11.2), как и неквалифицированные ссылки на переменные экземпляров, методы экземпляров и параметры типов лексически окружающих объявлений (§6.5.5.1, §6.5.6.1, §15.12.3).

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

Проверка исключений для статического инициализатора указана в §11.2.3.

END_OF_DOCUMENT_MARKER

8.8. Объявления конструкторов

Конструктор используется при создании объекта, являющегося экземпляром класса (§12.5, §15.9).

ConstructorDeclaration:
{МодификаторКонструктора} ДеклараторКонструктора [Исключения] ТелоКонструктора
ConstructorDeclarator:
[ПараметрыТипов] ПростоеИмяТипа ( [ПараметрПриёмника ,] [СписокФормальныхПараметров] )
SimpleTypeName:
ИдентификаторТипа

Правила в этом разделе применяются к конструкторам во всех объявлениях классов, включая объявления перечислений и записей. Однако для объявлений перечислений существуют специальные правила в отношении модификаторов конструкторов, тел конструкторов и конструкторов по умолчанию; эти правила изложены в §8.9.2. Специальные правила также применяются к объявлениям записей относительно конструкторов, как указано в §8.10.4.

ПростоеИмяТипа в ДекларатореКонструктора должно соответствовать простому имени класса, содержащего объявление конструктора, иначе произойдёт ошибка компиляции.

Во всех других отношениях объявление конструктора выглядит точно так же, как объявление метода, не имеющего результата (§8.4.5).

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

Конструкторы вызываются выражениями создания экземпляров классов (§15.9), преобразованиями и конкатенациями, вызванными оператором конкатенации строк + (§15.18.1), и явными вызовами конструкторов из других конструкторов (§8.8.7). Доступ к конструкторам управляется модификаторами доступа (§6.6), поэтому можно предотвратить создание экземпляра класса, объявив недоступный конструктор (§8.8.10).

Конструкторы никогда не вызываются выражениями вызова методов (§15.12).

Пример 8.8-1. Объявления конструкторов

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

8.8.1. Формальные параметры

Формальные параметры конструктора идентичны по синтаксису и семантике параметрам метода (§8.4.1).

Если последний формальный параметр конструктора — параметр с переменной арностью, конструктор является конструктором с переменной арностью. В противном случае это конструктор с фиксированной арностью.

Конструктор не-private внутреннего члена класса неявно объявляет в качестве первого формального параметра переменную, представляющую непосредственный внешний экземпляр класса (§15.9.2, §15.9.3).

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

  1. В выражении создания экземпляра класса для не-private внутреннего члена класса, §15.9.2 указывает на непосредственный внешний экземпляр внутреннего класса. Внутренний класс может быть сгенерирован компилятором, отличным от компилятора выражения создания экземпляра. Следовательно, должен быть стандартный способ, чтобы компилятор выражения создания передал ссылку (представляющую непосредственный внешний экземпляр) в конструктор внутреннего класса. Соответственно, язык программирования Java в этом разделе считает, что конструктор не-private внутреннего класса члена неявно объявляет начальный параметр для непосредственного внешнего экземпляра. §15.9.3 указывает, что экземпляр передаётся в конструктор.

  2. В выражении создания экземпляра класса для внутреннего локального класса или анонимного класса (не в статическом контексте), §15.9.2 указывает непосредственный внешний экземпляр локального/анонимного класса. Локальный/анонимный класс обязательно генерируется тем же компилятором, что и выражение создания экземпляра. Этот компилятор может представить непосредственный внешний экземпляр как пожелает. Языку программирования Java нет необходимости неявно объявлять параметр в конструкторе локального/анонимного класса.

  3. В выражении создания экземпляра класса для анонимного класса, и где суперкласс анонимного класса является внутренним классом (не в статическом контексте), §15.9.2 указывает на внешний экземпляр анонимного класса относительно суперкласса. Этот экземпляр должен быть передан из анонимного класса в его суперкласс, где он будет служить непосредственным внешним экземпляром. Поскольку суперкласс может быть сгенерирован компилятором, отличным от компилятора выражения создания экземпляра, необходимо передать экземпляр стандартным способом, передав его в качестве первого аргумента конструктору суперкласса. Обратите внимание, что сам анонимный класс обязательно генерируется тем же компилятором, что и выражение создания экземпляра, поэтому компилятор может передать непосредственный внешний экземпляр относительно суперкласса в анонимный класс как пожелает, прежде чем анонимный класс передаст экземпляр в конструктор суперкласса. Однако, для согласованности, язык программирования Java считает в §15.9.5.1, что в некоторых случаях конструктор анонимного класса неявно объявляет начальный параметр для непосредственного внешнего экземпляра относительно суперкласса.

Тот факт, что не-private внутренний член класса может быть доступен компилятору, отличного от компилятора, который его сгенерировал, в то время как внутренний локальный класс или анонимный класс всегда доступны тому же компилятору, который его сгенерировал, объясняет, почему бинарное имя не-private внутреннего класса члена определяется как предсказуемое, но бинарное имя внутреннего локального класса или анонимного класса не является предсказуемым (§13.1).

8.8.2. Подпись конструктора

Ошибка компиляции возникает, если в классе объявляются два конструктора с переопределяемыми подписями (§8.4.2).

Ошибка компиляции возникает, если в классе объявляются два конструктора, подписи которых имеют одинаковую стирание (§4.6).

8.8.3. Модификаторы конструктора

ConstructorModifier:
(один из)
Аннотация public protected private

Правила, касающиеся модификаторов аннотаций для объявления конструктора, указаны в §9.7.4 и §9.7.5.

Ошибка компиляции возникает, если одно и то же ключевое слово используется более одного раза в качестве модификатора в объявлении конструктора, или если объявление конструктора содержит более одного из модификаторов доступа public, protected и private (§6.6).

В обычном объявлении класса конструктор без модификаторов доступа имеет доступ по умолчанию (пакетный доступ).

Если в объявлении метода используются два или более (различных) модификатора метода, то принято (хотя это и не обязательно), что они расположены в порядке, соответствующем показанному выше в описании MethodModifier.

В отличие от методов, конструктор не может быть abstract, static, final, native, strictfp или synchronized:

  • Конструктор не наследуется, поэтому нет необходимости объявлять его final.

  • Конструктор с abstract никогда не может быть реализован.

  • Конструктор всегда вызывается относительно объекта, поэтому для конструктора не имеет смысла быть static.

  • Практически нет необходимости для конструктора быть synchronized, так как это заблокирует объект в процессе создания, который обычно не предоставляется другим потокам до тех пор, пока все конструкторы для объекта не завершат свою работу.

  • Отсутствие native конструкторов — произвольный выбор при проектировании языка, который упрощает проверку реализации Java Virtual Machine, что конструкторы суперклассов всегда правильно вызываются во время создания объекта.

  • Невозможность объявления конструктора как strictfp (в отличие от метода (§8.4.3)) — умышленный выбор при проектировании языка, который вытекает из (ныне устаревшей) возможности объявления класса как strictfp.

8.8.4. Обобщенные конструкторы

Конструктор является обобщенным, если он объявляет одну или несколько переменных типа (§4.4).

Эти переменные типа известны как параметры типа конструктора. Форма раздела параметров типа обобщенного конструктора идентична разделу параметров типа обобщенного класса (§8.1.2).

Конструктор может быть обобщенным независимо от того, является ли сам класс, в котором он объявлен, обобщенным.

Объявление обобщенного конструктора определяет набор конструкторов, по одному для каждого возможного вызова раздела параметров типа аргументами типа. Аргументы типа могут не потребоваться явным образом при вызове обобщенного конструктора, так как они часто могут быть выведены (§18 (Вывод типов)).

Область действия и перекрытие параметров типа конструктора указаны в §6.3 и §6.4.1.

Ссылка на параметр типа конструктора из явного оператора вызова конструктора или вложенного класса или интерфейса ограничены, как указано в §6.5.5.1.

8.8.5. Конструктор Throws

Оператор throws для конструктора идентичен по структуре и поведению оператору throws для метода (§8.4.6).

8.8.6. Тип конструктора

Тип конструктора состоит из его сигнатуры и типов исключений, указанных в его операторе throws.

8.8.7. Тело конструктора

Первое утверждение тела конструктора может быть явным вызовом другого конструктора того же класса или непосредственного суперкласса (§8.8.7.1).

ConstructorBody:
{ [ЯвныйВызовКонструктора] [УтвержденияБлока] }

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

Если тело конструктора не начинается с явного вызова конструктора и объявляемый конструктор не является частью примитивного класса Object, то тело конструктора неявно начинается с вызова конструктора суперкласса "super();", вызова конструктора его непосредственного суперкласса без аргументов.

За исключением возможности явных вызовов конструкторов и запрета явного возврата значения (§14.17), тело конструктора аналогично телу метода (§8.4.7).

Оператор return (§14.17) может использоваться в теле конструктора, если он не содержит выражения.

Пример 8.8.7-1. Тела конструкторов

class Point {
    int x, y;
    Point(int x, int y) { this.x = x; this.y = y; }
}
class ColoredPoint extends Point {
    static final int WHITE = 0, BLACK = 1;
    int color;
    ColoredPoint(int x, int y) {
        this(x, y, WHITE);
    }
    ColoredPoint(int x, int y, int color) {
        super(x, y);
        this.color = color;
    }
}

Здесь первый конструктор ColoredPoint вызывает второй, предоставляя дополнительный аргумент; второй конструктор ColoredPoint вызывает конструктор своего суперкласса Point, передавая координаты.


8.8.7.1. Явные вызовы конструкторов

ExplicitConstructorInvocation:
[TypeArguments] this ( [СписокАргументов] ) ;
[TypeArguments] super ( [СписокАргументов] ) ;
ИмяВыражения . [TypeArguments] super ( [СписокАргументов] ) ;
Первичное . [TypeArguments] super ( [СписокАргументов] ) ;

Ниже приведены соответствующие правила из §4.5.1 и §15.12 для удобства:

TypeArguments:
< СписокАргументовТипов >
ArgumentList:
Выражение {, Выражение}

Явные вызовы конструкторов делятся на два типа:

  • Альтернативные вызовы конструкторов начинаются с ключевого слова this (возможно, с явными параметрами типа). Они используются для вызова альтернативного конструктора того же класса.

  • Вызовы конструкторов суперкласса начинаются с ключевого слова super (возможно, с явными параметрами типа) или выражения Первичное или ИмяВыражения. Они используются для вызова конструктора непосредственного суперкласса. Они далее делятся:

    • Неквалифицированные вызовы конструкторов суперкласса начинаются с ключевого слова super (возможно, с явными параметрами типа).

    • Квалифицированные вызовы конструкторов суперкласса начинаются с выражения Первичное или ИмяВыражения. Они позволяют конструктору подкласса явно указать вновь созданный объект в отношении непосредственного суперкласса с точки зрения его ближайшего внешнего экземпляра (§8.1.3). Это может быть необходимо, когда суперкласс является вложенным классом.

Утверждение явного вызова конструктора вводит статическую область (§8.1.3), что ограничивает использование конструкций, которые ссылаются на текущий объект. Заметим, что ключевые слова this и super запрещены в статической области (§15.8.3, §15.11.2), как и неквалифицированные ссылки на переменные экземпляра, методы экземпляра и параметры типа лексически окружающих объявлений (§6.5.5.1, §6.5.6.1, §15.12.3).

Если TypeArguments присутствует слева от this или super, то при наличии любых параметров типа-уайлдкардов (§4.5.1) возникает ошибка компиляции.

Пусть C — класс, который создаётся, а S — непосредственный суперкласс C.

Если утверждение вызова конструктора суперкласса является неквалифицированным, то:

  • Если S является внутренним членом класса, но S не является членом класса, окружающего C, то возникает ошибка компиляции.

    В противном случае, пусть O — ближайший окружающий класс C, членом которого является S. C должен быть внутренним классом O (§8.1.3), иначе возникает ошибка компиляции.

  • Если S является внутренним локальным классом, и S не встречается в статической области, пусть O — ближайший окружающий класс или интерфейс объявления S. C должен быть внутренним классом O, иначе возникает ошибка компиляции.

Если утверждение вызова конструктора суперкласса является квалифицированным, то:

  • Если S не является вложенным классом или если объявление S находится в статической области, то возникает ошибка компиляции.

  • В противном случае, пусть p — выражение Первичное или ИмяВыражения непосредственно перед ".super", и пусть O — ближайший окружающий класс S. Возникает ошибка компиляции, если тип p не равен O или не является подклассом O, или если тип p недоступен (§6.6).

Типы исключений, которые может генерировать утверждение явного вызова конструктора, указаны в §11.2.2.

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

Вычисление утверждения вызова конструктора суперкласса выполняется следующим образом:

  1. Пусть i — это создаваемый экземпляр. Необходимо определить непосредственно окружающий экземпляр i относительно S (если таковой имеется):

    • Если S не является внутренним классом или объявление S происходит в статическом контексте, то непосредственно окружающий экземпляр i относительно S не существует.

    • В противном случае, если вызов конструктора суперкласса не квалифицирован, то S обязательно является внутренним локальным классом или внутренним членом класса.

      Если S является внутренним локальным классом, пусть O — это непосредственно окружающее объявление класса или интерфейса S.

      Если S является внутренним членом класса, пусть O — это самый внутренний окружающий класс C, членом которого является S.

      Пусть n — целое число (n ≥ 1) такое, что O является n-м лексически окружающим классом или интерфейсом объявления C.

      Непосредственно окружающий экземпляр i относительно S — это n-й лексически окружающий экземпляр this.

      Хотя S может быть членом C из-за наследования, нулевой лексически окружающий экземпляр this (то есть, this сам по себе) никогда не используется в качестве непосредственно окружающего экземпляра i относительно S.

    • В противном случае, если вызов конструктора суперкласса квалифицирован, то выражение Primary или ExpressionName, непосредственно предшествующее ".super", p, оценивается.

      Если p оценивается как null, то возникает исключение NullPointerException, и вызов конструктора суперкласса завершается внезапно.

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

  2. После определения непосредственно окружающего экземпляра i относительно S (если таковой имеется), вычисление вызова конструктора суперкласса продолжается путём вычисления аргументов конструктора слева направо, как при обычном вызове метода, а затем вызова конструктора.

  3. Наконец, если оператор вызова конструктора суперкласса завершается нормально, то выполняются все инициализаторы переменных экземпляра класса C и все инициализаторы экземпляров класса C. Если инициализатор экземпляра или инициализатор переменной экземпляра I предшествует текстуально другому инициализатору экземпляра или инициализатору переменной экземпляра J, то I выполняется перед J.

    Выполнение инициализаторов переменных экземпляра и инициализаторов экземпляров выполняется независимо от того, появляется ли вызов конструктора суперкласса как явный оператор вызова конструктора или предоставляется неявно. (Альтернативный вызов конструктора не выполняет это дополнительное неявное выполнение.)

Пример 8.8.7.1-1. Ограничения на явные операторы вызова конструктора

Если первый конструктор ColoredPoint в примере из §8.8.7 был изменён следующим образом:

class Point {
    int x, y;
    Point(int x, int y) { this.x = x; this.y = y; }
}
class ColoredPoint extends Point {
    static final int WHITE = 0, BLACK = 1;
    int color;
    ColoredPoint(int x, int y) {
        this(x, y, color);  // Changed to color from WHITE
    }
    ColoredPoint(int x, int y, int color) {
        super(x, y);
        this.color = color;
    }
}

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


Пример 8.8.7.1-2. Квалифицированный вызов конструктора суперкласса

В коде ниже, ChildOfInner не имеет лексически окружающего класса или интерфейса, поэтому экземпляр ChildOfInner не имеет окружающего экземпляра. Однако суперкласс ChildOfInner (Inner) имеет лексически окружающее объявление класса (Outer), и экземпляр Inner должен иметь окружающий экземпляр Outer. Окружающий экземпляр Outer устанавливается при создании экземпляра Inner. Следовательно, при создании экземпляра ChildOfInner, который неявно является экземпляром Inner, мы должны предоставить окружающий экземпляр Outer с помощью оператора квалифицированного вызова суперкласса в конструкторе ChildOfInner. Экземпляр Outer называется непосредственно окружающим экземпляром ChildOfInner относительно Inner.

class Outer {
    class Inner {}
}
class ChildOfInner extends Outer.Inner {
    ChildOfInner() { (new Outer()).super(); }
}

Возможно, неожиданно, один и тот же экземпляр Outer может служить непосредственно окружающим экземпляром ChildOfInner относительно Inner для нескольких экземпляров ChildOfInner. Эти экземпляры ChildOfInner неявным образом связаны с одним и тем же экземпляром Outer. В приведённой ниже программе это достигается путём передачи экземпляра Outer в конструктор ChildOfInner, который использует экземпляр в операторе квалифицированного вызова конструктора суперкласса. Правила для оператора явного вызова конструктора не запрещают использование формальных параметров конструктора, содержащего оператор.

class Outer {
    int secret = 5;
    class Inner {
        int  getSecret()      { return secret; }
        void setSecret(int s) { secret = s; }
    }
}
class ChildOfInner extends Outer.Inner {
    ChildOfInner(Outer x) { x.super(); }
}

public class Test {
    public static void main(String[] args) {
        Outer x = new Outer();
        ChildOfInner a = new ChildOfInner(x);
        ChildOfInner b = new ChildOfInner(x);
        System.out.println(b.getSecret());
        a.setSecret(6);
        System.out.println(b.getSecret());
    }
}

Эта программа выводит:

5
6

Эффект заключается в том, что манипуляции с переменными экземпляра в общем экземпляре Outer видны через ссылки на разные экземпляры ChildOfInner, даже если такие ссылки не являются алиасами в традиционном смысле.


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

Перегрузка конструкторов идентична по поведению перегрузке методов (§8.4.9). Перегрузка разрешается на этапе компиляции для каждого выражения создания экземпляра класса (§15.9).

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

Если класс не содержит объявлений конструкторов, то конструктор по умолчанию объявляется неявно. Формат конструктора по умолчанию для класса верхнего уровня, вложенного класса или локального класса следующий:

  • Конструктор по умолчанию имеет тот же модификатор доступа, что и класс, за исключением случаев, когда класс не имеет модификатора доступа, в этом случае конструктор по умолчанию имеет доступ к пакету (§6.6).

  • Конструктор по умолчанию не имеет формальных параметров, за исключением не-private вложенного внутреннего класса-члена, где конструктор по умолчанию неявно объявляет один формальный параметр, представляющий непосредственно окружающий экземпляр класса (§8.8.1, §15.9.2, §15.9.3).

  • Конструктор по умолчанию не имеет throws описания.

  • Если объявляемый класс является первоначальным классом Object, то конструктор по умолчанию имеет пустое тело. В противном случае конструктор по умолчанию просто вызывает конструктор суперкласса без аргументов.

Формат конструктора по умолчанию для анонимного класса указан в §15.9.5.1.

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

Пример 8.8.9-1. Конструкторы по умолчанию

Объявление:


public class Point {
    int x, y;
}

эквивалентно объявлению:


public class Point {
    int x, y;
    public Point() { super(); }
}

где конструктор по умолчанию является public, поскольку класс Point является public.


Пример 8.8.9-2. Доступность конструкторов по отношению к классам

Правило, что конструктор по умолчанию класса имеет тот же доступ, что и сам класс, простое и интуитивное. Однако это не подразумевает, что конструктор доступен всякий раз, когда доступен класс. Рассмотрим:

package p1;
public class Outer {
    protected class Inner {}
}
package p2;
class SonOfOuter extends p1.Outer {
    void foo() {
        new Inner();  // compile-time access error
    }
}

Конструктор по умолчанию для Inner является protected. Однако конструктор является protected относительно Inner, в то время как Inner является protected относительно Outer. Таким образом, Inner доступен в SonOfOuter, поскольку он является подклассом Outer. Конструктор Inner недоступен в SonOfOuter, потому что класс SonOfOuter не является подклассом Inner! Следовательно, даже если Inner доступен, его конструктор по умолчанию недоступен.


8.8.10. Предотвращение создания экземпляра класса

Класс может быть спроектирован таким образом, чтобы предотвратить создание экземпляров класса извне объявления класса, объявив по меньшей мере один конструктор, чтобы предотвратить создание конструктора по умолчанию, и объявив все конструкторы private (§6.6.1).

Класс public аналогичным образом может предотвратить создание экземпляров за пределами своего пакета, объявив по меньшей мере один конструктор, чтобы предотвратить создание конструктора по умолчанию с доступом public, и не объявив конструктор, который является public или protected (§6.6.2).

Пример 8.8.10-1. Предотвращение создания экземпляра с помощью доступности конструктора


class ClassOnly {
    private ClassOnly() { }
    static String just = "only the lonely";
}

Здесь класс ClassOnly нельзя создать экземпляр, в то время как в следующем коде:


package just;
public class PackageOnly {
    PackageOnly() { }
    String[] justDesserts = { "cheesecake", "ice cream" };
}

класс public PackageOnly можно создать экземпляр только внутри пакета just, в котором он объявлен. Это ограничение также будет действовать, если конструктор PackageOnly был protected, хотя в этом случае код в других пакетах мог бы создать экземпляры подклассов PackageOnly.


8.9. Классы перечислений

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

EnumDeclaration:
{МодификаторКласса} enum ИдентификаторТипа [РеализуетКласс] ТелоПеречисления

Объявление перечисления может задавать класс перечисления верхнего уровня (§7.6), вложенный класс перечисления (§8.5, §9.5) или локальный класс перечисления (§14.3).

ИдентификаторТипа в объявлении перечисления задаёт имя класса перечисления.

Ошибка компиляции, если объявление перечисления имеет модификатор abstract, final, sealed или non-sealed.

Класс перечисления явным образом final или явным образом sealed следующим образом:

  • Класс перечисления неявно final, если его объявление не содержит констант перечисления с телом класса (§8.9.1).

  • Класс перечисления E неявно sealed, если его объявление содержит хотя бы одну константу перечисления с телом класса. Разрешённые непосредственные подклассы (§8.1.6) E — это анонимные классы, неявно объявленные константами перечисления, имеющими тело класса.

Вложенный класс перечисления неявно static. То есть, каждый вложенный и локальный класс перечисления является static. Для объявления вложенного класса перечисления разрешено избыточно указывать модификатор static, но для объявления локального класса перечисления (§14.3) не разрешается.

Ошибка компиляции, если одно и то же ключевое слово используется более одного раза в качестве модификатора для объявления перечисления или если объявление перечисления имеет более одного из модификаторов доступа public, protected и private (§6.6).

Непосредственный тип суперкласса класса перечисления E — это Enum<E> (§8.1.4).

Объявление перечисления не имеет пункта extends, поэтому невозможно явно объявить тип непосредственного суперкласса, даже Enum<E>.

Класс перечисления не имеет экземпляров, кроме тех, которые определены его константами перечисления. Ошибка компиляции при попытке явного создания экземпляра класса перечисления (§15.9.1).

Помимо ошибки компиляции, существуют три механизма, которые гарантируют, что экземпляры класса перечисления не существуют, кроме тех, которые определены его константами перечисления:

  • Метод final clone в Enum гарантирует, что константы перечисления никогда не могут быть клонированы.

  • Запрещено рефлексивное создание экземпляров классов перечисления.

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

8.9.1. Константы перечисления

Тело объявления перечисления может содержать константы перечисления. Константа перечисления определяет экземпляр класса перечисления.

EnumBody:
{ [СписокКонстантПеречисления] [,] [ОбъявленияТелаПеречисления] }
EnumConstantList:
КонстантаПеречисления {, КонстантаПеречисления}
EnumConstant:
{МодификаторКонстантыПеречисления} Идентификатор [( [СписокАргументов] )] [ТелоКласса]
EnumConstantModifier:
Аннотация

Для удобства показана следующая продукция из §15.12:

ArgumentList:
Выражение {, Выражение}

Правила, касающиеся модификаторов аннотаций для объявления константы перечисления, указаны в §9.7.4 и §9.7.5.

Идентификатор в КонстантеПеречисления может использоваться в имени для ссылки на константу перечисления.

Область действия и затемнение константы перечисления определены в §6.3 и §6.4.1.

За константой перечисления могут следовать аргументы, которые передаются конструктору перечисления при создании константы во время инициализации класса, как описано позже в этом разделе. Конструктор для вызова выбирается с использованием обычных правил разрешения перегрузки (§15.12.2). Если аргументы опущены, предполагается пустой список аргументов.

Необязательное тело класса константы перечисления неявно объявляет анонимный класс (§15.9.5), который (i) является непосредственным подклассом непосредственно окружающего класса перечисления (§8.1.4), и (ii) является final (§8.1.1.2). Тело класса регулируется обычными правилами анонимных классов; в частности, оно не может содержать конструкторов. Методы экземпляров, объявленные в этих телах классов, могут вызываться за пределами окружающего класса перечисления только в том случае, если они переопределяют доступные методы в окружающем классе перечисления (§8.4.8).

Ошибка компиляции, если тело класса константы перечисления объявляет метод abstract.

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

Метод equals в Enum — это метод final, который просто вызывает super.equals на своём аргументе и возвращает результат, тем самым выполняя сравнение по идентичности.

8.9.2. Объявления тела перечисления

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

EnumBodyDeclarations:
; {Объявление тела класса}

Следующие правила из §8.1.7 показаны здесь для удобства:

ClassBodyDeclaration:
Объявление члена класса
Инициализатор экземпляра
Статический инициализатор
Объявление конструктора
ClassMemberDeclaration:
Объявление поля
Объявление метода
Объявление класса
Объявление интерфейса
;

Любые объявления конструкторов или членов в теле объявления перечисления применяются к классу перечисления точно так же, как если бы они присутствовали в теле обычного объявления класса, если не указано иное.

Если в объявлении перечисления объявление конструктора является public или protected (§6.6), это ошибка компиляции.

Если в объявлении конструктора в объявлении перечисления присутствует оператор вызова конструктора суперкласса (§8.8.7.1), это ошибка компиляции.

Обращение к полю static класса перечисления из конструктора, инициализатора экземпляра или инициализатора переменной экземпляра в объявлении перечисления класса является ошибкой компиляции, за исключением случаев, когда поле является константной переменной (§4.12.4).

В объявлении перечисления объявление конструктора без модификаторов доступа private.

В объявлении перечисления без объявлений конструкторов неявно объявляется конструктор по умолчанию. Конструктор по умолчанию private, не имеет формальных параметров и не имеет throws.

На практике компилятор, вероятно, будет зеркалить класс Enum, объявляя параметры String и int в конструкторе по умолчанию класса перечисления. Однако эти параметры не указаны как "неявно объявленные", потому что разные компиляторы не обязаны соглашаться по форме конструктора по умолчанию. Только компилятор объявления перечисления знает, как создать константы перечисления; другие компиляторы могут просто полагаться на неявно объявленные public static поля класса перечисления (§8.9.3), не учитывая, как эти поля были инициализированы.

Если в объявлении перечисления E есть abstract метод m как член, то E должен иметь как минимум одну константу перечисления, и все константы перечисления E должны иметь тела класса, предоставляющие конкретные реализации m.

В объявлении перечисления запрещается объявлять финализатор (§12.6). Экземпляр класса перечисления никогда не может быть финализирован.

Пример 8.9.2-1. Объявления тела перечисления

enum Coin {
    PENNY(1), NICKEL(5), DIME(10), QUARTER(25);
    Coin(int value) { this.value = value; }

    private final int value;
    public int value() { return value; }
}

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


Пример 8.9.2-2. Ограничение на самоссылку констант перечисления

Без правила доступа к полю static код, на первый взгляд, верный, потерпит неудачу во время выполнения из-за цикличности инициализации, присущей классам перечисления. (Цикличность существует в любом классе с полем static "типа сам себя"). Вот пример такого кода, который потерпит неудачу:

import java.util.Map;
import java.util.HashMap;

enum Color {
    RED, GREEN, BLUE;
    Color() { colorMap.put(toString(), this); }

    static final Map<String,Color> colorMap =
        new HashMap<String,Color>();
}

Статическая инициализация этого перечисления выбросит NullPointerException, потому что переменная static colorMap не инициализирована, когда выполняются конструкторы для констант перечисления. Указанное выше ограничение гарантирует, что такой код не может быть скомпилирован. Однако код легко можно переработать, чтобы он работал должным образом:

import java.util.Map;
import java.util.HashMap;

enum Color {
    RED, GREEN, BLUE;

    static final Map<String,Color> colorMap =
        new HashMap<String,Color>();
    static {
        for (Color c : Color.values())
            colorMap.put(c.toString(), c);
    }
}

Переработанный вариант явно корректен, так как статическая инициализация выполняется сверху вниз.


8.9.3. Члены перечисления

Членами класса перечисления E являются все следующие:

  • Члены, объявленные в теле объявления E.

  • Члены, унаследованные от Enum<E>.

  • Для каждой константы перечисления c, объявленной в теле объявления E, E имеет неявно объявленное public static final поле типа E, имеющее то же имя, что и c. Поле имеет инициализатор переменной, который инициализирует E и передает любые аргументы c выбранному конструктору для E. Поле имеет те же аннотации, что и c (если таковые имеются).

    Эти поля неявно объявляются в том же порядке, что и соответствующие константы перечисления, перед любыми static полями, явно объявленными в теле объявления E.

    Константа перечисления считается созданной при инициализации соответствующего неявно объявленного поля.

  • Неявно объявленный метод public static E[] values(), который возвращает массив, содержащий константы перечисления E в том же порядке, что и в теле объявления E.

  • Неявно объявленный метод public static E valueOf(String name), который возвращает константу перечисления E с указанным именем.

Следует, что объявление класса перечисления E не может содержать полей, конфликтующих с неявно объявленными полями, соответствующими константам перечисления E, а также не может содержать методы, конфликтующие с неявно объявленными методами или переопределяющие final методы класса Enum<E>.

Пример 8.9.3-1. Итерация по константам перечисления с помощью цикла с расширенным for

public class Test {
    enum Season { WINTER, SPRING, SUMMER, FALL }

    public static void main(String[] args) {
        for (Season s : Season.values())
            System.out.println(s);
    }
}

Эта программа выводит следующий результат:

WINTER
SPRING
SUMMER
FALL

Пример 8.9.3-2. Переключение на константы перечисления

Оператор switch (§14.11) полезен для имитации добавления метода в класс перечисления извне класса. В данном примере к классу color добавляется метод Coin из §8.9.2 и печатается таблица монет, их значения и цветов.

class Test {
    enum CoinColor { COPPER, NICKEL, SILVER }

    static CoinColor color(Coin c) {
        switch (c) {
            case PENNY:
                return CoinColor.COPPER;
            case NICKEL:
                return CoinColor.NICKEL;
            case DIME: case QUARTER:
                return CoinColor.SILVER;
            default:
                throw new AssertionError("Unknown coin: " + c);
        }
    }

    public static void main(String[] args) {
        for (Coin c : Coin.values())
            System.out.println(c + "\t\t" +
                               c.value() + "\t" + color(c));
    }
}

Эта программа выводит следующий результат:

PENNY           1       COPPER
NICKEL          5       NICKEL
DIME            10      SILVER
QUARTER         25      SILVER

Пример 8.9.3-3. Константы перечисления с телами класса

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

enum Operation {
    PLUS {
        double eval(double x, double y) { return x + y; }
    },
    MINUS {
        double eval(double x, double y) { return x - y; }
    },
    TIMES {
        double eval(double x, double y) { return x * y; }
    },
    DIVIDED_BY {
        double eval(double x, double y) { return x / y; }
    };

    // Each constant supports an arithmetic operation
    abstract double eval(double x, double y);

    public static void main(String args[]) {
        double x = Double.parseDouble(args[0]);
        double y = Double.parseDouble(args[1]);
        for (Operation op : Operation.values())
            System.out.println(x + " " + op + " " + y +
                               " = " + op.eval(x, y));
    }
}

Программа выводит следующий результат:

java Operation 2.0 4.0
2.0 PLUS 4.0 = 6.0
2.0 MINUS 4.0 = -2.0
2.0 TIMES 4.0 = 8.0
2.0 DIVIDED_BY 4.0 = 0.5

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


Пример 8.9.3-4. Несколько классов перечисления

В следующей программе класс игральной карты построен поверх двух простых перечислений.

import java.util.List;
import java.util.ArrayList;
class Card implements Comparable<Card>,
                      java.io.Serializable {
    public enum Rank { DEUCE, THREE, FOUR, FIVE, SIX, SEVEN,
                       EIGHT, NINE, TEN,JACK, QUEEN, KING, ACE }

    public enum Suit { CLUBS, DIAMONDS, HEARTS, SPADES }

    private final Rank rank;
    private final Suit suit;
    public Rank rank() { return rank; }
    public Suit suit() { return suit; }

    private Card(Rank rank, Suit suit) {
        if (rank == null || suit == null)
            throw new NullPointerException(rank + ", " + suit);
        this.rank = rank;
        this.suit = suit;
    }

    public String toString() { return rank + " of " + suit; }

    // Primary sort on suit, secondary sort on rank
    public int compareTo(Card c) {
        int suitCompare = suit.compareTo(c.suit);
        return (suitCompare != 0 ?
                    suitCompare :
                    rank.compareTo(c.rank));
    }

    private static final List<Card> prototypeDeck =
        new ArrayList<Card>(52);

    static {
        for (Suit suit : Suit.values())
            for (Rank rank : Rank.values())
                prototypeDeck.add(new Card(rank, suit));
    }

    // Returns a new deck
    public static List<Card> newDeck() {
        return new ArrayList<Card>(prototypeDeck);
    }
}

Следующая программа использует класс Card. Она принимает два целочисленных параметра в командной строке, представляющих количество рук для раздачи и количество карт в каждой руке:

import java.util.List;
import java.util.ArrayList;
import java.util.Collections;
class Deal {
    public static void main(String args[]) {
        int numHands     = Integer.parseInt(args[0]);
        int cardsPerHand = Integer.parseInt(args[1]);
        List<Card> deck  = Card.newDeck();
        Collections.shuffle(deck);
        for (int i=0; i < numHands; i++)
            System.out.println(dealHand(deck, cardsPerHand));
    }

    /**
     * Returns a new ArrayList consisting of the last n
     * elements of deck, which are removed from deck.
     * The returned list is sorted using the elements'
     * natural ordering.
     */
    public static <E extends Comparable<E>>
    ArrayList<E> dealHand(List<E> deck, int n) {
        int deckSize = deck.size();
        List<E> handView = deck.subList(deckSize - n, deckSize);
        ArrayList<E> hand = new ArrayList<E>(handView);
        handView.clear();
        Collections.sort(hand);
        return hand;
    }
}

Программа выводит следующий результат:

java Deal 4 3
[DEUCE of CLUBS, SEVEN of CLUBS, QUEEN of DIAMONDS]
[NINE of HEARTS, FIVE of SPADES, ACE of SPADES]
[THREE of HEARTS, SIX of HEARTS, TEN of SPADES]
[TEN of CLUBS, NINE of DIAMONDS, THREE of SPADES]

8.10. Классы записей

Объявление записи указывает новый класс записи, ограниченный вид класса, определяющий простую совокупность значений.

RecordDeclaration:
{Изготовитель класса} record Идентификатор типа [Параметры типа] Заголовок записи [Реализует класс] Тело записи

Объявление записи может указывать класс записи верхнего уровня (§7.6), класс записи члена (§8.5, §9.5) или локальный класс записи (§14.3).

Идентификатор типа в объявлении записи указывает имя класса записи.

Ошибка времени компиляции, если в объявлении записи есть модификатор abstract, sealed или non-sealed.

Класс записи неявно final. Допускается избыточное указание модификатора final при объявлении класса записи.

Вложенный класс записи неявно static. То есть каждый член-класс записи и локальный класс записи static. Допускается избыточное указание модификатора static при объявлении класса записи-члена, но недопустимо для объявления локального класса записи (§14.3).

Ошибка времени компиляции, если одно и то же ключевое слово встречается более одного раза в качестве модификатора для объявления записи или если объявление записи имеет более одного из модификаторов доступа public, protected и private (§6.6).

Тип непосредственного суперкласса класса записи – Record (§8.1.4).

Объявление записи не имеет фрагмента extends, поэтому явно указать тип непосредственного суперкласса нельзя, даже Record.

Механизм сериализации обрабатывает экземпляры класса записи иначе, чем обычные сериализуемые или внешние объекты. В частности, объект записи десериализуется с помощью канонического конструктора (§8.10.4).

8.10.1. Компоненты записи

Компоненты записи класса записи, если они есть, указываются в заголовке объявления записи. Каждый компонент записи состоит из типа (необязательно предваряемого одним или несколькими аннотациями) и идентификатора, который указывает имя компонента записи. Компонент записи соответствует двум членам класса записи: неявно объявленному полю private и явно или неявно объявленному методу доступа public (§8.10.3).

Если у класса записи нет компонентов записи, то в заголовке объявления записи появляется пустая пара скобок.

RecordHeader:
( [Список компонентов записи] )
RecordComponentList:
Компонент записи {, Компонент записи}
RecordComponent:
{Модификатор компонента записи} Немаркированный тип Идентификатор
Компонент записи с переменной арностью
VariableArityRecordComponent:
{Модификатор компонента записи} Немаркированный тип {Аннотация} ... Идентификатор
RecordComponentModifier:
Аннотация

Компонент записи может быть компонентом записи с переменной арностью, указанной многоточием после типа. Максимально один компонент записи с переменной арностью допускается для класса записи. Ошибка времени компиляции, если компонент записи с переменной арностью появляется где-либо в списке компонентов записи, кроме последней позиции.

Правила, касающиеся модификаторов аннотации для объявления компонента записи, указаны в §9.7.4 и §9.7.5.

Аннотации в объявлении компонента записи доступны через рефлексию, если их интерфейсы аннотаций применимы в контексте компонента записи (§9.6.4.1). Независимо от этого, аннотации в объявлении компонента записи распространяются на объявления членов и конструкторов класса записи, если их интерфейсы аннотаций применимы в других контекстах (§8.10.3, §8.10.4).

Ошибка времени компиляции для объявления записи, если объявлен компонент записи с именем clone, finalize, getClass, hashCode, notify, notifyAll, toString или wait.

Это имена методов без аргументов public и protected в Object. Запрет на их использование в качестве имён компонентов записи предотвращает путаницу в нескольких аспектах. Во-первых, каждый класс записи предоставляет реализации методов hashCode и toString, которые возвращают представления объекта записи в целом; они не могут быть методами доступа (§8.10.3) для компонентов записи, называемых hashCode или toString, и нет способа получить доступ к таким компонентам извне класса записи. Аналогично, некоторые классы записей могут предоставлять реализации методов clone и (к сожалению) finalize, поэтому компонент записи, называемый clone или finalize, не может быть доступен через метод доступа. Наконец, методы getClass, notify, notifyAll и wait в Object являются final, поэтому компоненты записи с такими же именами не могут иметь методы доступа. (Методы доступа имели бы такие же сигнатуры, как методы final, и, таким образом, пытались бы, но безуспешно, переопределить их.)

Ошибка времени компиляции, если в объявлении записи объявлены два компонента записи с одинаковым именем.

Объявленный тип компонента записи зависит от того, является ли он компонентом записи с переменной арностью:

  • Если компонент записи не является компонентом записи с переменной арностью, то объявленный тип обозначается как Немаркированный тип.

  • Если компонент записи является компонентом записи с переменной арностью, то объявленный тип является типом массива, указанным в §10.2.

Если объявленный тип компонента записи с переменной арностью имеет нереализуемый базовый тип (§4.7), то во время компиляции возникает предупреждение о проверке типа для объявления компонента записи с переменной арностью, если канонический конструктор (§8.10.4) не аннотирован @SafeVarargs (§9.6.4.7) или предупреждение подавляется @SuppressWarnings (§9.6.4.5).

8.10.2. Объявления тела записи

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

RecordBody:
{ {ОбъявлениеRecordBody} }
RecordBodyDeclaration:
ОбъявлениеТелаКласса
КонструкторCompact

Следующие производства из §8.1.7 приведены здесь для удобства:

ClassBodyDeclaration:
ОбъявлениеЧленаКласса
ИнициализаторЭкземпляра
СтатическийИнициализатор
ОбъявлениеКонструктора
ClassMemberDeclaration:
ОбъявлениеПоля
ОбъявлениеМетода
ОбъявлениеКласса
ОбъявлениеИнтерфейса
;

Оператор CompactConstructorDeclaration описан в §8.10.4.2.

Ошибка компиляции возникает, если тело объявления записи содержит объявление поля, которое не является static (см. §8.3.1.1).

Ошибка компиляции возникает, если тело объявления записи содержит объявление метода, являющегося abstract или native (см. §8.4.3.1, §8.4.3.4).

Ошибка компиляции возникает, если тело объявления записи содержит инициализатор экземпляра (см. §8.6).

8.10.3. Члены записи

Для каждого компонента записи класс записи имеет поле с таким же именем, как и компонент записи, и таким же типом, как и объявленный тип компонента записи. Это поле, которое неявно объявлено, называется полем компонента.

Поле компонента является private, final и не-static.

Поле компонента снабжено аннотациями, если они есть, которые появляются на соответствующем компоненте записи и интерфейсы которых применимы в контексте объявления поля или в контекстах типов, или в обоих (см. §9.7.4).

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

Если метод доступа к компоненту записи объявлен явно, то должны быть истинными все следующие утверждения, в противном случае возникает ошибка компиляции:

  • Тип возвращаемого значения метода доступа (§8.4.5) должен совпадать с объявленным типом компонента записи.

  • Метод доступа не должен быть обобщенным (§8.4.4).

  • Метод доступа должен быть public методом экземпляра без формальных параметров и без throws оговорки.

Если у класса записи есть компонент записи, для которого метод доступа не объявлен явно, тогда метод доступа для этого компонента записи объявляется неявно со следующими свойствами:

  • Его имя совпадает с именем компонента записи.

  • Его тип возвращаемого значения совпадает с объявленным типом компонента записи.

  • Он не обобщен.

  • Он является public методом экземпляра без формальных параметров и без throws оговорки.

  • Он снабжен аннотациями, если они есть, которые появляются на соответствующем компоненте записи и интерфейсы которых применимы в контексте объявления метода или в контекстах типов, или в обоих (см. §9.7.4).

  • Его тело возвращает значение соответствующего поля компонента.

Ограничения на имена компонентов записи (§8.10.1) означают, что ни один неявно объявленный метод доступа не имеет подписи, эквивалентной по перекрытию методу класса Object, который не является private. Явное объявление метода, которое принимает одно из ограниченных имен, например public void wait() {...}, не является методом доступа, поскольку wait никогда не является именем компонента записи.

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

Аннотации, которые переносятся на неявно объявленный метод доступа, должны привести к законному аннотированному методу. Например, в следующем объявлении записи неявно объявленный метод доступа x() был бы аннотирован @SafeVarargs, но такая аннотация является незаконной для метода с фиксированной арностью (§9.6.4.7):

record BadRecord(@SafeVarargs int x) {}  // Error

Классы записи могут явно объявлять методы экземпляра, кроме методов доступа, но не могут явно объявлять переменные экземпляра (§8.10.2). Допускаются явные объявления методов класса и переменных класса.

Все члены классов записей, включая неявно объявленные члены, подчиняются обычным правилам для объявлений членов в классе (§8.3, §8.4, §8.5).

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

Например, класс записи может унаследовать методы по умолчанию от своих непосредственных суперинтерфейсов, хотя тела методов по умолчанию не знают о полях компонентов класса записи. Следующая программа выводит Logged:

public class Test {
    interface Logging {
        default void logAction() {
            System.out.println("Logged");
        }	
    }

    record Point(int i, int j) implements Logging {}

    public static void main(String[] args) {
        Point p = new Point(10, 20);
        p.logAction();
    }
}

Класс записи предоставляет реализации всех abstract методов, объявленных в классе Record. Для каждого из следующих методов, если класс записи R не объявляет явно метод с теми же модификаторами, именем и подписью (§8.4.2), то метод неявно объявляется следующим образом:

  • Метод public final boolean equals(Object), который возвращает true только в том случае, если аргумент является экземпляром R, и текущий экземпляр равен экземпляру аргумента в каждом компоненте записи R; в противном случае возвращается false.

    Равенство экземпляра a класса записи R с другим экземпляром b того же класса записи в компоненте записи c определяется следующим образом:

    • Если тип компонента записи c является ссылочным типом, равенство определяется следующим образом: если значение поля компонента c как в a, так и в b является нулевой ссылкой, то возвращается true; если значение поля компонента c либо в a, либо в b, но не в обоих, является нулевой ссылкой, то возвращается false; в противном случае равенство определяется вызовом метода equals к значению поля компонента c из a с аргументом, являющимся значением поля компонента c из b.

    • Если тип компонента записи c является примитивным типом T, равенство определяется как при вызове метода static compare соответствующего обертывающего класса для T (§5.1.7), с первым аргументом, задаваемым значением поля компонента c из a, и вторым аргументом, задаваемым значением поля компонента c из b; если метод вернёт 0, то возвращается true, в противном случае возвращается false.

    Использование compare в обертывающих классах гарантирует, что неявно объявленный метод equals является рефлексивным и согласованно работает с неявно объявленным методом hashCode для классов записей, содержащих компоненты с плавающей точкой.

  • Метод public final int hashCode(), возвращающий значение хэш-кода, полученное из значений хэш-кодов каждого компонента записи R.

    Значение хэш-кода экземпляра a класса записи в компоненте записи c определяется следующим образом:

    • Если тип компонента записи c является ссылочным типом, то значение хэш-кода определяется как при вызове метода hashCode для значения поля компонента c из a.

    • Если тип компонента записи c является примитивным типом T, то значение хэш-кода определяется как при преобразовании значения поля компонента c из a в упакованный тип (§5.1.7), а затем вызове метода hashCode соответствующего обертывающего класса для T над полученным объектом.

  • Метод public final String toString(), возвращающий строку, полученную из имени класса записи и имён и строковых представлений каждого компонента записи R.

    Строковое представление компонента записи c экземпляра a класса записи определяется следующим образом:

    • Если тип компонента записи c является ссылочным типом, то строковое представление определяется как при вызове метода toString для значения поля компонента c из a.

    • Если тип компонента записи c является примитивным типом T, то строковое представление определяется как при преобразовании значения поля компонента c из a в упакованный тип (§5.1.7), а затем вызове метода toString соответствующего обертывающего класса для T над полученным объектом.

Обратите внимание, что равенство, значения хэш-кодов и строковые представления определяются, обращаясь непосредственно к значениям полей компонентов, а не вызывая методы-аксессоры.

Рассмотрим класс записи R с компонентами c1, ..., cn и неявно объявленным методом-аксессором для каждого компонента, а также неявно объявленным методом equals. Если экземпляр r1 класса R копируется следующим образом:

R r2 = new R(r1.c1(), r1.c2(), ..., r1.cn());

то, при условии, что r1 не является нулевой ссылкой, выражение r1.equals(r2) всегда оценивается как true. Явно объявленные методы-аксессоры и методы equals должны соблюдать это инвариант. Компилятор, как правило, не может проверить, соблюдают ли явно объявленные методы этот инвариант. Следующее объявление класса записи является плохим стилем, так как его методы-аксессоры обрезают компоненты x и y и тем самым препятствуют тому, чтобы p3 была equals к p1:

record SmallPoint(int x, int y) {
    public int x() { return this.x < 100 ? this.x : 100; }
    public int y() { return this.y < 100 ? this.y : 100; }

    public static void main(String[] args) {
        SmallPoint p1 = new SmallPoint(200,300);
        SmallPoint p2 = new SmallPoint(200,300);
        System.out.println(p1.equals(p2));  // prints true
	
        SmallPoint p3 = new SmallPoint(p1.x(), p1.y());
        System.out.println(p1.equals(p3));  // prints false
    }
}

8.10.4. Объявления конструкторов записей

Для обеспечения правильной инициализации компонентов записи, класс записи не объявляет неявно конструктор по умолчанию (§8.8.9). Вместо этого, класс записи имеет канонический конструктор, объявленный явно или неявно, который инициализирует все поля компонентов класса записи.

Существует два способа явного объявления канонического конструктора в объявлении записи: объявив обычный конструктор с подходящей сигнатурой (§8.10.4.1) или объявив компактный конструктор (§8.10.4.2).

Учитывая сигнатуру обычного конструктора, который квалифицируется как канонический, и сигнатуру, выведенную для компактного конструктора, правила сигнатур конструкторов (§8.8.2) означают, что наличие и обычного конструктора, квалифицирующегося как канонический, и компактного конструктора в объявлении записи является ошибкой времени компиляции.

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

  • Если класс записи является public, то канонический конструктор должен быть public; в противном случае возникает ошибка времени компиляции.

  • Если класс записи является protected, то канонический конструктор должен быть protected или public; в противном случае возникает ошибка времени компиляции.

  • Если класс записи имеет доступность пакета, то канонический конструктор не должен быть private; в противном случае возникает ошибка времени компиляции.

  • Если класс записи является private, то канонический конструктор может быть объявлен с любой доступностью.

Явно объявленный канонический конструктор может быть конструктором с фиксированным числом аргументов или конструктором с переменным числом аргументов (§8.8.1).

Если канонический конструктор не объявлен явно в объявлении класса записи R, то канонический конструктор r неявно объявляется в R со следующими свойствами:

  • Сигнатура r не содержит параметров типа и имеет формальные параметры, задаваемые выведенным списком формальных параметров R, определённым ниже.

  • r имеет тот же модификатор доступа, что и R, за исключением случая, когда R не имеет модификатора доступа, в этом случае r имеет доступность пакета.

  • r не имеет throws фрагмента.

  • Тело r инициализирует каждое поле компонента класса записи соответствующим формальным параметром r в порядке появления компонентов записи (соответствующих полям компонентов) в заголовке записи.

Выведенный список формальных параметров класса записи формируется путём получения формального параметра для каждого компонента записи в заголовке записи в порядке следования, как следует:

  • Если компонент записи не является компонентом переменной арности, то выведенный формальный параметр имеет то же имя и тип, что и компонент записи.

    Если компонент записи является компонентом переменной арности, то выведенный формальный параметр является параметром переменной арности (§8.4.1) с тем же именем и типом, что и компонент записи.

  • Выведенный формальный параметр снабжается аннотациями, если таковые имеются, которые присутствуют на компоненте записи и чьи интерфейсы аннотаций применимы в контексте формального параметра или в контекстах типов, или в обоих случаях (§9.7.4).

Объявление записи может содержать объявления конструкторов, которые не являются каноническими конструкторами. Тело каждого неканонического конструктора в объявлении записи должно начинаться с вызова альтернативного конструктора (§8.8.7.1), в противном случае возникает ошибка времени компиляции.

8.10.4.1. Обычные канонические конструкторы

(Некомпактный) конструктор в объявлении класса записи R является каноническим конструктором R, если его сигнатура эквивалентна сигнатуре вывода (§8.4.2) выведенной сигнатуры конструктора R.

Выведенная сигнатура конструктора класса записи R представляет собой сигнатуру, состоящую из имени R, без параметров типа и типов формальных параметров, полученных из заголовка записи R путём взятия объявленного типа каждого компонента записи в порядке следования.

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

Объявление (некомпактного) канонического конструктора должно удовлетворять всем следующим условиям, в противном случае возникает ошибка времени компиляции:

  • Каждый формальный параметр в списке формальных параметров должен иметь то же имя и тип, что и соответствующий компонент записи.

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

  • Конструктор не должен быть обобщённым (§8.8.4).

  • Конструктор не должен иметь throws фрагмента.

  • Тело конструктора не должно содержать оператора явного вызова конструктора (§8.8.7.1).

  • Все другие правила для объявлений конструкторов в нормальном объявлении класса должны быть выполнены (§8.8).

Следствием этих правил является то, что аннотации на компоненте записи могут отличаться от аннотаций на соответствующем формальном параметре явно объявленного канонического конструктора. Например, следующее объявление записи является корректным:

import java.lang.annotation.Target;
import java.lang.annotation.ElementType;

@interface Foo {}
@interface Bar {}

record Person(@Foo String name) {
    Person(@Bar String name) {
        this.name = name;
    }
}

8.10.4.2. Компактные канонические конструкторы

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

CompactConstructorDeclaration:
{МодификаторКонструктора} ПростоеИмяТипа ТелоКонструктора

Следующие производства из §8.8, §8.8.3 и §8.8.7 приведены здесь для удобства:

ConstructorModifier:
(один из)
Аннотация public protected private
SimpleTypeName:
ИдентификаторТипа
ConstructorBody:
{ [ЯвноеВызовКонструктора] [БлочныеИнструкции] }

При объявлении записи наличие более одного компактного объявления конструктора является ошибкой компиляции.

Формальные параметры компактного конструктора класса записи объявляются неявно. Они определяются выведенным списком формальных параметров класса записи (§8.10.4).

Компактный конструктор класса записи является конструктором с переменным числом аргументов (§8.8.1), если класс записи имеет компонент записи с переменным числом аргументов.

Подпись компактного объявления конструктора равна выведенной подписи конструктора класса записи (§8.10.4.1).

Тело компактного объявления конструктора должно удовлетворять всем следующим условиям, в противном случае произойдет ошибка компиляции:

  • Тело не должно содержать оператор возврата (§14.17).

  • Тело не должно содержать оператор явного вызова конструктора (§8.8.7.1).

  • Тело не должно содержать присвоения значений компонентам поля класса записи.

  • Все остальные правила для конструктора в обычном объявлении класса должны быть соблюдены (§8.8), за исключением требования, что компоненты поля класса записи должны быть определенно инициализированы и не определенно неинициализированы в конце компактного конструктора (§8.3.1.2).

Если у объявления записи есть компонент, имеющий имя c, то простое имя c в теле компактного конструктора обозначает неявный формальный параметр, названный c, а не поле компоненты, названное c.

После завершения последней инструкции (если она есть) в теле компактного конструктора (нормальное завершение, §14.1), все поля компонентов класса записи неявно инициализируются значениями соответствующих формальных параметров. Компоненты полей инициализируются в том порядке, в котором соответствующие компоненты записи объявлены в заголовке записи.

Цель компактного объявления конструктора — предоставить только код для проверки или нормализации параметров в теле конструктора; остальной код инициализации предоставляется компилятором. Например, следующий класс записи имеет компактный конструктор, упрощающий рациональное число:

record Rational(int num, int denom) {
    private static int gcd(int a, int b) {
        if (b == 0) return Math.abs(a);
        else return gcd(b, a % b);
    }
   
    Rational {
        int gcd = gcd(num, denom);
        num    /= gcd;
        denom  /= gcd;
    }
}

Компактный конструктор Rational {...} ведет себя так же, как и этот обычный конструктор:

Rational(int num, int demon) {
    int gcd = gcd(num, denom);
    num    /= gcd;
    denom  /= gcd;
    this.num   = num;
    this.denom = denom;
}

© Oracle and/or its affiliates. All rights reserved.
Licensed under the Oracle Technology Network License Agreement.

Spec-Zone.ru

Настройки Оффлайн Что нового Помощь О нас
Spec-Zone .ru
спецификации, руководства, описания, API