Глава 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.
TypeIdentifier в объявлении класса задаёт имя класса.
Ошибка компиляции, если класс имеет то же простое имя, что и любой из его окружающих классов или интерфейсов.
Область действия и перекрытие объявления класса указаны в §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), а не к классам верхнего уровня, локальным или анонимным.
Ошибка компиляции, если одно и то же ключевое слово появляется более одного раза в качестве модификатора для объявления класса, или если объявление класса имеет более одного из модификаторов доступа public, protected и private (§6.6).
Если в объявлении класса присутствует два или более (различных) модификатора класса, то принято, хотя и не обязательно, что они появляются в порядке, согласованном с показанным выше в производстве для 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 или фактически являться 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 или являются фактически 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, если такая фраза присутствует, или extends в противном случае.
Учитывая объявление обобщённого класса 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, если такая фраза присутствует.
Для обобщённого объявления класса 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 будет подкласс Point4d класса Point3d:
package morePoints;
public class Point4d extends Point3d {
public int w;
public void move(int dx, int dy, int dz, int dw) {
super.move(dx, dy, dz); w += dw;
}
}
то класс Point4d унаследует поле z, которое, будучи public, затем может быть доступно кодом в пакетах, отличных от morePoints, через переменные и выражения типа public типа Point4d.
Переменные типа класса объявляются с помощью объявлений полей.
Каждый дескриптор в ОбъявленииПоля объявляет одно поле. Идентификатор в дескрипторе может использоваться в имени для обращения к полю.
В одном ОбъявленииПоля может быть объявлено более одного поля с помощью нескольких дескрипторов; МодификаторыПоля и ТипБезАннотаций применяются ко всем дескрипторам в объявлении.
Раздел МодификаторыПоля описан в §8.3.1.
Объявленный тип поля обозначается ТипБезАннотаций, если в ТипБезАннотаций и ИдентификаторДескриптораПеременной нет пар скобок, и задаётся в §10.2 в противном случае.
Область видимости и перекрытие объявления поля указаны в §6.3 и §6.4.
Ошибка компиляции, если тело объявления класса объявляет два поля с одинаковым именем.
Если класс объявляет поле с определённым именем, то объявление этого поля называется скрытием всех и каждого доступных объявлений полей с тем же именем в суперклассах и суперинтерфейсах класса.
В этом отношении скрытие полей отличается от скрытия методов (§8.4.8.3), так как нет различия между static и не-static полями при скрытии полей, в то время как различие делается между static и не-static методами при скрытии методов.
К скрытому полю можно получить доступ, используя квалифицированное имя (§6.5.6.2), если оно static, или используя выражение доступа к полю, содержащее ключевое слово super (§15.11.2) или приведение к типу суперкласса.
В этом отношении скрытие полей аналогично скрытию методов.
Если объявление поля скрывает объявление другого поля, то два поля не обязательно должны иметь одинаковый тип.
Класс наследует от своего непосредственного суперкласса и непосредственных суперинтерфейсов все не-private поля суперкласса и суперинтерфейсов, которые доступны (§6.6) коду в классе и не скрыты объявлением в классе.
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.
Если одно и то же ключевое слово появляется более одного раза в качестве модификатора объявления поля, или если в объявлении поля присутствует более одного из модификаторов доступа public, protected и private (§6.6), то это ошибка времени компиляции.
Если в объявлении поля присутствует два или более (различных) модификатора полей, то принято, хотя и не обязательно, что они располагаются в порядке, соответствующем порядку, показанному выше в производстве для 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.
Пустая статическая переменная класса должна быть определенно инициализирована статической инициализацией класса, в котором она объявлена, в противном случае возникает ошибка компиляции (§8.7, §16.8).
Пустая переменная экземпляра должна быть определенно инициализирована, а также не должна быть определенно неинициализированной в конце каждого конструктора класса, в котором она объявлена, в противном случае возникает ошибка компиляции (§8.8, §16.9).
Переменные могут быть помечены как transient, чтобы указать, что они не являются частью постоянного состояния объекта.
Пример 8.3.1.3-1. Сохранение полей transient
Если экземпляр класса Point:
class Point {
int x, y;
transient float rho, theta;
}
был сохранен в постоянном хранилище службой системы, то сохранялись бы только поля x и y. Данная спецификация не определяет детали таких служб; см. спецификацию java.io.Serializable для примера такой службы.
Язык программирования Java позволяет потокам доступа к общим переменным (§17.1). Как правило, для обеспечения согласованного и надёжного обновления общих переменных поток должен гарантировать эксклюзивный доступ к этим переменным, получив блокировку, которая, как правило, обеспечивает взаимное исключение для этих общих переменных.
Язык программирования Java предоставляет второй механизм, volatile поля, который для некоторых целей удобнее, чем блокировка.
Поле может быть объявлено volatile, в этом случае модель памяти Java гарантирует, что все потоки видят согласованное значение переменной (§17.4).
Если переменная final также объявлена volatile, это ошибка времени компиляции.
Пример 8.3.1.4-1. volatile Поля
Если в следующем примере один поток многократно вызывает метод one (но не более Integer.MAX_VALUE раз в общем), а другой поток многократно вызывает метод two:
class Test {
static int i = 0, j = 0;
static void one() { i++; j++; }
static void two() {
System.out.println("i=" + i + " j=" + j);
}
}
то метод two иногда может вывести значение для j, которое больше значения i, так как пример не содержит синхронизации и, согласно правилам, описанным в §17.4, общие значения i и j могут быть обновлены в неправильном порядке.
Один из способов предотвратить такое поведение заключается в объявлении методов one и two как synchronized (§8.4.3.6):
class Test {
static int i = 0, j = 0;
static synchronized void one() { i++; j++; }
static synchronized void two() {
System.out.println("i=" + i + " j=" + j);
}
}
Это предотвращает одновременное выполнение метода one и метода two и гарантирует, что общие значения i и j обновляются до возвращения метода one. Таким образом, метод two никогда не увидит значение для j, большее, чем для i; на самом деле, он всегда наблюдает одинаковое значение для i и j.
Другой подход заключается в объявлении i и j как volatile:
class Test {
static volatile int i = 0, j = 0;
static void one() { i++; j++; }
static void two() {
System.out.println("i=" + i + " j=" + j);
}
}
Это позволяет методам one и two выполняться одновременно, но гарантирует, что обращения к общим значениям для i и j происходят ровно столько раз и в том же порядке, как они отображаются во время выполнения программы каждым потоком. Следовательно, общее значение для j никогда не будет больше, чем для i, потому что каждое обновление i должно отражаться в общем значении i до обновления j. Однако возможно, что любое конкретное обращение к методу two может наблюдать значение для j, намного большее, чем значение, наблюдавшееся для i, так как метод one может быть вызван многократно между моментом, когда метод two получает значение i, и моментом, когда метод два получает значение j.
См. §17.4 для более подробного обсуждения и примеров.
Если в объявлении поля у оператора есть инициализатор переменной, то у этого оператора есть семантика присваивания (§15.26) объявленной переменной.
Если оператор относится к переменной класса (то есть к static полю), то к его инициализатору применяются следующие правила:
-
Ошибка времени компиляции, если в инициализаторе используется ссылка по простому имени на любую переменную экземпляра.
-
Ошибка времени компиляции, если в инициализаторе встречается ключевое слово
this(§15.8.3) или ключевое словоsuper(§15.11.2, §15.12). -
Во время выполнения инициализатор вычисляется и присваивание выполняется ровно один раз при инициализации класса (§12.4.2).
Обратите внимание, что
staticполя, которые являются константами (§4.12.4), инициализируются до другихstaticполей (§12.4.2). Это также относится к интерфейсам (§9.3.1). При обращении к таким полям по простому имени они никогда не будут наблюдать свои значения по умолчанию (§4.12.5).
Если оператор относится к переменной экземпляра (то есть полю, которое не является static), то к его инициализатору применяются следующие правила:
-
Инициализатор может ссылаться по простому имени на любую переменную класса, объявленную в классе или унаследованную от него, даже на ту, чьё объявление находится справа от инициализатора (§3.5).
-
Инициализатор может ссылаться на текущий объект, используя ключевое слово
this(§15.8.3) или ключевое словоsuper(§15.11.2, §15.12). -
При выполнении инициализатор вычисляется и присваивание выполняется каждый раз при создании экземпляра класса (§12.5).
Ссылки из инициализаторов переменных на поля, которые могут ещё не быть инициализированы, подчиняются дополнительным ограничениям, как указано в §8.3.3 и §16 (Определённое присваивание).
Проверка исключений для инициализатора переменной в объявлении поля указана в §11.2.3.
Инициализаторы переменных также используются в объявлениях локальных переменных (§14.4), где инициализатор вычисляется и присваивание выполняется каждый раз при выполнении объявления локальной переменной.
Пример 8.3.2-1. Инициализация полей
class Point {
int x = 1, y = 5;
}
class Test {
public static void main(String[] args) {
Point p = new Point();
System.out.println(p.x + ", " + p.y);
}
}
Эта программа выводит:
1, 5
потому что присваивания x и y происходят всякий раз при создании нового Point.
Пример 8.3.2-2. Вперёд ссылка на переменную класса
class Test {
float f = j;
static int j = 1;
}
Эта программа компилируется без ошибок; она инициализирует j значением 1 при инициализации класса Test и инициализирует f текущим значением j каждый раз при создании экземпляра класса Test.
Ссылки на поле иногда ограничены, даже если поле находится в области видимости. Следующие правила ограничивают ссылки на поле вперёд (где текстовое использование предшествует объявлению поля), а также ссылки на само поле (где поле используется в своём собственном инициализаторе).
Для ссылки по простому имени на переменную класса f, объявленную в классе или интерфейсе C, это ошибка времени компиляции, если:
-
Ссылка появляется либо в инициализаторе переменной класса C, либо в статическом инициализаторе C (§8.7); и
-
Ссылка появляется либо в инициализаторе объявляемого
f, либо в точке слева от объявляемогоf; и -
Ссылка не находится в левой части выражения присваивания (§15.26); и
-
Внутренний класс или интерфейс, содержащий ссылку, — C.
Для ссылки по простому имени на переменную экземпляра f, объявленную в классе C, это ошибка времени компиляции, если:
-
Ссылка появляется либо в инициализаторе переменной экземпляра C, либо в инициализаторе экземпляра C (§8.6); и
-
Ссылка появляется в инициализаторе объявляемого
fили в точке слева от объявляемогоf; и -
Ссылка не находится в левой части выражения присваивания (§15.26); и
-
Внутренний класс, содержащий ссылку, — C.
Пример 8.3.3-1. Ограничения на ссылки на поля
Для этой программы возникает ошибка времени компиляции:
class Test1 {
int i = j; // compile-time error:
// incorrect forward reference
int j = 1;
}
в то время как следующая программа компилируется без ошибок:
class Test2 {
Test2() { k = 2; }
int j = 1;
int i = j;
int k;
}
даже несмотря на то, что конструктор для Test2 (§8.8) ссылается на поле k, которое объявлено на три строки ниже.
Вышеуказанные ограничения разработаны для обнаружения во время компиляции циклических или неправильных инициализаций. Таким образом, как:
class Z {
static int i = j + 2;
static int j = 4;
}
так и:
class Z {
static { i = j + 2; }
static int i, j;
static { j = 4; }
}
приводят к ошибкам времени компиляции. Доступы с помощью методов не проверяются таким образом, поэтому:
class Z {
static int peek() { return j; }
static int i = peek();
static int j = 1;
}
class Test {
public static void main(String[] args) {
System.out.println(Z.i);
}
}
выводит:
0
потому что инициализатор переменной для i использует метод класса peek для доступа к значению переменной j до того, как j была инициализирована своим инициализатором переменной, в этот момент она всё ещё имеет своё значение по умолчанию (§4.12.5).
Более сложный пример:
class UseBeforeDeclaration {
static {
x = 100;
// ok - assignment
int y = x + 1;
// error - read before declaration
int v = x = 3;
// ok - x at left hand side of assignment
int z = UseBeforeDeclaration.x * 2;
// ok - not accessed via simple name
Object o = new Object() {
void foo() { x++; }
// ok - occurs in a different class
{ x++; }
// ok - occurs in a different class
};
}
{
j = 200;
// ok - assignment
j = j + 1;
// error - right hand side reads before declaration
int k = j = j + 1;
// error - illegal forward reference to j
int n = j = 300;
// ok - j at left hand side of assignment
int h = j++;
// error - read before declaration
int l = this.j * 3;
// ok - not accessed via simple name
Object o = new Object() {
void foo(){ j++; }
// ok - occurs in a different class
{ j = j + 1; }
// ok - occurs in a different class
};
}
int w = x = 3;
// ok - x at left hand side of assignment
int p = x;
// ok - instance initializers may access static fields
static int u =
(new Object() { int bar() { return x; } }).bar();
// ok - occurs in a different class
static int x;
int m = j = 4;
// ok - j at left hand side of assignment
int o =
(new Object() { int bar() { return j; } }).bar();
// ok - occurs in a different class
int j;
}
Метод объявляет исполняемый код, который можно вызвать, передавая фиксированное количество значений в качестве аргументов.
Следующее производство из §4.3 показано здесь для удобства:
Раздел Список формальных параметров описан в §8.4.1, раздел Методический модификатор — в §8.4.3, раздел Параметры типа — в §8.4.4, раздел Результат — в §8.4.5, раздел Исключения — в §8.4.6, а раздел Тело метода — в §8.4.7.
Идентификатор в Деклараторе метода может использоваться в имени для ссылки на метод (§6.5.7.1, §15.12).
Область видимости и перекрытие объявления метода указаны в §6.3 и §6.4.
Параметр получателя — это необязательный синтаксический элемент для метода экземпляра или конструктора внутреннего класса. Для метода экземпляра параметр получателя представляет собой объект, для которого вызывается метод. Для конструктора внутреннего класса параметр получателя представляет собой непосредственно окружающий экземпляр только что созданного объекта. В обоих случаях параметр получателя существует исключительно для того, чтобы в исходном коде можно было обозначить тип представленного объекта, чтобы этот тип мог быть аннотирован (§9.7.4). Параметр получателя не является формальным параметром; точнее, он не является объявлением какого-либо вида переменной (§4.12.3), он никогда не привязывается к какому-либо значению, передаваемому в качестве аргумента в выражении вызова метода или выражении создания экземпляра класса, и он не оказывает никакого влияния во время выполнения.
Параметр получателя может появляться либо в Деклараторе метода метода экземпляра, либо в Деклараторе конструктора конструктора внутреннего класса, где внутренний класс не объявлен в статическом контексте (§8.1.3). Если параметр получателя появляется в любом другом типе метода или конструктора, возникает ошибка компиляции.
Тип и имя параметра получателя ограничены следующим образом:
-
В методе экземпляра тип параметра получателя должен быть классом или интерфейсом, в котором объявлен метод, а имя параметра получателя должно быть
this; в противном случае возникает ошибка компиляции. -
В конструкторе внутреннего класса тип параметра получателя должен быть классом или интерфейсом, являющимся непосредственно окружающим объявлением типа внутреннего класса, а имя параметра получателя должно быть Идентификатор
.this, где Идентификатор — простое имя класса или интерфейса, являющегося непосредственно окружающим объявлением типа внутреннего класса; в противном случае возникает ошибка компиляции.
Ошибка компиляции возникает, если тело объявления класса объявляет в качестве членов два метода с эквивалентными подписями для переопределения (§8.4.2).
Объявление метода, возвращающего массив, может разместить некоторые или все пары скобок, обозначающие тип массива, после списка формальных параметров. Этот синтаксис поддерживается для совместимости со старыми версиями языка программирования Java. Очень настоятельно рекомендуется не использовать этот синтаксис в новом коде.
Формальные параметры метода или конструктора, если таковые имеются, указываются списком параметров, разделённых запятыми. Каждый параметр состоит из типа (при необходимости, предваряемого модификатором final и/или одним или несколькими аннотациями) и идентификатора (при необходимости, за которым следуют квадратные скобки), определяющего имя параметра.
Если у метода или конструктора нет формальных параметров и нет параметра получателя, то в объявлении метода или конструктора отображается пустая пара скобок.
Следующие правила из §8.3 и §4.3 приведены здесь для удобства:
Формальный параметр метода или конструктора может быть параметром с переменным числом аргументов, который указывается эллипсами после типа. В методе или конструкторе разрешается не более одного параметра с переменным числом аргументов. Если параметр с переменным числом аргументов встречается не на последнем месте в списке параметров, это является ошибкой компиляции.
В грамматике для VariableArityParameter обратите внимание, что эллипсы (...) — это самостоятельный токен (§3.11). Разрешается вставлять пробелы между эллипсами и типом, но с точки зрения стиля это не рекомендуется.
Если последний формальный параметр является параметром с переменным числом аргументов, то метод является методом с переменным числом аргументов. В противном случае это метод с фиксированным числом аргументов.
Правила аннотаций-модификаторов в объявлении формального параметра и в объявлении параметра получателя задаются в §9.7.4 и §9.7.5.
Ошибка компиляции возникает, если final используется более одного раза как модификатор в объявлении формального параметра.
Область действия и затенение формального параметра определяются в §6.3 и §6.4.
Ошибка компиляции возникает, если метод или конструктор объявляют два формальных параметра с одинаковым именем. (То есть, в их объявлениях упоминается тот же Identifier.)
Ошибка компиляции возникает, если формальный параметр, объявленный final, присваивается внутри тела метода или конструктора.
Объявленный тип формального параметра зависит от того, является ли он параметром с переменным числом аргументов:
-
Если формальный параметр не является параметром с переменным числом аргументов, то объявленный тип обозначается UnannType, если в UnannType и VariableDeclaratorId отсутствуют квадратные скобки, и определяется в §10.2 в противном случае.
-
Если формальный параметр является параметром с переменным числом аргументов, то объявленный тип — это тип массива, определённый в §10.2.
Если объявленный тип параметра с переменным числом аргументов имеет нереализуемый элементный тип (§4.7), то при объявлении метода с переменным числом аргументов возникает предупреждение компилятора, если метод не аннотирован @SafeVarargs (§9.6.4.7) или предупреждение не подавляется с помощью @SuppressWarnings (§9.6.4.5).
При вызове метода или конструктора (§15.12) значения выражений фактических аргументов инициализируют вновь созданные переменные параметров, каждый из которых имеет объявленный тип, перед выполнением тела метода или конструктора. Identifier, который появляется в FormalParameter, может использоваться как простое имя в теле метода или конструктора для ссылки на формальный параметр.
Вызовы метода с переменным числом аргументов могут содержать больше выражений фактических аргументов, чем формальных параметров. Все выражения фактических аргументов, которые не соответствуют формальным параметрам, предшествующим параметру с переменным числом аргументов, будут вычислены, и результаты будут сохранены в массиве, который будет передан в вызов метода (§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 показывают, что тип параметра получателя может быть обозначен квалифицированным TypeName, как и любой другой тип; но имя параметра получателя в конструкторе вложенного класса должно использовать простое имя внешнего класса.
Два метода или конструктора, M и N, имеют одинаковую подпись, если у них одинаковое имя, одинаковые параметры типа (если есть) (§8.4.4), и, после адаптации формальных типов параметров N к параметрам типа M, одинаковые формальные типы параметров.
Подпись метода m1 является подписью-подмножеством подписи метода m2, если выполняется одно из следующих условий:
-
m2имеет ту же подпись, что иm1, или -
подпись
m1совпадает с стёртой (§4.6) подписьюm2.
Две подписи методов m1 и m2 являются эквивалентными для переопределения, если либо m1 является подписью-подмножеством m2, либо m2 является подписью-подмножеством m1.
Объявление двух методов с эквивалентными для переопределения подписями в классе является ошибкой компиляции.
Пример 8.4.2-1. Эквивалентные для переопределения подписи
class Point {
int x, y;
abstract void move(int dx, int dy);
void move(int dx, int dy) { x += dx; y += dy; }
}
Эта программа вызывает ошибку компиляции, так как она объявляет два move метода с одинаковой (и, следовательно, эквивалентной для переопределения) подписью. Это ошибка, даже если одно из объявлений является abstract.
Понятие подписи-подмножества призвано выразить отношение между двумя методами, подписи которых не идентичны, но один может переопределить другой. В частности, оно позволяет методу, подпись которого не использует обобщённые типы, переопределять любую обобщённую версию этого метода. Это важно, чтобы разработчики библиотек могли свободно обобщать методы независимо от клиентов, которые определяют подклассы или подинтерфейсы библиотеки.
Рассмотрим пример:
class CollectionConverter {
List toList(Collection c) {...}
}
class Overrider extends CollectionConverter {
List toList(Collection c) {...}
}
Теперь предположим, что этот код был написан до появления обобщений, и сейчас автор класса CollectionConverter решает обобщить код, таким образом:
class CollectionConverter {
<T> List<T> toList(Collection<T> c) {...}
}
Без специального разрешения Overrider.toList больше не будет переопределять CollectionConverter.toList. Вместо этого код будет незаконным. Это существенно затруднило бы использование обобщений, поскольку авторы библиотек были бы не склонны мигрировать существующий код.
Правила применения модификаторов аннотаций в объявлении метода указаны в §9.7.4 и §9.7.5.
Ошибка времени компиляции, если одно и то же ключевое слово используется более одного раза в качестве модификатора в объявлении метода, или если в объявлении метода присутствует более одного из модификаторов доступа public, protected и private (§6.6).
Ошибка времени компиляции, если в объявлении метода присутствует ключевое слово abstract, а также любое из ключевых слов private, static, final, native, strictfp или synchronized.
Ошибка времени компиляции, если в объявлении метода присутствует ключевое слово native, а также strictfp.
Если в объявлении метода присутствуют два или более (различных) модификатора метода, то по соглашению, хотя и не обязательно, они должны располагаться в порядке, соответствующем показанному выше в продукции для MethodModifier.
Объявление abstract метода вводит метод как член, задавая его сигнатуру (§8.4.2), результат (§8.4.5), и throws раздел, если есть (§8.4.6), но не предоставляет реализации (§8.4.7). Метод, который не является abstract, может называться конкретным методом.
Объявление abstract метода m должно находиться непосредственно внутри abstract класса (назовем его A), если это не происходит в объявлении перечисления (§8.9); в противном случае возникает ошибка времени компиляции.
Каждый подкласс A, который не является abstract (§8.1.1.1), должен предоставить реализацию для m; в противном случае возникает ошибка времени компиляции.
abstract класс может переопределять abstract метод, предоставив объявление другого abstract метода.
Это может обеспечить место для добавления документационного комментария, уточнения типа возвращаемого значения или указания того, что набор проверяемых исключений, которые могут быть выброшены этим методом, когда он реализуется его подклассами, должен быть ограничен.
Метод экземпляра, который не является abstract, может быть переопределен abstract методом.
Пример 8.4.3.1-1. Абстрактный/Абстрактное переопределение метода
class BufferEmpty extends Exception {
BufferEmpty() { super(); }
BufferEmpty(String s) { super(s); }
}
class BufferError extends Exception {
BufferError() { super(); }
BufferError(String s) { super(s); }
}
interface Buffer {
char get() throws BufferEmpty, BufferError;
}
abstract class InfiniteBuffer implements Buffer {
public abstract char get() throws BufferError;
}
Объявление метода переопределения get в классе InfiniteBuffer указывает, что метод get в любом подклассе InfiniteBuffer никогда не выбросит исключение BufferEmpty, предположительно потому, что он генерирует данные в буфере и, следовательно, никогда не может исчерпать данные.
Пример 8.4.3.1-2. Абстрактный/Не-абстрактное переопределение
Мы можем объявить abstract класс Point, который требует, чтобы его подклассы реализовали toString, если они должны быть завершенными, создаваемыми экземплярами классами:
abstract class Point {
int x, y;
public abstract String toString();
}
Это abstract объявление toString переопределяет метод toString класса Object, который не является abstract. (Класс Object — неявной непосредственный суперкласс класса Point). Добавление кода:
class ColoredPoint extends Point {
int color;
public String toString() {
return super.toString() + ": color " + color; // error
}
}
приведет к ошибке компиляции, потому что вызов super.toString() относится к методу toString в классе Point, который является abstract, и поэтому не может быть вызван. Метод toString класса Object может быть доступен классу ColoredPoint только в том случае, если класс Point явно делает его доступным через какой-либо другой метод, как в:
abstract class Point {
int x, y;
public abstract String toString();
protected String objString() { return super.toString(); }
}
class ColoredPoint extends Point {
int color;
public String toString() {
return objString() + ": color " + color; // correct
}
}
Метод, объявленный static, называется методом класса.
Ошибка времени компиляции, если используется имя параметра типа любой окружающего объявления в заголовке или теле статического метода.
Статический метод всегда вызывается без ссылки на конкретный объект. Ошибка времени компиляции, если происходит попытка сослаться на текущий объект с использованием ключевого слова 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).
Ошибка компиляции возникает, если Тип исключения, указанный в операторе throws, не является подтипом (§4.10) типа исключения.
Переменные типов допускаются в операторе throws, хотя они не допускаются в операторе try (§14.20).
Допускается, но не обязательно указывать классы необрабатываемых исключений (§11.1.1) в операторе throws.
Связь между оператором throws и проверкой исключений для тела метода или конструктора описана в §11.2.3.
В сущности, для каждого обрабатываемого исключения, которое может возникнуть в результате выполнения тела метода или конструктора, возникает ошибка компиляции, если его тип исключения или его супертип не указан в операторе throws в объявлении метода или конструктора.
Требование объявлять обрабатываемые исключения позволяет компилятору Java гарантировать, что код для обработки таких условий ошибки был включен. Методы или конструкторы, которые не обрабатывают исключительные ситуации, выброшенные как обрабатываемые исключения в их телах, обычно вызывают ошибки компиляции, если им не хватает соответствующих типов исключений в операторах throws. Язык программирования Java таким образом поощряет стиль программирования, где редкие и по-настоящему исключительные условия документируются таким образом.
Связь между оператором throws метода и операторами throws переопределенных или скрытых методов указана в §8.4.8.3.
Пример 8.4.6-1. Переменные типов как типы исключений
import java.io.FileNotFoundException;
interface PrivilegedExceptionAction<E extends Exception> {
void run() throws E;
}
class AccessController {
public static <E extends Exception>
Object doPrivileged(PrivilegedExceptionAction<E> action) throws E {
action.run();
return "success";
}
}
class Test {
public static void main(String[] args) {
try {
AccessController.doPrivileged(
new PrivilegedExceptionAction<FileNotFoundException>() {
public void run() throws FileNotFoundException {
// ... delete a file ...
}
});
} catch (FileNotFoundException f) { /* Do something */ }
}
}
Тело метода — это либо блок кода, реализующий метод, либо просто точка с запятой, указывающая на отсутствие реализации.
Тело метода должно быть точкой с запятой, если метод является abstract или native (§8.4.3.1, §8.4.3.4). Более точно:
-
Ошибка компиляции возникает, если объявление метода является либо
abstract, либоnativeи имеет блок в качестве тела. -
Ошибка компиляции возникает, если объявление метода не является
abstractи не являетсяnativeи имеет точку с запятой в качестве тела.
Если для метода, объявленного void, должна быть предоставлена реализация, но реализация не требует исполняемого кода, тело метода должно быть написано как блок, не содержащий операторов: "{ }".
Правила для return операторов в теле метода указаны в §14.17.
Если метод объявлен с типом возвращаемого значения (§8.4.5), то ошибка компиляции возникает, если тело метода может завершиться нормально (§14.1).
Другими словами, метод с типом возвращаемого значения должен возвращать значение только с помощью оператора return, который предоставляет возвращаемое значение; метод не может "выпасть из своего тела". См. §14.17 для точных правил о return операторах в теле метода.
Метод может иметь тип возвращаемого значения и при этом не содержать return операторов. Вот один пример:
class DizzyDean {
int pitch() { throw new RuntimeException("90 mph?!"); }
}
Класс C наследует от своего непосредственного суперкласса все конкретные методы m (как static, так и экземпляра) суперкласса, для которых выполняются все следующие условия:
-
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' (§8.4.8.1, §9.4.1.1) объявление методаm.
Класс не наследует private или static методы от своих суперинтерфейсов.
Обратите внимание, что методы переопределяются или скрываются по сигнатурам. Если, например, класс объявляет два public метода с одинаковым именем (§8.4.9), и подкласс переопределяет один из них, подкласс по-прежнему наследует другой метод.
Пример 8.4.8-1. Наследование
interface I1 {
int foo();
}
interface I2 {
int foo();
}
abstract class Test implements I1, I2 {}
Здесь класс abstract Test наследует метод abstract foo от интерфейса I1, а также метод abstract foo от интерфейса I2. Ключевой вопрос при определении наследования foo от I1: переопределяет ли метод foo в I2 метод I2 от foo в I1? Нет, потому что I1 и I2 не являются подинтерфейсами друг друга. Таким образом, с точки зрения класса Test, наследование foo от I1 не ограничено; аналогично для наследования foo от I2. Согласно §8.4.8.4, класс Test может наследовать оба метода foo; очевидно, он должен быть объявлен abstract или переопределить оба abstract foo метода с помощью конкретного метода.
Обратите внимание, что унаследованный конкретный метод может препятствовать наследованию abstract или метода по умолчанию. (Конкретный метод будет переопределять abstract или метод по умолчанию "от C", согласно §8.4.8.1 и §9.4.1.1.) Кроме того, один метод супертипа может препятствовать наследованию другого метода супертипа, если первый "уже" переопределяет второй — это то же правило, что и для интерфейсов (§9.4.1), и предотвращает конфликты, при которых наследуется несколько методов по умолчанию, и одна реализация явно должна заменить другую.
Метод экземпляра mC, объявленный в классе или унаследованный классом C, переопределяет метод C другой метод mA, объявленный в классе A, если выполняются все следующие условия:
-
C является подклассом A.
-
C не наследует
mA. -
Подпись
mCявляется подподписью (§8.4.2) подписиmA. -
Выполняется одно из следующих условий:
-
mAявляетсяpublic. -
mAявляетсяprotected. -
mAобъявлен с доступом по умолчанию в том же пакете, что и C, и либо C объявляетmC, либоmAявляется членом непосредственного суперкласса C. -
mAобъявлен с доступом по умолчанию, иmCпереопределяетmAиз какого-либо суперкласса C. -
mAобъявлен с доступом по умолчанию, иmCпереопределяет методm'из C (m'отличен отmCиmA), так чтоm'переопределяетmAиз какого-либо суперкласса C.
-
Если mC является не-abstract и переопределяет из C метод abstract mA, то mC считается реализующим mA из C.
Если переопределяемый метод, mA, является методом static, это ошибка времени компиляции.
В этом отношении переопределение методов отличается от скрытия полей (§8.3), поскольку разрешается, чтобы переменная экземпляра скрывала переменную static.
Метод экземпляра mC, объявленный в классе или унаследованный классом C, переопределяет метод C другой метод mI, объявленный в интерфейсе I, если выполняются все следующие условия:
-
I является суперинтерфейсом C.
-
mIне являетсяstatic. -
C не наследует
mI. -
Подпись
mCявляется подподписью (§8.4.2) подписиmI. -
mIявляетсяpublic.
Подпись переопределяемого метода может отличаться от переопределяемого метода, если формальный параметр в одном из методов имеет необработанный тип, а соответствующий параметр в другом — параметризованный тип. Это позволяет мигрировать существующий код для использования возможностей дженериков.
Понятие переопределения включает методы, которые переопределяют другой метод из какого-либо подкласса своего объявляющего класса. Это может происходить двумя способами:
-
Конкретный метод в дженерическом суперклассе может при определенных параметризациях иметь такую же подпись, как абстрактный метод в том же классе. В этом случае конкретный метод наследуется, а
abstractметод не (как описано выше). Наследованный метод должен считаться переопределяющим свой абстрактный аналог из C. (Эта ситуация усложняется доступом по умолчанию: если C находится в другом пакете, тоmAне был бы унаследован и не должен считаться переопределенным.) -
Метод, унаследованный от класса, может переопределять метод суперинтерфейса. (К счастью, доступ по умолчанию здесь не играет роли.)
Переопределяемый метод может быть вызван с помощью выражения вызова метода (§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', где сигнатура m является подсигнатурой (§8.4.2) сигнатуры m', в суперклассах и суперинтерфейсах C, которые в противном случае были бы доступны (§6.6) коду в 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 должен быть заменим на тип возвращаемого значения d2 (§8.4.5), в противном случае возникает ошибка компиляции.
Это правило позволяет использовать ковариантные типы возвращаемых значений — уточнение типа возвращаемого значения метода при его переопределении.
Если R1 не является подтипом R2, то возникает неустранимая ошибка компиляции, если её не подавить с помощью @SuppressWarnings (§9.6.4.5).
Метод, который переопределяет или скрывает другой метод, включая методы, реализующие методы abstract, определённые в интерфейсах, не может объявлять более проверочных исключений, чем переопределённый или скрытый метод.
В этом отношении переопределение методов отличается от скрытия полей (§8.3), так как допускается скрытие поля одного типа другим типом.
Более точно, предположим, что B — это класс или интерфейс, а A — суперкласс или суперинтерфейс B, и объявление метода m2 в B переопределяет или скрывает объявление метода m1 в A. Тогда:
-
Если
m2содержитthrows-описания, которые упоминают типы проверочных исключений, тоm1должен содержатьthrows-описание, иначе произойдёт ошибка компиляции. -
Для каждого типа проверочного исключения, перечисленного в
throws-описанииm2, тот же класс исключения или один из его супертипов должен встречаться в стирании (§4.6)throws-описанияm1; в противном случае произойдёт ошибка компиляции. -
Если нестираемое
throws-описаниеm1не содержит супертип каждого типа исключения вthrows-описанииm2(при необходимости адаптированный к параметрам типаm1), то возникает неустранимая ошибка компиляции, если её не подавить с помощью@SuppressWarnings(§9.6.4.5).
Ошибка компиляции возникает, если объявление типа T содержит член-метод m1 и существует метод m2, объявленный в T или его супертипе, при выполнении следующих условий:
-
m1иm2имеют одинаковое имя. -
m2доступен (§6.6) из T. -
Подпись
m1не является подподписью (§8.4.2) подписиm2. -
Подпись
m1или какой-либо методm1, который он (прямо или косвенно) переопределяет, имеет такое же стирание, как подписьm2или какой-либо методm2, который он (прямо или косвенно) переопределяет.
Эти ограничения необходимы, потому что дженерики реализуются через стирание. Вышеприведённое правило подразумевает, что методы, объявленные в одном классе с одинаковым именем, должны иметь различные стирания. Также оно подразумевает, что объявление типа не может реализовать или расширить два различных вызова одного и того же дженерик-интерфейса.
Модификатор доступа переопределяемого или скрываемого метода должен обеспечивать не меньше доступа, чем переопределяемый или скрываемый метод, как показано ниже:
-
Если переопределяемый или скрываемый метод является
public, то переопределяемый или скрываемый метод должен бытьpublic; в противном случае произойдёт ошибка компиляции. -
Если переопределяемый или скрываемый метод является
protected, то переопределяемый или скрываемый метод должен бытьprotectedилиpublic; в противном случае произойдёт ошибка компиляции. -
Если переопределяемый или скрываемый метод имеет доступ пакета, то переопределяемый или скрываемый метод не должен быть
private; в противном случае произойдёт ошибка компиляции.
Обратите внимание, что метод private не может быть переопределён или скрыт в техническом смысле этих терминов. Это означает, что подкласс может объявить метод с той же подписью, что и метод private в одном из своих суперклассов, и нет требования, чтобы тип возвращаемого значения или throws-описание такого метода имели какую-либо связь с типом возвращаемого значения или private-описанием метода в суперклассе.
Пример 8.4.8.3-1. Ковариантные типы возвращаемых значений
Следующие объявления являются законными в языке программирования Java начиная с Java SE 5.0:
class C implements Cloneable {
C copy() throws CloneNotSupportedException {
return (C)clone();
}
}
class D extends C implements Cloneable {
D copy() throws CloneNotSupportedException {
return (D)clone();
}
}
Расслабленное правило переопределения также позволяет ослабить условия для абстрактных классов, реализующих интерфейсы.
Пример 8.4.8.3-2. Неустранимая ошибка от типа возвращаемого значения
Рассмотрим:
class StringSorter {
// turns a collection of strings into a sorted list
List toList(Collection c) {...}
}
и предположим, что кто-то создаёт подкласс StringSorter:
class Overrider extends StringSorter {
List toList(Collection c) {...}
}
Теперь в какой-то момент автор StringSorter решает обобщить код:
class StringSorter {
// turns a collection of strings into a sorted list
List<String> toList(Collection<String> c) {...}
}
При компиляции Overrider по новому определению StringSorter возникнет неустранимая ошибка, так как тип возвращаемого значения Overrider.toList — List, который не является подтипом типа возвращаемого значения переопределённого метода List<String>.
Пример 8.4.8.3-3. Неправильное переопределение из-за throws
Эта программа использует обычную и общепринятую форму объявления нового типа исключения в своём объявлении класса BadPointException:
class BadPointException extends Exception {
BadPointException() { super(); }
BadPointException(String s) { super(s); }
}
class Point {
int x, y;
void move(int dx, int dy) { x += dx; y += dy; }
}
class CheckedPoint extends Point {
void move(int dx, int dy) throws BadPointException {
if ((x + dx) < 0 || (y + dy) < 0)
throw new BadPointException();
x += dx; y += dy;
}
}
Программа приводит к ошибке компиляции, потому что переопределение метода move в классе CheckedPoint объявляет, что оно будет выбрасывать проверочное исключение, которое move в классе Point не объявил. Если это не считалось ошибкой, вызывающий метод move по ссылке типа Point мог обнаружить нарушение соглашения между ним и Point, если это исключение было выбрашено.
Удаление throws-описания не помогает:
class CheckedPoint extends Point {
void move(int dx, int dy) {
if ((x + dx) < 0 || (y + dy) < 0)
throw new BadPointException();
x += dx; y += dy;
}
}
Теперь возникает другая ошибка компиляции, так как тело метода move не может сгенерировать проверочное исключение BadPointException, которое не появляется в throws-описании move.
Пример 8.4.8.3-4. Стирание влияет на переопределение
Класс не может иметь два члена-метода с одинаковым именем и стиранием типа:
class C<T> {
T id (T x) {...}
}
class D extends C<String> {
Object id(Object x) {...}
}
Это незаконно, так как D.id(Object) — член D, C<String>.id(String) объявлен в супертипе D, и:
-
Два метода имеют одинаковое имя,
id -
C<String>.id(String)доступен изD -
Подпись
D.id(Object)не является подподписью подписиC<String>.id(String) -
Два метода имеют одинаковое стирание
Два разных метода одного класса не могут переопределять методы с одинаковым стиранием:
class C<T> {
T id(T x) {...}
}
interface I<T> {
T id(T x);
}
class D extends C<String> implements I<Integer> {
public String id(String x) {...}
public Integer id(Integer x) {...}
}
Это тоже незаконно, так как D.id(String) — член D, D.id(Integer) объявлен в D, и:
-
Два метода имеют одинаковое имя,
id -
D.id(Integer)доступен изD -
Два метода имеют разные подписи (и ни одна не является подподписью другой)
-
D.id(String)переопределяетC<String>.id(String), аD.id(Integer)переопределяетI.id(Integer), но два переопределённых метода имеют одинаковое стирание
Класс может унаследовать несколько методов с эквивалентными подписями переопределения (§8.4.2).
Если класс C наследует конкретный метод, подпись которого эквивалентна подписи другого метода, унаследованного классом C, это ошибка времени компиляции.
Если класс C наследует метод по умолчанию, подпись которого эквивалентна подписи другого метода, унаследованного классом C, это ошибка времени компиляции, за исключением случаев, когда существует метод abstract, объявленный в суперклассе C и унаследованный классом C, который эквивалентен подписям двух методов.
Это исключение из строгих правил конфликтов default-abstract и default-default применяется, когда метод 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 метод, а также члены, унаследованные от неявного непосредственного суперкласса Object (§4.3.2), такой как метод hashCode. Обратите внимание, что Point не наследует метод toString класса Object, потому что этот метод переопределяется объявлением метода toString в классе Point.
Пример 8.4.9-2. Перегрузка, Переопределение и Скрытие
class Point {
int x = 0, y = 0;
void move(int dx, int dy) { x += dx; y += dy; }
int color;
}
class RealPoint extends Point {
float x = 0.0f, y = 0.0f;
void move(int dx, int dy) { move((float)dx, (float)dy); }
void move(float dx, float dy) { x += dx; y += dy; }
}
Здесь класс RealPoint скрывает объявления int переменных-экземпляров x и y класса Point собственными переменными-экземплярами float x и y, а также переопределяет метод move класса Point собственным методом move. Он также перегружает имя move другим методом с другой подписью (§8.4.2).
В этом примере члены класса RealPoint включают переменную-экземпляр color, унаследованную от класса Point, переменные-экземпляры float x и y, объявленные в RealPoint, и два move метода, объявленные в RealPoint.
Какой из перегруженных move методов класса RealPoint будет выбран для любого конкретного вызова метода, определится во время компиляции по процедуре разрешения перегрузки, описанной в §15.12.
Следующая программа — расширенный вариант предыдущей программы:
class Point {
int x = 0, y = 0, color;
void move(int dx, int dy) { x += dx; y += dy; }
int getX() { return x; }
int getY() { return y; }
}
class RealPoint extends Point {
float x = 0.0f, y = 0.0f;
void move(int dx, int dy) { move((float)dx, (float)dy); }
void move(float dx, float dy) { x += dx; y += dy; }
float getX() { return x; }
float getY() { return y; }
}
Здесь класс Point предоставляет методы getX и getY, возвращающие значения своих полей x и y; класс RealPoint затем переопределяет эти методы, объявляя методы с той же подписью. Результатом являются две ошибки во время компиляции, по одной на каждый метод, потому что типы возвращаемых значений не совпадают; методы в классе Point возвращают значения типа int, но потенциально переопределяющие методы в классе RealPoint возвращают значения типа float.
Эта программа исправляет ошибки предыдущей программы:
class Point {
int x = 0, y = 0;
void move(int dx, int dy) { x += dx; y += dy; }
int getX() { return x; }
int getY() { return y; }
int color;
}
class RealPoint extends Point {
float x = 0.0f, y = 0.0f;
void move(int dx, int dy) { move((float)dx, (float)dy); }
void move(float dx, float dy) { x += dx; y += dy; }
int getX() { return (int)Math.floor(x); }
int getY() { return (int)Math.floor(y); }
}
Здесь переопределяющие методы getX и getY в классе RealPoint имеют те же типы возвращаемых значений, что и методы класса Point, которые они переопределяют, поэтому этот код можно успешно скомпилировать.
Рассмотрим следующую тестовую программу:
class Test {
public static void main(String[] args) {
RealPoint rp = new RealPoint();
Point p = rp;
rp.move(1.71828f, 4.14159f);
p.move(1, -1);
show(p.x, p.y);
show(rp.x, rp.y);
show(p.getX(), p.getY());
show(rp.getX(), rp.getY());
}
static void show(int x, int y) {
System.out.println("(" + x + ", " + y + ")");
}
static void show(float x, float y) {
System.out.println("(" + x + ", " + y + ")");
}
}
Вывод этой программы:
(0, 0) (2.7182798, 3.14159) (2, 3) (2, 3)
Первая строка вывода иллюстрирует тот факт, что экземпляр класса RealPoint фактически содержит две целочисленные переменные, объявленные в классе Point; просто их имена скрыты от кода, который находится внутри объявления класса RealPoint (и всех возможных подклассов). Когда используется ссылка на экземпляр класса RealPoint в переменной типа Point для доступа к полю x, обращение осуществляется к целочисленному полю x, объявленному в классе Point. Тот факт, что его значение равно нулю, указывает на то, что вызов метода p.move(1, -1) не вызвал метод move класса Point; вместо этого он вызвал переопределяющий метод move класса RealPoint.
Вторая строка вывода показывает, что доступ к полю rp.x относится к полю x, объявленному в классе RealPoint. Это поле имеет тип float, и эта вторая строка вывода соответственно отображает значения с плавающей точкой. Кстати, это также иллюстрирует тот факт, что имя метода show перегружено; типы аргументов в вызове метода диктуют, какая из двух определений будет вызвана.
Последние две строки вывода показывают, что вызовы методов p.getX() и rp.getX() каждый вызывают метод getX, объявленный в классе RealPoint. Действительно, нет способа вызвать метод getX класса Point для экземпляра класса RealPoint извне тела RealPoint, независимо от типа переменной, которую мы можем использовать для хранения ссылки на объект. Таким образом, мы видим, что поля и методы ведут себя по-разному: скрытие отличается от переопределения.
Вложенный класс — это класс, объявление которого непосредственно заключено в теле другого класса или интерфейса (§8.1.6, §9.1.4).
Вложенный интерфейс — это интерфейс, объявление которого непосредственно заключено в теле другого класса или интерфейса (§8.1.6, §9.1.4).
Доступность объявления типа члена в классе определяется его модификатором доступа или §6.6, если модификатор доступа отсутствует.
Если один и тот же ключевой модификатор появляется более одного раза при объявлении типа члена класса, или если объявление типа члена имеет более одного из модификаторов доступа public, protected и private (§6.6), то это ошибка компиляции.
Область действия и перекрытие вложенного типа указаны в §6.3 и §6.4.
Если класс объявляет вложенный тип с определенным именем, то объявление этого типа считается скрывающим все и любые доступные объявления вложенных типов с тем же именем в суперклассах и суперинтерфейсах класса.
В этом отношении скрытие вложенных типов аналогично скрытию полей (§8.3).
Класс наследует от своего непосредственного суперкласса и непосредственных суперинтерфейсов все типы членов суперкласса и суперинтерфейсов, которые доступны коду в классе и не скрыты объявлением в классе.
Класс может унаследовать более одного вложенного типа с одинаковым именем, как от суперкласса и суперинтерфейсов, так и только от суперинтерфейсов. Такая ситуация сама по себе не является ошибкой компиляции. Однако любая попытка сослаться на любой такой вложенный тип по его простому имени в теле класса приведет к ошибке компиляции, поскольку ссылка является неоднозначной.
Может быть несколько путей наследования одного и того же объявления вложенного типа из интерфейса. В такой ситуации вложенный тип считается унаследованным только один раз, и на него можно ссылаться по его простому имени без неоднозначности.
Ключевое слово 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.
ПростоеИмяТипа в ДекларатореКонструктора должно совпадать с простым именем класса, содержащего объявление конструктора; в противном случае происходит ошибка компиляции.
Во всех других отношениях объявление конструктора выглядит точно так же, как объявление метода без результата (§8.4.5).
Объявления конструкторов не являются членами. Они никогда не наследуются и, следовательно, не подлежат скрытию или переопределению.
Конструкторы вызываются выражениями создания экземпляров класса (§15.9), преобразованиями и конкатенациями, вызванными оператором конкатенации строк + (§15.18.1), и явными вызовами конструкторов из других конструкторов (§8.8.7). Доступ к конструкторам регулируется модификаторами доступа (§6.6), поэтому можно предотвратить создание экземпляров класса, объявив недоступный конструктор (§8.8.10).
Конструкторы никогда не вызываются выражениями вызова метода (§15.12).
Пример 8.8-1. Объявления конструкторов
class Point {
int x, y;
Point(int x, int y) { this.x = x; this.y = y; }
}
Формальные параметры конструктора идентичны по синтаксису и семантике формальным параметрам метода (§8.4.1).
Конструктор не-private внутреннего вложенного класса неявно объявляет в качестве первого формального параметра переменную, представляющую немедленно окружающий экземпляр класса (§15.9.2, §15.9.3).
Причина, по которой только этот тип класса имеет неявно объявленный параметр конструктора, довольно тонкая. Следующее объяснение может быть полезным:
-
В выражении создания экземпляра класса для не-
privateвнутреннего вложенного класса, §15.9.2 определяет немедленно окружающий экземпляр вложенного класса. Вложенный класс может быть сгенерирован компилятором, отличным от компилятора выражения создания экземпляра. Поэтому должен быть стандартный способ, чтобы компилятор выражения создания передал ссылку (представляющую немедленно окружающий экземпляр) конструктору вложенного класса. Следовательно, язык программирования Java считает в этом разделе, что конструктор не-privateвнутреннего вложенного класса неявно объявляет начальный параметр для немедленно окружающего экземпляра. §15.9.3 указывает, что экземпляр передается в конструктор. -
В выражении создания экземпляра класса для локального класса (не в статическом контексте) или анонимного класса, §15.9.2 определяет немедленно окружающий экземпляр локального/анонимного класса. Локальный/анонимный класс обязательно сгенерирован тем же компилятором, что и выражение создания экземпляра. Этот компилятор может представлять немедленно окружающий экземпляр как пожелает. Языку программирования Java нет необходимости неявно объявлять параметр в конструкторе локального/анонимного класса.
-
В выражении создания экземпляра класса для анонимного класса, и где суперкласс анонимного класса является либо внутренним, либо локальным (не в статическом контексте), §15.9.2 определяет немедленно окружающий экземпляр анонимного класса по отношению к суперклассу. Этот экземпляр должен быть передан от анонимного класса к его суперклассу, где он будет служить немедленно окружающим экземпляром. Поскольку суперкласс может быть сгенерирован компилятором, отличным от компилятора выражения создания экземпляра, необходимо передать экземпляр стандартным способом, передав его в качестве первого аргумента конструктору суперкласса. Обратите внимание, что сам анонимный класс обязательно сгенерирован тем же компилятором, что и выражение создания экземпляра, поэтому компилятор мог бы передать немедленно окружающий экземпляр по отношению к суперклассу анонимному классу как пожелает, прежде чем анонимный класс передаст экземпляр в конструктор суперкласса. Однако для согласованности, язык программирования Java считает в §15.9.5.1, что в некоторых случаях конструктор анонимного класса неявно объявляет начальный параметр для немедленно окружающего экземпляра по отношению к суперклассу.
Тот факт, что не-private внутренний вложенный класс может быть доступен другим компилятором, чем тот, который его скомпилировал, в то время как локальный или анонимный класс всегда доступен тому же компилятору, что и скомпилировал его, объясняет, почему бинарное имя не-private внутреннего вложенного класса определяется как предсказуемое, но бинарное имя локального или анонимного класса не является таковым (§13.1).
Правила для модификаторов аннотаций в объявлении конструктора указаны в §9.7.4 и §9.7.5.
Если одно и то же ключевое слово встречается более одного раза в качестве модификатора в объявлении конструктора, или если объявление конструктора имеет более одного из модификаторов доступа public, protected и private (§6.6), то это ошибка времени компиляции.
В нормальном объявлении класса объявление конструктора без модификаторов доступа имеет доступ по умолчанию (пакетный доступ).
Если в объявлении метода присутствуют два или более (различных) модификаторов метода, то принято, хотя и не обязательно, что они располагаются в том же порядке, что и показано выше в определении для MethodModifier.
В отличие от методов, конструктор не может быть abstract, static, final, native, strictfp или synchronized:
-
Конструктор не наследуется, поэтому нет необходимости объявлять его
final. -
Конструктор
abstractникогда не мог быть реализован. -
Конструктор всегда вызывается относительно объекта, поэтому не имеет смысла, чтобы конструктор был
static. -
Нет практической необходимости, чтобы конструктор был
synchronized, так как это заблокирует объект, который находится в процессе создания, который обычно не предоставляется другим потокам, пока все конструкторы для объекта не завершат свою работу. -
Отсутствие
nativeконструкторов — произвольный выбор при разработке языка, который упрощает проверку реализации Java Virtual Machine, что конструкторы суперклассов всегда правильно вызываются во время создания объекта. -
Невозможность объявить конструктор как
strictfp(в отличие от метода (§8.4.3)) — осознанный выбор при разработке языка; он эффективно гарантирует, что конструктор является FP-строгим тогда и только тогда, когда его класс является FP-строгим (§15.4).
Конструктор является обобщенным, если он объявляет одну или несколько переменных типа (§4.4).
Эти переменные типа известны как параметры типа конструктора. Формат раздела параметров типа обобщенного конструктора идентичен разделу параметров типа обобщенного класса (§8.1.2).
Конструктор может быть обобщенным независимо от того, является ли класс, в котором он объявлен, обобщенным.
Объявление обобщенного конструктора определяет набор конструкторов, по одному для каждого возможного вызова раздела параметров типа с помощью аргументов типа. Аргументы типа могут не потребоваться явно при вызове обобщенного конструктора, так как их часто можно вывести (§18 (Выведение типов)).
Область действия и затенение параметра типа конструктора указаны в §6.3 и §6.4.
Раздел throws для конструктора идентичен по структуре и поведению разделу throws для метода (§8.4.6).
Первое оператор в теле конструктора может быть явным вызовом другого конструктора того же класса или непосредственного суперкласса (§8.8.7.1).
Является ошибкой компиляции, если конструктор напрямую или косвенно вызывает сам себя через серию одного или более явных вызовов конструкторов, вовлекая this.
Если тело конструктора не начинается с явного вызова конструктора, а объявленный конструктор не является частью первоначального класса Object, то тело конструктора неявно начинается с вызова конструктора суперкласса "super();", вызова конструктора его непосредственного суперкласса, не принимающего аргументов.
За исключением возможности явных вызовов конструкторов и запрета на явное возвращение значения (§14.17), тело конструктора подобно телу метода (§8.4.7).
Оператор return ( §14.17) может использоваться в теле конструктора, если он не включает выражения.
Пример 8.8.7-1. Тела конструкторов
class Point {
int x, y;
Point(int x, int y) { this.x = x; this.y = y; }
}
class ColoredPoint extends Point {
static final int WHITE = 0, BLACK = 1;
int color;
ColoredPoint(int x, int y) {
this(x, y, WHITE);
}
ColoredPoint(int x, int y, int color) {
super(x, y);
this.color = color;
}
}
Здесь первый конструктор ColoredPoint вызывает второй, предоставляя дополнительный аргумент; второй конструктор ColoredPoint вызывает конструктор своего суперкласса Point, передавая координаты.
this ( [ArgumentList] ) ; [TypeArguments]
super ( [ArgumentList] ) ; ExpressionName
. [TypeArguments] super ( [ArgumentList] ) ; Primary
. [TypeArguments] super ( [ArgumentList] ) ; Следующие порождающие правила из §4.5.1 и §15.12 показаны здесь для удобства:
Явные вызовы конструкторов делятся на два типа:
-
Альтернативные вызовы конструкторов начинаются со ключевого слова
this(возможно, с предшествующими явными аргументами типа). Они используются для вызова альтернативного конструктора того же класса. -
Вызовы конструкторов суперкласса начинаются либо со ключевого слова
super(возможно, с предшествующими явными аргументами типа), либо с выражения Primary или ExpressionName. Они используются для вызова конструктора непосредственного суперкласса. Они далее делятся на:-
Неквалифицированные вызовы конструкторов суперкласса начинаются со ключевого слова
super(возможно, с предшествующими явными аргументами типа). -
Квалифицированные вызовы конструкторов суперкласса начинаются с выражения Primary или ExpressionName. Они позволяют конструктору подкласса явно указать недавно созданный объект's непосредственно окружающий экземпляр по отношению к непосредственному суперклассу (§8.1.3). Это может быть необходимо, когда суперкласс является вложенным классом.
-
Явный оператор вызова конструктора в теле конструктора не может ссылаться на какие-либо переменные экземпляра или методы экземпляра или внутренние классы, объявленные в этом классе или любом суперклассе, или использовать this или super в любом выражении; в противном случае возникает ошибка компиляции.
Этот запрет на использование текущего экземпляра объясняет, почему явный оператор вызова конструктора считается выполняющимся в статическом контексте (§8.1.3).
Если TypeArguments присутствует слева от this или super, то это ошибка компиляции, если какой-либо из аргументов типа является дикими картами (§4.5.1).
Пусть C — класс, который создается, и пусть S — непосредственный суперкласс C.
Если оператор вызова конструктора суперкласса неквалифицирован, то:
-
Если S является вложенным членом класса, но S не является членом класса, окружающего C, то возникает ошибка компиляции.
Если оператор вызова конструктора суперкласса квалифицирован, то:
-
Если S не является вложенным классом, или если объявление S происходит в статическом контексте, то возникает ошибка компиляции.
-
В противном случае, пусть
p— выражение Primary или ExpressionName непосредственно перед ".super", и пусть O — непосредственно окружающий класс S. Возникает ошибка компиляции, если типpне O или подкласс O, или если типpнедоступен (§6.6).
Типы исключений, которые может выбросить явный оператор вызова конструктора, указаны в §11.2.2.
Оценка оператора альтернативного вызова конструктора осуществляется путём сначала оценки аргументов конструктора слева направо, как при обычном вызове метода; затем выполняется вызов конструктора.
Оценка оператора вызова конструктора суперкласса выполняется следующим образом:
-
Пусть
i— это создаваемый экземпляр. Необходимо определить ближайший обрамляющий экземплярiотносительно S (если таковой имеется):-
Если S не является внутренним классом или объявление S происходит в статическом контексте, то ближайший обрамляющий экземпляр
iотносительно S не существует. -
Если вызов конструктора суперкласса не квалифицирован, то S обязательно является локальным классом или внутренним членом класса.
Если S — локальный класс, то пусть O — ближайшее обрамляющее объявление типа S.
Если S — внутренний член класса, то пусть O — самый внутренний обрамляющий класс C, членом которого является S.
Пусть n — целое число (n ≥ 1) такое, что O является n-ым лексически обрамляющим объявлением типа C.
Ближайший обрамляющий экземпляр
iотносительно S — это n-ый лексически обрамляющий экземплярthis.Хотя S может быть членом C из-за наследования, нулевой лексически обрамляющий экземпляр
this(то есть,thisсам по себе) никогда не используется в качестве ближайшего обрамляющего экземпляра i относительно S. -
Если вызов конструктора суперкласса квалифицирован, то выражение Primary или ExpressionName непосредственно перед "
.super",p, оценивается.Если
pвычисляется какnull, генерируется исключениеNullPointerException, и вызов конструктора суперкласса завершается неожиданно.В противном случае результат этого вычисления — ближайший обрамляющий экземпляр
iотносительно S.
-
-
После определения ближайшего обрамляющего экземпляра
iотносительно S (если таковой имеется), оценка оператора вызова конструктора суперкласса выполняется путём оценки аргументов конструктора слева направо, как в обычном вызове метода, а затем вызова конструктора. -
Наконец, если оператор вызова конструктора суперкласса завершается нормально, выполняются все инициализаторы переменных экземпляра C и все инициализаторы экземпляров C. Если инициализатор экземпляра или инициализатор переменной экземпляра
Iрасположен текстово перед другим инициализатором экземпляра или инициализатором переменной экземпляраJ, тоIвыполняется доJ.Выполнение инициализаторов переменных экземпляра и инициализаторов экземпляров выполняется независимо от того, появляется ли вызов конструктора суперкласса как явный оператор вызова конструктора или предоставляется неявно. (Альтернативный вызов конструктора не выполняет этого дополнительного неявного выполнения.)
Пример 8.8.7.1-1. Ограничения на явные операторы вызова конструкторов
Если первый конструктор ColoredPoint в примере из §8.8.7 был изменён следующим образом:
class Point {
int x, y;
Point(int x, int y) { this.x = x; this.y = y; }
}
class ColoredPoint extends Point {
static final int WHITE = 0, BLACK = 1;
int color;
ColoredPoint(int x, int y) {
this(x, y, color); // Changed to color from WHITE
}
ColoredPoint(int x, int y, int color) {
super(x, y);
this.color = color;
}
}
то возникнет ошибка компиляции, так как переменная экземпляра color не может быть использована оператором явного вызова конструктора.
Пример 8.8.7.1-2. Квалифицированный вызов конструктора суперкласса
В коде ниже ChildOfInner не имеет лексически обрамляющего объявления типа, поэтому у экземпляра ChildOfInner нет обрамляющего экземпляра. Однако у суперкласса ChildOfInner (Inner) есть лексически обрамляющее объявление типа (Outer), и у экземпляра Inner должен быть обрамляющий экземпляр Outer. Обрамляющий экземпляр Outer устанавливается при создании экземпляра Inner. Поэтому, когда мы создаём экземпляр ChildOfInner, который неявно является экземпляром Inner, мы должны указать обрамляющий экземпляр Outer через квалифицированный оператор вызова конструктора суперкласса в конструкторе ChildOfInner. Экземпляр Outer называется ближайшим обрамляющим экземпляром ChildOfInner относительно Inner.
class Outer {
class Inner {}
}
class ChildOfInner extends Outer.Inner {
ChildOfInner() { (new Outer()).super(); }
}
Возможно, неожиданно, один и тот же экземпляр Outer может служить ближайшим обрамляющим экземпляром ChildOfInner относительно Inner для нескольких экземпляров ChildOfInner. Эти экземпляры ChildOfInner неявно связаны с одним и тем же экземпляром Outer. Нижеприведенная программа достигает этого, передавая экземпляр Outer в конструктор ChildOfInner, который использует экземпляр в квалифицированном операторе вызова конструктора суперкласса. Правила для оператора явного вызова конструктора не запрещают использование формальных параметров конструктора, содержащего данный оператор.
class Outer {
int secret = 5;
class Inner {
int getSecret() { return secret; }
void setSecret(int s) { secret = s; }
}
}
class ChildOfInner extends Outer.Inner {
ChildOfInner(Outer x) { x.super(); }
}
public class Test {
public static void main(String[] args) {
Outer x = new Outer();
ChildOfInner a = new ChildOfInner(x);
ChildOfInner b = new ChildOfInner(x);
System.out.println(b.getSecret());
a.setSecret(6);
System.out.println(b.getSecret());
}
}
Эта программа выводит:
5 6
Результат — манипуляции переменными экземпляра в общем экземпляре Outer видны через ссылки на разные экземпляры ChildOfInner, даже если такие ссылки не являются алиасами в обычном понимании.
Если класс не содержит объявлений конструкторов, то конструктор по умолчанию неявно объявляется. Формат конструктора по умолчанию для класса верхнего уровня, внутреннего класса-члена или локального класса следующий:
-
Конструктор по умолчанию имеет тот же модификатор доступа, что и класс, за исключением случаев, когда у класса нет модификатора доступа, в этом случае конструктор по умолчанию имеет доступ к пакету (§6.6).
-
Конструктор по умолчанию не имеет формальных параметров, за исключением не-
privateвнутреннего класса-члена, где конструктор по умолчанию неявно объявляет один формальный параметр, представляющий немедленно окружающий экземпляр класса (§8.8.1, §15.9.2, §15.9.3). -
Конструктор по умолчанию не имеет
throwsблоков. -
Если объявляемый класс является примитивным классом
Object, то конструктор по умолчанию имеет пустое тело. В противном случае конструктор по умолчанию просто вызывает конструктор суперкласса без аргументов.
Формат конструктора по умолчанию для анонимного класса указан в §15.9.5.1.
Если неявно объявлен конструктор по умолчанию, но у суперкласса нет доступного конструктора, принимающего нуль аргументов и не имеющего throws блока, то это ошибка на этапе компиляции.
Пример 8.8.9-1. Конструкторы по умолчанию
Объявление:
public class Point {
int x, y;
}
эквивалентно объявлению:
public class Point {
int x, y;
public Point() { super(); }
}
где конструктор по умолчанию является public, поскольку класс Point является public.
Пример 8.8.9-2. Доступность конструкторов по отношению к классам
Правило, согласно которому конструктор по умолчанию класса имеет тот же доступ, что и сам класс, просто и интуитивно. Однако это не означает, что конструктор доступен всякий раз, когда доступен класс. Рассмотрим:
package p1;
public class Outer {
protected class Inner {}
}
package p2;
class SonOfOuter extends p1.Outer {
void foo() {
new Inner(); // compile-time access error
}
}
Конструктор по умолчанию для Inner является protected. Однако конструктор protected по отношению к Inner, в то время как Inner является protected по отношению к Outer. Таким образом, Inner доступен в SonOfOuter, так как он является подклассом Outer. Конструктор Inner недоступен в SonOfOuter, потому что класс SonOfOuter не является подклассом Inner! Следовательно, даже если Inner доступен, его конструктор по умолчанию недоступен.
Класс может быть спроектирован таким образом, чтобы код вне объявления класса не мог создавать экземпляры класса, объявив по крайней мере один конструктор, чтобы предотвратить создание конструктора по умолчанию, и объявив все конструкторы private (§6.6.1).
Класс public аналогичным образом может предотвратить создание экземпляров вне пакета, объявив по крайней мере один конструктор, чтобы предотвратить создание конструктора по умолчанию с доступом public, и не объявив конструктор с public или protected доступом (§6.6.2).
Пример 8.8.10-1. Препятствие к созданию экземпляров посредством доступа к конструкторам
class ClassOnly {
private ClassOnly() { }
static String just = "only the lonely";
}
Здесь класс ClassOnly не может быть создан, в то время как в следующем коде:
package just;
public class PackageOnly {
PackageOnly() { }
String[] justDesserts = { "cheesecake", "ice cream" };
}
класс public PackageOnly может быть создан только в пакете just, в котором он объявлен. Это ограничение также применялось бы, если бы конструктор PackageOnly был protected, хотя в этом случае код в других пакетах мог бы создать подклассы PackageOnly.
Объявление перечисления задаёт новый тип перечисления, специальный вид типа класса.
Если в объявлении перечисления присутствует модификатор abstract или final, это ошибка компиляции.
Объявление перечисления неявным образом final, если в нём не содержится хотя бы одна константа перечисления с телом класса (§8.9.1).
Вложенный тип перечисления неявным образом static. Допускается избыточное указание модификатора static для вложенного типа перечисления.
Это подразумевает, что невозможно объявить тип перечисления в теле внутреннего класса (§8.1.3), поскольку внутренний класс не может содержать static членов, за исключением константных переменных.
Если один и тот же ключевой модификатор используется более одного раза в объявлении перечисления, или если объявление перечисления содержит более одного модификатора доступа public, protected и private, это ошибка компиляции (§6.6).
Прямым суперклассом типа перечисления 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. Поле имеет инициализатор переменной, который создает E и передает любые аргументыcв выбранный для E конструктор. Поле имеет те же аннотации, что и у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. Итерация по константам перечисления с помощью усовершенствованного цикла for
public class Test {
enum Season { WINTER, SPRING, SUMMER, FALL }
public static void main(String[] args) {
for (Season s : Season.values())
System.out.println(s);
}
}
Эта программа выводит:
WINTER SPRING SUMMER FALL
Пример 8.9.3-2. Переключение по константам перечисления
Оператор switch (§14.11) полезен для имитации добавления метода к типу перечисления извне типа. В этом примере добавляется метод color к типу Coin из §8.9.2 и выводится таблица монет, их значений и цветов.
class Test {
enum CoinColor { COPPER, NICKEL, SILVER }
static CoinColor color(Coin c) {
switch (c) {
case PENNY:
return CoinColor.COPPER;
case NICKEL:
return CoinColor.NICKEL;
case DIME: case QUARTER:
return CoinColor.SILVER;
default:
throw new AssertionError("Unknown coin: " + c);
}
}
public static void main(String[] args) {
for (Coin c : Coin.values())
System.out.println(c + "\t\t" +
c.value() + "\t" + color(c));
}
}
Эта программа выводит:
PENNY 1 COPPER NICKEL 5 NICKEL DIME 10 SILVER QUARTER 25 SILVER
Пример 8.9.3-3. Константы перечисления с телами классов
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.