Глава 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).
Объявление класса определяет новый именованный тип ссылки.
Существует два вида объявлений классов: обычные объявления классов и объявления перечислений.
ClassDeclaration:
NormalClassDeclaration
EnumDeclaration
NormalClassDeclaration:
ClassModifiersopt class Identifier TypeParametersopt
Superopt Interfacesopt ClassBody
Правила в этом разделе применяются ко всем объявлениям классов, если в этом спецификации не указано иное. Во многих случаях к объявлениям перечислений (§8.9) применяются специальные ограничения.
В объявлении класса Identifier задаёт имя класса.
Если класс имеет то же простое имя, что и любой из его окружающих классов или интерфейсов, возникает ошибка времени компиляции.
Область действия и перекрытие объявления класса описаны в §6.3 и §6.4.
Объявление класса может включать модификаторы класса.
ClassModifiers:
ClassModifier
ClassModifiers ClassModifier
ClassModifier: один из
Annotation public protected private
abstract static final strictfp
Если аннотация a (§9.7) в объявлении класса соответствует типу аннотации T (§9.6), и T имеет (мета-)аннотацию m, которая соответствует java.lang.annotation.Target, то m должен иметь элемент со значением java.lang.annotation.ElementType.TYPE, иначе произойдёт ошибка компиляции.
Модификатор доступа public (§6.6) относится только к классам верхнего уровня (§7.6) и к вложенным классам (§8.5), но не к локальным классам (§14.3) или анонимным классам (§15.9.5).
Модификаторы доступа protected и private (§6.6) относятся только к вложенным классам внутри непосредственно содержащего класса или объявления перечисления (§8.5).
Модификатор static относится только к вложенным классам (§8.5.1), а не к классам верхнего уровня, локальным или анонимным.
Ошибка компиляции, если один и тот же модификатор появляется более одного раза в объявлении класса.
Если в объявлении класса присутствует два или более (различных) модификаторов класса, то обычно (но необязательно), они располагаются в порядке, соответствующем порядку в приведенном выше правиле для ClassModifier.
abstract класс — это класс, который является неполным или должен считаться таковым.
Обычные классы могут иметь abstract методы (§8.4.3.1, §9.4), то есть методы, объявленные, но ещё не реализованные, только если они являются abstract классами.
Если обычный класс, который не является abstract, содержит abstract метод, то произойдёт ошибка компиляции.
Тип перечисления (§8.9) не должен быть объявлен abstract, иначе произойдёт ошибка компиляции.
Ошибка компиляции, если для типа перечисления E объявлен abstract метод m, если у E нет констант перечисления или константы E не имеют тел классов, предоставляющих конкретные реализации m.
Ошибка компиляции, если тело класса константы перечисления объявляет abstract метод.
Класс C имеет abstract методы, если выполняется одно из следующих условий:
-
C явно содержит объявление
abstractметода (§8.4.3). -
Любой из предков C имеет
abstractметод, и C не объявляет и не наследует метод, который реализует (§8.4.8.1) его. -
Непосредственный суперинтерфейс (§8.1.5) C объявляет или наследует метод (который, следовательно, обязательно
abstract), и C не объявляет и не наследует метод, который его реализует.
Ошибка компиляции, если происходит попытка создать экземпляр abstract класса с помощью выражения создания экземпляра класса (§15.9).
Подкласс abstract класса, который сам не является abstract, может быть создан, что приводит к выполнению конструктора для abstract класса и, следовательно, выполнению инициализаторов полей для переменных экземпляров этого класса.
Ошибка компиляции, если тип 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).
Эти переменные типа известны как параметры типа класса. Раздел параметров типа следует за именем класса и ограничен угловыми скобками.
TypeParameters:
< Список параметров типа >
Список параметров типа:
Список параметров типа , Параметр типа
Параметр типа
В разделе параметров типа класса переменная типа T непосредственно зависит от переменной типа S, если S является ограничением T, в то время как T зависит от S, если либо T непосредственно зависит от S, либо T непосредственно зависит от переменной типа U, которая зависит от S (используя это определение рекурсивно).
Это ошибка времени компиляции, если переменная типа в разделе параметров типа класса зависит от самой себя.
Область действия и перекрытие параметров типа класса указаны в §6.3 и §6.4.
Пример 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(); }
}
Объявление обобщённого класса определяет набор параметризованных типов (§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-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.
Внутренние классы включают локальные (§14.3), анонимные (§15.9.5) и нестатические классы-члены (§8.5).
Внутренние классы не могут объявлять статические инициализаторы (§8.7) или член-интерфейсы, иначе произойдёт ошибка компиляции.
Внутренние классы не могут объявлять статические члены, за исключением константных переменных (§4.12.4), иначе произойдёт ошибка компиляции.
Внутренние классы могут наследовать статические члены, которые не являются константными переменными, даже если не объявляют их.
Вложенные классы, которые не являются внутренними, могут свободно объявлять статические члены в соответствии с общими правилами языка программирования Java. Член-интерфейсы (§8.5) неявно static, поэтому их никогда не считают внутренними классами.
Пример 8.1.3-1. Объявления внутренних классов и статических членов
class HasStatic {
static int j = 100;
}
class Outer {
class Inner extends HasStatic {
static final int x = 3; // OK: compile-time constant
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).
Внутренний класс C является непосредственно вложенным классом класса O, если O — непосредственно окружающий по лексическому расположению класс C, и объявление C не происходит в статическом контексте.
Класс C является внутренним классом класса O, если он является либо непосредственно вложенным классом O, либо внутренним классом внутреннего класса O.
Класс 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. Непосредственно окружающий экземпляр объекта относительно непосредственного суперкласса его класса, если таковой есть, определяется при вызове конструктора суперкласса через оператор явного вызова конструктора.
Когда внутренний класс (объявление которого не происходит в статическом контексте) обращается к переменной экземпляра, являющейся членом лексически окружающего класса, используется переменная соответствующего лексически окружающего экземпляра.
Любая локальная переменная, формальный параметр или параметр исключения, используемые, но не объявленные во внутреннем классе, должны быть объявлены final.
Любая локальная переменная, используемая, но не объявленная во внутреннем классе, должна быть однозначно присвоена (§16) перед телом внутреннего класса.
Пустое final (§4.12.4) поле лексически окружающего класса не может быть присвоено внутри внутреннего класса, иначе произойдёт ошибка компиляции.
Пример 8.1.3-2. Объявления внутренних классов
class Outer {
int i = 100;
static void classMethod() {
final int l = 200;
class LocalInStaticContext {
int k = i; // Compile-time error
int m = l; // OK
}
}
void foo() {
class Local { // A local class
int j = i;
}
}
}
Объявление класса LocalInStaticContext происходит в статическом контексте из-за нахождения внутри статического метода classMethod. Переменные экземпляра класса Outer недоступны внутри тела статического метода. В частности, переменные экземпляра Outer недоступны внутри тела LocalInStaticContext. Однако, к локальным переменным из окружающего метода можно обращаться без ошибок (при условии, что они помечены final).
Внутренние классы, чьи объявления не происходят в статическом контексте, могут свободно обращаться к переменным экземпляра своего окружающего класса. Переменная экземпляра всегда определяется относительно экземпляра. В случае переменных экземпляра окружающего класса переменная экземпляра должна быть определена относительно окружающего экземпляра этого класса. Например, у класса Local выше есть окружающий экземпляр класса Outer. Как ещё один пример:
class WithDeepNesting {
boolean toBe;
WithDeepNesting(boolean b) { toBe = b; }
class Nested {
boolean theQuestion;
class DeeplyNested {
DeeplyNested(){
theQuestion = toBe || !toBe;
}
}
}
}
Здесь каждый экземпляр WithDeepNesting.Nested.DeeplyNested имеет окружающий экземпляр класса WithDeepNesting.Nested (его непосредственно окружающий экземпляр) и окружающий экземпляр класса WithDeepNesting (его второй лексически окружающий экземпляр).
Необязательная extends часть в объявлении обычного класса указывает прямой суперкласс текущего класса.
Супер:
extends ТипКласса
Следующее повторяется из §4.3, чтобы сделать здесь представление более ясным:
ТипКласса:
СпецификаторТипа ОбъявленныеАргументыопт
extends часть не должна появляться в определении класса Object, иначе произойдет ошибка времени компиляции, поскольку это исходный класс и у него нет прямого суперкласса.
Тип ТипКласса должен называть доступный (§6.6) тип класса, иначе произойдет ошибка времени компиляции.
Если указанный тип ТипКласса называет класс, который является final (§8.1.1.2), то произойдёт ошибка времени компиляции, поскольку классам final не разрешено иметь подклассы.
Ошибка времени компиляции возникает, если тип ТипКласса называет класс Enum или любое его обращение.
Если за спецификатором СпецификаторТипа следуют какие-либо аргументы типа, то это должно быть корректное обращение к объявлению типа, обозначаемому СпецификаторТипа, и ни один из аргументов типа не должен быть аргументом типа со звёздочкой, иначе произойдет ошибка времени компиляции.
Для (возможно, обобщённого) объявления класса C<F1,...,Fn> (n ≥ 0, C ≠ Object), прямым суперклассом типа класса C<F1,...,Fn> является тип, указанный в extends части объявления C, если extends часть присутствует, или Object в противном случае.
Пусть C<F1,...,Fn> (n > 0) — объявление обобщённого класса. Прямой суперкласс параметризованного типа C<T1,...,Tn>, где Ti (1 ≤ i ≤ n) — тип, это D<U1 θ,...,Uk θ>, где D<U1,...,Uk> — прямой суперкласс C<F1,...,Fn>, а θ — подстановка [F1:=T1,...,Fn:=Tn].
Прямым суперклассом типа перечисления E является Enum<E>.
Класс считается прямым подклассом своего прямого суперкласса. Прямой суперкласс — это класс, реализация которого служит основой для реализации текущего класса.
Пример 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.
Отношение подкласса — это транзитивное замыкание отношения прямого подкласса. Класс A является подклассом класса C, если выполняется одно из следующих условий:
-
A является прямым подклассом C
-
Существует класс B такой, что A является подклассом B, а B является подклассом C, применяя это определение рекурсивно.
Класс C называется суперклассом класса A, когда A является подклассом C.
Пример 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 (применяя это определение рекурсивно).
Ошибка времени компиляции возникает, если класс зависит от самого себя.
Если при загрузке классов (§12.2) обнаруживаются циклически объявленные классы, то выбрасывается ClassCircularityError.
Пример 8.1.4-3. Класс зависит от самого себя
class Point extends ColoredPoint { int x, y; }
class ColoredPoint extends Point { int color; }
Эта программа вызывает ошибку времени компиляции.
Необязательная implements часть в объявлении класса перечисляет имена интерфейсов, которые являются непосредственными суперинтерфейсами объявляемого класса.
Интерфейсы:
implements СписокИнтерфейсов
СписокИнтерфейсов:
ТипИнтерфейса
СписокИнтерфейсов , ТипИнтерфейса
Следующее повторяется из §4.3, чтобы сделать представление здесь более ясным:
ТипКласса:
СпецификаторТипа ОбъявленияТиповнеобяз
Каждый ТипИнтерфейса должен называть доступный (§6.6) тип интерфейса, иначе произойдёт ошибка компиляции.
Если за ИменемТипа следуют какие-либо типы аргументов, это должна быть корректная вызов объявления типа, обозначаемого ИменемТипа, и ни один из типов аргументов не может быть аргументом типа «подстановка», иначе произойдёт ошибка компиляции.
Возникает ошибка компиляции, если один и тот же интерфейс упоминается два или более раза в качестве непосредственного суперинтерфейса в рамках одного implements списка имен. Это верно даже если интерфейс называется по-разному.
Для объявления (возможно, обобщённого) класса C<F1,...,Fn> (n ≥ 0, C ≠ Object), непосредственными суперинтерфейсами типа класса C<F1,...,Fn> являются типы, указанные в implements части объявления C, если implements часть присутствует.
Пусть C<F1,...,Fn> (n > 0) — обобщённое объявление класса. Непосредственными суперинтерфейсами параметризованного типа класса C<T1,...,Tn>, где Ti (1 ≤ i ≤ n) — тип, являются все типы I<U1 θ,...,Uk θ>, где I<U1,...,Uk> — непосредственный суперинтерфейс C<F1,...,Fn>, и θ — подстановка [F1:=T1,...,Fn:=Tn].
Пример 8.1.5-1. Недопустимые суперинтерфейсы
class Redundant implements java.lang.Cloneable, Cloneable {
int x;
}
Эта программа приводит к ошибке компиляции, потому что имена java.lang.Cloneable и Cloneable ссылаются на один и тот же интерфейс.
Тип интерфейса I является суперинтерфейсом типа класса C, если выполняется хотя бы одно из следующих условий:
-
I — непосредственный суперинтерфейс C.
-
C имеет некоторый непосредственный суперинтерфейс J, для которого I является суперинтерфейсом в соответствии с определением «суперинтерфейса интерфейса», данным в §9.1.3.
-
I — суперинтерфейс непосредственного суперкласса C.
Класс может иметь суперинтерфейс более чем одним способом.
Говорят, что класс реализует все свои суперинтерфейсы.
Пример 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.
Если объявляемый класс не abstract, объявления всех членов методов каждого непосредственного суперинтерфейса должны быть реализованы либо объявлением в этом классе, либо существующим объявлением метода, унаследованным от непосредственного суперкласса, так как класс, который не является абстрактным, не может иметь абстрактные методы (§8.1.1.1).
Разрешено, чтобы одно объявление метода в классе реализовывало методы более чем одного суперинтерфейса.
Пример 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).
Класс не может одновременно быть подтипом двух типов интерфейсов, которые являются различными вызовами одного и того же обобщённого интерфейса (§9.1.2), или подтипом вызова обобщённого интерфейса и примитивного типа, обозначающего тот же обобщённый интерфейс, иначе произойдёт ошибка компиляции.
Это требование было введено для поддержки трансляции путём стирания типов (§4.6).
Пример 8.1.5-4. Недопустимое множественное наследование интерфейса
interface I<T> {}
class B implements I<Integer> {}
class C extends B implements I<String> {}
Тело класса может содержать объявления членов класса, то есть полей (§8.3), методов (§8.4), классов (§8.5) и интерфейсов (§8.5).
Тело класса также может содержать инициализаторы экземпляров (§8.6), статические инициализаторы (§8.7) и объявления конструкторов (§8.8) для класса.
ClassBody:
{ ClassBodyDeclarationsopt }
ClassBodyDeclarations:
ClassBodyDeclaration
ClassBodyDeclarations ClassBodyDeclaration
ClassBodyDeclaration:
ClassMemberDeclaration
InstanceInitializer
StaticInitializer
ConstructorDeclaration
ClassMemberDeclaration:
FieldDeclaration
MethodDeclaration
ClassDeclaration
InterfaceDeclaration
;
Область видимости и перекрытие объявления члена m, объявленного или унаследованного типом класса C, указаны в §6.3 и §6.4.
Если сам C является вложенным классом, могут быть определения одного и того же типа (переменной, метода или типа) и имени, что и m во внешних областях видимости. (Области видимости могут быть блоками, классами или пакетами). Во всех таких случаях член m, объявленный или унаследованный типом C, затеняет (§6.4.1) другие определения того же типа и имени.
Члены типа класса — это все следующее:
Члены класса, которые объявлены private, не наследуются подклассами этого класса.
Только члены класса, которые объявлены protected или public, наследуются подклассами, объявленными в пакете, отличном от того, в котором объявлен класс.
Конструкторы, статические инициализаторы и инициализаторы экземпляров не являются членами и поэтому не наследуются.
Мы используем фразу тип члена для обозначения:
-
Для поля — его тип.
-
Для метода — упорядоченную тройку из:
-
типов аргументов: список типов аргументов метода-члена.
-
возвращаемый тип: возвращаемый тип метода-члена.
-
блок throws: типы исключений, объявленные в блоке throws метода-члена.
-
Поля, методы и типы членов типа класса могут иметь одинаковые имена, поскольку они используются в разных контекстах и различаются различными процедурами поиска (§6.5). Однако это не рекомендуется с точки зрения стиля.
Пример 8.2-1. Использование членов класса
class Point {
int x, y;
private Point() { reset(); }
Point(int x, int y) { this.x = x; this.y = y; }
private void reset() { this.x = 0; this.y = 0; }
}
class ColoredPoint extends Point {
int color;
void clear() { reset(); } // error
}
class Test {
public static void main(String[] args) {
ColoredPoint c = new ColoredPoint(0, 0); // error
c.reset(); // error
}
}
Эта программа вызывает четыре ошибки времени компиляции.
Одна ошибка возникает, потому что ColoredPoint не объявляет конструктор с двумя int параметрами, как требуется для использования в main. Это иллюстрирует тот факт, что ColoredPoint не наследует конструкторы своего суперкласса Point.
Другая ошибка возникает, потому что ColoredPoint не объявляет конструкторы, и поэтому для него автоматически создаётся конструктор по умолчанию (§8.8.9), и этот конструктор по умолчанию эквивалентен:
ColoredPoint() { super(); }
который вызывает конструктор без аргументов для непосредственного суперкласса класса ColoredPoint. Ошибка заключается в том, что конструктор для Point, принимающий ноль аргументов, является private, и поэтому не доступен за пределами класса Point, даже через вызов конструктора суперкласса (§8.8.7).
Ещё две ошибки возникают, потому что метод reset класса Point является private, и поэтому не наследуется классом ColoredPoint. Следовательно, вызовы методов в методе clear класса ColoredPoint и в методе main класса Test некорректны.
Пример 8.2-2. Наследование членов класса с доступом по умолчанию
Рассмотрим пример, когда пакет points объявляет две единицы компиляции:
package points;
public class Point {
int x, y;
public void move(int dx, int dy) { x += dx; y += dy; }
}
и:
package points;
public class Point3d extends Point {
int z;
public void move(int dx, int dy, int dz) {
x += dx; y += dy; z += dz;
}
}
и третья единица компиляции в другом пакете:
import points.Point3d;
class Point4d extends Point3d {
int w;
public void move(int dx, int dy, int dz, int dw) {
x += dx; y += dy; z += dz; w += dw; // compile-time errors
}
}
Здесь оба класса в пакете points компилируются. Класс Point3d наследует поля x и y класса Point, потому что он находится в том же пакете, что и Point. Класс Point4d, который находится в другом пакете, не наследует поля x и y класса Point или поле z класса Point3d, и поэтому не компилируется.
Лучше было бы написать третью единицу компиляции так:
import points.Point3d;
class Point4d extends Point3d {
int w;
public void move(int dx, int dy, int dz, int dw) {
super.move(dx, dy, dz); w += dw;
}
}
используя метод move суперкласса Point3d для обработки dx, dy и dz. Если Point4d написано таким образом, он будет компилироваться без ошибок.
Пример 8.2-3. Наследование членов класса public и protected
Учитывая класс Point:
package points;
public class Point {
public int x, y;
protected int useCount = 0;
static protected int totalUseCount = 0;
public void move(int dx, int dy) {
x += dx; y += dy; useCount++; totalUseCount++;
}
}
поля public и protected x, y, useCount и totalUseCount наследуются во всех подклассах класса Point.
Поэтому эта тестовая программа в другом пакете может быть успешно скомпилирована:
class Test extends points.Point {
public void moveBack(int dx, int dy) {
x -= dx; y -= dy; useCount++; totalUseCount++;
}
}
Пример 8.2-4. Наследование членов класса private
class Point {
int x, y;
void move(int dx, int dy) {
x += dx; y += dy; totalMoves++;
}
private static int totalMoves;
void printMoves() { System.out.println(totalMoves); }
}
class Point3d extends Point {
int z;
void move(int dx, int dy, int dz) {
super.move(dx, dy); z += dz; totalMoves++; // error
}
}
Здесь переменная класса totalMoves может использоваться только внутри класса Point; она не наследуется подклассом Point3d. Ошибка времени компиляции возникает, потому что метод move класса Point3d пытается увеличить totalMoves.
Пример 8.2-5. Доступ к членам недоступных классов
Даже если класс не объявлен public, экземпляры класса могут быть доступны во время выполнения коду вне пакета, в котором он объявлен, посредством public суперкласса или суперинтерфейса. Экземпляр класса может быть присвоен переменной такого public типа. Вызов public метода объекта, на который ссылается такая переменная, может вызвать метод класса, если он реализует или переопределяет метод public суперкласса или суперинтерфейса. (В этой ситуации метод обязательно объявлен public, даже если он объявлен в классе, который не является public.)
Рассмотрим единицу компиляции:
package points;
public class Point {
public int x, y;
public void move(int dx, int dy) {
x += dx; y += dy;
}
}
и другую единицу компиляции другого пакета:
package morePoints;
class Point3d extends points.Point {
public int z;
public void move(int dx, int dy, int dz) {
super.move(dx, dy); z += dz;
}
public void move(int dx, int dy) {
move(dx, dy, 0);
}
}
public class OnePoint {
public static points.Point getOne() {
return new Point3d();
}
}
Вызов morePoints.OnePoint.getOne() в ещё одном пакете вернёт Point3d, который может использоваться как Point, даже если тип Point3d недоступен за пределами пакета morePoints. Затем может быть вызван двухаргументный вариант метода move для этого объекта, что допустимо, потому что метод move класса Point3d является public (так как любой метод, переопределяющий метод public, сам должен быть public, именно для того, чтобы такие ситуации работали правильно). К полям x и y этого объекта также можно получить доступ из такого третьего пакета.
Хотя поле z класса Point3d является public, получить доступ к этому полю из кода вне пакета morePoints, имея только ссылку на экземпляр класса Point3d в переменной p типа Point, невозможно. Это потому, что выражение p.z некорректно, поскольку p имеет тип Point, а класс Point не имеет поля с именем z; также выражение ((Point3d)p).z некорректно, потому что тип класса Point3d не может быть использован вне пакета morePoints.
Объявление поля z как public не бесполезно, однако. Если бы в пакете morePoints был подкласс Point4d класса Point3d:
package morePoints;
public class Point4d extends Point3d {
public int w;
public void move(int dx, int dy, int dz, int dw) {
super.move(dx, dy, dz); w += dw;
}
}
тогда класс Point4d унаследовал бы поле z, которое, будучи public, могло бы затем использоваться кодом в пакетах, отличных от morePoints, через переменные и выражения типа public типа Point4d.
Переменные типа класса вводятся с помощью объявлений полей.
FieldDeclaration:
FieldModifiersopt Type VariableDeclarators ;
VariableDeclarators:
VariableDeclarator
VariableDeclarators , VariableDeclarator
VariableDeclarator:
VariableDeclaratorId
VariableDeclaratorId = VariableInitializer
VariableDeclaratorId:
Identifier
VariableDeclaratorId [ ]
VariableInitializer:
Expression
ArrayInitializer
Описание FieldModifiers приведено в §8.3.1.
Идентификатор в деклараторе поля может использоваться в имени для ссылки на поле.
В одном объявлении поля можно объявить более одного поля, используя более одного декларатора; FieldModifiers и Type применяются ко всем деклараторам в объявлении.
Тип объявленного поля обозначается Type, который появляется в объявлении поля, за которым следуют любые парные скобки, следующие за Identifier в деклараторе.
В теле объявления класса не допускается объявление двух полей с одинаковым именем.
Область видимости и перекрытие объявления поля описаны в §6.3 и §6.4.
Если класс объявляет поле с определённым именем, то объявление этого поля считается скрывающим все и любые доступные объявления полей с тем же именем в суперклассах и суперинтерфейсах класса.
В этом отношении скрытие полей отличается от скрытия методов (§8.4.8.3), поскольку нет различия между static и не-static полями при скрытии полей, в то время как различие существует между static и не-static методами при скрытии методов.
К скрытому полю можно получить доступ, используя квалифицированное имя (§6.5.6.2), если оно static, или используя выражение доступа к полю, содержащее ключевое слово super (§15.11.2) или приведение к типу суперкласса.
В этом отношении скрытие полей аналогично скрытию методов.
Если объявление поля скрывает объявление другого поля, то два поля не обязательно должны иметь один и тот же тип.
Класс наследует от своего непосредственного суперкласса и непосредственных суперинтерфейсов все не-private поля суперкласса и суперинтерфейсов, которые доступны коду в классе и не скрываются объявлением в классе.
private поле суперкласса может быть доступно подклассу — например, если оба класса являются членами одного класса. Тем не менее, private поле никогда не наследуется подклассом.
Возможно, что класс унаследует более одного поля с одинаковым именем. Такая ситуация сама по себе не приводит к ошибке компиляции. Однако любая попытка в теле класса сослаться на любое такое поле по его простому имени приведет к ошибке компиляции, потому что такая ссылка является неоднозначной.
Может быть несколько путей, по которым одно и то же объявление поля может унаследоваться от интерфейса. В такой ситуации поле считается унаследованным только один раз, и к нему можно обратиться по его простому имени без неоднозначности.
Значение, хранящееся в поле типа float, всегда является элементом набора значений с плавающей точкой (§4.2.3); аналогично, значение, хранящееся в поле типа double, всегда является элементом набора значений с двойной точностью. Не допускается, чтобы поле типа float содержало элемент набора значений с расширенным порядком плавающей точки, который не является также элементом набора значений с плавающей точкой, а также для поля типа double содержать элемент набора значений с расширенным порядком двойной точности, который не является также элементом набора значений с двойной точностью.
Пример 8.3-1. Поля, унаследованные несколько раз
Класс может унаследовать два или более поля с одинаковым именем, либо от двух интерфейсов, либо от суперкласса и интерфейса. Ошибка компиляции возникает при любой попытке обратиться к любому неоднозначно унаследованному полю по его простому имени. К таким полям можно получить доступ с помощью квалифицированного имени или выражения доступа к полю, содержащего ключевое слово super (§15.11.2). В программе:
interface Frob { float v = 2.0f; }
class SuperTest { int v = 3; }
class Test extends SuperTest implements Frob {
public static void main(String[] args) {
new Test().printV();
}
void printV() { System.out.println(v); }
}
класс Test наследует два поля с именем v, одно от своего суперкласса SuperTest и одно от своего суперинтерфейса Frob. Это само по себе разрешается, но возникает ошибка компиляции из-за использования простого имени v в методе printV: невозможно определить, какое v подразумевается.
В следующем варианте используется выражение доступа к полю super.v для ссылки на поле с именем v, объявленное в классе SuperTest, и квалифицированное имя Frob.v для ссылки на поле с именем v, объявленное в интерфейсе Frob:
interface Frob { float v = 2.0f; }
class SuperTest { int v = 3; }
class Test extends SuperTest implements Frob {
public static void main(String[] args) {
new Test().printV();
}
void printV() {
System.out.println((super.v + Frob.v)/2);
}
}
Он компилируется и выводит:
2.5
Даже если два разных унаследованных поля имеют один и тот же тип, одно и то же значение и оба final, любая ссылка на любое из этих полей по простому имени считается неоднозначной и приводит к ошибке компиляции. В программе:
interface Color { int RED=0, GREEN=1, BLUE=2; }
interface TrafficLight { int RED=0, YELLOW=1, GREEN=2; }
class Test implements Color, TrafficLight {
public static void main(String[] args) {
System.out.println(GREEN); // compile-time error
System.out.println(RED); // compile-time error
}
}
не удивительно, что ссылка на GREEN должна считаться неоднозначной, потому что класс Test наследует два разных объявления для GREEN с разными значениями. Суть этого примера заключается в том, что ссылка на RED также считается неоднозначной, потому что унаследованы два различных объявления. Тот факт, что два поля с именем RED имеют один и тот же тип и одно и то же неизменяемое значение, не влияет на это суждение.
Пример 8.3-2. Повторное наследование полей
Если одно и то же объявление поля унаследовано от интерфейса по нескольким путям, поле считается унаследованным только один раз. К нему можно обратиться по его простому имени без неоднозначности. Например, в коде:
interface Colorable {
int RED = 0xff0000, GREEN = 0x00ff00, BLUE = 0x0000ff;
}
interface Paintable extends Colorable {
int MATTE = 0, GLOSSY = 1;
}
class Point { int x, y; }
class ColoredPoint extends Point implements Colorable {}
class PaintedPoint extends ColoredPoint implements Paintable {
int p = RED;
}
поля RED, GREEN и BLUE наследуются классом PaintedPoint как через его непосредственный суперкласс ColoredPoint, так и через его непосредственный суперинтерфейс Paintable. Простые имена RED, GREEN и BLUE тем не менее могут использоваться без неоднозначности внутри класса PaintedPoint для ссылки на поля, объявленные в интерфейсе Colorable.
МодификаторыПолей:
МодификаторПоля
МодификаторыПолей МодификаторПоля
МодификаторПоля: один из
Аннотация public protected private
static final transient volatile
Если аннотация a (§9.7) в объявлении поля соответствует типу аннотации T (§9.6), и T имеет (мета-)аннотацию m, которая соответствует java.lang.annotation.Target, то m должно иметь элемент со значением java.lang.annotation.ElementType.FIELD, в противном случае произойдёт ошибка компиляции.
Модификаторы доступа public, protected и private описаны в §6.6.
Ошибка компиляции возникает, если один и тот же модификатор встречается более одного раза в объявлении поля или если в объявлении поля используется более одного модификатора доступа public, protected и private.
Если в объявлении поля присутствуют два или более (различных) модификатора полей, то, хотя и не обязательно, принято, что они располагаются в порядке, соответствующем порядку в производстве для МодификатораПоля, показанном выше.
Если поле объявлено static, то существует ровно одна его инстанциация, независимо от того, сколько экземпляров (возможно, ноль) класса будет создано впоследствии. Статическое поле, иногда называемое классовой переменной, инициализируется при инициализации класса (§12.4).
Поле, которое не объявлено static (иногда называемое не-static полем), называется переменной экземпляра. При создании нового экземпляра класса (§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). Как классовые, так и переменные экземпляра (static и не-static поля) могут быть объявлены final.
Ошибка компиляции возникает, если пустая final (§4.12.4) классовая переменная не определена однозначно (§16.8) статическим инициализатором (§8.7) класса, в котором она объявлена.
Пустая final переменная экземпляра должна быть однозначно инициализирована (§16.9) в конце каждого конструктора (§8.8) класса, в котором она объявлена; в противном случае произойдёт ошибка компиляции.
Переменные могут быть помечены как transient, чтобы указать, что они не являются частью состояния объекта, подлежащего сохранению.
Пример 8.3.1.3-1. Сохранение временных полей
Если экземпляр класса Point:
class Point {
int x, y;
transient float rho, theta;
}
был сохранён в долговременное хранилище системой-сервисом, то будут сохранены только поля x и y. В данном спецификации не рассматриваются детали таких сервисов; см. спецификацию java.io.Serializable для примера такого сервиса.
Язык программирования Java позволяет потокам доступа к общим переменным (§17.1). Как правило, чтобы гарантировать согласованное и надежное обновление общих переменных, поток должен обеспечивать для них исключительное использование, получив блокировку, которая обычно обеспечивает взаимное исключение для этих общих переменных.
Язык программирования Java предоставляет второй механизм, поля volatile, который для некоторых целей удобнее, чем блокировки.
Поле может быть объявлено volatile, в этом случае Java Memory Model гарантирует, что все потоки видят согласованное значение переменной (§17.4).
Если переменная объявлена volatile, то она не может быть также объявлена с модификатором synchronized.
Пример 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) объявленной переменной, и:
-
Если декларатор относится к переменной класса (то есть, к полю класса), то инициализатор переменной вычисляется и присваивание выполняется ровно один раз, когда класс инициализируется (§12.4.2).
-
Если декларатор относится к переменной экземпляра (то есть, к полю, которое не является полем класса), то инициализатор переменной вычисляется и присваивание выполняется каждый раз, когда создается экземпляр класса (§12.5).
Пример 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.
Проверка исключений для инициализатора переменной в объявлении поля указана в §11.2.3.
Инициализаторы переменных также используются в операторах объявления локальных переменных (§14.4), где инициализатор вычисляется и присваивание выполняется каждый раз при выполнении оператора объявления локальной переменной.
Если ссылка с помощью простого имени на любую переменную экземпляра появляется в выражении инициализации переменной класса, то происходит ошибка времени компиляции.
Если ключевое слово this (§15.8.3) или ключевое слово super (§15.11.2, §15.12) встречается в выражении инициализации переменной класса, то происходит ошибка времени компиляции.
Во время выполнения поля static, которые являются final и инициализированы константными выражениями (§15.28), инициализируются в первую очередь (§12.4.2). Это также относится к таким полям в интерфейсах (§9.3.1). Эти поля являются «константами», которые никогда не будут наблюдаться с их значениями по умолчанию (§4.12.5), даже хитрыми программами (§13.4.9).
Использование переменных класса, чьи объявления появляются текстово после использования, иногда ограничено, даже если эти переменные класса находятся в области видимости. См. §8.3.2.3 для точных правил, касающихся ссылок вперед на переменные класса.
Выражения инициализации для переменных экземпляра могут использовать простое имя любой статической переменной, объявленной в классе или унаследованной классом, даже если объявление происходит текстово позже.
Пример 8.3.2.2-1. Инициализация полей вне порядка
class Test {
float f = j;
static int j = 1;
}
Эта программа компилируется без ошибок; она инициализирует j значением 1 при инициализации класса Test, и инициализирует f текущим значением j каждый раз при создании экземпляра класса Test.
Выражения инициализации для переменных экземпляра разрешено ссылаться на текущий объект this (§15.8.3) и использовать ключевое слово super (§15.11.2, §15.12).
Использование переменных экземпляра, чьи объявления появляются текстово после использования, иногда ограничено, даже если эти переменные экземпляра находятся в области видимости. См. §8.3.2.3 для точных правил, касающихся ссылок вперед на переменные экземпляра.
Объявление члена должно появляться текстово до его использования только если член является полем экземпляра (соответственно полем класса) класса или интерфейса C и выполняются все следующие условия:
-
Использование происходит в инициализаторе переменной экземпляра (соответственно переменной класса) C или в инициализаторе экземпляра (соответственно инициализаторе класса) C.
-
Использование не происходит в левой части присваивания.
-
Использование происходит через простое имя.
-
C является самым внутренним классом или интерфейсом, содержащим использование.
Ошибка времени компиляции возникает, если не выполнены какие-либо из четырех требований выше.
Пример 8.3.2.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;
}
даже если конструктор (§8.8) для Test ссылается на поле k, которое объявлено на три строки позже.
Вышеупомянутые ограничения предназначены для выявления на этапе компиляции циклических или других неверных инициализаций. Таким образом, оба:
class Z {
static int i = j + 2;
static int j = 4;
}
и:
class Z {
static { i = j + 2; }
static int i, j;
static { j = 4; }
}
приводят к ошибкам времени компиляции. Доступы методами не проверяются таким образом, поэтому:
class Z {
static int peek() { return j; }
static int i = peek();
static int j = 1;
}
class Test {
public static void main(String[] args) {
System.out.println(Z.i);
}
}
выводит:
0
потому что инициализатор переменной для i использует метод класса peek для доступа к значению переменной j, прежде чем j был инициализирован своим инициализатором переменной, в этот момент у него все еще есть значение по умолчанию (§4.12.5).
Более сложный пример:
class UseBeforeDeclaration {
static {
x = 100;
// ok - assignment
int y = x + 1;
// error - read before declaration
int v = x = 3;
// ok - x at left hand side of assignment
int z = UseBeforeDeclaration.x * 2;
// ok - not accessed via simple name
Object o = new Object() {
void foo() { x++; }
// ok - occurs in a different class
{ x++; }
// ok - occurs in a different class
};
}
{
j = 200;
// ok - assignment
j = j + 1;
// error - right hand side reads before declaration
int k = j = j + 1;
// error - illegal forward reference to j
int n = j = 300;
// ok - j at left hand side of assignment
int h = j++;
// error - read before declaration
int l = this.j * 3;
// ok - not accessed via simple name
Object o = new Object() {
void foo(){ j++; }
// ok - occurs in a different class
{ j = j + 1; }
// ok - occurs in a different class
};
}
int w = x = 3;
// ok - x at left hand side of assignment
int p = x;
// ok - instance initializers may access static fields
static int u =
(new Object() { int bar() { return x; } }).bar();
// ok - occurs in a different class
static int x;
int m = j = 4;
// ok - j at left hand side of assignment
int o =
(new Object() { int bar() { return j; } }).bar();
// ok - occurs in a different class
int j;
}
Метод объявляет исполняемый код, который можно вызвать, передав фиксированное количество значений в качестве аргументов.
ОбъявлениеМетода:
ЗаголовокМетода ТелоМетода
ЗаголовокМетода:
МодификаторыМетодаопт ТипыПараметровопт ТипВозвращаемогоЗначения ОбъявлениеМетода СписокИсключенийопт
ОбъявлениеМетода:
Идентификатор ( СписокФормальныхПараметровопт )
Список формальных параметров описан в §8.4.1, раздел модификаторов метода в §8.4.3, раздел типов параметров в §8.4.4, раздел типа возвращаемого значения в §8.4.5, раздел списка исключений в §8.4.6, а тело метода в §8.4.7.
Идентификатор в объявлении метода может использоваться в имени для ссылки на метод.
При компиляции возникает ошибка, если тело класса объявляет два метода с эквивалентными сигнатурами (§8.4.2).
Область видимости и затенение объявления метода определены в §6.3 и §6.4.
Для совместимости со старыми версиями платформы Java SE, объявление метода, возвращающего массив, может поместить (некоторые или все) пустые пары квадратных скобок, формирующие объявление типа массива, после списка формальных параметров. Это поддерживается устаревшей продукцией, но не должно использоваться в новом коде.
ОбъявлениеМетода:
ОбъявлениеМетода [ ]
Формальные параметры метода или конструктора, если таковые имеются, указываются в списке параметров, разделённых запятыми. Каждый параметр состоит из типа (предваряемого необязательно модификатором final и/или одной или несколькими аннотациями) и идентификатора (необязательно с последующими квадратными скобками), который указывает имя параметра.
Последний формальный параметр метода или конструктора является специальным: он может быть параметром с переменным числом аргументов, указанным эллипсом после типа.
Если последний формальный параметр является параметром с переменным числом аргументов, то метод является методом с переменным числом аргументов. В противном случае – метод с фиксированным числом аргументов.
Если метод или конструктор не имеют формальных параметров, в объявлении метода или конструктора появляется только пустая пара скобок.
СписокФормальныхПараметров:
ПоследнийФормальныйПараметр
СписокФормальныхПараметров , ПоследнийФормальныйПараметр
СписокФормальныхПараметров:
ФормальныйПараметр
СписокФормальныхПараметров , ФормальныйПараметр
ФормальныйПараметр:
МодификаторыПеременныхопт Тип ИмяПеременной
МодификаторыПеременных:
МодификаторПеременной
МодификаторыПеременных МодификаторПеременной
МодификаторПеременной: один из
Аннотация final
ПоследнийФормальныйПараметр:
МодификаторыПеременныхопт Тип... ИмяПеременной
ФормальныйПараметр
Следующее повторяется из §8.3, чтобы сделать представление здесь более ясным:
ИмяПеременной:
Идентификатор
ИмяПеременной [ ]
Если аннотация a (§9.7) на формальном параметре соответствует типу аннотации T (§9.6), и T имеет (мета-)аннотацию m, которая соответствует java.lang.annotation.Target, то m должно иметь элемент, значение которого равно java.lang.annotation.ElementType.PARAMETER, в противном случае возникает ошибка компиляции.
Область видимости и затенение формального параметра описаны в §6.3 и §6.4.
Ошибка компиляции возникает, если метод или конструктор объявляют два формальных параметра с одинаковым именем. (То есть в их объявлениях используется тот же Идентификатор.)
Ошибка компиляции возникает, если формальный параметр, объявленный final, присваивается внутри тела метода или конструктора.
Ошибка компиляции возникает при использовании смешанной нотации массивов (§10.2) для параметра с переменным числом аргументов.
Тип формального параметра обозначается типом Type, который появляется в его спецификации параметра, за которым следуют любые пары квадратных скобок, следующие за Идентификатором в деклараторе, за исключением параметра с переменным числом аргументов, чей декларированный тип является типом массива, а тип компонента массива – это Type, который появляется в спецификации параметра.
Если декларированный тип параметра с переменным числом аргументов имеет тип элемента, не поддающийся реализации (§4.7), то при объявлении метода с переменным числом аргументов возникает предупреждение о не проверяемой типизации, если метод не помечен аннотацией SafeVarargs (§9.6.3.7) или предупреждение о не проверяемой типизации подавляется аннотацией SuppressWarnings (§9.6.3.5).
При вызове метода или конструктора (§15.12) значения выражений фактических аргументов инициализируют созданные переменные параметров, каждый из декларированного типа, перед выполнением тела метода или конструктора. Идентификатор, который появляется в DeclaratorId, может использоваться как простое имя в теле метода или конструктора для ссылки на формальный параметр.
Вызовы метода с переменным числом аргументов могут содержать больше выражений фактических аргументов, чем формальных параметров. Все выражения фактических аргументов, не соответствующие формальным параметрам, предшествующим параметру с переменным числом аргументов, будут вычислены, и результаты будут сохранены в массив, который будет передан в вызов метода (§15.12.4.2).
Параметр метода или конструктора типа float всегда содержит элемент из набора значений типа float (§4.2.3); аналогично, параметр метода или конструктора типа double всегда содержит элемент из набора значений типа double. Недопустимо, чтобы параметр метода или конструктора типа float содержал элемент из набора значений с расширенным экспоненциальным представлением float, который не является элементом набора значений float, ни для параметра метода типа double содержать элемент из набора значений с расширенным экспоненциальным представлением double, который не является элементом набора значений double.
В тех случаях, когда выражение фактического аргумента, соответствующее переменной параметра, не строгое FP (§15.4), оценка этого выражения фактического аргумента может использовать промежуточные значения, взятые из соответствующих наборов значений с расширенным экспоненциальным представлением. Перед сохранением в переменной параметра результат такого выражения сопоставляется с ближайшим значением в соответствующем стандартном наборе значений методом преобразования вызова (§5.3).
Два метода имеют одинаковую подпись, если у них одинаковые имена и типы аргументов.
Два объявления методов или конструкторов M и N имеют одинаковые типы аргументов, если выполняются все следующие условия:
-
У них одинаковое количество формальных параметров (возможно, ноль).
-
У них одинаковое количество параметров типа (возможно, ноль).
-
Пусть A1, ..., An — параметры типа
M, а B1, ..., Bn — параметры типаN. После переименования каждого вхождения Bi в типеNна Ai границы соответствующих переменных типа совпадают, и типы формальных параметровMиNсовпадают.
Подпись метода 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. Вместо этого код был бы незаконным. Это значительно затруднило бы использование дженериков, так как авторы библиотек колебались бы мигрировать существующий код.
Модификаторы методов:
Модификатор метода
Модификаторы методов Модификатор метода
Модификатор метода: один из
Аннотация public protected private abstract
static final synchronized native strictfp
Если аннотация a (§9.7) в объявлении метода соответствует типу аннотации T (§9.6), и T имеет (мета-)аннотацию m, которая соответствует java.lang.annotation.Target, то m должно иметь элемент со значением java.lang.annotation.ElementType.METHOD; в противном случае возникает ошибка компиляции.
Возникает ошибка компиляции, если один и тот же модификатор встречается более одного раза в объявлении метода, или если объявление метода содержит более одного из модификаторов доступа public, protected и private (§6.6).
Возникает ошибка компиляции, если в объявлении метода присутствует ключевое слово abstract, а также любое из ключевых слов private, static, final, native, strictfp или synchronized.
Возникает ошибка компиляции, если в объявлении метода присутствует ключевое слово native, а также strictfp.
Если в объявлении метода указано два или более (различных) модификатора метода, то обычно (но не обязательно) их порядок соответствует порядку, показанному выше в производной для Модификатор метода.
Объявление abstract метода вводит метод в качестве члена, определяя его сигнатуру (§8.4.2), тип возвращаемого значения и throws предложение (если таковое имеется), но не предоставляет реализацию.
Объявление abstract метода m должно находиться непосредственно внутри abstract класса (назовите его A), если оно не содержится внутри перечисления (§8.9); в противном случае возникает ошибка компиляции.
Каждый подкласс A, который не является abstract (§8.1.1.1), должен предоставить реализацию для m; в противном случае возникает ошибка компиляции.
Подкласс не мог бы реализовать private abstract метод, потому что private методы не наследуются подклассами; поэтому такой метод никогда не мог бы использоваться.
abstract класс может переопределять abstract метод, предоставляя другое abstract объявление метода.
Это может предоставить место для документационного комментария, уточнения типа возвращаемого значения или объявления, что набор проверочных исключений (§11.2), которые могут быть брошены данным методом, когда он реализуется его подклассами, должен быть более ограничен.
Метод экземпляра, который не является abstract, может быть переопределен abstract методом.
Пример 8.4.3.1-1. Переопределение абстрактного/абстрактного метода
class BufferEmpty extends Exception {
BufferEmpty() { super(); }
BufferEmpty(String s) { super(s); }
}
class BufferError extends Exception {
BufferError() { super(); }
BufferError(String s) { super(s); }
}
interface Buffer {
char get() throws BufferEmpty, BufferError;
}
abstract class InfiniteBuffer implements Buffer {
public abstract char get() throws BufferError;
}
Переопределяющее объявление метода get в классе InfiniteBuffer указывает, что метод get в любом подклассе InfiniteBuffer никогда не бросает исключение BufferEmpty, предположительно потому, что он генерирует данные в буфере и, следовательно, никогда не может закончиться данными.
Пример 8.4.3.1-2. Переопределение абстрактного/не-абстрактного метода
Мы можем объявить abstract класс Point, который требует, чтобы его подклассы реализовывали toString, если они должны быть полными, инстанцируемыми классами:
abstract class Point {
int x, y;
public abstract String toString();
}
Это abstract объявление toString переопределяет не-abstract toString метод класса Object. (Класс Object - это неявный непосредственный суперкласс класса Point.) Добавление кода:
class ColoredPoint extends Point {
int color;
public String toString() {
return super.toString() + ": color " + color; // error
}
}
приводит к ошибке компиляции, потому что вызов super.toString() ссылается на метод toString в классе Point, который является abstract и поэтому не может быть вызван. Метод toString класса Object может быть доступен классу ColoredPoint только если класс Point явно делает его доступным через какой-либо другой метод, как в:
abstract class Point {
int x, y;
public abstract String toString();
protected String objString() { return super.toString(); }
}
class ColoredPoint extends Point {
int color;
public String toString() {
return objString() + ": color " + color; // correct
}
}
Метод, объявленный static, называется методом класса.
Возникает ошибка компиляции при использовании имени параметра типа любого окружающего объявления в заголовке или теле метода класса.
Метод класса всегда вызывается без ссылки на конкретный объект. Возникает ошибка компиляции при попытке обратиться к текущему объекту с использованием ключевого слова this (§15.8.3) или ключевого слова super (§15.11.2).
Метод, не объявленный static, называется методом экземпляра, а иногда и не-static методом.
Метод экземпляра всегда вызывается относительно объекта, который становится текущим объектом, к которому ключевые слова this и super ссылаются во время выполнения тела метода.
Метод может быть объявлен final, чтобы предотвратить переопределение или скрытие его подклассами.
Возникает ошибка компиляции при попытке переопределения или скрытия final метода.
private метод и все методы, объявленные непосредственно внутри final класса (§8.1.1.2), ведут себя так, как если бы они были final, поскольку их переопределение невозможно.
Во время выполнения генератор машинного кода или оптимизатор может «встраивать» тело final метода, заменяя вызов метода кодом в его теле. Процесс встраивания должен сохранять семантику вызова метода. В частности, если целевой объект вызова метода экземпляра является null, то должно быть брошено исключение NullPointerException, даже если метод встроен. Компилятор Java должен гарантировать, что исключение будет брошено в правильной точке, чтобы фактические аргументы метода были видны как оцененные в правильном порядке до вызова метода.
Рассмотрим пример:
final class Point {
int x, y;
void move(int dx, int dy) { x += dx; y += dy; }
}
class Test {
public static void main(String[] args) {
Point[] p = new Point[100];
for (int i = 0; i < p.length; i++) {
p[i] = new Point();
p[i].move(i, p.length-1-i);
}
}
}
Встраивание метода move класса Point в методе main преобразовало бы for цикл в форму:
for (int i = 0; i < p.length; i++) {
p[i] = new Point();
Point pi = p[i];
int j = p.length-1-i;
pi.x += i;
pi.y += j;
}
Цикл, возможно, будет подвергнут дальнейшей оптимизации.
Такое встраивание не может быть выполнено во время компиляции, если нельзя гарантировать, что Test и Point всегда будут перекомпилированы вместе, так что каждый раз, когда Point — и, в частности, его move метод — изменяется, код для Test.main также будет обновлён.
Метод, который является native, реализован в платформозависимом коде, обычно написанном на другом языке программирования, таком как C. Тело native метода задаётся только точкой с запятой, указывающей, что реализация опущена, вместо блока.
Например, класс 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).
Объявление обобщённого метода определяет набор методов, по одному для каждого возможного вызова раздела параметров типа с аргументами типа. Аргументы типа могут не нуждаться в явном предоставлении при вызове обобщённого метода, так как они часто могут быть выведены (§15.12.2.7).
Область действия и затемнение параметра типа метода указаны в §6.3.
Результат объявления метода либо объявляет тип значения, которое метод возвращает (тип возврата), либо использует ключевое слово void, чтобы указать, что метод не возвращает значение.
Результат:
Тип
void
Типы возврата могут различаться среди методов, которые переопределяют друг друга, если типы возврата являются ссылочными типами. Понятие замещаемости типа возврата поддерживает ковариантные возвращаемые значения, то есть специализацию типа возврата на подтип.
Объявление метода d1 с типом возврата R1 является заменяемым по типу возврата для другого метода d2 с типом возврата R2, если и только если выполняются следующие условия:
-
Если R1 является
void, то R2 являетсяvoid. -
Если R1 является примитивным типом, то R2 идентичен R1.
-
Если R1 является ссылочным типом, то:
-
R1 является подтипом R2 или R1 может быть преобразован в подтип R2 с помощью неявного преобразования (§5.1.9), или
-
R1 = |R2|
-
Неявное преобразование допускается в определении, несмотря на то, что оно некорректно, как специальное разрешение для беспрепятственной миграции из необобщенного в обобщенный код. Если неявное преобразование используется для определения того, что R1 замещаемо по типу возврата для R2, то R1 обязательно не является подтипом R2, и правила переопределения (§8.4.8.3, §9.4.1) потребуют предупреждения о неявном преобразовании во время компиляции.
Оператор throws используется для объявления всех типов проверяемых исключений (§11.1.1), которые могут быть брошены инструкциями в теле метода или конструктора (§11.2.2).
Бросает:
throws ExceptionTypeList
ExceptionTypeList:
ExceptionType
ExceptionTypeList , ExceptionType
ExceptionType:
ИмяТипа
ПеременнаяТипа
Это ошибка компиляции, если какой-либо ExceptionType, указанный в операторе throws, не является подтипом (§4.10) от Throwable.
Переменные типа допускаются в операторе throws, хотя они не допускаются в операторе catch.
Пример 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 */ }
}
}
Разрешается, но не требуется упоминание типов непроверяемых исключений (§11.1.1) в операторе throws.
Взаимосвязь между оператором throws и проверкой исключений для метода или тела конструктора указана в §11.2.3.
По существу, для каждого проверяемого исключения, которое может возникнуть в результате выполнения тела метода или конструктора, возникает ошибка компиляции, если его тип исключения или супертип его типа исключения не указан в операторе throws в объявлении метода или конструктора.
Требование объявлять проверяемые исключения позволяет компилятору Java гарантировать, что код для обработки таких условий ошибок был включен. Методы или конструкторы, которые не обрабатывают исключительные ситуации, брошенные как проверяемые исключения в их телах, обычно вызовут ошибки компиляции, если им не хватает соответствующих типов исключений в их операторах throws. Таким образом, язык программирования Java поощряет стиль программирования, где редкие и в остальном действительно исключительные условия документируются таким образом.
Взаимосвязь между оператором throws метода и операторами throws переопределяемых или скрываемых методов указана в §8.4.8.3.
Тело метода — это либо блок кода, реализующий метод, либо просто точка с запятой, указывающая на отсутствие реализации.
ТелоМетода:
Блок
;
Ошибка компиляции, если объявление метода имеет либо abstract, либо native и имеет блок в качестве тела.
Ошибка компиляции, если объявление метода не имеет ни abstract, ни native и имеет точку с запятой в качестве тела.
Если для объявленного void метода требуется реализация, но она не требует исполняемого кода, тело метода должно быть записано как блок, не содержащий операторов: "{ }".
Если метод объявлен void, то его тело не должно содержать оператор return (§14.17), имеющий Выражение, иначе произойдёт ошибка компиляции.
Если метод объявлен с типом возврата, то каждый оператор return (§14.17) в его теле должен иметь Выражение, иначе произойдёт ошибка компиляции.
Если метод объявлен с типом возврата, то при нормальном завершении выполнения тела метода (§14.1) произойдёт ошибка компиляции.
Другими словами, метод с типом возврата должен возвращать значение только с помощью оператора return, который предоставляет значение возврата; метод не может завершиться, "доходя до конца своего тела".
Обратите внимание, что метод может быть объявлен с типом возврата, но при этом не содержать операторов return. Вот один пример:
class DizzyDean {
int pitch() { throw new RuntimeException("90 mph?!"); }
}
Класс C наследует от своего непосредственного суперкласса и непосредственных суперинтерфейсов все abstract и не-abstract методы суперкласса и суперинтерфейсов, которые public, protected или объявлены с доступом по умолчанию в том же пакете, что и C, и не переопределены (§8.4.8.1) и не скрыты (§8.4.8.2) объявлением в классе.
Методы переопределяются или скрываются на основе сигнатуры.
Например, если класс объявляет два public метода с одинаковым именем (§8.4.9), и подкласс переопределяет один из них, подкласс всё ещё наследует другой метод.
Если метод не наследуется, он объявлен в классе, или метод не наследуется, он объявлен в интерфейсе и новое объявление является abstract, то новое объявление называется переопределением его.
Если метод не наследуется abstract, а новое объявление не является abstract, то новое объявление называется реализацией его.
Метод экземпляра m1, объявленный в классе C, переопределяет другой метод экземпляра m2, объявленный в классе A, если выполняются все следующие условия:
-
C является подклассом A.
-
Подпись
m1является подсигнатурой (§8.4.2) подписиm2. -
Либо:
-
m2имеет доступpublic,protectedили объявлен с доступом по умолчанию в том же пакете, что и C, или -
m1переопределяет методm3(m3отличается отm1,m3отличается отm2), при этомm3переопределяетm2.
-
Кроме того, если m1 не является abstract, то m1 называется реализацией всех и всех объявления abstract методов, которые он переопределяет.
Подпись переопределяемого метода может отличаться от переопределяемого, если формальный параметр в одном из методов имеет сырой тип, а соответствующий параметр в другом — параметризованный тип.
Правила позволяют подписи переопределяемого метода отличаться от переопределяемого, чтобы обеспечить миграцию существующего кода для использования возможностей дженериков. См. §8.4.2 для более подробного анализа.
Ошибка компиляции, если метод экземпляра переопределяет static метод.
В этом отношении переопределение методов отличается от скрытия полей (§8.3), так как разрешено, чтобы переменная экземпляра скрывала переменную static.
К переопределённому методу можно получить доступ, используя выражение вызова метода (§15.12), содержащее ключевое слово super.
Квалифицированное имя или приведение к типу суперкласса неэффективны для попытки получить доступ к переопределённому методу; в этом отношении переопределение методов отличается от скрытия полей (§15.12.4.4).
Наличие или отсутствие модификатора 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.
Если класс объявляет метод static метод m, то объявление m считается скрывающим любой метод m', где сигнатура m является подсигнатурой (§8.4.2) сигнатуры m' в суперклассах и суперинтерфейсах класса, к которому в противном случае код в классе имел бы доступ.
Если метод 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, то тип R1 должен быть совместим с типом R2 (§8.4.5), иначе произойдёт ошибка компиляции.
Это правило допускает ковариативные типы возвращаемых значений — уточнение типа возвращаемого значения метода при его переопределении.
Если R1 не является подтипом R2, то возникает предупреждение компилятора, которое можно отключить с помощью аннотации SuppressWarnings (§9.6.3.5).
Метод, переопределяющий или скрывающий другой метод, включая методы, реализующие abstract методы, определённые в интерфейсах, не может объявлять бросать больше проверок исключений, чем переопределяемый или скрываемый метод.
В этом отношении переопределение методов отличается от скрытия полей (§8.3), так как поле может скрывать поле другого типа.
Более точно, предположим, что B — это класс или интерфейс, а A — это суперкласс или суперинтерфейс B, и объявление метода n в B переопределяет или скрывает объявление метода m в A. Тогда:
-
Если
nсодержитthrowsсписок типов проверяемых исключений, тоmдолжен содержатьthrowsсписок типов проверяемых исключений, иначе произойдёт ошибка компиляции. -
Для каждого типа проверяемого исключения, указанного в списке
throwsиз объявленияn, тот же класс исключения или один из его супертипов должен присутствовать в стирании (§4.6) спискаthrowsиз объявленияm; в противном случае произойдёт ошибка компиляции. -
Если нестираемый
throwsсписок изmне содержит супертип каждого типа исключения изthrowsсписка изn, то происходит предупреждение компилятора.
Ошибка компиляции возникает, если объявление типа T имеет член-метод m1 и существует метод m2, объявленный в T или супертипе T, который удовлетворяет всем следующим условиям:
-
m1иm2имеют одинаковое имя. -
m2доступен из T. -
Подпись
m1не является подписью (§8.4.2) подписиm2. -
Подпись
m1или какой-либо методm1, который он переопределяет (прямо или косвенно), имеет то же стирание, что и подписьm2или какой-либо методm2, который он переопределяет (прямо или косвенно).
Эти ограничения необходимы, потому что дженерики реализуются через стирание. Правило выше подразумевает, что методы, объявленные в одном классе с одинаковым именем, должны иметь разные стирания. Оно также подразумевает, что объявление типа не может реализовать или расширить две различные вызовы одного и того же обобщённого интерфейса.
Модификатор доступа (§6.6) переопределяемого или скрываемого метода должен обеспечивать не меньший доступ, чем переопределяемый или скрываемый метод, следующим образом:
-
Если переопределяемый или скрываемый метод имеет модификатор доступа
public, то переопределяющий или скрывающий метод должен иметь модификатор доступаpublic; в противном случае произойдёт ошибка компиляции. -
Если переопределяемый или скрываемый метод имеет модификатор доступа
protected, то переопределяющий или скрывающий метод должен иметь модификатор доступаprotectedилиpublic; в противном случае произойдёт ошибка компиляции. -
Если переопределяемый или скрываемый метод имеет доступ по умолчанию (пакетный доступ), то переопределяющий или скрывающий метод не должен иметь модификатор доступа
private; в противном случае произойдёт ошибка компиляции.
Обратите внимание, что private метод не может быть скрыт или переопределён в техническом смысле этих терминов. Это означает, что подкласс может объявить метод с той же подписью, что и private метод в одном из своих суперклассов, и нет требования, чтобы тип возвращаемого значения или throws список такого метода были связаны каким-либо образом с типом возвращаемого значения или private списком 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, то это ошибка компиляции. Такая ситуация может возникнуть, если суперкласс является обобщенным, и у него есть два метода, которые были различными в обобщенном объявлении, но имеют одинаковую подпись в конкретном вызове.
В противном случае возможны два случая:
-
Если один из унаследованных методов не является
abstract, то есть два подслучая:-
Если метод, который не является
abstract, являетсяstatic, то возникает ошибка компиляции. -
В противном случае метод, который не является
abstract, рассматривается как переопределяющий и, следовательно, реализующий все остальные методы от имени наследующего класса.Если подпись метода, который не является
abstract, не является подподписью каждого из других унаследованных методов, то возникает неустранимая предупреждение компиляции, если оно не подавлено аннотациейSuppressWarnings(§9.6.3.5).Если тип возвращаемого значения метода, который не является
abstract, не является подтипом типа возвращаемого значения любого из других унаследованных методов, то возникает неустранимая предупреждение компиляции, если оно не подавлено аннотациейSuppressWarnings(§9.6.3.5).Ошибка компиляции возникает, если тип возвращаемого значения метода, который не является
abstract, не является замещаемым типом возвращаемого значения (§8.4.5) для каждого из других унаследованных методов.Ошибка компиляции возникает, если унаследованный метод, который не является
abstract, имеет секциюthrows, которая конфликтует (§8.4.6) с любым другим унаследованным методом.
-
-
Если все унаследованные методы являются
abstract, то класс обязательно является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, независимо от типа переменной, которую мы можем использовать для хранения ссылки на объект. Таким образом, мы видим, что поля и методы ведут себя по-разному: скрытие отличается от переопределения.
Класс, объявление которого непосредственно заключено в объявлении другого класса или интерфейса, называется классом-членом.
Интерфейс, объявление которого непосредственно заключено в объявлении другого класса или интерфейса, называется интерфейсом-членом.
Доступность типа-члена в объявлении класса задаётся в §6.6.
Если объявление типа-члена содержит более одного из модификаторов доступа public, protected и private, возникает ошибка компиляции.
Объявления типов-членов могут иметь аннотационные модификаторы (§9.7), как и любые другие объявления типов или членов.
Область действия и перекрытие типа-члена определяются в §6.3 и §6.4.
Если класс объявляет тип-член с определенным именем, то объявление этого типа скрывает все доступные объявления типов-членов с тем же именем в суперклассах и суперинтерфейсах класса.
В этом отношении скрытие типов-членов аналогично скрытию полей (§8.3).
Класс наследует все типы-члены, которые не являются private, из своего непосредственного суперкласса и непосредственных суперинтерфейсов, которые доступны коду в классе и не скрыты объявлением в классе.
Класс может унаследовать два или более объявления типов с одинаковым именем, либо от двух интерфейсов, либо от своего суперкласса и интерфейса. Попытка обратиться к любому унаследованному классу или интерфейсу с одинаковым именем считается ошибкой компиляции.
Если одно и то же объявление типа наследуется от нескольких интерфейсов, класс или интерфейс считается унаследованным только один раз. К нему можно обратиться по его простому имени без неоднозначности.
Ключевое слово static может изменять объявление типа-члена C в теле невложенного класса или интерфейса T. Его эффект заключается в том, что C не является вложенным классом. Так же как метод static класса T не имеет текущего экземпляра T в своем теле, C также не имеет текущего экземпляра T, а также не имеет каких-либо лексически окружающих экземпляров.
Если класс static содержит использование не-static члена окружающего класса, возникает ошибка компиляции.
Интерфейс-член неявно static (§9.1.1). Разрешается избыточно указывать модификатор static при объявлении интерфейса-члена.
Инициализатор экземпляра, объявленный в классе, выполняется при создании экземпляра класса (§12.5, §15.9, §8.8.7.1).
InstanceInitializer:
Block
Если инициализатор экземпляра не может завершиться нормально, возникает ошибка компиляции (§14.21).
Если оператор return (§14.17) появляется в любом месте внутри инициализатора экземпляра, возникает ошибка компиляции.
Инициализаторы экземпляров могут ссылаться на текущий объект с помощью ключевого слова this (§15.8.3), использовать ключевое слово super (§15.11.2, §15.12) и использовать любые переменные типов в области видимости.
Использование переменных экземпляра, объявления которых появляются текстово после использования, иногда ограничено, даже если эти переменные экземпляра находятся в области видимости. Подробные правила, регулирующие обратную ссылку на переменные экземпляра, см. в §8.3.2.3.
Проверка исключений для инициализатора экземпляра описана в §11.2.3.
Статический инициализатор, объявленный в классе, выполняется при инициализации класса (§12.4.2). Вместе с любыми инициализаторами полей для переменных класса (§8.3.2), статические инициализаторы могут использоваться для инициализации переменных класса класса.
StaticInitializer:
static Block
Если статический инициализатор не может завершиться нормально, возникает ошибка компиляции (§14.21).
Если оператор return (§14.17) появляется в любом месте внутри статического инициализатора, возникает ошибка компиляции.
Если ключевое слово this (§15.8.3) или ключевое слово super (§15.11, §15.12) или любая переменная типа, объявленная за пределами статического инициализатора, появляется в любом месте внутри статического инициализатора, возникает ошибка компиляции.
Использование переменных класса, объявления которых появляются текстово после использования, иногда ограничено, даже если эти переменные класса находятся в области видимости. Подробные правила, регулирующие обратную ссылку на переменные класса, см. в §8.3.2.3.
Проверка исключений для статического инициализатора описана в §11.2.3.
Конструктор используется для создания объекта, являющегося экземпляром класса (§12.5, §15.9).
ConstructorDeclaration:
ConstructorModifiersopt ConstructorDeclarator
Throwsopt ConstructorBody
ConstructorDeclarator:
TypeParametersopt SimpleTypeName ( FormalParameterListopt )
Идентификатор SimpleTypeName в ConstructorDeclarator должен совпадать с простым именем класса, содержащего объявление конструктора; в противном случае произойдёт ошибка компиляции.
Во всех остальных аспектах объявление конструктора выглядит как объявление метода без возвращаемого значения (§8.4.5).
Объявления конструкторов не являются членами. Они никогда не наследуются и, следовательно, не подвержены скрытию или переопределению.
Пример 8.8-1. Декларации конструкторов
class Point {
int x, y;
Point(int x, int y) { this.x = x; this.y = y; }
}
Конструкторы вызываются выражениями создания экземпляров классов (§15.9), преобразованиями и конкатенациями, обусловленными оператором конкатенации строк + (§15.18.1), и явными вызовами конструкторов из других конструкторов (§8.8.7).
Конструкторы никогда не вызываются выражениями вызова метода (§15.12).
Доступ к конструкторам регулируется модификаторами доступа (§6.6).
Это полезно, например, для предотвращения создания экземпляров, объявляя недоступный конструктор (§8.8.10).
Формальные параметры и параметры типа конструктора идентичны по синтаксису и семантике параметрам методов (§8.4.1).
Ошибка компиляции возникает при объявлении двух конструкторов с эквивалентными подписями (§8.4.2) в одном классе.
Ошибка компиляции возникает при объявлении двух конструкторов с одинаковой стёртой подписью (§4.6) в одном классе.
ConstructorModifiers:
ConstructorModifier
ConstructorModifiers ConstructorModifier
ConstructorModifier: один из
Annotation public protected private
Если аннотация a (§9.7) на конструкторе соответствует типу аннотации T (§9.6), и T имеет (мета-)аннотацию m, соответствующую java.lang.annotation.Target, то m должен иметь элемент со значением java.lang.annotation.ElementType.CONSTRUCTOR, в противном случае произойдёт ошибка компиляции.
Модификаторы доступа public, protected и private рассматриваются в §6.6.
Ошибка компиляции возникает, если один и тот же модификатор встречается более одного раза в объявлении конструктора или если объявление конструктора содержит более одного из модификаторов доступа public, protected и private.
Ошибка компиляции возникает, если конструктор типа перечисления (§8.9) объявлен public или protected.
Если для конструктора обычного класса не указан модификатор доступа, конструктор имеет доступ по умолчанию.
Если для конструктора типа перечисления не указан модификатор доступа, конструктор имеет private доступ.
Если в объявлении метода присутствует два или более (различных) модификатора метода, обычно, хотя и не обязательно, они должны быть расположены в порядке, согласованном с тем, который показан выше в продукции для 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).
Объявление обобщённого конструктора определяет набор конструкторов, по одному для каждого возможного вызова раздела параметров типа с аргументами типа. Аргументы типа могут не потребоваться явно при вызове обобщённого конструктора, так как они часто могут быть выведены (§15.12.2.7).
Область действия и перекрытие параметра типа конструктора определены в §6.3 и §6.4.
Оператор throws для конструктора аналогичен оператору throws для метода (§8.4.6).
Первое утверждение тела конструктора может быть явным вызовом другого конструктора того же класса или непосредственного суперкласса (§8.8.7.1).
ТелоКонструктора:
{ ЯвныйВызовКонструкторанеобязательно БлоковыеУтверждениянеобязательно }
Ошибка компиляции возникает, если конструктор напрямую или косвенно вызывает сам себя через серию одного или нескольких явных вызовов конструкторов, включающих this.
Если конструктор является конструктором для типа перечисления (§8.9), ошибка компиляции возникает при явном вызове конструктора суперкласса.
Если тело конструктора не начинается с явного вызова конструктора, и объявляемый конструктор не является частью примитивного класса Object, то тело конструктора неявно начинается с вызова конструктора суперкласса "super();", вызова конструктора его непосредственного суперкласса, который не принимает аргументы.
За исключением возможности явных вызовов конструкторов, тело конструктора подобно телу метода (§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, передавая координаты.
ExplicitConstructorInvocation:
NonWildTypeArgumentsopt this ( ArgumentListopt ) ;
NonWildTypeArgumentsopt super ( ArgumentListopt ) ;
Primary . NonWildTypeArgumentsopt super ( ArgumentListopt ) ;
NonWildTypeArguments:
< ReferenceTypeList >
ReferenceTypeList:
ReferenceType
ReferenceTypeList , ReferenceType
Явные вызовы конструкторов можно разделить на два типа:
-
Вызовы альтернативных конструкторов начинаются с ключевого слова
this(возможно, с явными аргументами типа). Они используются для вызова альтернативного конструктора того же класса. -
Вызовы конструкторов суперкласса начинаются с ключевого слова
super(возможно, с явными аргументами типа) или выражения Primary. Они используются для вызова конструктора непосредственного суперкласса.Вызовы конструкторов суперкласса можно подразделить на:
-
Неквалифицированные вызовы конструкторов суперкласса начинаются с ключевого слова
super(возможно, с явными аргументами типа). -
Квалифицированные вызовы конструкторов суперкласса начинаются с выражения Primary.
Они позволяют конструктору подкласса явно указать экземпляр, непосредственно окружающий вновь созданный объект, относительно непосредственного суперкласса (§8.1.3). Это может потребоваться, когда суперкласс является вложенным классом.
-
Явный вызов конструктора в теле конструктора не может ссылаться на какие-либо переменные экземпляра, методы экземпляра или внутренние классы, объявленные в этом классе или любом суперклассе, или использовать this или super в любом выражении; в противном случае возникает ошибка компиляции.
Типы исключений, которые может генерировать явный вызов конструктора, указаны в §11.2.2.
Пример 8.8.7.1-1. Квалифицированный вызов конструктора суперкласса
class Outer {
class Inner {}
}
class ChildOfInner extends Outer.Inner {
ChildOfInner() { (new Outer()).super(); }
}
Пример 8.8.7.1-2. Ограничения на явные вызовы конструкторов
Если первый конструктор класса 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 не может использоваться явным вызовом конструктора.
Пусть C — класс, который создаётся, а S — непосредственный суперкласс C.
Ошибка компиляции возникает, если S недоступен (§6.6).
Если вызов конструктора суперкласса квалифицирован, то:
-
Если S не является вложенным классом или если объявление S происходит в статическом контексте, то возникает ошибка компиляции.
-
В противном случае пусть
p— это выражение Primary непосредственно перед ".super". Пусть O — самый внутренний лексически окружающий класс S.Ошибка компиляции возникает, если тип
pне O или подкласс O, или если типpнедоступен (§6.6).
Если вызов конструктора суперкласса неквалифицирован, и если S является внутренним членом класса, то это ошибка компиляции, если S не является членом лексически окружающего класса C по объявлению или наследованию.
Вычисление инструкции вызова альтернативного конструктора выполняется сначала путём вычисления аргументов конструктора слева направо, как в обычном вызове метода, а затем путём вызова конструктора.
Вычисление инструкции вызова конструктора суперкласса более сложное, как показано ниже:
-
Пусть C — класс, который создаётся, пусть S — непосредственный суперкласс C, и пусть
i— создаваемый экземпляр. -
Необходимо определить непосредственный окружающий экземпляр
iотносительно S (если он существует):-
Если S не является вложенным классом или если объявление S происходит в статическом контексте, то непосредственный окружающий экземпляр
iотносительно S не существует. -
Если вызов конструктора суперкласса квалифицирован, то выражение Primary
pнепосредственно перед ".super" вычисляется.Если
pвычисляется доnull, то генерируетсяNullPointerException, и вызов конструктора суперкласса завершается неожиданно.В противном случае результат этого вычисления является непосредственным окружающим экземпляром
iотносительно S. -
Если вызов конструктора суперкласса не квалифицирован, то:
-
Если S является локальным классом (§14.3), то пусть O — самый внутренний лексически окружающий класс S. Пусть n — целое число, такое что O является n-ым лексически окружающим классом C.
Непосредственный окружающий экземпляр
iотносительно S — это n-ый лексически окружающий экземплярthis. -
В противном случае, S является внутренним членом класса (§8.5).
Пусть O — самый внутренний лексически окружающий класс S, и пусть n — целое число, такое что O является n-ым лексически окружающим классом C.
Непосредственный окружающий экземпляр
iотносительно S — это n-ый лексически окружающий экземплярthis.
-
-
-
После определения непосредственного окружающего экземпляра
iотносительно S (если он существует), вычисление инструкции вызова конструктора суперкласса продолжается путём вычисления аргументов конструктора слева направо, как в обычном вызове метода, а затем путём вызова конструктора. -
Наконец, если инструкция вызова конструктора суперкласса завершается успешно, то выполняются все инициализаторы переменных экземпляра C и все инициализаторы экземпляра C. Если инициализатор экземпляра или инициализатор переменной экземпляра
Iтекстово предшествует другому инициализатору экземпляра или инициализатору переменной экземпляраJ, тоIвыполняется передJ.Выполнение инициализаторов переменных экземпляра и инициализаторов экземпляров выполняется независимо от того, появляется ли вызов конструктора суперкласса как явная инструкция вызова конструктора или предоставляется автоматически. (Альтернативный вызов конструктора не выполняет это дополнительное неявное выполнение.)
Если класс не содержит объявления конструкторов, то неявно объявляется конструктор по умолчанию без формальных параметров и без фрагмента throws.
Если объявляемый класс является примитивным классом Object, то конструктор по умолчанию имеет пустое тело. В противном случае конструктор по умолчанию просто вызывает конструктор суперкласса без аргументов.
Ошибка компиляции, если неявно объявлен конструктор по умолчанию, но у суперкласса нет доступного конструктора (§6.6), принимающего ноль аргументов и не имеющего фрагмента throws.
В классе типа, если класс объявлен public, то конструктору по умолчанию неявно присваивается модификатор доступа public (§6.6); если класс объявлен protected, то конструктору по умолчанию неявно присваивается модификатор доступа protected (§6.6); если класс объявлен private, то конструктору по умолчанию неявно присваивается модификатор доступа private (§6.6); в противном случае конструктор по умолчанию имеет доступ по умолчанию, неявно определяемый отсутствием модификатора доступа.
В типе перечисления конструктор по умолчанию неявно private (§8.9.2).
Пример 8.8.9-1. Конструкторы по умолчанию
Объявление:
public class Point {
int x, y;
}
эквивалентно объявлению:
public class Point {
int x, y;
public Point() { super(); }
}
где конструктор по умолчанию public, поскольку класс Point является public.
Пример 8.8.9-2. Доступность конструкторов по сравнению с классами
Правило, что конструктор по умолчанию класса имеет тот же модификатор доступа, что и сам класс, простое и интуитивное. Однако это не подразумевает, что конструктор доступен всякий раз, когда доступен сам класс. Рассмотрим:
package p1;
public class Outer {
protected class Inner {}
}
package p2;
class SonOfOuter extends p1.Outer {
void foo() {
new Inner(); // compile-time access error
}
}
Конструктор для Inner является protected. Однако конструктор является protected по отношению к Inner, в то время как Inner является protected по отношению к Outer. Таким образом, Inner доступен в SonOfOuter, так как это подкласс Outer. Конструктор Inner не доступен в SonOfOuter, поскольку класс SonOfOuter не является подклассом Inner! Следовательно, даже если Inner доступен, его конструктор по умолчанию не доступен.
Класс может быть спроектирован таким образом, чтобы код за пределами объявления класса не мог создать экземпляры класса, объявляя как минимум один конструктор для предотвращения создания неявного конструктора и делая все конструкторы private.
Класс public аналогичным образом может предотвратить создание экземпляров за пределами пакета, объявив как минимум один конструктор для предотвращения создания конструктора по умолчанию с общедоступным доступом и не объявив конструкторов, которые public.
Пример 8.8.10-1. Предотвращение создания экземпляров через доступность конструкторов
class ClassOnly {
private ClassOnly() { }
static String just = "only the lonely";
}
Здесь класс ClassOnly нельзя создать, в то время как в следующем коде:
package just;
public class PackageOnly {
PackageOnly() { }
String[] justDesserts = { "cheesecake", "ice cream" };
}
класс PackageOnly можно создать только в пакете just, в котором он объявлен.
Объявление перечисления задает новый тип перечисления.
ОбъявлениеПеречисления:
МодификаторыКлассанеобязательно enum Идентификатор Интерфейсынеобязательно ТелоПеречисления
ТелоПеречисления:
{ ПостоянныеПеречислениянеобязательно ,необязательно ОбъявленияТелаПеречислениянеобязательно }
Типы перечислений (§8.9) не должны быть объявлены abstract; это приведёт к ошибке компиляции.
Тип перечисления неявным образом final, если он не содержит ни одной константы перечисления, имеющей тело класса.
Явное объявление типа перечисления как final является ошибкой компиляции.
Вложенные типы перечислений неявным образом static. Разрешается явное объявление вложенного типа перечисления как static.
Это подразумевает, что невозможно определить локальное (§14.3) перечисление или перечисление во внутреннем классе (§8.1.3).
Прямым суперклассом типа перечисления с именем E является Enum<E> (§8.1.4).
Тип перечисления не имеет экземпляров, кроме тех, которые определены его константами перечисления. Попытка явного создания экземпляра типа перечисления является ошибкой компиляции (§15.9.1).
Метод final clone в Enum гарантирует, что константы перечисления никогда не могут быть клонированы, а специальное обращение механизмом сериализации гарантирует, что дублирующие экземпляры никогда не создаются в результате десериализации. Рефлексивное создание экземпляров типов перечислений запрещено. Эти четыре вещи гарантируют, что экземпляры типа перечисления существуют только в том виде, в котором они определены константами перечисления.
Тело типа перечисления может содержать константы перечисления. Константа перечисления определяет экземпляр типа перечисления.
КонстантыПеречисления:
КонстантаПеречисления
КонстантыПеречисления , КонстантаПеречисления
КонстантаПеречисления:
Аннотациинеобязательно Идентификатор Аргументынеобязательно ТелоКлассанеобязательно
Аргументы:
( СписокАргументовнеобязательно )
ОбъявленияТелаПеречисления:
; ОбъявленияТелаКлассанеобязательно
Константа перечисления может быть необязательно снабжена модификаторами аннотаций. Если аннотация a (§9.7) на константе перечисления соответствует типу аннотации T (§9.6), и T имеет (мета-)аннотацию m, соответствующую java.lang.annotation.Target, то m должен иметь элемент, значение которого является java.lang.annotation.ElementType.FIELD, в противном случае возникает ошибка компиляции.
Идентификатор в КонстантеПеречисления может быть использован в имени для ссылки на константу перечисления.
Область видимости и перекрытие константы перечисления заданы в §6.3 и §6.4.
Константа перечисления может быть дополнена аргументами, которые передаются в конструктор типа перечисления, когда константа создаётся во время инициализации класса, как описано далее в этом разделе. Вызываемый конструктор выбирается с использованием обычных правил перегрузки (§15.12.2). Если аргументы опущены, предполагается пустой список аргументов.
Необязательное тело класса константы перечисления неявным образом определяет объявление анонимного класса (§15.9.5), который расширяет непосредственно окружающий тип перечисления. Тело класса подчиняется обычным правилам анонимных классов; в частности, оно не может содержать никаких конструкторов.
Методы экземпляра, объявленные в этих телах классов, могут вызываться за пределами окружающего типа перечисления только в том случае, если они переопределяют доступные методы в окружающем типе перечисления.
Определение абстрактного метода в теле класса константы перечисления является ошибкой компиляции.
Поскольку существует только один экземпляр каждой константы перечисления, разрешается использовать оператор == вместо метода equals при сравнении двух ссылок на объекты, если известно, что по меньшей мере одна из них относится к константе перечисления.
Метод equals в Enum является методом final, который просто вызывает super.equals на своём аргументе и возвращает результат, тем самым выполняя сравнение по идентичности.
Пример 8.9.1-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.1-2. Использование java.util.EnumSet для поддиапазонов
import java.util.EnumSet;
public class Test {
enum Day { MONDAY, TUESDAY, WEDNESDAY,
THURSDAY, FRIDAY, SATURDAY, SUNDAY }
public static void main(String[] args) {
System.out.print("Weekdays: ");
for (Day d : EnumSet.range(Day.MONDAY, Day.FRIDAY))
System.out.print(d + " ");
}
}
Эта программа выводит:
Weekdays: MONDAY TUESDAY WEDNESDAY THURSDAY FRIDAY
java.util.EnumSet содержит богатую коллекцию статических фабрик, поэтому этот метод можно обобщить, чтобы работать с несмежными подмножествами, а также с поддиапазонами. На первый взгляд может показаться избыточным создание объекта java.util.EnumSet для одной итерации, но они настолько малозатратны, что этот приём рекомендуется для итерации по поддиапазону. Внутренне, java.util.EnumSet представляется с помощью одного long, предполагая, что тип перечисления имеет 64 или меньше элементов.
Любые объявления конструкторов или членов внутри объявления перечисления применяются к типу перечисления точно так же, как если бы они были присутствовали в теле класса обычного объявления класса, если не указано иное.
Ошибка компиляции, если объявление конструктора типа перечисления является public или protected.
Если у типа перечисления нет объявлений конструкторов, тогда автоматически предоставляется конструктор private, который не принимает параметров (чтобы соответствовать неявной пустой списку аргументов).
Ошибка компиляции для объявления перечисления, если объявляется деструктор. Экземпляр типа перечисления никогда не может быть завершен.
Ошибка компиляции для типа перечисления E, имеющего абстрактный метод m в качестве члена, если E не имеет одного или нескольких констант перечисления, и все константы перечисления E имеют тела классов, которые обеспечивают конкретные реализации m.
В дополнение к членам, которые тип перечисления E наследует от Enum<E>, для каждой объявленной константы перечисления с именем n, тип перечисления имеет неявное объявление public static final поля с именем n типа E. Эти поля считаются объявленными в том же порядке, что и соответствующие константы перечисления, прежде чем любые static поля, явно объявленные в типе перечисления. Каждое такое поле инициализируется соответствующей константой перечисления. Каждое такое поле также считается аннотированным теми же аннотациями, что и соответствующая константа перечисления. Константа перечисления считается созданной, когда соответствующее поле инициализируется.
Кроме того, если E — имя типа перечисления, то этот тип имеет следующие неявные объявленные static методы:
/** * 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);
Следует, что объявления типов перечисления не могут содержать полей, конфликтующих с константами перечисления, и не могут содержать методов, конфликтующих с автоматически сгенерированными методами (values() и valueOf(String)) или методами, которые переопределяют методы final в перечислении (equals(Object), hashCode(), clone(), compareTo(Object), name(), ordinal() и getDeclaringClass()).
Ошибка компиляции при обращении к полю static типа перечисления, которое не является константой (§4.12.4) из конструкторов, блоков инициализации экземпляров или выражений инициализации переменных экземпляров этого типа.
Ошибка компиляции для конструкторов, блоков инициализации экземпляров или выражений инициализации переменных экземпляра константы перечисления e, которые ссылаются на e или на константу перечисления того же типа, которая объявлена справа от e.
Пример 8.9.2-1. Ограничение на самоссылку константы перечисления
Без этого правила, на первый взгляд разумный код завершался бы ошибкой во время выполнения из-за цикличности инициализации, присущей типам перечисления. (Цикличность существует в любом классе с полем «самотипизированного» static.) Вот пример того типа кода, который завершился бы ошибкой:
import java.util.Map;
import java.util.HashMap;
enum Color {
RED, GREEN, BLUE;
static final Map<String,Color> colorMap =
new HashMap<String,Color>();
Color() { colorMap.put(toString(), this); }
}
Статическая инициализация этого типа перечисления выбросила бы NullPointerException, потому что переменная static colorMap была бы неинициализированной, когда выполнялись бы конструкторы констант перечисления. Указанное выше ограничение гарантирует, что такой код не будет компилироваться.
Обратите внимание, что пример легко можно переработать для правильной работы:
import java.util.Map;
import java.util.HashMap;
enum Color {
RED, GREEN, BLUE;
static final Map<String,Color> colorMap =
new HashMap<String,Color>();
static {
for (Color c : Color.values())
colorMap.put(c.toString(), c);
}
}
Переработанный вариант очевидно правильный, поскольку статическая инициализация происходит сверху вниз.
Пример 8.9.2-2. Тип перечисления с членами
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, переданное через конструктор. Поле представляет собой значение, в центах, американской монеты. Обратите внимание, что нет ограничений на тип или количество параметров, которые могут быть объявлены конструктором типа перечисления.
Оператор switch полезен для имитации добавления метода к типу перечисления извне типа. В этом примере добавляется метод color к типу Coin, и печатается таблица монет, их значений и цветов.
class Test {
public static void main(String[] args) {
for (Coin c : Coin.values())
System.out.println(c + "\t\t" +
c.value() + "\t" + color(c));
}
private enum CoinColor { COPPER, NICKEL, SILVER }
private 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);
}
}
}
Эта программа выводит:
PENNY 1 COPPER NICKEL 5 NICKEL DIME 10 SILVER QUARTER 25 SILVER
Пример 8.9.2-3. Несколько типов перечисления
В следующей программе класс игральной карты создан на основе двух простых типов перечисления. Обратите внимание, что каждый тип перечисления был бы так же длинным, как весь пример, за исключением функции перечисления:
import java.util.List;
import java.util.ArrayList;
class Card implements Comparable<Card>,
java.io.Serializable {
public enum Rank { DEUCE, THREE, FOUR, FIVE, SIX, SEVEN,
EIGHT, NINE, TEN,JACK, QUEEN, KING, ACE }
public enum Suit { CLUBS, DIAMONDS, HEARTS, SPADES }
private final Rank rank;
private final Suit suit;
public Rank rank() { return rank; }
public Suit suit() { return suit; }
private Card(Rank rank, Suit suit) {
if (rank == null || suit == null)
throw new NullPointerException(rank + ", " + suit);
this.rank = rank;
this.suit = suit;
}
public String toString() { return rank + " of " + suit; }
// Primary sort on suit, secondary sort on rank
public int compareTo(Card c) {
int suitCompare = suit.compareTo(c.suit);
return (suitCompare != 0 ?
suitCompare :
rank.compareTo(c.rank));
}
private static final List<Card> prototypeDeck =
new ArrayList<Card>(52);
static {
for (Suit suit : Suit.values())
for (Rank rank : Rank.values())
prototypeDeck.add(new Card(rank, suit));
}
// Returns a new deck
public static List<Card> newDeck() {
return new ArrayList<Card>(prototypeDeck);
}
}
Следующая программа использует класс Card. Она принимает два целочисленных параметра в командной строке, представляющие количество рук для раздачи и количество карт в каждой руке:
import java.util.List;
import java.util.ArrayList;
import java.util.Collections;
class Deal {
public static void main(String args[]) {
int numHands = Integer.parseInt(args[0]);
int cardsPerHand = Integer.parseInt(args[1]);
List<Card> deck = Card.newDeck();
Collections.shuffle(deck);
for (int i=0; i < numHands; i++)
System.out.println(dealHand(deck, cardsPerHand));
}
/**
* Returns a new ArrayList consisting of the last n
* elements of deck, which are removed from deck.
* The returned list is sorted using the elements'
* natural ordering.
*/
public static <E extends Comparable<E>>
ArrayList<E> dealHand(List<E> deck, int n) {
int deckSize = deck.size();
List<E> handView = deck.subList(deckSize - n, deckSize);
ArrayList<E> hand = new ArrayList<E>(handView);
handView.clear();
Collections.sort(hand);
return hand;
}
}
Программа выводит:
java Deal 4 3 [DEUCE of CLUBS, SEVEN of CLUBS, QUEEN of DIAMONDS] [NINE of HEARTS, FIVE of SPADES, ACE of SPADES] [THREE of HEARTS, SIX of HEARTS, TEN of SPADES] [TEN of CLUBS, NINE of DIAMONDS, THREE of SPADES]
Пример 8.9.2-4. Константы перечисления с телами классов
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), поскольку шаблон исключает возможность забыть добавить поведение для новой константы (поскольку объявление перечисления вызовет ошибку компиляции).
© Oracle and/or its affiliates. All rights reserved.
Licensed under the Oracle Technology Network License Agreement.