Глава 15. Выражения
Оглавление
- 15.1. Вычисление, Обозначение и Результат
- 15.2. Формы Выражений
- 15.3. Тип Выражения
- 15.4. Выражения FP-strict
- 15.5. Выражения и Проверки во Время Выполнения
- 15.6. Нормальное и Прерывистое Завершение Вычисления
- 15.7. Порядок Вычисления
- 15.8. Первичные Выражения
- 15.9. Выражения Создание Экземпляра Класса
- 15.10. Выражения Создание и Доступ к Массивам
- 15.11. Выражения Доступа к Полям
- 15.12. Выражения Вызова Метода
-
- 15.12.1. Компиляционная Фаза 1: Определение Класса или Интерфейса для Поиска
- 15.12.2. Компиляционная Фаза 2: Определение Подписи Метода
-
- 15.12.2.1. Определение Возможных Применимых Методов
- 15.12.2.2. Фаза 1: Определение Соответствующих Методов с Соответствующей Арностью, Применимых по Строгому Вызову
- 15.12.2.3. Фаза 2: Определение Соответствующих Методов с Соответствующей Арностью, Применимых по Свободному Вызову
- 15.12.2.4. Фаза 3: Определение Применимых Методов с Переменной Арностью
- 15.12.2.5. Выбор Наиболее Специфичного Метода
- 15.12.2.6. Тип Вызова Метода
- 15.12.3. Компиляционная Фаза 3: Подходит ли Выбранный Метод?
- 15.12.4. Вычисление во Время Выполнения Вызова Метода
- 15.13. Выражения Ссылка на Метод
- 15.14. Постфиксные Выражения
- 15.15. Унарные Операторы
- 15.16. Выражения Приведения Типов
- 15.17. Мультипликативные Операторы
- 15.18. Аддитивные Операторы
- 15.19. Операторы Сдвига
- 15.20. Операторы Сравнения
- 15.21. Операторы Равенства
- 15.22. Битовые и Логические Операторы
- 15.23. Условный Оператор И
&& - 15.24. Условный Оператор ИЛИ
|| - 15.25. Условный Оператор
? : - 15.26. Операторы Присваивания
- 15.27. Lambda Выражения
Большая часть работы в программе выполняется путём вычисления выражений, либо для получения побочных эффектов, таких как присвоение переменным, либо для получения их значений, которые могут быть использованы в качестве аргументов или операндов в более крупных выражениях, или для влияния на последовательность выполнения в операторах, или для всего этого одновременно.
В этом разделе описаны значения выражений и правила их вычисления.
При вычислении (исполнении) выражения в программе результат обозначает одно из трёх:
Если выражение обозначает переменную, и требуется значение для дальнейшего вычисления, то используется значение этой переменной. В этом контексте, если выражение обозначает переменную или значение, мы можем говорить просто о значении выражения.
Преобразование набора значений (§5.1.13) применяется к результату каждого выражения, которое генерирует значение, включая случаи использования значения переменной типа float или double.
Выражение обозначает ничего тогда и только тогда, когда это вызов метода (§15.12), вызывающий метод, который не возвращает значения, то есть метод, объявленный void (§8.4). Такое выражение может быть использовано только как оператор выражения (§14.8) или как единственное выражение в теле лямбда-выражения (§15.27.2), поскольку любой другой контекст, в котором может появиться выражение, требует, чтобы выражение обозначало что-то. Оператор выражения или тело лямбда-выражения, которое является вызовом метода, также может вызывать метод, который производит результат; в этом случае возвращаемое методом значение бездействует.
Вычисление выражения может производить побочные эффекты, поскольку выражения могут содержать встроенные присваивания, операторы инкремента, операторы декремента и вызовы методов.
Выражение встречается в:
-
Объявлении какого-либо типа (класса или интерфейса), который объявляется: в инициализаторе поля, в статическом инициализаторе, в инициализаторе экземпляра, в объявлении конструктора, в объявлении метода или в аннотации.
-
Аннотации в объявлении пакета или в объявлении верхнего уровня типа.
Выражения можно грубо разделить на следующие синтаксические формы:
Приоритет операторов управляется иерархией грамматических правил. Оператор с самым низким приоритетом — стрелка лямбда-выражения (->), за ним следуют операторы присваивания. Таким образом, все выражения синтаксически включены в нетерминалы LambdaExpression и AssignmentExpression:
Когда некоторые выражения появляются в определённых контекстах, они считаются поли-выражениями. Следующие формы выражений могут быть поли-выражениями:
Правила, определяющие, является ли выражение одной из этих форм поли-выражением, приведены в отдельных разделах, которые описывают эти формы выражений.
Выражения, которые не являются поли-выражениями, являются самостоятельными выражениями. Самостоятельные выражения — это выражения вышеупомянутых форм, если они не определены как поли-выражения, а также все выражения всех других форм. Выражения всех других форм называются самостоятельной формой.
Некоторые выражения имеют значение, которое может быть определено во время компиляции. Это постоянные выражения (§15.28).
Если выражение обозначает переменную или значение, то выражение имеет тип, известный во время компиляции. Тип самостоятельного выражения может быть определён полностью из содержимого выражения; в отличие от этого, тип поли-выражения может зависеть от целевого типа выражения (§5 (Преобразования и контексты)). Правила определения типа выражения описаны отдельно для каждого вида выражения.
Значение выражения совместимо с присваиванием (§5.2) типу выражения, если не происходит загрязнения кучи (§4.12.2).
Аналогично, значение, хранящееся в переменной, всегда совместимо с типом переменной, если не происходит загрязнения кучи.
Другими словами, значение выражения, тип которого T, всегда подходит для присваивания переменной типа T.
Обратите внимание, что выражение, тип которого — класс F, объявленный final, гарантированно имеет значение, которое является либо ссылкой на null, либо объектом, класс которого — сам F, поскольку типы final не имеют подклассов.
Если тип выражения является float или double, то возникает вопрос о том, из какого множества значений (§4.2.3) берется значение выражения. Это определяется правилами преобразования множеств значений (§5.1.13); эти правила, в свою очередь, зависят от того, является ли выражение FP-строгим.
Каждое константное выражение (§15.28) является FP-строгим.
Если выражение не является константным выражением, то рассмотрите все объявления классов, интерфейсов и методов, содержащие это выражение. Если любое такое объявление имеет модификатор strictfp (§8.1.1.3, §8.4.3.5, §9.1.1.2), то выражение является FP-строгим.
Если класс, интерфейс или метод X объявлен как strictfp, то X и любой класс, интерфейс, метод, конструктор, инициализатор экземпляра, статический инициализатор или инициализатор переменной внутри X считаются FP-строгими.
Обратите внимание, что значение элемента аннотации (§9.7) всегда FP-строгое, потому что оно всегда является константным выражением.
Следовательно, выражение не является FP-строгим тогда и только тогда, когда оно не является константным выражением и не появляется ни в одном объявлении, имеющем модификатор strictfp.
В FP-строгом выражении все промежуточные значения должны быть элементами множества значений с плавающей точкой или множества значений с двойной точностью, что подразумевает, что результаты всех FP-строгих выражений должны быть такими, как предсказывает арифметика IEEE 754 для операндов, представленных с одинарной и двойной точностью.
В выражении, которое не является FP-строгим, реализации предоставляется некоторое пространство для использования расширенного диапазона экспоненты для представления промежуточных результатов; общий эффект, грубо говоря, заключается в том, что вычисление может дать «правильный ответ» в ситуациях, когда исключительное использование множества значений с плавающей точкой или множества значений с двойной точностью может привести к переполнению или потере точности.
Если тип выражения является примитивным типом, то значение выражения имеет тот же примитивный тип.
Если тип выражения является ссылочным типом, то класс ссылаемого объекта, или даже является ли значение ссылкой на объект, а не null, необязательно известен во время компиляции. Существуют несколько мест в языке программирования Java, где фактический класс ссылаемого объекта влияет на выполнение программы способом, который нельзя вывести из типа выражения. Они таковы:
-
Вызов метода (§15.12). Конкретный метод, используемый для вызова
o.m(...), выбирается на основе методов, которые являются частью класса или интерфейса, который является типомo. Для виртуальных методов класс объекта, на который ссылается значениеoво время выполнения, участвует в процессе, поскольку подкласс может переопределить конкретный метод, уже объявленный в родительском классе, так что этот переопределенный метод будет вызван. (Переопределенный метод может или не может выбрать дальнейший вызов исходного переопределенногоmметода.) -
Оператор
instanceof(§15.20.2). Выражение, тип которого является ссылочным типом, может быть проверено с помощьюinstanceof, чтобы выяснить, является ли класс объекта, на который ссылается значение выражения во время выполнения, совместимым по присваиванию (§5.2) с каким-либо другим ссылочным типом. -
Приведение типов (§5.5, §15.16). Класс объекта, на который ссылается значение операндного выражения, может не быть совместим с типом, указанным при приведении. Для ссылочных типов это может потребовать проверки во время выполнения, которая выбрасывает исключение, если класс ссылаемого объекта, определяемый во время выполнения, не совместим по присваиванию (§5.2) с целевым типом.
-
Присваивание элементу массива ссылочного типа (§10.5, §15.13, §15.26.1). Правила проверки типов допускают, что тип массива S
[]может рассматриваться как подтип T[], если S является подтипом T, но это требует проверки во время выполнения при присваивании элементу массива, аналогичной проверке, выполняемой при приведении типов. -
Обработка исключений (§14.20). Исключение перехватывается блоком
catchтолько в том случае, если класс объекта брошенного исключения являетсяinstanceofтипом формального параметра блокаcatch.
Ситуации, когда класс объекта не известен статически, могут привести к ошибкам типа во время выполнения.
Кроме того, есть ситуации, когда статически известный тип может быть неверным во время выполнения. Такие ситуации могут возникать в программе, которая вызывает предупреждения о не проверенных типах во время компиляции. Такие предупреждения выдаются в ответ на операции, безопасность которых нельзя гарантировать статически, и которые не могут быть немедленно проверены динамически, потому что они связаны с нереализуемыми типами (§4.7). В результате динамические проверки впоследствии в ходе выполнения программы могут обнаружить несоответствия и привести к ошибкам типа во время выполнения.
Ошибка типа во время выполнения может произойти только в следующих ситуациях:
-
При приведении типов, когда фактический класс объекта, на который ссылается значение операндного выражения, не совместим с целевым типом, указанным оператором приведения (§5.5, §15.16); в этом случае выбрасывается
ClassCastException. -
При автоматически сгенерированном приведении типов, введенном для обеспечения корректности операции над нереализуемым типом (§4.7).
-
При присваивании элементу массива ссылочного типа, когда фактический класс объекта, значение которого должно быть присвоено, не совместим с фактическим типом компонента массива во время выполнения (§10.5, §15.13, §15.26.1); в этом случае выбрасывается
ArrayStoreException. -
Когда исключение не перехватывается ни одним блоком
catchоператораtry(§14.20); в этом случае поток управления, который встретил исключение первым, попытается вызвать обработчик необработанных исключений (§11.3), а затем завершится.
Каждый выражение имеет нормальный режим вычисления, в котором выполняются определённые вычислительные шаги. Следующие разделы описывают нормальный режим вычисления для каждого вида выражения.
Если все шаги выполняются без выброса исключения, то выражение считается завершенным нормально.
Однако, если вычисление выражения вызывает исключение, то выражение считается завершенным прерывисто. Прерывистое завершение всегда имеет связанную причину, которая всегда является throw с заданным значением.
Исключения во время выполнения генерируются предопределёнными операторами следующим образом:
-
Выражение создания экземпляра класса (§15.9.4), выражение создания массива (§15.10.2), выражение ссылки на метод (§15.13.3), выражение инициализатора массива (§10.6), выражение оператора конкатенации строк (§15.18.1) или лямбда-выражение (§15.27.4) генерирует
OutOfMemoryError, если доступно недостаточно памяти. -
Выражение создания массива (§15.10.2) генерирует
NegativeArraySizeException, если значение любого выражения размерности меньше нуля. -
Выражение доступа к элементу массива (§15.10.4) генерирует
NullPointerException, если значение выражения ссылки на массив являетсяnull. -
Выражение доступа к элементу массива (§15.10.4) генерирует
ArrayIndexOutOfBoundsException, если значение выражения индекса массива отрицательно или больше или равноlengthмассива. -
Выражение доступа к полю (§15.11) генерирует
NullPointerException, если значение выражения ссылки на объект являетсяnull. -
Выражение вызова метода (§15.12), вызывающее метод экземпляра, генерирует
NullPointerException, если целевая ссылка являетсяnull. -
Выражение приведения типа (§15.16) генерирует
ClassCastException, если приведение типа в ходе выполнения оказалось недопустимым. -
Операторы целочисленного деления (§15.17.2) или целочисленного остатка от деления (§15.17.3) генерируют
ArithmeticException, если значение выражения правого операнда равно нулю. -
Присваивание элементу массива с типом ссылки (§15.26.1), выражение вызова метода (§15.12) или префиксные/постфиксные операторы инкремента (§15.14.2, §15.15.1) или декремента (§15.14.3, §15.15.2) могут выбросить исключение
OutOfMemoryErrorв результате преобразования в условный тип (§5.1.7). -
Присваивание элементу массива с типом ссылки (§15.26.1) генерирует исключение
ArrayStoreException, когда присваиваемое значение не совместимо с типом элемента массива (§10.5).
Выражение вызова метода также может привести к выбросу исключения, если во время выполнения тела метода возникает исключение, приводящее к прерывистому завершению его выполнения.
Выражение создания экземпляра класса также может привести к выбросу исключения, если во время выполнения конструктора возникает исключение, приводящее к прерывистому завершению его выполнения.
Различные ошибки связывания и виртуальной машины также могут возникнуть во время вычисления выражения. По своей природе такие ошибки трудно предсказать и обработать.
Если возникает исключение, то вычисление одного или нескольких выражений может быть прервано до завершения всех шагов их нормального режима вычисления; такие выражения считаются завершёнными прерывисто.
Если вычисление выражения требует вычисления подвыражения, то прерывистое завершение подвыражения всегда вызывает немедленное прерывистое завершение самого выражения с той же причиной, и все последующие шаги в нормальном режиме вычисления не выполняются.
Термины «завершение нормально» и «завершение прерывисто» также применяются к выполнению операторов (§14.1). Оператор может завершиться прерывисто по различным причинам, а не только из-за выброшенного исключения.
Язык программирования Java гарантирует, что операнды операторов, по-видимому, вычисляются в определённом порядке вычисления, а именно слева направо.
Рекомендуется, чтобы код не зависел существенно от этого спецификации. Код обычно яснее, когда каждое выражение содержит не более одного побочного эффекта, как свою внешнюю операцию, и когда код не зависит от того, какая именно исключение возникает в результате левостороннего вычисления выражений.
Левый операнд бинарного оператора, по-видимому, полностью вычисляется до того, как любая часть правого операнда будет вычислена.
Если оператор является оператором сложного присваивания (§15.26.2), то вычисление левого операнда включает как запоминание переменной, обозначаемой левым операндом, так и извлечение и сохранение значения этой переменной для использования в неявной бинарной операции.
Если вычисление левого операнда бинарного оператора завершается внезапно, ни одна часть правого операнда, по-видимому, не была вычислена.
Пример 15.7.1-1. Левый операнд вычисляется первым
В следующей программе оператор * имеет левый операнд, содержащий присваивание переменной, и правый операнд, содержащий ссылку на ту же переменную. Значение, полученное ссылкой, будет отражать тот факт, что присваивание произошло первым.
class Test1 {
public static void main(String[] args) {
int i = 2;
int j = (i=3) * i;
System.out.println(j);
}
}
Эта программа выводит:
9
Запрещено, чтобы вычисление оператора * привело к 6 вместо 9.
Пример 15.7.1-2. Неявный левый операнд в операторе сложного присваивания
В следующей программе оба оператора присваивания сначала извлекают и запоминают значение левого операнда, которое равно 9, прежде чем будет вычислен правый операнд оператора сложения, в этот момент переменная устанавливается в значение 3.
class Test2 {
public static void main(String[] args) {
int a = 9;
a += (a = 3); // first example
System.out.println(a);
int b = 9;
b = b + (b = 3); // second example
System.out.println(b);
}
}
Эта программа выводит:
12 12
Запрещено, чтобы любое из присваиваний (сложное для a, простое для b) привело к результату 6.
См. также пример в §15.26.2.
Пример 15.7.1-3. Внезапное завершение вычисления левого операнда
class Test3 {
public static void main(String[] args) {
int j = 1;
try {
int i = forgetIt() / (j = 2);
} catch (Exception e) {
System.out.println(e);
System.out.println("Now j = " + j);
}
}
static int forgetIt() throws Exception {
throw new Exception("I'm outta here!");
}
}
Эта программа выводит:
java.lang.Exception: I'm outta here! Now j = 1
То есть, левый операнд forgetIt() оператора / генерирует исключение, прежде чем будет вычислен правый операнд, и в нём произойдет присваивание 2 переменной j.
Язык программирования Java гарантирует, что каждый операнд оператора (за исключением условных операторов &&, || и ? :) по-видимому, полностью вычисляется перед выполнением любой части самой операции.
Если бинарный оператор является целочисленным делением / (§15.17.2) или целочисленным остатком % (§15.17.3), то его выполнение может вызвать ArithmeticException, но это исключение выбрасывается только после того, как оба операнда бинарного оператора были вычислены и только если эти вычисления завершились нормально.
Пример 15.7.2-1. Вычисление операндов перед операцией
class Test {
public static void main(String[] args) {
int divisor = 0;
try {
int i = 1 / (divisor * loseBig());
} catch (Exception e) {
System.out.println(e);
}
}
static int loseBig() throws Exception {
throw new Exception("Shuffle off to Buffalo!");
}
}
Эта программа выводит:
java.lang.Exception: Shuffle off to Buffalo!
а не:
java.lang.ArithmeticException: / by zero
поскольку ни одна часть операции деления, включая сообщение об исключении деления на ноль, не может, по-видимому, произойти до завершения вызова loseBig, даже если реализация может определить или предположить, что операция деления обязательно приведет к исключению деления на ноль.
Язык программирования Java соблюдает порядок вычисления, явно указанный в скобках и неявно заданный приоритетом операторов.
Реализация языка программирования Java не может использовать алгебраические тождества, такие как ассоциативный закон, для переписывания выражений в более удобный порядок вычисления, если она не может доказать, что заменяющее выражение эквивалентно по значению и по наблюдаемым побочным эффектам, даже при наличии нескольких потоков выполнения (используя модель выполнения потоков в §17 (Потоки и блокировки)), для всех возможных вычислительных значений, которые могут быть вовлечены.
В случае вычислений с плавающей точкой это правило применяется также к бесконечности и значениям не число (NaN).
Например, !(x<y) не может быть переписано как x>=y, потому что эти выражения имеют разные значения, если либо x, либо y равно NaN или оба равны NaN.
В частности, вычисления с плавающей точкой, которые, по-видимому, являются математически ассоциативными, вряд ли будут вычислительно ассоциативными. Такие вычисления не должны переупорядочиваться по наивным причинам.
Например, неверно, чтобы компилятор Java переписал 4.0*x*0.5 как 2.0*x; хотя здесь округлений нет, существуют большие значения x, для которых первое выражение даёт бесконечность (из-за переполнения), а второе выражение даёт конечный результат.
Например, тестовая программа:
strictfp class Test {
public static void main(String[] args) {
double d = 8e+307;
System.out.println(4.0 * d * 0.5);
System.out.println(2.0 * d);
}
}
выводит:
Infinity 1.6e+308
потому что первое выражение переполняется, а второе — нет.
В отличие от этого, целочисленное сложение и умножение являются доказуемо ассоциативными в языке программирования Java.
Например, a+b+c, где a, b и c являются локальными переменными (это упрощающее предположение избегает проблем, связанных с несколькими потоками и volatile переменными), всегда даст один и тот же ответ, независимо от того, вычисляется ли он как (a+b)+c или как a+(b+c); если выражение b+c встречается поблизости в коде, умный компилятор Java может использовать это общее подвыражение.
В выражении вызова метода или конструктора или создания экземпляра класса выражения аргументов могут появляться в скобках, разделенные запятыми. Каждое выражение аргумента, по-видимому, полностью вычисляется перед выполнением любой части выражения аргумента справа от него.
Если вычисление выражения аргумента внезапно завершается, ни одна часть любого выражения аргумента справа от него, по-видимому, не была вычислена.
Пример 15.7.4-1. Порядок вычисления при вызове метода
class Test1 {
public static void main(String[] args) {
String s = "going, ";
print3(s, s, s = "gone");
}
static void print3(String a, String b, String c) {
System.out.println(a + b + c);
}
}
Эта программа выводит:
going, going, gone
потому что присваивание строки "gone" переменной s происходит после вычисления первых двух аргументов print3.
Пример 15.7.4-2. Внезапное завершение выражения аргумента
class Test2 {
static int id;
public static void main(String[] args) {
try {
test(id = 1, oops(), id = 3);
} catch (Exception e) {
System.out.println(e + ", id=" + id);
}
}
static int test(int a, int b, int c) {
return a + b + c;
}
static int oops() throws Exception {
throw new Exception("oops");
}
}
Эта программа выводит:
java.lang.Exception: oops, id=1
потому что присваивание 3 переменной id не выполняется.
Порядок вычисления некоторых выражений не полностью охвачен этими общими правилами, поскольку эти выражения могут иногда вызывать исключительные ситуации, которые должны быть указаны. См. подробные объяснения порядка вычисления для следующих типов выражений:
Основные выражения включают в себя большинство простейших типов выражений, из которых строятся все остальные: литералы, создания объектов, обращение к полям, вызов методов, ссылки на методы и доступ к элементам массивов. Выражение в скобках также синтаксически обрабатывается как основное выражение.
Эта часть грамматики языка программирования Java необычна по двум причинам. Во-первых, можно ожидать, что простые имена, такие как имена локальных переменных и параметров методов, будут основными выражениями. По техническим причинам имена объединяются с основными выражениями немного позже, когда вводятся постфиксные выражения (§15.14).
Технические причины связаны с возможностью левостороннего разбора программ Java с только одним токеном предварительного просмотра. Рассмотрим выражения (z[3]) и (z[]). Первое — это скобочное обращение к элементу массива (§15.10.3), а второе — начало приведения типов (§15.16). В момент, когда токен предварительного просмотра равен [, левосторонний разбор приведет к редукции z до нетерминального символа Имя. В контексте приведения типов мы предпочитаем не сводить имя к Основному выражению, но если Имя было одним из вариантов для Основного выражения, то мы не смогли бы определить, производить ли редукцию (то есть мы не смогли бы определить, является ли текущая ситуация скобочным доступом к элементу массива или приведением типа) без просмотра двух токенов вперед, до токена, следующего за [. Представленная здесь грамматика избегает этой проблемы, сохраняя Имя и Основное выражение разделенными и позволяя использовать их в других правилах синтаксиса (тех, что для ClassInstanceCreationExpression, MethodInvocation, ArrayAccess и PostfixExpression, но не для FieldAccess, поскольку это использует идентификатор напрямую). Эта стратегия фактически откладывает вопрос о том, следует ли рассматривать Имя как Основное выражение, до тех пор, пока не будет доступно больше контекста.
Вторая необычная особенность избегает потенциальной грамматической неоднозначности в выражении "new int[3][3]", которое в Java всегда означает однократное создание многомерного массива, но без соответствующей грамматической тонкости может быть интерпретировано как эквивалентное "(new int[3])[3]".
Эта неоднозначность устраняется путем разделения ожидаемого определения Основного выражения на Основное выражение и Основное выражение без создания массива. (Это можно сравнить с разделением Утверждение на Утверждение и Утверждение без краткого if (§14.5) для избежания проблемы "висящего else".)
Литерал (§3.10) обозначает фиксированное, неизменяемое значение.
Следующая продукция из §3.10 показана здесь для удобства:
Тип литерала определяется следующим образом:
-
Тип целочисленного литерала (§3.10.1), заканчивающегося на
Lилиl, равенlong(§4.2.1).Тип любого другого целочисленного литерала равен
int(§4.2.1). -
Тип вещественного литерала с плавающей точкой (§3.10.2), заканчивающегося на
Fилиf, равенfloat, а его значение должно быть элементом множества значений типа float (§4.2.3).Тип любого другого вещественного литерала с плавающей точкой равен
double, а его значение должно быть элементом множества значений типа double (§4.2.3). -
Тип литерала null
null(§3.10.7) — тип null (§4.1); его значение — ссылка null.
Вычисление лексического литерала всегда завершается без ошибок.
Литерал класса — это выражение, состоящее из имени класса, интерфейса, массива или примитивного типа, или псевдотипа void, за которым следует '.' и токен class.
Тип C.class, где C — имя класса, интерфейса или типа массива (§4.3), равен типу Class<C>.
Тип p.class, где p — имя примитивного типа (§4.2), равен типу Class<B>, где B — тип выражения типа p после преобразования к обёртке (§5.1.7).
Тип void.class (§8.4.5) равен Class<Void>.
Ошибка компиляции, если именованный тип — это переменная типа (§4.4) или параметризованный тип (§4.5), или массив, тип элемента которого — переменная или параметризованный тип.
Ошибка компиляции, если именованный тип не обозначает доступный (§6.6) и находящийся в области видимости (§6.3) тип в точке появления литерала класса.
Литерал класса вычисляется в объект Class для именованного типа (или для void), как определено загрузчиком классов, определяющим класс текущего экземпляра (§12.2).
Ключевое слово this может использоваться только в следующих контекстах:
Если оно встречается где-либо еще, возникает ошибка компиляции.
Ключевое слово this может использоваться в лямбда-выражении только если это разрешено в контексте, в котором появляется лямбда-выражение. В противном случае возникает ошибка компиляции.
При использовании в качестве основного выражения ключевое слово this обозначает значение, являющееся ссылкой на объект, для которого был вызван метод экземпляра или метод по умолчанию (§15.12), или на объект, который создаётся. Значение, обозначаемое this в теле лямбда-выражения, совпадает со значением, обозначаемым this в окружающем контексте.
Ключевое слово this также используется в явных операторах вызова конструктора (§8.8.7.1).
Тип this — это тип класса или интерфейса T внутри которого встречается ключевое слово this.
Методы по умолчанию обеспечивают уникальную возможность доступа к this внутри интерфейса. (Все другие методы интерфейса являются либо abstract, либо static, и поэтому не предоставляют доступа к this). В результате this может иметь тип интерфейса.
Во время выполнения класс фактического объекта, на который ссылается, может быть T, если T — тип класса, или класс, являющийся подтипом T.
Пример 15.8.3-1. Выражение this
class IntVector {
int[] v;
boolean equals(IntVector other) {
if (this == other)
return true;
if (v.length != other.v.length)
return false;
for (int i = 0; i < v.length; i++) {
if (v[i] != other.v[i]) return false;
}
return true;
}
}
Здесь класс IntVector реализует метод equals, который сравнивает два вектора. Если другой вектор — это тот же объект вектора, для которого был вызван метод equals, то проверка может пропустить сравнения длины и значений. Метод equals реализует эту проверку, сравнивая ссылку на другой объект с this.
Любой лексически окружающий экземпляр (§8.1.3) может быть указан путём явного квалифицирования ключевого слова this.
Пусть T — тип, обозначаемый TypeName. Пусть n — целое число такое, что T является n'тым лексически окружающим объявлением типа класса или интерфейса, в котором появляется квалифицированное выражение this.
Значение выражения вида TypeName.this — это n'тый лексически окружающий экземпляр this.
Тип выражения — T.
Ошибка компиляции, если выражение встречается в классе или интерфейсе, который не является вложенным классом класса T или самим классом T.
Выражение в скобках — это основной выражение, тип которого соответствует типу содержащего выражения, а его значение во время выполнения — значению содержащего выражения. Если содержащее выражение обозначает переменную, то выражение в скобках также обозначает эту переменную.
Использование скобок влияет только на порядок вычисления, за исключением частного случая, в котором (-2147483648) и (-9223372036854775808L) допустимы, а -(2147483648) и -(9223372036854775808L) — нет.
Это связано с тем, что десятичные литералы 2147483648 и 9223372036854775808L допускаются только как операнд унарного минуса (§3.10.1).
В частности, наличие или отсутствие скобок вокруг выражения никак (кроме случая, указанного выше) не влияет на:
-
выбор множества значений (§4.2.3) для значения выражения типа
floatилиdouble. -
является ли переменная определённо присвоенной, определённо присвоенной когда
true, определённо присвоенной когдаfalse, определённо не присвоенной, определённо не присвоенной когдаtrue, или определённо не присвоенной когдаfalse(§16 (Определённая присвоение)).
Если выражение в скобках встречается в контексте определённого вида с целевым типом T (§5 (Преобразования и контексты)), содержащее выражение аналогично появляется в контексте того же вида с целевым типом T.
Если содержащее выражение является поливыражением (§15.2), выражение в скобках также является поливыражением. В противном случае это автономное выражение.
Выражение создания экземпляра класса используется для создания новых объектов, являющихся экземплярами классов.
Следующее правило из §15.12 приведено здесь для удобства:
Выражение создания экземпляра класса указывает класс, подлежащий созданию, возможно, с последующими аргументами типа (§4.5.1) или алмазом (<>), если создаваемый класс является обобщённым (§8.1.2), за которым следует (возможно, пустой) список фактических аргументов значения для конструктора.
Если список аргументов типа для класса пуст — форма алмаза <> — аргументы типа класса вычисляются автоматически. Допускается, хотя и настоятельно не рекомендуется, наличие пробелов между "<" и ">" алмаза.
Если конструктор является обобщённым (§8.8.4), аргументы типа конструктора аналогичным образом могут быть вычислены автоматически или переданы явно. Если они переданы явно, аргументы типа конструктора сразу следуют за ключевым словом new.
Ошибка компиляции, если выражение создания экземпляра класса предоставляет аргументы типа для конструктора, но использует форму алмаза для аргументов типа класса.
Это правило введено, потому что вычисление аргументов типа обобщенного класса может влиять на ограничения аргументов типа обобщенного конструктора.
Если TypeArguments присутствует непосредственно после new или непосредственно перед (, то это ошибка компиляции, если какие-либо аргументы типа являются дикими картами (§4.5.1).
Типы исключений, которые может вызвать выражение создания экземпляра класса, указаны в §11.2.1.
Выражения создания экземпляров классов имеют две формы:
-
Неквалифицированные выражения создания экземпляров классов начинаются с ключевого слова
new.Неквалифицированное выражение создания экземпляра класса может использоваться для создания экземпляра класса, независимо от того, является ли класс верхним уровнем (§7.6), членом (§8.5, §9.5), локальным (§14.3) или анонимным классом (§15.9.5).
-
Квалифицированные выражения создания экземпляров классов начинаются с выражения Примитив или Имя выражения.
Квалифицированное выражение создания экземпляра класса позволяет создавать экземпляры вложенных внутренних классов и их анонимных подклассов.
И неквалифицированные, и квалифицированные выражения создания экземпляров классов могут быть необязательно завершены телом класса. Такое выражение создания экземпляра класса объявляет анонимный класс (§15.9.5) и создаёт экземпляр его.
Выражение создания экземпляра класса является поливыражением (§15.2), если оно использует форму алмаза для аргументов типа класса и появляется в контексте присваивания или вызова (§5.2, §5.3). В противном случае это самостоятельное выражение.
Мы говорим, что класс создан, когда экземпляр класса создаётся выражением создания экземпляра класса. Создание экземпляра класса включает определение класса для создания экземпляра (§15.9.1), окружающих экземпляров (если таковые имеются) нового экземпляра (§15.9.2) и конструктора, который должен быть вызван для создания нового экземпляра (§15.9.3).
Если выражение создания экземпляра класса заканчивается телом класса, то создаваемый класс является анонимным классом. Тогда:
-
Если выражение создания экземпляра класса не квалифицировано:
ClassOrInterfaceTypeToInstantiate должно обозначать доступный, не-
finalи не являющийся типом перечисления класс; или интерфейс, который является доступным. В противном случае возникает ошибка компиляции.Если ClassOrInterfaceTypeToInstantiate заканчивается
<>, возникает ошибка компиляции.Если ClassOrInterfaceTypeToInstantiate заканчивается TypeArguments, то ClassOrInterfaceTypeToInstantiate должно обозначать корректно сформированный параметризованный тип (§4.5), иначе возникает ошибка компиляции.
Пусть T — тип, обозначаемый ClassOrInterfaceTypeToInstantiate. Если T обозначает класс, то объявляется анонимный непосредственный подкласс T. Если T обозначает интерфейс, то объявляется анонимный непосредственный подкласс
Object, реализующий T. В любом случае, тело подкласса является ClassBody, заданным в выражении создания экземпляра класса.Создаваемый класс — это анонимный подкласс.
-
Если выражение создания экземпляра класса квалифицировано:
ClassOrInterfaceTypeToInstantiate должно однозначно обозначать внутренний класс, который является доступным, не-
final, не является типом перечисления и является членом времени компиляции типа Primary выражения или ExpressionName. В противном случае возникает ошибка компиляции.Если ClassOrInterfaceTypeToInstantiate заканчивается
<>, возникает ошибка компиляции.Если ClassOrInterfaceTypeToInstantiate заканчивается TypeArguments, то ClassOrInterfaceTypeToInstantiate должно обозначать корректно сформированный параметризованный тип, в противном случае возникает ошибка компиляции.
Пусть T — тип, обозначаемый ClassOrInterfaceTypeToInstantiate. Объявляется анонимный непосредственный подкласс T. Тело подкласса — это ClassBody, заданное в выражении создания экземпляра класса.
Создаваемый класс — это анонимный подкласс.
Если выражение создания экземпляра класса не объявляет анонимный класс, то:
-
Если выражение создания экземпляра класса не квалифицировано:
ClassOrInterfaceTypeToInstantiate должно обозначать доступный, не-
abstractи не являющийся типом перечисления класс. В противном случае возникает ошибка компиляции.Если ClassOrInterfaceTypeToInstantiate заканчивается
<>, но класс, обозначаемый ClassOrInterfaceTypeToInstantiate, не является обобщённым, возникает ошибка компиляции.Если ClassOrInterfaceTypeToInstantiate заканчивается TypeArguments, то ClassOrInterfaceTypeToInstantiate должно обозначать корректно сформированный параметризованный класс, в противном случае возникает ошибка компиляции.
Создаваемый класс — это класс, обозначаемый ClassOrInterfaceTypeToInstantiate.
-
Если выражение создания экземпляра класса квалифицировано:
ClassOrInterfaceTypeToInstantiate должно однозначно обозначать внутренний класс, который является доступным, не-
abstract, не является типом перечисления и является членом времени компиляции типа Primary выражения или ExpressionName.Если ClassOrInterfaceTypeToInstantiate заканчивается
<>, и класс, обозначаемый ClassOrInterfaceTypeToInstantiate, не является обобщённым, возникает ошибка компиляции.Если ClassOrInterfaceTypeToInstantiate заканчивается TypeArguments, то ClassOrInterfaceTypeToInstantiate должно обозначать корректно сформированный параметризованный класс, в противном случае возникает ошибка компиляции.
Создаваемый класс — это класс, обозначаемый ClassOrInterfaceTypeToInstantiate.
Пусть C — класс, который создаётся, и пусть i — экземпляр, который создаётся. Если C — вложенный класс, то i может иметь непосредственно окружающий экземпляр (§8.1.3), определяемый следующим образом:
-
Если C — анонимный класс, то:
-
Если выражение создания экземпляра класса выполняется в статическом контексте, то
iне имеет непосредственно окружающего экземпляра. -
В противном случае, непосредственно окружающий экземпляр
i— этоthis.
-
-
Если C — локальный класс, то:
-
Если C встречается в статическом контексте, то
iне имеет непосредственно окружающего экземпляра. -
В противном случае, если выражение создания экземпляра класса выполняется в статическом контексте, возникает ошибка времени компиляции.
-
В противном случае, пусть O — непосредственно окружающий класс C. Пусть n — целое число такое, что O — n-ый лексически окружающий тип объявления класса, в котором появляется выражение создания экземпляра класса.
Непосредственно окружающий экземпляр
i— это n-ый лексически окружающий экземплярthis.
-
-
Если C — вложенный членский класс, то:
-
Если выражение создания экземпляра класса не квалифицировано, то:
-
Если выражение создания экземпляра класса выполняется в статическом контексте, возникает ошибка времени компиляции.
-
В противном случае, если C является членом класса, окружающего класс, в котором появляется выражение создания экземпляра класса, то пусть O — непосредственно окружающий класс, членом которого является C. Пусть n — целое число такое, что O — n-ый лексически окружающий тип объявления класса, в котором появляется выражение создания экземпляра класса.
Непосредственно окружающий экземпляр
i— это n-ый лексически окружающий экземплярthis. -
В противном случае, возникает ошибка времени компиляции.
-
-
Если выражение создания экземпляра класса квалифицировано, то непосредственно окружающий экземпляр
i— это объект, который является значением выражения Primary или ExpressionName.
-
Если C — анонимный класс, а его непосредственный суперкласс S — вложенный класс, то i может иметь непосредственно окружающий экземпляр относительно S, определяемый следующим образом:
-
Если S — локальный класс, то:
-
Если S встречается в статическом контексте, то
iне имеет непосредственно окружающего экземпляра относительно S. -
В противном случае, если выражение создания экземпляра класса выполняется в статическом контексте, возникает ошибка времени компиляции.
-
В противном случае, пусть O — непосредственно окружающий класс S. Пусть n — целое число такое, что O — n-ый лексически окружающий тип объявления класса, в котором появляется выражение создания экземпляра класса.
Непосредственно окружающий экземпляр
iотносительно S — это n-ый лексически окружающий экземплярthis.
-
-
Если S — вложенный членский класс, то:
-
Если выражение создания экземпляра класса не квалифицировано, то:
-
Если выражение создания экземпляра класса выполняется в статическом контексте, возникает ошибка времени компиляции.
-
В противном случае, если S является членом класса, окружающего класс, в котором появляется выражение создания экземпляра класса, то пусть O — непосредственно окружающий класс, членом которого является S. Пусть n — целое число такое, что O — n-ый лексически окружающий тип объявления класса, в котором появляется выражение создания экземпляра класса.
Непосредственно окружающий экземпляр
iотносительно S — это n-ый лексически окружающий экземплярthis. -
В противном случае, возникает ошибка времени компиляции.
-
-
Если выражение создания экземпляра класса квалифицировано, то непосредственно окружающий экземпляр
iотносительно S — это объект, который является значением выражения Primary или ExpressionName.
-
Пусть C — класс, который нужно создать. Для создания экземпляра C, i, конструктор C выбирается во время компиляции по следующим правилам:
Сначала определяются фактические аргументы вызова конструктора:
-
Если C — анонимный класс с непосредственным суперклассом S, то:
-
Если S не является внутренним классом или если S — локальный класс, который встречается в статическом контексте, то аргументы конструктора — это аргументы в списке аргументов выражения создания экземпляра класса, если таковые имеются, в порядке их появления в выражении.
-
В противном случае, первый аргумент конструктора — это непосредственно окружающий экземпляр
iпо отношению к S (§15.9.2), а последующие аргументы конструктора — это аргументы в списке аргументов выражения создания экземпляра класса, если таковые имеются, в порядке их появления в выражении создания экземпляра класса.
-
-
Если C — локальный класс или
privateвнутренний член-класс, то аргументы конструктора — это аргументы в списке аргументов выражения создания экземпляра класса, если таковые имеются, в порядке их появления в выражении создания экземпляра класса. -
Если C — не-
privateвнутренний член-класс, то первый аргумент конструктора — это непосредственно окружающий экземплярi(§8.8.1, §15.9.2), а последующие аргументы его конструктора — это аргументы в списке аргументов выражения создания экземпляра класса, если таковые имеются, в порядке их появления в выражении создания экземпляра класса. -
В противном случае, аргументы конструктора — это аргументы в списке аргументов выражения создания экземпляра класса, если таковые имеются, в порядке их появления в выражении.
Во-вторых, определяется конструктор C и соответствующий тип возвращаемого значения и throws фрагмент:
-
Если выражение создания экземпляра класса использует
<>для удаления аргументов типа класса, то определяется список методовm1...mnдля целей разрешения перегрузки и вывода аргументов типа.Пусть
c1...cn— это конструкторы класса C. Пусть#m— автоматически сгенерированное имя, отличное от всех имен конструкторов и методов в C. Для всех j (1 ≤ j ≤ n),mjопределяется черезcjследующим образом:-
Вначале определяется подстановка θ
jдля подстановки типов вcj.Пусть F1...Fp — параметризованные типы C, и пусть G1...Gq — параметризованные типы (если таковые имеются)
cj. Пусть X1...Xp и Y1...Yq — переменные типа с разными именами, которые не находятся в области видимости в теле C.θ
jявляется[F1:=X1, ..., Fp:=Xp, G1:=Y1, ..., Gq:=Yq]. -
Модификаторы
mj— те же, что и уcj. -
Параметризованные типы
mj— это X1...Xp,Y1...Yq. Предел каждого параметра, если таковой имеется, — это θj, примененный к соответствующему параметризованному пределу в C илиcj. -
Тип возвращаемого значения
mj— это θj, примененный к C<F1,...,Fp>. -
Имя
mj—#m. -
(Возможно пустой) список типов аргументов
mj— это θj, примененный к типам аргументовcj. -
(Возможно пустой) список типов исключений
mj— это θj, примененный к типам исключенийcj. -
Тело
mjне имеет значения.
Для выбора конструктора временно рассматриваются
m1...mnкак члены C. Затем один изm1...mnвыбирается, как определено выражениями аргументов выражения создания экземпляра класса, используя процесс, описанный в §15.12.2.Если нет единственного наиболее специфичного метода, который одновременно применим и доступен, возникает ошибка компиляции.
В противном случае, где
mj— выбранный метод,cj— выбранный конструктор. Тип возвращаемого значения иthrowsфрагментcjсовпадают с типом возвращаемого значения иthrowsфрагментом, определенными дляmj(§15.12.2.6). -
-
В противном случае, выражение создания экземпляра класса не использует
<>для удаления аргументов типа класса.Пусть T — тип, обозначаемый C, за которым следуют любые аргументы типа класса в выражении. Описанный в §15.12.2 процесс, измененный для обработки конструкторов, используется для выбора одного из конструкторов T и определения его
throwsфрагмента.Если нет единственного наиболее специфичного конструктора, который одновременно применим и доступен, возникает ошибка компиляции (как и при вызовах методов).
В противном случае, тип возврата — T.
Ошибка компиляции возникает, если аргумент выражения создания экземпляра класса не совместим со своим целевым типом, полученным из типа вызова (§15.12.2.6).
Если объявление во время компиляции применимо по вызову с переменным числом аргументов (§15.12.2.4), то, где последний тип формального параметра типа вызова конструктора — Fn[], ошибка компиляции возникает, если тип, являющийся стиранием Fn, недоступен в момент вызова.
Тип выражения создания экземпляра класса — это тип возврата выбранного конструктора, как определено выше.
Обратите внимание, что тип выражения создания экземпляра класса может быть типом анонимного класса, в этом случае вызываемый конструктор — анонимный конструктор (§15.9.5.1).
При выполнении выражение создания экземпляра класса вычисляется следующим образом.
Во-первых, если выражение создания экземпляра класса является квалифицированным выражением создания экземпляра класса, то квалифицируемое основное выражение вычисляется. Если квалифицирующее выражение вычисляет значение null, то возникает исключение NullPointerException, и выражение создания экземпляра класса завершается внезапно. Если квалифицирующее выражение завершается внезапно, то выражение создания экземпляра класса также завершается внезапно по той же причине.
Далее, выделяется память для нового экземпляра класса. Если памяти недостаточно для выделения объекта, вычисление выражения создания экземпляра класса завершается внезапно путем выброса исключения OutOfMemoryError.
Новый объект содержит новые экземпляры всех полей, объявленных в указанном типе класса и всех его суперклассах. При создании каждого нового экземпляра поля он инициализируется своим значением по умолчанию (§4.12.5).
Затем фактические аргументы конструктора вычисляются слева направо. Если вычисление любого из аргументов завершается внезапно, то любые выражения аргументов справа от него не вычисляются, и выражение создания экземпляра класса завершается внезапно по той же причине.
Далее, вызывается выбранный конструктор указанного типа класса. Это приводит к вызову как минимум одного конструктора для каждого суперкласса типа класса. Этот процесс может быть направлен явными операторами вызова конструктора (§8.8) и подробно описан в §12.5.
Значение выражения создания экземпляра класса является ссылкой на только что созданный объект указанного класса. При каждом вычислении выражения создаётся новый объект.
Пример 15.9.4-1. Порядок вычисления и обнаружение недостатка памяти
Если при вычислении выражения создания экземпляра класса обнаруживается недостаток памяти для выполнения операции создания, то выбрасывается исключение OutOfMemoryError. Эта проверка выполняется до вычисления каких-либо выражений аргументов.
Например, тестовая программа:
class List {
int value;
List next;
static List head = new List(0);
List(int n) { value = n; next = head; head = this; }
}
class Test {
public static void main(String[] args) {
int id = 0, oldid = 0;
try {
for (;;) {
++id;
new List(oldid = id);
}
} catch (Error e) {
List.head = null;
System.out.println(e.getClass() + ", " + (oldid==id));
}
}
}
выводит:
class java.lang.OutOfMemoryError, false
потому что условие недостатка памяти обнаружено до вычисления выражения аргумента oldid = id.
Сравните это с обработкой выражений создания массивов, для которых условие недостатка памяти обнаруживается после вычисления выражений размера (§15.10.2).
Объявление анонимного класса автоматически выводится из выражения создания экземпляра класса компилятором Java.
Анонимный класс никогда не является abstract (§8.1.1.1).
Анонимный класс всегда неявно final (§8.1.1.2).
Анонимный класс всегда является внутренним классом (§8.1.3); он никогда не является static (§8.1.1, §8.5.1).
У анонимного класса не может быть явно объявленного конструктора. Вместо этого для анонимного класса неявно объявляется анонимный конструктор. Форма анонимного конструктора для анонимного класса C с прямым суперклассом S следующая:
-
Если S не является внутренним классом или если S является локальным классом, который встречается в статическом контексте, то анонимный конструктор имеет один формальный параметр для каждого фактического аргумента выражения создания экземпляра класса, в котором объявлен C.
Фактические аргументы выражения создания экземпляра класса используются для определения конструктора
csтипа S по тем же правилам, что и для вызова методов (§15.12). Тип каждого формального параметра анонимного конструктора должен быть идентичен соответствующему формальному параметруcs.Тело конструктора состоит из явного вызова конструктора (§8.8.7.1) в форме
super(...), где фактические аргументы — это формальные параметры конструктора в порядке их объявления. -
В противном случае первый формальный параметр конструктора C представляет значение немедленно окружающего экземпляра
iотносительно S (§15.9.2, §15.9.3). Тип этого параметра — тип класса, который непосредственно включает объявление S.Конструктор имеет дополнительный формальный параметр для каждого фактического аргумента выражения создания экземпляра класса, объявившего анонимный класс. n'ый формальный параметр
eсоответствует n-1'ому фактическому аргументу.Фактические аргументы выражения создания экземпляра класса используются для определения конструктора
csтипа S по тем же правилам, что и для вызова методов (§15.12). Тип каждого формального параметра анонимного конструктора должен быть идентичен соответствующему формальному параметруcs.Тело конструктора состоит из явного вызова конструктора (§8.8.7.1) в форме
o.super(...), гдеo— это первый формальный параметр конструктора, а фактические аргументы — последующие формальные параметры конструктора в порядке их объявления.
В любом случае, раздел throws анонимного конструктора должен перечислять все исключения, обрабатываемые выражением явного вызова конструктора суперкласса, содержащегося в анонимном конструкторе, и все исключения, обрабатываемые любыми инициализаторами экземпляров или инициализаторами переменных экземпляров анонимного класса.
Обратите внимание, что возможно, что сигнатура анонимного конструктора ссылается на недоступный тип (например, если такой тип встречался в сигнатуре конструктора суперкласса cs). Это само по себе не приводит к ошибкам ни во время компиляции, ни во время выполнения.
Выражение создания массива используется для создания новых массивов (§10 (Массивы)).
Следующая продукция из §4.3 показана здесь для удобства:
Выражение создания массива создает объект, который является новым массивом, элементы которого имеют тип, указанный в PrimitiveType или ClassOrInterfaceType.
Это ошибка времени компиляции, если ClassOrInterfaceType не обозначает реализуемый тип (§4.7). В противном случае, ClassOrInterfaceType может именовать любой именованный ссылочный тип, даже тип класса abstract (§8.1.1.1) или тип интерфейса.
Вышеприведенные правила подразумевают, что тип элемента в выражении создания массива не может быть параметризованным типом, за исключением случаев, когда все аргументы типа параметризованного типа являются не ограниченными универсальными маркерами.
Тип каждого выражения измерения в DimExpr должен быть типом, преобразуемым (§5.1.8) к целочисленному типу, в противном случае произойдет ошибка времени компиляции.
Каждое выражение измерения претерпевает числовое повышение по унарной операции (§5.6.1). Полученный тип должен быть int, в противном случае произойдет ошибка времени компиляции.
Тип выражения создания массива — это тип массива, который может быть обозначен копией выражения создания массива, из которого удалены ключевое слово new и каждое выражение DimExpr и инициализатор массива.
Например, тип выражения создания:
new double[3][3][]
равен:
double[][][]
При выполнении выражение создания массива ведет себя следующим образом:
-
Если нет выражений измерения, то должен быть инициализатор массива. Новый выделенный массив будет инициализирован значениями, предоставленными инициализатором массива, как описано в §10.6. Значение инициализатора массива становится значением выражения создания массива.
-
В противном случае, инициализатора массива нет, и:
-
Сначала выражения измерения вычисляются слева направо. Если вычисление любого из выражений завершается внезапно, выражения справа от него не вычисляются.
-
Далее проверяются значения выражений измерения. Если значение любого выражения DimExpr меньше нуля, то генерируется исключение
NegativeArraySizeException. -
Далее выделяется место для нового массива. Если места недостаточно для выделения массива, вычисление выражения создания массива завершается внезапно сбросом исключения
OutOfMemoryError. -
Затем, если появляется единственное выражение DimExpr, создается одномерный массив заданной длины, и каждый элемент массива инициализируется своим значением по умолчанию (§4.12.5).
-
В противном случае, если появляется n выражений DimExpr, то создание массива фактически выполняет набор вложенных циклов глубиной n
-1для создания предполагаемых массивов массивов.Многомерный массив не обязан иметь массивы одинаковой длины на каждом уровне.
-
Пример 15.10.2-1. Вычисление создания массива
В выражении создания массива с одним или несколькими выражениями измерения каждое выражение измерения полностью вычисляется до того, как любая часть любого выражения измерения справа от него. Таким образом:
class Test1 {
public static void main(String[] args) {
int i = 4;
int ia[][] = new int[i][i=3];
System.out.println(
"[" + ia.length + "," + ia[0].length + "]");
}
}
выводит:
[4,3]
потому что первое измерение вычисляется как 4, прежде чем второе выражение измерения установит i в 3.
Если вычисление выражения измерения завершается внезапно, ни одна часть любого выражения измерения справа от него не будет, как будто, вычислена. Таким образом:
class Test2 {
public static void main(String[] args) {
int[][] a = { { 00, 01 }, { 10, 11 } };
int i = 99;
try {
a[val()][i = 1]++;
} catch (Exception e) {
System.out.println(e + ", i=" + i);
}
}
static int val() throws Exception {
throw new Exception("unimplemented");
}
}
выводит:
java.lang.Exception: unimplemented, i=99
потому что вложенное присваивание, которое устанавливает i в 1, никогда не выполняется.
Пример 15.10.2-2. Создание многомерного массива
Декларация:
float[][] matrix = new float[3][3];
эквивалентна по поведению:
float[][] matrix = new float[3][]; for (intd= 0;d< matrix.length;d++) matrix[d] = new float[3];
и:
Age[][][][][] Aquarius = new Age[6][10][8][12][];
эквивалентно:
Age[][][][][] Aquarius = new Age[6][][][][]; for (intd1= 0;d1< Aquarius.length;d1++) { Aquarius[d1] = new Age[10][][][]; for (intd2= 0;d2< Aquarius[d1].length;d2++) { Aquarius[d1][d2] = new Age[8][][]; for (intd3= 0;d3< Aquarius[d1][d2].length;d3++) { Aquarius[d1][d2][d3] = new Age[12][]; } } }
с d, d1, d2 и d3 заменены именами, которые еще не объявлены локально. Таким образом, одно выражение new фактически создает один массив длиной 6, 6 массивов длиной 10, 6x10 = 60 массивов длиной 8 и 6x10x8 = 480 массивов длиной 12. В этом примере пятое измерение, которое содержало бы фактические элементы массива (ссылки на объекты Age), инициализируется только нулевыми ссылками. Эти массивы могут быть заполнены позже другим кодом, например:
Age[] Hair = { new Age("quartz"), new Age("topaz") };
Aquarius[1][9][6][9] = Hair;
Треугольная матрица может быть создана следующим образом:
float triang[][] = new float[100][];
for (int i = 0; i < triang.length; i++)
triang[i] = new float[i+1];
Если вычисление выражения создания массива обнаруживает недостаточность памяти для выполнения операции создания, то генерируется исключение OutOfMemoryError. Если выражение создания массива не имеет инициализатора массива, то эта проверка выполняется только после того, как вычисление всех выражений измерения завершилось без ошибок. Если выражение создания массива имеет инициализатор массива, то исключение OutOfMemoryError может возникнуть при выделении объекта ссылочного типа во время вычисления выражения инициализатора переменной или при выделении места для массива для хранения значений (возможно, вложенного) инициализатора массива.
Пример 15.10.2-3. OutOfMemoryError и вычисление выражений измерения
class Test3 {
public static void main(String[] args) {
int len = 0, oldlen = 0;
Object[] a = new Object[0];
try {
for (;;) {
++len;
Object[] temp = new Object[oldlen = len];
temp[0] = a;
a = temp;
}
} catch (Error e) {
System.out.println(e + ", " + (oldlen==len));
}
}
}
Эта программа выводит:
java.lang.OutOfMemoryError, true
потому что условие недостатка памяти обнаруживается после вычисления выражения измерения oldlen = len.
Сравните это с выражениями создания экземпляров классов (§15.9), которые обнаруживают условие недостатка памяти до вычисления выражений аргументов (§15.9.4).
Выражение доступа к массиву ссылается на переменную, которая является компонентом массива.
Выражение доступа к массиву содержит два подвыражения: выражение ссылки на массив (перед левой квадратной скобкой) и выражение индекса (в квадратных скобках).
Обратите внимание, что выражение ссылки на массив может быть именем или любым первичным выражением, которое не является выражением создания массива (§15.10).
Тип выражения ссылки на массив должен быть типом массива (назовем его T[], массивом, компоненты которого имеют тип T), в противном случае произойдет ошибка компиляции.
Выражение индекса подвергается числовому продвижению (§5.6.1). Продвинутый тип должен быть int, в противном случае произойдет ошибка компиляции.
Тип выражения доступа к массиву — результат применения преобразования захвата (§5.1.10) к T.
Результат выражения доступа к массиву — переменная типа T, а именно переменная внутри массива, выбранная значением выражения индекса.
Эта результирующая переменная, которая является компонентом массива, никогда не рассматривается final, даже если выражение ссылки на массив обозначало final переменную.
Во время выполнения вычисление выражения доступа к массиву происходит следующим образом:
-
Сначала вычисляется выражение ссылки на массив. Если это вычисление завершается прерывисто, то доступ к массиву завершается по той же причине, и выражение индекса не вычисляется.
-
В противном случае вычисляется выражение индекса. Если это вычисление завершается прерывисто, то доступ к массиву завершается по той же причине.
-
В противном случае, если значение выражения ссылки на массив равно
null, то выбрасываетсяNullPointerException. -
В противном случае, значение выражения ссылки на массив действительно относится к массиву. Если значение выражения индекса меньше нуля или больше или равно размеру массива
length, то выбрасываетсяArrayIndexOutOfBoundsException. -
В противном случае, результатом доступа к массиву является переменная типа T, внутри массива, выбранная по значению выражения индекса.
Пример 15.10.4-1. Ссылка на массив вычисляется первой
При доступе к массиву выражение слева от скобок, кажется, полностью вычисляется до того, как любая часть выражения внутри скобок вычисляется. Например, в (явственно чудовищном) выражении a[(a=b)[3]], выражение a полностью вычисляется до выражения (a=b)[3]; это означает, что исходное значение a извлекается и запоминается, в то время как выражение (a=b)[3] вычисляется. Затем этот массив, на который ссылается исходное значение a, индексируется значением, которое является элементом 3 другого массива (возможно, того же массива), на который ссылалось b и теперь также ссылается на a.
Таким образом, программа:
class Test1 {
public static void main(String[] args) {
int[] a = { 11, 12, 13, 14 };
int[] b = { 0, 1, 2, 3 };
System.out.println(a[(a=b)[3]]);
}
}
выводит:
14
потому что значение чудовищного выражения эквивалентно a[b[3]] или a[3] или 14.
Пример 15.10.4-2. Прерывистое завершение вычисления ссылки на массив
Если вычисление выражения слева от скобок завершается прерывисто, никакая часть выражения внутри скобок, кажется, не вычисляется. Таким образом, программа:
class Test2 {
public static void main(String[] args) {
int index = 1;
try {
skedaddle()[index=2]++;
} catch (Exception e) {
System.out.println(e + ", index=" + index);
}
}
static int[] skedaddle() throws Exception {
throw new Exception("Ciao");
}
}
выводит:
java.lang.Exception: Ciao, index=1
потому что встроенное присваивание 2 к index никогда не происходит.
Пример 15.10.4-3. null Ссылка на массив
Если выражение ссылки на массив производит null вместо ссылки на массив, то во время выполнения выбрасывается NullPointerException, но только после того, как все части выражения доступа к массиву будут вычислены и только если эти вычисления завершились нормально. Таким образом, программа:
class Test3 {
public static void main(String[] args) {
int index = 1;
try {
nada()[index=2]++;
} catch (Exception e) {
System.out.println(e + ", index=" + index);
}
}
static int[] nada() { return null; }
}
выводит:
java.lang.NullPointerException, index=2
потому что встроенное присваивание 2 к index происходит до проверки на null выражение ссылки на массив. В качестве связанного примера, программа:
class Test4 {
public static void main(String[] args) {
int[] a = null;
try {
int i = a[vamoose()];
System.out.println(i);
} catch (Exception e) {
System.out.println(e);
}
}
static int vamoose() throws Exception {
throw new Exception("Twenty-three skidoo!");
}
}
всегда выводит:
java.lang.Exception: Twenty-three skidoo!
NullPointerException никогда не происходит, потому что выражение индекса должно быть полностью вычислено, прежде чем любая другая часть доступа к массиву произойдёт, и это включает в себя проверку на то, является ли значение выражения ссылки на массив null.
Выражение доступа к полю может получить доступ к полю объекта или массива, ссылка на который является значением либо выражения, либо специального ключевого слова super.
Значение выражения доступа к полю определяется по тем же правилам, что и для квалифицированных имён (§6.5.6.2), но ограничено тем, что выражение не может обозначать пакет, тип класса или тип интерфейса.
Также можно обратиться к полю текущего экземпляра или текущего класса, используя простое имя (§6.5.6.1).
Тип Primary должен быть ссылочным типом T, в противном случае произойдёт ошибка компиляции.
Значение выражения доступа к полю определяется следующим образом:
-
Если идентификатор называет несколько доступных (§6.6) полей-членов в типе T, то доступ к полю является неоднозначным, и возникает ошибка компиляции.
-
Если идентификатор не называет доступное поле-член в типе T, то доступ к полю не определён, и возникает ошибка компиляции.
-
В противном случае, идентификатор называет единственное доступное поле-член в типе T, и тип выражения доступа к полю — это тип поля-члена после преобразования захвата (§5.1.10).
При выполнении выражения доступа к полю результат вычисляется следующим образом: (предполагая, что программа корректна с точки зрения анализа определённой присваиваемости, т.е. каждая пустая final переменная присваивается перед доступом)
-
Если поле является
static:-
Вычисляется выражение Primary, и результат отбрасывается. Если вычисление выражения Primary завершается внезапно, то выражение доступа к полю завершается внезапно по той же причине.
-
Если поле — это непустое
finalполе, то результат — это значение указанной переменной класса в классе или интерфейсе, который является типом выражения Primary. -
Если поле не является
final, или является пустымfinalи доступ к полю происходит в статическом инициализаторе или инициализаторе переменной класса, то результатом является переменная, а именно указанная переменная класса в классе, который является типом выражения Primary.
-
-
Если поле не является
static:-
Вычисляется выражение Primary. Если вычисление выражения Primary завершается внезапно, то выражение доступа к полю завершается внезапно по той же причине.
-
Если значение Primary равно
null, то выбрасываетсяNullPointerException. -
Если поле — это непустое
final, то результатом является значение поля-члена с указанным именем в типе T, найденного в объекте, на который ссылается значение Primary. -
Если поле не является
final, или является пустымfinal, и доступ к полю происходит в конструкторе или инициализаторе переменной экземпляра, то результатом является переменная, а именно имя поля-члена в типе T, найденного в объекте, на который указывает значение Primary.
-
Обратите внимание, что при определении поля, которое будет использоваться, используется только тип выражения Primary, а не класс фактического объекта, на который указывает ссылка во время выполнения.
Пример 15.11.1-1. Статическая привязка для доступа к полю
class S { int x = 0; }
class T extends S { int x = 1; }
class Test1 {
public static void main(String[] args) {
T t = new T();
System.out.println("t.x=" + t.x + when("t", t));
S s = new S();
System.out.println("s.x=" + s.x + when("s", s));
s = t;
System.out.println("s.x=" + s.x + when("s", s));
}
static String when(String name, Object t) {
return " when " + name + " holds a "
+ t.getClass() + " at run time.";
}
}
Эта программа выводит:
t.x=1 when t holds a class T at run time. s.x=0 when s holds a class S at run time. s.x=0 when s holds a class T at run time.
Последняя строка показывает, что поле, к которому происходит доступ, действительно не зависит от класса объекта, на который ссылаются во время выполнения; даже если s содержит ссылку на объект класса T, выражение s.x ссылается на поле x класса S, потому что тип выражения s — S. Объекты класса T содержат два поля с именем x, одно для класса T и одно для его суперкласса S.
Отсутствие динамического поиска при доступе к полям позволяет программам работать эффективно с простыми реализациями. Возможность поздней привязки и переопределения доступна, но только когда используются методы экземпляра. Рассмотрим тот же пример с использованием методов экземпляра для доступа к полям:
class S { int x = 0; int z() { return x; } }
class T extends S { int x = 1; int z() { return x; } }
class Test2 {
public static void main(String[] args) {
T t = new T();
System.out.println("t.z()=" + t.z() + when("t", t));
S s = new S();
System.out.println("s.z()=" + s.z() + when("s", s));
s = t;
System.out.println("s.z()=" + s.z() + when("s", s));
}
static String when(String name, Object t) {
return " when " + name + " holds a "
+ t.getClass() + " at run time.";
}
}
Теперь вывод:
t.z()=1 when t holds a class T at run time. s.z()=0 when s holds a class S at run time. s.z()=1 when s holds a class T at run time.
Последняя строка показывает, что метод, к которому происходит доступ, зависит от класса объекта, на который ссылаются во время выполнения; когда s содержит ссылку на объект класса T, выражение s.z() ссылается на метод z класса T, несмотря на то, что тип выражения s — S. Метод z класса T переопределяет метод z класса S.
Пример 15.11.1-2. Переменная получателя не имеет значения для доступа к классовому полю
Следующая программа демонстрирует, что ссылка null может использоваться для доступа к классовой (static) переменной без вызова исключения:
class Test3 {
static String mountain = "Chocorua";
static Test3 favorite(){
System.out.print("Mount ");
return null;
}
public static void main(String[] args) {
System.out.println(favorite().mountain);
}
}
Она компилируется, выполняется и выводит:
Mount Chocorua
Даже если результатом favorite() является null, исключение NullPointerException не выбрасывается. Вывод "Mount " демонстрирует, что выражение Primary действительно полностью вычисляется во время выполнения, несмотря на то, что только его тип, а не значение, используется для определения поля для доступа (поскольку поле mountain является static).
Форма super.Идентификатор ссылается на поле с именем Идентификатор текущего объекта, но с текущим объектом, рассматриваемым как экземпляр суперкласса текущего класса.
Форма T.super.Идентификатор ссылается на поле с именем Идентификатор лексически окружающего экземпляра, соответствующего T, но с этим экземпляром, рассматриваемым как экземпляр суперкласса T.
Формы с ключевым словом super допустимы только в методе экземпляра, инициализаторе экземпляра или конструкторе класса, или в инициализаторе переменной экземпляра класса. Если они встречаются где-либо ещё, возникает ошибка компиляции.
Это точно те же ситуации, в которых ключевое слово this может использоваться в объявлении класса (§15.8.3).
Ошибка компиляции, если формы с ключевым словом super появляются в объявлении класса Object, так как Object не имеет суперкласса.
Предположим, что выражение доступа к полю super.f появляется внутри класса C, а непосредственный суперкласс C — класс S. Если f в S доступно из класса C (§6.6), то super.f обрабатывается так, как если бы это было выражение this.f в теле класса S. В противном случае возникает ошибка компиляции.
Таким образом, super.f может получить доступ к полю f, доступному в классе S, даже если это поле скрыто объявлением поля f в классе C.
Предположим, что выражение доступа к полю T.super.f появляется внутри класса C, а непосредственный суперкласс класса, обозначенного T, — класс с полным квалифицированным именем S. Если f в S доступно из C, то T.super.f обрабатывается так, как если бы это было выражение this.f в теле класса S. В противном случае возникает ошибка компиляции.
Таким образом, T.super.f может получить доступ к полю f, доступному в классе S, даже если это поле скрыто объявлением поля f в классе T.
Ошибка компиляции, если текущий класс не является внутренним классом класса T или T сам по себе.
Пример 15.11.2-1. Выражение super
interface I { int x = 0; }
class T1 implements I { int x = 1; }
class T2 extends T1 { int x = 2; }
class T3 extends T2 {
int x = 3;
void test() {
System.out.println("x=\t\t" + x);
System.out.println("super.x=\t\t" + super.x);
System.out.println("((T2)this).x=\t" + ((T2)this).x);
System.out.println("((T1)this).x=\t" + ((T1)this).x);
System.out.println("((I)this).x=\t" + ((I)this).x);
}
}
class Test {
public static void main(String[] args) {
new T3().test();
}
}
Эта программа выводит:
x= 3 super.x= 2 ((T2)this).x= 2 ((T1)this).x= 1 ((I)this).x= 0
Внутри класса T3, выражение super.x имеет тот же эффект, что и ((T2)this).x, когда x имеет доступ к пакету. Обратите внимание, что super.x не указан в терминах приведения типов из-за трудностей с доступом к protected членам суперкласса.
Выражение вызова метода используется для вызова метода класса или экземпляра.
( [СписокАргументов] ) ИмяТипа
. [ПараметрыТипов] Идентификатор ( [СписокАргументов] ) ИмяВыражения
. [ПараметрыТипов] Идентификатор ( [СписокАргументов] ) Основное
. [ПараметрыТипов] Идентификатор ( [СписокАргументов] ) super . [ПараметрыТипов] Идентификатор ( [СписокАргументов] ) ИмяТипа
. super . [ПараметрыТипов] Идентификатор ( [СписокАргументов] ) Разрешение имени метода во время компиляции более сложно, чем разрешение имени поля, из-за возможности перегрузки методов. Вызов метода во время выполнения также более сложен, чем доступ к полю, из-за возможности переопределения метода экземпляра.
Определение метода, который будет вызван выражением вызова метода, включает несколько шагов. Следующие три раздела описывают обработку выражения вызова метода во время компиляции. Определение типа выражения вызова метода указано в §15.12.3.
Типы исключений, которые может вызвать выражение вызова метода, указаны в §11.2.1.
Ошибка компиляции, если имя слева от правейшего ".", который встречается перед ( в MethodInvocation, не может быть классифицировано как TypeName или ExpressionName (§6.5.2).
Если ПараметрыТипов присутствуют слева от Идентификатор, то ошибка компиляции, если какие-либо из аргументов типа являются условными (§4.5.1).
Выражение вызова метода является поливыражением, если все следующие условия истинны:
-
Вызов появляется в контексте присваивания или вызова (§5.2, §5.3).
-
Если вызов квалифицирован (то есть любой вид MethodInvocation, кроме первого), то вызов опускает ПараметрыТипов слева от Идентификатор.
-
Метод, который будет вызван, как определено в последующих подразделах, является универсальным (§8.4.4) и имеет тип возврата, который упоминает по крайней мере один из параметров типа метода.
В противном случае выражение вызова метода является отдельным выражением.
Первый шаг при обработке вызова метода во время компиляции заключается в определении имени вызываемого метода и класса или интерфейса, в котором следует искать определения методов с таким именем.
Имя метода задается именемМетода или идентификатором, непосредственно предшествующим левой круглой скобке вызоваМетода.
Для определения класса или интерфейса для поиска необходимо рассмотреть шесть случаев, в зависимости от формы, предшествующей левой круглой скобке вызоваМетода:
-
Если форма представляет собой именемМетода, то есть только идентификатор, то:
Если идентификатор находится в области видимости объявления метода с таким именем (§6.3, §6.4.1), то:
-
Если есть охватывающее объявление типа, членом которого является этот метод, пусть T будет самым внутренним таким объявлением типа. Класс или интерфейс для поиска — это T.
Эта политика поиска называется «правило гребня». Она фактически ищет методы в иерархии суперклассов вложенного класса перед поиском методов в охватывающем классе и его иерархии суперклассов. См. §6.5.7.1 для примера.
-
В противном случае, объявление видимого метода может находиться в области видимости из-за одного или нескольких объявлений single-static-import или static-import-on-demand. Класс или интерфейс для поиска отсутствует, так как метод, который нужно вызвать, определяется позже (§15.12.2.1).
-
-
Если форма имеет вид ИмяТипа
.[ТипАргументов] Идентификатор, то типом для поиска является тип, обозначенный ИмяТипа. -
Если форма имеет вид ИмяВыражения
.[ТипАргументов] Идентификатор, то классом или интерфейсом для поиска является объявленный тип T переменной, обозначенной ИмяВыражения, если T является типом класса или интерфейса, или верхняя граница T, если T является переменной типа. -
Если форма имеет вид ОсновнойЭлемент
.[ТипАргументов] Идентификатор, то пусть T будет типом выражения ОсновнойЭлемент. Класс или интерфейс для поиска — это T, если T является типом класса или интерфейса, или верхняя граница T, если T является переменной типа.Ошибка компиляции, если T не является типом ссылки.
-
Если форма имеет вид
super.[ТипАргументов] Идентификатор, то классом для поиска является суперкласс класса, объявление которого содержит вызов метода.Пусть T будет объявлением типа, непосредственно охватывающим вызов метода. Ошибка компиляции, если T является классом
Objectили T является интерфейсом. -
Если форма имеет вид ИмяТипа
.super.[ТипАргументов] Идентификатор, то:-
Ошибка компиляции, если ИмяТипа обозначает ни класс, ни интерфейс.
-
Если ИмяТипа обозначает класс C, то классом для поиска является суперкласс C.
Ошибка компиляции, если C не является охватывающим объявлением типа текущего класса, или если C — класс
Object.Пусть T будет объявлением типа, непосредственно охватывающим вызов метода. Ошибка компиляции, если T является классом
Object. -
В противном случае, ИмяТипа обозначает интерфейс для поиска, I.
Ошибка компиляции, если I не является непосредственным суперинтерфейсом T, или если существует другой непосредственный суперкласс или непосредственный суперинтерфейс T, J, такой, что J является подтипом I.
-
Синтаксис ИмяТипа . super перегружен: традиционно, ИмяТипа относится к охватывающему объявлению типа, являющемуся классом, а целевым объектом является суперкласс этого класса, как если бы вызов был неквалифицированным super в охватывающем объявлении типа.
class Superclass {
void foo() { System.out.println("Hi"); }
}
class Subclass1 extends Superclass {
void foo() { throw new UnsupportedOperationException(); }
Runnable tweak = new Runnable() {
void run() {
Subclass1.super.foo(); // Gets the 'println' behavior
}
};
}
Для поддержки вызова методов по умолчанию в суперинтерфейсах, ИмяТипа также может относиться к непосредственному суперинтерфейсу текущего класса или интерфейса, а целевой объект — это этот суперинтерфейс.
interface Superinterface {
default void foo() { System.out.println("Hi"); }
}
class Subclass2 implements Superinterface {
void foo() { throw new UnsupportedOperationException(); }
void tweak() {
Superinterface.super.foo(); // Gets the 'println' behavior
}
}
Ни один синтаксис не поддерживает комбинацию этих форм, то есть вызов метода суперинтерфейса охватывающего объявления типа, являющегося классом, как если бы вызов был в форме ИмяИнтерфейса . super в охватывающем объявлении типа.
class Subclass3 implements Superinterface {
void foo() { throw new UnsupportedOperationException(); }
Runnable tweak = new Runnable() {
void run() {
Subclass3.Superinterface.super.foo(); // Illegal
}
};
}
Обходным путём является введение метода private в охватывающее объявление типа, который выполняет вызов интерфейса super.
На втором шаге происходит поиск методов-членов в типе, определённом на предыдущем шаге. На этом шаге имя метода и выражения аргументов используются для поиска методов, которые одновременно являются доступными и применимыми, то есть объявления, которые могут быть правильно вызваны с заданными аргументами.
Может быть найдено несколько таких методов, в этом случае выбирается наиболее специфичный. Описание (сигнатура плюс тип возвращаемого значения) наиболее специфичного метода используется во время выполнения для выполнения диспетчеризации метода.
Метод считается применимым, если он применим в соответствии с одним из следующих способов: строгий вызов (§15.12.2.2), нестрогий вызов (§15.12.2.3) или вызов с переменной арностью (§15.12.2.4).
Некоторые выражения аргументов, содержащие неявные лямбда-выражения (§15.27.1) или неточные ссылки на методы (§15.13.1), игнорируются при проверке применимости, потому что их смысл невозможно определить до выбора целевого типа.
Хотя вызов метода может быть поливыражением, только выражения его аргументов – не целевой тип вызова – влияют на выбор применимых методов.
Процесс определения применимости начинается с определения потенциально применимых методов (§15.12.2.1).
Остальная часть процесса разделена на три фазы для обеспечения совместимости с версиями языка программирования Java до Java SE 5.0. Фазы следующие:
-
Первая фаза (§15.12.2.2) выполняет разрешение перегрузки без использования преобразований boxing или unboxing, или вызова метода с переменной арностью. Если во время этой фазы применимый метод не найден, обработка продолжается на второй фазе.
Это гарантирует, что любые вызовы, которые были допустимы в языке программирования Java до Java SE 5.0, не считаются неоднозначными в результате введения методов с переменной арностью, неявного boxing и/или unboxing. Однако объявление метода с переменной арностью (§8.4.1) может изменить выбранный метод для данного выражения вызова метода, так как метод с переменной арностью обрабатывается как метод с фиксированной арностью на первой фазе. Например, объявление
m(Object...)в классе, который уже объявляетm(Object), приводит к тому, чтоm(Object)больше не выбирается для некоторых выражений вызова (например,m(null)), так какm(Object[])является более специфичным. -
Вторая фаза (§15.12.2.3) выполняет разрешение перегрузки, позволяя использовать преобразования boxing и unboxing, но по-прежнему исключая использование вызова метода с переменной арностью. Если применимый метод не найден на этой фазе, обработка продолжается на третьей фазе.
Это гарантирует, что метод никогда не выбирается с помощью вызова метода с переменной арностью, если он применим с помощью вызова метода с фиксированной арностью.
-
Третья фаза (§15.12.2.4) позволяет объединить перегрузку с методами переменной арности, boxing и unboxing.
Определение того, является ли метод применимым, в случае общих методов (§8.4.4), потребует анализа аргументов типов. Аргументы типа могут передаваться явно или неявно. Если они передаются неявно, границы аргументов типа должны быть выведены (§18 (Вывод типов)) из выражений аргументов.
Если в одной из трёх фаз проверки применимости было определено несколько применимых методов, то выбирается наиболее специфичный из них, как указано в разделе §15.12.2.5.
Для проверки применимости типы аргументов вызова, как правило, не могут быть входными данными анализа. Это связано с тем, что:
-
Аргументы вызова метода могут быть поливыражениями
-
Поливыражения не могут быть типизированы без целевого типа
-
Разрешение перегрузки должно быть завершено, прежде чем целевые типы аргументов будут известны
Вместо этого входом в проверку применимости является список выражений аргументов, которые можно проверить на совместимость с потенциальными целевыми типами, даже если конечные типы выражений неизвестны.
Обратите внимание, что разрешение перегрузки независимо от целевого типа. Это связано с двумя причинами:
-
Во-первых, это делает модель пользователя более доступной и менее подверженной ошибкам. Смысл имени метода (то есть объявление, соответствующее имени) слишком фундаментален для понимания программы, чтобы зависеть от тонких контекстных подсказок. (В отличие от других поливыражений, поведение которых может различаться в зависимости от целевого типа; но изменения в поведении всегда ограничены и по существу эквивалентны, в то время как нет таких гарантий в отношении поведения произвольного набора методов, которые разделяют имя и арность.)
-
Во-вторых, это позволяет другим свойствам — таким, как то, является ли метод поливыражением (§15.12) или как классифицировать условное выражение (§15.25) — зависеть от смысла имени метода, даже до того, как будет известен целевой тип.
Пример 15.12.2-1. Применимость метода
class Doubler {
static int two() { return two(1); }
private static int two(int i) { return 2*i; }
}
class Test extends Doubler {
static long two(long j) { return j+j; }
public static void main(String[] args) {
System.out.println(two(3));
System.out.println(Doubler.two(3)); // compile-time error
}
}
Для вызова метода two(1) внутри класса Doubler существует два доступных метода с именем two, но только второй является применимым, поэтому он и вызывается во время выполнения.
Для вызова метода two(3) внутри класса Test существуют два применимых метода, но только метод класса Test доступен, поэтому он вызывается во время выполнения (аргумент 3 преобразуется к типу long).
Для вызова метода Doubler.two(3) класс Doubler, а не класс Test, ищутся методы с именем two; единственный применимый метод недоступен, поэтому этот вызов метода вызывает ошибку компиляции.
Другой пример:
class ColoredPoint {
int x, y;
byte color;
void setColor(byte color) { this.color = color; }
}
class Test {
public static void main(String[] args) {
ColoredPoint cp = new ColoredPoint();
byte color = 37;
cp.setColor(color);
cp.setColor(37); // compile-time error
}
}
Здесь во время компиляции возникает ошибка для второго вызова setColor, так как применимый метод не может быть найден во время компиляции. Тип литерала 37 — int, и int не может быть преобразован к byte с помощью преобразования вызова. Преобразование присваивания, используемое при инициализации переменной color, выполняет неявное преобразование константы из типа int в byte, что разрешено, так как значение 37 достаточно мало, чтобы быть представлено в типе byte; но такое преобразование не разрешено для преобразования вызова.
Однако, если метод setColor был объявлен для приёма int вместо byte, то оба вызова метода были бы корректными; первый вызов был бы разрешён, потому что преобразование вызова допускает расширяющее преобразование от byte до int. Однако в теле setColor тогда потребовался бы сужающий cast:
void setColor(int color) { this.color = (byte)color; }
Вот пример неоднозначности перегрузки. Рассмотрим программу:
class Point { int x, y; }
class ColoredPoint extends Point { int color; }
class Test {
static void test(ColoredPoint p, Point q) {
System.out.println("(ColoredPoint, Point)");
}
static void test(Point p, ColoredPoint q) {
System.out.println("(Point, ColoredPoint)");
}
public static void main(String[] args) {
ColoredPoint cp = new ColoredPoint();
test(cp, cp); // compile-time error
}
}
Этот пример приводит к ошибке на этапе компиляции. Проблема в том, что существуют два объявления test, которые применимы и доступны, и ни одно из них не является более специфичным, чем другое. Поэтому вызов метода является неоднозначным.
Если бы было добавлено третье определение test:
static void test(ColoredPoint p, ColoredPoint q) {
System.out.println("(ColoredPoint, ColoredPoint)");
}
то оно было бы более специфичным, чем другие два, и вызов метода больше не был бы неоднозначным.
Пример 15.12.2-2. Тип возвращаемого значения не учитывается при выборе метода
class Point { int x, y; }
class ColoredPoint extends Point { int color; }
class Test {
static int test(ColoredPoint p) {
return p.color;
}
static String test(Point p) {
return "Point";
}
public static void main(String[] args) {
ColoredPoint cp = new ColoredPoint();
String s = test(cp); // compile-time error
}
}
Здесь наиболее специфическое объявление метода test — это то, которое принимает параметр типа ColoredPoint. Поскольку тип результата метода — int, возникает ошибка компиляции, так как int не может быть преобразован к String с помощью преобразования присваивания. Этот пример показывает, что типы результатов методов не участвуют в разрешении перегруженных методов, поэтому второй метод test, возвращающий String, не выбирается, даже если его тип результата позволил бы программе компилироваться без ошибки.
Пример 15.12.2-3. Выбор наиболее конкретного метода
Наиболее конкретный метод выбирается во время компиляции; его описание определяет, какой метод фактически выполняется во время выполнения. Если к классу добавляется новый метод, то исходный код, который был скомпилирован со старым определением класса, может не использовать новый метод, даже если повторная компиляция приведет к выбору этого метода.
Например, рассмотрим два модуля компиляции, один для класса Point:
package points;
public class Point {
public int x, y;
public Point(int x, int y) { this.x = x; this.y = y; }
public String toString() { return toString(""); }
public String toString(String s) {
return "(" + x + "," + y + s + ")";
}
}
и один для класса ColoredPoint:
package points;
public class ColoredPoint extends Point {
public static final int
RED = 0, GREEN = 1, BLUE = 2;
public static String[] COLORS =
{ "red", "green", "blue" };
public byte color;
public ColoredPoint(int x, int y, int color) {
super(x, y);
this.color = (byte)color;
}
/** Copy all relevant fields of the argument into
this ColoredPoint object. */
public void adopt(Point p) { x = p.x; y = p.y; }
public String toString() {
String s = "," + COLORS[color];
return super.toString(s);
}
}
Теперь рассмотрим третий модуль компиляции, который использует ColoredPoint:
import points.*;
class Test {
public static void main(String[] args) {
ColoredPoint cp =
new ColoredPoint(6, 6, ColoredPoint.RED);
ColoredPoint cp2 =
new ColoredPoint(3, 3, ColoredPoint.GREEN);
cp.adopt(cp2);
System.out.println("cp: " + cp);
}
}
Выходные данные:
cp: (3,3,red)
Программист, который кодировал класс Test, ожидал увидеть слово green, потому что фактический аргумент, ColoredPoint, имеет поле color, и color, по-видимому, является "релевантным полем". (Конечно, документация пакета points должна была быть гораздо точнее!)
Обратите внимание, что наиболее конкретный метод (и, действительно, единственный применимый метод) для вызова метода adopt имеет подпись, указывающую на метод с одним параметром, тип которого Point. Эта подпись становится частью двоичного представления класса Test, созданного компилятором Java, и используется вызовом метода во время выполнения.
Предположим, программист сообщил об этой ошибке программного обеспечения, и разработчик пакета points решил после долгих размышлений исправить ее, добавив метод в класс ColoredPoint:
public void adopt(ColoredPoint p) {
adopt((Point)p);
color = p.color;
}
Если затем программист запустит старый двоичный файл для Test с новым двоичным файлом для ColoredPoint, выходные данные останутся:
cp: (3,3,red)
потому что старый двоичный файл для Test все еще имеет описание "один параметр, тип которого Point; void" для вызова метода cp.adopt(cp2). Если исходный код для Test перекомпилируется, компилятор Java обнаружит, что теперь существуют два применимых метода adopt, и что подпись для более конкретного метода — "один параметр, тип которого ColoredPoint; void"; запуск программы затем даст желаемый результат:
cp: (3,3,green)
Проявляя предусмотрительность в отношении таких проблем, разработчик пакета points мог исправить класс ColoredPoint, чтобы он работал как со свежескомпилированным, так и со старым кодом, добавив защитный код в старый метод adopt для поддержки старого кода, который все еще вызывает его с аргументами ColoredPoint:
public void adopt(Point p) {
if (p instanceof ColoredPoint)
color = ((ColoredPoint)p).color;
x = p.x; y = p.y;
}
В идеале исходный код следует перекомпилировать всякий раз, когда изменяется код, от которого он зависит. Однако в среде, где разные классы поддерживаются различными организациями, это не всегда возможно. Защитное программирование с внимательным отношением к проблемам эволюции классов может сделать обновленный код гораздо более надежным. Подробнее о двоичной совместимости и эволюции типов см. §13 (Двоичная совместимость).
Класс или интерфейс, определенный на шаге 1 компиляции (§15.12.1), ищутся все методы-члены, которые потенциально могут быть применены к этому вызову метода; члены, унаследованные от суперклассов и суперинтерфейсов, также включаются в этот поиск.
Кроме того, если форма выражения вызова метода — ИмяМетода — то есть, единственный Идентификатор — то поиск потенциально применимых методов также рассматривает все методы-члены, импортированные объявлениями single-static-import и static-import-on-demand в единице компиляции, где происходит вызов метода (§7.5.3, §7.5.4) и которые не затеняются в месте появления вызова метода.
Метод-член является потенциально применимым к вызову метода, если и только если выполняются все следующие условия:
-
Имя члена идентично имени метода в вызове метода.
-
Член доступен (§6.6) для класса или интерфейса, в котором появляется вызов метода.
Доступность метода-члена при вызове метода зависит от модификатора доступа (
public,protected, отсутствует модификатор (доступ по пакетам), илиprivate) в объявлении члена и от места, где появляется вызов метода. -
Если член — метод фиксированной арности с арностью n, арность вызова метода равна n, и для всех i (1 ≤ i ≤ n) i-й аргумент вызова метода потенциально совместим, как определено ниже, с типом i-го параметра метода.
-
Если член — метод переменной арности с арностью n, то для всех i (1 ≤ i ≤ n-1) i-й аргумент вызова метода потенциально совместим с типом i-го параметра метода; и, где n-й параметр метода имеет тип T
[], выполняется одно из следующих условий:-
Арность вызова метода равна n-1.
-
Арность вызова метода равна n, и n-й аргумент вызова метода потенциально совместим с T или T
[]. -
Арность вызова метода равна m, где m > n, и для всех i (n ≤ i ≤ m), i-й аргумент вызова метода потенциально совместим с T.
-
-
Если вызов метода включает явные аргументы типа, и член — это метод с параметрами, то количество аргументов типа равно количеству параметров типа метода.
Этот пункт подразумевает, что негенерированный метод может быть потенциально применим к вызову, предоставляющему явные аргументы типа. Действительно, он может оказаться применим. В таком случае аргументы типа просто будут проигнорированы.
Это правило вытекает из вопросов совместимости и принципов подстановки. Поскольку интерфейсы или суперклассы могут быть обобщены независимо от своих подтипов, мы можем переопределять обобщенные методы необобщенными. Однако переопределяющий (не обобщенный) метод должен быть применим к вызовам обобщенного метода, включая вызовы, которые явно передают аргументы типа. В противном случае подтип не мог бы быть заменен своим обобщенным супертипом.
Если поиск не выявит по крайней мере один потенциально применимый метод, возникает ошибка времени компиляции.
Выражение потенциально совместимо с целевым типом в соответствии со следующими правилами:
-
Выражение лямбда-функции (§15.27) потенциально совместимо с типом функционального интерфейса (§9.8), если выполняются все следующие условия:
-
Арность целевого типа функции такая же, как и арность выражения лямбда-функции.
-
Если целевой тип функции имеет
voidвозвращаемое значение, то тело лямбда-функции является либо выражением оператора (§14.8), либо блоком, совместимым с void (§15.27.2). -
Если целевой тип функции имеет (не-
void) возвращаемый тип, то тело лямбда-функции является либо выражением, либо блоком, совместимым со значением (§15.27.2).
-
-
Выражение ссылки на метод (§15.13) потенциально совместимо с типом функционального интерфейса, если арность типа функции равна n, существует по крайней мере один потенциально применимый метод для выражения ссылки на метод с арностью n (§15.13.1), и выполняется одно из следующих условий:
-
Выражение ссылки на метод имеет форму ТипСсылок
::[АргументыТипов] Идентификатор и по крайней мере один потенциально применимый метод i)staticи поддерживает арность n, или ii) неstaticи поддерживает арность n-1. -
Выражение ссылки на метод имеет другую форму, и по крайней мере один потенциально применимый метод не
static.
-
-
Выражение лямбда-функции или ссылка на метод потенциально совместимы с переменной типа, если переменная типа является параметром типа кандидата.
-
Выражение в скобках (§15.8.5) потенциально совместимо с типом, если входящее в него выражение потенциально совместимо с этим типом.
-
Условное выражение (§15.25) потенциально совместимо с типом, если каждое из выражений второго и третьего операндов потенциально совместимо с этим типом.
-
Выражение создания экземпляра класса, выражение вызова метода или выражение в автономной форме (§15.2) потенциально совместимо с любым типом.
Определение потенциальной применимости выходит за рамки базовой проверки арности и также учитывает наличие и «форму» целевых типов функциональных интерфейсов. В некоторых случаях, связанных с выводом типов, выражение лямбда-функции, появляющееся в качестве аргумента вызова метода, не может быть корректно типизировано до разрешения перегрузки. Эти правила позволяют учитывать форму выражения лямбда-функции, отбрасывая явно неверные целевые типы, которые в противном случае могли бы вызвать ошибки неоднозначности.
Выражение аргумента считается применимым для потенциально применимого метода m, если оно не имеет одного из следующих форм:
-
Явно типизированное лямбда-выражение (§15.27.1).
-
Выражение ссылки на метод с неточной типизацией (§15.13.1).
-
Если
mявляется обобщенным методом, а вызов метода не предоставляет явные типы аргументов, явно типизированное лямбда-выражение или точная ссылка на метод, для которого соответствующий целевой тип (полученный из подписиm) является параметром типаm. -
Явно типизированное лямбда-выражение, тело которого является выражением, которое не относится к применимости.
-
Явно типизированное лямбда-выражение, тело которого является блоком, где по крайней мере одно выражение результата не относится к применимости.
-
Выражение в скобках (§15.8.5), содержащее выражение, которое не относится к применимости.
-
Условное выражение (§15.25), где второй или третий операнд не относится к применимости.
Пусть m — потенциально применимый метод (§15.12.2.1) с арностью n и формальными типами параметров F1 ... Fn, а e1, ..., en — выражения фактических аргументов вызова метода. Тогда:
-
Если
m— обобщенный метод, и вызов метода не предоставляет явные типы аргументов, то применимость метода определяется, как указано в §18.5.1. -
Если
m— обобщенный метод, и вызов метода предоставляет явные типы аргументов, то пусть R1 ... Rp (p ≥ 1) — параметры типаm, Bl — объявленное ограничение Rl (1 ≤ l ≤ p), и U1, ..., Up — явные типы аргументов, заданные в вызове метода. Тогдаmявляется применимым по строгому вызову, если оба следующих условия верны:-
Для 1 ≤ i ≤ n, если
eiотносится к применимости, тоeiсовместим в контексте строгого вызова с Fi[R1:=U1, ..., Rp:=Up]. -
Для 1 ≤ l ≤ p, Ul
<:Bl[R1:=U1, ..., Rp:=Up].
-
-
Если
mне является обобщенным методом, тоmявляется применимым по строгому вызову, если для 1 ≤ i ≤ n, либоeiсовместим в контексте строгого вызова с Fi, либоeiне относится к применимости.
Если метод, применимый по строгому вызову, не найден, поиск применимых методов продолжается со второй фазы (§15.12.2.3).
В противном случае выбирается наиболее специфичный метод (§15.12.2.5) среди методов, применимых по строгому вызову.
Смысл явно типизированного лямбда-выражения или ссылки на метод с неточной типизацией достаточно неясен до разрешения целевого типа, поэтому аргументы, содержащие эти выражения, не считаются применимыми; они просто игнорируются (кроме ожидаемой арности) до завершения разрешения перегрузки.
Пусть m — потенциально применимый метод (§15.12.2.1) с арностью n и формальными типами параметров F1, ..., Fn, а e1, ..., en — выражения фактических аргументов вызова метода. Тогда:
-
Если
m— обобщенный метод, и вызов метода не предоставляет явные типы аргументов, то применимость метода определяется, как указано в §18.5.1. -
Если
m— обобщенный метод, и вызов метода предоставляет явные типы аргументов, то пусть R1 ... Rp (p ≥ 1) — параметры типаm, Bl — объявленное ограничение Rl (1 ≤ l ≤ p), и U1 ... Up — явные типы аргументов, заданные в вызове метода. Тогдаmявляется применимым по слабому вызову, если оба следующих условия верны:-
Для 1 ≤ i ≤ n, если
eiотносится к применимости (§15.12.2.2), тоeiсовместим в контексте слабого вызова с Fi[R1:=U1, ..., Rp:=Up]. -
Для 1 ≤ l ≤ p, Ul
<:Bl[R1:=U1, ..., Rp:=Up].
-
-
Если
mне является обобщенным методом, тоmявляется применимым по слабому вызову, если для 1 ≤ i ≤ n, либоeiсовместим в контексте слабого вызова с Fi, либоeiне относится к применимости.
Если метод, применимый по слабому вызову, не найден, поиск применимых методов продолжается с третьей фазы (§15.12.2.4).
В противном случае выбирается наиболее специфичный метод (§15.12.2.5) среди методов, применимых по слабому вызову.
Если метод с переменным числом аргументов имеет формальные типы параметров F1, ..., Fn-1, Fn[], то i-й тип параметра с переменным числом аргументов метода определяется следующим образом:
-
Для i ≤ n-1, i-й тип параметра с переменным числом аргументов — это Fi.
-
Для i ≥ n, i-й тип параметра с переменным числом аргументов — это Fn.
Пусть m — потенциально применимый метод (§15.12.2.1) с переменным числом аргументов, T1, ..., Tk — первые k типов параметров с переменным числом аргументов m, а e1, ..., ek — выражения фактических аргументов вызова метода. Тогда:
-
Если
m— это метод с дженериками, и вызов метода не предоставляет явные типы аргументов, то применимость метода определяется, как указано в §18.5.1. -
Если
m— это метод с дженериками, и вызов метода предоставляет явные типы аргументов, то пусть R1 ... Rp (p ≥ 1) — типы параметровm, Bl — объявленная граница Rl (1 ≤ l ≤ p), а U1 ... Up — явные типы аргументов, заданные в вызове метода. Тогдаmявляется применимым с переменным числом аргументов, если:-
Для 1 ≤ i ≤ k, если
eiотносится к применимости (§15.12.2.2), тоeiсовместим в контексте свободного вызова с Ti[R1:=U1, ..., Rp:=Up]. -
Для 1 ≤ l ≤ p, Ul
<:Bl[R1:=U1, ..., Rp:=Up].
-
-
Если
mне является методом с дженериками, тоmприменим с переменным числом аргументов, если для 1 ≤ i ≤ k, либоeiсовместим в контексте свободного вызова с Ti, либоeiне относится к применимости.
Если применимый метод с переменным числом аргументов не найден, возникает ошибка времени компиляции.
В противном случае, выбирается наиболее конкретный метод (§15.12.2.5) среди методов, применимых с переменным числом аргументов.
Если более одного метода-члена одновременно доступны и применимы к вызову метода, необходимо выбрать один, чтобы определить дескриптор для диспетчеризации метода во время выполнения. Язык программирования Java использует правило выбора наиболее специфичного метода.
Неформальная интуиция заключается в том, что один метод более специфичен, чем другой, если любой вызов, обрабатываемый первым методом, может быть передан другому методу без ошибки во время компиляции. В таких случаях, как явное типизированное выражение лямбда-выражения в качестве аргумента (§15.27.1) или вызов с переменным числом аргументов (§15.12.2.4), допускается определенная гибкость для адаптации одной сигнатуры к другой.
Один применимый метод m1 является более специфичным, чем другой применимый метод m2, для вызова с выражениями аргументов e1, ..., ek, если выполняется любое из следующих условий:
-
m2является обобщенным, иm1определяется как более специфичный, чемm2для выражений аргументовe1, ...,ekв соответствии с §18.5.4. -
m2не является обобщенным, иm1иm2применимы по строгому или свободному вызову, и гдеm1имеет формальные типы параметров S1, ..., Sn, аm2имеет формальные типы параметров T1, ..., Tn, тип Si является более специфичным, чем Ti, для аргументаeiдля всех i (1 ≤ i ≤ n, n = k). -
m2не является обобщенным, иm1иm2применимы по вызову с переменным числом аргументов, и где первые k типов параметров с переменным числом аргументовm1равны S1, ..., Sk, а первые k типов параметров с переменным числом аргументовm2равны T1, ..., Tk, тип Si является более специфичным, чем Ti, для аргументаeiдля всех i (1 ≤ i ≤ k). Кроме того, еслиm2имеет k+1 параметр, то k+1'ый тип параметра с переменным числом аргументовm1является подтипом k+1'го типа параметра с переменным числом аргументовm2.
Вышеперечисленные условия являются единственными условиями, при которых один метод может быть более специфичным, чем другой.
Тип S является более специфичным, чем тип T для любого выражения, если S <: T (§4.10).
Тип функционального интерфейса S является более специфичным, чем тип функционального интерфейса T для выражения e, если T не является подтипом S и выполняется одно из следующих условий (где U1 ... Uk и R1 являются типами параметров и типом возврата типа функции захвата S, а V1 ... Vk и R2 являются типами параметров и типом возврата типа функции T):
-
Если
eявляется явным типизированным лямбда-выражением (§15.27.1), то выполняется одно из следующих условий:-
R2 является
void. -
R1
<:R2. -
R1 и R2 являются типами функциональных интерфейсов, и существует по крайней мере одно выражение результата, и R1 является более специфичным, чем R2 для каждого выражения результата
e.(Выражение результата лямбда-выражения с блочным телом определено в §15.27.2; выражение результата лямбда-выражения с выражением-телом просто представляет собой само тело.)
-
R1 является примитивным типом, а R2 является ссылочным типом, и существует по крайней мере одно выражение результата, и каждое выражение результата
eявляется автономным выражением (§15.2) примитивного типа. -
R1 является ссылочным типом, а R2 является примитивным типом, и существует по крайней мере одно выражение результата, и каждое выражение результата
eявляется либо автономным выражением ссылочного типа, либо поливыражением.
-
-
Если
eявляется выражением точного обращения к методу (§15.13.1), то i) для всех i (1 ≤ i ≤ k), Ui совпадает с Vi, и ii) выполняется одно из следующих условий:-
R2 является
void. -
R1
<:R2. -
R1 является примитивным типом, R2 является ссылочным типом, и декларация метода обращения во время компиляции имеет тип возврата, который является примитивным типом.
-
R1 является ссылочным типом, R2 является примитивным типом, и декларация метода обращения во время компиляции имеет тип возврата, который является ссылочным типом.
-
-
Если
eявляется выражением в скобках, то одно из этих условий применяется рекурсивно к содержащемуся выражению. -
Если
eявляется условным выражением, то для каждого из второго и третьего операндов одно из этих условий применяется рекурсивно.
Метод m1 является строго более специфичным, чем другой метод m2 тогда и только тогда, когда m1 является более специфичным, чем m2, и m2 не является более специфичным, чем m1.
Метод называется максимально специфичным для вызова метода, если он доступен и применим, и нет другого метода, который применим и доступен, который является строго более специфичным.
Если существует ровно один максимально специфичный метод, то этот метод фактически является наиболее специфичным методом; он обязательно более специфичен, чем любой другой доступный метод, который применим. Затем он подвергается дополнительной проверке во время компиляции, как указано в §15.12.3.
Возможен случай, когда ни один метод не является наиболее специфичным, поскольку существует два или более методов, которые являются максимально специфичными. В этом случае:
-
Если все максимально специфичные методы имеют эквивалентные сигнатуры переопределения (§8.4.2), то:
-
Если ровно один из максимально специфичных методов является конкретным (то есть не
abstractили по умолчанию), он является наиболее специфичным методом. -
В противном случае, если все максимально специфичные методы являются
abstractили по умолчанию, и сигнатуры всех максимально специфичных методов имеют одинаковую стёртую форму (§4.6), то наиболее специфичный метод выбирается произвольно среди подмножества максимально специфичных методов, имеющих наиболее специфичный тип возвращаемого значения.В этом случае, наиболее специфичный метод считается
abstract. Также, наиболее специфичный метод считается выбрасывающим проверочную ошибку, если и только если эта ошибка или её стёртая форма объявлена вthrowsразделах каждого из максимально специфичных методов.
-
-
В противном случае, вызов метода является неоднозначным, и происходит ошибка компиляции.
Тип вызова наиболее специфичного, доступного и применимого метода является типом метода (§8.2), выражающим целевые типы аргументов вызова, результат (тип возвращаемого значения или void) вызова и типы исключений вызова. Он определяется следующим образом:
-
Если выбранный метод является обобщённым, и вызов метода не предоставляет явные типы аргументов, тип вызова определяется, как указано в §18.5.2.
-
Если выбранный метод является обобщённым и вызов метода предоставляет явные типы аргументов, пусть Pi — параметры типа метода, а Ti — явные типы аргументов, предоставленные для вызова метода (1 ≤ i ≤ p). Тогда:
-
Если для применимости метода была необходима неявная приведение типов, то типы параметров типа вызова получаются путём применения подстановки
[P1:=T1, ..., Pp:=Tp]к типам параметров типа метода, а тип возвращаемого значения и типы исключений типа вызова даются стёртой формой типа возвращаемого значения и типов исключений типа метода. -
Если для применимости метода не было необходимости в неявном приведении типов, то тип вызова получается путём применения подстановки
[P1:=T1, ..., Pp:=Tp]к типу метода.
-
-
Если выбранный метод не является обобщённым, то:
-
Если для применимости метода была необходима неявная приведение типов, типы параметров типа вызова — это типы параметров типа метода, а тип возвращаемого значения и типы исключений даются стёртыми формами типа возвращаемого значения и типов исключений типа метода.
-
В противном случае, если выбранный метод является
getClassметодом классаObject(§4.3.2), тип вызова такой же, как тип метода, за исключением того, что тип возвращаемого значения — этоClass<?extends|T|>, где T — тип, который был искан, как определено в §15.12.1, а |T| обозначает стёртую форму T (§4.6). -
В противном случае, тип вызова такой же, как тип метода.
-
Если существует наиболее конкретное объявление метода для вызова метода, оно называется объявлением во время компиляции для вызова метода.
Ошибка времени компиляции, если аргумент вызова метода несовместим с его целевым типом, как полученно из типа вызова объявления во время компиляции.
Если объявление во время компиляции применимо с помощью вызова с переменным числом аргументов, то, где последний тип формального параметра типа вызова метода — Fn[], ошибка времени компиляции, если тип, который является стиранием Fn, недоступен в точке вызова.
Если объявление во время компиляции void, то вызов метода должен быть выражением верхнего уровня (то есть, Выражение в операторе выражения или в части ForInit или ForUpdate оператора for), в противном случае возникает ошибка времени компиляции. Такой вызов метода не производит значение и поэтому должен использоваться только в ситуации, когда значение не требуется.
Кроме того, подходит ли объявление во время компиляции, может зависеть от формы выражения вызова метода перед левой скобкой, как указано ниже:
-
Если форма — ИмяМетода — то есть просто Идентификатор — и объявление во время компиляции — метод экземпляра, то:
-
Ошибка времени компиляции, если вызов метода происходит в статическом контексте (§8.1.3).
-
В противном случае пусть C — непосредственно окружающий класс, членом которого является объявление во время компиляции. Если вызов метода не находится непосредственно внутри C или вложенного класса C, возникает ошибка времени компиляции.
-
-
Если форма — ИмяТипа
.[TypeArguments] Идентификатор, то объявление во время компиляции должно бытьstatic, в противном случае возникает ошибка времени компиляции. -
Если форма — ИмяВыражения
.[TypeArguments] Идентификатор или Основное.[TypeArguments] Идентификатор, то объявление во время компиляции не должно быть методомstatic, объявленным в интерфейсе, в противном случае возникает ошибка времени компиляции. -
Если форма —
super.[TypeArguments] Идентификатор, то:-
Ошибка времени компиляции, если объявление во время компиляции
abstract. -
Ошибка времени компиляции, если вызов метода происходит в статическом контексте.
-
-
Если форма — ИмяТипа
.super.[TypeArguments] Идентификатор, то:-
Ошибка времени компиляции, если объявление во время компиляции
abstract. -
Ошибка времени компиляции, если вызов метода происходит в статическом контексте.
-
Если ИмяТипа обозначает класс C, то если вызов метода не находится непосредственно внутри C или вложенного класса C, возникает ошибка времени компиляции.
-
Если ИмяТипа обозначает интерфейс, пусть T — объявление типа, непосредственно окружающего вызов метода. Ошибка времени компиляции, если существует метод, отличный от объявления во время компиляции, который переопределяет (§9.4.1) объявление во время компиляции из непосредственного суперкласса или непосредственного суперинтерфейса T.
В случае, когда суперинтерфейс переопределяет метод, объявленный в интерфейсе предка, это правило предотвращает то, что дочерний интерфейс «пропускает» переопределение, просто добавив предка в свой список непосредственных суперинтерфейсов. Правильный способ доступа к функциональности предка — через непосредственный суперинтерфейс, и только если этот интерфейс выбирает раскрытие желаемого поведения. (В качестве альтернативы разработчик может свободно определить свой дополнительный суперинтерфейс, который раскрывает желаемое поведение с помощью
superвызова метода.)
-
Типы параметров во время компиляции и результат во время компиляции определяются следующим образом:
-
Если объявление во время компиляции для вызова метода — не метод полиморфный по сигнатуре, то типы параметров во время компиляции — типы формальных параметров объявления во время компиляции, а результат во время компиляции — результат, выбранный для объявления во время компиляции (§15.12.2.6).
-
Если объявление во время компиляции для вызова метода — метод полиморфный по сигнатуре, то:
-
Типы параметров во время компиляции — статические типы выражений фактических аргументов. Выражение аргумента, которое является литералом null
null(§3.10.7), рассматривается как имеющий статический типVoid. -
Результат во время компиляции определяется следующим образом:
-
Если выражение вызова метода является оператором выражения, результат во время компиляции —
void. -
В противном случае, если выражение вызова метода является операндом выражения приведения типов (§15.16), результат во время компиляции — стирание типа выражения приведения типов (§4.6).
-
В противном случае результат во время компиляции — объявленный тип возврата полиморфного метода по сигнатуре,
Object.
-
-
Метод является полиморфным по сигнатуре, если все перечисленные ниже условия выполняются:
-
Он объявлен в
java.lang.invoke.MethodHandleклассе. -
Он принимает один параметр с переменным числом аргументов (§8.4.1) с объявленным типом
Object[]. -
Он имеет тип возврата
Object. -
Он
native.
В Java SE 8 единственные методы полиморфные по сигнатуре — invoke и invokeExact методы класса java.lang.invoke.MethodHandle.
Следующая информация во время компиляции затем связывается с вызовом метода для использования во время выполнения:
-
Имя метода.
-
Квалифицирующий тип вызова метода (§13.1).
-
Количество параметров и типы параметров во время компиляции в порядке их следования.
-
Результат во время компиляции или
void. -
Режим вызова, вычисленный следующим образом:
-
Если квалифицирующий тип объявления метода — класс, то:
-
Если объявление во время компиляции имеет модификатор
static, то режим вызова —static. -
В противном случае, если объявление во время компиляции имеет модификатор
private, то режим вызова —nonvirtual. -
В противном случае, если часть вызова метода перед левой скобкой имеет вид
super.Идентификатор или вид ИмяТипа.super.Идентификатор, то режим вызова —super. -
В противном случае режим вызова —
virtual.
-
-
Если квалифицирующий тип вызова метода — интерфейс, то режим вызова —
interface.
-
Если результат вызова типа компиляционного объявления не является void, то тип выражения вызова метода получается путем применения преобразования захвата (§5.1.10) к типу возвращаемого значения вызова типа компиляционного объявления.
Во время выполнения вызова метода требуется пять шагов. Во-первых, может быть вычислена ссылка на целевой объект. Во-вторых, вычисляются выражения аргументов. В-третьих, проверяется доступность вызываемого метода. В-четвертых, находится фактический код выполняемого метода. В-пятых, создается новая фреймворк активации, при необходимости выполняется синхронизация, и управление передается коду метода.
Необходимо рассмотреть шесть случаев, в зависимости от формы вызова метода:
-
Если форма — ИмяМетода (то есть только Идентификатор), то:
-
Если режим вызова —
static, то ссылка на целевой объект отсутствует. -
В противном случае, пусть T — объявление окружающего типа, членом которого является метод, и пусть n — целое число такое, что T — n-ое лексически окружающее объявление типа класса, объявление которого содержит вызов метода непосредственно. Ссылка на целевой объект — n-ый лексически окружающий экземпляр
this.Ошибка во время компиляции, если n-ый лексически окружающий экземпляр
thisне существует.
-
-
Если форма — ИмяТипа
.[АргументыТипов] Идентификатор, то ссылка на целевой объект отсутствует. -
Если форма — ИмяВыражения
.[АргументыТипов] Идентификатор, то:-
Если режим вызова —
static, то ссылка на целевой объект отсутствует. ИмяВыражения вычисляется, но результат затем отбрасывается. -
В противном случае, ссылка на целевой объект — значение, обозначаемое ИмяВыражения.
-
-
Если форма — ПервичноеВыражение
.[АргументыТипов] Идентификатор, то:-
Если режим вызова —
static, то ссылка на целевой объект отсутствует. Выражение ПервичноеВыражение вычисляется, но результат затем отбрасывается. -
В противном случае, выражение ПервичноеВыражение вычисляется, и результат используется в качестве ссылки на целевой объект.
В любом случае, если вычисление выражения ПервичноеВыражение завершается внезапно, ни какая часть выражения аргумента не была вычислена, и вызов метода завершается внезапно по той же причине.
-
-
Если форма —
super.[АргументыТипов] Идентификатор, то ссылка на целевой объект — значениеthis. -
Если форма — ИмяТипа
.super.[АргументыТипов] Идентификатор, то если ИмяТипа обозначает класс, то ссылка на целевой объект — значение ИмяТипа.this; в противном случае, ссылка на целевой объект — значениеthis.
Пример 15.12.4.1-1. Ссылки на целевые объекты и методы static
Когда ссылка на целевой объект вычисляется и затем отбрасывается, потому что режим вызова — static, ссылка не проверяется на то, является ли она null:
class Test1 {
static void mountain() {
System.out.println("Monadnock");
}
static Test1 favorite(){
System.out.print("Mount ");
return null;
}
public static void main(String[] args) {
favorite().mountain();
}
}
что выводит:
Mount Monadnock
Здесь favorite() возвращает null, но исключение NullPointerException не выбрасывается.
Пример 15.12.4.1-2. Порядок вычисления при вызове метода
В рамках вызова метода экземпляра (§15.12) есть выражение, которое обозначает вызываемый объект. Это выражение полностью вычисляется до вычисления какой-либо части выражения аргумента вызова метода.
Например, в:
class Test2 {
public static void main(String[] args) {
String s = "one";
if (s.startsWith(s = "two"))
System.out.println("oops");
}
}
вхождение s перед ".startsWith" вычисляется сначала, перед выражением аргумента s = "two". Следовательно, ссылка на строку "one" запоминается как ссылка на целевой объект до того, как локальная переменная s изменится на ссылку на строку "two". В результате, метод startsWith вызывается для целевого объекта "one" с аргументом "two", поэтому результатом вызова является false, так как строка "one" не начинается с "two". Отсюда следует, что программа не выводит "oops".
Процесс вычисления списка аргументов отличается в зависимости от того, является ли вызываемый метод методом с фиксированной арностью или методом с переменной арностью (§8.4.1).
Если вызываемый метод — метод с переменной арностью m, у него обязательно есть n > 0 формальных параметров. Последний формальный параметр m обязательно имеет тип T[] для некоторого T, и m обязательно вызывается с k ≥ 0 выражениями аргументов.
Если m вызывается с k ≠ n выражениями аргументов или если m вызывается с k = n выражениями аргументов и тип k-го выражения аргумента не совместим по присваиванию с T[], то список аргументов (e1, ..., en-1, en, ..., ek) вычисляется так, как будто он был записан как (e1, ..., en-1, new |T[]| { en, ..., ek }), где |T[]| обозначает стирание (§4.6) T[].
Предыдущий абзац составлен для обработки взаимодействия параметризованных типов и массивов типов, которые происходят в виртуальной машине Java с стиранием дженериков. Иными словами, если тип элемента T параметра массива с переменной арностью не является подлежащим реификации, например List, то с выражением создания массива (§15.10) необходимо соблюдать особую осторожность, потому что тип элемента создаваемого массива должен быть подлежащим реификации. С помощью стирания типа массива конечного выражения в списке аргументов мы гарантируем получение подлежащего реификации типа элемента. Затем, поскольку выражение создания массива появляется в контексте вызова (§5.3), возможно неявное преобразование от типа массива с типом элемента, подлежащим реификации, к типу массива с типом элемента, не подлежащим реификации, а именно, к типу параметра с переменной арностью. Компилятор Java должен выдавать предупреждение о неявном преобразовании на этом этапе. Реализация компилятора Java от Oracle идентифицирует это предупреждение как более информативное создание массива неявных дженериков. <String>
Выражения аргументов (возможно, переписанные, как описано выше) теперь вычисляются, чтобы получить значения аргументов. Каждое значение аргумента соответствует ровно одному из n формальных параметров метода.
Выражения аргументов, если таковые имеются, вычисляются в порядке следования слева направо. Если вычисление любого выражения аргумента завершается внезапно, то ни одна часть выражения аргумента справа от него не была вычислена, и вызов метода завершается внезапно по той же причине. Результат вычисления j-го выражения аргумента — j-ое значение аргумента, для 1 ≤ j ≤ n. Вычисление продолжается, используя значения аргументов, как описано ниже.
Пусть C — класс, содержащий вызов метода, T — квалифицирующий тип вызова метода (§13.1), а m — имя метода, определённое во время компиляции (§15.12.3).
Реализация языка программирования Java должна гарантировать, что метод m по-прежнему существует в типе T как часть процесса связывания. Если это не так, возникает NoSuchMethodError (подкласс IncompatibleClassChangeError).
Если режим вызова — interface, то реализация должна также проверить, что целевой тип ссылки по-прежнему реализует указанный интерфейс. Если целевой тип ссылки не реализует интерфейс, возникает IncompatibleClassChangeError.
Реализация также должна гарантировать во время связывания, что тип T и метод m доступны:
-
Для типа T:
-
Если T находится в том же пакете, что и C, то T доступен.
-
Если T находится в другом пакете по сравнению с C, и T является
public, то T доступен. -
Если T находится в другом пакете по сравнению с C, и T является
protected, то T доступен, если и только если C является подклассом T.
-
-
Для метода
m:-
Если
mимеет доступpublic, тоmдоступен. (Все члены интерфейсов имеют доступpublic(§9.2).) -
Если
mимеет доступprotected, тоmдоступен, если и только если T находится в том же пакете, что и C, или C является T или подклассом T. -
Если
mимеет пакетный доступ, тоmдоступен, если и только если T находится в том же пакете, что и C. -
Если
mимеет доступprivate, тоmдоступен, если и только если C равен T, C содержит T, T содержит C или T и C оба содержатся третьим классом.
-
Если T или m недоступны, возникает IllegalAccessError (§12.3).
Стратегия поиска метода зависит от режима вызова.
Если режим вызова равен static, целевая ссылка не требуется, и переопределение запрещено. Вызывается метод m класса T.
В противном случае, вызывается метод экземпляра, и имеется целевая ссылка. Если целевая ссылка равна null, в этот момент выбрасывается исключение NullPointerException. В противном случае, целевая ссылка относится к целевому объекту и будет использоваться как значение ключевого слова this в вызываемом методе. Затем рассматриваются остальные четыре возможности для режима вызова.
Если режим вызова равен nonvirtual, переопределение запрещено. Вызывается метод m класса T.
В противном случае, если режим вызова равен virtual, и T и m совместно указывают на полиморфный метод с определенной сигнатурой (§15.12.3), то целевой объект является экземпляром java.lang.invoke.MethodHandle. Рукоятка метода инкапсулирует тип, который сопоставляется с информацией, связанной с вызовом метода во время компиляции (§15.12.3). Подробности этого сопоставления приведены в Спецификации виртуальной машины Java, издание Java SE 8 и API платформы Java SE. Если сопоставление успешно, целевой метод, инкапсулированный рукояткой метода, вызывается напрямую и немедленно, и процедура в §15.12.4.5 не выполняется.
В противном случае, режим вызова равен interface, virtual или super, и переопределение может произойти. Используется динамический поиск метода. Процесс динамического поиска начинается с класса S, определяемого следующим образом:
-
Если режим вызова равен
interfaceилиvirtual, то S изначально является фактическим классом во время выполнения R целевого объекта.Это верно даже если целевой объект является массивом. (Обратите внимание, что для режима вызова
interface, R обязательно реализует T; для режима вызоваvirtual, R обязательно является T или подклассом T.) -
Если режим вызова равен
super, то S изначально является квалифицируемым типом (§13.1) вызова метода.
Динамический поиск метода использует следующую процедуру для поиска в классе S, а затем в суперклассах и суперинтерфейсах класса S по мере необходимости, для метода m.
Пусть X — тип во время компиляции целевой ссылки вызова метода. Тогда:
-
Если класс S содержит объявление метода с именем
mс тем же описателем (то же количество параметров, те же типы параметров и тот же возвращаемый тип), что и требуется для вызова метода, как определено во время компиляции (§15.12.3), то:-
Если режим вызова равен
superилиinterface, то это вызываемый метод, и процедура завершается. -
Если режим вызова равен
virtual, и объявление в S переопределяет X.m(§8.4.8.1), то объявленный в S метод является вызываемым методом, и процедура завершается.
-
-
В противном случае, если S имеет суперкласс, процедура поиска шагов 1 и 2 выполняется рекурсивно с использованием непосредственного суперкласса S вместо S; вызываемый метод, если таковой имеется, является результатом рекурсивного вызова этой процедуры поиска.
-
Если ни один метод не найден предыдущими двумя шагами, то суперинтерфейсы S проверяются на наличие подходящего метода.
Рассматривается набор кандидатных методов со следующими свойствами: i) каждый метод объявлен в (непосредственном или косвенном) суперинтерфейсе S; ii) каждый метод имеет имя и описатель, необходимые для вызова метода; iii) каждый метод не является
static; iv) для каждого метода, где объявляющий интерфейс метода равен I, нет другого метода, удовлетворяющего (i) - (iii), который объявлен в подинтерфейсе I.Если этот набор содержит метод по умолчанию, то один такой метод является вызываемым методом. В противном случае, метод
abstractв наборе выбирается в качестве вызываемого метода.
Динамический поиск метода может вызвать следующие ошибки:
-
Если вызываемый метод равен
abstract, выбрасывается исключениеAbstractMethodError. -
Если вызываемый метод по умолчанию, и более чем один метод по умолчанию появляется в наборе кандидатов на шаге 3 выше, выбрасывается исключение
IncompatibleClassChangeError. -
Если режим вызова равен
interface, и выбранный метод не являетсяpublic, выбрасывается исключениеIllegalAccessError.
Вышеприведенная процедура (если она завершается без ошибок) найдет доступный, не-abstract метод для вызова, при условии, что все классы и интерфейсы в программе были корректно скомпилированы. Однако, если это не так, могут возникнуть различные ошибки, как указано выше; дополнительные сведения о поведении виртуальной машины Java в этих обстоятельствах приведены в Спецификации виртуальной машины Java, издание Java SE 8.
Процесс динамического поиска, хотя и описан здесь явно, часто реализуется неявно, например, как побочный эффект построения и использования таблиц диспетчеризации методов по классу или построения других структур по классу, используемых для эффективной диспетчеризации.
Пример 15.12.4.4-1. Переопределение и вызов метода
class Point {
final int EDGE = 20;
int x, y;
void move(int dx, int dy) {
x += dx; y += dy;
if (Math.abs(x) >= EDGE || Math.abs(y) >= EDGE)
clear();
}
void clear() {
System.out.println("\tPoint clear");
x = 0; y = 0;
}
}
class ColoredPoint extends Point {
int color;
void clear() {
System.out.println("\tColoredPoint clear");
super.clear();
color = 0;
}
}
Здесь подкласс ColoredPoint расширяет абстракцию clear, определенную в его суперклассе Point. Он делает это, переопределяя метод clear своим собственным методом, который вызывает метод clear своего суперкласса с помощью формы super.clear().
Этот метод затем вызывается всякий раз, когда целевой объект вызова clear является объектом класса ColoredPoint. Даже метод move в Point вызывает метод clear класса ColoredPoint, когда класс this равен ColoredPoint, как показано выводом этой тестовой программы:
class Test1 {
public static void main(String[] args) {
Point p = new Point();
System.out.println("p.move(20,20):");
p.move(20, 20);
ColoredPoint cp = new ColoredPoint();
System.out.println("cp.move(20,20):");
cp.move(20, 20);
p = new ColoredPoint();
System.out.println("p.move(20,20), p colored:");
p.move(20, 20);
}
}
что равно:
p.move(20,20):
Point clear
cp.move(20,20):
ColoredPoint clear
Point clear
p.move(20,20), p colored:
ColoredPoint clear
Point clear
Переопределение иногда называется "поздняя привязка самоссылок"; в этом примере это означает, что ссылка на clear в теле Point.move (которая фактически является синтаксическим сокращением для this.clear) вызывает метод, выбранный "поздно" (во время выполнения, на основе класса во время выполнения объекта, на который ссылается this), а не метод, выбранный "рано" (во время компиляции, только на основе типа this). Это предоставляет программисту мощный способ расширения абстракций и является ключевой идеей в объектно-ориентированном программировании.
Пример 15.12.4.4-2. Вызов метода с использованием super
Переопределенный метод экземпляра суперкласса можно получить, используя ключевое слово super для доступа к членам непосредственного суперкласса, минуя любое переопределение в классе, содержащем вызов метода.
При доступе к переменной экземпляра, super означает то же, что и приведение типа this (§15.11.2), но это эквивалентность не выполняется для вызова метода. Это демонстрируется на примере:
class T1 {
String s() { return "1"; }
}
class T2 extends T1 {
String s() { return "2"; }
}
class T3 extends T2 {
String s() { return "3"; }
void test() {
System.out.println("s()=\t\t" + s());
System.out.println("super.s()=\t" + super.s());
System.out.println("((T2)this).s()=\t" + ((T2)this).s());
System.out.println("((T1)this).s()=\t" + ((T1)this).s());
}
}
class Test2 {
public static void main(String[] args) {
T3 t3 = new T3();
t3.test();
}
}
что дает вывод:
s()= 3 super.s()= 2 ((T2)this).s()= 3 ((T1)this).s()= 3
Приведения к типам T1 и T2 не изменяют вызываемый метод, поскольку метод экземпляра, который будет вызван, выбирается в соответствии с классом во время выполнения объекта, на который ссылается this. Приведение не меняет класс объекта; оно только проверяет, что класс совместим с указанным типом.
Метод m в некотором классе S определён для вызова.
Теперь создаётся новый фрейм активации, содержащий целевую ссылку (если есть) и значения аргументов (если есть), а также достаточно места для локальных переменных и стека для вызываемого метода и любой другой служебной информации, которая может потребоваться реализации (указатель стека, счётчик команд, ссылка на предыдущий фрейм активации и т. п.). Если памяти недостаточно для создания такого фрейма активации, генерируется исключение StackOverflowError.
Созданный фрейм активации становится текущим фреймом активации. Это приводит к присвоению значений аргументов соответствующим только что созданным параметрам метода и к доступности целевой ссылки как this, если она есть. Перед присвоением каждого значения аргумента соответствующей переменной параметра, оно подвергается преобразованию вызова (§5.3), что включает в себя любое необходимое преобразование набора значений (§5.1.13).
Если стирание (§4.6) типа вызываемого метода отличается от стирания типа объявления метода вызова на этапе компиляции (§15.12.3), то если любое из значений аргументов является объектом, который не является экземпляром подкласса или подинтерфейса стирания соответствующего типа формального параметра в объявлении вызова метода на этапе компиляции, то генерируется исключение ClassCastException.
Если метод m является native методом, но необходимый нативный, зависящий от реализации, двоичный код не загружен или по другим причинам не может быть динамически связан, то генерируется исключение UnsatisfiedLinkError.
Если метод m не является synchronized, управление передаётся в тело вызываемого метода m.
Если метод m является synchronized, то перед передачей управления необходимо заблокировать объект. Дальнейшее выполнение невозможно до тех пор, пока текущий поток не получит блокировку. Если есть целевая ссылка, то должен быть заблокирован целевой объект; в противном случае необходимо заблокировать объект Class для класса S, класса метода m. После этого управление передаётся в тело вызываемого метода m. Объект автоматически разблокируется по завершении выполнения тела метода, будь то нормальное завершение или прерывание. Поведение блокировки и разблокировки точно такое же, как если бы тело метода было вложено в оператор synchronized (§14.19).
Пример 15.12.4.5-1. Подпись вызываемого метода отличается от подписи метода на этапе компиляции
Рассмотрим объявления:
abstract class C<T> {
abstract T id(T x);
}
class D extends C<String> {
String id(String x) { return x; }
}
Теперь, при вызове:
C c = new D(); c.id(new Object()); // fails with a ClassCastException
Стирание фактического вызываемого метода, D.id(), отличается по своей подписи от стирания объявления метода на этапе компиляции, C.id(). Первый принимает аргумент типа String, а второй — типа Object. Вызов завершается с исключением ClassCastException до выполнения тела метода.
Такие ситуации могут возникнуть только если программа порождает предупреждение, не проверяемое на этапе компиляции (§4.8, §5.1.9, §5.5.2, §8.4.1, §8.4.8.3, §8.4.8.4, §9.4.1.2, §15.12.4.2).
Реализации могут обеспечивать эти семантики, создавая мосты-методы. В данном примере в классе D будет создан следующий мост-метод:
Object id(Object x) { return id((String) x); }
Именно этот метод будет фактически вызван виртуальной машиной Java в ответ на вызов c.id(new
Object()), показанный выше, и он выполнит преобразование и завершится ошибкой, как требуется.
Выражение ссылки на метод используется для указания на вызов метода без фактического выполнения этого вызова. Некоторые формы выражений ссылок на методы также позволяют создавать экземпляры классов (§15.9) или создавать массивы (§15.10), которые обрабатываются как вызов метода.
:: [TypeArguments] Identifier ReferenceType
:: [TypeArguments] Identifier Primary
:: [TypeArguments] Identifier super :: [TypeArguments] Identifier TypeName
. super :: [TypeArguments] Identifier ClassType
:: [TypeArguments] new ArrayType
:: new Если TypeArguments присутствует справа от ::, то это ошибка времени компиляции, если какой-либо из аргументов типа является универсальным (§4.5.1).
Если выражение ссылки на метод имеет вид ExpressionName :: [TypeArguments] Identifier или Primary :: [TypeArguments] Identifier, то это ошибка времени компиляции, если тип ExpressionName или Primary не является типом ссылки.
Если выражение ссылки на метод имеет вид super :: [TypeArguments] Identifier, пусть T будет объявлением типа, непосредственно охватывающим выражение ссылки на метод. Это ошибка времени компиляции, если T является классом Object или T является интерфейсом.
Если выражение ссылки на метод имеет вид TypeName . super :: [TypeArguments] Identifier, то:
-
Если TypeName обозначает класс C, то это ошибка времени компиляции, если C не является лексически охватывающим классом текущего класса или если C является классом
Object. -
Если TypeName обозначает интерфейс I, пусть T будет объявлением типа, непосредственно охватывающим выражение ссылки на метод. Это ошибка времени компиляции, если I не является непосредственным суперинтерфейсом T или если существует другой непосредственный суперкласс или непосредственный суперинтерфейс T, J, такой что J является подтипом I.
-
Если TypeName обозначает переменную типа, то возникает ошибка времени компиляции.
Если выражение ссылки на метод имеет вид super :: [TypeArguments] Identifier или TypeName . super :: [TypeArguments] Identifier, то это ошибка времени компиляции, если выражение встречается в статическом контексте.
Если выражение ссылки на метод имеет вид ClassType :: [TypeArguments] new, то:
-
ClassType должен обозначать доступный класс, не являющийся
abstract, и не являющийся типом перечисления, в противном случае происходит ошибка времени компиляции. -
Если ClassType обозначает параметризованный тип (§4.5), то это ошибка времени компиляции, если какие-либо из его аргументов типа являются универсальными.
-
Если ClassType обозначает тип-сырец (§4.8), то это ошибка времени компиляции, если TypeArguments присутствует после
::.
Если выражение ссылки на метод имеет вид ArrayType :: new, то ArrayType должен обозначать тип, который может быть реализован (§4.7), в противном случае происходит ошибка времени компиляции.
Целевая ссылка экземпляра метода (§15.12.4.1) может быть предоставлена выражением ссылки на метод, используя ExpressionName, Primary или super, или она может быть предоставлена позже, когда метод вызывается. Экземпляр, непосредственно охватывающий экземпляр нового внутреннего класса (§15.9.2), предоставляется лексически охватывающим экземпляром this (§8.1.3).
Когда более чем один член-метод типа имеет одинаковое имя или когда класс имеет более одного конструктора, соответствующий метод или конструктор выбирается на основе типа функционального интерфейса, на который направлено выражение, как указано в §15.13.1.
Если метод или конструктор является обобщенным, соответствующие аргументы типа могут быть либо выведены, либо предоставлены явно. Аналогично, аргументы типа обобщенного типа, упоминаемого в выражении ссылки на метод, могут быть явно предоставлены или выведены.
Выражения ссылок на методы всегда являются поливыражениями (§15.2).
Ошибка времени компиляции возникает, если выражение ссылки на метод появляется в программе в другом месте, кроме контекста присваивания (§5.2), контекста вызова (§5.3) или контекста приведения типов (§5.5).
Вычисление выражения ссылки на метод создает экземпляр типа функционального интерфейса (§9.8). Вычисление ссылки на метод не вызывает выполнение соответствующего метода; вместо этого это может произойти в более позднее время, когда вызывается соответствующий метод функционального интерфейса.
Вот некоторые выражения ссылок на методы, сначала без целевой ссылки, а затем с целевой ссылкой:
String::length // instance method System::currentTimeMillis // static method List<String>::size // explicit type arguments for generic type List::size // inferred type arguments for generic type int[]::clone T::tvarMember System.out::println "abc"::length foo[x]::bar (test ? list.replaceAll(String::trim) : list) :: iterator super::toString
Вот еще несколько выражений ссылок на методы:
String::valueOf // overload resolution needed Arrays::sort // type arguments inferred from context Arrays::<String>sort // explicit type arguments
Вот некоторые выражения ссылок на методы, которые представляют отложенное создание объекта или массива:
ArrayList<String>::new // constructor for parameterized type
ArrayList::new // inferred type arguments
// for generic class
Foo::<Integer>new // explicit type arguments
// for generic constructor
Bar<String>::<Integer>new // generic class, generic constructor
Outer.Inner::new // inner class constructor
int[]::new // array creation
Невозможно указать конкретную сигнатуру для сопоставления, например, Arrays::sort(int[]). Вместо этого функциональный интерфейс предоставляет типы аргументов, которые используются как входные данные для алгоритма разрешения перегрузки (§15.12.2). Это должно удовлетворить подавляющее большинство случаев использования; когда возникает редкая необходимость в более точном управлении, можно использовать лямбда-выражение.
Использование синтаксиса аргумента типа в имени класса перед разделителем (List<String>::size) создает проблему разбора при различении между < в качестве скобки аргумента типа и < в качестве оператора «меньше чем». Теоретически, это не хуже, чем разрешить аргументы типа в выражениях приведения типов; однако разница заключается в том, что случай приведения типов возникает только при встрече маркера (; с добавлением выражений ссылок на методы начало каждого выражения потенциально является параметризованным типом.
Объявление ссылки на метод во время компиляции — это метод, на который ссылается выражение. В особых случаях объявление во время компиляции фактически не существует, а представляет собой условный метод, который отображает создание экземпляра класса или массив. Выбор объявления во время компиляции зависит от целевого типа функции, указанного выражением, точно так же, как объявление во время компиляции вызова метода зависит от аргументов вызова (§15.12).
Поиск объявления во время компиляции отражает процесс вызовов методов в §15.12.1 и §15.12.2, как показано ниже:
-
Сначала определяется тип для поиска:
-
Если выражение ссылки на метод имеет вид ИмяВыражения
::[АргументыТипов] Идентификатор или Основное::[АргументыТипов] Идентификатор, тип для поиска — это тип выражения, предшествующего токену::. -
Если выражение ссылки на метод имеет вид ТипСсылка
::[АргументыТипов] Идентификатор, тип для поиска — это результат применения преобразования захвата (§5.1.10) к ТипСсылка. -
Если выражение ссылки на метод имеет вид
super::[АргументыТипов] Идентификатор, тип для поиска — это тип суперкласса класса, чья декларация содержит ссылку на метод. -
Если выражение ссылки на метод имеет вид ИмяТипа
.super::[АргументыТипов] Идентификатор, то если ИмяТипа обозначает класс, тип для поиска — это тип суперкласса указанного класса; в противном случае ИмяТипа обозначает интерфейс, а соответствующий тип суперинтерфейса класса или интерфейса, чья декларация содержит ссылку на метод, — это тип для поиска. -
Для двух других форм (с участием
::new) метод, на который ссылаются, является условным, и нет типа для поиска.
-
-
Во-вторых, для целевого типа функции с n параметрами определяется набор потенциально применимых методов:
-
Если выражение ссылки на метод имеет вид ТипСсылка
::[АргументыТипов] Идентификатор, потенциально применимые методы — это методы-члены типа для поиска, которые имеют соответствующее имя (заданное Идентификатор), доступность, арность (n или n-1) и арность аргументов типа (полученную из [АргументыТипов]), как указано в §15.12.2.1.Рассматриваются две разные арности, n и n-1, чтобы учесть возможность того, что эта форма ссылается на либо метод-статический метод, либо метод-член.
-
Если выражение ссылки на метод имеет вид ТипКласса
::[АргументыТипов]new, набор потенциально применимых методов — это набор условных методов, соответствующих конструкторам ТипКласса.Если ТипКласса — это примитивный тип, но не является нестатическим членом примитивного типа, кандидатами на методы-члены являются те, которые указаны в §15.9.3 для выражения создания экземпляра класса, использующего
<>для исключения аргументов типа для класса.В противном случае кандидатами на методы-члены являются конструкторы ТипКласса, рассматриваемые так, как будто они являются методами с типом возврата ТипКласса. Среди этих кандидатов выбираются методы с соответствующей доступностью, арностью (n) и арностью аргументов типа (полученной из [АргументыТипов]), как указано в §15.12.2.1.
-
Если выражение ссылки на метод имеет вид ТипМассива
::new, рассматривается один условный метод. Метод имеет один параметр типаint, возвращает ТипМассива и не имеетthrowsоговорки. Если n = 1, это единственный потенциально применимый метод; в противном случае потенциально применимых методов нет. -
Для всех остальных форм потенциально применимые методы — это методы-члены типа для поиска, которые имеют соответствующее имя (указанное Идентификатор), доступность, арность (n) и арность аргументов типа (полученную из [АргументыТипов]), как указано в §15.12.2.1.
-
-
Наконец, если нет потенциально применимых методов, то объявление во время компиляции отсутствует.
В противном случае, учитывая целевой функциональный тип с параметрами типов P1, ..., Pn и набор потенциально применимых методов, объявление во время компиляции выбирается следующим образом:
-
Если выражение ссылки на метод имеет вид ТипСсылка
::[АргументыТипов] Идентификатор, то выполняются два поиска наиболее специфичного применимого метода. Каждый поиск выполняется, как указано в §15.12.2.2 до §15.12.2.5, с пояснениями ниже. Каждый поиск может вернуть метод или, в случае ошибки, как указано в §15.12.2.2 до §15.12.2.5, не вернуть результат.В первом поиске ссылка на метод обрабатывается как вызов с выражениями аргументов типов P1, ..., Pn; аргументы типов, если таковые имеются, задаются выражением ссылки на метод.
Во втором поиске, если P1, ..., Pn не пусто и P1 является подтипом ТипСсылка, то выражение ссылки на метод обрабатывается как выражение вызова метода с выражениями аргументов типов P2, ..., Pn. Если ТипСсылка является сырым типом, и существует параметризация этого типа, G
<...>, который является супертипом P1, то тип для поиска — результат применения преобразования захвата (§5.1.10) к G<...>; в противном случае тип для поиска такой же, как тип первого поиска. Опять же, аргументы типов, если таковые имеются, задаются выражением ссылки на метод.Если первый поиск возвращает метод
static, и ни один не-методstaticне применим в соответствии с §15.12.2.2, §15.12.2.3 или §15.12.2.4 во время второго поиска, то объявление во время компиляции — результат первого поиска.В противном случае, если ни один метод
staticне применим в соответствии с §15.12.2.2, §15.12.2.3 или §15.12.2.4 во время первого поиска, и второй поиск возвращает метод не-static, то объявление во время компиляции — результат второго поиска.В противном случае объявление во время компиляции отсутствует.
-
Для всех других форм выражения ссылки на метод выполняется один поиск наиболее специфичного применимого метода. Поиск выполняется, как указано в §15.12.2.2 до §15.12.2.5, с пояснениями ниже.
Ссылка на метод обрабатывается как вызов с выражениями аргументов типов P1, ..., Pn; аргументы типов, если таковые имеются, задаются выражением ссылки на метод.
Если поиск приводит к ошибке, как указано в §15.12.2.2 до §15.12.2.5, или если наиболее специфичный применимый метод является
static, то объявление во время компиляции отсутствует.В противном случае объявление во время компиляции — это наиболее специфичный применимый метод.
-
Ошибка во время компиляции, если выражение ссылки на метод имеет вид ТипСсылка :: [АргументыТипов] Идентификатор, и объявление во время компиляции является static, а ТипСсылка не является простым или квалифицированным именем (§6.2).
Ошибка во время компиляции, если выражение ссылки на метод имеет вид super :: [АргументыТипов] Идентификатор или ИмяТипа . super :: [АргументыТипов] Идентификатор, и объявление во время компиляции является abstract.
Ошибка во время компиляции, если выражение ссылки на метод имеет вид ИмяТипа . super :: [АргументыТипов] Идентификатор, и ИмяТипа обозначает интерфейс, и существует метод, отличный от объявления во время компиляции, который переопределяет (§8.4.8, §9.4.1) объявление во время компиляции из непосредственного суперкласса или непосредственного суперинтерфейса типа, объявление которого непосредственно включает выражение ссылки на метод.
Ошибка во время компиляции, если выражение ссылки на метод имеет вид ТипКласса :: [АргументыТипов] new, и произошла бы ошибка во время компиляции при определении заключительной инстанции для ТипКласса, как указано в §15.9.2 (обрабатывая выражение ссылки на метод так, как если бы это было выражение создания экземпляра класса без квалификатора).
Выражение ссылки на метод вида ТипСсылка :: [АргументыТипов] Идентификатор может интерпретироваться по-разному. Если Идентификатор относится к методу экземпляра, то неявная лямбда-функция имеет дополнительный параметр по сравнению с тем, если Идентификатор относится к методу static. Возможно, что ТипСсылка имеет оба вида применимых методов, поэтому алгоритм поиска, описанный выше, идентифицирует их отдельно, поскольку для каждого случая существуют различные типы параметров.
Пример неоднозначности:
interface Fun<T,R> { R apply(T arg); }
class C {
int size() { return 0; }
static int size(Object arg) { return 0; }
void test() {
Fun<C, Integer> f1 = C::size;
// Error: instance method size()
// or static method size(Object)?
}
}
Эту неоднозначность нельзя разрешить, предоставив применимый метод экземпляра, который является более специфичным, чем применимый метод static:
interface Fun<T,R> { R apply(T arg); }
class C {
int size() { return 0; }
static int size(Object arg) { return 0; }
int size(C arg) { return 0; }
void test() {
Fun<C, Integer> f1 = C::size;
// Error: instance method size()
// or static method size(Object)?
}
}
Поиск достаточно умен, чтобы игнорировать неоднозначности, в которых все применимые методы (из обоих поисков) являются методами экземпляра:
interface Fun<T,R> { R apply(T arg); }
class C {
int size() { return 0; }
int size(Object arg) { return 0; }
int size(C arg) { return 0; }
void test() {
Fun<C, Integer> f1 = C::size;
// OK: reference is to instance method size()
}
}
Для удобства, когда имя обобщенного типа используется для ссылки на метод экземпляра (где получатель становится первым параметром), целевой тип используется для определения аргументов типа. Это упрощает использование, например, Pair::first вместо Pair<String,Integer>::first. Аналогично, ссылка на метод, например, Pair::new, обрабатывается как «алмазное» создание экземпляра (new
Pair<>()). Поскольку «алмаз» неявный, эта форма не создаёт сырой тип; на самом деле нет способа выразить ссылку на конструктор сырого типа.
Для некоторых выражений ссылки на метод существует только одно возможное объявление во время компиляции с только одним возможным типом вызова (§15.12.2.6), независимо от целевого функционального типа. Такие выражения ссылки на метод называются точными. Выражение ссылки на метод, которое не является точным, называется неточным.
Выражение ссылки на метод, заканчивающееся Идентификатор, является точным, если оно удовлетворяет всем следующим условиям:
-
Если выражение ссылки на метод имеет вид ТипСсылка
::[АргументыТипов] Идентификатор, то ТипСсылка не обозначает примитивный тип. -
Тип для поиска содержит ровно один метод-член с именем Идентификатор, доступный классу или интерфейсу, в котором появляется выражение ссылки на метод.
-
Этот метод не является методом с переменным числом аргументов (§8.4.1).
-
Если этот метод является обобщённым (§8.4.4), то выражение ссылки на метод предоставляет АргументыТипов.
Выражение ссылки на метод вида ТипКласса :: [АргументыТипов] new является точным, если оно удовлетворяет всем следующим условиям:
-
Тип, обозначаемый ТипКласса, не является примитивным типом или является не-
staticчленом типа примитивного типа. -
Тип, обозначаемый ТипКласса, имеет ровно один конструктор, доступный для класса или интерфейса, в котором появляется выражение ссылки на метод.
-
Этот конструктор не является конструктором с переменным числом аргументов.
-
Если этот конструктор является обобщённым, то выражение ссылки на метод предоставляет АргументыТипов.
Выражение ссылки на метод вида ТипМассива :: new всегда является точным.
Выражение ссылки на метод совместимо в контексте присваивания, вызова или приведения к типу T, если T — тип функционального интерфейса (§9.8), и выражение совместимо с типом функции основного целевого типа, полученного из T.
Основной целевой тип выводится из T следующим образом:
-
Если T — тип функционального интерфейса с параметрами подстановки с подстановочными знаками, то основной целевой тип — это параметризация T без подстановочных знаков (§9.9).
-
В противном случае основной целевой тип — T.
Выражение ссылки на метод совместимо с типом функции, если оба следующих условия истинны:
-
Тип функции определяет единственное объявление во время компиляции, соответствующее ссылке.
-
Выполняется одно из следующих условий:
-
Результат типа функции —
void. -
Результат типа функции — R, и результат применения преобразования захвата (§5.1.10) к типу возвращаемого значения типа вызова (§15.12.2.6) выбранного объявления во время компиляции — R' (где R — целевой тип, который может использоваться для вывода R'), и ни R, ни R' не являются
void, и R' совместим с R в контексте присваивания.
-
В случае необходимости неявного преобразования для применимости объявления во время компиляции и если это преобразование вызовет предупреждение о неявном преобразовании в контексте вызова, возникает предупреждение о неявном преобразовании во время компиляции.
Возникает предупреждение о неявном преобразовании во время компиляции, если для совместимости типа возвращаемого значения R' с типом возвращаемого значения типа функции R было необходимо неявное преобразование, и это преобразование вызовет предупреждение о неявном преобразовании в контексте присваивания.
Если выражение ссылки на метод совместимо с типом назначения T, то тип выражения, U, — это основной целевой тип, выведенный из T.
Возникает ошибка во время компиляции, если какой-либо класс или интерфейс, упомянутый в U или в типе функции U, недоступен из класса или интерфейса, в котором появляется выражение ссылки на метод.
Для каждого не-static метода-члена m типа U, если тип функции U имеет подсигнатуру сигнатуры m, то нотационный метод, тип которого — тип функции U, считается переопределяющим m, и может возникнуть ошибка или предупреждение во время компиляции, указанные в §8.4.8.3.
Для каждого типа исключения X, указанного в пункте throws типа вызова объявления во время компиляции, X или его суперкласс должен быть упомянут в пункте throws типа функции U, в противном случае произойдёт ошибка во время компиляции.
Ключевой идеей, определяющей совместимость, является то, что ссылка на метод совместима тогда и только тогда, когда эквивалентное лямбда-выражение (x, y, z) совместимо. (Это неформальное утверждение, и есть проблемы, которые затрудняют или делают невозможным формальное определение семантики в терминах такого переписывания.) -> exp.<T1,
T2>method(x, y, z)
Эти правила совместимости обеспечивают удобный механизм преобразования из одного функционального интерфейса в другой:
Task t = () -> System.out.println("hi");
Runnable r = t::invoke;
Реализация может быть оптимизирована таким образом, что при передаче объекта, полученного из лямбда-выражения, и его преобразовании к различным типам, это не приводит к множеству уровней адаптационной логики вокруг основного тела лямбда-выражения.
В отличие от лямбда-выражения, ссылка на метод может быть совместима с обобщённым типом функции (то есть типом функции, имеющим параметры типа). Это связано с тем, что лямбда-выражению необходимо было бы иметь возможность объявлять параметры типа, а никакой синтаксис не поддерживает этого, в то время как для ссылки на метод такое объявление не требуется. Например, следующая программа является допустимой:
interface ListFactory {
<T> List<T> make();
}
ListFactory lf = ArrayList::new;
List<String> ls = lf.make();
List<Number> ln = lf.make();
Во время выполнения оценка выражения ссылки на метод аналогична оценке выражения создания экземпляра класса, поскольку нормальное завершение приводит к ссылке на объект. Оценка выражения ссылки на метод отличается от вызова самого метода.
Во-первых, если выражение ссылки на метод начинается с ExpressionName или Primary, это подвыражение оценивается. Если подвыражение вычисляется в null, возникает NullPointerException, и выражение ссылки на метод завершается аварийно. Если подвыражение завершается аварийно, выражение ссылки на метод завершается аварийно по той же причине.
Далее, либо выделяется и инициализируется новый экземпляр класса со свойствами, указанными ниже, либо ссылка на существующий экземпляр класса со свойствами, указанными ниже. Если нужно создать новый экземпляр, но недостаточно места для выделения объекта, оценка выражения ссылки на метод завершается аварийно, вызывая OutOfMemoryError.
Значением выражения ссылки на метод является ссылка на экземпляр класса со следующими свойствами:
-
Класс реализует целевой функциональный интерфейс и, если целевой тип является типом пересечения, каждый другой тип интерфейса, упомянутый в пересечении.
-
Если выражение ссылки на метод имеет тип U, для каждого не-
staticметода-членаmтипа U:Если функциональный тип U имеет подпись подписи
m, то класс объявляет метод вызова, который переопределяетm. Тело метода вызова вызывает метод, на который ссылаются, создает экземпляр класса или создает массив, как описано ниже. Если результат метода вызова неvoid, то тело возвращает результат вызова метода или создания объекта после любых необходимых преобразований присваивания (§5.2).Если стирание типа переопределяемого метода отличается по своей сигнатуре от стирания функционального типа U, то перед вызовом метода или созданием объекта тело метода вызова проверяет, что каждое значение аргумента является экземпляром подкласса или подуинтерфейса стирания соответствующего типа параметра в функциональном типе U; если нет, вызывается
ClassCastException. -
Класс не переопределяет другие методы функционального типа интерфейса или других типов интерфейсов, упомянутых выше, хотя он может переопределять методы класса
Object.
Тело метода вызова зависит от формы выражения ссылки на метод следующим образом:
-
Если форма — ExpressionName
::[TypeArguments] Identifier или Primary::[TypeArguments] Identifier, то тело метода вызова имеет эффект выражения вызова метода для объявления времени компиляции, которое является объявлением времени компиляции выражения ссылки на метод. Выполнение выражения вызова метода во время выполнения указано в разделах §15.12.4.3, §15.12.4.4 и §15.12.4.5, где:-
Режим вызова выводится из объявления времени компиляции, как указано в разделе §15.12.3.
-
Целевой ссылкой является значение ExpressionName или Primary, как определено при оценке выражения ссылки на метод.
-
Аргументами выражения вызова метода являются формальные параметры метода вызова.
-
-
Если форма — ReferenceType
::[TypeArguments] Identifier, то тело метода вызова аналогичным образом имеет эффект выражения вызова метода для объявления времени компиляции, которое является объявлением времени компиляции выражения ссылки на метод. Выполнение выражения вызова метода во время выполнения указано в разделах §15.12.4.3, §15.12.4.4 и §15.12.4.5, где:-
Режим вызова выводится из объявления времени компиляции, как указано в разделе §15.12.3.
-
Если объявление времени компиляции является методом экземпляра, то целевой ссылкой является первый формальный параметр метода вызова. В противном случае целевой ссылки нет.
-
Если объявление времени компиляции является методом экземпляра, то аргументами выражения вызова метода (если таковые имеются) являются второй и последующие формальные параметры метода вызова. В противном случае аргументами выражения вызова метода являются формальные параметры метода вызова.
-
-
Если форма —
super::[TypeArguments] Identifier или TypeName.super::[TypeArguments] Identifier, то тело метода вызова имеет эффект выражения вызова метода для объявления времени компиляции, которое является объявлением времени компиляции выражения ссылки на метод. Выполнение выражения вызова метода во время выполнения указано в разделах §15.12.4.3, §15.12.4.4 и §15.12.4.5, где:-
Режим вызова —
super. -
Если выражение ссылки на метод начинается с TypeName, которое называет класс, целевой ссылкой является значение TypeName
.thisв точке, в которой оценивается ссылка на метод. В противном случае целевой ссылкой является значениеthisв точке, в которой оценивается ссылка на метод. -
Аргументами выражения вызова метода являются формальные параметры метода вызова.
-
-
Если форма — ClassType
::[TypeArguments]new, то тело метода вызова имеет эффект выражения создания экземпляра класса формыnew[TypeArguments] ClassType(A1, ..., An), где аргументы A1, ..., An являются формальными параметрами метода вызова, и где: -
Если форма — Type
[]k::new(k ≥ 1), то тело метода вызова имеет тот же эффект, что и выражение создания массива формыnewType[size][]k-1, где size является единственным параметром метода вызова. (Обозначение[]k указывает на последовательность из k пар квадратных скобок.)
Если тело метода вызова имеет эффект выражения вызова метода, то типы параметров времени компиляции и результат вызова метода во время компиляции определяются, как указано в §15.12.3. Для определения результата во время компиляции выражение вызова метода является оператором выражения, если результат вызываемого метода является void, и Выражение оператора return, если результат вызываемого метода не является void.
Эффект этого определения, когда объявление ссылки на метод во время компиляции является полиморфным по сигнатуре, заключается в следующем:
-
Типы параметров для вызова метода — это типы соответствующих аргументов.
-
Вызов метода является либо
void, либо имеет тип возвращаемого значенияObject, в зависимости от того, является ли вызываемый метод, который включает вызов метода,voidили имеет тип возвращаемого значения.
Время вычисления выражения ссылки на метод более сложное, чем у лямбда-выражений (§15.27.4). Когда выражение ссылки на метод имеет выражение (а не тип) перед разделителем ::, это подвыражение вычисляется немедленно. Результат вычисления сохраняется до вызова метода соответствующего функционального интерфейсного типа; в этот момент результат используется в качестве целевой ссылки для вызова. Это означает, что выражение перед разделителем :: вычисляется только тогда, когда программа встречает выражение ссылки на метод, и не перевычисляется при последующих вызовах функционального интерфейсного типа.
Интересно сравнить обработку null здесь с ее обработкой во время вызова метода. При вычислении выражения вызова метода возможно, что Основной элемент, который квалифицирует вызов, вычисляется в null, но не происходит повышения NullPointerException. Это происходит, когда вызываемый метод является static (несмотря на синтаксис вызова, предполагающий метод экземпляра). Поскольку применимый метод для выражения ссылки на метод, квалифицированного Основным элементом, запрещается быть static (§15.13.1), вычисление выражения ссылки на метод проще — Основной элемент null всегда вызывает NullPointerException.
Постфиксные выражения включают использование постфиксных ++ и -- операторов. Имена не рассматриваются как первичные выражения (§15.8), но обрабатываются отдельно в грамматике, чтобы избежать определенных неоднозначностей. Они становятся взаимозаменяемыми только здесь, на уровне приоритета постфиксных выражений.
Правила вычисления имен выражений приведены в §6.5.6.
Постфиксное выражение, за которым следует оператор ++, является выражением постфиксного инкремента.
Результат постфиксного выражения должен быть переменной, преобразуемой (§5.1.8) в числовой тип, в противном случае возникает ошибка компиляции.
Тип выражения постфиксного инкремента — это тип переменной. Результат выражения постфиксного инкремента — это не переменная, а значение.
При выполнении, если вычисление операндного выражения завершается неожиданно, то выражение постфиксного инкремента также завершается неожиданно по той же причине, и инкрементация не выполняется. В противном случае к значению переменной добавляется значение 1, и сумма сохраняется обратно в переменную. Перед добавлением выполняется двоичное числовое продвижение (§5.6.2) для значения 1 и значения переменной. При необходимости сумма сужается с помощью преобразования сужения примитивного типа (§5.1.3) и/или подвергается преобразованию упаковки (§5.1.7) в тип переменной перед сохранением. Значение выражения постфиксного инкремента — это значение переменной до сохранения нового значения.
Обратите внимание, что двоичное числовое продвижение, упомянутое выше, может включать преобразование распаковки (§5.1.8) и преобразование множества значений (§5.1.13). При необходимости преобразование множества значений применяется к сумме перед сохранением ее в переменную.
Переменную, объявленную final, нельзя инкрементировать, потому что при использовании доступа к такой final переменной в качестве выражения результат — это значение, а не переменная. Поэтому она не может использоваться в качестве операнда оператора постфиксного инкремента.
Постфиксное выражение, за которым следует оператор --, является выражением постфиксного декремента.
Результат постфиксного выражения должен быть переменной, преобразуемой (§5.1.8) в числовой тип, в противном случае возникает ошибка компиляции.
Тип выражения постфиксного декремента — это тип переменной. Результат выражения постфиксного декремента — это не переменная, а значение.
При выполнении, если вычисление операндного выражения завершается неожиданно, то выражение постфиксного декремента также завершается неожиданно по той же причине, и декрементация не выполняется. В противном случае из значения переменной вычитается значение 1, и разность сохраняется обратно в переменную. Перед вычитанием выполняется двоичное числовое продвижение (§5.6.2) для значения 1 и значения переменной. При необходимости разность сужается с помощью преобразования сужения примитивного типа (§5.1.3) и/или подвергается преобразованию упаковки (§5.1.7) в тип переменной перед сохранением. Значение выражения постфиксного декремента — это значение переменной до сохранения нового значения.
Обратите внимание, что двоичное числовое продвижение, упомянутое выше, может включать преобразование распаковки (§5.1.8) и преобразование множества значений (§5.1.13). При необходимости преобразование множества значений применяется к разности перед сохранением ее в переменную.
Переменную, объявленную final, нельзя декрементировать, потому что при использовании доступа к такой final переменной в качестве выражения результат — это значение, а не переменная. Поэтому она не может использоваться в качестве операнда оператора постфиксного декремента.
Операторы +, -, ++, --, ~, !, а также оператор приведения типов (§15.16) называются унарными операторами.
Следующее производство из §15.16 показано здесь для удобства:
Выражения с унарными операторами группируются справа налево, поэтому -~x означает то же, что и -(~x).
Эта часть грамматики содержит некоторые уловки для избежания двух потенциальных синтаксических неоднозначностей.
Первая потенциальная неоднозначность возникает в выражениях, таких как (p)+q, которое, как кажется программисту на C или C++, может быть либо приведением к типу p унарной операции + над q, либо бинарным сложением двух величин p и q. В C и C++ анализатор решает эту проблему, выполняя ограниченный объём семантического анализа во время разбора, чтобы знать, является ли p именем типа или именем переменной.
Java использует другой подход. Результат оператора + должен быть числовым, и все имена типов, участвующие в приведениях числовых значений, являются известными ключевыми словами. Таким образом, если p — ключевое слово, обозначающее примитивный тип, то (p)+q может иметь смысл только как приведение унарного выражения. Однако, если p не является ключевым словом, обозначающим примитивный тип, то (p)+q может иметь смысл только как бинарная арифметическая операция. Аналогичные замечания относятся к оператору -. Приведенная выше грамматика разбивает CastExpression на два случая, чтобы сделать это различие. Нетерминал UnaryExpression включает все унарные операторы, но нетерминал UnaryExpressionNotPlusMinus исключает использование всех унарных операторов, которые также могут быть бинарными операторами, которыми в Java являются + и -.
Вторая потенциальная неоднозначность заключается в том, что выражение (p)++, как может показаться программисту на C или C++, может быть либо постфиксным инкрементом выражения в скобках, либо началом приведения, например, в (p)++q. Как и прежде, анализаторы C и C++ знают, является ли p именем типа или именем переменной. Но анализатор, использующий только предварительный просмотр одного токена и не выполняющий семантический анализ во время разбора, не сможет определить, когда ++ — это токен предварительного просмотра, является ли (p) следует считать выражением Primary или оставить его на более позднее рассмотрение в качестве части CastExpression.
В Java результат оператора ++ должен быть числовым, а все имена типов, участвующие в приведениях числовых значений, являются известными ключевыми словами. Таким образом, если p — ключевое слово, обозначающее примитивный тип, то (p)++ может иметь смысл только как приведение выражения префиксного инкремента, и обязательно должен быть операнд, например, q, следующий за ++. Однако, если p не является ключевым словом, обозначающим примитивный тип, то (p)++ может иметь смысл только как постфиксный инкремент p. Аналогичные замечания относятся к оператору --. Следовательно, нетерминал UnaryExpressionNotPlusMinus также исключает использование префиксных операторов ++ и --.
Унарное выражение, предваряемое оператором ++, представляет собой выражение префиксного инкремента.
Результат унарного выражения должен быть переменной типа, преобразуемого (§5.1.8) в числовой тип, в противном случае возникает ошибка компиляции.
Тип выражения префиксного инкремента — тип переменной. Результат выражения префиксного инкремента — не переменная, а значение.
Во время выполнения, если вычисление выражения операнда завершается прерыванием, то выражение префиксного инкремента завершается прерыванием по той же причине, и инкрементация не происходит. В противном случае к значению переменной добавляется значение 1, и сумма записывается обратно в переменную. Перед сложением выполняется бинарное числовое продвижение (§5.6.2) над значением 1 и значением переменной. При необходимости сумма сужается с помощью сужающего преобразования примитивного типа (§5.1.3) и/или подвергается преобразованию упаковки (§5.1.7) к типу переменной перед записью. Значение выражения префиксного инкремента — значение переменной после записи нового значения.
Обратите внимание, что упомянутое выше бинарное числовое продвижение может включать преобразование распаковки (§5.1.8) и преобразование множества значений (§5.1.13). При необходимости, преобразование множества значений применяется к сумме перед её записью в переменную.
Переменную, объявленную final, нельзя инкрементировать, потому что при использовании такой final переменной в качестве выражения результатом является значение, а не переменная. Таким образом, она не может быть использована в качестве операнда оператора префиксного инкремента.
Унарное выражение, перед которым стоит оператор --, является выражением префиксного декремента.
Результат унарного выражения должен быть переменной типа, преобразуемой (§5.1.8) в числовой тип, в противном случае возникает ошибка компиляции.
Тип выражения префиксного декремента — тип переменной. Результатом выражения префиксного декремента является не переменная, а значение.
Во время выполнения, если вычисление операнда завершается прерыванием, то выражение префиксного декремента завершается прерыванием по той же причине, и декрементация не происходит. В противном случае, значение 1 вычитается из значения переменной, а разность записывается обратно в переменную. Перед вычитанием выполняется двоичное численное продвижение (§5.6.2) над значением 1 и значением переменной. При необходимости, разность сужается с помощью сужающего преобразования примитивного типа (§5.1.3) и/или подвергается преобразованию к объекту (§5.1.7) к типу переменной перед записью. Значение выражения префиксного декремента — значение переменной после записи нового значения.
Обратите внимание, что двоичное численное продвижение, упомянутое выше, может включать преобразование к примитивному типу (§5.1.8) и преобразование множества значений (§5.1.13). При необходимости, к разности применяется преобразование формата перед её записью в переменную.
Переменную, объявленную final, нельзя декрементировать, потому что при использовании доступа к такой переменной final в качестве выражения, результатом является значение, а не переменная. Таким образом, она не может быть использована в качестве операнда оператора префиксного декремента.
Тип операнда выражения унарного оператора + должен быть типом, преобразуемым (§5.1.8) в примитивный числовой тип, в противном случае возникает ошибка компиляции.
Выполняется унарное численное продвижение (§5.6.1) над операндом. Тип выражения унарного плюса — это продвинутый тип операнда. Результатом выражения унарного плюса является не переменная, а значение, даже если результатом выражения операнда является переменная.
Во время выполнения значение выражения унарного плюса — это продвинутое значение операнда.
Тип операнда выражения унарного оператора - должен быть типом, преобразуемым (§5.1.8) в примитивный числовой тип, в противном случае возникает ошибка компиляции.
Выполняется унарное численное продвижение (§5.6.1) над операндом.
Тип выражения унарного минуса — это продвинутый тип операнда.
Обратите внимание, что унарное численное продвижение выполняет преобразование множества значений (§5.1.13). Из какого бы множества значений ни было взято значение продвинутого операнда, операция унарного отрицания выполняется, и результат также извлекается из этого же множества значений. Затем этот результат подвергается дальнейшему преобразованию множества значений.
Во время выполнения значение выражения унарного минуса — это арифметическое отрицание продвинутого значения операнда.
Для целых значений отрицание равно вычитанию из нуля. Язык программирования Java использует представление целых чисел в дополнении до двух, и диапазон значений в дополнении до двух несимметричен, поэтому отрицание максимального отрицательного int или long приводит к тому же максимальному отрицательному числу. В этом случае происходит переполнение, но исключение не выбрасывается. Для всех целых значений x, -x равно (~x)+1.
Для чисел с плавающей точкой отрицание не равно вычитанию из нуля, потому что если x равно +0.0, то 0.0-x равно +0.0, но -x равно -0.0. Унарный минус просто инвертирует знак числа с плавающей точкой. Важные частные случаи:
-
Если операнд — NaN, результат — NaN. (Напомним, что NaN не имеет знака (§4.2.3).)
-
Если операнд — бесконечность, результат — бесконечность с противоположным знаком.
-
Если операнд — ноль, результат — ноль с противоположным знаком.
Тип операнда выражения унарного оператора ~ должен быть типом, преобразуемым (§5.1.8) в примитивный целочисленный тип, в противном случае возникает ошибка компиляции.
Выполняется унарное численное продвижение (§5.6.1) над операндом. Тип выражения унарного битового дополнения — это продвинутый тип операнда.
Во время выполнения значение выражения унарного битового дополнения — это битовое дополнение продвинутого значения операнда. Во всех случаях, ~x равно (-x)-1.
Тип операнда выражения унарного оператора ! должен быть типом boolean или Boolean, в противном случае возникает ошибка компиляции.
Тип выражения унарного логического отрицания — boolean.
Во время выполнения операнд подвергается преобразованию к примитивному типу (§5.1.8), если это необходимо. Значение выражения унарного логического отрицания — true, если значение операнда (возможно, преобразованное) — false, и false, если значение операнда (возможно, преобразованное) — true.
Выражение приведения типов преобразует во время выполнения значение одного числового типа в аналогичное значение другого числового типа; или подтверждает во время компиляции, что тип выражения является boolean; или проверяет во время выполнения, что значение ссылки относится к объекту, класс которого совместим со специфицированным типом ссылки или списком типов ссылок.
Скобки и тип или список типов, которые они содержат, иногда называются оператором приведения.
Следующее производство из §4.4 показано здесь для удобства:
Если оператор приведения содержит список типов — то есть, ТипСсылки за которым следует одно или несколько ДополнительныхОграничений — то должны быть истинны все следующие условия, в противном случае произойдет ошибка во время компиляции:
-
ТипСсылки должен обозначать тип класса или интерфейса.
-
Стирания (§4.6) всех указанных типов должны быть попарно различными.
-
Никакие два указанных типа не могут быть подтипами различных параметризаций одного и того же обобщенного интерфейса.
Целевой тип для контекста приведения (§5.5), введенный выражением приведения типов, это либо ТипПримитивногоТипа, либо ТипСсылки (если за ним не следуют ДополнительныеОграничения), появляющиеся в операторе приведения, или пересечение типов, обозначаемое ТипСсылки и ДополнительныеОграничения в операторе приведения.
Тип выражения приведения типов — это результат применения преобразования захвата (§5.1.10) к этому целевому типу.
Приведения типов могут использоваться для явного «размечения» выражения лямбда-функции или выражения ссылки на метод конкретным целевым типом. Для обеспечения соответствующей гибкости целевой тип может быть списком типов, обозначающим тип пересечения, при условии, что пересечение индуцирует функциональный интерфейс (§9.8).
Результат выражения приведения типов не является переменной, а значением, даже если результат выражения-операнда является переменной.
Оператор приведения типов не влияет на выбор множества значений (§4.2.3) для значения типа float или типа double. Следовательно, приведение к типу float в выражении, которое не является FP-строгим (§15.4), не обязательно приводит к преобразованию его значения в элемент множества значений с плавающей точкой, а приведение к типу double в выражении, которое не является FP-строгим, не обязательно приводит к преобразованию его значения в элемент множества значений с двойной точностью.
Ошибка во время компиляции возникает, если тип операнда во время компиляции никогда не может быть приведен к типу, указанному оператором приведения, согласно правилам преобразования приведения (§5.5).
В противном случае, во время выполнения, значение операнда преобразуется (если необходимо) преобразованием приведения к типу, указанному оператором приведения.
Исключение ClassCastException выбрасывается, если приведение типа во время выполнения является недопустимым.
Некоторые приведения типов приводят к ошибке во время компиляции. Некоторые приведения типов могут быть доказаны во время компиляции, всегда правильными во время выполнения. Например, всегда правильно преобразовать значение типа класса в тип его суперкласса; такое приведение не должно требовать специальных действий во время выполнения. Наконец, некоторые приведения типов не могут быть доказаны как всегда правильные или всегда неправильные во время компиляции. Такие приведения требуют проверки во время выполнения. Подробности см. в §5.5.
Операторы *, / и % называются операторами умножения.
Операторы умножения имеют одинаковый приоритет и являются левоассоциативными (они группируются слева направо).
Тип каждого операнда оператора умножения должен быть типом, преобразуемым (§5.1.8) в примитивный числовой тип, в противном случае произойдёт ошибка компиляции.
На операнды выполняется двоичное числовое продвижение (§5.6.2).
Обратите внимание, что двоичное числовое продвижение выполняет преобразование множества значений (§5.1.13) и может выполнять преобразование unboxing (§5.1.8).
Тип выражения умножения — это тип его операндов после продвижения.
Если тип после продвижения — int или long, то выполняется целочисленное умножение.
Если тип после продвижения — float или double, то выполняется умножение с плавающей точкой.
Двоичный оператор * выполняет умножение, получая произведение своих операндов.
Умножение является коммутативным, если выражения операндов не содержат побочных эффектов.
Целочисленное умножение ассоциативно, когда все операнды одного типа.
Умножение с плавающей точкой не является ассоциативным.
Если при целочисленном умножении происходит переполнение, то результатом являются младшие биты математического произведения, представленные в формате дополнения до двух достаточной разрядности. В результате, если произошло переполнение, то знак результата может не совпадать со знаком математического произведения значений двух операндов.
Результат умножения с плавающей точкой определяется правилами арифметики IEEE 754:
-
Если один из операндов NaN, то результат NaN.
-
Если результат не NaN, то его знак положителен, если оба операнда имеют одинаковый знак, и отрицателен, если знаки операндов различны.
-
Умножение бесконечности на ноль приводит к NaN.
-
Умножение бесконечности на конечное значение приводит к знаковой бесконечности. Знак определяется указанным выше правилом.
-
В оставшихся случаях, когда не участвуют бесконечность или NaN, вычисляется точное математическое произведение. Затем выбирается множество значений с плавающей точкой:
-
Если выражение умножения FP-strict (§15.4):
-
Если тип выражения умножения
float, то должно быть выбрано множество значений float. -
Если тип выражения умножения
double, то должно быть выбрано множество значений double.
-
-
Если выражение умножения не FP-strict:
-
Если тип выражения умножения
float, то может быть выбрано множество значений float или множество значений float-extended-exponent, по усмотрению реализации. -
Если тип выражения умножения
double, то может быть выбрано множество значений double или множество значений double-extended-exponent, по усмотрению реализации.
-
Далее, из выбранного множества значений должно быть выбрано значение для представления произведения.
Если величина произведения слишком велика для представления, то мы говорим, что операция переполнилась; результатом тогда является бесконечность соответствующего знака.
В противном случае произведение округляется до ближайшего значения в выбранном множестве значений с использованием режима округления IEEE 754 до ближайшего значения. Язык программирования Java требует поддержки постепенного уменьшения, как определено в IEEE 754 (§4.2.4).
-
Несмотря на то, что может произойти переполнение, уменьшение или потеря информации, вычисление оператора умножения * никогда не вызывает исключения во время выполнения.
Бинарный / оператор выполняет деление, получая частное своих операндов. Левый операнд — это делимое, а правый — делитель.
Целочисленное деление округляется к 0. То есть, частное, полученное для операндов n и d, являющихся целыми числами после бинарного числового преобразования (§5.6.2), является целым значением q, модуль которого как можно больше, при условии |d ⋅ q| ≤ |n|. Кроме того, q положительно, когда |n| ≥ |d| и n и d имеют одинаковый знак, но q отрицательно, когда |n| ≥ |d| и n и d имеют противоположные знаки.
Существует один особый случай, который не удовлетворяет этому правилу: если делимое — это отрицательное целое число с наибольшим возможным значением для своего типа, а делитель — -1, то происходит переполнение, и результат равен делимому. Несмотря на переполнение, в этом случае исключение не выбрасывается. С другой стороны, если значение делителя при целочисленном делении равно 0, то выбрасывается ArithmeticException.
Результат дробного деления определяется правилами арифметики IEEE 754:
-
Если любой из операндов является NaN, результат — NaN.
-
Если результат не NaN, знак результата положительный, если оба операнда имеют одинаковый знак, и отрицательный, если операнды имеют разные знаки.
-
Деление бесконечности на бесконечность приводит к NaN.
-
Деление бесконечности на конечное значение приводит к знакомой бесконечности. Знак определяется вышеуказанным правилом.
-
Деление конечного значения на бесконечность приводит к знакомому нулю. Знак определяется вышеуказанным правилом.
-
Деление нуля на ноль приводит к NaN; деление нуля на любое другое конечное значение приводит к знаковому нулю. Знак определяется вышеуказанным правилом.
-
Деление ненулевого конечного значения на ноль приводит к знакомой бесконечности. Знак определяется вышеуказанным правилом.
-
В остальных случаях, где не участвуют ни бесконечность, ни NaN, вычисляется точное математическое частное. Затем выбирается набор плавающего значения:
-
Если выражение деления — FP-строгое (§15.4):
-
Если тип выражения деления —
float, то должен быть выбран набор плавающих значений. -
Если тип выражения деления —
double, то должен быть выбран набор двойных значений.
-
-
Если выражение деления не FP-строгое:
-
Если тип выражения деления —
float, то может быть выбран набор плавающих значений или набор плавающих значений с расширенным порядком, по усмотрению реализации. -
Если тип выражения деления —
double, то может быть выбран набор двойных значений или набор двойных значений с расширенным порядком, по усмотрению реализации.
-
Далее, значение должно быть выбрано из выбранного набора значений для представления частного.
Если модуль частного слишком велик для представления, мы говорим, что операция переполняется; результат затем является бесконечностью с соответствующим знаком.
В противном случае частное округляется до ближайшего значения в выбранном наборе значений с использованием режима округления IEEE 754 до ближайшего. Язык программирования Java требует поддержки постепенного подпорогового значения, как определено в IEEE 754 (§4.2.4).
-
Несмотря на то, что может произойти переполнение, подпороговое значение, деление на ноль или потеря информации, оценка оператора дробного деления / никогда не вызывает исключение времени выполнения.
Бинарный % оператор, как говорят, возвращает остаток от своих операндов от подразумеваемого деления; левый операнд — это делимое, а правый — делитель.
В C и C++ оператор остатка принимает только целочисленные операнды, но в языке программирования Java он также принимает операнды с плавающей точкой.
Операция остатка для операндов, которые являются целыми числами после бинарного числового преобразования (§5.6.2), производит значение результата, такое что (a/b)*b+(a%b) равно a.
Это тождество выполняется даже в том особом случае, когда делимое — это отрицательное целое число с наибольшим возможным значением для своего типа, а делитель — -1 (остаток — 0).
Из этого правила следует, что результат операции остатка может быть отрицательным только в том случае, если делимое отрицательное, и может быть положительным только в том случае, если делимое положительное. Кроме того, модуль результата всегда меньше модуля делителя.
Если значение делителя для оператора целочисленного остатка равно 0, то выбрасывается ArithmeticException.
Пример 15.17.3-1. Оператор целочисленного остатка
class Test1 {
public static void main(String[] args) {
int a = 5%3; // 2
int b = 5/3; // 1
System.out.println("5%3 produces " + a +
" (note that 5/3 produces " + b + ")");
int c = 5%(-3); // 2
int d = 5/(-3); // -1
System.out.println("5%(-3) produces " + c +
" (note that 5/(-3) produces " + d + ")");
int e = (-5)%3; // -2
int f = (-5)/3; // -1
System.out.println("(-5)%3 produces " + e +
" (note that (-5)/3 produces " + f + ")");
int g = (-5)%(-3); // -2
int h = (-5)/(-3); // 1
System.out.println("(-5)%(-3) produces " + g +
" (note that (-5)/(-3) produces " + h + ")");
}
}
Эта программа выводит:
5%3 produces 2 (note that 5/3 produces 1) 5%(-3) produces 2 (note that 5/(-3) produces -1) (-5)%3 produces -2 (note that (-5)/3 produces -1) (-5)%(-3) produces -2 (note that (-5)/(-3) produces 1)
Результат операции остатка с плавающей точкой, вычисленной оператором %, не совпадает с результатом операции остатка, определённой в IEEE 754. Операция остатка IEEE 754 вычисляет остаток от округленного деления, а не усеченного деления, и поэтому её поведение не аналогично поведению обычного оператора остатка для целых чисел. Вместо этого, язык программирования Java определяет % для операций с плавающей точкой, чтобы он вел себя аналогично оператору остатка для целых чисел; это можно сравнить с функцией C-библиотеки fmod. Операцию остатка IEEE 754 можно вычислить с помощью библиотечной процедуры Math.IEEEremainder.
Результат операции остатка с плавающей точкой определяется правилами арифметики IEEE 754:
-
Если любой из операндов является NaN, результат — NaN.
-
Если результат не NaN, знак результата равен знаку делимого.
-
Если делимое является бесконечностью, или делитель — нулём, или оба, результат — NaN.
-
Если делимое конечно, а делитель — бесконечность, результат равен делимому.
-
Если делимое — ноль, а делитель — конечное число, результат равен делимому.
-
В остальных случаях, где не участвуют ни бесконечность, ни ноль, ни NaN, остаток с плавающей запятой r от деления делимого n на делитель d определяется математическим соотношением r = n - (d ⋅ q), где q — целое число, которое отрицательно только в том случае, если n/d отрицательно, и положительно только в том случае, если n/d положительно, и модуль которого как можно больше, не превышая модуль истинного математического частного n и d.
Вычисление оператора остатка с плавающей точкой % никогда не вызывает исключение времени выполнения, даже если правый операнд равен нулю. Переполнение, подпороговое значение или потеря точности не могут произойти.
Пример 15.17.3-2. Оператор остатка с плавающей точкой
class Test2 {
public static void main(String[] args) {
double a = 5.0%3.0; // 2.0
System.out.println("5.0%3.0 produces " + a);
double b = 5.0%(-3.0); // 2.0
System.out.println("5.0%(-3.0) produces " + b);
double c = (-5.0)%3.0; // -2.0
System.out.println("(-5.0)%3.0 produces " + c);
double d = (-5.0)%(-3.0); // -2.0
System.out.println("(-5.0)%(-3.0) produces " + d);
}
}
Эта программа выводит:
5.0%3.0 produces 2.0 5.0%(-3.0) produces 2.0 (-5.0)%3.0 produces -2.0 (-5.0)%(-3.0) produces -2.0
Операторы + и - называются операторами сложения.
Операторы сложения имеют одинаковый приоритет и являются левоассоциативными (они группируются слева направо).
Если тип любого операнда оператора + равен String, то операция представляет собой конкатенацию строк.
В противном случае, тип каждого из операндов оператора + должен быть типом, преобразуемым (§5.1.8) в примитивный числовой тип; в противном случае возникает ошибка времени компиляции.
В любом случае, тип каждого из операндов бинарного оператора - должен быть типом, преобразуемым (§5.1.8) в примитивный числовой тип; в противном случае возникает ошибка времени компиляции.
Если только один операнд-выражение имеет тип String, то для другого операнда выполняется преобразование в строку (§5.1.11) во время выполнения, чтобы получить строку.
Результат конкатенации строк — ссылка на объект String, являющийся конкатенацией двух строковых операндов. Символы левого операнда предшествуют символам правого операнда в вновь созданной строке.
Объект String создается заново (§12.5), если выражение не является константным выражением (§15.28).
Реализация может выбрать выполнение преобразования и конкатенации в один шаг для того, чтобы избежать создания и последующего удаления промежуточного объекта String. Для повышения производительности при многократной конкатенации строк компилятор Java может использовать класс StringBuffer или аналогичную технику, чтобы уменьшить количество промежуточных объектов String, создаваемых при вычислении выражения.
Для примитивных типов реализация также может оптимизировать создание обёртки, напрямую преобразовывая примитивный тип в строку.
Пример 15.18.1-1. Конкатенация строк
Выражение в примере:
"The square root of 2 is " + Math.sqrt(2)
даёт результат:
"The square root of 2 is 1.4142135623730952"
Оператор + является левоассоциативным, независимо от того, интерпретируется ли он как оператор конкатенации строк или сложения чисел. В некоторых случаях необходимо быть внимательным, чтобы получить желаемый результат. Например, выражение:
a + b + c
всегда означает:
(a + b) + c
Поэтому результат выражения:
1 + 2 + " fiddlers"
равен:
"3 fiddlers"
но результат выражения:
"fiddlers " + 1 + 2
равен:
"fiddlers 12"
Пример 15.18.1-2. Конкатенация строк и условные операторы
В этом юмористическом примере:
class Bottles {
static void printSong(Object stuff, int n) {
String plural = (n == 1) ? "" : "s";
loop: while (true) {
System.out.println(n + " bottle" + plural
+ " of " + stuff + " on the wall,");
System.out.println(n + " bottle" + plural
+ " of " + stuff + ";");
System.out.println("You take one down "
+ "and pass it around:");
--n;
plural = (n == 1) ? "" : "s";
if (n == 0)
break loop;
System.out.println(n + " bottle" + plural
+ " of " + stuff + " on the wall!");
System.out.println();
}
System.out.println("No bottles of " +
stuff + " on the wall!");
}
public static void main(String[] args) {
printSong("slime", 3);
}
}
метод printSong выведет вариант детской песенки. Популярные значения для stuff включают "pop" и "beer"; наиболее популярное значение для n — 100. Вот вывод программы:
3 bottles of slime on the wall, 3 bottles of slime; You take one down and pass it around: 2 bottles of slime on the wall! 2 bottles of slime on the wall, 2 bottles of slime; You take one down and pass it around: 1 bottle of slime on the wall! 1 bottle of slime on the wall, 1 bottle of slime; You take one down and pass it around: No bottles of slime on the wall!
В коде обратите внимание на тщательное условное формирование единственного "bottle" при необходимости, а не множественного "bottles"; обратите также внимание на то, как оператор конкатенации строк использовался для разделения длинной константы:
"You take one down and pass it around:"
на две части для избежания неудобно длинной строки в исходном коде.
Бинарный оператор + выполняет сложение при применении к двум операндам числового типа, генерируя сумму операндов.
Бинарный оператор - выполняет вычитание, генерируя разность двух числовых операндов.
Над операндами выполняется бинарное числовое продвижение (§5.6.2).
Обратите внимание, что бинарное числовое продвижение выполняет преобразование набора значений (§5.1.13) и может выполнять распаковку (§5.1.8).
Тип выражения сложения с числовыми операндами — это тип продвижения его операндов.
Если этот тип продвижения — int или long, то выполняется целочисленная арифметика.
Если этот тип продвижения — float или double, то выполняется арифметика с плавающей точкой.
Сложение является коммутативной операцией, если выражения операндов не имеют побочных эффектов.
Целочисленное сложение является ассоциативным, когда все операнды одного типа.
Сложение с плавающей точкой не является ассоциативным.
Если целочисленное сложение приводит к переполнению, то результатом являются младшие биты математической суммы, представленной в некотором достаточно большом формате со знаком. Если происходит переполнение, то знак результата не совпадает со знаком математической суммы двух значений операндов.
Результат сложения с плавающей точкой определяется следующими правилами арифметики IEEE 754:
-
Если один из операндов — NaN, результат — NaN.
-
Сумма двух бесконечностей противоположных знаков — NaN.
-
Сумма двух бесконечностей одного знака — бесконечность этого знака.
-
Сумма бесконечности и конечного значения равна бесконечному операнду.
-
Сумма двух нулей противоположных знаков — положительный ноль.
-
Сумма двух нулей одного знака — ноль этого знака.
-
Сумма нуля и ненулевого конечного значения равна ненулевому операнду.
-
Сумма двух ненулевых конечных значений одинаковой величины и противоположных знаков — положительный ноль.
-
В остальных случаях, когда нет бесконечности, нуля или NaN, и операнды имеют одинаковый знак или разную величину, вычисляется точная математическая сумма. Затем выбирается набор значений с плавающей точкой:
-
Если выражение сложения — FP-строгое (§15.4):
-
Если тип выражения сложения —
float, то выбирается набор значений с плавающей точкой. -
Если тип выражения сложения —
double, то выбирается набор значений с двойной точностью.
-
-
Если выражение сложения не FP-строгое:
-
Если тип выражения сложения —
float, то может быть выбран набор значений с плавающей точкой или набор значений с расширенным порядком, по усмотрению реализации. -
Если тип выражения сложения —
double, то может быть выбран набор значений с двойной точностью или набор значений с двойным расширенным порядком, по усмотрению реализации.
-
Затем из выбранного набора значений должно быть выбрано значение для представления суммы.
Если величина суммы слишком велика для представления, то говорят о переполнении операции; результатом является бесконечность соответствующего знака.
В противном случае сумма округляется до ближайшего значения в выбранном наборе значений с использованием режима округления IEEE 754 до ближайшего. Язык программирования Java требует поддержки постепенного подпорогового значения, как определено в IEEE 754 (§4.2.4).
-
Бинарный оператор - выполняет вычитание при применении к двум операндам числового типа, генерируя разность своих операндов; операнд слева — уменьшаемое, а операнд справа — вычитаемое.
Для целочисленного и для вычитания с плавающей точкой всегда верно, что a-b дает тот же результат, что и a+(-b).
Обратите внимание, что для целочисленных значений вычитание из нуля равно отрицанию. Однако для операндов с плавающей точкой вычитание из нуля не равно отрицанию, потому что если x — +0.0, то 0.0-x — +0.0, но -x — -0.0.
Несмотря на то, что может произойти переполнение, недополнение или потеря информации, вычисление числового оператора сложения никогда не вызывает исключения во время выполнения.
Операторы << (сдвиг влево), >> (сдвиг вправо со знаком) и >>> (сдвиг вправо без знака) называются операторами сдвига. Левый операнд оператора сдвига представляет значение, которое необходимо сдвинуть, а правый операнд определяет расстояние сдвига.
Операторы сдвига являются левоассоциативными (они группируются слева направо).
Для каждого операнда отдельно выполняется преобразование арифметического типа (§5.6.1). (Преобразование бинарного арифметического типа (§5.6.2) для операндов не выполняется.)
Если тип каждого из операндов оператора сдвига после преобразования арифметического типа не является примитивным целочисленным типом, это ошибка времени компиляции.
Тип выражения сдвига — это преобразованный тип левого операнда.
Если преобразованный тип левого операнда — это int, то в качестве расстояния сдвига используются только пять младших битов правого операнда. Будто правый операнд был подвергнут побитовому логическому оператору И & (§15.22.1) с маской 0x1f (0b11111). Таким образом, расстояние сдвига всегда находится в диапазоне 0 до 31 включительно.
Если преобразованный тип левого операнда — это long, то в качестве расстояния сдвига используются только шесть младших битов правого операнда. Будто правый операнд был подвергнут побитовому логическому оператору И & (§15.22.1) с маской 0x3f (0b111111). Таким образом, расстояние сдвига всегда находится в диапазоне 0 до 63 включительно.
Во время выполнения операции сдвига выполняются над двоично-дополнительным целочисленным представлением значения левого операнда.
Значение n << s равно n, сдвинутому влево на s разрядов; это эквивалентно (даже если происходит переполнение) умножению на два в степени s.
Значение n >> s равно n, сдвинутому вправо на s разрядов со знаком. Результирующее значение равно floor(n / 2s). Для неотрицательных значений n это эквивалентно целочисленному делению с отбрасыванием дробной части, вычисляемому оператором целочисленного деления /, на два в степени s.
Значение n >>> s равно n, сдвинутому вправо на s разрядов с нулевым расширением, где:
-
Если n положительно, то результат совпадает с результатом n
>>s. -
Если n отрицательно и тип левого операнда — это
int, то результат равен выражению(n>>s)+(2<<~s). -
Если n отрицательно и тип левого операнда — это
long, то результат равен выражению(n>>s)+(2L<<~s).
Дополнительный член (2 << ~s) или (2L << ~s) исключает распространяемый бит знака.
Обратите внимание, что из-за неявного маскирования правого операнда оператора сдвига ~s в качестве расстояния сдвига эквивалентно 31-s при сдвиге значения int и 63-s при сдвиге значения long.
Операторы числового сравнения <, >, <=, и >=, а также оператор instanceof, называются операторами отношения.
Операторы отношения являются левоассоциативными (они группируются слева направо).
Однако этот факт бесполезен. Например, a<b<c парсится как (a<b)<c, что всегда является ошибкой во время компиляции, потому что тип a<b всегда boolean, а < не является оператором для значений boolean.
Тип выражения отношения всегда boolean.
Тип каждого из операндов оператора числового сравнения должен быть типом, преобразуемым (§5.1.8) в примитивный числовой тип; в противном случае возникает ошибка во время компиляции.
Над операндами выполняется двоичное числовое продвижение (§5.6.2).
Обратите внимание, что двоичное числовое продвижение выполняет преобразование множества значений (§5.1.13) и может выполнять преобразование распаковки (§5.1.8).
Если тип продвинутых операндов является int или long, то выполняется сравнение целых чисел со знаком.
Если тип продвинут до float или double, то выполняется сравнение с плавающей точкой.
Сравнение выполняется точно для значений с плавающей точкой, независимо от наборов значений, из которых были взяты их представляющие значения.
Результат сравнения с плавающей точкой, как определяется спецификацией стандарта IEEE 754, таков:
-
Если какой-либо операнд является NaN, то результат — false.
-
Все значения, кроме NaN, упорядочены, при этом отрицательная бесконечность меньше всех конечных значений, а положительная бесконечность больше всех конечных значений.
-
Положительный ноль и отрицательный ноль считаются равными.
Например,
-0.0<0.0этоfalse, но-0.0<=0.0этоtrue.Обратите внимание, что методы
Math.minиMath.maxрассматривают отрицательный ноль как строго меньший, чем положительный ноль.
С учётом этих соображений для чисел с плавающей точкой, следующие правила действуют для целых операндов или для операндов с плавающей точкой, отличных от NaN:
-
Значение, полученное оператором
<, равноtrue, если значение левого операнда меньше значения правого операнда, в противном случае —false. -
Значение, полученное оператором
<=, равноtrue, если значение левого операнда меньше или равно значению правого операнда, в противном случае —false. -
Значение, полученное оператором
>, равноtrue, если значение левого операнда больше значения правого операнда, в противном случае —false. -
Значение, полученное оператором
>=, равноtrue, если значение левого операнда больше или равно значению правого операнда, в противном случае —false.
Тип операнда RelationalExpression оператора instanceof должен быть типом ссылки или типом null; в противном случае возникает ошибка во время компиляции.
Ошибка во время компиляции возникает, если ReferenceType, упомянутый после оператора instanceof, не обозначает реифицируемый тип ссылки (§4.7).
Если приведение (§15.16) RelationalExpression к ReferenceType было бы отклонено как ошибка во время компиляции, то выражение отношения instanceof аналогичным образом приводит к ошибке во время компиляции. В такой ситуации результат выражения instanceof никогда не мог бы быть истинным.
Во время выполнения, результат оператора instanceof равен true, если значение RelationalExpression не равно null и ссылка могла бы быть приведена к ReferenceType без повышения исключения ClassCastException. В противном случае результат равен false.
Пример 15.20.2-1. Оператор instanceof
class Point { int x, y; }
class Element { int atomicNumber; }
class Test {
public static void main(String[] args) {
Point p = new Point();
Element e = new Element();
if (e instanceof Point) { // compile-time error
System.out.println("I get your point!");
p = (Point)e; // compile-time error
}
}
}
Эта программа приводит к двум ошибкам во время компиляции. Приведение (Point)e неверно, потому что ни один экземпляр Element или его возможных подклассов (здесь не показано) не может быть экземпляром какого-либо подкласса Point. Выражение instanceof неверно по той же причине. Если бы, с другой стороны, класс Point был подклассом Element (в этом примере это довольно странная концепция):
class Point extends Element { int x, y; }
то приведение было бы возможным, хотя оно потребовало бы проверки во время выполнения, и выражение instanceof было бы осмысленным и допустимым. Приведение (Point)e никогда не вызывало бы исключения, потому что оно не выполнялось бы, если бы значение e не могло быть корректно приведено к типу Point.
Операторы == (равно) и != (не равно) называются операторами равенства.
Операторы равенства являются левоассоциативными (они группируются слева направо).
Однако этот факт практически никогда не полезен. Например, a==b==c анализируется как (a==b)==c. Тип результата a==b всегда boolean, и, следовательно, c должен быть типа boolean, иначе произойдёт ошибка компиляции. Таким образом, a==b==c не проверяет, все ли a, b и c равны.
Операторы равенства являются коммутативными, если выражения-операнды не имеют побочных эффектов.
Операторы равенства аналогичны операторам сравнения, за исключением более низкого приоритета. Таким образом, a<b==c<d равно true всякий раз, когда a<b и c<d имеют одинаковое значение истинности.
Операторы равенства могут использоваться для сравнения двух операндов, которые могут быть преобразованы (§5.1.8) в числовой тип, или двух операндов типа boolean или Boolean, или двух операндов, каждый из которых является либо ссылочным типом, либо типом null. Все остальные случаи приводят к ошибке компиляции.
Тип выражения равенства всегда boolean.
Во всех случаях a!=b даёт тот же результат, что и !(a==b).
Если оба операнда оператора равенства являются числовыми типами, или один является числовым типом, а другой может быть преобразован (§5.1.8) в числовой тип, то над операндами выполняется двойное числовое продвижение (§5.6.2).
Обратите внимание, что двойное числовое продвижение выполняет преобразование множества значений (§5.1.13) и может выполнять разворачивание (§5.1.8).
Если тип операндов после продвижения является int или long, выполняется целочисленная проверка на равенство.
Если тип продвижения является float или double, выполняется проверка на равенство с плавающей точкой.
Сравнение с плавающей точкой выполняется точно, независимо от набора значений, из которого были получены представляющие их значения.
Проверка на равенство с плавающей точкой выполняется в соответствии с правилами стандарта IEEE 754:
-
Если любой операнд является NaN, то результат
==равенfalse, а результат!=равенtrue.Действительно, тест
x!=xравенtrueтогда и только тогда, когда значениеxравно NaN.Методы
Float.isNaNиDouble.isNaNтакже могут использоваться для проверки, является ли значение NaN. -
Положительный ноль и отрицательный ноль считаются равными.
Например,
-0.0==0.0равноtrue. -
В противном случае два различных значения с плавающей точкой считаются неравными операторами равенства.
В частности, существует одно значение, представляющее положительную бесконечность, и одно значение, представляющее отрицательную бесконечность; каждое сравнивается только с самим собой и не сравнивается ни с какими другими значениями.
С учётом этих соображений для чисел с плавающей точкой, следующие правила применяются к целочисленным операндам или к операндам с плавающей точкой, отличным от NaN:
-
Значение, полученное от оператора
==, равноtrue, если значение левого операнда равно значению правого операнда; в противном случае результат равенfalse. -
Значение, полученное от оператора
!=, равноtrue, если значение левого операнда не равно значению правого операнда; в противном случае результат равенfalse.
Если оба операнда оператора равенства являются типа boolean, или если один операнд является типа boolean, а другой — типа Boolean, то операция является логическим сравнением на равенство.
Логические операторы равенства являются ассоциативными.
Если один из операндов является типа Boolean, он подвергается преобразованию разворачивания (§5.1.8).
Результат == равен true, если оба операнда (после любого необходимого преобразования разворачивания) являются true или оба являются false; в противном случае результат равен false.
Результат != равен false, если оба операнда являются true или оба являются false; в противном случае результат равен true.
Таким образом, != ведет себя так же, как ^ (§15.22.2), когда применяется к операндам boolean.
Если оба операнда оператора равенства являются либо ссылочного типа, либо типа null, то операция является сравнением объектов по ссылке.
Ошибка компиляции возникает, если невозможно преобразовать тип любого операнда в тип другого с помощью преобразования приведения типов (§5.5). Величины двух операндов обязательно будут отличаться (исключая случай, когда оба значения равны null).
Во время выполнения результат == равен true, если оба значения операндов являются null или оба ссылаются на один и тот же объект или массив; в противном случае результат равен false.
Результат != равен false, если оба значения операндов являются null или оба ссылаются на один и тот же объект или массив; в противном случае результат равен true.
Хотя == может использоваться для сравнения ссылок типа String, такое сравнение на равенство определяет, ссылаются ли два операнда на один и тот же объект String. Результат равен false, если операнды представляют собой разные объекты String, даже если они содержат одну и ту же последовательность символов (§3.10.5). Содержимое двух строк s и t можно проверить на равенство вызовом метода s.equals(t).
Операторы битовых операций и логических операций включают оператор И &, оператор исключающего ИЛИ ^ и оператор включительно ИЛИ |.
Эти операторы имеют разную приоритетность, при этом & имеет наивысший приоритет, а | — наименьший.
Каждый из этих операторов синтаксически левоассоциативен (они группируются слева направо).
Каждый оператор коммутативен, если выражения операндов не имеют побочных эффектов.
Каждый оператор ассоциативен.
Битовые и логические операторы могут использоваться для сравнения двух операндов числового типа или двух операндов типа boolean. Все другие случаи приводят к ошибке на этапе компиляции.
Когда оба операнда оператора &, ^ или | относятся к типу, который может быть преобразован (§5.1.8) в примитивный целочисленный тип, сначала выполняется двоичное численное продвижение операндов (§5.6.2).
Тип выражения битового оператора — это тип продвинутых операндов.
Для & значение результата — битовое И значений операндов.
Для ^ значение результата — битовое исключающее ИЛИ значений операндов.
Для | значение результата — битовое включительно ИЛИ значений операндов.
Например, результат выражения:
0xff00 & 0xf0f0
равен:
0xf000
Результат выражения:
0xff00 ^ 0xf0f0
равен:
0x0ff0
Результат выражения:
0xff00 | 0xf0f0
равен:
0xfff0
Когда оба операнда оператора &, ^ или | имеют тип boolean или Boolean, тогда тип выражения битового оператора — boolean. Во всех случаях операнды подвергаются преобразованию распаковки (§5.1.8) по мере необходимости.
Для & значение результата — true, если значения обоих операндов — true; в противном случае результат — false.
Для ^ значение результата — true, если значения операндов разные; в противном случае результат — false.
Для | значение результата — false, если значения обоих операндов — false; в противном случае результат — true.
Оператор условного И && похож на оператор & (§15.22.2), но оценивает свой правый операнд только если значение его левого операнда — true.
Оператор условного И синтаксически левоассоциативен (он группируется слева направо).
Оператор условного И полностью ассоциативен по отношению к побочным эффектам и значению результата. То есть для любых выражений a, b и c, вычисление выражения (( даёт тот же результат и те же побочные эффекты в том же порядке, что и вычисление выражения a) &&
(b)) && (c)(.a) &&
((b) &&
(c))
Каждый операнд оператора условного И должен иметь тип boolean или Boolean, в противном случае произойдёт ошибка на этапе компиляции.
Тип выражения условного И всегда boolean.
Во время выполнения сначала вычисляется выражение левого операнда; если результат имеет тип Boolean, он подвергается преобразованию распаковки (§5.1.8).
Если полученное значение — false, значение выражения условного И — false, и выражение правого операнда не вычисляется.
Если значение левого операнда — true, тогда вычисляется правое выражение; если результат имеет тип Boolean, он подвергается преобразованию распаковки (§5.1.8). Полученное значение становится значением выражения условного И.
Таким образом, && вычисляет тот же результат, что и & с boolean операндами. Отличие только в том, что выражение правого операнда вычисляется условно, а не всегда.
Оператор условного ИЛИ || похож на оператор | (§15.22.2), но вычисляет свой правый операнд только если значение его левого операнда — false.
Оператор условного ИЛИ синтаксически левоассоциативен (он группируется слева направо).
Оператор условного ИЛИ полностью ассоциативен по отношению к побочным эффектам и значению результата. То есть для любых выражений a, b и c, вычисление выражения (( даёт тот же результат и те же побочные эффекты в том же порядке, что и вычисление выражения a) ||
(b)) || (c)(.a) ||
((b) ||
(c))
Каждый операнд оператора условного ИЛИ должен иметь тип boolean или Boolean, в противном случае произойдёт ошибка на этапе компиляции.
Тип выражения условного ИЛИ всегда boolean.
Во время выполнения сначала вычисляется выражение левого операнда; если результат имеет тип Boolean, он подвергается преобразованию распаковки (§5.1.8).
Если полученное значение — true, значение выражения условного ИЛИ — true, и выражение правого операнда не вычисляется.
Если значение левого операнда — false, тогда вычисляется правое выражение; если результат имеет тип Boolean, он подвергается преобразованию распаковки (§5.1.8). Полученное значение становится значением выражения условного ИЛИ.
Таким образом, || вычисляет тот же результат, что и | с boolean или Boolean операндами. Отличие только в том, что выражение правого операнда вычисляется условно, а не всегда.
Условный оператор ? : использует булево значение одного выражения для определения, какое из двух других выражений должно быть вычислено.
Условный оператор синтаксически ассоциативен справа налево (группировка справа налево). Таким образом, a?b:c?d:e?f:g означает то же, что и a?b:(c?d:(e?f:g)).
Условный оператор имеет три операнда. ? стоит между первым и вторым выражениями, а : — между вторым и третьим.
Первое выражение должно быть типа boolean или Boolean, в противном случае произойдёт ошибка компиляции.
Ошибка компиляции возникает, если второй или третий операнд — вызов метода void.
Фактически, согласно грамматике операторов выражения (§14.8), условное выражение не может использоваться в любом контексте, где может использоваться вызов метода void.
Существует три вида условных выражений, классифицируемых по вторым и третьим операндам: булевы условные выражения, численные условные выражения и ссылочные условные выражения. Правила классификации следующие:
-
Если второе и третье выражения — булевы выражения, то условное выражение — булевое условное выражение.
Для классификации выражения как булева, следующие выражения являются булевыми:
-
Выражение в отдельной форме (§15.2), имеющее тип
booleanилиBoolean. -
Выражение в скобках (§15.8.5).
-
Выражение создания экземпляра класса (§15.9) для класса
Boolean. -
Выражение вызова метода (§15.12) для которого выбранный наиболее специфичный метод (§15.12.2.5) имеет возвращаемый тип
booleanилиBoolean.Обратите внимание, что для универсального метода это тип до инстанцирования аргументов типа метода.
-
Условное выражение
boolean.
-
-
Если второе и третье выражения — численные выражения, то условное выражение — численное условное выражение.
Для классификации выражения как числового, следующие выражения являются числовыми:
-
Выражение в отдельной форме (§15.2) с типом, преобразуемым в числовой тип (§4.2, §5.1.8).
-
Числовое выражение в скобках (§15.8.5).
-
Выражение создания экземпляра класса (§15.9) для класса, преобразуемого в числовой тип.
-
Выражение вызова метода (§15.12) для которого выбранный наиболее специфичный метод (§15.12.2.5) имеет возвращаемый тип, преобразуемый в числовой тип.
-
Числовое условное выражение.
-
-
В противном случае, условное выражение — ссылочное условное выражение.
Процесс определения типа условного выражения зависит от вида условного выражения, как описано в следующих разделах.
В следующих таблицах обобщаются вышеприведенные правила, предоставляя тип условного выражения для всех возможных типов его вторых и третьих операндов. bnp(..) обозначает применение бинарного числового повышения. Форма "T | bnp(..)" используется, когда один операнд — константное выражение типа int и может быть представлен в типе T, где бинарное числовое повышение используется, если операнд не может быть представлен в типе T. Тип операнда Object означает любой ссылочный тип, кроме типа null и восьми wrapper-классов Boolean, Byte, Short, Character, Integer, Long, Float, Double.
Таблица 15.25-A. Тип условного выражения (Примитивный 3-й операнд, Часть I)
| 3-й → | byte | short | char | int |
|---|---|---|---|---|
| 2-й ↓ | ||||
byte | byte | short | bnp(byte,char) |
byte | bnp(byte,int) |
Byte | byte | short | bnp(Byte,char) |
byte | bnp(Byte,int) |
short | short | short | bnp(short,char) |
short | bnp(short,int) |
Short | short | short | bnp(Short,char) |
short | bnp(Short,int) |
char | bnp(char,byte) | bnp(char,short) | char |
char | bnp(char,int) |
Character | bnp(Character,byte) | bnp(Character,short) | char |
char | bnp(Character,int) |
int |
byte | bnp(int,byte) |
short | bnp(int,short) |
char | bnp(int,char) | int |
Integer | bnp(Integer,byte) | bnp(Integer,short) | bnp(Integer,char) | int |
long | bnp(long,byte) | bnp(long,short) | bnp(long,char) | bnp(long,int) |
Long | bnp(Long,byte) | bnp(Long,short) | bnp(Long,char) | bnp(Long,int) |
float | bnp(float,byte) | bnp(float,short) | bnp(float,char) | bnp(float,int) |
Float | bnp(Float,byte) | bnp(Float,short) | bnp(Float,char) | bnp(Float,int) |
double | bnp(double,byte) | bnp(double,short) | bnp(double,char) | bnp(double,int) |
Double | bnp(Double,byte) | bnp(Double,short) | bnp(Double,char) | bnp(Double,int) |
boolean | lub(Boolean,Byte) | lub(Boolean,Short) | lub(Boolean,Character) | lub(Boolean,Integer) |
Boolean | lub(Boolean,Byte) | lub(Boolean,Short) | lub(Boolean,Character) | lub(Boolean,Integer) |
null | lub(null,Byte) | lub(null,Short) | lub(null,Character) | lub(null,Integer) |
Object | lub(Object,Byte) | lub(Object,Short) | lub(Object,Character) | lub(Object,Integer) |
Таблица 15.25-B. Тип условного выражения (Примитивный 3-й операнд, Часть II)
| 3-й → | long | float | double | boolean |
|---|---|---|---|---|
| 2-й ↓ | ||||
byte | bnp(byte,long) | bnp(byte,float) | bnp(byte,double) | lub(Byte,Boolean) |
Byte | bnp(Byte,long) | bnp(Byte,float) | bnp(Byte,double) | lub(Byte,Boolean) |
short | bnp(short,long) | bnp(short,float) | bnp(short,double) | lub(Short,Boolean) |
Short | bnp(Short,long) | bnp(Short,float) | bnp(Short,double) | lub(Short,Boolean) |
char | bnp(char,long) | bnp(char,float) | bnp(char,double) | lub(Character,Boolean) |
Character | bnp(Character,long) | bnp(Character,float) | bnp(Character,double) | lub(Character,Boolean) |
int | bnp(int,long) | bnp(int,float) | bnp(int,double) | lub(Integer,Boolean) |
Integer | bnp(Integer,long) | bnp(Integer,float) | bnp(Integer,double) | lub(Integer,Boolean) |
long | long | bnp(long,float) | bnp(long,double) | lub(Long,Boolean) |
Long | long | bnp(Long,float) | bnp(Long,double) | lub(Long,Boolean) |
float | bnp(float,long) | float | bnp(float,double) | lub(Float,Boolean) |
Float | bnp(Float,long) | float | bnp(Float,double) | lub(Float,Boolean) |
double | bnp(double,long) | bnp(double,float) | double | lub(Double,Boolean) |
Double | bnp(Double,long) | bnp(Double,float) | double | lub(Double,Boolean) |
boolean | lub(Boolean,Long) | lub(Boolean,Float) | lub(Boolean,Double) | boolean |
Boolean | lub(Boolean,Long) | lub(Boolean,Float) | lub(Boolean,Double) | boolean |
null | lub(null,Long) | lub(null,Float) | lub(null,Double) | lub(null,Boolean) |
Object | lub(Object,Long) | lub(Object,Float) | lub(Object,Double) | lub(Object,Boolean) |
Таблица 15.25-C. Тип условного выражения (справочный 3-й операнд, Часть I)
| 3-й → | Byte | Short | Character | Integer |
|---|---|---|---|---|
| 2-й ↓ | ||||
byte | byte | short | bnp(byte,Character) | bnp(byte,Integer) |
Byte | Byte | short | bnp(Byte,Character) | bnp(Byte,Integer) |
short | short | short | bnp(short,Character) | bnp(short,Integer) |
Short | short | Short | bnp(Short,Character) | bnp(Short,Integer) |
char | bnp(char,Byte) | bnp(char,Short) | char | bnp(char,Integer) |
Character | bnp(Character,Byte) | bnp(Character,Short) | Character | bnp(Character,Integer) |
int |
byte | bnp(int,Byte) |
short | bnp(int,Short) |
char | bnp(int,Character) | int |
Integer | bnp(Integer,Byte) | bnp(Integer,Short) | bnp(Integer,Character) | Integer |
long | bnp(long,Byte) | bnp(long,Short) | bnp(long,Character) | bnp(long,Integer) |
Long | bnp(Long,Byte) | bnp(Long,Short) | bnp(Long,Character) | bnp(Long,Integer) |
float | bnp(float,Byte) | bnp(float,Short) | bnp(float,Character) | bnp(float,Integer) |
Float | bnp(Float,Byte) | bnp(Float,Short) | bnp(Float,Character) | bnp(Float,Integer) |
double | bnp(double,Byte) | bnp(double,Short) | bnp(double,Character) | bnp(double,Integer) |
Double | bnp(Double,Byte) | bnp(Double,Short) | bnp(Double,Character) | bnp(Double,Integer) |
boolean | lub(Boolean,Byte) | lub(Boolean,Short) | lub(Boolean,Character) | lub(Boolean,Integer) |
Boolean | lub(Boolean,Byte) | lub(Boolean,Short) | lub(Boolean,Character) | lub(Boolean,Integer) |
null | Byte | Short | Character | Integer |
Object | lub(Object,Byte) | lub(Object,Short) | lub(Object,Character) | lub(Object,Integer) |
Таблица 15.25-D. Тип условного выражения (Ссылка на 3-й операнд, Часть II)
| 3-й → | Long | Float | Double | Boolean |
|---|---|---|---|---|
| 2-й ↓ | ||||
byte | bnp(byte,Long) | bnp(byte,Float) | bnp(byte,Double) | lub(Byte,Boolean) |
Byte | bnp(Byte,Long) | bnp(Byte,Float) | bnp(Byte,Double) | lub(Byte,Boolean) |
short | bnp(short,Long) | bnp(short,Float) | bnp(short,Double) | lub(Short,Boolean) |
Short | bnp(Short,Long) | bnp(Short,Float) | bnp(Short,Double) | lub(Short,Boolean) |
char | bnp(char,Long) | bnp(char,Float) | bnp(char,Double) | lub(Character,Boolean) |
Character | bnp(Character,Long) | bnp(Character,Float) | bnp(Character,Double) | lub(Character,Boolean) |
int | bnp(int,Long) | bnp(int,Float) | bnp(int,Double) | lub(Integer,Boolean) |
Integer | bnp(Integer,Long) | bnp(Integer,Float) | bnp(Integer,Double) | lub(Integer,Boolean) |
long | long | bnp(long,Float) | bnp(long,Double) | lub(Long,Boolean) |
Long | Long | bnp(Long,Float) | bnp(Long,Double) | lub(Long,Boolean) |
float | bnp(float,Long) | float | bnp(float,Double) | lub(Float,Boolean) |
Float | bnp(Float,Long) | Float | bnp(Float,Double) | lub(Float,Boolean) |
double | bnp(double,Long) | bnp(double,Float) | double | lub(Double,Boolean) |
Double | bnp(Double,Long) | bnp(Double,Float) | Double | lub(Double,Boolean) |
boolean | lub(Boolean,Long) | lub(Boolean,Float) | lub(Boolean,Double) | boolean |
Boolean | lub(Boolean,Long) | lub(Boolean,Float) | lub(Boolean,Double) | Boolean |
null | Long | Float | Double | Boolean |
Object | lub(Object,Long) | lub(Object,Float) | lub(Object,Double) | lub(Object,Boolean) |
Таблица 15.25-E. Тип условного выражения (Ссылка на 3-й операнд, Часть III)
| 3-й → | null | Object |
|---|---|---|
| 2-й ↓ | ||
byte | lub(Byte,null) | lub(Byte,Object) |
Byte | Byte | lub(Byte,Object) |
short | lub(Short,null) | lub(Short,Object) |
Short | Short | lub(Short,Object) |
char | lub(Character,null) | lub(Character,Object) |
Character | Character | lub(Character,Object) |
int | lub(Integer,null) | lub(Integer,Object) |
Integer | Integer | lub(Integer,Object) |
long | lub(Long,null) | lub(Long,Object) |
Long | Long | lub(Long,Object) |
float | lub(Float,null) | lub(Float,Object) |
Float | Float | lub(Float,Object) |
double | lub(Double,null) | lub(Double,Object) |
Double | Double | lub(Double,Object) |
boolean | lub(Boolean,null) | lub(Boolean,Object) |
Boolean | Boolean | lub(Boolean,Object) |
null | null | lub(null,Object) |
Object | Object | Object |
Во время выполнения сначала вычисляется выражение первого операнда условного выражения. При необходимости выполняется преобразование unboxing.
Полученное boolean значение используется для выбора либо второго, либо третьего выражения операнда:
-
Если значение первого операнда равно
true, то выбирается выражение второго операнда. -
Если значение первого операнда равно
false, то выбирается выражение третьего операнда.
Затем выбранное выражение операнда вычисляется, а полученное значение преобразуется к типу условного выражения в соответствии с правилами, указанными ниже.
Это преобразование может включать преобразования boxing или unboxing (§5.1.7, §5.1.8).
Выражение операнда, которое не выбрано, не оценивается для данного конкретного вычисления условного выражения.
Условные выражения типа boolean являются самостоятельными выражениями (§15.2).
Тип условного выражения типа boolean определяется следующим образом:
-
Если второй и третий операнды оба имеют тип
Boolean, условное выражение имеет типBoolean. -
В противном случае, условное выражение имеет тип
boolean.
Числовые условные выражения являются самостоятельными выражениями (§15.2).
Тип числового условного выражения определяется следующим образом:
-
Если второй и третий операнды имеют одинаковый тип, то это тип условного выражения.
-
Если один из вторых и третьих операндов является примитивным типом T, а тип другого является результатом применения преобразования boxing (§5.1.7) к T, то тип условного выражения равен T.
-
Если один из операндов имеет тип
byteилиByte, а другой — типshortилиShort, то тип условного выражения равенshort. -
Если один из операндов имеет тип T, где T является
byte,shortилиchar, а другой операнд — константное выражение (§15.28) типаint, значение которого представимо в типе T, то тип условного выражения равен T. -
Если один из операндов имеет тип T, где T является
Byte,ShortилиCharacter, а другой операнд — константное выражение типаint, значение которого представимо в типе U, являющемся результатом применения преобразования unboxing к T, то тип условного выражения равен U. -
В противном случае применяется бинарное числовое продвижение (§5.6.2) к типам операндов, и тип условного выражения — это тип второго и третьего операндов после продвижения.
Обратите внимание, что бинарное числовое продвижение выполняет преобразование множества значений (§5.1.13) и может выполнять преобразование unboxing (§5.1.8).
Ссылочное условное выражение является поливыражением, если оно встречается в контексте присваивания или вызова (§5.2. §5.3). В противном случае, это выражение типа «стоп-выражение».
Когда полисправочное условное выражение появляется в контексте определенного типа с целевым типом T, его второе и третье операндные выражения также появляются в контексте того же типа с целевым типом T.
Тип полисправочного условного выражения такой же, как его целевой тип.
Тип автономного ссылочного условного выражения определяется следующим образом:
-
Если второй и третий операнды имеют один и тот же тип (который может быть типом null), то это тип условного выражения.
-
Если тип одного из вторых и третьих операндов — тип null, а тип другого операнда — ссылочный тип, то тип условного выражения — этот ссылочный тип.
-
В противном случае, второй и третий операнды имеют типы S1 и S2 соответственно. Пусть T1 — тип, полученный в результате применения преобразования boxing к S1, а T2 — тип, полученный в результате применения преобразования boxing к S2. Тип условного выражения — результат применения преобразования capture (§5.1.10) к lub(T1, T2).
Поскольку ссылочные условные выражения могут быть поливыражениями, они могут «передавать» контекст своим операндам. Это позволяет лямбда-выражениям и выражениям ссылки на методы появляться в качестве операндов:
return ... ? (x->x) : (x->-x);
Это также позволяет использовать дополнительную информацию для улучшения проверки типов вызовов генерических методов. До Java SE 8 это присваивание было правильно типизировано:
List<String> ls = Arrays.asList();
но это не было:
List<String> ls = ... ? Arrays.asList() : Arrays.asList("a","b");
Приведенные выше правила позволяют рассматривать оба присваивания как правильно типизированные.
Обратите внимание, что ссылочное условное выражение не обязательно содержит поливыражение в качестве операнда, чтобы быть поливыражением. Это поливыражение просто в силу контекста, в котором оно появляется. Например, в следующем коде условное выражение является поливыражением, а каждый операнд рассматривается как находящийся в контексте присваивания, нацеленного на Class:<? super
Integer>
Class<? super Integer> choose(boolean b,
Class<Integer> c1,
Class<Number> c2) {
return b ? c1 : c2;
}
Если бы условное выражение не было поливыражением, произошла бы ошибка времени компиляции, так как его тип был бы lub(Class, <Integer>Class) = <Number>Class, что несовместимо с типом возвращаемого значения <? extends Number>choose.
Существует 12 операторов присваивания; все они синтаксически правоассоциативны (они группируются справа налево). Таким образом, a=b=c означает a=(b=c), что присваивает значение c переменной b, а затем присваивает значение b переменной a.
=*=/=%=+=-=<<=>>=>>>=&=^=|=
Результат первого операнда оператора присваивания должен быть переменной, в противном случае возникает ошибка времени компиляции.
Этот операнд может быть именованной переменной, например, локальной переменной или полем текущего объекта или класса, или это может быть вычисленная переменная, которая может получиться в результате доступа к полю (§15.11) или доступа к массиву (§15.10.3).
Тип выражения присваивания — это тип переменной после преобразования capture (§5.1.10).
Во время выполнения результат выражения присваивания — это значение переменной после выполнения присваивания. Результат выражения присваивания не является переменной.
Переменная, которая объявлена final, не может быть присвоена (если только она не явно не инициализирована (§16 (Определённость присваивания))), потому что при использовании доступа к такой final переменной в качестве выражения результатом является значение, а не переменная, поэтому ее нельзя использовать в качестве первого операнда оператора присваивания.
Ошибка времени компиляции возникает, если тип правого операнда не может быть преобразован к типу переменной посредством преобразования присваивания (§5.2).
Во время выполнения выражение оценивается одним из трех способов.
Если выражение левого операнда является выражением доступа к полю e.f (§15.11), возможно, заключенное в одну или несколько пар скобок, то:
-
Сначала вычисляется выражение
e. Если вычислениеeзавершается прерывисто, выражение присваивания завершается прерывисто по той же причине. -
Затем вычисляется правый операнд. Если вычисление правого выражения завершается прерывисто, выражение присваивания завершается прерывисто по той же причине.
-
Затем, если поле, обозначенное
e.f, не являетсяstatic, и результат вычисленияeвыше являетсяnull, то выбрасываетсяNullPointerException. -
В противном случае переменной, обозначенной
e.f, присваивается значение правого операнда, вычисленное выше.
Если левый операнд является выражением доступа к элементу массива (§15.10.3), возможно, заключенное в одну или несколько пар скобок, то:
-
Сначала вычисляется подвыражение ссылки на массив выражения доступа к элементу массива левого операнда. Если это вычисление завершается прерывисто, то выражение присваивания завершается прерывисто по той же причине; подвыражение индекса (выражения доступа к элементу массива левого операнда) и правый операнд не вычисляются, и присваивание не происходит.
-
В противном случае вычисляется подвыражение индекса выражения доступа к элементу массива левого операнда. Если это вычисление завершается прерывисто, то выражение присваивания завершается прерывисто по той же причине, и правый операнд не вычисляется, и присваивание не происходит.
-
В противном случае вычисляется правый операнд. Если это вычисление завершается прерывисто, то выражение присваивания завершается прерывисто по той же причине, и присваивание не происходит.
-
В противном случае, если значение подвыражения ссылки на массив является
null, то присваивание не происходит, и выбрасываетсяNullPointerException. -
В противном случае, значение подвыражения ссылки на массив действительно ссылается на массив. Если значение подвыражения индекса меньше нуля или больше или равно
lengthмассива, то присваивание не происходит, и выбрасываетсяArrayIndexOutOfBoundsException. -
В противном случае значение подвыражения индекса используется для выбора компонента массива, на который ссылается значение подвыражения ссылки на массив.
Этот компонент является переменной; назовем его тип SC. Также пусть TC будет типом левого операнда оператора присваивания, определенного во время компиляции. Тогда существует два варианта:
-
Если TC — примитивный тип, то SC обязательно совпадает с TC.
Значение правого операнда преобразуется к типу выбранного компонента массива, подвергается преобразованию набора значений (§5.1.13) к соответствующему стандартному набору значений (не набору значений с расширенным порядком), и результат преобразования сохраняется в компоненте массива.
-
Если TC — тип ссылки, то SC может не совпадать с TC, а скорее типом, который расширяет или реализует TC.
Пусть RC — класс объекта, на который ссылается значение правого операнда во время выполнения.
Компилятор Java может доказать во время компиляции, что компонент массива будет иметь тип TC точно (например, TC может быть
final). Но если компилятор Java не может доказать во время компиляции, что компонент массива будет иметь тип TC точно, то проверка должна быть выполнена во время выполнения, чтобы убедиться, что класс RC совместим с присваиванием (§5.2) с фактическим типом SC компонента массива.Эта проверка аналогична сужению приведения (§5.5, §15.16), за исключением того, что если проверка завершается неудачно, выбрасывается
ArrayStoreException, а неClassCastException.Если класс RC не может быть присвоен типу SC, то присваивание не выполняется, и выбрасывается
ArrayStoreException.В противном случае значение ссылки правого операнда сохраняется в выбранном компоненте массива.
-
В противном случае требуются три шага:
-
Сначала левый операнд вычисляется для получения переменной. Если это вычисление завершается прерывисто, то выражение присваивания завершается прерывисто по той же причине; правый операнд не вычисляется, и присваивание не происходит.
-
В противном случае вычисляется правый операнд. Если это вычисление завершается прерывисто, то выражение присваивания завершается прерывисто по той же причине, и присваивание не происходит.
-
В противном случае значение правого операнда преобразуется к типу переменной левого операнда, подвергается преобразованию набора значений (§5.1.13) к соответствующему стандартному набору значений (не набору значений с расширенным порядком), и результат преобразования сохраняется в переменной.
Пример 15.26.1-1. Простое присваивание компоненту массива
class ArrayReferenceThrow extends RuntimeException { }
class IndexThrow extends RuntimeException { }
class RightHandSideThrow extends RuntimeException { }
class IllustrateSimpleArrayAssignment {
static Object[] objects = { new Object(), new Object() };
static Thread[] threads = { new Thread(), new Thread() };
static Object[] arrayThrow() {
throw new ArrayReferenceThrow();
}
static int indexThrow() {
throw new IndexThrow();
}
static Thread rightThrow() {
throw new RightHandSideThrow();
}
static String name(Object q) {
String sq = q.getClass().getName();
int k = sq.lastIndexOf('.');
return (k < 0) ? sq : sq.substring(k+1);
}
static void testFour(Object[] x, int j, Object y) {
String sx = x == null ? "null" : name(x[0]) + "s";
String sy = name(y);
System.out.println();
try {
System.out.print(sx + "[throw]=throw => ");
x[indexThrow()] = rightThrow();
System.out.println("Okay!");
} catch (Throwable e) { System.out.println(name(e)); }
try {
System.out.print(sx + "[throw]=" + sy + " => ");
x[indexThrow()] = y;
System.out.println("Okay!");
} catch (Throwable e) { System.out.println(name(e)); }
try {
System.out.print(sx + "[" + j + "]=throw => ");
x[j] = rightThrow();
System.out.println("Okay!");
} catch (Throwable e) { System.out.println(name(e)); }
try {
System.out.print(sx + "[" + j + "]=" + sy + " => ");
x[j] = y;
System.out.println("Okay!");
} catch (Throwable e) { System.out.println(name(e)); }
}
public static void main(String[] args) {
try {
System.out.print("throw[throw]=throw => ");
arrayThrow()[indexThrow()] = rightThrow();
System.out.println("Okay!");
} catch (Throwable e) { System.out.println(name(e)); }
try {
System.out.print("throw[throw]=Thread => ");
arrayThrow()[indexThrow()] = new Thread();
System.out.println("Okay!");
} catch (Throwable e) { System.out.println(name(e)); }
try {
System.out.print("throw[1]=throw => ");
arrayThrow()[1] = rightThrow();
System.out.println("Okay!");
} catch (Throwable e) { System.out.println(name(e)); }
try {
System.out.print("throw[1]=Thread => ");
arrayThrow()[1] = new Thread();
System.out.println("Okay!");
} catch (Throwable e) { System.out.println(name(e)); }
testFour(null, 1, new StringBuffer());
testFour(null, 9, new Thread());
testFour(objects, 1, new StringBuffer());
testFour(objects, 1, new Thread());
testFour(objects, 9, new StringBuffer());
testFour(objects, 9, new Thread());
testFour(threads, 1, new StringBuffer());
testFour(threads, 1, new Thread());
testFour(threads, 9, new StringBuffer());
testFour(threads, 9, new Thread());
}
}
Эта программа выводит:
throw[throw]=throw => ArrayReferenceThrow throw[throw]=Thread => ArrayReferenceThrow throw[1]=throw => ArrayReferenceThrow throw[1]=Thread => ArrayReferenceThrow null[throw]=throw => IndexThrow null[throw]=StringBuffer => IndexThrow null[1]=throw => RightHandSideThrow null[1]=StringBuffer => NullPointerException null[throw]=throw => IndexThrow null[throw]=Thread => IndexThrow null[9]=throw => RightHandSideThrow null[9]=Thread => NullPointerException Objects[throw]=throw => IndexThrow Objects[throw]=StringBuffer => IndexThrow Objects[1]=throw => RightHandSideThrow Objects[1]=StringBuffer => Okay! Objects[throw]=throw => IndexThrow Objects[throw]=Thread => IndexThrow Objects[1]=throw => RightHandSideThrow Objects[1]=Thread => Okay! Objects[throw]=throw => IndexThrow Objects[throw]=StringBuffer => IndexThrow Objects[9]=throw => RightHandSideThrow Objects[9]=StringBuffer => ArrayIndexOutOfBoundsException Objects[throw]=throw => IndexThrow Objects[throw]=Thread => IndexThrow Objects[9]=throw => RightHandSideThrow Objects[9]=Thread => ArrayIndexOutOfBoundsException Threads[throw]=throw => IndexThrow Threads[throw]=StringBuffer => IndexThrow Threads[1]=throw => RightHandSideThrow Threads[1]=StringBuffer => ArrayStoreException Threads[throw]=throw => IndexThrow Threads[throw]=Thread => IndexThrow Threads[1]=throw => RightHandSideThrow Threads[1]=Thread => Okay! Threads[throw]=throw => IndexThrow Threads[throw]=StringBuffer => IndexThrow Threads[9]=throw => RightHandSideThrow Threads[9]=StringBuffer => ArrayIndexOutOfBoundsException Threads[throw]=throw => IndexThrow Threads[throw]=Thread => IndexThrow Threads[9]=throw => RightHandSideThrow Threads[9]=Thread => ArrayIndexOutOfBoundsException
Наиболее интересный случай из списка - тринадцатый с конца:
Threads[1]=StringBuffer => ArrayStoreException
что указывает на то, что попытка сохранить ссылку на StringBuffer в массив, компоненты которого имеют тип Thread, вызывает ошибку ArrayStoreException. Код правильно типизирован во время компиляции: присваивание имеет левый операнд типа Object[] и правый операнд типа Object. Во время выполнения, первый фактический аргумент метода testFour - ссылка на экземпляр "массива Thread", а третий фактический аргумент - ссылка на экземпляр класса StringBuffer.
Составное выражение присваивания вида E1 op=
E2 эквивалентно выражению E1 = (T) ((E1) op
(E2)), где T — тип E1, за исключением того, что E1 вычисляется только один раз.
Например, следующий код корректен:
short x = 3; x += 4.6;
и приводит к тому, что x имеет значение 7, поскольку оно эквивалентно:
short x = 3; x = (short)(x + 4.6);
Во время выполнения выражение оценивается одним из двух способов.
Если левостороннее операндное выражение не является выражением доступа к массиву, то:
-
Сначала левосторонний операнд оценивается для получения переменной. Если эта оценка завершается внезапно, то выражение присваивания завершается внезапно по той же причине; правосторонний операнд не оценивается, и присваивание не происходит.
-
В противном случае сохраненное значение левостороннего операнда сохраняется, а затем оценивается правосторонний операнд. Если эта оценка завершается внезапно, то выражение присваивания завершается внезапно по той же причине, и присваивание не происходит.
-
В противном случае сохраненное значение левосторонней переменной и значение правостороннего операнда используются для выполнения двоичной операции, указанной составным оператором присваивания. Если эта операция завершается внезапно, то выражение присваивания завершается внезапно по той же причине и присваивание не происходит.
-
В противном случае результат двоичной операции преобразуется к типу левосторонней переменной, подвергается преобразованию множества значений (§5.1.13) к соответствующему стандартному множеству значений (не расширенному множеству значений с плавающей точкой), и результат преобразования сохраняется в переменной.
Если левостороннее операндное выражение является выражением доступа к массиву (§15.10.3), то:
-
Сначала оценивается подвыражение ссылки на массив левостороннего операндного выражения доступа к массиву. Если эта оценка завершается внезапно, то выражение присваивания завершается внезапно по той же причине; индексное подвыражение (левостороннего операндного выражения доступа к массиву) и правосторонний операнд не оцениваются, и присваивание не происходит.
-
В противном случае оценивается индексное подвыражение левостороннего операндного выражения доступа к массиву. Если эта оценка завершается внезапно, то выражение присваивания завершается внезапно по той же причине, и правосторонний операнд не оценивается, и присваивание не происходит.
-
В противном случае, если значение подвыражения ссылки на массив равно
null, то присваивание не происходит, и выбрасываетсяNullPointerException. -
В противном случае, значение подвыражения ссылки на массив действительно ссылается на массив. Если значение индексного подвыражения меньше нуля или больше или равно размеру
lengthмассива, то присваивание не происходит, и выбрасываетсяArrayIndexOutOfBoundsException. -
В противном случае значение индексного подвыражения используется для выбора компонента массива, на который ссылается значение подвыражения ссылки на массив. Значение этого компонента сохраняется, а затем оценивается правосторонний операнд. Если эта оценка завершается внезапно, то выражение присваивания завершается внезапно по той же причине, и присваивание не происходит.
Для простого оператора присваивания оценка правостороннего операнда происходит до проверок подвыражения ссылки на массив и индексного подвыражения, но для составного оператора присваивания оценка правостороннего операнда происходит после этих проверок.
-
В противном случае рассмотрите компонент массива, выбранный на предыдущем шаге, значение которого было сохранено. Этот компонент является переменной; назовите его тип S. Также пусть T будет типом левостороннего операнда оператора присваивания, определённого во время компиляции.
-
Если T — примитивный тип, то S обязательно совпадает с T.
Сохраненное значение компонента массива и значение правостороннего операнда используются для выполнения двоичной операции, указанной составным оператором присваивания.
Если эта операция завершается внезапно (единственная возможность — целочисленное деление на ноль — см. §15.17.2), то выражение присваивания завершается внезапно по той же причине, и присваивание не происходит.
В противном случае результат двоичной операции преобразуется к типу выбранного компонента массива, подвергается преобразованию множества значений (§5.1.13) к соответствующему стандартному множеству значений (не расширенному множеству значений с плавающей точкой), и результат преобразования сохраняется в компоненте массива.
-
Если T — тип ссылки, то он должен быть
String. Поскольку классString— это классfinal, S также должен бытьString.Поэтому проверка во время выполнения, которая иногда требуется для простого оператора присваивания, никогда не требуется для составного оператора присваивания.
Сохраненное значение компонента массива и значение правостороннего операнда используются для выполнения двоичной операции (конкатенации строк), указанной составным оператором присваивания (который обязательно является
+=). Если эта операция завершается внезапно, то выражение присваивания завершается внезапно по той же причине и присваивание не происходит.В противном случае
Stringрезультат двоичной операции сохраняется в компоненте массива.
-
Пример 15.26.2-1. Составное присваивание компоненту массива
class ArrayReferenceThrow extends RuntimeException { }
class IndexThrow extends RuntimeException { }
class RightHandSideThrow extends RuntimeException { }
class IllustrateCompoundArrayAssignment {
static String[] strings = { "Simon", "Garfunkel" };
static double[] doubles = { Math.E, Math.PI };
static String[] stringsThrow() {
throw new ArrayReferenceThrow();
}
static double[] doublesThrow() {
throw new ArrayReferenceThrow();
}
static int indexThrow() {
throw new IndexThrow();
}
static String stringThrow() {
throw new RightHandSideThrow();
}
static double doubleThrow() {
throw new RightHandSideThrow();
}
static String name(Object q) {
String sq = q.getClass().getName();
int k = sq.lastIndexOf('.');
return (k < 0) ? sq : sq.substring(k+1);
}
static void testEight(String[] x, double[] z, int j) {
String sx = (x == null) ? "null" : "Strings";
String sz = (z == null) ? "null" : "doubles";
System.out.println();
try {
System.out.print(sx + "[throw]+=throw => ");
x[indexThrow()] += stringThrow();
System.out.println("Okay!");
} catch (Throwable e) { System.out.println(name(e)); }
try {
System.out.print(sz + "[throw]+=throw => ");
z[indexThrow()] += doubleThrow();
System.out.println("Okay!");
} catch (Throwable e) { System.out.println(name(e)); }
try {
System.out.print(sx + "[throw]+=\"heh\" => ");
x[indexThrow()] += "heh";
System.out.println("Okay!");
} catch (Throwable e) { System.out.println(name(e)); }
try {
System.out.print(sz + "[throw]+=12345 => ");
z[indexThrow()] += 12345;
System.out.println("Okay!");
} catch (Throwable e) { System.out.println(name(e)); }
try {
System.out.print(sx + "[" + j + "]+=throw => ");
x[j] += stringThrow();
System.out.println("Okay!");
} catch (Throwable e) { System.out.println(name(e)); }
try {
System.out.print(sz + "[" + j + "]+=throw => ");
z[j] += doubleThrow();
System.out.println("Okay!");
} catch (Throwable e) { System.out.println(name(e)); }
try {
System.out.print(sx + "[" + j + "]+=\"heh\" => ");
x[j] += "heh";
System.out.println("Okay!");
} catch (Throwable e) { System.out.println(name(e)); }
try {
System.out.print(sz + "[" + j + "]+=12345 => ");
z[j] += 12345;
System.out.println("Okay!");
} catch (Throwable e) { System.out.println(name(e)); }
}
public static void main(String[] args) {
try {
System.out.print("throw[throw]+=throw => ");
stringsThrow()[indexThrow()] += stringThrow();
System.out.println("Okay!");
} catch (Throwable e) { System.out.println(name(e)); }
try {
System.out.print("throw[throw]+=throw => ");
doublesThrow()[indexThrow()] += doubleThrow();
System.out.println("Okay!");
} catch (Throwable e) { System.out.println(name(e)); }
try {
System.out.print("throw[throw]+=\"heh\" => ");
stringsThrow()[indexThrow()] += "heh";
System.out.println("Okay!");
} catch (Throwable e) { System.out.println(name(e)); }
try {
System.out.print("throw[throw]+=12345 => ");
doublesThrow()[indexThrow()] += 12345;
System.out.println("Okay!");
} catch (Throwable e) { System.out.println(name(e)); }
try {
System.out.print("throw[1]+=throw => ");
stringsThrow()[1] += stringThrow();
System.out.println("Okay!");
} catch (Throwable e) { System.out.println(name(e)); }
try {
System.out.print("throw[1]+=throw => ");
doublesThrow()[1] += doubleThrow();
System.out.println("Okay!");
} catch (Throwable e) { System.out.println(name(e)); }
try {
System.out.print("throw[1]+=\"heh\" => ");
stringsThrow()[1] += "heh";
System.out.println("Okay!");
} catch (Throwable e) { System.out.println(name(e)); }
try {
System.out.print("throw[1]+=12345 => ");
doublesThrow()[1] += 12345;
System.out.println("Okay!");
} catch (Throwable e) { System.out.println(name(e)); }
testEight(null, null, 1);
testEight(null, null, 9);
testEight(strings, doubles, 1);
testEight(strings, doubles, 9);
}
}
Эта программа выводит:
throw[throw]+=throw => ArrayReferenceThrow throw[throw]+=throw => ArrayReferenceThrow throw[throw]+="heh" => ArrayReferenceThrow throw[throw]+=12345 => ArrayReferenceThrow throw[1]+=throw => ArrayReferenceThrow throw[1]+=throw => ArrayReferenceThrow throw[1]+="heh" => ArrayReferenceThrow throw[1]+=12345 => ArrayReferenceThrow null[throw]+=throw => IndexThrow null[throw]+=throw => IndexThrow null[throw]+="heh" => IndexThrow null[throw]+=12345 => IndexThrow null[1]+=throw => NullPointerException null[1]+=throw => NullPointerException null[1]+="heh" => NullPointerException null[1]+=12345 => NullPointerException null[throw]+=throw => IndexThrow null[throw]+=throw => IndexThrow null[throw]+="heh" => IndexThrow null[throw]+=12345 => IndexThrow null[9]+=throw => NullPointerException null[9]+=throw => NullPointerException null[9]+="heh" => NullPointerException null[9]+=12345 => NullPointerException Strings[throw]+=throw => IndexThrow doubles[throw]+=throw => IndexThrow Strings[throw]+="heh" => IndexThrow doubles[throw]+=12345 => IndexThrow Strings[1]+=throw => RightHandSideThrow doubles[1]+=throw => RightHandSideThrow Strings[1]+="heh" => Okay! doubles[1]+=12345 => Okay! Strings[throw]+=throw => IndexThrow doubles[throw]+=throw => IndexThrow Strings[throw]+="heh" => IndexThrow doubles[throw]+=12345 => IndexThrow Strings[9]+=throw => ArrayIndexOutOfBoundsException doubles[9]+=throw => ArrayIndexOutOfBoundsException Strings[9]+="heh" => ArrayIndexOutOfBoundsException doubles[9]+=12345 => ArrayIndexOutOfBoundsException
Наиболее интересные случаи из них — одиннадцатый и двенадцатый с конца:
Strings[1]+=throw => RightHandSideThrow doubles[1]+=throw => RightHandSideThrow
Это те случаи, когда правосторонняя часть, выбрасывающая исключение, действительно выбрасывает исключение; кроме того, это единственные такие случаи в этом списке. Это демонстрирует, что оценка правостороннего операнда действительно происходит после проверок на значение ссылки на массив null и выход за пределы индекса.
Пример 15.26.2-2. Значение левосторонней части составного присваивания сохраняется до оценки правосторонней части
class Test {
public static void main(String[] args) {
int k = 1;
int[] a = { 1 };
k += (k = 4) * (k + 2);
a[0] += (a[0] = 4) * (a[0] + 2);
System.out.println("k==" + k + " and a[0]==" + a[0]);
}
}
Эта программа выводит:
k==25 and a[0]==25
Значение 1 от k сохраняется составным оператором присваивания += до оценки его правостороннего операнда (k = 4) * (k + 2). Затем оценка этого правостороннего операнда присваивает 4 в k, вычисляет значение 6 для k + 2 и затем умножает 4 на 6, чтобы получить 24. Это добавляется к сохранённому значению 1, чтобы получить 25, которое затем сохраняется в k оператором +=. Аналогичный анализ применим к случаю с использованием a[0].
Короче говоря, операторы:
k += (k = 4) * (k + 2); a[0] += (a[0] = 4) * (a[0] + 2);
ведут себя точно так же, как операторы:
k = k + (k = 4) * (k + 2); a[0] = a[0] + (a[0] = 4) * (a[0] + 2);
Lambda-выражение похоже на метод: оно содержит список формальных параметров и тело — выражение или блок кода — выраженное в терминах этих параметров.
Lambda-выражения всегда являются поливыражениями (§15.2).
Если lambda-выражение используется в программе не в контексте присваивания (§5.2), вызова (§5.3) или приведения типов (§5.5), это синтаксическая ошибка.
Выполнение lambda-выражения приводит к созданию экземпляра функционального интерфейса (§9.8). Выполнение lambda-выражения не вызывает выполнение тела выражения; это может произойти позже, при вызове соответствующего метода функционального интерфейса.
Вот несколько примеров lambda-выражений:
() -> {} // No parameters; result is void
() -> 42 // No parameters, expression body
() -> null // No parameters, expression body
() -> { return 42; } // No parameters, block body with return
() -> { System.gc(); } // No parameters, void block body
() -> { // Complex block body with returns
if (true) return 12;
else {
int result = 15;
for (int i = 1; i < 10; i++)
result *= i;
return result;
}
}
(int x) -> x+1 // Single declared-type parameter
(int x) -> { return x+1; } // Single declared-type parameter
(x) -> x+1 // Single inferred-type parameter
x -> x+1 // Parentheses optional for
// single inferred-type parameter
(String s) -> s.length() // Single declared-type parameter
(Thread t) -> { t.start(); } // Single declared-type parameter
s -> s.length() // Single inferred-type parameter
t -> { t.start(); } // Single inferred-type parameter
(int x, int y) -> x+y // Multiple declared-type parameters
(x, y) -> x+y // Multiple inferred-type parameters
(x, int y) -> x+y // Illegal: can't mix inferred and declared types
(x, final y) -> x+y // Illegal: no modifiers with inferred types
Данный синтаксис имеет преимущество в минимизации скобок вокруг простых lambda-выражений, что особенно полезно, когда lambda-выражение является аргументом метода или когда тело представляет собой другое lambda-выражение. Он также чётко различает выражения и операторы, что предотвращает неоднозначности и чрезмерную зависимость от маркеров ';'. Когда для визуального выделения всего lambda-выражения или тела выражения необходимы дополнительные скобки, поддерживаются круглые скобки (точно так же, как и в других случаях, когда порядок операций не ясен).
Синтаксис имеет некоторые проблемы с синтаксическим анализом. Язык программирования Java всегда требовал произвольного предвосхищения, чтобы различать типы и выражения после маркера '(': то, что следует за ним, может быть приведением типа или выражением в скобках. Это ухудшилось с появлением дженериков, которые повторно использовали бинарные операторы '<' и '>' в типах. Lambda-выражения вводят новую возможность: токены, следующие за '(', могут описывать тип, выражение или список параметров lambda. Некоторые токены сразу указывают на список параметров (аннотации, final); в других случаях существуют определённые шаблоны, которые должны интерпретироваться как списки параметров (два имени подряд, ',', не вложенное внутри '<' и '>'); и иногда решение не может быть принято до тех пор, пока не встретится '->' после ')'. Наиболее простой способ эффективного синтаксического анализа — это использование автомата состояний: каждое состояние представляет подмножество возможных интерпретаций (тип, выражение или параметры), и когда автомат переходит в состояние, в котором множество является единственным, парсер знает, какой случай перед ним.
Нет специальной нульарной формы: lambda-выражение с нулевыми аргументами выражается как () . Очевидный специальный случай синтаксиса, ->
..., не работает, потому что он вводит неоднозначность между списками аргументов и приведениями типов: ->
...(x) .->
...
Lambda-выражения не могут объявлять параметры типов. Хотя это было бы логично с точки зрения семантики, естественный синтаксис (список параметров типов перед списком параметров) приводит к запутанным неоднозначностям. Например, рассмотрите:
foo( (x) < y , z > (w) -> v )
Это может быть вызов foo с одним аргументом (дженерическое lambda-приведение к типу x), или это может быть вызов foo с двумя аргументами, оба результатами сравнений, второе сравнивает z с lambda-выражением. (Строго говоря, lambda-выражение бессмысленно как операнд бинарного оператора >, но это слишком тонкое предположение для построения грамматики.)
Существует прецедент разрешения неоднозначностей, связанных с приведениями типов, который по существу запрещает использование - и + после приведения типов не примитивных типов (§15.15), но расширение этого подхода к дженерикам lambda потребовало бы существенных изменений в грамматике.
Формальные параметры lambda-выражения могут иметь либо объявленные типы, либо типы, определяемые по умолчанию. Эти стили не могут быть смешаны: lambda-выражение не может объявлять типы некоторых параметров, а оставлять другие для вывода. Только параметры с объявленными типами могут иметь модификаторы.
Приведены следующие правила из §4.3, §8.3 и §8.4.1 для удобства:
Параметры получателя не разрешены в FormalParameters lambda-выражения, как указано в §8.4.1.
Lambda-выражение, формальные параметры которого имеют объявленные типы, называется явным типом, а lambda-выражение, формальные параметры которого имеют типы, определяемые по умолчанию, называется неявным типом. Lambda-выражение с нулевыми параметрами является явным типом.
Если формальные параметры имеют типы, определяемые по умолчанию, то эти типы выводятся (§15.27.3) из типа функционального интерфейса, на который направлено lambda-выражение.
Синтаксис формальных параметров с объявленными типами такой же, как и синтаксис параметров объявления метода (§8.4.1).
Объявленный тип формального параметра зависит от того, является ли он параметром переменной арности:
-
Если формальный параметр не является параметром переменной арности, то объявленный тип обозначается UnannType, если в UnannType и VariableDeclaratorId нет пар скобок, и определяется в §10.2 в противном случае.
-
Если формальный параметр является параметром переменной арности, то объявленный тип определяется в §10.2. (Обратите внимание, что «смешанный формат» не разрешен для параметров переменной арности.)
Никаких различий нет между следующими списками параметров lambda-выражения:
(int... x)->.. (int[] x)->..
В соответствии с правилами переопределения, можно использовать любой вариант, независимо от того, является ли абстрактный метод функционального интерфейса параметром фиксированной или переменной арности. Поскольку lambda-выражения никогда не вызываются напрямую, введение int..., где функциональный интерфейс использует int[], не может повлиять на окружающую программу. В теле lambda-выражения параметр переменной арности обрабатывается так же, как параметр типа массива.
Правила применения модификаторов аннотаций к объявлению формального параметра указаны в §9.7.4 и §9.7.5.
Если final появляется более одного раза как модификатор объявления формального параметра, это ошибка компиляции.
Ошибка компиляции возникает при использовании смешанного обозначения массивов (§10.2) для параметра переменной арности.
Область действия и скрытие объявления формального параметра указаны в §6.3 и §6.4.
Ошибка компиляции, если lambda-выражение объявляет два формальных параметра с одинаковым именем. (То есть, в их объявлениях упоминается один и тот же Идентификатор.)
Ошибка компиляции, если параметр lambda имеет имя _ (то есть, одиночный символ подчеркивания).
Использование имени переменной _ в любом контексте не рекомендуется. Будущие версии Java могут зарезервировать это имя как ключевое слово и/или присвоить ему специальную семантику.
Ошибка компиляции, если параметр получателя (§8.4.1) появляется в FormalParameters lambda-выражения.
Ошибка компиляции, если формальный параметр, объявленный final, присваивается внутри тела lambda-выражения.
При вызове lambda-выражения (через выражение вызова метода (§15.12)), значения выражений фактических аргументов инициализируют только что созданные переменные параметров, каждый из которых имеет объявленный или выведенный тип, перед выполнением тела lambda-выражения. Идентификатор, который появляется в VariableDeclaratorId или InferredFormalParameterList, может использоваться как простое имя в теле lambda-выражения для ссылки на формальный параметр.
Параметр lambda-выражения типа float всегда содержит элемент множества значений с плавающей точкой (§4.2.3); аналогично, параметр lambda-выражения типа double всегда содержит элемент множества значений с двойной точностью. Не разрешается, чтобы параметр lambda-выражения типа float содержал элемент множества расширенных экспонент с плавающей точкой, который не является элементом множества значений с плавающей точкой, также как и параметр типа double, который содержит элемент множества расширенных экспонент с двойной точностью, который не является элементом множества значений с двойной точностью.
Когда типы параметров lambda-выражения выводятся, одно и то же тело lambda-выражения можно интерпретировать по-разному в зависимости от контекста, в котором оно появляется. В частности, типы выражений в теле, исключения, выбрасываемые телом, и корректность кода в теле зависят от выведенных типов параметров. Это подразумевает, что вывод типов параметров должен происходить «до» попытки проверки типа тела lambda-выражения.
Тело лямбда-выражения представляет собой либо одно выражение, либо блок (§14.2). Как и тело метода, тело лямбда-выражения описывает код, который будет выполнен при каждом вызове.
В отличие от кода, присутствующего в объявлениях анонимных классов, смысл имен и ключевых слов this и super, появляющихся в теле лямбда-выражения, а также доступность ссылающихся объявлений, совпадают со значениями в окружающем контексте (за исключением того, что параметры лямбда-выражения вводят новые имена).
Прозрачность this (как явного, так и неявного) в теле лямбда-выражения — то есть, её обработка так же, как и в окружающем контексте — обеспечивает большую гибкость для реализации и предотвращает зависимость смысла неквалифицированных имен в теле от разрешения перегрузки.
Практически говоря, лямбда-выражению редко требуется ссылаться на себя (как для рекурсивного вызова, так и для вызова других методов), в то время как чаще требуется использование имен для ссылки на элементы в окружающем классе, которые иначе были бы скрыты (this, toString()). Если для лямбда-выражения необходимо ссылаться на себя (как будто через this), следует использовать ссылку на метод или анонимный внутренний класс.
Блочное тело лямбда-выражения является совместимым с void, если каждая инструкция return в блоке имеет вид return;.
Блочное тело лямбда-выражения является совместимым со значением, если оно не может завершиться нормально (§14.21) и каждая инструкция return в блоке имеет вид return Выражение;.
Если блочное тело лямбда-выражения не является ни совместимым с void, ни совместимым со значением, то это ошибка компиляции.
В блочном теле лямбда-выражения, совместимом со значением, выражения результата — это любые выражения, которые могут произвести значение вызова. В частности, для каждой инструкции вида return Выражение ;, содержащейся в теле, Выражение является выражением результата.
Следующие тела лямбда-выражений совместимы с void:
()->{} ()->{ System.out.println("done"); }
Эти тела лямбда-выражений совместимы со значением:
()->{ return "done"; } ()->{ if (...) return 1; else return 0; }
Эти тела лямбда-выражений оба:
()->{ throw new RuntimeException(); } ()->{ while (true); }
Это тело лямбда-выражения не является ни тем, ни другим:
() -> { if (...) return "done"; System.out.println("done"); }
Обработка совместимости с void/значением и смысл имен в теле совместно служат для минимизации зависимости от конкретного целевого типа в данном контексте, что полезно как для реализации, так и для понимания программистом. В то время как выражения могут иметь разные типы во время разрешения перегрузки в зависимости от целевого типа, смысл неквалифицированных имен и базовая структура тела лямбда-выражения не изменяются.
Обратите внимание, что определение совместимости с void/значением не является строго структурным свойством: «может завершиться нормально» зависит от значений константных выражений, которые могут включать имена, ссылающиеся на константные переменные.
Любая локальная переменная, формальный параметр или параметр исключения, используемые, но не объявленные в лямбда-выражении, должны быть либо объявлены final, либо являться фактически final (§4.12.4), иначе происходит ошибка компиляции в месте использования.
Любая локальная переменная, используемая, но не объявленная в теле лямбда-выражения, должна быть однозначно присвоена (§16 (Однозначное присваивание)) до тела лямбда-выражения, иначе произойдёт ошибка компиляции.
Аналогичные правила использования переменных применяются в теле внутреннего класса (§8.1.3). Ограничение на фактически final переменные препятствует доступу к динамически изменяющимся локальным переменным, чьё захват, скорее всего, введёт проблемы с параллельностью. По сравнению с ограничением final, оно снижает бюрократическую нагрузку на программистов.
Ограничение на фактически final переменные включает стандартные переменные циклов, но не переменные усовершенствованных циклов for, которые обрабатываются как отдельные для каждого шага цикла (§14.14.2).
Следующие тела лямбда-выражений демонстрируют использование фактически final переменных.
void m1(int x) {
int y = 1;
foo(() -> x+y);
// Legal: x and y are both effectively final.
}
void m2(int x) {
int y;
y = 1;
foo(() -> x+y);
// Legal: x and y are both effectively final.
}
void m3(int x) {
int y;
if (...) y = 1;
foo(() -> x+y);
// Illegal: y is effectively final, but not definitely assigned.
}
void m4(int x) {
int y;
if (...) y = 1; else y = 2;
foo(() -> x+y);
// Legal: x and y are both effectively final.
}
void m5(int x) {
int y;
if (...) y = 1;
y = 2;
foo(() -> x+y);
// Illegal: y is not effectively final.
}
void m6(int x) {
foo(() -> x+1);
x++;
// Illegal: x is not effectively final.
}
void m7(int x) {
foo(() -> x=1);
// Illegal: x is not effectively final.
}
void m8() {
int y;
foo(() -> y=1);
// Illegal: y is not definitely assigned before the lambda.
}
void m9(String[] arr) {
for (String s : arr) {
foo(() -> s);
// Legal: s is effectively final
// (it is a new variable on each iteration)
}
}
void m10(String[] arr) {
for (int i = 0; i < arr.length; i++) {
foo(() -> arr[i]);
// Illegal: i is not effectively final
// (it is not final, and is incremented)
}
}
Лямбда-выражение совместимо в контексте присваивания, вызова или приведения типов с целевым типом T, если T — функциональный интерфейс (§9.8) и выражение совместимо с типом функции базового целевого типа, полученного из T.
Базовый целевой тип выводится из T следующим образом:
-
Если T — функциональный интерфейс с параметрами-подстановочными символами, и лямбда-выражение явно типизировано, то базовый целевой тип определяется, как описано в §18.5.3.
-
Если T — функциональный интерфейс с параметрами-подстановочными символами, и лямбда-выражение неявно типизировано, то базовый целевой тип — это параметризация T без подстановочных символов (§9.9).
-
В противном случае, базовый целевой тип — T.
Лямбда-выражение совместимо с типом функции, если выполняются все следующие условия:
-
Тип функции не имеет параметров типа.
-
Количество параметров лямбда-выражения равно количеству типов параметров типа функции.
-
Если лямбда-выражение явно типизировано, его формальные типы параметров совпадают с типами параметров типа функции.
-
Если предполагается, что параметры лямбда-выражения имеют те же типы, что и типы параметров типа функции, то:
-
Если тип результата функции —
void, то тело лямбда-выражения — это выражение-утверждение (§14.8) или блок, совместимый сvoid. -
Если тип результата функции — тип R (не
void), то либо i) тело лямбда-выражения — это выражение, совместимое с R в контексте присваивания, либо ii) тело лямбда-выражения — это блок, совместимый со значениями, и каждое выражение результата (§15.27.2) совместимо с R в контексте присваивания.
-
Если лямбда-выражение совместимо с целевым типом T, то тип выражения U — это базовый целевой тип, выведенный из T.
Ошибка времени компиляции, если любой класс или интерфейс, упомянутый в U или типе функции U, недоступен из класса или интерфейса, в котором находится лямбда-выражение.
Для каждого метода m, который не является static, члена U, если тип функции U имеет подсигнатуру подписи m, то условный метод с типом метода, равным типу функции U, считается переопределяющим m, и могут возникнуть ошибки времени компиляции или предупреждения, указанные в §8.4.8.3.
Проверяемое исключение, которое может быть вызвано в теле лямбда-выражения, может привести к ошибке времени компиляции, как указано в §11.2.3.
Типы параметров явно типизированных лямбда-выражений должны точно соответствовать типам параметров типа функции. Хотя возможно больше гибкости — например, разрешить упаковку или контравариантность, такая общность кажется ненужной и не соответствует тому, как работает переопределение в объявлениях классов. Программист должен точно знать, какой тип функции целевой при написании лямбда-выражения, поэтому он должен точно знать, какую сигнатуру необходимо переопределить. (В отличие от этого, для ссылок на методы это не так, поэтому при их использовании разрешена большая гибкость.) Кроме того, большая гибкость с типами параметров увеличит сложность вывода типов и разрешения перегрузки.
Обратите внимание, что, хотя упаковка не разрешена в строгом контексте вызова, упаковка выражений результата лямбда-выражений всегда разрешена — то есть, выражение результата появляется в контексте присваивания, независимо от контекста, окружающего лямбда-выражение. Однако, если явно типизированное лямбда-выражение является аргументом перегруженного метода, подпись метода, избегающая упаковки или распаковки результата лямбда-выражения, предпочтительнее по проверке на максимальную специфичность (§15.12.2.5).
Если тело лямбда-выражения — это выражение-утверждение (то есть выражение, которое может быть использовано в качестве самостоятельного утверждения), оно совместимо с типом функции, производящим void; любой результат просто отбрасывается. Например, оба приведенных ниже варианта корректны:
// Predicate has abooleanresult java.util.function.Predicate<String> p = s->list.add(s); // Consumer has avoidresult java.util.function.Consumer<String> c = s->list.add(s);
В целом, лямбда-выражение вида () -> expr, где expr — это выражение-утверждение, интерпретируется как либо () -> { return expr; } или () -> { expr; }, в зависимости от целевого типа.
Во время выполнения оценка лямбда-выражения похожа на оценку выражения создания экземпляра класса, поскольку нормальное завершение приводит к ссылке на объект. Оценка лямбда-выражения отличается от выполнения тела лямбда-выражения.
Либо создается и инициализируется новый экземпляр класса с указанными ниже свойствами, либо ссылается на существующий экземпляр класса с этими свойствами. Если новый экземпляр должен быть создан, но памяти недостаточно, оценка лямбда-выражения прерывается с выбрасыванием OutOfMemoryError.
Значение лямбда-выражения — ссылка на экземпляр класса со следующими свойствами:
-
Класс реализует целевой тип функционального интерфейса и, если целевой тип — тип пересечения, каждый другой тип интерфейса, упомянутый в пересечении.
-
Если лямбда-выражение имеет тип U, для каждого метода
m, который не являетсяstatic, члена U:Если тип функции U имеет подсигнатуру подписи
m, то класс объявляет метод, переопределяющийm. Тело метода вычисляет тело лямбда-выражения, если это выражение, или выполняет тело лямбда-выражения, если это блок; если ожидается результат, он возвращается из метода.Если стирание типа метода, переопределяемого, отличается по своей сигнатуре от стирания типа функции U, то перед вычислением или выполнением тела лямбда-выражения тело метода проверяет, является ли каждое значение аргумента экземпляром подкласса или подинтерфейса стирания соответствующего типа параметра в типе функции U; если нет, то выбрасывается
ClassCastException. -
Класс не переопределяет другие методы целевого типа функционального интерфейса или других указанных типов интерфейсов, хотя он может переопределять методы класса
Object.
Эти правила предназначены для предоставления гибкости реализациям языка программирования Java, поскольку:
-
Новый объект не обязательно должен выделяться при каждой оценке.
-
Объекты, созданные различными лямбда-выражениями, не обязательно должны принадлежать различным классам (если тела идентичны, например).
-
Каждый создаваемый оценкой объект не обязательно должен принадлежать одному и тому же классу (переменные локальных переменных, например, могут быть внедрены).
-
Если доступен "существующий экземпляр", он не обязательно был создан при предыдущей оценке лямбда-выражения (он мог быть выделен во время инициализации содержащего класса, например).
Если целевой тип функционального интерфейса является подтипом java.io.Serializable, то полученный объект автоматически будет экземпляром сериализуемого класса. Превращение объекта, полученного из лямбда-выражения, в сериализуемый объект может потребовать дополнительных затрат времени выполнения и иметь последствия для безопасности, поэтому объекты, производные от лямбда-выражений, не требуются для сериализации "по умолчанию".
Выражение, обозначающее значение примитивного типа или String, которое не завершается аномально и составлено только с использованием следующих элементов:
-
Литералы примитивных типов и литералы типа
String(§3.10.1, §3.10.2, §3.10.3, §3.10.4, §3.10.5) -
Приведения к примитивным типам и приведения к типу
String(§15.16) -
Унарные операторы
+,-,~и!(но не++или--) (§15.15.3, §15.15.4, §15.15.5, §15.15.6) -
Множительные операторы
*,/и%(§15.17) -
Сложение операторы
+и-(§15.18) -
Операторы сдвига
<<,>>и>>>(§15.19) -
Операторы сравнения
<,<=,>и>=(но неinstanceof) (§15.20) -
Операторы сравнения на равенство
==и!=(§15.21) -
Битовые и логические операторы
&,^и|(§15.22) -
Условный оператор И
&&и условный оператор ИЛИ||(§15.23, §15.24) -
Тернарный условный оператор
? :(§15.25) -
Выражения в скобках (§15.8.5), содержащие вложенное постоянное выражение.
-
Простые имена (§6.5.6.1), которые ссылаются на константные переменные (§4.12.4).
-
Квалифицированные имена (§6.5.6.2) вида ТипИмя
.Идентификатор, которые ссылаются на константные переменные (§4.12.4).
Постоянные выражения типа String всегда «интернируются», чтобы использовать метод String.intern для совместного использования уникальных экземпляров.
Постоянное выражение всегда обрабатывается как строгое с плавающей точкой (§15.4), даже если оно используется в контексте, где неконстантное выражение не рассматривается как строгое с плавающей точкой.
Постоянные выражения используются в качестве меток case в утверждениях switch (§14.11) и имеют особое значение для преобразования присваивания (§5.2) и инициализации класса или интерфейса (§12.4.2). Они также могут определять возможность завершения утверждения while, do или for нормально (§14.21), а также тип условного оператора ? : с числовыми операндами.
Пример 15.28-1. Постоянные выражения
true (short)(1*2*3*4*5*6) Integer.MAX_VALUE / 2 2.0 * Math.PI "The integer " + Long.MAX_VALUE + " is mighty big."
© Oracle and/or its affiliates. All rights reserved.
Licensed under the Oracle Technology Network License Agreement.