Глава 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.1).
Класс верхнего уровня — это класс, который не является вложенным классом.
Вложенный класс — это любой класс, объявление которого встречается внутри тела другого класса или интерфейса.
В этой главе рассматриваются общие семантические правила для всех классов — как верхнего уровня (§7.6), так и вложенных (включая внутренние классы (§8.5, §9.5), локальные классы (§14.3) и анонимные классы (§15.9.5)). Подробности, специфичные для определенных видов классов, рассматриваются в разделах, посвященных этим конструкциям.
Именованный класс может быть объявлен abstract (§8.1.1.1) и должен быть объявлен абстрактным, если он не полностью реализован; такой класс нельзя создать экземпляр, но его можно расширить подклассами. Класс может быть объявлен final (§8.1.1.2), в этом случае он не может иметь подклассы. Если класс объявлен public, то к нему можно обратиться из других пакетов. Каждый класс, кроме Object, является расширением (то есть подклассом) одного существующего класса (§8.1.4) и может реализовывать интерфейсы (§8.1.5). Классы могут быть обобщенными (§8.1.2), то есть они могут объявлять переменные типов, значения которых могут различаться для разных экземпляров класса.
Классы могут быть снабжены аннотациями (§9.7), как и любой другой вид объявления.
Тело класса объявляет члены (поля, методы, вложенные классы и интерфейсы), инициализаторы экземпляров и статические инициализаторы и конструкторы (§8.1.6). Область (§6.3) члена (§8.2) — всё тело объявления класса, к которому принадлежит член. Объявления полей, методов, внутренних классов, внутренних интерфейсов и конструкторов могут включать модификаторы доступа (§6.6) public, protected или private. Члены класса включают как объявленные, так и унаследованные члены (§8.2). Новые поля могут скрывать поля, объявленные в суперклассе или суперинтерфейсе. Новые члены класса и интерфейса могут скрывать классы или интерфейсы, объявленные в суперклассе или суперинтерфейсе. Новые методы могут скрывать, реализовывать или переопределять методы, объявленные в суперклассе или суперинтерфейсе.
Объявления полей (§8.3) описывают переменные класса, которые инициализируются один раз, и переменные экземпляра, которые инициализируются для каждого экземпляра класса.
Поле может быть объявлено final (§8.3.1.2), в этом случае ему можно присвоить значение только один раз. Любое объявление поля может включать инициализатор.
Объявления внутренних классов (§8.5) описывают вложенные классы, которые являются членами окружающего класса.
Внутренние классы могут быть static, в этом случае они не имеют доступа к переменным экземпляров окружающего класса; или они могут быть внутренними классами (§8.1.3).
Объявления внутренних интерфейсов (§8.5) описывают вложенные интерфейсы, которые являются членами окружающего класса.
END_OF_DOCUMENT_MARKERОбъявления методов (§8.4) описывают код, который может быть вызван выражениями вызова метода (§15.12). Метод класса вызывается относительно типа класса; метод экземпляра вызывается относительно некоторого конкретного объекта, являющегося экземпляром типа класса. Метод, чьё объявление не указывает, как он реализован, должен быть объявлен abstract. Метод может быть объявлен final (§8.4.3.3), в этом случае он не может быть скрыт или переопределён. Метод может быть реализован зависимым от платформы native кодом (§8.4.3.4). synchronized метод (§8.4.3.6) автоматически блокирует объект перед выполнением своего тела и автоматически разблокирует объект при возврате, как если бы он использовал оператор synchronized ( §14.19), тем самым позволяя его действиям синхронизироваться с действиями других потоков (§17 (Потоки и блокировки)).
Имена методов могут быть перегружены (§8.4.9).
Инициализаторы экземпляров (§8.6) являются блоками исполняемого кода, которые могут быть использованы для помощи в инициализации экземпляра при его создании (§15.9).
Статические инициализаторы (§8.7) являются блоками исполняемого кода, которые могут быть использованы для помощи в инициализации класса.
Конструкторы (§8.8) аналогичны методам, но не могут быть вызваны напрямую вызовом метода; они используются для инициализации новых экземпляров класса. Как и методы, они могут быть перегружены (§8.8.8).
Объявление класса задаёт новый именованный тип ссылки.
Существует два типа объявлений классов: обычные объявления классов и объявления перечислений.
Правила в этом разделе применяются ко всем объявлениям классов, включая объявления перечислений. Однако, специальные правила применяются к объявлениям перечислений относительно модификаторов класса, вложенных классов и суперклассов; эти правила изложены в §8.9.
Идентификатор в объявлении класса задаёт имя класса.
Если у класса есть то же простое имя, что и у любого из его окружающих классов или интерфейсов, это ошибка времени компиляции.
Область действия и затемнение объявления класса указаны в §6.3 и §6.4.
В объявлении класса могут быть модификаторы класса.
Правила для модификаторов аннотаций в объявлении класса указаны в §9.7.4 и §9.7.5.
Модификатор доступа public (§6.6) относится только к классам верхнего уровня (§7.6) и вложенным классам (§8.5), а не к локальным классам (§14.3) или анонимным классам (§15.9.5).
Модификаторы доступа protected и private относятся только к вложенным классам внутри непосредственно включающего объявления класса (§8.5).
Модификатор static относится только к вложенным классам (§8.5.1), а не к классам верхнего уровня, локальным или анонимным.
Если одно и то же ключевое слово встречается более одного раза в качестве модификатора для объявления класса, это синтаксическая ошибка.
Если в объявлении класса присутствуют два или более (различных) модификатора класса, то принято (но не обязательно), что они располагаются в порядке, согласованном с представленным выше в произведении для 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, удовлетворяющий обе спецификации абстрактных методов, так как метод в интерфейсе 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 . . .
}
Класс может быть объявлен final, если его определение завершено и подклассы нежелательны или не требуются.
Если имя final класса появляется в секции extends другого объявления класса (§8.1.4), это синтаксическая ошибка; это подразумевает, что final класс не может иметь подклассы.
Если класс объявлен как final и abstract, это синтаксическая ошибка, так как реализация такого класса никогда не сможет быть завершена (§8.1.1.1).
Поскольку у final класса никогда нет подклассов, методы final класса никогда не переопределяются (§8.4.8.1).
Эффект модификатора strictfp заключается в том, что все float или double выражения внутри объявления класса (включая инициализаторы переменных, инициализаторы экземпляров, статические инициализаторы и конструкторы) являются явно FP-строгими (§15.4).
Это подразумевает, что все методы, объявленные в классе, и все вложенные типы, объявленные в классе, неявно strictfp.
Класс является обобщённым, если он объявляет одну или более переменных типа (§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.
Объявление обобщённого класса определяет набор параметризованных типов (§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 работает только с необобщёнными классами.
Ошибка времени компиляции возникает при ссылке на параметр типа обобщённого класса C в любом из следующих случаев:
Пример 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 классом (§8.5), локальным классом (§14.3) или анонимным классом (§15.9.5). Класс-член интерфейса неявно static (§9.5), поэтому никогда не считается вложенным классом.
Если вложенный класс объявляет статический инициализатор (§8.7), это ошибка времени компиляции.
Если вложенный класс объявляет член, который явно или неявно static, это ошибка времени компиляции, за исключением случая, когда этот член является константой (§4.12.4).
Вложенный класс может наследовать static члены, которые не являются константами, даже если он не может их объявлять.
Вложенный класс, который не является вложенным, может свободно объявлять static члены в соответствии с обычными правилами языка программирования Java.
Пример 8.1.3-1. Объявления вложенных классов и статических членов
class HasStatic {
static int j = 100;
}
class Outer {
class Inner extends HasStatic {
static final int x = 3; // OK: constant variable
static int y = 4; // Compile-time error: an inner class
}
static class NestedButNotInner{
static int z = 5; // OK: not an inner class
}
interface NeverInner {} // Interfaces are never inner
}
Выражение или оператор находятся в статическом контексте, если и только если ближайший метод, конструктор, инициализатор экземпляра, статический инициализатор, инициализатор поля или оператор явного вызова конструктора, окружающий выражение или оператор, является статическим методом, статическим инициализатором, инициализатором статической переменной или оператором явного вызова конструктора (§8.8.7.1).
Вложенный класс C является прямым вложенным классом класса или интерфейса O, если O является непосредственно окружающим объявлением типа C, и объявление C не происходит в статическом контексте.
Класс C является вложенным классом класса или интерфейса O, если он является либо прямым вложенным классом O, либо вложенным классом вложенного класса O.
Необычно, но возможно, что непосредственно окружающим объявлением типа вложенного класса является интерфейс. Это происходит только тогда, когда класс объявлен в теле метода по умолчанию (§9.4). В частности, это происходит, если анонимный или локальный класс объявлен в теле метода по умолчанию, или класс-член объявлен в теле анонимного класса, объявленного в теле метода по умолчанию.
Класс или интерфейс O является нулевым лексически окружающим объявлением типа самого себя.
Класс O является n-ым лексически окружающим объявлением типа класса C, если он является непосредственно окружающим объявлением типа n-1-го лексически окружающего объявления типа C.
Экземпляр i прямого вложенного класса C класса или интерфейса O связан с экземпляром O, известным как непосредственно окружающий экземпляр i. Непосредственно окружающий экземпляр объекта, если таковой имеется, определяется при создании объекта (§15.9.2).
Объект o является нулевым лексически окружающим экземпляром самого себя.
Объект o является n-ым лексически окружающим экземпляром экземпляра i, если он является непосредственно окружающим экземпляром n-1-го лексически окружающего экземпляра i.
Экземпляр вложенного класса I, объявление которого происходит в статическом контексте, не имеет лексически окружающих экземпляров. Однако, если I объявлен непосредственно внутри статического метода или статического инициализатора, то у I есть окружающий блок, который является ближайшим оператором блока, лексически окружающим объявление I.
Для каждого суперкласса S класса C, который сам является прямым вложенным классом класса или интерфейса SO, существует экземпляр SO, связанный с i, известный как непосредственно окружающий экземпляр i по отношению к S. Непосредственно окружающий экземпляр объекта по отношению к прямому суперклассу его класса, если таковой имеется, определяется при вызове конструктора суперкласса с помощью оператора явного вызова конструктора (§8.8.7.1).
Когда вложенный класс (объявление которого не происходит в статическом контексте) ссылается на переменную экземпляра, являющуюся членом лексически окружающего объявления типа, используется переменная соответствующего лексически окружающего экземпляра.
Любая локальная переменная, формальный параметр или параметр исключения, используемые, но не объявленные во вложенном классе, должны быть либо объявлены final, либо фактически окончательными (§4.12.4), в противном случае возникает ошибка времени компиляции при попытке использования.
Любая локальная переменная, используемая, но не объявленная во вложенном классе, должна быть однозначно присвоена (§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 (его второй лексически окружающий экземпляр).
Необязательная extends фраза в обычном объявлении класса указывает прямой родительский класс текущего класса.
extends Тип класса Фраза extends не должна появляться в определении класса Object, иначе произойдёт ошибка на этапе компиляции, поскольку это первичный класс и у него нет прямого родительского класса.
Тип Тип класса должен указывать доступный тип класса (§6.6), в противном случае произойдёт ошибка на этапе компиляции.
Ошибка на этапе компиляции произойдёт, если Тип класса указывает на класс, который является final, так как классы, отмеченные как final, не могут иметь подклассы (§8.1.1.2).
Ошибка на этапе компиляции произойдёт, если Тип класса указывает на класс Enum или на любое обращение к Enum (§8.9).
Если Тип класса имеет типы аргументов, он должен обозначать корректно сформированный параметризованный тип (§4.5), и ни один из типов аргументов не может быть аргументами типа с подстановочными знаками, в противном случае произойдёт ошибка на этапе компиляции.
Для объявления класса (возможно, обобщённого) C<F1,...,Fn> (n ≥ 0, C ≠ Object), прямой родительский класс типа класса C<F1,...,Fn> — это тип, указанный в extends фразе объявления C, если такая фраза присутствует, или Object в противном случае.
Для объявления обобщённого класса C<F1,...,Fn> (n > 0), прямой родительский класс параметризованного типа класса C<T1,...,Tn>, где Ti (1 ≤ i ≤ n) — тип, равен D<U1 θ,...,Uk θ>, где D<U1,...,Uk> — прямой родительский класс C<F1,...,Fn>, а θ — подстановка [F1:=T1,...,Fn:=Tn].
Класс считается прямым подклассом своего прямого родительского класса. Прямой родительский класс — это класс, чья реализация служит основой для реализации текущего класса.
Взаимоотношение «подкласс» — это транзитивное замыкание отношения «прямой подкласс». Класс A является подклассом класса C, если выполняется одно из следующих условий:
-
A — прямой подкласс C
-
Существует класс B такой, что A — подкласс B, а B — подкласс C, и это определение применяется рекурсивно.
Класс C называется родительским классом класса A, когда A является подклассом C.
Пример 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 прямо зависит от типа T, если T упомянут в extends или implements фразе C как родительский класс или родительский интерфейс, или как квалификатор в полном квалифицированном имени родительского класса или родительского интерфейса.
Класс C зависит от ссылочного типа T, если выполняется одно из следующих условий:
-
C прямо зависит от T.
-
C прямо зависит от интерфейса I, который зависит (§9.1.3) от T.
-
C прямо зависит от класса D, который зависит от T (используя это определение рекурсивно).
Ошибка на этапе компиляции произойдёт, если класс зависит от самого себя.
Если циклически объявленные классы обнаруживаются во время выполнения, при загрузке классов, то генерируется исключение 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), иначе возникает ошибка компиляции.
Если у ТипИнтерфейса есть аргументы типа, он должен обозначать правильно сформированный параметризованный тип (§4.5), и ни один из аргументов типа не должен быть аргументом типа с подстановкой, иначе возникает ошибка компиляции.
Ошибка компиляции возникает, если один и тот же интерфейс упоминается как непосредственный суперинтерфейс более одного раза в одной implements части. Это справедливо даже в том случае, если интерфейс назван по-разному.
Пример 8.1.5-1. Недопустимые Суперинтерфейсы
class Redundant implements java.lang.Cloneable, Cloneable {
int x;
}
Эта программа приводит к ошибке компиляции, потому что имена java.lang.Cloneable и Cloneable относятся к одному интерфейсу.
Для объявления класса (возможно, обобщенного) C<F1,...,Fn> (n ≥ 0, C ≠ Object), непосредственные суперинтерфейсы типа класса C<F1,...,Fn> — это типы, указанные в implements части объявления C, если такая implements часть присутствует.
Для обобщенного объявления класса C<F1,...,Fn> (n > 0), непосредственные суперинтерфейсы параметризованного типа класса C<T1,...,Tn>, где Ti (1 ≤ i ≤ n) — тип, — это все типы I<U1 θ,...,Uk θ>, где I<U1,...,Uk> является непосредственным суперинтерфейсом C<F1,...,Fn>, а θ — подстановка [F1:=T1,...,Fn:=Tn].
Тип интерфейса I является суперинтерфейсом типа класса C, если выполняется хотя бы одно из следующих условий:
-
I является непосредственным суперинтерфейсом C.
-
C имеет некоторый непосредственный суперинтерфейс J, для которого I является суперинтерфейсом, используя определение "суперинтерфейса интерфейса", приведенное в §9.1.3.
-
I является суперинтерфейсом непосредственного суперкласса C.
Класс может иметь суперинтерфейс более чем одним способом.
Класс считается реализующим все свои суперинтерфейсы.
Класс не может одновременно быть подтипом двух типов интерфейсов, которые являются различными параметризациями одного и того же обобщенного интерфейса (§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-3. Реализация Методов Суперинтерфейса
interface Colorable {
void setColor(int color);
int getColor();
}
class Point { int x, y; };
class ColoredPoint extends Point implements Colorable {
int color;
}
Эта программа приводит к ошибке компиляции, потому что ColoredPoint не является abstract классом, но не предоставляет реализации методов setColor и getColor интерфейса Colorable.
В следующей программе:
interface Fish { int getNumberOfScales(); }
interface Piano { int getNumberOfScales(); }
class Tuna implements Fish, Piano {
// You can tune a piano, but can you tuna fish?
public int getNumberOfScales() { return 91; }
}
метод getNumberOfScales в классе Tuna имеет имя, сигнатуру и тип возвращаемого значения, совпадающие с методом, объявленным в интерфейсе Fish, а также совпадающие с методом, объявленным в интерфейсе Piano; он считается реализующим оба.
С другой стороны, в ситуации, подобной этой:
interface Fish { int getNumberOfScales(); }
interface StringBass { double getNumberOfScales(); }
class Bass implements Fish, StringBass {
// This declaration cannot be correct,
// no matter what type is used.
public ?? getNumberOfScales() { return 91; }
}
невозможно объявить метод с именем getNumberOfScales, чья сигнатура и тип возвращаемого значения совместимы с сигнатурами и типами возвращаемых значений методов, объявленных в интерфейсе Fish и в интерфейсе StringBass, потому что класс не может иметь несколько методов с одинаковой сигнатурой и различными примитивными типами возвращаемых значений (§8.4). Поэтому невозможно для одного класса реализовать оба интерфейса Fish и StringBass (§8.4.8).
Тело класса может содержать объявления членов класса, то есть поля (§8.3), методы (§8.4), классы (§8.5) и интерфейсы (§8.5).
Тело класса также может содержать инициализаторы экземпляров (§8.6), статические инициализаторы (§8.7) и объявления конструкторов (§8.8) для класса.
Область действия и перекрытие объявления члена m, объявленного в или унаследованного классом типа C, указаны в §6.3 и §6.4.
Если сам C является вложенным классом, могут быть определения того же типа (переменной, метода или типа) и имени, что и m во внешних областях действия. (Области действия могут быть блоками, классами или пакетами.) Во всех таких случаях член m, объявленный в или унаследованный классом C, перекрывает (§6.4.1) другие определения того же типа и имени.
Члены типа класса — это всё из следующего:
Члены класса, которые объявлены private, не унаследуются подклассами этого класса.
Только члены класса, объявленные protected или public, унаследуются подклассами, объявленными в пакете, отличном от того, в котором объявлен класс.
Конструкторы, статические инициализаторы и инициализаторы экземпляров не являются членами и поэтому не наследуются.
Фраза тип члена обозначает:
-
Для поля — его тип.
-
Для метода — упорядоченную 4-ю кортеж, состоящую из:
-
Параметры типа: объявления любых параметров типа члена метода.
-
Типы аргументов: список типов аргументов члена метода.
-
Тип возвращаемого значения: тип возвращаемого значения члена метода.
-
throwsпредложение: типы исключений, объявленные вthrowsпредложении члена метода.
-
Поля, методы и типы членов типа класса могут иметь одинаковое имя, так как они используются в разных контекстах и различаются различными процедурами поиска (§6.5). Однако это не рекомендуется с точки зрения стиля.
Пример 8.2-1. Использование членов класса
class Point {
int x, y;
private Point() { reset(); }
Point(int x, int y) { this.x = x; this.y = y; }
private void reset() { this.x = 0; this.y = 0; }
}
class ColoredPoint extends Point {
int color;
void clear() { reset(); } // error
}
class Test {
public static void main(String[] args) {
ColoredPoint c = new ColoredPoint(0, 0); // error
c.reset(); // error
}
}
Эта программа вызывает четыре ошибки времени компиляции.
Одна ошибка возникает, потому что ColoredPoint не объявляет конструктор с двумя int параметрами, как требуется при использовании в main. Это иллюстрирует тот факт, что ColoredPoint не наследует конструкторы своего суперкласса Point.
Другая ошибка возникает, потому что ColoredPoint не объявляет конструкторов, и поэтому для него неявно объявляется конструктор по умолчанию (§8.8.9), и этот конструктор по умолчанию эквивалентен:
ColoredPoint() { super(); }
который вызывает конструктор без аргументов для непосредственного суперкласса класса ColoredPoint. Ошибка заключается в том, что конструктор для Point, принимающий нулевые аргументы, является private, и поэтому недоступен за пределами класса Point, даже через вызов конструктора суперкласса (§8.8.7).
Еще две ошибки возникают, потому что метод reset класса Point является private и поэтому не наследуется классом ColoredPoint. Таким образом, вызовы методов в методе clear класса ColoredPoint и в методе main класса Test неверны.
Пример 8.2-2. Наследование членов класса с доступом к пакету
Рассмотрим пример, когда пакет points объявляет две единицы компиляции:
package points;
public class Point {
int x, y;
public void move(int dx, int dy) { x += dx; y += dy; }
}
и:
package points;
public class Point3d extends Point {
int z;
public void move(int dx, int dy, int dz) {
x += dx; y += dy; z += dz;
}
}
и третья единица компиляции в другом пакете:
import points.Point3d;
class Point4d extends Point3d {
int w;
public void move(int dx, int dy, int dz, int dw) {
x += dx; y += dy; z += dz; w += dw; // compile-time errors
}
}
Здесь оба класса в пакете points компилируются. Класс Point3d наследует поля x и y класса Point, потому что он находится в том же пакете, что и Point. Класс Point4d, который находится в другом пакете, не наследует поля x и y класса Point или поле z класса Point3d, поэтому не компилируется.
Лучший способ написать третью единицу компиляции — это:
import points.Point3d;
class Point4d extends Point3d {
int w;
public void move(int dx, int dy, int dz, int dw) {
super.move(dx, dy, dz); w += dw;
}
}
используя метод move суперкласса Point3d для обработки dx, dy и dz. Если Point4d записано таким образом, оно будет компилироваться без ошибок.
Пример 8.2-3. Наследование членов класса public и protected
Учитывая класс Point:
package points;
public class Point {
public int x, y;
protected int useCount = 0;
static protected int totalUseCount = 0;
public void move(int dx, int dy) {
x += dx; y += dy; useCount++; totalUseCount++;
}
}
поля x, y, useCount и totalUseCount, public и protected наследуются во всех подклассах 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.
Переменные типа класса объявляются с помощью объявлений полей.
Каждое объявление в FieldDeclaration объявляет одно поле. Идентификатор в объявлении может использоваться в имени для ссылки на поле.
В одном FieldDeclaration можно объявить более одного поля, используя несколько объявлений; FieldModifierы и UnannType применяются ко всем объявлениям в объявлении.
Раздел FieldModifier описан в §8.3.1.
Объявленный тип поля обозначается UnannType, если в UnannType и VariableDeclaratorId нет пар скобок, и определяется в §10.2 в противном случае.
Область действия и перекрытие объявления поля указаны в §6.3 и §6.4.
Ошибка компиляции возникает, если тело объявления класса объявляет два поля с одинаковым именем.
Если класс объявляет поле с определенным именем, то объявление этого поля считается скрывающим любые и все доступные объявления полей с тем же именем в суперклассах и суперинтерфейсах класса.
В этом отношении скрытие полей отличается от скрытия методов (§8.4.8.3), так как нет различия между static и не-static полями при скрытии полей, в то время как различие проводится между static и не-static методами при скрытии методов.
К скрытому полю можно получить доступ, используя квалифицированное имя (§6.5.6.2), если оно static, или используя выражение доступа к полю, содержащее ключевое слово super (§15.11.2) или приведение к типу суперкласса.
В этом отношении скрытие полей аналогично скрытию методов.
Если объявление поля скрывает объявление другого поля, то эти два поля не обязательно должны иметь одинаковый тип.
Класс наследует от своего непосредственного суперкласса и непосредственных суперинтерфейсов все не-private поля суперкласса и суперинтерфейсов, которые доступны коду в классе и не скрыты объявлением в классе.
private поле суперкласса может быть доступно подклассу — например, если оба класса являются членами одного класса. Тем не менее, private поле никогда не наследуется подклассом.
Класс может унаследовать более одного поля с одинаковым именем. Такая ситуация сама по себе не приводит к ошибке компиляции. Однако любая попытка в теле класса сослаться на любое такое поле по его простому имени приведет к ошибке компиляции, так как такая ссылка является неоднозначной.
Может быть несколько путей, по которым одно и то же объявление поля может быть унаследовано от интерфейса. В такой ситуации поле считается унаследованным только один раз, и к нему можно обратиться по его простому имени без неоднозначности.
Значение, хранящееся в поле типа float, всегда является элементом множества значений с плавающей точкой (§4.2.3); аналогично, значение, хранящееся в поле типа double, всегда является элементом множества значений с двойной точностью. Не допускается, чтобы поле типа float содержало элемент множества значений с расширенным порядком плавающей точки, который не является также элементом множества значений с плавающей точкой, а также для поля типа double содержать элемент множества значений с расширенным порядком двойной точности, который не является также элементом множества значений с двойной точностью.
Пример 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.
Если одно и то же ключевое слово используется более одного раза в качестве модификатора в объявлении поля, возникает ошибка компиляции.
Если в объявлении поля присутствует два или более (различных) модификатора поля, то рекомендуется (хотя это не обязательно), чтобы они располагались в том же порядке, что и показано выше в схеме для FieldModifier.
Если поле объявлено статическим, существует ровно одна его копия независимо от того, сколько экземпляров (возможно, ноль) класса будет создано. Статическое поле, иногда называемое переменной класса, инициализируется при инициализации класса (§12.4).
Поле, которое не объявлено статическим (иногда называемое нестатическим полем), называется переменной экземпляра. При создании нового экземпляра класса (§12.5) для каждой переменной экземпляра, объявленной в этом классе или его предках, создаётся новая переменная, связанная с этим экземпляром.
Пример 8.3.1.1-1. Статические поля
class Point {
int x, y, useCount;
Point(int x, int y) { this.x = x; this.y = y; }
static final Point origin = new Point(0, 0);
}
class Test {
public static void main(String[] args) {
Point p = new Point(1,1);
Point q = new Point(2,2);
p.x = 3;
p.y = 3;
p.useCount++;
p.origin.useCount++;
System.out.println("(" + q.x + "," + q.y + ")");
System.out.println(q.useCount);
System.out.println(q.origin == Point.origin);
System.out.println(q.origin.useCount);
}
}
Эта программа выведет:
(2,2) 0 true 1
что показывает, что изменение полей x, y и useCount объекта p не влияет на поля объекта q, поскольку эти поля являются переменными экземпляров в различных объектах. В этом примере переменная класса origin класса Point ссылается как с использованием имени класса в качестве квалификатора в Point.origin, так и с использованием переменных типа класса в выражениях доступа к полям (§15.11), как в p.origin и q.origin. Эти два способа доступа к переменной класса origin класса обращаются к одному и тому же объекту, что подтверждается тем, что значение выражения сравнения ссылок (§15.21.3):
q.origin==Point.origin
истинно. Дополнительным доказательством является то, что инкрементирование:
p.origin.useCount++;
приводит к тому, что значение q.origin.useCount становится 1; это происходит потому, что p.origin и q.origin ссылаются на одну и ту же переменную.
Пример 8.3.1.1-2. Скрытие переменных класса
class Point {
static int x = 2;
}
class Test extends Point {
static double x = 4.7;
public static void main(String[] args) {
new Test().printX();
}
void printX() {
System.out.println(x + " " + super.x);
}
}
Эта программа выведет:
4.7 2
потому что объявление x в классе Test скрывает определение x в классе Point, поэтому класс Test не наследует поле x от своего суперкласса Point. Внутри объявления класса Test простое имя x относится к полю, объявленному внутри класса Test. Код в классе Test может ссылаться на поле x класса Point как на super.x (или, поскольку x является static, как на Point.x). Если объявление Test.x удалено:
class Point {
static int x = 2;
}
class Test extends Point {
public static void main(String[] args) {
new Test().printX();
}
void printX() {
System.out.println(x + " " + super.x);
}
}
то поле x класса Point больше не скрыто в классе Test; вместо этого простое имя x теперь относится к полю Point.x. Код в классе Test может по-прежнему ссылаться на это же поле как на super.x. Таким образом, вывод от этой модифицированной программы:
2 2
Пример 8.3.1.1-3. Скрытие переменных экземпляра
class Point {
int x = 2;
}
class Test extends Point {
double x = 4.7;
void printBoth() {
System.out.println(x + " " + super.x);
}
public static void main(String[] args) {
Test sample = new Test();
sample.printBoth();
System.out.println(sample.x + " " + ((Point)sample).x);
}
}
Эта программа выведет:
4.7 2 4.7 2
потому что объявление x в классе Test скрывает определение x в классе Point, поэтому класс Test не наследует поле x от своего суперкласса Point. Следует отметить, однако, что, хотя поле x класса Point не наследуется классом Test, оно тем не менее реализуется экземплярами класса Test. Другими словами, каждый экземпляр класса Test содержит два поля, одно типа int и одно типа double. Оба поля имеют имя x, но внутри объявления класса Test простое имя x всегда относится к полю, объявленному внутри класса Test. Код в методах экземпляров класса Test может ссылаться на переменную экземпляра x класса Point как на super.x.
Код, использующий выражение доступа к полю для доступа к полю x, обратится к полю с именем x в классе, указанном типом выражения ссылки. Таким образом, выражение sample.x обращается к значению типа double, переменной экземпляра, объявленной в классе Test, поскольку тип переменной sample является Test, но выражение ((Point)sample).x обращается к значению типа int, переменной экземпляра, объявленной в классе Point, из-за приведения к типу Point.
Если объявление x удалено из класса Test, как в программе:
class Point {
static int x = 2;
}
class Test extends Point {
void printBoth() {
System.out.println(x + " " + super.x);
}
public static void main(String[] args) {
Test sample = new Test();
sample.printBoth();
System.out.println(sample.x + " " + ((Point)sample).x);
}
}
то поле x класса Point больше не скрывается в классе Test. Внутри методов экземпляров в объявлении класса Test простое имя x теперь относится к полю, объявленному внутри класса Point. Код в классе Test по-прежнему может ссылаться на это же поле как на super.x. Выражение sample.x по-прежнему ссылается на поле x внутри типа Test, но это поле теперь является наследуемым полем и, таким образом, относится к полю x, объявленному в классе Point. Вывод от этой модифицированной программы:
2 2 2 2
Поле может быть объявлено final (§4.12.4). Как статические, так и переменные экземпляра (статические и нестатические поля) могут быть объявлены final.
Пустая статическая переменная final должна быть однозначно присвоена статической инициализацией класса, в котором она объявлена, иначе произойдёт ошибка компиляции (§8.7, §16.8).
Пустая переменная экземпляра final должна быть однозначно присвоена в конце каждого конструктора класса, в котором она объявлена, иначе произойдёт ошибка компиляции (§8.8, §16.9).
Переменные могут быть помечены как transient, чтобы указать, что они не являются частью состояния объекта.
Пример 8.3.1.3-1. Сохранение полей transient
Если экземпляр класса Point:
class Point {
int x, y;
transient float rho, theta;
}
был сохранён в постоянное хранилище системной службой, то будут сохранены только поля x и y. Данное спецификация не описывает детали таких служб; см. спецификацию java.io.Serializable для примера такой службы.
Язык программирования Java позволяет потокам доступа к общим переменным (§17.1). Как правило, чтобы гарантировать, что общие переменные обновляются последовательно и надёжно, поток должен обеспечить эксклюзивный доступ к таким переменным, получив блокировку, которая, как правило, обеспечивает взаимное исключение для этих общих переменных.
Язык программирования Java предоставляет второй механизм, поля volatile, который более удобен для некоторых целей.
Поле может быть объявлено volatile, в этом случае модель памяти Java гарантирует, что все потоки видят согласованное значение переменной (§17.4).
Если переменная volatile также объявлена final, это является ошибкой на стадии компиляции.
Пример 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 синхронизированными (§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) декларированной переменной.
Если инициализатор относится к переменной класса (то есть полю класса), то применяются следующие правила:
-
Это ошибка компиляции, если в инициализаторе используется ссылка по простому имени на любую переменную экземпляра.
-
Это ошибка компиляции, если в инициализаторе встречаются ключевые слова
this(§15.8.3) илиsuper(§15.11.2, §15.12). -
Во время выполнения инициализатор вычисляется и присваивание выполняется ровно один раз, когда класс инициализируется (§12.4.2).
Обратите внимание, что поля класса, являющиеся константами (§4.12.4), инициализируются до других полей класса (§12.4.2). Это также относится к интерфейсам (§9.3.1). Такие поля никогда не будут иметь значения по умолчанию (§4.12.5), даже в хитроумных программах.
Если инициализатор относится к переменной экземпляра (то есть полю, которое не является полем класса), то применяются следующие правила:
-
Инициализатор может использовать простое имя любой переменной класса, объявленной в классе или унаследованной от него, даже если её объявление находится после инициализатора.
-
Инициализатор может ссылаться на текущий объект
this(§15.8.3) и использовать ключевое словоsuper(§15.11.2, §15.12). -
Во время выполнения инициализатор вычисляется и присваивание выполняется каждый раз при создании экземпляра класса (§12.5).
Проверка исключений для инициализатора переменной в объявлении поля описана в §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.
Использование переменных класса, объявление которых расположено текстово позже места их использования, иногда ограничено, даже если эти переменные класса находятся в области видимости (§6.3). Конкретно, это ошибка компиляции, если все перечисленные ниже условия выполняются одновременно:
-
Объявление переменной класса в классе или интерфейсе C расположено текстово позже места использования этой переменной;
-
Место использования – это простое имя в инициализаторе переменной класса C или статическом инициализаторе C;
-
Место использования не находится в левой части оператора присваивания;
-
C – это самый внутренний класс или интерфейс, содержащий место использования.
Использование переменных экземпляра, объявление которых расположено текстово позже места их использования, иногда ограничено, даже если эти переменные экземпляра находятся в области видимости. Конкретно, это ошибка компиляции, если все перечисленные ниже условия выполняются одновременно:
-
Объявление переменной экземпляра в классе или интерфейсе C расположено текстово позже места использования этой переменной;
-
Место использования – это простое имя в инициализаторе переменной экземпляра C или инициализаторе экземпляра C;
-
Место использования не находится в левой части оператора присваивания;
-
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, клаузула MethodModifier в §8.4.3, клаузула TypeParameters в §8.4.4, клаузула Result в §8.4.5, клаузула Throws в §8.4.6, и тело метода в §8.4.7.
Идентификатор в деклараторе метода может использоваться в имени для ссылки на метод (§6.5.7.1, §15.12).
Ошибка компиляции возникает, если тело класса объявляет два метода с эквивалентными по переопределению сигнатурами (§8.4.2).
Область видимости и скрытие объявления метода определены в §6.3 и §6.4.
Разрешается объявление метода, возвращающего массив, в котором некоторые или все пары скобок, обозначающие тип массива, размещаются после списка формальных параметров. Этот синтаксис поддерживается для совместимости с ранними версиями языка программирования Java. Очень настоятельно рекомендуется не использовать этот синтаксис в новом коде.
Формальные параметры метода или конструктора, если таковые имеются, задаются списком параметров, разделённых запятыми. Каждый параметр состоит из типа (возможно, предваряемого модификатором final и/или одним или несколькими аннотациями) и идентификатора (возможно, за которым следуют скобки), определяющего имя параметра.
Если метод или конструктор не имеют формальных параметров, в объявлении метода или конструктора отображается только пустая пара скобок.
Следующие правила из §4.3 и §8.3 приведены здесь для удобства:
Последний формальный параметр метода или конструктора является особым: он может быть параметром с переменным числом аргументов, что указывается эллипсом после типа.
Обратите внимание, что эллипс (...) является отдельным токеном (§3.11). Между ним и типом можно вставить пробелы, но это не рекомендуется с точки зрения стиля.
Если последний формальный параметр является параметром с переменным числом аргументов, метод является методом с переменным числом аргументов. В противном случае, это метод с фиксированным числом аргументов.
Параметр получателя — это необязательный синтаксический элемент для метода экземпляра или конструктора внутреннего класса. Для метода экземпляра параметр получателя представляет собой объект, для которого вызывается метод. Для конструктора внутреннего класса параметр получателя представляет собой непосредственный внешний экземпляр только что созданного объекта. В любом случае, параметр получателя существует исключительно для возможности обозначения типа представленного объекта в исходном коде, чтобы его можно было аннотировать. Параметр получателя не является формальным параметром; точнее, это не объявление какого-либо типа переменной (§4.12.3), он никогда не связывается ни с каким значением, переданным в качестве аргумента в выражении вызова метода или квалифицированном выражении создания экземпляра класса, и не оказывает никакого влияния во время выполнения.
Правила применения модификаторов аннотаций к объявлению формального параметра и параметра получателя указаны в §9.7.4 и §9.7.5.
Ошибка компиляции, если final появляется более одного раза как модификатор для объявления формального параметра.
Ошибка компиляции при использовании смешанной записи массивов (§10.2) для параметра с переменным числом аргументов.
Область действия и перекрытие формального параметра указаны в §6.3 и §6.4.
Ошибка компиляции, если метод или конструктор объявляют два формальных параметра с одинаковым именем. (То есть, их объявления упоминают тот же самый Идентификатор.)
Ошибка компиляции, если формальный параметр, объявленный final, присваивается в теле метода или конструктора.
Параметр получателя может появиться только в FormalParameterList метода экземпляра или конструктора внутреннего класса; в противном случае возникает ошибка компиляции.
Где разрешен параметр получателя, его тип и имя задаются следующим образом:
-
В методе экземпляра тип параметра получателя должен быть классом или интерфейсом, в котором объявлен метод, а имя параметра получателя должно быть
this; в противном случае возникает ошибка компиляции. -
В конструкторе внутреннего класса тип параметра получателя должен быть классом или интерфейсом, являющимся непосредственно окружающим объявлением типа внутреннего класса, а имя параметра получателя должно быть Идентификатор
.this, где Идентификатор — это простое имя класса или интерфейса, являющегося непосредственно окружающим объявлением типа внутреннего класса; в противном случае возникает ошибка компиляции.
Объявленный тип формального параметра зависит от того, является ли он параметром с переменным числом аргументов:
-
Если формальный параметр не является параметром с переменным числом аргументов, то объявленный тип обозначается UnannType, если в UnannType и VariableDeclaratorId нет пар скобок, и задается в §10.2 в противном случае.
-
Если формальный параметр является параметром с переменным числом аргументов, то объявленный тип задаётся в §10.2. (Обратите внимание, что «смешанная запись» не допускается для параметров с переменным числом аргументов.)
Если объявленный тип параметра с переменным числом аргументов имеет нереализуемый элемент-тип (§4.7), то во время компиляции возникает предупреждение о недопустимом типе для объявления метода с переменным числом аргументов, если метод не аннотирован с помощью @SafeVarargs (§9.6.4.7) или предупреждение о недопустимом типе подавлено с помощью @SuppressWarnings (§9.6.4.5).
При вызове метода или конструктора (§15.12) значения выражений фактических аргументов инициализируют вновь созданные переменные параметров, каждый из объявленного типа, до начала выполнения тела метода или конструктора. Идентификатор, который появляется в DeclaratorId, может использоваться в качестве простого имени в теле метода или конструктора для ссылки на формальный параметр.
Вызовы метода с переменным числом аргументов могут содержать больше выражений фактических аргументов, чем формальных параметров. Все выражения фактических аргументов, которые не соответствуют формальным параметрам, предшествующим параметру с переменным числом аргументов, будут вычислены, и результаты будут сохранены в массив, который будет передан в вызов метода (§15.12.4.2).
Параметр метода или конструктора типа float всегда содержит элемент множества значений с плавающей точкой (§4.2.3); аналогично, параметр метода или конструктора типа double всегда содержит элемент множества значений с двойной точностью. Не допускается, чтобы параметр метода или конструктора типа float содержал элемент множества значений с расширенным порядком для чисел с плавающей точкой, который не является также элементом множества значений с плавающей точкой, а также параметр метода типа double содержал элемент множества значений с расширенным порядком для чисел с двойной точностью, который не является также элементом множества значений с двойной точностью.
Если выражение фактического аргумента, соответствующего переменной параметра, не является FP-строгим (§15.4), вычисление этого выражения фактического аргумента допускается с использованием промежуточных значений, взятых из соответствующих наборов значений с расширенной мантиссой. Перед сохранением в переменной параметра результат такого выражения отображается на ближайшее значение в соответствующем стандартном наборе значений путём применения преобразования вызова (§5.3).
Ниже приведены примеры параметров получателя в методах экземпляра и конструкторах внутренних классов:
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 показывают, что тип параметра получателя может быть обозначен с помощью квалифицированного имени типа, как и любой другой тип; но что имя параметра получателя в конструкторе внутреннего класса должно использовать простое имя окружающего класса.
Два метода или конструктора, 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.
Если одно и то же ключевое слово используется более одного раза в качестве модификатора в объявлении метода, то это ошибка времени компиляции.
Если в объявлении метода используется ключевое слово 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 переопределяет не-abstract toString метод класса Object. (Класс Object является неявным прямым суперклассом класса Point.) Добавление кода:
class ColoredPoint extends Point {
int color;
public String toString() {
return super.toString() + ": color " + color; // error
}
}
приводит к ошибке времени компиляции, потому что обращение super.toString() относится к методу toString в классе Point, который является abstract и поэтому не может быть вызван. Метод toString класса Object может быть доступен классу ColoredPoint только если класс Point явно делает его доступным через какой-либо другой метод, как в:
abstract class Point {
int x, y;
public abstract String toString();
protected String objString() { return super.toString(); }
}
class ColoredPoint extends Point {
int color;
public String toString() {
return objString() + ": color " + color; // correct
}
}
Метод, объявленный static, называется методом класса.
Использование имени параметра типа любого окружающего объявления в заголовке или теле статического метода является ошибкой времени компиляции.
Статический метод всегда вызывается без ссылки на конкретный объект. Попытка сослаться на текущий объект с использованием ключевого слова this (§15.8.3) или ключевого слова super (§15.11.2) является ошибкой времени компиляции.
Метод, который не объявлен static, называется методом объекта, а иногда и не-static методом.
Метод-объект всегда вызывается относительно объекта, который становится текущим объектом, к которому во время выполнения тела метода относятся ключевые слова this и super.
Метод может быть объявлен 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;
}
Эффект модификатора strictfp состоит в том, что все float или double выражения внутри тела метода будут явно FP-строгими (§15.4).
Метод 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.
Два метода или конструктора 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).
Ошибка компиляции, если тип ExceptionType, указанный в операторе throws, не является подтипом (§4.10) типа Throwable.
Переменные типов допускаются в операторе throws, хотя они не допускаются в операторе catch (§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 унаследует от своего непосредственного суперкласса все конкретные методы m (как статические, так и инстансные) суперкласса, для которых выполняются все следующие условия:
-
mявляется членом непосредственного суперкласса C. -
mимеетpublic,protectedили объявлен с доступом по умолчанию в том же пакете, что и C. -
Ни один метод, объявленный в C, не имеет сигнатуры, являющейся подсигнатурой (§8.4.2) сигнатуры
m.
Класс C унаследует от своего непосредственного суперкласса и непосредственных суперинтерфейсов все abstract и методы по умолчанию (§9.4) m, для которых выполняются все следующие условия:
-
mявляется членом непосредственного суперкласса или непосредственного суперинтерфейса, D, C. -
mимеетpublic,protectedили объявлен с доступом по умолчанию в том же пакете, что и C. -
Ни один метод, объявленный в C, не имеет сигнатуры, являющейся подсигнатурой (§8.4.2) сигнатуры
m. -
Ни один конкретный метод, унаследованный C от своего непосредственного суперкласса, не имеет сигнатуры, являющейся подсигнатурой сигнатуры
m. -
Нет метода
m', который является членом непосредственного суперкласса или непосредственного суперинтерфейса, D', C (mразличен отm', D различен от D'), такого, что методm' из D' переопределяет объявление методаm.
Класс не наследует static методы от своих суперинтерфейсов.
Обратите внимание, что унаследованный конкретный метод может предотвратить наследование abstract или метода по умолчанию. (Позже мы утвердим, что конкретный метод переопределяет abstract или метод по умолчанию "от C"). Также один супертип-метод может предотвратить наследование другого супертип-метода, если первый "уже" переопределяет второй — это то же правило, что и для интерфейсов (§9.4.1), и предотвращает конфликты, при которых наследуются несколько методов по умолчанию, и одна реализация явно предназначена для замены другой.
Обратите внимание, что методы переопределяются или скрываются на основе сигнатур. Например, если класс объявляет два public метода с одинаковым именем (§8.4.9), и подкласс переопределяет один из них, подкласс по-прежнему наследует другой метод.
Метод экземпляра mC, объявленный или унаследованный классом C, переопределяет из C другой метод mA, объявленный в классе A, если выполняются все следующие условия:
-
A является суперклассом C.
-
C не наследует
mA. -
Подпись
mCявляется подписью (§8.4.2) подписиmA. -
Выполняется одно из следующих условий:
-
mAявляетсяpublic. -
mAявляетсяprotected. -
mAобъявлен с доступом по умолчанию в том же пакете, что и C, и либо C объявляетmC, либоmAявляется членом непосредственного суперкласса C. -
mAобъявлен с доступом по умолчанию, иmCпереопределяетmAиз некоторого суперкласса C. -
mAобъявлен с доступом по умолчанию иmCпереопределяет методm'из C (m'отличается отmCиmA), так чтоm'переопределяетmAиз некоторого суперкласса C.
-
Если метод экземпляра, не являющийся abstract, mC переопределяет метод abstract, mA из класса C, то mC называется реализующим mA из C.
Метод экземпляра mC, объявленный или унаследованный классом C, переопределяет из C другой метод mI, объявленный в интерфейсе I, если выполняются все следующие условия:
-
I является суперинтерфейсом C.
-
mIявляетсяabstractили методом по умолчанию. -
Подпись
mCявляется подписью (§8.4.2) подписиmI.
Подпись переопределяющего метода может отличаться от переопределяемого, если формальный параметр в одном из методов имеет непараметризованный тип, а соответствующий параметр в другом имеет параметризованный тип. Это позволяет мигрировать существующий код для использования возможностей дженериков.
Понятие переопределения включает методы, переопределяющие другой метод из некоторого подкласса своего объявляющего класса. Это может произойти двумя способами:
-
Конкретный метод в обобщённом суперклассе может, при определенных параметризациях, иметь ту же подпись, что и абстрактный метод в этом классе. В этом случае конкретный метод наследуется, а
abstractметод нет (как описано выше). Тогда наследованный метод следует рассматривать как переопределяющий свой абстрактный аналог из C. (Эта ситуация усложняется доступом по умолчанию: если C находится в другом пакете, тоmAвсё равно не был бы унаследован и не должен считаться переопределённым.) -
Метод, унаследованный от класса, может переопределять метод из суперинтерфейса. (К счастью, доступ по умолчанию здесь не важен.)
Если метод экземпляра переопределяет метод static, возникает ошибка компиляции.
В этом отношении переопределение методов отличается от скрытия полей (§8.3), так как допустимо, чтобы переменная экземпляра скрывала переменную static.
К переопределённому методу можно получить доступ, используя выражение вызова метода (§15.12), содержащее ключевое слово super. Квалифицированное имя или приведение к типу суперкласса неэффективны для попытки доступа к переопределённому методу.
В этом отношении переопределение методов отличается от скрытия полей.
Наличие или отсутствие модификатора strictfp совершенно не влияет на правила переопределения методов и реализации абстрактных методов. Например, разрешено, чтобы метод, не являющийся строгим по FP, переопределял строгий по FP метод, и разрешено, чтобы строгий по FP метод переопределял метод, не являющийся строгим по FP.
Пример 8.4.8.1-1. Переопределение
class Point {
int x = 0, y = 0;
void move(int dx, int dy) { x += dx; y += dy; }
}
class SlowPoint extends Point {
int xLimit, yLimit;
void move(int dx, int dy) {
super.move(limit(dx, xLimit), limit(dy, yLimit));
}
static int limit(int d, int limit) {
return d > limit ? limit : d < -limit ? -limit : d;
}
}
Здесь класс SlowPoint переопределяет объявление метода move класса Point своим собственным методом move, который ограничивает расстояние, которое точка может перемещаться при каждом вызове метода. Когда метод move вызывается для экземпляра класса SlowPoint, переопределенное определение в классе SlowPoint всегда будет вызываться, даже если ссылка на объект SlowPoint взята из переменной, тип которой Point.
Пример 8.4.8.1-2. Переопределение
Переопределение облегчает подклассам расширение поведения существующего класса, как показано в этом примере:
import java.io.OutputStream;
import java.io.IOException;
class BufferOutput {
private OutputStream o;
BufferOutput(OutputStream o) { this.o = o; }
protected byte[] buf = new byte[512];
protected int pos = 0;
public void putchar(char c) throws IOException {
if (pos == buf.length) flush();
buf[pos++] = (byte)c;
}
public void putstr(String s) throws IOException {
for (int i = 0; i < s.length(); i++)
putchar(s.charAt(i));
}
public void flush() throws IOException {
o.write(buf, 0, pos);
pos = 0;
}
}
class LineBufferOutput extends BufferOutput {
LineBufferOutput(OutputStream o) { super(o); }
public void putchar(char c) throws IOException {
super.putchar(c);
if (c == '\n') flush();
}
}
class Test {
public static void main(String[] args) throws IOException {
LineBufferOutput lbo = new LineBufferOutput(System.out);
lbo.putstr("lbo\nlbo");
System.out.print("print\n");
lbo.putstr("\n");
}
}
Эта программа выводит:
lbo print lbo
Класс BufferOutput реализует очень простую буферизованную версию OutputStream, очищая вывод, когда буфер заполняется или вызывается flush. Подкласс LineBufferOutput объявляет только конструктор и один метод putchar, который переопределяет метод putchar из BufferOutput. Он наследует методы putstr и flush от класса BufferOutput.
В методе putchar объекта LineBufferOutput, если символьный аргумент является новой строкой, то он вызывает метод flush. Важный момент в отношении переопределения в этом примере заключается в том, что метод putstr, который объявлен в классе BufferOutput, вызывает метод putchar, определенный текущим объектом this, который не обязательно является методом putchar, объявленным в классе BufferOutput.
Таким образом, когда putstr вызывается в main с использованием объекта LineBufferOutput lbo, вызов putchar в теле метода putstr является вызовом putchar объекта lbo, переопределяющего объявления putchar, которое проверяет новую строку. Это позволяет подклассу BufferOutput изменять поведение метода putstr без его повторного определения.
Документация класса, такого как BufferOutput, который предназначен для расширения, должна чётко указывать контракт между классом и его подклассами и чётко указывать, что подклассы могут переопределять метод putchar таким образом. Поэтому разработчик класса BufferOutput не хотел бы изменять реализацию putstr в будущих реализациях BufferOutput, чтобы не использовать метод putchar, так как это нарушит существующий контракт с подклассами. См. обсуждение двоичной совместимости в §13 (Двоичная совместимость), особенно §13.2.
Если класс C объявляет или наследует метод static m, то он считается скрывающим любой метод m', где подпись m является подподписью (§8.4.2) подписи m' в суперклассах и суперинтерфейсах C, которые в противном случае были бы доступны коду в C.
Если метод 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, в противном случае произойдёт ошибка компиляции. -
Для каждого типа проверяемого исключения, перечисленного в предложении
throwsm2, тот же класс исключений или один из его супертипов должен присутствовать в стирании (§4.6) предложенияthrowsm1; в противном случае произойдёт ошибка компиляции. -
Если нестираемое предложение
throwsm1не содержит супертип каждого типа исключения в предложенииthrowsm2(при необходимости адаптированный к параметрам типаm1), возникает неустраняемое предупреждение при компиляции.
Ошибка компиляции возникает, если объявление типа T имеет метод-член m1 и существует метод m2, объявленный в T или его супертипе, при выполнении следующих условий:
-
m1иm2имеют одинаковое имя. -
m2доступен из T. -
Подпись
m1не является подписью (§8.4.2) подписиm2. -
Подпись
m1или некоторого методаm1, который переопределяет (прямо или косвенно), имеет такое же стирание, как подписьm2или некоторого методаm2, который переопределяет (прямо или косвенно).
Эти ограничения необходимы, потому что дженерики реализуются с помощью стирания. Приведённое выше правило подразумевает, что методы, объявленные в одном классе с тем же именем, должны иметь разные стирания. Оно также подразумевает, что объявление типа не может реализовывать или расширять два разных вызова одного и того же обобщённого интерфейса.
Модификатор доступа (§6.6) переопределяемого или скрываемого метода должен обеспечивать не менее широкого доступа, чем переопределяемый или скрываемый метод, как указано ниже:
-
Если переопределяемый или скрываемый метод имеет
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 пункты в этом случае не вызывают ошибок.)
Может быть несколько путей, по которым одно и то же объявление метода унаследовано из интерфейса. Этот факт не создаёт трудностей и никогда сам по себе не приводит к ошибке компиляции.
Если у класса два метода (объявленные в одном классе, унаследованные или один объявленный, а другой унаследованный) имеют одинаковое имя, но подписи не являются эквивалентными по переопределению, то имя метода считается перегруженным.
Этот факт не создаёт трудностей и сам по себе никогда не приводит к ошибке компиляции. Не существует обязательной связи между типами возвращаемых значений или между throws пунктами двух методов с одинаковым именем, если их подписи не эквивалентны по переопределению.
При вызове метода (§15.12), количество фактических аргументов (и любых явных аргументов типа) и типы аргументов во время компиляции используются для определения подписи метода, который будет вызван (§15.12.2). Если метод, который нужно вызвать, является методом экземпляра, фактический вызываемый метод будет определён во время выполнения, используя динамический поиск методов (§15.12.4).
Пример 8.4.9-1. Перегрузка
class Point {
float x, y;
void move(int dx, int dy) { x += dx; y += dy; }
void move(float dx, float dy) { x += dx; y += dy; }
public String toString() { return "("+x+","+y+")"; }
}
Здесь класс Point имеет два члена, которые являются методами с одинаковым именем, move. Метод move класса Point, выбранный для конкретного вызова метода, определяется во время компиляции процедурой разрешения перегрузки, описанной в §15.12.
В общем, члены класса Point представляют собой float переменные-члены x и y, объявленные в Point, два объявленных move метода, объявленный toString метод и члены, которые Point наследует от своего неявного непосредственного суперкласса Object (§4.3.2), например, метод hashCode. Обратите внимание, что Point не наследует метод toString класса Object, потому что этот метод переопределяется объявлением метода toString в классе Point.
Пример 8.4.9-2. Перегрузка, Переопределение и Скрытие
class Point {
int x = 0, y = 0;
void move(int dx, int dy) { x += dx; y += dy; }
int color;
}
class RealPoint extends Point {
float x = 0.0f, y = 0.0f;
void move(int dx, int dy) { move((float)dx, (float)dy); }
void move(float dx, float dy) { x += dx; y += dy; }
}
Здесь класс RealPoint скрывает объявления int переменных-членов x и y класса Point, используя свои собственные float переменные-члены x и y, и переопределяет метод move класса Point своим собственным методом move. Он также перегружает имя move другим методом с другой подписью (§8.4.2).
В этом примере члены класса RealPoint включают переменную-член color, унаследованную от класса Point, float переменные-члены x и y, объявленные в RealPoint, и два move метода, объявленные в RealPoint.
Какой из этих перегруженных move методов класса RealPoint будет выбран для любого конкретного вызова метода, определяется во время компиляции процедурой разрешения перегрузки, описанной в §15.12.
Следующая программа является расширенной вариацией предыдущей программы:
class Point {
int x = 0, y = 0, color;
void move(int dx, int dy) { x += dx; y += dy; }
int getX() { return x; }
int getY() { return y; }
}
class RealPoint extends Point {
float x = 0.0f, y = 0.0f;
void move(int dx, int dy) { move((float)dx, (float)dy); }
void move(float dx, float dy) { x += dx; y += dy; }
float getX() { return x; }
float getY() { return y; }
}
Здесь класс Point предоставляет методы getX и getY, которые возвращают значения его полей x и y; класс RealPoint затем переопределяет эти методы, объявляя методы с той же подписью. Результатом являются две ошибки во время компиляции, по одной на метод, потому что типы возвращаемых значений не совпадают; методы в классе Point возвращают значения типа int, а методы-кандидаты на переопределение в классе RealPoint возвращают значения типа float.
Эта программа исправляет ошибки предыдущей программы:
class Point {
int x = 0, y = 0;
void move(int dx, int dy) { x += dx; y += dy; }
int getX() { return x; }
int getY() { return y; }
int color;
}
class RealPoint extends Point {
float x = 0.0f, y = 0.0f;
void move(int dx, int dy) { move((float)dx, (float)dy); }
void move(float dx, float dy) { x += dx; y += dy; }
int getX() { return (int)Math.floor(x); }
int getY() { return (int)Math.floor(y); }
}
Здесь методы переопределения getX и getY в классе RealPoint имеют те же типы возвращаемых значений, что и методы класса Point, которые они переопределяют, поэтому этот код можно успешно скомпилировать.
Рассмотрим теперь эту тестовую программу:
class Test {
public static void main(String[] args) {
RealPoint rp = new RealPoint();
Point p = rp;
rp.move(1.71828f, 4.14159f);
p.move(1, -1);
show(p.x, p.y);
show(rp.x, rp.y);
show(p.getX(), p.getY());
show(rp.getX(), rp.getY());
}
static void show(int x, int y) {
System.out.println("(" + x + ", " + y + ")");
}
static void show(float x, float y) {
System.out.println("(" + x + ", " + y + ")");
}
}
Вывод этой программы:
(0, 0) (2.7182798, 3.14159) (2, 3) (2, 3)
Первая строка вывода иллюстрирует тот факт, что экземпляр класса RealPoint фактически содержит две целые переменные, объявленные в классе Point; дело лишь в том, что их имена скрыты от кода, находящегося в объявлении класса RealPoint (и любых его подклассов). При обращении к полю x, используя ссылку на экземпляр класса RealPoint в переменной типа Point, обращается к целочисленному полю x, объявленному в классе Point. Тот факт, что его значение равно нулю, свидетельствует о том, что вызов метода p.move(1, -1) не вызвал метод move класса Point; вместо этого он вызвал переопределённый метод move класса RealPoint.
Вторая строка вывода показывает, что обращение к полю rp.x ссылается на поле x, объявленное в классе RealPoint. Это поле типа float, и эта вторая строка вывода соответственно отображает значения с плавающей точкой. Кстати, это также иллюстрирует тот факт, что имя метода show является перегруженным; типы аргументов в вызове метода определяют, какая из двух определений будет вызвана.
Последние две строки вывода показывают, что вызовы методов p.getX() и rp.getX() каждый вызывает метод getX, объявленный в классе RealPoint. Действительно, нет способа вызвать метод getX класса Point для экземпляра класса RealPoint извне тела RealPoint, независимо от типа переменной, которую мы можем использовать для хранения ссылки на объект. Таким образом, мы видим, что поведение полей и методов различается: скрытие отличается от переопределения.
Класс-член — это класс, объявление которого непосредственно заключено в теле другого класса или интерфейса (§8.1.6, §9.1.4).
Интерфейс-член — это интерфейс, объявление которого непосредственно заключено в теле другого класса или интерфейса (§8.1.6, §9.1.4).
Доступность типа члена в объявлении класса или интерфейса указана в §6.6.
Если один и тот же ключевой идентификатор появляется более одного раза в качестве модификатора для объявления типа члена в классе, это является ошибкой компиляции.
Область действия и перекрытие типа члена указаны в §6.3 и §6.4.
Если класс объявляет тип члена с определенным именем, то объявление этого типа считается скрывающим любые и все доступные объявления типов членов с тем же именем в суперклассах и суперинтерфейсах класса.
В этом отношении скрытие типов членов аналогично скрытию полей (§8.3).
Класс наследует от своего непосредственного суперкласса и непосредственных суперинтерфейсов все не-private типы членов суперкласса и суперинтерфейсов, которые доступны коду в классе и не скрыты объявлением в классе.
Класс может унаследовать два или более объявления типов с одинаковым именем, либо от двух интерфейсов, либо от своего суперкласса и интерфейса. Попытка обратиться к любому унаследованному классу или интерфейсу с неоднозначным именем является ошибкой компиляции.
Если одно и то же объявление типа наследуется из интерфейса по нескольким путям, класс или интерфейс считается унаследованным только один раз. К нему можно обратиться по его простому имени без неоднозначности.
Ключевое слово static может изменять объявление типа члена C в теле не-вложенного класса или интерфейса T. Его действие состоит в том, чтобы объявить, что C не является вложенным классом. Точно так же, как метод static T не имеет текущего экземпляра T в своем теле, C также не имеет текущего экземпляра T, и у него нет никаких лексически окружающих экземпляров.
Если класс static содержит использование не-static члена окружающего класса, это является ошибкой компиляции.
Интерфейс-член неявно static (§9.1.1). Допускается в объявлении интерфейса-члена избыточно указывать модификатор static.
Инициализатор экземпляра, объявленный в классе, выполняется при создании экземпляра класса (§12.5, §15.9, §8.8.7.1).
Если инициализатор экземпляра не может завершиться нормально, это является ошибкой компиляции (§14.21).
Если оператор 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.21).
Если оператор return (§14.17) появляется где-либо внутри статического инициализатора, это является ошибкой компиляции.
Если ключевое слово this (§15.8.3) или ключевое слово super (§15.11, §15.12) или любая переменная типа, объявленная вне статического инициализатора, появляется где-либо внутри статического инициализатора, это является ошибкой компиляции.
Использование переменных класса, объявления которых появляются текстово после использования, иногда ограничено, даже если эти переменные класса находятся в области видимости. См. §8.3.3 для точных правил, определяющих прямые ссылки на переменные класса.
Обработка исключений для статического инициализатора указана в §11.2.3.
Конструктор используется для создания объекта, являющегося экземпляром класса (§12.5, §15.9).
Правила в этом разделе применяются к конструкторам во всех объявлениях классов, включая объявления перечислений. Однако для объявлений перечислений действуют особые правила относительно модификаторов конструкторов, тел конструкторов и конструкторов по умолчанию; эти правила указаны в §8.9.2.
Простое имя типа (SimpleTypeName) в деклараторе конструктора (ConstructorDeclarator) должно быть простым именем класса, содержащего объявление конструктора, в противном случае произойдет ошибка компиляции.
Во всех других отношениях объявление конструктора выглядит точно так же, как объявление метода без результата (§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.
Если один и тот же ключевое слово встречается более одного раза в качестве модификатора в объявлении конструктора, это является ошибкой компиляции.
В нормальном объявлении класса объявление конструктора без модификаторов доступа имеет доступ к пакету.
Если в объявлении метода используются два или более (различных) модификаторов метода, то обычно (хотя это и не обязательно), они располагаются в том же порядке, что и в представленном выше правиле для MethodModifier.
В отличие от методов, конструктор не может быть abstract, static, final, native, strictfp или synchronized:
-
Конструктор не наследуется, поэтому нет необходимости объявлять его
final. -
Конструктор
abstractникогда не может быть реализован. -
Конструктор всегда вызывается относительно объекта, поэтому не имеет смысла объявлять конструктор
static. -
В практическом плане нет необходимости для конструктора быть
synchronized, потому что это заблокирует объект в процессе создания, что обычно недоступно другим потокам до тех пор, пока все конструкторы объекта не завершат свою работу. -
Отсутствие
nativeконструкторов — это произвольное решение в дизайне языка, что упрощает проверку реализации Java Virtual Machine на то, что конструкторы суперклассов всегда должным образом вызываются во время создания объекта. -
Невозможность объявить конструктор как
strictfp(в отличие от метода (§8.4.3)) — это преднамеренное решение в дизайне языка; это эффективно гарантирует, что конструктор является FP-строгим тогда и только тогда, когда его класс является FP-строгим (§15.4).
Конструктор является обобщенным, если он объявляет одну или несколько переменных типа (§4.4).
Эти переменные типа известны как параметры типа конструктора. Формат раздела параметров типа обобщенного конструктора идентичен разделу параметров типа обобщенного класса (§8.1.2).
Конструктор может быть обобщенным независимо от того, является ли класс, в котором он объявлен, обобщенным.
Объявление обобщенного конструктора определяет набор конструкторов, по одному для каждого возможного вызова раздела параметров типа аргументами типа. Аргументы типа могут не потребоваться явно при вызове обобщенного конструктора, так как они часто могут быть выведены (§18 (Выведение типов)).
Область действия и перекрытие параметров типа конструктора указаны в §6.3 и §6.4.
Оператор 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). Это может потребоваться, если суперкласс является вложенным классом.
-
Выражение явного вызова конструктора в теле конструктора не может ссылаться на переменные экземпляра или методы экземпляра или вложенные классы, объявленные в этом классе или любом суперклассе, или использовать this или super в любом выражении; в противном случае произойдет ошибка времени компиляции.
Это ограничение на использование текущего экземпляра объясняет, почему выражение явного вызова конструктора считается выполняющимся в статическом контексте (§8.1.3).
Если Типы аргументов присутствуют слева от this или super, то ошибка времени компиляции произойдет, если любой из типов аргументов является универсальными типами (§4.5.1).
Пусть C — класс, который создается, и пусть S — непосредственный суперкласс C.
Если выражение вызова конструктора суперкласса неквалифицировано, то:
-
Если S является вложенным членом класса, но S не является членом лексически окружающего объявления типа C, то возникает ошибка времени компиляции.
Если выражение вызова конструктора суперкласса квалифицировано, то:
-
Если S не является вложенным классом или если объявление S происходит в статическом контексте, то возникает ошибка времени компиляции.
-
В противном случае, пусть
p— это выражение Первичное выражение или Имя выражения, непосредственно предшествующее ".super", и пусть O — это непосредственно окружающий класс S. Ошибка времени компиляции произойдет, если типpне является O или подклассом O, или если типpнедоступен (§6.6).
Типы исключений, которые может вызвать выражение явного вызова конструктора, указаны в §11.2.2.
Вычисление выражения вызова альтернативного конструктора происходит путем сначала вычисления аргументов конструктора слева направо, как при обычном вызове метода, а затем вызова конструктора.
Вычисление выражения вызова конструктора суперкласса происходит следующим образом:
-
Пусть
i— это экземпляр, который создается. Необходимо определить ближайший внешний экземплярiотносительно S (если таковой имеется):-
Если S не является вложенным классом или если объявление S происходит в статическом контексте, то ближайшего внешнего экземпляра
iотносительно S не существует. -
Если вызов конструктора суперкласса неквалифицирован, то S обязательно является локальным классом или вложенным членом класса.
Пусть O — это непосредственно окружающий класс S, и пусть n — целое число такое, что O — это n'ый лексически окружающий тип объявления C.
Ближайший внешний экземпляр
iотносительно S — это n'ый лексически окружающий экземплярthis. -
Если вызов конструктора суперкласса квалифицирован, то выражение Первичное выражение или Имя выражения, непосредственно предшествующее "
.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, даже если такие ссылки не являются алиасами в традиционном смысле.
Перегрузка конструкторов идентична по поведению перегрузке методов (§8.4.9). Перегрузка разрешается во время компиляции для каждого выражения создания экземпляра класса (§15.9).
Если класс не содержит объявлений конструкторов, то неявно объявляется конструктор по умолчанию. Форма конструктора по умолчанию для внешнего класса, внутреннего класса-члена или локального класса такова:
-
Конструктор по умолчанию имеет тот же уровень доступа, что и класс (§6.6).
-
Конструктор по умолчанию не имеет формальных параметров, за исключением не-
privateвнутреннего класса-члена, где конструктор по умолчанию неявно объявляет один формальный параметр, представляющий немедленно окружающий экземпляр класса (§8.8.1, §15.9.2, §15.9.3). -
Конструктор по умолчанию не содержит блоков
throws. -
Если объявляемый класс является первоначальным классом
Object, то конструктор по умолчанию имеет пустое тело. В противном случае конструктор по умолчанию просто вызывает конструктор суперкласса без аргументов.
Форма конструктора по умолчанию для анонимного класса указана в §15.9.5.1.
Ошибка компиляции, если неявно объявлен конструктор по умолчанию, но у суперкласса нет доступного конструктора, принимающего ноль аргументов и не имеющего блока throws.
Пример 8.8.9-1. Конструкторы по умолчанию
Объявление:
public class Point {
int x, y;
}
эквивалентно объявлению:
public class Point {
int x, y;
public Point() { super(); }
}
где конструктор по умолчанию является public, потому что класс Point является public.
Пример 8.8.9-2. Доступность конструкторов и классов
Правило, что конструктор по умолчанию класса имеет тот же уровень доступа, что и сам класс, просто и интуитивно. Однако это не подразумевает, что конструктор доступен всякий раз, когда доступен класс. Рассмотрим:
package p1;
public class Outer {
protected class Inner {}
}
package p2;
class SonOfOuter extends p1.Outer {
void foo() {
new Inner(); // compile-time access error
}
}
Конструктор по умолчанию для Inner есть protected. Однако, конструктор является protected относительно Inner, в то время как Inner является protected относительно Outer. Таким образом, Inner доступен в SonOfOuter, так как он является подклассом Outer. Конструктор Inner не доступен в SonOfOuter, так как класс SonOfOuter не является подклассом Inner! Следовательно, даже если Inner доступен, его конструктор по умолчанию недоступен.
Класс может быть спроектирован таким образом, чтобы код за пределами объявления класса не мог создавать экземпляры класса, объявив хотя бы один конструктор, чтобы предотвратить создание конструктора по умолчанию, и объявив все конструкторы private.
Класс public также может предотвратить создание экземпляров за пределами своего пакета, объявив хотя бы один конструктор, чтобы предотвратить создание конструктора по умолчанию с доступом public, и не объявив ни одного конструктора с доступом public.
Пример 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" };
}
класс PackageOnly может быть создан только внутри пакета just, в котором он объявлен.
Объявление перечисления определяет новый тип перечисления, особый вид типа класса.
Если в объявлении перечисления есть модификатор abstract или final, это ошибка времени компиляции.
Объявление перечисления неявно final, если оно не содержит хотя бы одной константы перечисления с телом класса (§8.9.1).
Вложенный тип перечисления неявно static. Допускается избыточно указывать модификатор static при объявлении вложенного типа перечисления.
Это означает, что невозможно объявить тип перечисления внутри тела внутреннего класса (§8.1.3), так как внутренний класс не может иметь static членов, за исключением константных переменных.
Если один и тот же ключевой идентификатор встречается более одного раза как модификатор в объявлении перечисления, это ошибка времени компиляции.
Прямым суперклассом типа перечисления E является Enum<E> (§8.1.4).
Тип перечисления не имеет экземпляров, кроме тех, что определены его константами перечисления. Попытка явно создать экземпляр типа перечисления является ошибкой времени компиляции (§15.9.1).
Помимо ошибки времени компиляции, существуют три дополнительных механизма, гарантирующие, что экземпляры типа перечисления существуют только в виде его констант:
-
finalcloneметод вEnumгарантирует, что константы перечисления никогда не могут быть клонированы. -
Рефлексивное создание экземпляров типов перечислений запрещено.
-
Специальное обращение механизма сериализации гарантирует, что при десериализации никогда не создаются дубликаты экземпляров.
Тело объявления перечисления может содержать константы перечисления. Константа перечисления определяет экземпляр типа перечисления.
Ниже приводится фрагмент из §15.12 для удобства:
Правила применения аннотационных модификаторов к объявлению константы перечисления описаны в §9.7.4 и §9.7.5.
Идентификатор в константе перечисления может использоваться в имени для ссылки на эту константу.
Область действия и перекрытие константы перечисления указаны в §6.3 и §6.4.
За константой перечисления могут следовать аргументы, которые передаются в конструктор перечисления при создании константы во время инициализации класса, как описано далее в этом разделе. Вызываемый конструктор выбирается по стандартным правилам разрешения перегрузки (§15.12.2). Если аргументы опущены, предполагается пустой список аргументов.
Необязательное тело класса константы перечисления неявно определяет объявление анонимного класса (§15.9.5), который расширяет непосредственно окружающий тип перечисления. Тело класса подчиняется стандартным правилам для анонимных классов; в частности, оно не может содержать конструкторов. Методы экземпляра, объявленные в этих телах классов, могут вызываться за пределами окружающего типа перечисления только в том случае, если они переопределяют доступные методы в окружающем типе перечисления (§8.4.8).
Ошибка времени компиляции, если тело класса константы перечисления объявляет метод abstract.
Поскольку существует только один экземпляр каждой константы перечисления, разрешается использовать оператор == вместо метода equals при сравнении двух ссылок на объекты, если известно, что хотя бы одна из них ссылается на константу перечисления.
Метод equals в Enum - это final метод, который просто вызывает super.equals на своем аргументе и возвращает результат, выполняя сравнение по идентичности.
Помимо констант перечисления, тело объявления перечисления может содержать объявления конструкторов и членов, а также инициализаторы экземпляра и статические инициализаторы.
Следующие производства из §8.1.6 показаны здесь для удобства:
Любые объявления конструкторов или членов в теле объявления перечисления применяются к типу перечисления точно так же, как если бы они присутствовали в теле обычного объявления класса, если не указано иное.
Если в объявлении перечисления объявлен конструктор, он должен иметь доступность или public, или protected (§6.6).
Если в объявлении перечисления объявлен конструктор, он не может содержать оператор вызова конструктора суперкласса (§8.8.7.1).
Ссылка на static поле типа перечисления из конструкторов, инициализаторов экземпляров или выражений инициализаторов переменных экземпляра типа перечисления является ошибкой компиляции, если поле не является константой (§4.12.4).
В объявлении перечисления объявление конструктора без модификаторов доступа считается private.
В объявлении перечисления без объявлений конструкторов неявно объявлен конструктор по умолчанию. Конструктор по умолчанию private, не имеет формальных параметров и не имеет throws.
На практике компилятор, скорее всего, будет отражать тип Enum, объявляя String и int параметры в конструкторе по умолчанию типа перечисления. Однако эти параметры не указаны как "неявно объявленные", потому что разные компиляторы не обязаны соглашаться с формой конструктора по умолчанию. Только компилятор типа перечисления знает, как создать константы перечисления; другие компиляторы могут просто полагаться на неявно объявленные public static поля типа перечисления (§8.9.3), не заботясь о том, как эти поля были инициализированы.
Ошибка компиляции, если в объявлении перечисления E есть abstract метод m в качестве члена, если у E хотя бы одна константа перечисления, и у всех констант E есть тела классов, которые предоставляют конкретные реализации m.
Ошибка компиляции, если объявление перечисления объявляет финализатор (§12.6). Экземпляр типа перечисления никогда не может быть завершен.
Пример 8.9.2-1. Декларации тела перечисления
enum Coin {
PENNY(1), NICKEL(5), DIME(10), QUARTER(25);
Coin(int value) { this.value = value; }
private final int value;
public int value() { return value; }
}
Каждая константа перечисления обеспечивает различное значение в поле value, передаваемое через конструктор. Поле представляет собой значение американской монеты в цента. Обратите внимание, что нет ограничений на параметры, которые могут быть объявлены конструктором типа перечисления.
Пример 8.9.2-2. Ограничение на самоссылку константы перечисления
Без правила доступа к полю static, кажущийся разумный код потерпит неудачу во время выполнения из-за цикличности инициализации, присущей типам перечислений. (Цикличность существует в любом классе с полем static "типа самого себя".) Вот пример такого кода, который потерпит неудачу:
import java.util.Map;
import java.util.HashMap;
enum Color {
RED, GREEN, BLUE;
Color() { colorMap.put(toString(), this); }
static final Map<String,Color> colorMap =
new HashMap<String,Color>();
}
Статическая инициализация этого перечисления вызовет NullPointerException, потому что переменная static colorMap не инициализирована, когда выполняются конструкторы констант перечисления. Вышеупомянутое ограничение гарантирует, что такой код не может быть скомпилирован. Однако код можно легко переработать для правильной работы:
import java.util.Map;
import java.util.HashMap;
enum Color {
RED, GREEN, BLUE;
static final Map<String,Color> colorMap =
new HashMap<String,Color>();
static {
for (Color c : Color.values())
colorMap.put(c.toString(), c);
}
}
Переработанный вариант явно правильный, так как статическая инициализация выполняется сверху вниз.
Члены типа перечисления E состоят из следующего:
-
Члены, объявленные в теле объявления E.
-
Члены, унаследованные от
Enum<E>. -
Для каждой константы перечисления
c, объявленной в теле объявления E, у E есть неявно объявленноеpublicstaticfinalполе типа E с тем же именем, что и уc. Поле имеет инициализатор переменной, состоящий изc, и имеет те же аннотации, что и уc.Эти поля неявно объявляются в том же порядке, что и соответствующие константы перечисления, прежде чем любые
staticполя явно объявленные в теле объявления E.Константа перечисления считается созданной при инициализации соответствующего неявно объявленного поля.
-
Следующие неявно объявленные методы:
/** * Returns an array containing the constants of this enum * type, in the order they're declared. This method may be * used to iterate over the constants as follows: * * for(E c : E.values()) * System.out.println(c); * * @return an array containing the constants of this enum * type, in the order they're declared */ public static E[] values(); /** * Returns the enum constant of this type with the specified * name. * The string must match exactly an identifier used to declare * an enum constant in this type. (Extraneous whitespace * characters are not permitted.) * * @return the enum constant with the specified name * @throws IllegalArgumentException if this enum type has no * constant with the specified name */ public static E valueOf(String name);
Отсюда следует, что объявление типа перечисления E не может содержать полей, конфликтующих с неявно объявленными полями, соответствующими константам перечисления E, а также не может содержать методы, конфликтующие с неявно объявленными методами или переопределяющие final методы класса Enum<E>.
Пример 8.9.3-1. Итерирование по константам перечисления с циклом foreach
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. Константы перечисления с телами классов
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 в базовом типе (Operation), так как шаблон исключает возможность забыть добавить поведение для новой константы (так как объявление перечисления вызовет ошибку компиляции).
Пример 8.9.3-4. Несколько типов перечислений
В следующей программе класс игральной карты построен на основе двух простых перечислений.
import java.util.List;
import java.util.ArrayList;
class Card implements Comparable<Card>,
java.io.Serializable {
public enum Rank { DEUCE, THREE, FOUR, FIVE, SIX, SEVEN,
EIGHT, NINE, TEN,JACK, QUEEN, KING, ACE }
public enum Suit { CLUBS, DIAMONDS, HEARTS, SPADES }
private final Rank rank;
private final Suit suit;
public Rank rank() { return rank; }
public Suit suit() { return suit; }
private Card(Rank rank, Suit suit) {
if (rank == null || suit == null)
throw new NullPointerException(rank + ", " + suit);
this.rank = rank;
this.suit = suit;
}
public String toString() { return rank + " of " + suit; }
// Primary sort on suit, secondary sort on rank
public int compareTo(Card c) {
int suitCompare = suit.compareTo(c.suit);
return (suitCompare != 0 ?
suitCompare :
rank.compareTo(c.rank));
}
private static final List<Card> prototypeDeck =
new ArrayList<Card>(52);
static {
for (Suit suit : Suit.values())
for (Rank rank : Rank.values())
prototypeDeck.add(new Card(rank, suit));
}
// Returns a new deck
public static List<Card> newDeck() {
return new ArrayList<Card>(prototypeDeck);
}
}
Следующая программа использует класс Card. Она принимает два целых параметра в командной строке, представляющих количество раздач и количество карт в каждой раздаче:
import java.util.List;
import java.util.ArrayList;
import java.util.Collections;
class Deal {
public static void main(String args[]) {
int numHands = Integer.parseInt(args[0]);
int cardsPerHand = Integer.parseInt(args[1]);
List<Card> deck = Card.newDeck();
Collections.shuffle(deck);
for (int i=0; i < numHands; i++)
System.out.println(dealHand(deck, cardsPerHand));
}
/**
* Returns a new ArrayList consisting of the last n
* elements of deck, which are removed from deck.
* The returned list is sorted using the elements'
* natural ordering.
*/
public static <E extends Comparable<E>>
ArrayList<E> dealHand(List<E> deck, int n) {
int deckSize = deck.size();
List<E> handView = deck.subList(deckSize - n, deckSize);
ArrayList<E> hand = new ArrayList<E>(handView);
handView.clear();
Collections.sort(hand);
return hand;
}
}
Программа выводит:
java Deal 4 3 [DEUCE of CLUBS, SEVEN of CLUBS, QUEEN of DIAMONDS] [NINE of HEARTS, FIVE of SPADES, ACE of SPADES] [THREE of HEARTS, SIX of HEARTS, TEN of SPADES] [TEN of CLUBS, NINE of DIAMONDS, THREE of SPADES]
© Oracle and/or its affiliates. All rights reserved.
Licensed under the Oracle Technology Network License Agreement.