Глава 8. Классы
Оглавление
- 8.1. Объявления классов
- 8.2. Члены класса
- 8.3. Объявления полей
- 8.4. Объявления методов
- 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.8. Перегрузка конструкторов
- 8.8.9. Конструктор по умолчанию
- 8.8.10. Препятствие к созданию экземпляра класса
- 8.9. Классы перечислений
- 8.10. Классы записей
Объявление класса определяет новый класс и описывает его реализацию (§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.9) и объявления записей (§8.10).
Класс также неявно объявляется выражением создания экземпляра класса (§15.9.5) и константой перечисления, завершающейся телом класса (§8.9.1).
ИдентификаторТипа в объявлении класса указывает имя класса.
Если класс имеет то же простое имя, что и любой из его содержащих классов или интерфейсов, возникает ошибка компиляции.
Область видимости и перекрытие объявления класса указаны в §6.3 и §6.4.1.
В объявлении класса могут присутствовать модификаторы класса.
Правила, касающиеся модификаторов аннотаций для объявления класса, указаны в §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, возникает ошибка компиляции.
Если в объявлении класса присутствует два или более (разных) модификатора класса, то обычно, хотя и не обязательно, они располагаются в порядке, согласованном с приведенным выше в производстве для МодификатораКласса.
Абстрактный класс — это класс, который неполный или должен считаться неполным.
Если попытка создать экземпляр абстрактного класса с помощью выражения создания экземпляра класса (§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 . . .
}
Класс может быть объявлен 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.
Модификатор strictfp в объявлении класса устарел и не должен использоваться в новом коде. Его наличие или отсутствие не влияет на время компиляции или выполнение.
Модификатор 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).
Класс является обобщённым, если объявление класса объявляет одну или несколько переменных типа (§4.4).
Эти переменные типа известны как параметры типа класса. Раздел параметров типа следует за именем класса и ограничен угловыми скобками.
Следующие правила из §4.4 представлены здесь для удобства:
Правила, касающиеся модификаторов аннотаций для объявления параметра типа, указаны в §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);
}
}
Вложенный класс — это вложенный класс, который не явным или неявным образом static.
Вложенный класс может быть одним из следующих:
Следующие вложенные классы неявным образом static, поэтому не являются вложенными классами:
Все правила, которые применяются к вложенным классам, применяются и к вложенным классам. В частности, вложенный класс может объявлять и наследовать 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).
Конструкт (выражение, объявление локальной переменной, объявление локального класса, объявление локального интерфейса или выражение) располагается в статическом контексте, если самый внутренний:
-
объявление метода,
-
объявление поля,
-
объявление конструктора,
-
инициализатор экземпляра,
-
статический инициализатор или
-
выражение явного вызова конструктора
который включает конструкт, является одним из следующих:
Обратите внимание, что конструкт, который появляется в объявлении конструктора или инициализаторе экземпляра, не находится в статическом контексте.
Цель статического контекста — разграничить код, который не должен явно или неявно ссылаться на текущий экземпляр класса, объявление которого лексически включает статический контекст. Следовательно, код, который находится в статическом контексте, ограничен следующими способами:
-
выражения
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-ой лексически окружающий экземпляр).
Необязательная extends фраза в объявлении обычного класса указывает тип непосредственного надкласса объявляемого класса.
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>. -
Для класса-записи R, тип непосредственного надкласса —
Record. -
Для анонимного класса, тип непосредственного надкласса определяется в §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 зависит от самого себя.
Необязательная implements строка в объявлении класса указывает прямые типы суперинтерфейсов объявляемого класса.
implements Список типов интерфейсов Каждый Тип интерфейса должен указывать доступный интерфейс (§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).
Необязательная permits фраза в объявлении обычного класса определяет все классы, предназначенные в качестве прямых подклассов объявляемого класса (§8.1.1.2).
Если объявление класса содержит 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 {}
Если sealed класс C связан с именованным модулем (§7.3), то каждый класс, указанный в permits фразе объявления C, должен быть связан с тем же модулем, что и C, в противном случае возникает ошибка компиляции.
Если sealed класс C связан с безымянным модулем (§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.3), методов (§8.4), классов и интерфейсов (§8.5).
Тело класса также может содержать инициализаторы экземпляров (§8.6), статические инициализаторы (§8.7) и объявления конструкторов (§8.8) для класса.
Область действия и перекрытие объявления члена m, объявленного или унаследованного классом C, указаны в §6.3 и §6.4.1.
Если C является вложенным классом, могут быть определения того же типа (переменной, метода или типа) и имени, что и m во внешних областях действия. (Области действия могут быть блоками, классами или пакетами.) Во всех таких случаях член m, объявленный или унаследованный классом C, перекрывает другие определения того же типа и имени.
Члены класса включают следующее:
Члены класса, объявленные private, не наследуются подклассами этого класса.
Только члены класса, объявленные protected или public, наследуются подклассами, объявленными в пакете, отличном от пакета, в котором объявлен класс.
Конструкторы, статические инициализаторы и инициализаторы экземпляров не являются членами и, следовательно, не наследуются.
Под типом члена понимается:
-
Для поля — его тип.
-
Для метода — упорядоченная 4-ка (известная как тип метода), состоящая из:
-
Параметры типа: объявления любых параметров типа члена метода (§8.4.4).
-
Типы параметров: список типов формальных параметров члена метода (§8.4.1).
-
Тип возвращаемого значения: тип возвращаемого значения члена метода (§8.4.5).
-
Клауза
throws: типы исключений, объявленные в клаузеthrowsчлена метода (§8.4.6).
-
Поля, методы, вложенные классы и вложенные интерфейсы класса могут иметь одинаковые имена, поскольку используются в разных контекстах и различаются различными процедурами поиска (§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.
Переменные класса вводятся с помощью объявлений полей.
Ниже приведена продукция из §4.3 для удобства:
Каждый декларатор в FieldDeclaration объявляет одно поле. Идентификатор в деклараторе может быть использован в имени для ссылки на поле.
В одном FieldDeclaration можно объявить более одного поля, используя несколько деклараторов; модификаторы полей и UnannType применяются ко всем деклараторам в объявлении.
Раздел FieldModifier описан в §8.3.1.
Объявленный тип поля обозначается UnannType, если в UnannType и VariableDeclaratorId не появляются скобки, а в противном случае определяется в §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.
Правила, касающиеся модификаторов аннотаций для объявления поля, указаны в §9.7.4 и §9.7.5.
Если один и тот же ключевой слова используется более одного раза в качестве модификатора для объявления поля, или если объявление поля имеет более одного из модификаторов доступа public, protected и private (§6.6), то это ошибка времени компиляции.
Если в объявлении поля присутствует два или более (различных) модификатора полей, то обычно (но не обязательно) их порядок соответствует порядку, показанному выше в производстве для FieldModifier.
Если поле объявлено статическим, то существует ровно одна его инстанция, независимо от того, сколько экземпляров (возможно, ноль) класса в конечном итоге будет создано. Статическое поле, иногда называемое переменной класса, инициализируется при инициализации класса (§12.4).
Поле, которое не объявлено статическим, называется переменной экземпляра, а иногда и нестатическим полем. При создании нового экземпляра класса (§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
Поле может быть объявлено final (§4.12.4). Как статические, так и переменные экземпляра (статические и нестатические поля) могут быть объявлены final.
Пустая статическая переменная final должна быть однозначно присвоена статической инициализацией класса, в котором она объявлена, иначе произойдёт ошибка компиляции (§8.7, §16.8).
Пустая переменная экземпляра final должна быть однозначно присвоена и при этом не должна быть однозначно неприсвоенной в конце каждого конструктора класса, в котором она объявлена, иначе произойдёт ошибка компиляции (§8.8, §16.9).
Переменные могут быть помечены transient, чтобы указать, что они не являются частью постоянного состояния объекта.
Пример 8.3.1.3-1. Сохранение полей transient
Если экземпляр класса Point:
class Point {
int x, y;
transient float rho, theta;
}
был сохранён в постоянное хранилище службой системы, то будут сохранены только поля x и y. Данное спецификация не описывает детали таких служб; см. спецификацию java.io.Serializable для примера такой службы.
Язык программирования Java позволяет потокам доступа к общим переменным (§17.1). Как правило, для обеспечения согласованного и надёжного обновления общих переменных поток должен гарантировать исключительное использование таких переменных, получив блокировку, которая, как правило, обеспечивает взаимное исключение для этих общих переменных.
Язык программирования Java предоставляет второй механизм, поля volatile, который в некоторых случаях более удобен, чем блокировки.
Поле может быть объявлено volatile, в этом случае Модель памяти Java гарантирует, что все потоки увидят согласованное значение переменной (§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 для более подробного обсуждения и примеров.
Если в объявлении поля декларатор имеет инициализатор переменной, то декларатор имеет семантику присваивания (§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.
Ссылки на поле иногда ограничены, даже если поле находится в области видимости. Следующие правила ограничивают перекрёстные ссылки на поле (где текст использования предшествует объявлению поля), а также самоссылку (где поле используется в своём собственном инициализаторе).
Для ссылки по простому имени на переменную класса f, объявленную в классе или интерфейсе C, это ошибка времени компиляции, если:
-
Ссылка появляется либо в инициализаторе переменной класса C, либо в статическом инициализаторе C (§8.7); и
-
Ссылка появляется либо в инициализаторе объявляемого поля
f, либо в позиции слева от объявляемого поляf; и -
Ссылка не находится в левой части оператора присваивания (§15.26); и
-
Внутренний класс или интерфейс, охватывающий ссылку, это C.
Для ссылки по простому имени на переменную экземпляра f, объявленную в классе C, это ошибка времени компиляции, если:
-
Ссылка появляется либо в инициализаторе переменной экземпляра C, либо в инициализаторе экземпляра C (§8.6); и
-
Ссылка появляется в инициализаторе объявляемого поля
fили в позиции слева от объявляемого поляf; и -
Ссылка не находится в левой части оператора присваивания (§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;
}
Метод объявляет исполняемый код, который можно вызвать, передавая фиксированное число значений в качестве аргументов.
Ниже приводится производящее правило из §4.3 для удобства:
Блок СписокФормальныхПараметров описан в §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. Очень настоятельно рекомендуется не использовать этот синтаксис в новом коде.
Формальные параметры метода или конструктора, если таковые имеются, указываются в виде списка спецификаторов параметров, разделённых запятыми. Каждый спецификатор параметра состоит из типа (при необходимости с модификатором final и/или одним или несколькими аннотациями) и идентификатора (возможно, в скобках), который указывает имя параметра.
Если у метода или конструктора нет формальных параметров и нет параметра получателя, то в объявлении метода или конструктора появляется пустая пара круглых скобок.
Следующие правила из §8.3 и §4.3 показаны здесь для удобства:
Формальный параметр метода или конструктора может быть параметром переменной арности, указанным многоточием после типа. Максимально один параметр переменной арности разрешён для метода или конструктора. Ошибка компиляции, если параметр переменной арности появляется где-либо в списке спецификаторов параметров, кроме последней позиции.
В грамматике для VariableArityParameter обратите внимание, что многоточие (...) является отдельным токеном (§3.11). В нём можно разместить пробелы между ним и типом, но это не рекомендуется со стилистической точки зрения.
Если последний формальный параметр метода является параметром переменной арности, метод является методом переменной арности. В противном случае это метод фиксированной арности.
Правила, касающиеся модификаторов аннотаций для объявления формального параметра и для параметра получателя, указаны в §9.7.4 и §9.7.5.
Ошибка компиляции, если final появляется более одного раза как модификатор для объявления формального параметра.
Область действия и перекрытие формального параметра указаны в §6.3 и §6.4.
Ссылки на формальный параметр из вложенного класса или интерфейса или лямбда-выражения ограничены, как указано в §6.5.6.1.
Ошибка компиляции, если метод или конструктор объявляют два формальных параметра с одинаковым именем. (То есть, в их объявлениях упоминается один и тот же Идентификатор.)
Ошибка компиляции, если формальный параметр, объявленный final, присваивается в теле метода или конструктора.
Объявленный тип формального параметра зависит от того, является ли он параметром переменной арности:
-
Если формальный параметр не является параметром переменной арности, то объявленный тип обозначается ТипБезАннотаций, если в ТипБезАннотаций и ИдентификаторДекларатораПеременной не присутствуют скобки, и указан в §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, как любой другой тип; но что имя параметра получателя в конструкторе внутреннего класса должно использовать простое имя окружающего класса.
Два метода или конструктора, 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. Вместо этого код был бы незаконным. Это существенно затруднило бы использование обобщений, поскольку авторы библиотек избегали бы миграции существующего кода.
Правила, касающиеся модификаторов аннотаций для объявления метода, указаны в §9.7.4 и §9.7.5.
Ошибка времени компиляции, если одно и то же ключевое слово используется более одного раза в качестве модификатора для объявления метода, или если объявление метода содержит более одного из модификаторов доступа public, protected и private (§6.6).
Ошибка времени компиляции, если в объявлении метода присутствует ключевое слово abstract, а также любое из ключевых слов private, static, final, native, strictfp или synchronized.
Ошибка времени компиляции, если в объявлении метода присутствует ключевое слово native, а также strictfp.
Если в объявлении метода присутствуют два или более (различных) модификатора метода, то рекомендуется (но не обязательно), чтобы они располагались в порядке, согласованном с представленным выше в продукции для MethodModifier.
Объявление абстрактного метода вводит метод как член, задавая его сигнатуру (§8.4.2), результат (§8.4.5) и, при необходимости, предложение обработки исключений (§8.4.6), но не предоставляет реализацию (§8.4.7). Метод, который не является абстрактным, может называться конкретным методом.
Объявление абстрактного метода m должно появляться непосредственно внутри класса abstract (назовем его A), за исключением случаев, когда оно встречается внутри объявления перечисления (§8.9); в противном случае возникает ошибка времени компиляции.
Каждый подкласс A, который не является абстрактным (§8.1.1.1), должен предоставить реализацию для m, в противном случае возникает ошибка времени компиляции.
Класс, не являющийся абстрактным, может переопределять абстрактный метод, предоставляя другое объявление метода.
Это может быть место для размещения документального комментария, уточнения типа возвращаемого значения или объявления того, что набор исключений, которые могут быть выброшены этим методом, при реализации его подклассами, должен быть более ограничен.
Метод-инстанс, который не является абстрактным, может быть переопределён абстрактным методом.
Пример 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();
}
Это объявление метода toString переопределяет не-абстрактный метод toString класса Object. (Object — явный непосредственный суперкласс класса Point.) Добавление кода:
class ColoredPoint extends Point {
int color;
public String toString() {
return super.toString() + ": color " + color; // error
}
}
приведёт к ошибке компиляции, потому что обращение super.toString() относится к методу toString в классе Point, который является абстрактным и поэтому не может быть вызван. Метод 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.1.3), который ограничивает использование конструкций, ссылающихся на текущий объект. В частности, ключевые слова this и super запрещены в статическом контексте (§15.8.3, §15.11.2), как и неквалифицированные ссылки на переменные экземпляра, методы экземпляра и параметры типа лексически окружающих объявлений (§6.5.5.1, §6.5.6.1, §15.12.3).
Метод, который не объявлен как статический, называется методом экземпляра, а иногда и не-статическим методом.
Метод экземпляра всегда вызывается относительно объекта, который становится текущим объектом, к которому ключевые слова this и super относятся во время выполнения тела метода.
Ссылки на метод экземпляра из статического контекста или вложенного класса или интерфейса ограничены, как указано в §15.12.3.
Метод может быть объявлен как финальный, чтобы предотвратить его переопределение или скрытие подклассами.
Ошибка времени компиляции, если вы пытаетесь переопределить или скрыть финальный метод.
Финальный метод и все методы, объявленные непосредственно внутри финального класса (§8.1.1.2), ведут себя так, как если бы они были финальными, поскольку их нельзя переопределить.
Во время выполнения генератор или оптимизатор машинного кода может «встраивать» тело финального метода, заменяя вызов метода кодом в его теле. Процесс встраивания должен сохранять семантику вызова метода. В частности, если целевым объектом вызова метода экземпляра является финальный, то должно быть выброшено исключение, даже если метод встроен. Компилятор 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 также обновлялся.
Метод, объявленный как 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;
}
Метод 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 произошли одновременно.
Метод является обобщенным, если он объявляет одну или несколько переменных типов (§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 путем применения θ, как определено выше, к типу.
Результат объявления метода либо объявляет тип значения, которое метод возвращает (тип возвращаемого значения), либо использует ключевое слово void, чтобы указать, что метод не возвращает значение.
Если результат не void, то тип возвращаемого значения метода обозначается UnannType, если после списка формальных параметров нет пар скобок, и определяется в §10.2 в противном случае.
Типы возвращаемых значений могут различаться среди методов, которые переопределяют друг друга, если типы возвращаемых значений являются ссылочными типами. Понятие замещаемости типа возвращаемого значения поддерживает ковариантные возвращаемые значения, то есть специализацию типа возвращаемого значения до подтипа.
Объявление метода d1 с типом возвращаемого значения R1 является замещаемым по типу возвращаемого значения для другого метода d2 с типом возвращаемого значения R2, если выполняется одно из следующих условий:
-
Если R1 является
void, то R2 являетсяvoid. -
Если R1 является примитивным типом, то R2 идентичен R1.
-
Если R1 является ссылочным типом, то выполняется одно из следующих условий:
Неявное преобразование разрешено в определении, несмотря на то, что оно не является надежным, как специальное разрешение для беспрепятственной миграции из необобщенного кода в обобщенный. Если неявное преобразование используется для определения того, что R1 является замещаемым по типу возвращаемого значения для R2, то R1 обязательно не является подтипом R2, и правила переопределения (§8.4.8.3, §9.4.1) потребуют предупреждения о неявном преобразовании во время компиляции.
Блок throws используется для обозначения любых исключений, которые могут быть сгенерированы в теле метода или конструктора (§11.1.1) (§11.2.2).
Если ТипИсключения в блоке throws не является подтипом (§4.10) исключения, то это ошибка компиляции.
Переменные типов допускаются в блоке 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 */ }
}
}
Тело метода — это либо блок кода, реализующий метод, либо просто точка с запятой, указывающая на отсутствие реализации.
Тело метода должно быть точкой с запятой, если метод является 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?!"); }
}
Класс 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), и предотвращает конфликты, в которых наследуется несколько методов по умолчанию, а одна реализация явно предназначена для замещения другой.
Метод экземпляра 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.
Подпись переопределяемого метода может отличаться от переопределённого, если формальный параметр в одном из методов имеет необработанный тип, а соответствующий параметр в другом имеет параметризованный тип. Это позволяет мигрировать существующий код, чтобы воспользоваться преимуществами дженериков.
Понятие переопределения включает методы, которые переопределяют другой из некоторого подкласса объявляющего класса. Это может происходить двумя способами:
-
Конкретный метод в дженерическом суперклассе может, при определённых параметризациях, иметь такую же подпись, как и абстрактный метод в этом классе. В этом случае конкретный метод наследуется, а абстрактный метод - нет (как описано выше). Тогда наследованный метод следует считать переопределяющим своего абстрактного коллегу из 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.IOException;
import java.io.OutputStream;
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.
Если класс C объявляет или наследует метод m, то данный метод m скрывает любой метод m', объявленный в классе или интерфейсе 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, чтобы определить во время выполнения, какой метод экземпляра вызвать.
Если объявление метода 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.2).
Если класс C наследует конкретный метод, чья сигнатура эквивалентна сигнатуре другого метода, унаследованного классом C, то это ошибка времени компиляции.
Если класс C наследует метод по умолчанию, чья сигнатура эквивалентна сигнатуре другого метода, унаследованного классом C, то это ошибка времени компиляции, за исключением случаев, когда существует метод abstract, объявленный в суперклассе C и унаследованный классом C, эквивалентный по сигнатуре этим двум методам.
Это исключение из строгих правил конфликтов методов по умолчанию и методов по умолчанию-по-умолчанию возникает, когда метод abstract объявлен в суперклассе: утверждение об абстрактности, идущее из иерархии суперклассов, по существу превалирует над методом по умолчанию, заставляя метод по умолчанию действовать так, как если бы он был abstract. Однако метод abstract из класса не переопределяет метод(ы) по умолчанию, потому что интерфейсам всё ещё разрешено уточнять сигнатуру метода abstract, пришедшего из иерархии классов.
Обратите внимание, что это исключение не применяется, если все эквивалентные по сигнатуре методы abstract, унаследованные классом C, были объявлены в интерфейсах.
В противном случае набор эквивалентных по сигнатуре методов состоит как минимум из одного метода abstract и нуля или более методов по умолчанию; тогда класс обязательно является классом abstract и считается, что наследует все эти методы.
Один из унаследованных методов должен быть замещаемым по типу возвращаемого значения для каждого другого унаследованного метода; в противном случае произойдёт ошибка времени компиляции. (throws клаузы в этом случае не вызывают ошибок.)
Может быть несколько путей, по которым одно и то же объявление метода унаследовано из интерфейса. Этот факт не создаёт трудностей и никогда сам по себе не приводит к ошибке времени компиляции.
Пример 8.4.8.4-1. Наследование методов с эквивалентными сигнатурами переопределения
Первая ошибка времени компиляции выше, касающаяся класса C, который наследует конкретный метод, может произойти, если суперкласс C является дженерическим, и суперкласс имеет два метода, которые были различными в дженерическом объявлении, но имеют одинаковую сигнатуру в параметризации (§4.5), используемой классом C. Например:
class A<T> {
void m(String s) {} // 1
void m(T t) {} // 2
}
class C extends A<String> {}
Класс C наследует два метода от своего непосредственного суперкласса типа A<String>: метод m(String), отмеченный в 1, и (из-за параметризации C класса A) метод m(String), отмеченный в 2. Эти методы имеют одинаковую сигнатуру, поэтому эквивалентны по сигнатуре переопределения.
Если у класса есть два метода (оба объявлены в одном классе, оба унаследованы классом или один объявлен, а другой унаследован), имеющие одинаковое имя, но разные сигнатуры, не являющиеся эквивалентными для переопределения, то имя метода называется перегруженным.
Этот факт не вызывает трудностей и никогда сам по себе не приводит к ошибке на этапе компиляции. Нет обязательной связи между типами возвращаемых значений или между блоками 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 (и любых подклассов, которые могут быть у него). При обращении к полю x с помощью ссылки на экземпляр класса RealPoint в переменной типа Point, обращается к целочисленному полю 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.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 вложенные классы и интерфейсы суперкласса и суперинтерфейсов, которые доступны коду в классе и не скрыты объявлением в классе.
Возможна ситуация, когда класс наследует более одного вложенного класса или интерфейса с одинаковым именем, либо от суперкласса и суперинтерфейсов, либо только от суперинтерфейсов. Такая ситуация сама по себе не вызывает ошибки компиляции. Однако любая попытка в теле класса обратиться к такому вложенному классу или интерфейсу по его простому имени приведет к ошибке компиляции, так как ссылка является неоднозначной.
Может быть несколько путей, по которым одно и то же объявление вложенного класса или интерфейса наследуется из интерфейса. В такой ситуации вложенный класс или интерфейс считается унаследованным только один раз, и к нему можно обратиться по его простому имени без неоднозначности.
Инициализатор экземпляра, объявленный в классе, выполняется при создании экземпляра класса (§12.5, §15.9, §8.8.7.1).
Ошибка компиляции, если инициализатор экземпляра не может завершиться нормально (§14.22).
Ошибка компиляции, если оператор return (§14.17) появляется где-либо в инициализаторе экземпляра.
Инициализатор экземпляра может ссылаться на текущий объект с помощью ключевого слова this (§15.8.3) или ключевого слова super (§15.11.2, §15.12), а также использовать любые имеющиеся в рамках видимости типы переменных.
Ограничения на то, как инициализатор экземпляра может ссылаться на переменные экземпляра, даже когда переменные экземпляра находятся в области видимости, указаны в §8.3.3.
Проверка исключений для инициализатора экземпляра указана в §11.2.3.
Статический инициализатор, объявленный в классе, выполняется при инициализации класса (§12.4.2). Вместе с инициализаторами полей для переменных класса (§8.3.2), статические инициализаторы могут использоваться для инициализации переменных класса.
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.
Конструктор используется при создании объекта, являющегося экземпляром класса (§12.5, §15.9).
Правила в этой секции применяются к конструкторам во всех объявлениях классов, включая объявления перечислений и записей. Однако для объявлений перечислений действуют особые правила по отношению к модификаторам конструкторов, телам конструкторов и конструкторам по умолчанию; эти правила изложены в §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.4.1).
Если последним формальным параметром конструктора является параметр переменной арности, то конструктор является конструктором переменной арности. В противном случае это конструктор фиксированной арности.
Конструктор не-private внутреннего члена класса неявно объявляет в качестве первого формального параметра переменную, представляющую немедленно окружающий экземпляр класса (§15.9.2, §15.9.3).
Логика, объясняющая, почему только такой тип класса имеет неявно объявленный параметр конструктора, довольно тонкая. Следующее объяснение может быть полезным:
-
В выражении создания экземпляра класса для не-
privateвнутреннего члена класса §15.9.2 указывает на немедленно окружающий экземпляр внутреннего класса. Внутренний класс мог быть сгенерирован компилятором, отличным от компилятора выражения создания экземпляра. Поэтому должен быть стандартный способ, которым компилятор выражения создания экземпляра передаст ссылку (представляющую немедленно окружающий экземпляр) в конструктор внутреннего класса. Следовательно, язык программирования Java определяет в этом разделе, что конструктор не-privateвнутреннего члена класса неявно объявляет начальный параметр для немедленно окружающего экземпляра. §15.9.3 указывает, что экземпляр передаётся в конструктор. -
В выражении создания экземпляра класса для внутреннего локального класса или анонимного класса (не в статическом контексте), §15.9.2 указывает на немедленно окружающий экземпляр локального/анонимного класса. Локальный/анонимный класс обязательно генерируется тем же компилятором, что и выражение создания экземпляра. Этот компилятор может представлять немедленно окружающий экземпляр как пожелает. Язык программирования Java не нуждается в том, чтобы неявно объявлять параметр в конструкторе локального/анонимного класса.
-
В выражении создания экземпляра класса для анонимного класса, и когда суперкласс анонимного класса является внутренним классом (не в статическом контексте), §15.9.2 указывает на немедленно окружающий экземпляр анонимного класса относительно суперкласса. Этот экземпляр должен передаваться от анонимного класса к его суперклассу, где он будет служить немедленно окружающим экземпляром. Поскольку суперкласс мог быть сгенерирован компилятором, отличным от компилятора выражения создания экземпляра, необходимо передавать экземпляр стандартным способом, передавая его в качестве первого аргумента в конструктор суперкласса. Обратите внимание, что сам анонимный класс обязательно генерируется тем же компилятором, что и выражение создания экземпляра, поэтому компилятор мог передать немедленно окружающий экземпляр относительно суперкласса в анонимный класс как пожелает, прежде чем анонимный класс передаст экземпляр в конструктор суперкласса. Однако для согласованности, язык программирования Java считает в §15.9.5.1, что в некоторых случаях конструктор анонимного класса неявно объявляет начальный параметр для немедленно окружающего экземпляра относительно суперкласса.
Тот факт, что не-private внутренний член класса может быть доступен другому компилятору, чем тот, который его скомпилировал, в то время как внутренний локальный класс или анонимный класс всегда доступны тому же компилятору, который его скомпилировал, объясняет, почему бинарное имя не-private внутреннего члена класса определяется как предсказуемое, но бинарное имя внутреннего локального класса или анонимного класса нет (§13.1).
Правила, касающиеся модификаторов аннотаций для объявления конструктора, указаны в §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.
Конструктор является обобщённым, если он объявляет одну или более переменных типа (§4.4).
Эти переменные типа известны как параметры типа конструктора. Форма раздела параметров типа обобщённого конструктора идентична разделу параметров типа обобщённого класса (§8.1.2).
Возможна ситуация, когда конструктор обобщённый независимо от того, является ли класс, в котором он объявлен, обобщённым.
Объявление обобщённого конструктора определяет набор конструкторов, по одному для каждого возможного вызова раздела параметров типа аргументами типа. Аргументы типа могут не нуждаться в явном указании при вызове обобщённого конструктора, так как они часто могут быть выведены (§18 (Вывод типов)).
Область действия и перекрытие параметров типа конструктора указаны в §6.3 и §6.4.1.
Ссылки на параметр типа конструктора из явного оператора вызова конструктора или вложенного класса или интерфейса ограничены, как указано в §6.5.5.1.
Раздел throws для конструктора идентичен по структуре и поведению разделу throws для метода (§8.4.6).
Первое утверждение тела конструктора может быть явным вызовом другого конструктора того же класса или непосредственного суперкласса (§8.8.7.1).
Для конструктора является ошибкой компиляции непосредственный или косвенный вызов самого себя через серию одного или нескольких явных вызовов конструкторов, включающих 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, передавая координаты.
this ( [ArgumentList] ) ; [TypeArguments]
super ( [ArgumentList] ) ; ExpressionName
. [TypeArguments] super ( [ArgumentList] ) ; Primary
. [TypeArguments] super ( [ArgumentList] ) ; Для удобства приведены следующие производные из §4.5.1 и §15.12:
Явные вызовы конструкторов делятся на два типа:
-
Альтернативные вызовы конструкторов начинаются со слова
this(возможно, с предваряющими явными типами аргументов). Они используются для вызова альтернативного конструктора того же класса. -
Вызовы конструкторов суперкласса начинаются со слова
super(возможно, с предваряющими явными типами аргументов) или выражения Primary или ExpressionName. Они используются для вызова конструктора непосредственного суперкласса. Они далее подразделяются:-
Безусловные вызовы конструкторов суперкласса начинаются со слова
super(возможно, с предваряющими явными типами аргументов). -
Условные вызовы конструкторов суперкласса начинаются с выражения Primary или ExpressionName. Они позволяют конструктору подкласса явно указать недавно созданный объект с непосредственным отношением к непосредственному суперклассу (§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будет выражением Primary или ExpressionName непосредственно перед ".super", и пусть O будет непосредственно окружающим классом S. Возникает ошибка компиляции, если типpне O или подкласс O, или если типpнедоступен (§6.6).
Типы исключений, которые может вызвать утверждение явного вызова конструктора, указаны в §11.2.2.
Вычисление утверждения вызова альтернативного конструктора происходит путём сначала вычисления аргументов конструктора слева направо, как и в обычном вызове метода, а затем вызова конструктора.
Вычисление утверждения вызова конструктора суперкласса происходит следующим образом:
-
Пусть
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.
-
-
После определения ближайшего внешнего экземпляра
iотносительно S (если таковой имеется), вычисление вызова конструктора суперкласса продолжается вычислением аргументов конструктора слева направо, как в обычном вызове метода; затем вызывается конструктор. -
Наконец, если вызов конструктора суперкласса завершился нормально, то выполняются все инициализаторы переменных экземпляра 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, даже если такие ссылки не являются алиасами в обычном смысле.
Если класс не содержит объявлений конструкторов, то конструктор по умолчанию неявно объявляется. Форма конструктора по умолчанию для класса верхнего уровня, внутреннего класса или локального класса следующая:
-
Конструктор по умолчанию имеет тот же модификатор доступа, что и класс, если класс не имеет модификатора доступа, конструктор по умолчанию имеет доступ к пакету (§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 доступен, его конструктор по умолчанию недоступен.
Класс может быть спроектирован для предотвращения создания экземпляров класса кодом за пределами объявления класса, объявив хотя бы один конструктор, для предотвращения создания конструктора по умолчанию, и объявив все конструкторы 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.
Объявление перечисления задаёт новый класс перечисления, ограниченный вид класса, который определяет небольшой набор именованных экземпляров класса.
Объявление перечисления может указывать верхнеуровневый класс перечисления (§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).
Помимо ошибки компиляции, три дополнительных механизма гарантируют, что экземпляры класса перечисления существуют только в виде констант перечисления:
-
Метод
finalcloneвEnumгарантирует, что константы перечисления нельзя клонировать. -
Рефлексивное создание экземпляров классов перечислений запрещено.
-
Специальная обработка механизмом сериализации гарантирует, что дубликаты экземпляров не создаются в результате десериализации.
Тело объявления перечисления может содержать константы перечисления. Константа перечисления определяет экземпляр класса перечисления.
Следующая продукция из §15.12 приведена здесь для удобства:
Правила, касающиеся модификаторов аннотаций для объявления константы перечисления, описаны в §9.7.4 и §9.7.5.
Идентификатор в константе перечисления предоставляет имя неявного поля класса перечисления (§8.9.3), которое можно использовать для ссылки на константу перечисления.
Константу перечисления можно дополнить аргументами, которые передаются конструктору перечисления при создании константы во время инициализации класса, как описано позже в этом разделе. Вызываемый конструктор выбирается по обычным правилам разрешения перегрузки (§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.1.7 показаны здесь для удобства:
Любые объявления конструкторов или членов в теле объявления перечисления применяются к классу перечисления точно так же, как если бы они были присутствовали в теле обычного объявления класса, если не указано иное.
Если объявление конструктора в объявлении перечисления является 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.HashMap;
import java.util.Map;
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.HashMap;
import java.util.Map;
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);
}
}
Переработанная версия очевидно правильна, так как статическая инициализация выполняется сверху вниз.
Членами класса перечисления E являются все следующие:
-
Члены, объявленные в теле объявления E.
-
Члены, унаследованные от
Enum<E>. -
Для каждой константы перечисления
c, объявленной в теле объявления E, E имеет неявно объявленное полеpublicstaticfinalтипа E, имеющее то же имя, что иc. Поле имеет инициализатор переменной, который создает E и передает любые аргументыcв выбранный конструктор для E. Поле имеет те же аннотации, что иc(если таковые имеются).Эти поля неявно объявляются в том же порядке, что и соответствующие константы перечисления, перед любыми
staticполями, явно объявленными в теле объявления E.Константа перечисления считается созданной при инициализации соответствующего неявно объявленного поля.
-
Неявно объявленный метод
publicstaticE[]values(), который возвращает массив, содержащий константы перечисления E в том же порядке, в котором они появляются в теле объявления E. -
Неявно объявленный метод
publicstaticEvalueOf(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.ArrayList;
import java.util.List;
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.ArrayList;
import java.util.Collections;
import java.util.List;
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]
Объявление записи указывает новый класс записи, ограниченный тип класса, который определяет простую агрегацию значений.
Объявление записи может указывать класс записи верхнего уровня (§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).
Компоненты записи класса записи, если таковые имеются, указываются в заголовке объявления записи. Каждый компонент записи состоит из типа (при необходимости с префиксом одной или нескольких аннотаций) и идентификатора, который определяет имя компонента записи. Компонент записи соответствует двум членам класса записи: неявной private переменной и public методу доступа, объявленному явно или неявно (§8.10.3).
Если у класса записи нет компонентов записи, то в заголовке объявления записи появляется пустая пара скобок.
Компонент записи может быть компонентом записи переменной арности, указанный многоточием после типа. Для класса записи допускается не более одного компонента переменной арности. Если компонент переменной арности встречается в списке компонентов записи не на последнем месте, это ошибка компиляции.
Правила, касающиеся модификаторов аннотаций для компонента записи, указаны в §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.1.7 приведены здесь для удобства:
Оператор CompactConstructorDeclaration описан в §8.10.4.2.
Ошибка компиляции, если тело объявления записи содержит объявление поля, которое не является static (§8.3.1.1).
Ошибка компиляции, если тело объявления записи содержит объявление метода, которое является abstract или native (§8.4.3.1, §8.4.3.4).
Ошибка компиляции, если тело объявления записи содержит инициализатор объекта (§8.6).
Для каждого компонента записи класс записи имеет поле с тем же именем, что и компонент записи, и тем же типом, что и объявленный тип компонента записи. Это поле, которое объявляется неявно, известно как поле компонента.
Поле компонента является private, final и не-static.
Поле компонента аннотируется, если есть, аннотациями, которые появляются на соответствующем компоненте записи и чьи интерфейсы аннотаций применимы в контексте объявления поля или контекста типа, или обоих (§9.7.4).
Кроме того, для каждого компонента записи класс записи имеет метод с тем же именем, что и компонент записи, и пустым списком формальных параметров. Этот метод, который объявляется явно или неявно, известен как метод-аксессор.
Если метод-аксессор для компонента записи объявлен явно, то все перечисленное ниже должно быть истинно, иначе произойдёт ошибка компиляции:
Если у класса записи есть компонент записи, для которого метод-аксессор не объявлен явно, то метод-аксессор для этого компонента объявляется неявно со следующими свойствами:
-
Его имя совпадает с именем компонента записи.
-
Его тип возвращаемого значения совпадает с объявленным типом компонента записи.
-
Он не является дженерическим.
-
Он является обычным методом-инстансом без формальных параметров и без
throws-блока. -
Он аннотирован, если есть, аннотациями, которые появляются на соответствующем компоненте записи и чьи интерфейсы аннотаций применимы в контексте объявления метода или контекста типа, или обоих (§9.7.4).
-
Его тело возвращает значение соответствующего поля компонента.
Ограничения на имена компонентов записи (§8.10.1) означают, что ни один неявно объявленный метод-аксессор не имеет сигнатуры, которая эквивалентна сигнатуре переопределяемого не-private метода класса Object. Явное объявление метода, которое использует одно из ограниченных имён, таких как public void
wait() {...}, не является методом-аксессором, так как wait никогда не является именем компонента записи.
Аннотации, которые появляются на компоненте записи, не распространяются на явно объявленный метод-аксессор для этого компонента записи. В некоторых ситуациях программисту может потребоваться дублировать аннотации компонента записи на явно объявленном методе-аксессоре, но это обычно не требуется.
Аннотации, которые распространяются на неявно объявленный метод-аксессор, должны приводить к законно аннотированному методу. Например, в следующем объявлении записи неявно объявленный метод-аксессор x() был бы аннотирован с помощью @SafeVarargs, но такая аннотация является незаконной для метода с фиксированной арностью (§9.6.4.7):
record BadRecord(@SafeVarargs int x) {} // Error
Область действия и перекрытие поля компонента и метода-аксессора определены в §6.3 и §6.4.1. (Компонент записи, которому они соответствуют, не является объявлением, поэтому у него нет своей области действия.)
Классы записей могут явно объявлять методы-инстансы, отличные от методов-аксессоров, но не могут явно объявлять переменные-инстансы (§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, равенство определяется так, как если бы вызывался методstaticcompareкласса-обертки, соответствующего типу 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.8.9). Вместо этого, класс записи имеет канонический конструктор, объявленный явно или неявно, который инициализирует все поля компонентов класса записи.
Существует два способа явного объявления канонического конструктора в объявлении записи: объявление обычного конструктора с подходящей сигнатурой (§8.10.4.1) или объявление компактного конструктора (§8.10.4.2).
Учитывая сигнатуру обычного конструктора, который подходит как канонический, и сигнатуру, полученную для компактного конструктора, правила сигнатур конструкторов (§8.8.2) означают, что наличие в объявлении записи как обычного конструктора, который подходит как канонический, так и компактного конструктора является ошибкой на этапе компиляции.
В любом случае, явно объявленный канонический конструктор должен предоставлять не меньше доступа, чем класс записи, как указано ниже:
-
Если класс записи является
public, то канонический конструктор должен бытьpublic; в противном случае возникает ошибка на этапе компиляции. -
Если класс записи является
protected, то канонический конструктор должен бытьprotectedилиpublic; в противном случае возникает ошибка на этапе компиляции. -
Если класс записи имеет доступ по умолчанию (package access), то канонический конструктор не должен быть
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), в противном случае возникает ошибка на этапе компиляции.
Конструктор (не компактный) в объявлении класса записи 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.8, §8.8.3 и §8.8.7 показаны здесь для удобства:
При объявлении записи не должно быть более одного компактного объявления конструктора. Это ошибка времени компиляции.
Формальные параметры компактного конструктора класса записи объявляются неявно. Они задаются производным списком формальных параметров класса записи (§8.10.4).
Компактный конструктор класса записи является конструктором с переменным числом аргументов (§8.8.1), если класс записи имеет компонент записи с переменным числом аргументов.
Подпись компактного объявления конструктора равна производной подписи конструктора класса записи (§8.10.4.1).
Тело компактного объявления конструктора должно удовлетворять всем следующим условиям, в противном случае возникает ошибка времени компиляции:
-
Тело не должно содержать оператор явного вызова конструктора (§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 denom) {
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.