Глава 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.
Если в объявлении класса присутствуют два или более (разных) модификатора класса, то принято, хотя и не обязательно, что они располагаются в порядке, согласованном с представленным выше в произведении для ClassModifier.
abstract класс — это неполный класс или класс, который предполагается неполным.
Ошибка компиляции, если выполняется попытка создать экземпляр abstract класса с помощью выражения создания экземпляра класса (§15.9.1).
Подкласс abstract класса, который сам по себе не является abstract, может быть создан, что приводит к выполнению конструктора abstract класса и, следовательно, к выполнению инициализаторов полей для переменных экземпляра этого класса.
Обычный класс может иметь abstract методы, то есть методы, объявленные, но ещё не реализованные (§8.4.3.1), только если это abstract класс. Ошибка компиляции, если обычный класс, который не является abstract, имеет abstract метод.
Класс C имеет abstract методы, если выполняется одно из следующих условий:
-
Любой из методов члена (§8.2) класса C — объявленный или унаследованный — является
abstract. -
Любой из суперклассов C имеет
abstractметод, объявленный с доступом пакета, и не существует метода, который переопределяетabstractметод из C или из суперкласса C.
Ошибка компиляции при объявлении типа abstract класса, если невозможно создать подкласс, реализующий все его abstract методы. Эта ситуация может возникнуть, если у класса будут в качестве членов два abstract метода с одинаковой сигнатурой метода (§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, который должен быть объявлен abstract, потому что он содержит объявление abstract метода с именем alert. Подкласс Point с именем ColoredPoint наследует abstract метод alert, поэтому он также должен быть объявлен abstract. С другой стороны, подкласс Point с именем SimplePoint предоставляет реализацию alert, поэтому он не должен быть abstract.
Заявление:
Point p = new Point();
приведёт к ошибке компиляции; класс Point не может быть создан, потому что он abstract. Однако переменная типа Point может корректно быть инициализирована ссылкой на любой подкласс Point, и класс SimplePoint не является abstract, поэтому оператор:
Point p = new SimplePoint();
будет корректным. Создание экземпляра 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, удовлетворяющий спецификациям обоих abstract методов, поскольку метод в интерфейсе Colorable требует, чтобы этот метод не возвращал значения, в то время как метод в классе Colored требует, чтобы этот метод возвращал значение типа int (§8.4).
Тип класса должен быть объявлен abstract только в том случае, если предполагается, что могут быть созданы подклассы для завершения реализации. Если предполагается просто предотвратить создание экземпляра класса, правильным способом выразить это является объявление конструктора (§8.8.10) без аргументов, сделать его private, никогда не вызывать его и не объявлять других конструкторов. Класс такого вида обычно содержит методы и переменные класса.
Класс 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 показано здесь для удобства:
Каждый дескриптор в объявлении поля объявляет одно поле. Дескриптор должен содержать Идентификатор, в противном случае произойдёт ошибка компиляции. Идентификатор может использоваться для ссылок на поле.
В одном объявлении поля может быть объявлено несколько полей, используя несколько дескрипторов; МодификаторПоля и ТипБезАннотаций применяются ко всем дескрипторам в объявлении.
Описание МодификаторПоля описано в §8.3.1.
Объявленный тип поля обозначается ТипБезАннотаций, если в ТипБезАннотаций и ИдентификаторДескриптораПеременной нет пар скобок, в противном случае он определяется в §10.2.
Область видимости и перекрытие объявления поля указаны в §6.3 и §6.4.1.
Ошибка компиляции возникает, если тело объявления класса объявляет два поля с одинаковым именем.
Если класс объявляет поле с определённым именем, то объявление этого поля, как говорят, скрывает все доступные объявления полей с тем же именем в суперклассах и суперинтерфейсах класса.
В этом отношении скрытие полей отличается от скрытия методов (§8.4.8.3), поскольку нет различия между static и не-static полями при скрытии полей, в то время как различие проводится между static и не-static методами при скрытии методов.
К скрытому полю можно получить доступ, используя полное имя (§6.5.6.2), если оно static, или используя выражение доступа к полю, которое содержит ключевое слово super (§15.11.2) или приведение к типу суперкласса.
В этом отношении скрытие полей аналогично скрытию методов.
Если объявление поля скрывает объявление другого поля, то эти два поля могут иметь разные типы.
Класс наследует от своего непосредственного суперкласса и непосредственных суперинтерфейсов все не-private поля суперкласса и суперинтерфейсов, которые доступны (§6.6) коду в классе и не скрываются объявлением в классе.
private поле суперкласса может быть доступно подклассу - например, если оба класса являются членами одного класса. Тем не менее, private поле никогда не наследуется подклассом.
Возможно, что класс наследует более одного поля с одинаковым именем, либо от своего суперкласса и суперинтерфейсов, либо только от своих суперинтерфейсов. Такая ситуация сама по себе не приводит к ошибке компиляции. Однако любая попытка в теле класса обратиться к любому такому полю по его простому имени приведёт к ошибке компиляции, потому что ссылка является неоднозначной.
Может быть несколько путей, по которым одно и то же объявление поля наследуется из интерфейса. В такой ситуации поле рассматривается как наследованное только один раз, и к нему можно обращаться по его простому имени без неоднозначности.
Пример 8.3-1. Поля, унаследованные от нескольких классов
Класс может унаследовать два или более поля с одинаковым именем, либо от своего суперкласса и суперинтерфейса, либо от двух суперинтерфейсов. Любая попытка обратиться к такому неоднозначно унаследованному полю по его простому имени приводит к ошибке компиляции. Для однозначного доступа к таким полям можно использовать полное имя или выражение доступа к полю, содержащее ключевое слово super (§15.11.2). В программе:
interface Frob { float v = 2.0f; }
class SuperTest { int v = 3; }
class Test extends SuperTest implements Frob {
public static void main(String[] args) {
new Test().printV();
}
void printV() { System.out.println(v); }
}
класс Test наследует два поля с именем v, одно от своего суперкласса SuperTest и одно от своего суперинтерфейса Frob. Само по себе это разрешено, но возникает ошибка компиляции из-за использования простого имени v в методе printV: невозможно определить, какое поле v подразумевается.
В следующем варианте используется выражение доступа к полю super.v для ссылки на поле с именем v, объявленное в классе SuperTest, и полное имя Frob.v для ссылки на поле с именем v, объявленное в интерфейсе Frob:
interface Frob { float v = 2.0f; }
class SuperTest { int v = 3; }
class Test extends SuperTest implements Frob {
public static void main(String[] args) {
new Test().printV();
}
void printV() {
System.out.println((super.v + Frob.v)/2);
}
}
Он компилируется и выводит:
2.5
Даже если два разных унаследованных поля имеют одинаковый тип, одинаковое значение и оба final, любая ссылка на любое из этих полей по простому имени считается неоднозначной и приводит к ошибке компиляции. В программе:
interface Color { int RED=0, GREEN=1, BLUE=2; }
interface TrafficLight { int RED=0, YELLOW=1, GREEN=2; }
class Test implements Color, TrafficLight {
public static void main(String[] args) {
System.out.println(GREEN); // compile-time error
System.out.println(RED); // compile-time error
}
}
не удивительно, что ссылка на GREEN считается неоднозначной, поскольку класс Test наследует два разных объявления для GREEN с разными значениями. Суть этого примера заключается в том, что ссылка на RED также считается неоднозначной, поскольку унаследованы два разных объявления. Тот факт, что два поля с именем RED имеют одинаковый тип и то же неизменное значение, не влияет на это суждение.
Пример 8.3-2. Повторное наследование полей
Если одно и то же объявление поля унаследовано из интерфейса по нескольким путям, поле считается унаследованным только один раз. На него можно ссылаться по его простому имени без неоднозначности. Например, в коде:
interface Colorable {
int RED = 0xff0000, GREEN = 0x00ff00, BLUE = 0x0000ff;
}
interface Paintable extends Colorable {
int MATTE = 0, GLOSSY = 1;
}
class Point { int x, y; }
class ColoredPoint extends Point implements Colorable {}
class PaintedPoint extends ColoredPoint implements Paintable {
int p = RED;
}
поля RED, GREEN и BLUE унаследованы классом PaintedPoint как через его непосредственный суперкласс ColoredPoint, так и через его непосредственный суперинтерфейс Paintable. Простые имена RED, GREEN и BLUE тем не менее могут использоваться без неоднозначности внутри класса PaintedPoint для ссылки на поля, объявленные в интерфейсе Colorable.
Правила, касающиеся модификаторов аннотаций для объявления поля, указаны в §9.7.4 и §9.7.5.
Если одно и то же ключевое слово появляется более одного раза в качестве модификатора для объявления поля, или если объявление поля имеет более одного из модификаторов доступа public, protected и private (§6.6), возникает ошибка времени компиляции.
Если в объявлении поля присутствуют два или более (различных) модификатора поля, то, хотя это и не обязательно, принято, чтобы они располагались в порядке, соответствующем приведенному выше в произведении для FieldModifier.
Если поле объявлено static, то существует ровно одна его инстанция, независимо от того, сколько экземпляров (возможно, ноль) класса в конечном итоге будет создано. Поле, объявленное как static, иногда называемое переменной класса, инициализируется при инициализации класса (§12.4).
Поле, не объявленное как static, называется переменной экземпляра, и иногда называется не-static полем. При каждом создании нового экземпляра класса (§12.5) для каждой переменной экземпляра, объявленной в этом классе или в любом из его суперклассов, создается новая переменная, связанная с этим экземпляром.
Объявление переменной класса вводит статический контекст (§8.1.3), который ограничивает использование конструкций, ссылающихся на текущий объект. В частности, ключевые слова this и super запрещены в статическом контексте (§15.8.3, §15.11.2), как и неквалифицированные ссылки на переменные экземпляров, методы экземпляров и параметры типов лексически окружающих объявлений (§6.5.5.1, §6.5.6.1, §15.12.3).
Ссылки на переменную экземпляра из статического контекста или вложенного класса или интерфейса ограничены, как указано в §6.5.6.1.
Пример 8.3.1.1-1. Статические поля
class Point {
int x, y, useCount;
Point(int x, int y) { this.x = x; this.y = y; }
static final Point origin = new Point(0, 0);
}
class Test {
public static void main(String[] args) {
Point p = new Point(1,1);
Point q = new Point(2,2);
p.x = 3;
p.y = 3;
p.useCount++;
p.origin.useCount++;
System.out.println("(" + q.x + "," + q.y + ")");
System.out.println(q.useCount);
System.out.println(q.origin == Point.origin);
System.out.println(q.origin.useCount);
}
}
Эта программа выводит:
(2,2) 0 true 1
показывающая, что изменение полей x, y и useCount класса p не влияет на поля класса q, поскольку эти поля являются переменными экземпляров в разных объектах. В этом примере переменная класса origin класса Point ссылается как с использованием имени класса в качестве квалификатора в Point.origin, так и с использованием переменных типа класса в выражениях доступа к полям (§15.11), как в p.origin и q.origin. Эти два способа доступа к переменной класса origin класса обращаются к одному и тому же объекту, что подтверждается тем, что значение выражения равенства ссылок (§15.21.3):
q.origin==Point.origin
является истинным. Дополнительным подтверждением является то, что инкрементация:
p.origin.useCount++;
приводит к тому, что значение q.origin.useCount становится 1; это так, потому что p.origin и q.origin ссылаются на одну и ту же переменную.
Пример 8.3.1.1-2. Скрытие переменных класса
class Point {
static int x = 2;
}
class Test extends Point {
static double x = 4.7;
public static void main(String[] args) {
new Test().printX();
}
void printX() {
System.out.println(x + " " + super.x);
}
}
Эта программа выводит:
4.7 2
потому что объявление x в классе Test скрывает определение x в классе Point, поэтому класс Test не наследует поле x от своего суперкласса Point. Внутри объявления класса Test простое имя x ссылается на поле, объявленное внутри класса Test. Код в классе Test может ссылаться на поле x класса Point как на super.x (или, поскольку x является static, как на Point.x). Если объявление Test.x удалено:
class Point {
static int x = 2;
}
class Test extends Point {
public static void main(String[] args) {
new Test().printX();
}
void printX() {
System.out.println(x + " " + super.x);
}
}
то поле x класса Point больше не скрывается внутри класса Test; вместо этого простое имя x теперь ссылается на поле Point.x. Код в классе Test по-прежнему может ссылаться на это же поле как на super.x. Таким образом, вывод от этой изменённой программы:
2 2
Поле может быть объявлено final (§4.12.4). Как переменные класса, так и экземпляра (static и не-static поля) могут быть объявлены 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, где Идентификатор — это простое имя класса или интерфейса, являющегося непосредственным внешним объявлением типа внутреннего класса; в противном случае возникает ошибка компиляции.
Объявление метода, возвращающего массив, может поместить некоторые или все пары скобок, обозначающие тип массива, после списка формальных параметров. Этот синтаксис поддерживается для совместимости со старыми версиями языка программирования 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) значения выражений фактических аргументов инициализируют новые переменные параметров, каждый с объявленным типом, перед выполнением тела метода или конструктора. Идентификатор, который появляется в ФормальныйПараметр, может использоваться как простое имя в теле метода или конструктора для ссылки на формальный параметр.
Вызовы метода переменной арности могут содержать больше выражений фактических аргументов, чем формальных параметров. Все выражения фактических аргументов, которые не соответствуют формальным параметрам, предшествующим параметру переменной арности, будут вычислены, и результаты будут сохранены в массиве, который будет передан вызову метода (§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.
Объявление метода abstract вводит метод в качестве члена, предоставляя его сигнатуру (§8.4.2), результат (§8.4.5), и, при необходимости, оговорку throws (§8.4.6), но не предоставляет реализацию (§8.4.7). Метод, который не является abstract, может быть назван конкретным методом.
Объявление метода abstract m должно находиться непосредственно внутри класса abstract (назовём его A), если оно не встречается в объявлении перечисления (§8.9); в противном случае возникает ошибка компиляции.
Каждый подкласс A, который не является abstract (§8.1.1.1), должен предоставить реализацию для m, иначе возникает ошибка компиляции.
Класс abstract может переопределить метод abstract, предоставив другое объявление метода abstract.
Это может быть местом для размещения комментария документации, уточнения типа возвращаемого значения или объявления того, что набор проверочных исключений, которые может генерировать этот метод при реализации его подклассами, будет более ограниченным.
Метод экземпляра, который не является abstract, может быть переопределён методом abstract.
Пример 8.4.3.1-1. Переопределение абстрактного/абстрактного метода
class BufferEmpty extends Exception {
BufferEmpty() { super(); }
BufferEmpty(String s) { super(s); }
}
class BufferError extends Exception {
BufferError() { super(); }
BufferError(String s) { super(s); }
}
interface Buffer {
char get() throws BufferEmpty, BufferError;
}
abstract class InfiniteBuffer implements Buffer {
public abstract char get() throws BufferError;
}
Переопределяющее объявление метода get в классе InfiniteBuffer указывает, что метод get в любом подклассе InfiniteBuffer никогда не генерирует исключение BufferEmpty, предположительно потому, что он генерирует данные в буфере и, следовательно, никогда не исчерпывает данные.
Пример 8.4.3.1-2. Переопределение абстрактного/неабстрактного
Мы можем объявить класс abstract Point, который требует от своих подклассов реализацию toString, чтобы они были полными, инстанцируемыми классами:
abstract class Point {
int x, y;
public abstract String toString();
}
Это объявление abstract метода toString переопределяет метод toString класса Object, который не является abstract. (Object — это неявный непосредственный суперкласс класса Point.) Добавление кода:
class ColoredPoint extends Point {
int color;
public String toString() {
return super.toString() + ": color " + color; // error
}
}
приводит к ошибке компиляции, потому что вызов super.toString() ссылается на метод toString в классе Point, который является abstract, и поэтому не может быть вызван. Метод toString класса Object может быть доступен классу ColoredPoint только если класс Point явно делает его доступным через какой-либо другой метод, как в:
abstract class Point {
int x, y;
public abstract String toString();
protected String objString() { return super.toString(); }
}
class ColoredPoint extends Point {
int color;
public String toString() {
return objString() + ": color " + color; // correct
}
}
Метод, объявленный как static, называется методом класса.
Метод класса всегда вызывается без ссылки на конкретный объект. Объявление метода класса вводит статический контекст (§8.1.3), который ограничивает использование конструкций, ссылающихся на текущий объект. Заметим, что ключевые слова this и super запрещены в статическом контексте (§15.8.3, §15.11.2), как и неквалифицированные ссылки на переменные экземпляра, методы экземпляра и параметры типа лексически окружающих объявлений (§6.5.5.1, §6.5.6.1, §15.12.3).
Метод, не объявленный как static, называется методом экземпляра, а иногда и не-static методом.
Метод экземпляра всегда вызывается относительно объекта, который становится текущим объектом, к которому ключевые слова this и super ссылаются во время выполнения тела метода.
Ссылки на метод экземпляра из статического контекста или вложенного класса или интерфейса ограничены, как указано в §15.12.3.
Метод может быть объявлен final, чтобы предотвратить его переопределение или скрытие подклассами.
Попытка переопределить или скрыть метод final является ошибкой компиляции.
Метод private и все методы, объявленные непосредственно внутри класса final (§8.1.1.2), ведут себя так, как если бы они были final, так как их невозможно переопределить.
Во время выполнения генератор машинного кода или оптимизатор может "встроить" тело метода final, заменив вызов метода кодом в его теле. Процесс инлайнинга должен сохранять семантику вызова метода. В частности, если целевой объект вызова метода экземпляра является null, то должно быть выброшено исключение NullPointerException, даже если метод встроен. Компилятор Java должен гарантировать, что исключение будет выброшено в правильной точке, чтобы фактические аргументы метода были оценены в правильном порядке до вызова метода.
Рассмотрим пример:
final class Point {
int x, y;
void move(int dx, int dy) { x += dx; y += dy; }
}
class Test {
public static void main(String[] args) {
Point[] p = new Point[100];
for (int i = 0; i < p.length; i++) {
p[i] = new Point();
p[i].move(i, p.length-1-i);
}
}
}
Встраивание метода move класса Point в метод main преобразует цикл for в форму:
for (int i = 0; i < p.length; i++) {
p[i] = new Point();
Point pi = p[i];
int j = p.length-1-i;
pi.x += i;
pi.y += j;
}
Цикл может быть далее оптимизирован.
Такое инлайнинга не может быть выполнено во время компиляции, если нельзя гарантировать, что Test и Point будут всегда перекомпилированы вместе, так что каждый раз, когда Point - и, в частности, его метод move - изменяется, код для Test.main также будет обновлён.
Метод, который является 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) от Throwable, возникает ошибка компиляции.
Переменные типа допускаются в блоке throws, несмотря на то, что они не допускаются в блоке try (§14.20).
Допускается, но не требуется, указывать типы исключений без проверки (§11.1.1) в блоке throws.
Взаимосвязь между блоком throws и проверкой исключений для тела метода или конструктора описана в §11.2.3.
В сущности, для каждого исключения с проверкой, которое может возникнуть в результате выполнения тела метода или конструктора, возникает ошибка компиляции, если его тип исключения или супертип его типа исключения не указан в блоке throws в объявлении метода или конструктора.
Требование объявлять исключения с проверкой позволяет компилятору Java гарантировать, что код для обработки таких условий ошибки включён. Методы или конструкторы, которые не обрабатывают исключительные ситуации, выброшенные как исключения с проверкой в их телах, обычно вызовут ошибки компиляции, если им не хватает соответствующих типов исключений в их блоках throws. Таким образом, язык программирования Java поощряет стиль программирования, где редкие и по-настоящему исключительные условия документируются таким образом.
Взаимосвязь между блоком throws метода и блоками throws переопределённых или скрытых методов описана в §8.4.8.3.
Пример 8.4.6-1. Переменные типов как типы выброшенных исключений
import java.io.FileNotFoundException;
interface PrivilegedExceptionAction<E extends Exception> {
void run() throws E;
}
class AccessController {
public static <E extends Exception>
Object doPrivileged(PrivilegedExceptionAction<E> action) throws E {
action.run();
return "success";
}
}
class Test {
public static void main(String[] args) {
try {
AccessController.doPrivileged(
new PrivilegedExceptionAction<FileNotFoundException>() {
public void run() throws FileNotFoundException {
// ... delete a file ...
}
});
} catch (FileNotFoundException f) { /* Do something */ }
}
}
Тело метода — это либо блок кода, реализующий метод, либо просто точка с запятой, указывающая на отсутствие реализации.
Тело метода должно быть точкой с запятой, если метод является 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).
Наследование для интерфейсов определено в §9.1.3.
Класс не наследует 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 "от I2" (§9.4.1.1) метод 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.
Подпись переопределяемого метода может отличаться от переопределяемого, если формальный параметр в одном из методов имеет непараметризованный тип, а соответствующий параметр в другом — параметризованный тип. Это позволяет мигрировать существующий код, чтобы использовать возможности дженериков.
Понятие переопределения включает методы, которые переопределяют другой из некоторого подкласса их объявляющего класса. Это может происходить двумя способами:
-
Конкретный метод в родовом суперклассе может, при определенных параметризациях, иметь такую же подпись, как и метод
abstractв этом классе. В этом случае конкретный метод наследуется, а методabstractнет (как описано выше). Затем унаследованный метод должен рассматриваться как переопределяющий свой абстрактный аналог из C. (Эта ситуация усложняется доступом по умолчанию: если C находится в другом пакете, тоmAвсе равно не унаследовался и не должен считаться переопределенным.) -
Метод, унаследованный от класса, может переопределять метод суперинтерфейса. (Здесь доступ по умолчанию не является проблемой.)
К переопределённому методу можно обратиться, используя выражение вызова метода (§15.12), содержащее ключевое слово super. Квалифицированное имя или приведение к типу суперкласса неэффективны при попытке обратиться к переопределённому методу.
В этом отношении переопределение методов отличается от скрытия полей.
Наличие или отсутствие модификатора strictfp абсолютно не влияет на правила переопределения методов и реализации методов abstract. Например, разрешается, чтобы метод, не являющийся strictfp, переопределял метод strictfp, и разрешается, чтобы метод strictfp переопределял метод, не являющийся strictfp.
Пример 8.4.8.1-1. Переопределение
class Point {
int x = 0, y = 0;
void move(int dx, int dy) { x += dx; y += dy; }
}
class SlowPoint extends Point {
int xLimit, yLimit;
void move(int dx, int dy) {
super.move(limit(dx, xLimit), limit(dy, yLimit));
}
static int limit(int d, int limit) {
return d > limit ? limit : d < -limit ? -limit : d;
}
}
Здесь класс SlowPoint переопределяет объявления метода move класса Point собственным методом move, который ограничивает расстояние, на которое может сместиться точка при каждом вызове метода. При вызове метода move для экземпляра класса SlowPoint всегда будет вызываться переопределённое определение в классе SlowPoint, даже если ссылка на объект SlowPoint взята из переменной, тип которой является Point.
Пример 8.4.8.1-2. Переопределение
Переопределение упрощает подклассам расширение поведения существующего класса, как показано в этом примере:
import java.io.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 реализует очень простую буферизованную версию буфера вывода, очищая вывод, когда буфер заполнен или вызывается 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', объявленный в классе или интерфейсе 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 или его суперклассе или суперинтерфейсе, 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 (и тех, которые могут у него быть). Когда ссылка на экземпляр класса RealPoint в переменной типа Point используется для доступа к полю x, обращается к целочисленному полю x, объявленному в классе Point. Тот факт, что его значение равно нулю, указывает на то, что вызов метода p.move(1, -1) не вызвал метод move класса Point; вместо этого он вызвал переопределяющий метод move класса RealPoint.
Вторая строка вывода показывает, что доступ к полю rp.x ссылается на поле x, объявленное в классе RealPoint. Это поле имеет тип float, и эта вторая строка вывода соответственно отображает значения с плавающей точкой. Кстати, это также иллюстрирует тот факт, что имя метода show является перегруженным; типы аргументов в вызове метода определяют, какая из двух определений будет вызвана.
Последние две строки вывода показывают, что вызовы методов p.getX() и rp.getX() каждый вызывают метод getX, объявленный в классе RealPoint. Действительно, нет способа вызвать метод getX класса Point для экземпляра класса RealPoint извне тела RealPoint, независимо от типа переменной, которую мы можем использовать для хранения ссылки на объект. Таким образом, мы видим, что поля и методы ведут себя по-разному: скрытие отличается от переопределения.
Вложенный класс — это класс, объявление которого непосредственно вложено в тело другого класса или интерфейса (§8.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).
Тип конструктора состоит из его сигнатуры и типов исключений, заданных его разделом throws.
Первым оператором тела конструктора может быть явный вызов другого конструктора того же класса или непосредственного суперкласса (§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 ( [СписокАргументов] ) ; [ТипАргументов]
super ( [СписокАргументов] ) ; ИмяВыражения
. [ТипАргументов] super ( [СписокАргументов] ) ; Первичное
. [ТипАргументов] super ( [СписокАргументов] ) ; Приведены следующие правила из §4.5.1 и §15.12 для удобства:
Явные вызовы конструкторов делятся на два типа:
-
Вызовы альтернативных конструкторов начинаются со ключевого слова
this(возможно, с явными типами аргументов). Они используются для вызова альтернативного конструктора того же класса. -
Вызовы конструкторов суперкласса начинаются либо со ключевого слова
super(возможно, с явными типами аргументов), либо с выражения Первичное или ИмяВыражения. Они используются для вызова конструктора непосредственного суперкласса. Они далее подразделяются:-
Неквалифицированные вызовы конструкторов суперкласса начинаются со ключевого слова
super(возможно, с явными типами аргументов). -
Квалифицированные вызовы конструкторов суперкласса начинаются с выражения Первичное или ИмяВыражения. Они позволяют конструктору подкласса явно указать вновь созданный объект в отношении непосредственного суперкласса в контексте внешнего экземпляра (§8.1.3). Это может быть необходимо, когда суперкласс является внутренним классом.
-
Явный вызов конструктора вводит статический контекст (§8.1.3), который ограничивает использование конструкций, ссылающихся на текущий объект. Заметим, что ключевые слова this и super запрещены в статическом контексте (§15.8.3, §15.11.2), как и неквалифицированные ссылки на переменные экземпляра, методы экземпляра и параметры типа лексически окружающих объявлений (§6.5.5.1, §6.5.6.1, §15.12.3).
Если ТипАргументов присутствует слева от 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 — внутренний локальный класс, объявление которого происходит в статическом контексте, пусть
N— ближайшееstaticобъявление метода,staticобъявление поля или статический инициализатор, окружающий объявление S. ЕслиNне является ближайшимstaticобъявлением метода,staticобъявлением поля или статическим инициализатором, окружающим вызов конструктора суперкласса, то происходит ошибка компиляции.
Если вызов конструктора суперкласса квалифицирован, то:
-
Если S не является внутренним классом или если объявление S происходит в статическом контексте, то происходит ошибка компиляции.
-
В противном случае пусть
p— выражение Первичное или ИмяВыражения, непосредственно предшествующее ".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).
Если в объявлении перечисления из конструктора, инициализатора экземпляра или инициализатора переменной экземпляра ссылаются на поле класса перечисления, это ошибка компиляции, если поле не является константой (§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).
Идентификатор TypeIdentifier в объявлении записи указывает имя класса записи.
Это ошибка времени компиляции, если объявление записи имеет модификатор 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).
Кроме того, для каждого компонента записи класс записи имеет метод с тем же именем, что и компонент записи, и пустым списком формальных параметров. Этот метод, который объявляется явно или неявно, известен как метод-аксессор.
Если метод-аксессор для компонента записи объявлен явно, то должны быть истинными все следующие условия, в противном случае возникает ошибка компиляции:
Если у класса записи есть компонент записи, для которого метод-аксессор не объявлен явно, то метод-аксессор для этого компонента записи объявляется неявно со следующими свойствами:
-
Его имя совпадает с именем компонента записи.
-
Его тип возвращаемого значения совпадает с объявленным типом компонента записи.
-
Он не является дженерическим.
-
Он является
publicметодом экземпляра без формальных параметров и безthrowsпункта. -
Он аннотируется, если таковые имеются, аннотациями, которые появляются на соответствующем компоненте записи, и чьи интерфейсы аннотаций применимы в контексте объявления метода или в контекстах типов, или в обоих (§9.7.4).
-
Его тело возвращает значение соответствующего поля компонента.
Ограничения на имена компонентов записи (§8.10.1) означают, что ни один неявно объявленный метод-аксессор не имеет сигнатуры, эквивалентной по переопределению методу класса private. Явное объявление метода, которое принимает одно из ограниченных имен, такое как 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; в противном случае возникает ошибка времени компиляции. -
Если класс записи имеет доступ пакета, то канонический конструктор не должен быть
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).
Тело объявления компактного конструктора должно удовлетворять всем следующим условиям, иначе произойдет ошибка времени компиляции:
-
Тело не должно содержать оператора
return(§14.17). -
Тело не должно содержать оператора явного вызова конструктора (§8.8.7.1).
-
Тело не должно содержать присваивания полю компоненты класса записи.
-
Все остальные правила для конструктора в нормальном объявлении класса должны выполняться (§8.8), кроме требования, что поля компонентов класса записи должны быть определенно присвоены и, более того, не определенно не присвоены в конце компактного конструктора (§8.3.1.2).
Если в объявлении записи есть компонент с именем c, то простое имя c в теле компактного конструктора обозначает неявный формальный параметр с именем c, а не поле компоненты с именем c.
После завершения последнего оператора, если таковой имеется, в теле компактного конструктора (нормальное завершение (§14.1)), все поля компонентов класса записи неявно инициализируются значениями соответствующих формальных параметров. Поля компонентов инициализируются в том порядке, в котором соответствующие компоненты записи объявлены в заголовке записи.
Целью компактного объявления конструктора является то, что в теле конструктора необходимо указать только код для проверки или нормализации параметров; остальной код инициализации предоставляет компилятор. Например, следующий класс записи имеет компактный конструктор, который упрощает рациональное число:
record Rational(int num, int denom) {
private static int gcd(int a, int b) {
if (b == 0) return Math.abs(a);
else return gcd(b, a % b);
}
Rational {
int gcd = gcd(num, denom);
num /= gcd;
denom /= gcd;
}
}
Компактный конструктор Rational
{...} ведет себя так же, как и этот обычный конструктор:
Rational(int num, int 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.