Глава 15. Выражения
Содержание
- 15.1. Оценка, Обозначение и Результат
- 15.2. Формы Выражений
- 15.3. Тип Выражения
- 15.4. Выражения с Плавающей Точкой
- 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. Лямбда-Выражения
Большая часть работы в программе выполняется путём вычисления выражений, либо для их побочных эффектов, таких как присваивание переменным, либо для их значений, которые могут быть использованы в качестве аргументов или операндов в более крупных выражениях, либо для влияния на последовательность выполнения в операторах, либо и для того, и для другого.
В этом разделе описываются значения выражений и правила их вычисления.
Когда выражение в программе вычисляется (исполняется), результат обозначает одно из трёх:
Если выражение обозначает переменную, и требуется значение для дальнейшего вычисления, то используется значение этой переменной. В этом контексте, если выражение обозначает переменную или значение, мы можем говорить просто о значении выражения.
Выражение обозначает ничего тогда и только тогда, когда это вызов метода (§15.12), который вызывает метод, не возвращающий значение, то есть метод объявленный void (§8.4). Такое выражение может быть использовано только как оператор выражения (§14.8) или как единственное выражение тела лямбда-выражения (§15.27.2), поскольку любой другой контекст, в котором может появиться выражение, требует, чтобы выражение обозначало что-то. Оператор выражения или тело лямбда-выражения, являющееся вызовом метода, также может вызывать метод, который производит результат; в этом случае возвращаемое значение метода отбрасывается.
Вычисление выражения может иметь побочные эффекты, поскольку выражения могут содержать вложенные присваивания, операторы инкремента, операторы декремента, вызовы методов и, в switch выражениях, произвольные операторы.
Выражение встречается в одном из следующих случаев:
-
Объявление некоторого класса или интерфейса, который объявляется: в инициализаторе поля, в статическом инициализаторе, в инициализаторе экземпляра, в объявлении конструктора, в объявлении метода или в аннотации.
-
Аннотация на объявлении модуля, пакета или верхнего уровня класса или интерфейса.
Выражения можно грубо разделить на следующие синтаксические формы:
Преимущество операторов управляется иерархией грамматических правил. Оператор с наименьшим приоритетом — стрелка лямбда-выражения (->), за ним следуют операторы присваивания. Таким образом, все выражения синтаксически включены в LambdaExpression и AssignmentExpression нетерминалы:
Когда некоторые выражения появляются в определённых контекстах, они считаются поли-выражениями. Следующие формы выражений могут быть поли-выражениями:
Правила, определяющие, является ли выражение одного из этих видов поли-выражением, даны в отдельных разделах, определяющих эти формы выражений.
Выражения, которые не являются поли-выражениями, являются самостоятельными выражениями. Самостоятельные выражения — это выражения вышеупомянутых видов, когда они определены не как поли-выражения, а также все выражения всех других видов. Выражения всех других форм называются самостоятельной формой.
Некоторые выражения имеют значение, которое может быть определено на этапе компиляции. Это постоянные выражения (§15.29).
Если выражение обозначает переменную или значение, то выражение имеет тип, известный во время компиляции. Тип автономного выражения может быть определён полностью по содержимому выражения; в отличие от этого, тип полиморфного выражения может зависеть от целевого типа выражения (§5 (Преобразования и контексты)). Правила определения типа выражения подробно описаны ниже для каждого типа выражения.
Значение выражения совместимо с присваиванием (§5.2) с типом выражения, если не происходит загрязнение кучи (§4.12.2).
Аналогично, значение, хранящееся в переменной, всегда совместимо с типом переменной, если не происходит загрязнение кучи.
Другими словами, значение выражения, тип которого T, всегда подходит для присваивания переменной типа T.
Обратите внимание, что если тип выражения — это тип класса, который называет класс C, то объявление класса C как final или sealed (§8.1.1.2) имеет последствия для значения выражения:
-
Если C является
final, то выражение гарантированно имеет значение, которое либо (i) является нулевой ссылкой, либо (ii) является объектом, класс которого — сам C, так как классыfinalне имеют подклассов. -
Если C является
sealed, то выражение гарантированно имеет значение, которое либо (i) является нулевой ссылкой, либо (ii) является объектом, класс которого — сам C, либо (iii) совместимо с присваиванием с одним из разрешённых непосредственных подклассов C (§8.1.6). -
Если C свободно расширяем, то выражение гарантированно имеет значение, которое либо (i) является нулевой ссылкой, либо (ii) является объектом, класс которого — сам C, либо (iii) совместимо с присваиванием с C.
Вещественное выражение — это выражение, тип которого является float или double (§4.2.3). Вещественные выражения типа float обозначают значения, которые точно соответствуют значениям, представимым в 32-битном формате IEEE 754 binary32. Вещественные выражения типа double обозначают значения, которые точно соответствуют значениям, представимым в 64-битном формате IEEE 754 binary64.
Многие операторы сравнения и числовые операторы языка программирования Java, которые могут быть использованы для построения вещественных выражений, соответствуют операциям IEEE 754, как и преобразования, которые действуют на значения с плавающей запятой (Таблица 15.4-A).
Таблица 15.4-A. Соответствие операциям IEEE 754
| Оператор/Преобразование | Операция IEEE 754 |
|---|---|
Числовые операторы сравнения <, <=, > и >= (§15.20.1) | compareQuietLess, compareQuietLessEqual, compareQuietGreater, compareQuietGreaterEqual |
Числовые операторы равенства == и != (§15.21.1) | compareQuietEqual, compareQuietNotEqual |
Унарный оператор минус - (§15.15.4) | negate |
Мультипликативные операторы * и / (§15.17.1, §15.17.2) | умножение, деление |
Аддитивные операторы + и - (§15.18.2) | сложение, вычитание |
| Расширяющее преобразование примитивного типа из целочисленного типа (§5.1.2) | convertFromInt |
| Сужающее преобразование примитивного типа к целочисленному типу (§5.1.3) | convertToIntegerTowardZero |
Преобразование между float и double
| convertFormat |
Оператор остатка от деления для чисел с плавающей запятой % (§15.17.3) не соответствует операции остатка от деления IEEE 754.
Некоторые операции IEEE 754 без соответствующих операторов в языке программирования Java предоставляются через методы в классах Math и StrictMath, включая метод sqrt для операции извлечения квадратного корня IEEE 754, метод fma для операции IEEE 754 fusedMultiplyAdd и метод IEEEremainder для операции остатка от деления IEEE 754.
Язык программирования Java требует поддержки чисел с плавающей запятой strictfp IEEE 754 и strictfp, что облегчает доказательство желательных свойств конкретных числовых алгоритмов. Операции с плавающей запятой не «обнуляются» при подсчете результатов, если полученное значение — float.
Результат оператора с плавающей запятой языка программирования Java должен соответствовать результату соответствующей операции IEEE 754 над теми же операндами. Для конечных результатов это подразумевает, что знак, мантисса и порядок вещественного результата должны быть такими, как указано в IEEE 754.
Требование соответствия знака, мантиссы и порядка исключает некоторые преобразования, которые могли бы быть разрешены, если бы поведение чисел с плавающей запятой было менее точно определено. Например, -x обычно не может быть заменено на (0.0 -
x), потому что знак результата будет отличаться, если x является -0.0. Кроме того, другие, возможно, изменяющие значение преобразования, такие как замена (a * b +
c) вызовом методу библиотеки fused multiply-accumulate, не допускаются, если результат не может быть доказан как идентичный.
Нет обстоятельств, когда при вычислении вещественного выражения могут быть использованы промежуточные результаты, имеющие большую точность или больший диапазон порядка, чем указано типом выражения.
Операция с плавающей запятой, которая приводит к переполнению, производит бесконечность со знаком.
Операция с плавающей запятой, которая приводит к потере точности, производит поднормальное значение или ноль со знаком.
Операция с плавающей запятой, для которой нет единственного математически определенного результата, производит NaN.
Все числовые операции с NaN в качестве операнда дают NaN в качестве результата.
Поскольку NaN не упорядочен, любая числовая операция сравнения, включающая один или два NaN, возвращает false, любая операция == сравнения, включающая NaN, возвращает false, и любая операция != сравнения, включающая NaN, возвращает true.
Арифметика с плавающей запятой является приближением к действительной арифметике. Хотя существует бесконечное количество вещественных чисел, в конкретном формате с плавающей запятой имеется только конечное число значений. В языке программирования Java правило округления — это функция, используемая для отображения действительного числа в значение с плавающей запятой в заданном формате. Для действительных чисел в диапазоне, представимом в формате с плавающей запятой, непрерывный отрезок числовой оси отображается в одно значение с плавающей запятой. Действительное число, значение которого численно равно значению с плавающей запятой, отображается в это значение с плавающей запятой; например, действительное число 1,5 отображается в значение с плавающей запятой 1,5 в заданном формате. Язык программирования Java определяет два правила округления следующим образом:
-
Правило округления до ближайшего значения применяется ко всем операторам с плавающей запятой, за исключением (i) преобразования в целое значение и (ii) остатка от деления с плавающей запятой. Согласно правилу округления до ближайшего значения, неточные результаты должны быть округлены до ближайшего представимого значения к бесконечно точному результату; если два ближайших представимых значения находятся на одинаковом расстоянии, выбирается значение, у которого младший бит равен нулю.
Правило округления до ближайшего значения соответствует атрибуту направления округления по умолчанию для двоичной арифметики в IEEE 754, roundTiesToEven.
Атрибут направления округления roundTiesToEven был известен как режим округления «до ближайшего» в версии стандарта IEEE 754 1985 года. Правило округления в языке программирования Java названо в честь этого режима округления.
-
Правило округления к нулю применяется к (i) преобразованию значения с плавающей запятой в целое значение (§5.1.3) и (ii) остатку от деления с плавающей запятой (§15.17.3). Согласно правилу округления к нулю, неточные результаты округляются до ближайшего представимого значения, которое не больше по модулю, чем бесконечно точный результат. Для преобразования в целое число правило округления к нулю эквивалентно усечению, где дробные биты мантиссы отбрасываются.
Правило округления к нулю соответствует атрибуту направления округления roundTowardZero для двоичной арифметики в IEEE 754.
Атрибут направления округления roundTowardZero был известен как режим округления «к нулю» в версии стандарта IEEE 754 1985 года. Правило округления в языке программирования Java названо в честь этого режима округления.
Язык программирования Java требует, чтобы каждый оператор с плавающей запятой округлял свой результат до точности результата. Правило округления, используемое для каждого оператора с плавающей запятой, — это либо округление до ближайшего, либо округление к нулю, как указано выше.
Java 1.0 и 1.1 требовали строгого вычисления вещественных выражений. Строгое вычисление означает, что каждый float операнд соответствует значению, представимому в формате IEEE 754 binary32, каждый double операнд соответствует значению, представимому в формате IEEE 754 binary64, и каждый оператор с плавающей запятой с соответствующей операцией IEEE 754 соответствует результату IEEE 754 для тех же операндов.
Строгое вычисление обеспечивает предсказуемые результаты, но вызывало проблемы с производительностью в реализациях Java Virtual Machine для некоторых семейств процессоров, распространенных в эпоху Java 1.0/1.1. Поэтому в Java 1.2 и до Java SE 16 платформа Java SE разрешила реализации Java Virtual Machine иметь один или два набора значений, связанные с каждым типом с плавающей запятой. Тип float был связан с набором значений float и набором значений float с расширенным порядком, а тип double был связан с набором значений double и набором значений double с расширенным порядком. Набор значений float соответствовал значениям, представимым в формате IEEE 754 binary32; набор значений float с расширенным порядком имел то же количество битов точности, но больший диапазон порядка. Аналогично, набор значений double соответствовал значениям, представимым в формате IEEE 754 binary64; набор значений double с расширенным порядком имел то же количество битов точности, но больший диапазон порядка. Разрешение по умолчанию использования наборов значений с расширенным порядком смягчило проблемы производительности на некоторых семействах процессоров.
Для совместимости, Java 1.2 разрешало программисту запретить использованию расширенных наборов значений показателя степени. Программист выражал это, помещая модификатор strictfp на объявление класса, интерфейса или метода. strictfp ограничивало семантику с плавающей точкой любых вложенных выражений, используя набор значений типа float для float выражений и набор значений типа double для double выражений, гарантируя, что результаты таких выражений были полностью предсказуемы. Код, измененный с помощью strictfp, таким образом, имел ту же семантику с плавающей точкой, что и в Java 1.0 и 1.1.
В Java SE 17 и более поздних версиях платформа Java SE всегда требует строгой оценки выражений с плавающей точкой. Более новые члены семейств процессоров, у которых были проблемы с производительностью при реализации строгой оценки, больше не сталкиваются с этой трудностью. Это спецификация больше не связывает float и double с четырьмя наборами значений, описанными выше, и модификатор strictfp больше не влияет на оценку выражений с плавающей точкой. Для совместимости strictfp остается ключевым словом в Java SE 21 (§3.8) и продолжает иметь ограничения на его использование (§8.4.3, §9.4), хотя компиляторы Java рекомендуются предупреждать программиста о его устаревшем статусе. Будущие версии языка программирования Java могут переопределить или удалить ключевое слово strictfp.
Если тип выражения — примитивный тип, то значение выражения — того же примитивного типа.
Если тип выражения — ссылочный тип, то класс ссылаемого объекта, или даже является ли значение ссылкой на объект, а не null, не обязательно известен во время компиляции. Есть несколько мест в языке программирования Java, где фактический класс ссылаемого объекта влияет на выполнение программы способом, который нельзя вывести из типа выражения. Они таковы:
-
Вызов метода (§15.12). Конкретный метод, используемый для вызова
o.m(...), выбирается на основе методов, которые являются частью класса или интерфейса, который является типомo. Для методов экземпляра класс объекта, на который ссылается фактическое значениеo, участвует, потому что подкласс может переопределить определенный метод, уже объявленный в родительском классе, так что вызывается этот переопределенный метод. (Переопределяющий метод может или не может выбрать дальнейшее вызов исходного переопределенногоmметода.) -
Оператор
instanceof(§15.20.2). Выражение, тип которого является ссылочным типом, может быть проверено с помощьюinstanceof, чтобы узнать, может ли класс объекта, на который ссылается фактическое значение выражения, быть преобразован в другой ссылочный тип. -
Приведение типов (§15.16). Класс объекта, на который ссылается фактическое значение операнда выражения, может не быть совместим с типом, указанным оператором приведения. Для ссылочных типов это может потребовать проверки во время выполнения, которая генерирует исключение, если класс ссылаемого объекта, как определяется во время выполнения, не может быть преобразован в целевой тип.
-
Присвоение компоненту массива ссылочного типа (§10.5, §15.13, §15.26.1). Правила проверки типов позволяют обрабатывать тип массива S
[]как подтип T[], если S является подтипом T, но это требует проверки во время выполнения для присваивания компоненту массива, подобной проверке, выполняемой для приведения типов. -
Обработка исключений (§14.20). Исключение перехватывается блоком
catchтолько в том случае, если класс объекта, выбросившего исключение, является подклассом типа формального параметра блока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). -
Выражение
switch(§15.28) или улучшенное выражениеswitch(§14.11.2) выбрасываетMatchException, если ни одна метка переключателя не применяется к значению выражения селектора.
Выражение вызова метода также может привести к выбрасыванию исключения, если происходит исключение, которое приводит к прерывистому завершению выполнения тела метода.
Выражение создания экземпляра класса также может привести к выбрасыванию исключения, если происходит исключение, которое приводит к прерывистому завершению выполнения конструктора.
Также могут возникать различные ошибки связывания и виртуальной машины во время вычисления выражения. По своей природе такие ошибки трудно предсказать и трудно обрабатывать.
Если возникает исключение, то вычисление одного или нескольких выражений может быть прервано до завершения всех шагов их нормального режима вычисления; такие выражения считаются завершенными прерывисто.
Если вычисление выражения требует вычисления подвыражения, то прерывистое завершение подвыражения всегда вызывает немедленное прерывистое завершение самого выражения с тем же основанием, и все последующие шаги в нормальном режиме вычисления не выполняются.
Термины «завершение нормально» и «завершение прерывисто» также применяются к выполнению операторов (§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, для которых первое выражение производит бесконечность (из-за переполнения), а второе выражение производит конечный результат.
Например, тестовая программа:
class Test {
public static void main(String[] args) {
double d = 8E307;
System.out.println(4.0 * d * 0.5);
System.out.println(2.0 * d);
}
}
выводит:
Infinity 1.6E308
потому что первое выражение переполняется, а второе — нет.
В отличие от этого, целочисленное сложение и умножение являются доказуемо ассоциативными в языке программирования 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]".
Эта неоднозначность устраняется путем разделения ожидаемого определения Основное на Основное и PrimaryNoNewArray. (Это можно сравнить с разделением Утверждение на Утверждение и StatementNoShortIf (§14.5) для устранения проблемы «висящего else».)
Литерал (§3.10) обозначает фиксированное, неизменяемое значение.
Следующая производная из §3.10 показана здесь для удобства:
Тип литерала определяется следующим образом:
-
Тип целочисленного литерала (§3.10.1), заканчивающегося на
Lилиl(ell), являетсяlong(§4.2.1).Тип любого другого целочисленного литерала является
int(§4.2.1). -
Тип вещественного литерала (§3.10.2), заканчивающегося на
Fилиf, являетсяfloat(§4.2.3).Тип любого другого вещественного литерала является
double(§4.2.3). -
Тип логического литерала (§3.10.3) является
boolean(§4.2.5). -
Тип строкового литерала (§3.10.5) или текстового блока (§3.10.6) является
String(§4.3.3). -
Тип нулевого литерала
null(§3.10.8) - тип null (§4.1); его значение - нулевая ссылка.
Вычисление лексического литерала всегда завершается успешно.
Литерал класса — это выражение, состоящее из имени класса, интерфейса, массива, примитивного типа или псевдотипа void, за которым следует «.» и токен class.
ТипИмениКлассa должен обозначать доступный класс или интерфейс (§6.6). Если ТипИмениКлассa обозначает недоступный класс или интерфейс, или тип переменной, это ошибка времени компиляции.
Тип 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>.
Литерал класса вычисляет значение объекта класса, интерфейса, массива или примитивного типа (или для void), как определено загрузчиком класса, определяющим класс текущего экземпляра (§12.2).
Ключевое слово this может использоваться в качестве выражения в следующих контекстах:
При использовании в качестве выражения ключевое слово this обозначает значение, являющееся ссылкой на объект, для которого был вызван метод экземпляра (§15.12), или на объект, который в данный момент создаётся. Значение, обозначаемое this в теле лямбда-выражения (§15.27.2), совпадает со значением this в окружающем контексте.
Ключевое слово this также используется в явных вызовах конструкторов (§8.8.7.1) и для обозначения параметра получателя метода или конструктора (§8.4).
Ошибка времени компиляции возникает, если выражение this появляется в статическом контексте (§8.1.3).
Пусть C — это самый внутренний объявленный класс или интерфейс, окружающий выражение this. Если C является обобщённым, с параметрами типа F1,...,Fn, тип this — это C<F1,...,Fn>. В противном случае, тип this — это C.
Во время выполнения, класс фактического объекта, на который указывает ссылка, может быть C или подклассом C (§8.1.5).
Пример 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.
Пусть n — целое число такое, что ТипИмениКлассa обозначает n'ый лексически окружающий класс или интерфейс, объявленный непосредственно перед квалифицированным выражением this.
Значение квалифицированного выражения this ТипИмениКлассa.this — это n'ый лексически окружающий экземпляр this.
Если ТипИмениКлассa обозначает обобщённый класс с параметрами типа F1,...,Fn, тип квалифицированного выражения this — это ТипИмениКлассa<F1,...,Fn>. В противном случае, тип квалифицированного выражения this — это ТипИмениКлассa.
Ошибка времени компиляции возникает, если квалифицированное выражение this появляется в статическом контексте (§8.1.3).
Ошибка времени компиляции возникает, если класс или интерфейс, чьё объявление непосредственно содержит квалифицированное выражение this, не является внутренним классом ТипИмениКлассa или ТипИмениКлассa само по себе.
Выражение в скобках — это выражение-операнд, тип которого совпадает с типом содержащегося выражения, а значение которого во время выполнения равно значению содержащегося выражения. Если содержащееся выражение обозначает переменную, то выражение в скобках также обозначает эту переменную.
Использование скобок влияет только на порядок вычисления, за исключением частного случая, когда (-2147483648) и (-9223372036854775808L) являются допустимыми, но -(2147483648) и -(9223372036854775808L) являются недопустимыми.
Это связано с тем, что десятичные литералы 2147483648 и 9223372036854775808L разрешены только в качестве операнда унарного минуса (§3.10.1).
В частности, наличие или отсутствие скобок вокруг выражения не влияет на то, является ли переменная определённо присвоенной, определённо присвоенной, когда true, определённо присвоенной, когда false, определённо не присвоенной, определённо не присвоенной, когда true или определённо не присвоенной, когда false (§16 (Определённое присвоение)).
Если выражение в скобках появляется в контексте определенного типа с целевым типом T (§5 (Преобразования и контексты)), то содержащееся выражение аналогично появляется в контексте того же типа с целевым типом T.
Если содержащееся выражение является поливыражением (§15.2), то выражение в скобках также является поливыражением. В противном случае это автономное выражение.
Поливыражение в скобках совместимо с целевым типом T, если его содержащееся выражение совместимо с T.
Выражение создания экземпляра класса используется для создания новых объектов, являющихся экземплярами классов.
Следующая продукция из §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).
-
Выражения создания экземпляра указанных классов начинаются с выражения-операнда Primary или ExpressionName.
Выражение создания экземпляра указанного класса позволяет создавать экземпляры вложенных внутренних классов и их анонимных подклассов.
И выражения создания экземпляра неуказанных, и выражения создания экземпляра указанных классов могут необязательно заканчиваться телом класса. Такое выражение создания экземпляра класса объявляет анонимный класс (§15.9.5) и создает его экземпляр.
Выражение создания экземпляра класса является поливыражением (§15.2), если оно использует алмазную форму для аргументов типа класса и появляется в контексте присваивания или вызова (§5.2, §5.3). В противном случае это автономное выражение.
Мы говорим, что класс создается, когда экземпляр класса создается выражением создания экземпляра класса. Создание экземпляра класса включает определение класса, который необходимо создать (§15.9.1), включающих экземпляров (при наличии) нового созданного экземпляра (§15.9.2) и конструктора, который необходимо вызвать для создания нового экземпляра (§15.9.3).
Если ClassOrInterfaceTypeToInstantiate заканчивается TypeArguments (а не <>), то ClassOrInterfaceTypeToInstantiate должно обозначать хорошо сформированный параметризованный тип (§4.5), иначе возникает ошибка компиляции.
Если ClassOrInterfaceTypeToInstantiate заканчивается <>, но класс или интерфейс, обозначаемый Identifier в ClassOrInterfaceTypeToInstantiate, не является обобщенным, то возникает ошибка компиляции.
Если выражение создания экземпляра класса заканчивается телом класса, то создаваемый класс — анонимный класс. Тогда:
-
Если выражение создания экземпляра класса не квалифицировано, то:
Identifier в ClassOrInterfaceTypeToInstantiate должен обозначать доступный, свободно расширяемый (§8.1.1.2) класс, не являющийся классом перечисления, или доступный и свободно расширяемый интерфейс (§9.1.1.4). В противном случае возникает ошибка компиляции.
Если Identifier в ClassOrInterfaceTypeToInstantiate обозначает класс, C, то объявляется анонимный непосредственный подкласс C. Если TypeArguments присутствует, то C имеет типы аргументов, заданные TypeArguments; если
<>присутствует, то типы аргументов C будут выведены в §15.9.3; в противном случае C не имеет типов аргументов. Тело подкласса — ClassBody, заданное в выражении создания экземпляра класса. Создаваемый класс — анонимный подкласс.Если Identifier в ClassOrInterfaceTypeToInstantiate обозначает интерфейс, I, то объявляется анонимный непосредственный подкласс
Object, реализующий I. Если TypeArguments присутствует, то I имеет типы аргументов, заданные TypeArguments; если<>присутствует, то типы аргументов I будут выведены в §15.9.3; в противном случае I не имеет типов аргументов. Тело подкласса — ClassBody, заданное в выражении создания экземпляра класса. Создаваемый класс — анонимный подкласс. -
Если выражение создания экземпляра класса квалифицировано, то:
Identifier в ClassOrInterfaceTypeToInstantiate должен однозначно обозначать внутренний класс, доступный, свободно расширяемый, не являющийся классом перечисления и являющийся членом времени компиляции типа Primary выражения или ExpressionName. В противном случае возникает ошибка компиляции.
Пусть Identifier в ClassOrInterfaceTypeToInstantiate обозначает класс, C. Объявляется анонимный непосредственный подкласс C. Если TypeArguments присутствует, то C имеет типы аргументов, заданные TypeArguments; если
<>присутствует, то типы аргументов C будут выведены в §15.9.3; в противном случае C не имеет типов аргументов. Тело подкласса — ClassBody, заданное в выражении создания экземпляра класса. Создаваемый класс — анонимный подкласс.
Если выражение создания экземпляра класса не объявляет анонимный класс, то:
-
Если выражение создания экземпляра класса не квалифицировано, то:
Identifier в ClassOrInterfaceTypeToInstantiate должен обозначать класс, доступный, не-
abstractи не являющийся классом перечисления. В противном случае возникает ошибка компиляции.Создаваемый класс задается Identifier в ClassOrInterfaceTypeToInstantiate. Если TypeArguments присутствует, то класс имеет типы аргументов, заданные TypeArguments; если
<>присутствует, то типы аргументов класса будут выведены в §15.9.3; в противном случае класс не имеет типов аргументов. -
Если выражение создания экземпляра класса квалифицировано, то:
ClassOrInterfaceTypeToInstantiate должно однозначно обозначать внутренний класс, доступный, не-
abstract, не являющийся классом перечисления и являющийся членом времени компиляции типа Primary выражения или ExpressionName.Создаваемый класс задается Identifier в ClassOrInterfaceTypeToInstantiate. Если TypeArguments присутствует, то класс имеет типы аргументов, заданные TypeArguments; если
<>присутствует, то типы аргументов класса будут выведены в §15.9.3; в противном случае класс не имеет типов аргументов.
Пусть C — класс, который экземпляризируется, и пусть i — экземпляр, который создаётся. Если C — внутренний класс, то i может иметь непосредственно окружающий экземпляр (§8.1.3), определяемый следующим образом:
-
Если C — анонимный класс, то:
-
Если выражение создания экземпляра класса происходит в статическом контексте, то
iне имеет непосредственно окружающего экземпляра. -
В противном случае, непосредственно окружающий экземпляр
i— этоthis.
-
-
Если C — внутренний локальный класс, то:
-
Если C встречается в статическом контексте, то
iне имеет непосредственно окружающего экземпляра. -
В противном случае, если выражение создания экземпляра класса происходит в статическом контексте, возникает ошибка компиляции.
-
В противном случае, пусть O — непосредственно окружающее объявление класса или интерфейса для C, а U — непосредственно окружающее объявление класса или интерфейса для выражения создания экземпляра класса.
Если U не является внутренним классом O или самим O, возникает ошибка компиляции.
Пусть n — целое число такое, что O — n-й лексически окружающий класс или интерфейс объявления U.
Непосредственно окружающий экземпляр
i— это n-й лексически окружающий экземплярthis.
-
-
Если C — внутренний член класса, то:
-
Если выражение создания экземпляра класса не квалифицировано, то:
-
Если выражение создания экземпляра класса происходит в статическом контексте, возникает ошибка компиляции.
-
В противном случае, если C не является членом какого-либо класса, объявление которого лексически включает выражение создания экземпляра класса, возникает ошибка компиляции.
-
В противном случае, пусть O — самый внутренний окружающий класс объявления, членом которого является C, а U — непосредственно окружающее объявление класса или интерфейса для выражения создания экземпляра класса.
Если U не является внутренним классом O или самим O, возникает ошибка компиляции.
Пусть n — целое число такое, что O — n-й лексически окружающий класс или интерфейс объявления U
Непосредственно окружающий экземпляр
i— это n-й лексически окружающий экземплярthis.
-
-
Если выражение создания экземпляра класса квалифицировано, то непосредственно окружающий экземпляр
i— это объект, который является значением выражения Primary или ExpressionName.
-
Если C — анонимный класс, а его непосредственный суперкласс S — внутренний класс, то i может иметь непосредственно окружающий экземпляр относительно S, определяемый следующим образом:
-
Если S — внутренний локальный класс, то:
-
Если S встречается в статическом контексте, то
iне имеет непосредственно окружающего экземпляра относительно S. -
В противном случае, если выражение создания экземпляра класса происходит в статическом контексте, возникает ошибка компиляции.
-
В противном случае, пусть O — непосредственно окружающее объявление класса или интерфейса для S, а U — непосредственно окружающее объявление класса или интерфейса для выражения создания экземпляра класса.
Если U не является внутренним классом O или самим O, возникает ошибка компиляции.
Пусть n — целое число такое, что O — n-й лексически окружающий класс или интерфейс объявления U.
Непосредственно окружающий экземпляр
iотносительно S — это n-й лексически окружающий экземплярthis.
-
-
Если S — внутренний член класса, то:
-
Если выражение создания экземпляра класса не квалифицировано, то:
-
Если выражение создания экземпляра класса происходит в статическом контексте, возникает ошибка компиляции.
-
В противном случае, если S не является членом какого-либо класса, объявление которого включает выражение создания экземпляра класса, возникает ошибка компиляции.
-
В противном случае, пусть O — самый внутренний окружающий класс объявления, членом которого является S, а U — непосредственно окружающее объявление класса или интерфейса для выражения создания экземпляра класса.
Если U не является внутренним классом O или самим O, возникает ошибка компиляции.
Пусть n — целое число такое, что O — n-й лексически окружающий класс или интерфейс объявления U.
Непосредственно окружающий экземпляр
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 часть и тип возвращаемого значения.
-
Если выражение создания экземпляра класса не использует
<>, то:-
Если C не является анонимным классом, то:
Пусть T будет типом, обозначенным C, за которым следуют любые аргументы типа класса в выражении. Процесс, указанный в §15.12.2, модифицированный для обработки конструкторов, используется для выбора одного из конструкторов T и определения его
throwsпункта.Если в T нет уникального наиболее конкретного конструктора, который является одновременно применимым и доступным (§6.6), то возникает ошибка времени компиляции (как при вызовах методов).
В противном случае типом возвращаемого значения, соответствующим выбранному конструктору, является T.
-
Если C является анонимным классом, то:
Процесс, указанный в §15.12.2, модифицированный для обработки конструкторов, используется для выбора одного из конструкторов непосредственного суперкласса типа C и определения его
throwsпункта.Если нет уникального наиболее конкретного конструктора в непосредственном суперклассе типа C, который является одновременно применимым и доступным, то возникает ошибка времени компиляции (как при вызовах методов).
В противном случае анонимный конструктор C выбирается в качестве конструктора C (§15.9.5.1). Его тело состоит из явного вызова конструктора (§8.8.7.1) конструктора, выбранного в типе непосредственного суперкласса C.
throwsпункт выбранного конструктора включает исключения вthrowsпункте конструктора, выбранного в типе непосредственного суперкласса C.Типом возвращаемого значения, соответствующим выбранному конструктору, является тип анонимного класса.
-
-
Если выражение создания экземпляра класса использует
<>, то:Если C не является анонимным классом, пусть D будет тем же, что и C. Если C является анонимным классом, пусть D будет суперклассом или суперинтерфейсом C, названным выражением создания экземпляра класса.
Если D является классом, пусть
c1, ...,cnбудут конструкторами класса D. Если D является интерфейсом, пустьc1, ...,cnбудет списком-одиночкой (n = 1), содержащим конструктор без аргументов классаObject.Список методов
m1, ...,mnопределяется с целью разрешения перегрузки и вывода аргумента типа. Для всех j (1 ≤ j ≤ n),mjопределяется в терминахcjследующим образом:-
Сначала определяется подстановка θ
jдля создания экземпляров типов вcj.Пусть F1, ..., Fp будут параметрами типа D, а G1, ..., Gq будут параметрами типа (если есть)
cj. Пусть X1, ..., Xp и Y1, ..., Yq будут переменными типа с различными именами, которые не находятся в области видимости в теле D.θ
jis[F1:=X1, ..., Fp:=Xp, G1:=Y1, ..., Gq:=Yq]. -
Параметрами типа
mjявляются X1, ..., Xp, Y1, ..., Yq. Граница каждого параметра типа, если таковая имеется, представляет собой θj, примененный к соответствующей границе параметра типа в D илиcj. -
Типом возвращаемого значения
mjявляется θj, примененный к D<F1, ..., Fp>. -
(Возможно, пустой) список типов аргументов
mjявляется θj, примененным к типам аргументовcj. -
(Возможно, пустой) список выбрасываемых типов
mjявляется θj, примененным к выбрасываемым типамcj. -
Модификаторами
mjявляются модификаторыcj. -
Имя
mj— это#m, автоматически сгенерированное имя, которое отличается от всех имен конструкторов и методов в D и является общим дляm1, ...,mn. -
Тело
mjне имеет значения.
Чтобы выбрать конструктор, мы временно рассматриваем
m1, ...,mnкак члены D. Один изm1, ...,mnвыбирается, как определяется выражениями аргументов выражения создания экземпляра класса, используя процесс, указанный в §15.12.2.Если нет уникального наиболее конкретного метода, который является одновременно применимым и доступным, то возникает ошибка времени компиляции.
В противном случае, где
mj— выбранный метод:-
Если C не является анонимным классом, то
cjвыбирается в качестве конструктора C.throwsпункт выбранного конструктора совпадает сthrowsпунктом, определенным дляmj.Тип возвращаемого значения, соответствующий выбранному конструктору, является типом возвращаемого значения, определенным для
mj(§15.12.2.6). -
Если C является анонимным классом, то анонимный конструктор C выбирается в качестве конструктора C. Его тело состоит из явного вызова конструктора (§8.8.7.1)
cj.throwsпункт выбранного конструктора включает исключения вthrowsпункте, определенном дляmj.Тип возвращаемого значения, соответствующий выбранному конструктору, является типом анонимного класса.
Если выражение создания экземпляра класса является поливыражением, то его совместимость с целевым типом определяется в соответствии с §18.5.2.1, используя
mjв качестве выбранного методаm.Тестирование на совместимость с целевым типом может происходить несколько раз до принятия окончательного решения о целевом типе выражения создания экземпляра класса и типе возвращаемого значения, соответствующем выбранному конструктору. Например, выражение вызова включающего метода может потребовать тестирования выражения создания экземпляра класса на совместимость с типами формальных параметров различных методов.
Если C является анонимным классом, то его тип непосредственного суперкласса или тип непосредственного суперинтерфейса является типом возвращаемого значения, определенным для
mj(§15.12.2.6).Ошибка времени компиляции, если тип непосредственного суперкласса или тип непосредственного суперинтерфейса, или любое подвыражение в нем («подвыражение» включает аргументы типа параметризованных типов, границы аргументов типа подстановочных знаков и типы элементов типов массивов, но исключает границы переменных типа), имеет одну из следующих форм:
-
Переменная типа, которая не была объявлена как параметр типа (например, переменная типа, созданная преобразованием захвата).
-
Тип пересечения.
-
Тип класса или интерфейса, где объявление класса или интерфейса недоступно из класса или интерфейса, в котором отображается выражение создания экземпляра класса.
-
Ошибка времени компиляции, если аргумент выражения создания экземпляра класса несовместим с его целевым типом, как производный от типа вызова (§15.12.2.6).
Если объявление в момент компиляции применимо для вызова с переменным числом аргументов (§15.12.2.4), то, если тип последнего формального параметра типа вызова конструктора равен Fn[], то в момент компиляции возникает ошибка, если тип, являющийся стиранием типа Fn, недоступен в точке вызова.
Тип выражения создания экземпляра класса — это тип возвращаемого значения выбранного конструктора, как определено выше.
Во время выполнения вычисление выражения создания экземпляра класса происходит следующим образом.
Сначала, если выражение создания экземпляра класса является квалифицированным выражением создания экземпляра класса, квалифицирующее выражение-первичный элемент вычисляется. Если квалифицирующее выражение вычисляет значение null, возникает NullPointerException, и выражение создания экземпляра класса завершается внезапно. Если квалифицирующее выражение завершается внезапно, выражение создания экземпляра класса завершается внезапно по той же причине.
Далее, выделяется память для нового экземпляра класса. Если памяти недостаточно для выделения объекта, вычисление выражения создания экземпляра класса завершается внезапно с помощью исключения OutOfMemoryError.
Новый объект содержит новые экземпляры всех полей, объявленных в указанном классе и всех его суперклассах. При создании каждого нового экземпляра поля он инициализируется своим значением по умолчанию (§4.12.5).
Далее, фактические аргументы конструктору вычисляются слева направо. Если вычисление любого из аргументов завершается внезапно, любые выражения аргументов справа не вычисляются, и выражение создания экземпляра класса завершается внезапно по той же причине.
Затем вызывается выбранный конструктор указанного класса. Это приводит к вызову по крайней мере одного конструктора для каждого суперкласса класса. Этот процесс может быть направлен явными операторами вызова конструкторов (§8.8.7.1) и подробно описан в §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).
Анонимный класс неявно объявляется выражением создания экземпляра класса или константой перечисления, которая заканчивается телом класса (§8.9.1).
Анонимный класс никогда не является abstract (§8.1.1.1).
Анонимный класс никогда не является sealed (§8.1.1.2), и, следовательно, у него нет разрешенных непосредственных подклассов (§8.1.6).
Анонимный класс, объявленный выражением создания экземпляра класса, никогда не является final (§8.1.1.2).
Анонимный класс, объявленный константой перечисления, всегда является final.
Анонимный класс, не являющийся final, важен при приведении типов, в частности, при сужении преобразования ссылок, разрешенном для оператора приведения типа (§5.5). С другой стороны, это неактуально для наследования, так как невозможно объявить подкласс анонимного класса (анонимный класс не может быть назван с помощью extends фрагмента), несмотря на то, что анонимный класс не является final.
Анонимный класс всегда является внутренним классом (§8.1.3).
Как и локальный класс или интерфейс (§14.3), анонимный класс не является членом ни одного пакета, класса или интерфейса (§7.1, §8.5).
Тип непосредственного суперкласса или тип непосредственного суперинтерфейса анонимного класса, объявленного выражением создания экземпляра класса, задаётся выражением (§15.9.1), с необходимыми выводами типов аргументов при выборе конструктора (§15.9.3). Если задан тип непосредственного суперинтерфейса, тип непосредственного суперкласса является Object.
Тип непосредственного суперкласса анонимного класса, объявленного константой перечисления, — это тип объявляющего класса перечисления.
ClassBody выражения создания экземпляра класса или константы перечисления объявляет поля (§8.3), методы (§8.4), внутренние классы (§8.5), внутренние интерфейсы (§9.1.1.3), инициализаторы экземпляров (§8.6) и статические инициализаторы (§8.7) анонимного класса. Конструктор анонимного класса всегда неявный (§15.9.5.1).
Если выражение создания экземпляра класса с ClassBody использует алмаз (<>) для типов аргументов класса, подлежащего созданию, то для всех методов, не являющихся private, объявленных в ClassBody, метод объявляется так, как будто он аннотирован @Override (§9.6.4.4).
При использовании <>, выведенные типы аргументов могут не соответствовать ожиданиям программиста. В результате, супертип анонимного класса может не соответствовать ожиданиям, а методы, объявленные в анонимном классе, могут не переопределять методы супертипа как предполагалось. Обращение к таким методам так, как будто они аннотированы @Override (если они не аннотированы явно @Override), помогает избежать создания некорректных программ.
У анонимного класса не может быть явно объявленного конструктора. Вместо этого для анонимного класса неявно объявляется анонимный конструктор. Форма анонимного конструктора для анонимного класса C с непосредственным суперклассом S такова:
-
Если S не является внутренним классом или если S является локальным классом, который встречается в статическом контексте, то анонимный конструктор имеет один формальный параметр для каждого фактического аргумента выражения создания экземпляра класса или константы перечисления, которые объявляют C.
Фактические аргументы выражения создания экземпляра класса или константы перечисления используются для определения конструктора
xS, как указано в §15.9.3. Тип каждого формального параметра анонимного конструктора делается идентичным соответствующему формальному параметруx.Тело анонимного конструктора состоит из явного вызова конструктора (§8.8.7.1) вида
super(...), где фактические аргументы — это формальные параметры анонимного конструктора в порядке их объявления. Вызываемый конструктор суперкласса — этоx. -
В противном случае первый формальный параметр анонимного конструктора представляет значение непосредственно окружающего экземпляра
iотносительно S (§15.9.2). Тип этого параметра — тип класса, непосредственно окружающего объявление S.Анонимный конструктор имеет дополнительный формальный параметр для каждого фактического аргумента выражения создания экземпляра класса, который объявил анонимный класс. n'-ый формальный параметр соответствует n-1'-му фактическому аргументу.
Фактические аргументы выражения создания экземпляра класса используются для определения конструктора
xS, как указано в §15.9.3. Тип каждого формального параметра анонимного конструктора делается идентичным соответствующему формальному параметруx.Тело анонимного конструктора состоит из явного вызова конструктора вида
o.super(...), гдеo— первый формальный параметр анонимного конструктора, а фактические аргументы — последующие формальные параметры конструктора в порядке их объявления. Вызываемый конструктор суперкласса — этоx.
Во всех случаях throws анонимного конструктора перечисляет все исключения, которые обрабатываются явным оператором вызова конструктора, содержащимся в анонимном конструкторе, как указано в §15.9.3, и все исключения, обрабатываемые любыми инициализаторами экземпляров или инициализаторами переменных экземпляра анонимного класса.
Обратите внимание, что возможно, что сигнатура анонимного конструктора ссылается на недоступный тип (например, если такой тип встречается в сигнатуре конструктора суперкласса x). Это само по себе не приводит к ошибкам ни на этапе компиляции, ни на этапе выполнения.
Выражение создания массива используется для создания новых массивов (§10 (Массивы)).
Следующее производство из §4.3 показано здесь для удобства:
Выражение создания массива создаёт объект, представляющий собой новый массив, элементы которого имеют тип, указанный в PrimitiveType или ClassOrInterfaceType.
Если ClassOrInterfaceType не обозначает реабилизируемый тип (§4.7), это является ошибкой на этапе компиляции. В противном случае ClassOrInterfaceType может обозначать любой именованный ссылочный тип, даже тип класса abstract (§8.1.1.1) или интерфейса.
Вышеприведённые правила подразумевают, что тип элемента в выражении создания массива не может быть параметризованным типом, если все типы аргументов параметризованного типа не являются неограниченными условными типами.
Тип каждого выражения измерения в DimExpr должен быть типом, преобразуемым (§5.1.8) в целочисленный тип; в противном случае произойдёт ошибка на этапе компиляции.
Каждое выражение измерения подвергается числовому продвижению с унарным значением (§5.6). Полученный тип должен быть 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).
Выражение доступа к массиву ссылается на переменную, являющуюся компонентом массива.
Выражение доступа к массиву содержит два подвыражения: выражение ссылки на массив (перед левой квадратной скобкой) и выражение индекса (внутри скобок).
Обратите внимание, что выражение ссылки на массив может быть именем или любым выражением типа primary, которое не является выражением создания массива, за исключением случаев, когда выражение создания массива имеет инициализатор массива (§15.10.1).
Тип выражения ссылки на массив должен быть типом массива (назовём его T[], массивом, компоненты которого имеют тип T), иначе произойдёт ошибка компиляции.
Выражение индекса подвергается числовому продвижению (§5.6). Продвинутый тип должен быть 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и доступ к полю происходит в инициализаторе переменной класса (§8.3.2) или статическом инициализаторе (§8.7), то результатом является переменная, а именно указанная переменная класса в классе, который является типом выражения Primary.
-
-
Если поле не является
static:-
Выражение Primary оценивается. Если вычисление выражения Primary завершается прерыванием, выражение доступа к полю завершается прерыванием по той же причине.
-
Если значение Primary равно
null, то выбрасываетсяNullPointerException. -
Если поле является непустым
final, то результатом является значение названного поля-члена в типе T, найденного в объекте, на который ссылается значение Primary. -
Если поле не является
final, или является пустымfinalи доступ к полю происходит в инициализаторе переменной экземпляра (§8.3.2), инициализаторе экземпляра (§8.6) или конструкторе (§8.8), то результатом является переменная, а именно, названное поле-член в типе 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. Переменная получателя не имеет значения для доступа к полю static
Следующая программа демонстрирует, что нулевая ссылка может использоваться для доступа к переменной класса (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, появляется в статическом контексте, то это синтаксическая ошибка (§8.1.3).
Для выражения доступа к полю в форме super.Идентификатор:
-
Если непосредственно окружающее класс или интерфейс объявление выражения доступа к полю является классом
Objectили интерфейсом, то это синтаксическая ошибка.
Для выражения доступа к полю в форме T.super.Идентификатор:
-
Если T является классом
Objectили интерфейсом, то это синтаксическая ошибка. -
Пусть U — непосредственно окружающее класс или интерфейс объявление выражения доступа к полю. Если U не является вложенным классом T или самим T, то это синтаксическая ошибка.
Предположим, что выражение доступа к полю 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.
Пример 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), то:
-
Если существует объявление класса или интерфейса, в котором данный метод является членом, то пусть E будет самым внутренним таким классом или интерфейсом. Тип для поиска — это тип E
.this(§15.8.4).Эта стратегия поиска называется «правилом гребенки». Она фактически ищет методы в иерархии суперклассов вложенного класса перед поиском методов во внешнем классе и его иерархии суперклассов. См. §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.[ПараметрыТипов] Идентификатор, то тип для поиска — тип непосредственного суперкласса класса, в объявлении которого содержится вызов метода.Пусть E будет объявлением класса или интерфейса, непосредственно окружающим вызов метода. Ошибка компиляции, если E — класс
Objectили интерфейс. -
Если форма имеет вид ИмяТипа
.super.[ПараметрыТипов] Идентификатор, то:-
Ошибка компиляции, если ИмяТипа обозначает не класс и не интерфейс.
-
Если ИмяТипа обозначает класс C, то тип для поиска — тип непосредственного суперкласса C.
Ошибка компиляции, если C не является лексически вложенным классом объявления метода вызова, или если C — класс
Object.Пусть E — класс или интерфейс, непосредственно окружающий вызов метода. Ошибка компиляции, если E — класс
Object. -
В противном случае ИмяТипа обозначает интерфейс I.
Пусть E — класс или интерфейс, непосредственно окружающий вызов метода. Ошибка компиляции, если I не является непосредственным суперинтерфейсом E, или если существует другой непосредственный суперкласс или непосредственный суперинтерфейс E, J, такой что J является подклассом или подинтерфейсом I.
Тип для поиска — это тип I, являющийся непосредственным типом суперинтерфейса E.
-
Синтаксис ИмяТипа . 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.27.1) или неточные ссылки на методы (§15.13.1), игнорируются тестами применимости, потому что их значение нельзя определить до тех пор, пока не будет выбран целевой тип вызова. С другой стороны, только выражения аргументов – а не целевой тип вызова – влияют на тесты применимости, даже если выражение вызова метода является поливыражением.
Процесс определения применимости начинается с определения потенциально применимых методов (§15.12.2.1). Затем, чтобы обеспечить совместимость с языком программирования Java до Java SE 5.0, процесс продолжается в трех фазах:
-
В первой фазе выполняется разрешение перегрузки без разрешения преобразований boxing или unboxing, или использования вызовов методов с переменным числом аргументов. Если примененный метод не найден на этой фазе, обработка переходит ко второй фазе.
Это гарантирует, что любые вызовы, которые были допустимы в языке программирования Java до Java SE 5.0, не считаются неоднозначными в результате введения методов с переменным числом аргументов, неявного boxing и/или unboxing. Однако объявление метода с переменным числом аргументов (§8.4.1) может изменить метод, выбранный для данного выражения вызова метода, потому что метод с переменным числом аргументов обрабатывается как метод с фиксированным числом аргументов на первой фазе. Например, объявление
m(Object...)в классе, который уже объявляетm(Object), приводит к тому, чтоm(Object)больше не выбирается для некоторых выражений вызова (таких какm(null)), так какm(Object[])более конкретный. -
На второй фазе выполняется разрешение перегрузки, позволяющее использовать boxing и unboxing, но все еще исключая использование вызовов методов с переменным числом аргументов. Если применимый метод не найден на этой фазе, обработка переходит к третьей фазе.
Это гарантирует, что метод никогда не выбирается с помощью вызова метода с переменным числом аргументов, если он применим с помощью вызова метода с фиксированным числом аргументов.
-
На третьей фазе разрешается комбинировать перегрузку с методами с переменным числом аргументов, boxing и unboxing.
Метод применим, если он применим по одному из строгих вызовов (первая фаза, §15.12.2.2), свободных вызовов (вторая фаза, §15.12.2.3) или вызовов с переменным числом аргументов (третья фаза, §15.12.2.4). Определение того, применим ли метод, в случае универсальных методов (§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 потребовалось бы сужающее приведение:
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) в объявлении члена, от наследования члена классом или интерфейсом, определённым на этапе компиляции 1, и от того, где появляется вызов метода. -
Если член — метод с фиксированной арностью 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) потенциально совместимо с типом функционального интерфейса T (§9.8), если выполняются все следующие условия:
-
Арность типа функции T (§9.9) совпадает с арностью выражения лямбда-функции.
-
Если тип функции T имеет
voidвозвращаемое значение, то тело лямбда-функции — это выражение-оператор (§14.8) или блок, совместимый с void (§15.27.2). -
Если тип функции T имеет (не-
void) возвращаемый тип, то тело лямбда-функции — это выражение или блок, совместимый со значением (§15.27.2).
-
-
Выражение ссылки на метод (§15.13) потенциально совместимо с типом функционального интерфейса T, если, где арность типа функции T равна n, существует по крайней мере один потенциально применимый метод, когда выражение ссылки на метод направлено на тип функции с арностью n (§15.13.1), и выполняется одно из следующих условий:
-
Выражение ссылки на метод имеет вид ТипСсылка
::[АргументыТипа] Идентификатор и по крайней мере один потенциально применимый метод либо (i)staticи поддерживает арность n, либо (ii) неstaticи поддерживает арность n-1. -
Выражение ссылки на метод имеет другой вид, и по крайней мере один потенциально применимый метод не
static.
-
-
Выражение лямбда-функции или выражение ссылки на метод потенциально совместимо с переменной типа, если переменная типа — параметр типа кандидатного метода.
-
Выражение в скобках (§15.8.5) потенциально совместимо с типом, если содержащееся в нём выражение совместимо с этим типом.
-
Условное выражение (§15.25) потенциально совместимо с типом, если каждое из его второго и третьего операндных выражений потенциально совместимо с этим типом.
-
Выражение switch (§15.28) потенциально совместимо с типом, если все его результирующие выражения потенциально совместимы с этим типом.
-
Выражение создания экземпляра класса, выражение вызова метода или выражение автономной формы (§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](§5.3). -
Для 1 ≤ l ≤ p, Ul
<:Bl[R1:=U1, ..., Rp:=Up].
-
-
Если
mне является обобщенным методом, тоmявляется применимым по строгому вызову, если для 1 ≤ i ≤ n, либоeiсовместим в контексте строгого вызова с Fi (§5.3), либоeiне относится к применимости.
Если не найден ни один метод, применимый по строгому вызову, поиск применимых методов продолжается на фазе 2 (§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](§5.3). -
Для 1 ≤ l ≤ p, Ul
<:Bl[R1:=U1, ..., Rp:=Up].
-
-
Если
mне является обобщенным методом, тоmявляется применимым по свободному вызову, если для 1 ≤ i ≤ n, либоeiсовместим в контексте свободного вызова с Fi (§5.3), либоeiне относится к применимости.
Если не найден ни один метод, применимый по свободному вызову, поиск применимых методов продолжается на фазе 3 (§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](§5.3). -
Для 1 ≤ l ≤ p, Ul
<:Bl[R1:=U1, ..., Rp:=Up].
-
-
Если
mне является методом с дженериками, тоmявляется применимым по вызову с переменным числом аргументов, если для 1 ≤ i ≤ k либоeiсовместим в контексте слабого вызова с Ti (§5.3), либо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, если все следующие условия выполняются:
-
Интерфейс S не является ни суперинтерфейсом, ни субинтерфейсом интерфейса T.
Если S или T являются пересекающимися типами, то никакой интерфейс S не является суперинтерфейсом или субинтерфейсом любого интерфейса T. (Интерфейсы пересекающегося типа здесь относятся к множеству интерфейсов, которые появляются как (возможно параметризованные) типы интерфейсов в пересечении.)
-
Пусть MTS — тип функции захвата S, а MTT — тип функции T. MTS и MTT должны иметь одинаковые параметры типа (если таковые имеются) (§8.4.4).
-
Пусть P1, ..., Pn — формальные типы параметров MTS, адаптированные к параметрам типа MTT. Пусть P1', ..., Pn' — формальные типы параметров типа функции S (без захвата), адаптированные к параметрам типа MTT. Пусть Q1, ..., Qn — формальные типы параметров MTT. Тогда для всех i (1 ≤ i ≤ n), Qi
<:Pi и Qi = Pi'.В общем случае это правило утверждает, что формальные типы параметров, полученные из S и T, совпадают. Однако в случае, когда S является типом с параметрами-замещающими переменными, проверка является более сложной, чтобы позволить переменным захвата появляться в формальных типах параметров: во-первых, каждый формальный тип параметра T должен быть подтипом соответствующего формального типа параметра захвата S; во-вторых, после сопоставления подстановочных переменных с их границами (§9.9), формальные типы параметров результирующих типов функций совпадают.
-
Пусть RS — тип возвращаемого значения MTS, адаптированный к параметрам типа MTT, а RT — тип возвращаемого значения MTT. Должно выполняться одно из следующих условий:
-
eявляется явным лямбда-выражением (§15.27.1), и выполняется одно из следующих условий:-
RT является
void. -
RS
<:RT. -
RS и RT являются типами функциональных интерфейсов, и существует по крайней мере одно выражение результата, и RS более специфичен, чем RT для каждого выражения результата
e.Выражение результата лямбда-выражения с блочным телом определено в §15.27.2; выражение результата лямбда-выражения с выражением тела — это просто само тело.
-
RS является примитивным типом, а RT — ссылочным типом, и существует по крайней мере одно выражение результата, и каждое выражение результата
eявляется автономным выражением (§15.2) примитивного типа. -
RS является ссылочным типом, а RT — примитивным типом, и существует по крайней мере одно выражение результата, и каждое выражение результата
eявляется либо автономным выражением ссылочного типа, либо поли-выражением.
-
-
eявляется точным выражением ссылки на метод (§15.13.1), и выполняется одно из следующих условий:-
RT является
void. -
RS
<:RT. -
RS является примитивным типом, RT — ссылочным типом, и объявление метода для ссылки на метод на этапе компиляции имеет тип возвращаемого значения, являющийся примитивным типом.
-
RS является ссылочным типом, RT — примитивным типом, и объявление метода для ссылки на метод на этапе компиляции имеет тип возвращаемого значения, являющийся ссылочным типом.
-
-
eявляется выражением в скобках, и одно из этих условий применяется рекурсивно к содержащемуся выражению. -
eявляется условным выражением, и для каждого из второго и третьего операндов одно из этих условий применяется рекурсивно. -
eявляетсяswitchвыражением, и для каждого из его выражений результата одно из этих условий применяется рекурсивно.
-
Метод m1 является строго более специфичным, чем другой метод m2, тогда и только тогда, когда m1 является более специфичным, чем m2, и m2 не является более специфичным, чем m1.
Говорят, что метод является максимально специфичным для вызова метода, если он доступен и применим, и нет другого метода, который является доступным и применимым и который строго более специфичен.
Если существует ровно один максимально специфичный метод, то этот метод является фактически наиболее специфичным методом; он обязательно более специфичен, чем любой другой доступный метод, который применим. Затем он подвергается дальнейшим проверкам на этапе компиляции, как указано в §15.12.3.
Возможно, что ни один метод не является наиболее специфичным, так как существуют два или более максимально специфичных метода. В этом случае:
-
Если у всех максимально специфичных методов есть эквивалентные по переопределению сигнатуры (§8.4.2), и ровно один из максимально специфичных методов является конкретным (то есть ни
abstract, ни дефолтный), то это и есть наиболее специфичный метод. -
В противном случае, если у всех максимально специфичных методов есть эквивалентные по переопределению сигнатуры, и все максимально специфичные методы являются
abstractили дефолтными, и объявления этих методов имеют одинаковые стёртые типы параметров, и по крайней мере один максимально специфичный метод является предпочтительным согласно правилам ниже, то наиболее специфичный метод выбирается произвольно среди подмножества максимально специфичных методов, которые являются предпочтительными. Наиболее специфичный метод затем считаетсяabstract.Максимально специфичный метод является предпочтительным, если у него есть:
-
подсигнатура, которая является подсигнатурой сигнатуры каждого максимально специфичного метода; и
-
тип возвращаемого значения R (возможно
void), где либо R такой же, как и у каждого максимально специфичного метода, либо R является ссылочным типом и является подтипом каждого возвращаемого типа максимально специфичных методов (после адаптации для любых параметров типа (§8.4.4), если у двух методов одинаковая сигнатура).
Если ни один метод не является предпочтительным по вышеуказанным правилам, то максимально специфичный метод является предпочтительным, если он:
-
имеет сигнатуру, являющуюся подсигнатурой сигнатуры каждого максимально специфичного метода; и
-
является замещаемым по возвращаемому типу (§8.4.5) для каждого максимально специфичного метода.
Типы выбрасываемых исключений наиболее специфичного метода выводятся из
throwsпунктов максимально специфичных методов следующим образом:-
Если наиболее специфичный метод является обобщенным, то
throwsпункты сначала адаптируются к параметрам типа наиболее специфичного метода (§8.4.4).Если наиболее специфичный метод не является обобщенным, но по крайней мере один максимально специфичный метод является обобщенным, то
throwsпункты сначала стираются. -
Затем, типы выбрасываемых исключений включают каждый тип E, который удовлетворяет следующим ограничениям:
-
E упоминается в одном из
throwsпунктов. -
Для каждого
throwsпункта, E является подтипом некоторого типа, указанного в этом пункте.
-
Эти правила вывода типа единственного метода из группы перегруженных методов также используются для определения типа функции функционального интерфейса (§9.9).
-
-
В противном случае, вызов метода является неопределённым, и возникает ошибка компиляции.
Тип вызова наиболее специфичного доступного и применимого метода — это тип метода (§8.2), который выражает целевые типы аргументов вызова, результат (тип возвращаемого значения или void) вызова и типы исключений вызова. Он определяется следующим образом:
-
Если выбранный метод является обобщенным, и вызов метода не предоставляет явных аргументов типа, тип вызова выводится, как указано в §18.5.2.
В этом случае, если выражение вызова метода является поли-выражением, то его совместимость с целевым типом определяется, как указано в §18.5.2.1.
Проверка совместимости с целевым типом может выполняться несколько раз, прежде чем будет принято окончательное решение о целевом типе и типе вызова выражения вызова метода. Например, окружающее выражение вызова метода может потребовать проверки более глубокого выражения вызова метода на совместимость с различными типами формальных параметров методов.
-
Если выбранный метод является обобщенным и вызов метода предоставляет явные аргументы типа, пусть 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, недоступен (§6.6) в момент вызова.
Если объявление на этапе компиляции является void, то вызов метода должен быть выражением верхнего уровня (то есть, выражение в операторе выражения или в части ForInit или ForUpdate for оператора), иначе возникает ошибка на этапе компиляции. Такой вызов метода не производит никакого значения и поэтому должен использоваться только в ситуациях, где значение не требуется.
Кроме того, вопрос о том, является ли объявление на этапе компиляции подходящим, может зависеть от формы выражения вызова метода перед левой скобкой, как следует:
-
Если форма представляет собой ИмяМетода - то есть, просто Идентификатор - и объявление на этапе компиляции является методом экземпляра, то:
-
Ошибка на этапе компиляции, если вызов метода происходит в статическом контексте (§8.1.3).
-
В противном случае пусть T будет классом или интерфейсом для поиска (§15.12.1). Ошибка на этапе компиляции, если внутреннее вложенное объявление класса или интерфейса вызова метода не является T или внутренним классом T.
-
-
Если форма представляет собой ИмяТипа
.[АргументыТипов] Идентификатор, то объявление на этапе компиляции должно бытьstatic, иначе возникает ошибка на этапе компиляции. -
Если форма представляет собой ИмяВыражения
.[АргументыТипов] Идентификатор или Первичное.[АргументыТипов] Идентификатор, то объявление на этапе компиляции не должно быть методомstatic, объявленным в интерфейсе, иначе возникает ошибка на этапе компиляции. -
Если форма представляет собой
super.[АргументыТипов] Идентификатор, то:-
Ошибка на этапе компиляции, если объявление на этапе компиляции является
abstract. -
Ошибка на этапе компиляции, если вызов метода происходит в статическом контексте.
-
-
Если форма представляет собой ИмяТипа
.super.[АргументыТипов] Идентификатор, то:-
Ошибка на этапе компиляции, если объявление на этапе компиляции является
abstract. -
Ошибка на этапе компиляции, если вызов метода происходит в статическом контексте.
-
Если ИмяТипа обозначает класс C, то если объявление класса или интерфейса, непосредственно окружающего вызов метода, не является C или внутренним классом C, возникает ошибка на этапе компиляции.
-
Если ИмяТипа обозначает интерфейс, пусть E будет объявлением класса или интерфейса, непосредственно окружающим вызов метода. Возникает ошибка на этапе компиляции, если существует метод, отличный от объявления на этапе компиляции, который переопределяет (§9.4.1) объявление на этапе компиляции из непосредственного суперкласса или непосредственного суперинтерфейса E.
В случае, когда суперинтерфейс переопределяет метод, объявленный в интерфейсе предка, это правило предотвращает «пропуск» переопределения дочерним интерфейсом, просто добавив предка в его список непосредственных суперинтерфейсов. Правильный способ доступа к функциональности предка – через непосредственный суперинтерфейс, и только если этот интерфейс решает раскрыть желаемое поведение. (В альтернативном варианте программист свободен определить дополнительный суперинтерфейс, который раскрывает желаемое поведение с помощью
superвызова метода.)
-
Типы параметров на этапе компиляции и результат на этапе компиляции определяются следующим образом:
-
Если объявление на этапе компиляции для вызова метода не является методом полиморфной сигнатуры, то:
-
Типы параметров на этапе компиляции являются типами формальных параметров объявления на этапе компиляции.
-
Результат на этапе компиляции – результат типа вызова объявления на этапе компиляции (§15.12.2.6).
-
-
Если объявление на этапе компиляции для вызова метода является методом полиморфной сигнатуры, то:
-
Типы параметров на этапе компиляции являются типами выражений фактических аргументов. Выражение аргумента, являющееся нулевым литералом
null(§3.10.8), обрабатывается как имеющий типVoid. -
Результат на этапе компиляции определяется следующим образом:
-
Если метод полиморфной сигнатуры является либо
void, или имеет тип возврата, отличный отObject, результат на этапе компиляции – результат типа вызова объявления на этапе компиляции (§15.12.2.6). -
В противном случае, если выражение вызова метода является оператором выражения, результат на этапе компиляции –
void. -
В противном случае, если выражение вызова метода является операндом выражения приведения типа (§15.16), результат на этапе компиляции – стирание типа выражения приведения типа (§4.6).
-
В противном случае, результат на этапе компиляции – тип возврата метода полиморфной сигнатуры,
Object.
-
-
Метод является полиморфным по сигнатуре, если все следующие утверждения верны:
-
Он объявлен в классе
java.lang.invoke.MethodHandleили классеjava.lang.invoke.VarHandle. -
Он имеет единственный параметр с переменным числом аргументов (§8.4.1), тип которого объявлен как
Object[]. -
Он является
native.
Затем с вызовом метода связывается следующая информация на этапе компиляции для использования во время выполнения:
-
Имя метода.
-
Квалифицирующий класс или интерфейс вызова метода ( §13.1).
-
Количество параметров и типы параметров во время компиляции, по порядку.
-
Результат времени компиляции.
-
Режим вызова, вычисляемый следующим образом:
-
Если объявление во время компиляции имеет модификатор
static, то режим вызова равенstatic. -
В противном случае, если часть вызова метода перед левой скобкой имеет форму
super.Identifier или форму TypeName.super.Identifier, то режим вызова равенsuper. -
В противном случае, если квалифицирующий класс или интерфейс вызова метода на самом деле является интерфейсом, то режим вызова равен
interface. -
В противном случае режим вызова равен
virtual.
-
Если результат типа вызова объявления во время компиляции не является void, то тип выражения вызова метода получается путем применения преобразования захвата (§5.1.10) к типу возвращаемого значения типа вызова объявления во время компиляции.
Во время выполнения вызов метода требует пяти шагов. Во-первых, может быть вычислена целевая ссылка. Во-вторых, вычисляются выражения аргументов. В-третьих, проверяется доступность вызываемого метода. В-четвертых, находится фактический код для выполнения метода. В-пятых, создается новый кадр активации, при необходимости выполняется синхронизация, и управление передается в код метода.
Есть шесть случаев, которые следует учитывать в зависимости от формы вызова метода:
-
Если форма представляет собой MethodName — то есть просто Identifier — то:
-
Если режим вызова равен
static, то целевая ссылка отсутствует. -
В противном случае, пусть T будет объявлением типа, членом которого является метод, и пусть n будет целым числом таким, что T является n-м лексически охватывающим объявлением типа класса, чье объявление непосредственно содержит вызов метода. Целевой ссылкой является n-й лексически охватывающий экземпляр
this.Это ошибка времени компиляции, если n-й лексически охватывающий экземпляр
thisне существует.
-
-
Если форма представляет собой TypeName
.[TypeArguments] Identifier, то целевая ссылка отсутствует. -
Если форма представляет собой ExpressionName
.[TypeArguments] Identifier, то:-
Если режим вызова равен
static, то целевая ссылка отсутствует. ExpressionName вычисляется, но результат затем отбрасывается. -
В противном случае целевой ссылкой является значение, обозначенное ExpressionName.
-
-
Если форма представляет собой Primary
.[TypeArguments] Identifier, то:-
Если режим вызова равен
static, то целевая ссылка отсутствует. Выражение Primary вычисляется, но результат затем отбрасывается. -
В противном случае выражение Primary вычисляется, и результат используется в качестве целевой ссылки.
В любом случае, если вычисление выражения Primary завершается аварийно, то ни одна часть какого-либо выражения аргумента, похоже, не была вычислена, и вызов метода завершается аварийно по той же причине.
-
-
Если форма представляет собой
super.[TypeArguments] Identifier, то целевой ссылкой является значениеthis. -
Если форма представляет собой TypeName
.super.[TypeArguments] Identifier, то если TypeName обозначает класс, целевой ссылкой является значение TypeName.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 формальных параметров, и тип n-го параметра типа вызова (§15.12.2.6) m обязательно имеет тип T[] для некоторого T. Пусть k - количество фактических выражений аргументов в вызове метода: если k ≠ n, или если k = n и тип k-го выражения аргумента не совместим с присваиванием с T[], тогда m был признан применимым путём вызова с переменной арностью (§15.12.2.4). В этом случае список аргументов (e1, ..., en-1, en, ..., ek) оценивается так, как если бы он был записан как (e1, ..., en-1, new |T[]| { en, ..., ek }), где |T[]| обозначает стирание (§4.6) T[].
Предшествующий абзац составлен для обработки взаимодействия параметризованных типов и массивов, которое происходит в виртуальной машине Java с потёртыми дженериками. А именно, если элементный тип T параметра переменного массива не может быть реализован, например List<String>, то необходимо проявлять особую осторожность при создании выражения массива (§15.10), так как тип элемента созданного массива должен быть реализуем. Стиранием типа массива конечного выражения в списке аргументов гарантируется получение реализуемого типа элемента. Затем, так как выражение создания массива появляется в контексте вызова (§5.3), возможна неявная конвертация от типа массива с реализуемым типом элемента к типу массива с нереализуемым типом элемента, конкретно к типу параметра переменной арности. Компилятор Java обязан выдать предупреждение о неявной конвертации на этапе компиляции. Справочная реализация компилятора Java от Oracle идентифицирует это предупреждение о неявной конвертации как более информативное создание нереализуемых массивов дженериков.
Выражения аргументов (возможно, переписанные, как описано выше) теперь оцениваются, чтобы получить значения аргументов. Каждое значение аргумента соответствует ровно одному из n формальных параметров метода.
Выражения аргументов, если таковые имеются, оцениваются в порядке следования слева направо. Если оценка любого выражения аргумента завершается прерыванием, то никакая часть любого выражения аргумента справа от него не оценивается, и вызов метода завершается прерыванием по той же причине. Результатом оценки j-го выражения аргумента является j-е значение аргумента для 1 ≤ j ≤ n. Далее оценка продолжается, используя значения аргументов, как описано ниже.
В этом разделе:
Реализация языка программирования Java должна гарантировать, что класс или интерфейс Q доступен на этапе линковки:
-
Если Q находится в том же пакете, что и D, то Q доступен.
-
Если Q находится в другом пакете, чем D, и их пакеты находятся в одном модуле, и Q является
publicилиprotected, то Q доступен. -
Если Q находится в другом пакете, чем D, и их пакеты находятся в разных модулях, и модуль Q экспортирует пакет Q в модуль D, и Q является
publicилиprotected, то Q доступен.
Если Q является protected, он обязательно является вложенным классом или интерфейсом, поэтому на этапе компиляции его доступность зависит от доступности окружающих классов и интерфейсов. Однако во время линковки его доступность не зависит от доступности окружающих классов и интерфейсов. Кроме того, во время линковки доступность protected Q такая же, как и доступность public Q. Эти расхождения в контроле доступа на этапе компиляции (§6.6) и контроле доступа во время выполнения обусловлены ограничениями виртуальной машины Java.
Реализация должна также гарантировать, что метод m всё ещё может быть найден в Q или в суперклассе или суперинтерфейсе Q. Если m не может быть найден, то происходит NoSuchMethodError (который является подклассом IncompatibleClassChangeError). Если m может быть найден, то пусть C - класс или интерфейс, в котором объявлен m. Реализация должна гарантировать, что объявление m в C доступно для D:
-
Если
mявляетсяpublic, тоmдоступен. -
Если
mявляетсяprotected, тоmдоступен тогда и только тогда, когда (i) либо D находится в том же пакете, что и C, либо D является подклассом C или C сам по себе; и (ii) еслиmявляетсяprotectedэкземпляром метода, то Q должен быть подклассом D или D сам по себе.Это единственное место, где Q участвует в проверке
m, так как экземпляр методаprotectedможет вызываться только через квалифицирующий класс или интерфейс, соответствующий типу вызывающего объекта. -
Если у
mесть доступ к пакету, тоmдоступен тогда и только тогда, когда D находится в том же пакете, что и C. -
Если
mявляетсяprivate, тоmдоступен тогда и только тогда, когда D равен C, или D содержит C, или C содержит D, или C и D оба содержатся третьим классом или интерфейсом.
Если Q или m не доступен, возникает IllegalAccessError (§12.3).
Если режим вызова является interface, то реализация должна проверить, что целевой класс ссылки всё ещё реализует указанный интерфейс. Если целевой класс ссылки больше не реализует интерфейс, то возникает IncompatibleClassChangeError.
Как и в предыдущем разделе (§15.12.4.3):
-
Пусть Q — квалифицирующий класс или интерфейс вызова метода (§13.1).
-
Пусть
m— метод, найденный в Q или в его суперклассе или суперинтерфейсе. (Обратите внимание, чтоmбыл лишь именем метода в предыдущем разделе; здесь это фактическое объявление.) -
Пусть C — класс или интерфейс, который объявляет
m.
Стратегия поиска вызываемого метода зависит от режима вызова:
-
Если режим вызова —
static, то ссылка на целевой объект не нужна, и переопределение запрещено. Вызываемым методом будет методmкласса или интерфейса C. -
В противном случае вызывается метод экземпляра, и есть ссылка на целевой объект. Если ссылка на целевой объект —
null, в этот момент будет брошено исключениеNullPointerException. В противном случае ссылка на целевой объект относится к целевому объекту и будет использована в качестве значения ключевого словаthisв вызываемом методе. Затем рассматриваются три других возможности для режима вызова:-
Если режим вызова —
super, переопределение запрещено. Вызываемым методом будет методmкласса или интерфейса C. Еслиmравноabstract, будет брошено исключениеAbstractMethodError. -
В противном случае, если режим вызова —
virtual, а Q иmсовместно указывают на полиморфный метод сигнатуры (§15.12.3), то целевой объект является экземпляромjava.lang.invoke.MethodHandleилиjava.lang.invoke.VarHandle. Целевой объект encapsulates состояние, которое сопоставляется с информацией, связанной с вызовом метода во время компиляции. Подробности этого сопоставления приведены в спецификации Java Virtual Machine, Java SE 21 Edition, и API платформы Java SE. Если сопоставление успешно, то либо метод, на который ссылается экземплярjava.lang.invoke.MethodHandle, вызывается непосредственно и немедленно, либо переменная, представленная экземпляромjava.lang.invoke.VarHandle, обращается к ней напрямую и немедленно, и в обоих случаях процедура в §15.12.4.5 не выполняется. Если сопоставление не выполняется, будет брошено исключениеjava.lang.invoke.WrongMethodTypeException. -
В противном случае режим вызова —
interfaceилиvirtual.Если метод
mкласса или интерфейса C —private, то это вызываемый метод.В противном случае может произойти переопределение. Используется динамический поиск метода, описанный ниже, для определения вызываемого метода. Процедура поиска начинается с класса R, фактического класса времени выполнения целевого объекта.
Обратите внимание, что для режима вызова
interface, R обязательно реализует Q; для режима вызоваvirtual, R обязательно равен Q или является подклассом Q. Если целевой объект является массивом, то R — «класс», представляющий тип массива.
-
Процедура динамического поиска метода следующая. Пусть S — класс для поиска, начиная с R. Тогда:
-
Если класс S содержит объявление метода, который переопределяет метод
mкласса или интерфейса C из R (§8.4.8.1), то этот переопределяющий метод является вызываемым методом, и процедура завершается. -
В противном случае, если у S есть суперкласс, то шаги 1 и 2 этой процедуры поиска выполняются рекурсивно с использованием непосредственного суперкласса S вместо S; вызываемый метод, если таковой имеется, является результатом рекурсивного вызова этой процедуры поиска.
-
Если ни один метод не найден на предыдущих двух шагах, то в суперинтерфейсах S ищется подходящий метод.
Рассматривается набор кандидатов-методов с следующими свойствами: (i) каждый метод объявлен в (прямом или косвенном) суперинтерфейсе R; (ii) каждый метод имеет имя и дескриптор, необходимые для вызова метода; (iii) каждый метод не является не-
staticи не является не-private; (iv) для каждого метода, где интерфейс объявления метода — I, нет другого метода, удовлетворяющего (i) – (iii), который объявлен в подинтерфейсе I.Если этот набор содержит метод по умолчанию, такой метод является вызываемым методом. В противном случае метод, не являющийся методом по умолчанию, из набора выбирается в качестве вызываемого метода.
Динамический поиск метода может вызвать следующие ошибки:
-
Если вызываемый метод —
abstract, будет брошено исключениеAbstractMethodError. -
Если вызываемый метод —
default, и в наборе кандидатов на шаге 3 выше появляется более одного метода по умолчанию, будет брошено исключениеIncompatibleClassChangeError. -
Если режим вызова —
interface, и вызываемый метод не является ниpublic, ниprivate, будет брошено исключениеIllegalAccessError.
Вышеописанная процедура (если она завершится без ошибок) найдёт не-abstract, доступный метод для вызова, при условии, что все классы и интерфейсы в программе были корректно скомпилированы. Однако, если это не так, могут возникнуть различные ошибки, как указано выше; дополнительные сведения о поведении Java Virtual Machine в этих обстоятельствах приведены в спецификации Java Virtual Machine, Java SE 21 Edition.
Процесс динамического поиска, хотя и описан здесь явно, часто будет реализован неявно, например, как побочный эффект построения и использования таблиц диспетчеризации методов на основе класса или построения других структур на основе класса, используемых для эффективной диспетчеризации.
Пример 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).
Если стёртый вид (§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.6, §5.1.9, §8.4.1, §8.4.8.3, §15.13.2, §15.12.4.2, §15.27.3).
Реализации могут обеспечить эти семантики, создавая мосты методов. В приведённом примере в классе D будет создан следующий мост метода:
Object id(Object x) { return id((String) x); }
Это метод, который фактически будет вызван виртуальной машиной Java в ответ на вызов c.id(new
Object()), показанный выше, и он выполнит приведение типов и завершится ошибкой, как требуется.
Выражение ссылки на метод используется для ссылки на вызов метода без фактического выполнения вызова. Определенные формы выражения ссылки на метод также позволяют создавать экземпляры класса (§15.9) или создавать массивы (§15.10), как будто это был вызов метода.
:: [АргументыТипов] Идентификатор Основное
:: [АргументыТипов] Идентификатор ТипСсылка
:: [АргументыТипов] Идентификатор super :: [АргументыТипов] Идентификатор ИмяТипа
. super :: [АргументыТипов] Идентификатор ТипКласса
:: [АргументыТипов] new ТипМассива
:: new Если АргументыТипов присутствует справа от ::, то это ошибка времени компиляции, если любой из аргументов типов является неопределенным (§4.5.1).
Если выражение ссылки на метод имеет вид ИмяВыражения :: [АргументыТипов] Идентификатор или Основное :: [АргументыТипов] Идентификатор, то это ошибка времени компиляции, если тип ИмяВыражения или Основного не является типом ссылки.
Если выражение ссылки на метод имеет вид super :: [АргументыТипов] Идентификатор, пусть E — объявление класса или интерфейса, непосредственно окружающего выражение ссылки на метод. Это ошибка времени компиляции, если E — класс Object или если E — интерфейс.
Если выражение ссылки на метод имеет вид ИмяТипа . super :: [АргументыТипов] Идентификатор, то:
-
Если ИмяТипа обозначает класс C, то это ошибка времени компиляции, если C не является лексически окружающим классом текущего класса или если C — класс
Object. -
Если ИмяТипа обозначает интерфейс I, то пусть E — объявление класса или интерфейса, непосредственно окружающего выражение ссылки на метод. Это ошибка времени компиляции, если I не является непосредственным суперинтерфейсом E или если существует какой-либо другой непосредственный суперкласс или непосредственный суперинтерфейс E, J, такой, что J является подклассом или подинтерфейсом I.
-
Если ИмяТипа обозначает переменную типа, то происходит ошибка времени компиляции.
Если выражение ссылки на метод имеет вид super :: [АргументыТипов] Идентификатор или ИмяТипа . super :: [АргументыТипов] Идентификатор, то это ошибка времени компиляции, если выражение встречается в статическом контексте (§8.1.3).
Если выражение ссылки на метод имеет вид ТипКласса :: [АргументыТипов] new, то:
-
ТипКласса должен называть доступный (§6.6), не
abstractи не перечисление класс, иначе происходит ошибка времени компиляции. -
Если ТипКласса обозначает параметризованный тип (§4.5), то это ошибка времени компиляции, если любой из его аргументов типов является неопределенным.
-
Если ТипКласса обозначает сырой тип (§4.8), то это ошибка времени компиляции, если АргументыТипов присутствует после
::.
Если выражение ссылки на метод имеет вид ТипМассива :: new, то ТипМассива должен обозначать реифицируемый тип (§4.7), иначе происходит ошибка времени компиляции.
Целевая ссылка экземпляра метода (§15.12.4.1) может быть предоставлена выражением ссылки на метод с помощью ИмяВыражения, Основного или 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.3).
Поиск объявления на этапе компиляции отражает процесс поиска методов для вызовов методов в §15.12.1 и §15.12.2, как показано ниже:
-
Сначала определяется тип для поиска:
-
Если выражение ссылки на метод имеет вид ИмяВыражения
::[АргументыТипов] Идентификатор или ОсновноеВыражение::[АргументыТипов] Идентификатор, тип для поиска — это тип выражения, предшествующего токену::. -
Если выражение ссылки на метод имеет вид ТипСсылка
::[АргументыТипов] Идентификатор, тип для поиска — это результат применения преобразования захвата (§5.1.10) к ТипСсылка. -
Если выражение ссылки на метод имеет вид
super::[АргументыТипов] Идентификатор, тип для поиска — это тип суперкласса немедленно окружающего объявления класса или интерфейса выражения ссылки на метод.Пусть T — это объявление класса или интерфейса, непосредственно окружающее выражение ссылки на метод. Ошибка компиляции, если T — это класс
Objectили интерфейс. -
Если выражение ссылки на метод имеет вид ИмяТипа
.super::[АргументыТипов] Идентификатор, то если ИмяТипа обозначает класс, тип для поиска — это тип суперкласса указанного класса; в противном случае ИмяТипа обозначает интерфейс для поиска.Ошибка компиляции, если ИмяТипа не является лексически окружающим объявлением класса или интерфейса выражения ссылки на метод, а также непосредственным суперинтерфейсом непосредственно окружающего объявления класса или интерфейса выражения ссылки на метод.
Ошибка компиляции, если ИмяТипа — это класс Object.
Ошибка компиляции, если ИмяТипа — это интерфейс, и существует другой непосредственный суперкласс или непосредственный суперинтерфейс непосредственно окружающего объявления класса или интерфейса выражения ссылки на метод, J, такой, что J является подклассом или подинтерфейсом ИмяТипа.
-
Для двух других форм (с участием
::new) метод, на который ссылаются, является условным, и тип для поиска не существует.
-
-
Во-вторых, для заданного целевого типа функции с n параметрами определяется набор потенциально применимых методов:
-
Если выражение ссылки на метод имеет вид ТипСсылка
::[АргументыТипов] Идентификатор, потенциально применимые методы:-
методы-члены типа для поиска, которые потенциально применимы (§15.12.2.1) для вызова метода, который называет Идентификатор, имеет арность n, имеет аргументы типа АргументыТипов и появляется в том же классе, что и выражение ссылки на метод; плюс
-
методы-члены типа для поиска, которые потенциально применимы для вызова метода, который называет Идентификатор, имеет арность n-1, имеет аргументы типа АргументыТипов и появляется в том же классе, что и выражение ссылки на метод.
Рассматриваются две разные арности, n и n-1, чтобы учесть возможность того, что эта форма относится либо к методу-
static, либо к методу экземпляра. -
-
Если выражение ссылки на метод имеет вид ТипКласса
::[АргументыТипов]new, потенциально применимые методы — это набор условных методов, соответствующих конструкторам ТипКласса.Если ТипКласса является сырым типом, но не является не-
staticчленом типа сырого типа, кандидатными методами-членами являются те, которые указаны в §15.9.3 для выражения создания экземпляра класса, которое использует<>для исключения аргументов типа класса. В противном случае, кандидатные методы-члены — это конструкторы ТипКласса, рассматриваемые как методы с типом возврата ТипКласса.Среди этих кандидатов потенциально применимые методы — это условные методы, которые были бы потенциально применимы для вызова метода, имеющего арность n, аргументы типа АргументыТипов и находящегося в том же классе, что и выражение ссылки на метод.
-
Если выражение ссылки на метод имеет вид ТипМассива
::new, рассматривается один условный метод. Метод имеет один параметр типаint, возвращает ТипМассива и не имеетthrows. Если n = 1, это единственный потенциально применимый метод; в противном случае потенциально применимых методов нет. -
Для всех остальных форм потенциально применимые методы — это методы-члены типа для поиска, которые потенциально применимы для вызова метода, который называет Идентификатор, имеет арность n, имеет аргумент типа АргументыТипов и появляется в том же классе, что и выражение ссылки на метод.
-
-
Наконец, если нет потенциально применимых методов, то нет объявления во время компиляции.
В противном случае, учитывая целевой тип функции с параметрами P1, ..., Pn и набор потенциально применимых методов, объявление во время компиляции выбирается следующим образом:
-
Если выражение ссылки на метод имеет вид ReferenceType
::[TypeArguments] Identifier, то выполняются два поиска наиболее специфичного применимого метода. Каждый поиск выполняется, как указано в §15.12.2.2 по §15.12.2.5 с пояснениями ниже. Каждый поиск генерирует набор применимых методов и, возможно, выделяет наиболее специфичный метод из набора. В случае ошибки, как указано в §15.12.2.4, набор применимых методов пуст. В случае ошибки, как указано в §15.12.2.5, нет наиболее специфичного метода.В первом поиске ссылка на метод обрабатывается так, как если бы это было обращение с выражениями аргументов типов P1, ..., Pn. Типовые аргументы, если таковые имеются, задаются выражением ссылки на метод.
Во втором поиске, если P1, ..., Pn не пусто и P1 является подтипом ReferenceType, то выражение ссылки на метод обрабатывается так, как если бы это было выражение вызова метода с выражениями аргументов типов P2, ..., Pn. Если ReferenceType является сырым типом, и существует параметризация этого типа, G
<...>, который является надтипом P1, тип для поиска — результат преобразования захвата (§5.1.10), применённого к G<...>; в противном случае, тип для поиска такой же, как тип первого поиска. Типовые аргументы, если таковые имеются, задаются выражением ссылки на метод.Если первый поиск приводит к наиболее специфичному методу, который является
static, и набор применимых методов, полученный во втором поиске, не содержит методов, отличных отstatic, то объявление во время компиляции — это наиболее специфичный метод первого поиска.В противном случае, если набор применимых методов, полученный в первом поиске, не содержит
staticметодов, а второй поиск даёт наиболее специфичный метод, который не являетсяstatic, то объявление во время компиляции — это наиболее специфичный метод второго поиска.В противном случае, объявление во время компиляции отсутствует.
-
Для всех других форм выражения ссылки на метод выполняется один поиск наиболее специфичного применимого метода. Поиск выполняется, как указано в §15.12.2.2 по §15.12.2.5 с пояснениями ниже.
Ссылка на метод обрабатывается так, как если бы это было обращение с выражениями аргументов типов P1, ..., Pn; типовые аргументы, если таковые имеются, задаются выражением ссылки на метод.
Если поиск приводит к ошибке, как указано в §15.12.2.2 по §15.12.2.5, или если наиболее специфичный применимый метод является
static, то объявление во время компиляции отсутствует.В противном случае, объявление во время компиляции — это наиболее специфичный применимый метод.
-
Ошибка во время компиляции, если выражение ссылки на метод имеет вид ReferenceType :: [TypeArguments] Identifier, и объявление во время компиляции является static, и ReferenceType не является простым или квалифицированным именем (§6.2).
Ошибка во время компиляции, если выражение ссылки на метод имеет вид super :: [TypeArguments] Identifier или TypeName . super :: [TypeArguments] Identifier, и объявление во время компиляции является abstract.
Ошибка во время компиляции, если выражение ссылки на метод имеет вид super :: [TypeArguments] Identifier или TypeName . super :: [TypeArguments] Identifier, и выражение ссылки на метод используется в статическом контексте (§8.1.3).
Ошибка во время компиляции, если выражение ссылки на метод имеет вид TypeName . super :: [TypeArguments] Identifier, и TypeName обозначает класс C, а ближайший к выражению ссылки на метод класс или интерфейс объявления метода не является C или внутренним классом C.
Ошибка во время компиляции, если выражение ссылки на метод имеет вид TypeName . super :: [TypeArguments] Identifier, и TypeName обозначает интерфейс, и существует метод, отличный от объявления во время компиляции, который переопределяет объявление во время компиляции из непосредственного суперкласса или непосредственного суперинтерфейса класса или интерфейса, чьё объявление непосредственно включает выражение ссылки на метод (§8.4.8, §9.4.1).
Ошибка во время компиляции, если выражение ссылки на метод имеет вид ClassType :: [TypeArguments] new и произошла бы ошибка во время компиляции при определении включающей инстанции для ClassType, как указано в §15.9.2 (обрабатывая выражение ссылки на метод, как если бы это было выражение создания экземпляра класса без квалификатора).
Выражение ссылки на метод вида ReferenceType :: [TypeArguments] Identifier может интерпретироваться по-разному. Если Identifier относится к методу экземпляра, то неявная лямбда-функция имеет дополнительный параметр по сравнению с тем, если Identifier относится к методу static. Возможно, что ReferenceType имеет оба типа применимых методов, поэтому алгоритм поиска выше идентифицирует их отдельно, так как для каждого случая есть разные типы параметров.
Пример неоднозначности:
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), независимо от целевого типа функции. Такие выражения ссылки на метод называются точными. Выражение ссылки на метод, которое не является точным, называется неточным.
Выражение ссылки на метод, заканчивающееся Identifier, является точным, если оно удовлетворяет всем следующим условиям:
-
Если выражение ссылки на метод имеет вид ТипСсылы
::[АргументыТипов] Идентификатор, то ТипСсылы не обозначает необработанный тип. -
Тип для поиска имеет ровно один метод-член с именем Идентификатор, который доступен классу или интерфейсу, в котором появляется выражение ссылки на метод.
-
Этот метод не является методом с переменным числом аргументов (§8.4.1).
-
Если этот метод является обобщенным (§8.4.4), то выражение ссылки на метод предоставляет АргументыТипов.
Выражение ссылки на метод вида ТипКласса :: [АргументыТипов] new является точным, если оно удовлетворяет всем следующим условиям:
-
Тип, обозначаемый ТипКласса, не является необработанным или является не-
staticчленом типа необработанного типа. -
Тип, обозначаемый ТипКласса, имеет ровно один конструктор, который доступен классу или интерфейсу, в котором появляется выражение ссылки на метод.
-
Этот конструктор не является конструктором с переменным числом аргументов.
-
Если этот конструктор является обобщенным, то выражение ссылки на метод предоставляет АргументыТипов.
Выражение ссылки на метод вида ТипМассива :: new всегда является точным.
Выражение ссылки на метод совместимо в контексте присваивания, вызова или приведения типов с типом назначения T, если T является типом функционального интерфейса (§9.8), и выражение соответствует типу функции базового целевого типа, полученного из T.
Базовый целевой тип выводится из T следующим образом:
-
Если T является типом функционального интерфейса с параметрами-подстановочными знаками, то базовый целевой тип — это неподстановочное параметризованное представление (§9.9) T.
-
В противном случае базовый целевой тип — T.
Выражение ссылки на метод соответствует типу функции, если оба следующих утверждения истинны:
-
Тип функции идентифицирует одно объявление во время компиляции, соответствующее ссылке.
-
Выполняется одно из следующих условий:
-
Результат типа функции —
void. -
Результат типа функции — R, и результат применения преобразования захвата (§5.1.10) к типу возвращаемого значения типа вызова (§15.12.2.6) выбранного объявления во время компиляции — R' (где R — тип назначения, который можно использовать для вывода R'), и ни R, ни R' не являются
void, и R' совместим с R в контексте присваивания.
-
Если для применимости объявления во время компиляции было необходимо преобразование без проверки, и это преобразование вызовет предупреждение без проверки в контексте вызова, то возникает предупреждение без проверки во время компиляции, если оно не подавлено @SuppressWarnings (§9.6.4.5).
Если для совместимости типа возвращаемого значения R' с типом возвращаемого значения типа функции R, описанном выше, было необходимо преобразование без проверки, и это преобразование вызовет предупреждение без проверки в контексте присваивания, то возникает предупреждение без проверки во время компиляции, если оно не подавлено @SuppressWarnings.
Если выражение ссылки на метод совместимо с типом назначения T, то типом выражения U является базовый целевой тип, полученный из T.
Ошибка компиляции, если какой-либо класс или интерфейс, упомянутый в U или в типе функции U, недоступен (§6.6) из класса или интерфейса, в котором появляется выражение ссылки на метод.
Для каждого метода-члена static метода m U, если тип функции U имеет подсигнатуру сигнатуры m, то предполагаемый метод, тип которого — тип функции U, считается переопределяющим m, и могут возникнуть ошибки компиляции или предупреждения без проверки, указанные в §8.4.8.3.
Для каждого типа исключения с проверкой X, указанного в throws разделе типа вызова объявления во время компиляции, X или суперкласс 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.
Время оценки выражения ссылки на метод сложнее, чем время оценки лямбда-выражений (§15.27.4). Когда выражение ссылки на метод имеет выражение (а не тип) перед разделителем ::, это подвыражение вычисляется немедленно. Результат вычисления сохраняется до тех пор, пока не будет вызван метод соответствующего функционального интерфейсного типа; в этот момент результат используется в качестве целевой ссылки для вызова. Это означает, что выражение перед разделителем :: вычисляется только при встрече выражения ссылки на метод и не перевычисляется при последующих вызовах функционального интерфейсного типа.
Интересно сравнить обработку null здесь с ее обработкой во время вызова метода. При оценке выражения вызова метода возможно, что Основное, квалифицирующее вызов, вычисляется до null, но ни одно NullPointerException не возникает. Это происходит, когда вызываемый метод является static (несмотря на синтаксис вызова, предполагающий метод экземпляра). Поскольку применимый метод для выражения ссылки на метод, квалифицированного Основным, запрещается быть static (§15.13.1), оценка выражения ссылки на метод проще - null Основное всегда вызывает NullPointerException.
Постфиксные выражения включают использование постфиксных ++ и -- операторов. Имена не считаются первичными выражениями (§15.8), но обрабатываются отдельно в грамматике для избежания определенных неоднозначностей. Они становятся взаимозаменяемыми только здесь, на уровне приоритета постфиксных выражений.
Правила оценки имен выражений приведены в §6.5.6.
Постфиксное выражение, за которым следует оператор ++, является постфиксным выражением инкремента.
Результат постфиксного выражения должен быть переменной, конвертируемой (§5.1.8) в числовой тип, в противном случае возникает ошибка компиляции.
Тип постфиксного выражения инкремента — тип переменной. Результат постфиксного выражения инкремента — это значение, а не переменная.
Во время выполнения, если оценка операндного выражения завершается прерыванием, то постфиксное выражение инкремента завершается прерыванием по той же причине и инкрементация не выполняется. В противном случае значение 1 добавляется к значению переменной, а сумма сохраняется обратно в переменную. Перед добавлением выполняется бинарное числовое продвижение (§5.6) над значением 1 и значением переменной. При необходимости сумма сужается с помощью сужающего примитивного преобразования (§5.1.3) и/или подвергается преобразованию упаковки (§5.1.7) в тип переменной перед сохранением. Значение постфиксного выражения инкремента — значение переменной до сохранения нового значения.
Обратите внимание, что бинарное числовое продвижение, упомянутое выше, может включать преобразование распаковки (§5.1.8).
Переменная, объявленная final, не может быть инкрементирована, потому что при использовании доступа к такой final переменной как выражения результат — это значение, а не переменная. Таким образом, она не может использоваться в качестве операнда постфиксного оператора инкремента.
Постфиксное выражение, за которым следует оператор --, является постфиксным выражением декремента.
Результат постфиксного выражения должен быть переменной, конвертируемой (§5.1.8) в числовой тип, в противном случае возникает ошибка компиляции.
Тип постфиксного выражения декремента — тип переменной. Результат постфиксного выражения декремента — это значение, а не переменная.
Во время выполнения, если оценка операндного выражения завершается прерыванием, то постфиксное выражение декремента завершается прерыванием по той же причине и декрементация не выполняется. В противном случае значение 1 вычитается из значения переменной, а разность сохраняется обратно в переменную. Перед вычитанием выполняется бинарное числовое продвижение (§5.6) над значением 1 и значением переменной. При необходимости разность сужается с помощью сужающего примитивного преобразования (§5.1.3) и/или подвергается преобразованию упаковки (§5.1.7) в тип переменной перед сохранением. Значение постфиксного выражения декремента — значение переменной до сохранения нового значения.
Обратите внимание, что бинарное числовое продвижение, упомянутое выше, может включать преобразование распаковки (§5.1.8).
Переменная, объявленная final, не может быть декрементирована, потому что при использовании доступа к такой final переменной как выражения результат — это значение, а не переменная. Таким образом, она не может использоваться в качестве операнда постфиксного оператора декремента.
Операторы +, -, ++, --, ~, !, а также оператор приведения типов (§15.16) называются унарными операторами. Унарное выражение — это либо унарный оператор, применяемый к операнду, либо выражение switch (§15.28).
Выражения с унарными операторами группируются справа налево, поэтому -~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) над значением 1 и значением переменной. При необходимости сумма сужается с помощью сужающего преобразования примитивных типов (§5.1.3) и/или подвергается преобразованию упаковывания (§5.1.7) к типу переменной перед записью. Значение выражения префиксного инкремента — это значение переменной после записи нового значения.
Обратите внимание, что двоичное числовое продвижение, упомянутое выше, может включать преобразование распаковки (§5.1.8).
Переменную, объявленную final, нельзя инкрементировать, так как при использовании доступа к такой переменной как выражение, результат является значением, а не переменной. Поэтому она не может использоваться в качестве операнда оператора префиксного инкремента.
Унарное выражение, предваряемое оператором --, является выражением префиксного декремента.
Результат унарного выражения должен быть переменной типа, преобразуемого (§5.1.8) в числовой тип; в противном случае возникает ошибка компиляции.
Тип выражения префиксного декремента — это тип переменной. Результат выражения префиксного декремента — это значение, а не переменная.
Во время выполнения, если вычисление выражения-операнда завершается неожиданно, то выражение префиксного декремента завершается по той же причине, и декрементирование не выполняется. В противном случае из значения переменной вычитается значение 1, и разность записывается обратно в переменную. Перед вычитанием выполняется двоичное числовое продвижение (§5.6) над значением 1 и значением переменной. При необходимости разность сужается с помощью сужающего преобразования примитивных типов (§5.1.3) и/или подвергается преобразованию упаковывания (§5.1.7) к типу переменной перед записью. Значение выражения префиксного декремента — это значение переменной после записи нового значения.
Обратите внимание, что двоичное числовое продвижение, упомянутое выше, может включать преобразование распаковки (§5.1.8).
Переменную, объявленную final, нельзя декрементировать, так как при использовании доступа к такой переменной как выражение, результат является значением, а не переменной. Поэтому она не может использоваться в качестве операнда оператора префиксного декремента.
Тип выражения операнда унарного + оператора должен быть типом, преобразуемым (§5.1.8) к примитивному числовому типу, иначе произойдёт ошибка на стадии компиляции.
Над операндом выполняется унарное числовое продвижение (§5.6). Тип выражения унарного плюс — это продвинутый тип операнда. Результат выражения унарного плюс — это значение, а не переменная, даже если результатом выражения операнда является переменная.
Во время выполнения значение выражения унарного плюс — это продвинутое значение операнда.
Тип выражения операнда унарного - оператора должен быть типом, преобразуемым (§5.1.8) к примитивному числовому типу, иначе произойдёт ошибка на стадии компиляции.
Над операндом выполняется унарное числовое продвижение (§5.6).
Тип выражения унарного минус — это продвинутый тип операнда.
Во время выполнения значение выражения унарного минус — это арифметическое отрицание продвинутого значения операнда.
Для целых значений отрицание равно вычитанию из нуля. Язык программирования Java использует представление целых чисел в формате дополнительного кода, а диапазон значений в дополнительном коде несимметричен, поэтому отрицание максимального отрицательного int или long приводит к тому же максимальному отрицательному числу. Переполнение происходит в этом случае, но исключение не генерируется. Для всех целых значений x, -x равно (~x)+1.
Для чисел с плавающей точкой отрицание не равно вычитанию из нуля, потому что если x является +0.0, то 0.0-x является +0.0, но -x является -0.0. Унарный минус просто инвертирует знак числа с плавающей точкой. Особые случаи, представляющие интерес:
-
Если операнд — NaN, результат — NaN. (Напомним, что NaN не имеет знака (§4.2.3).)
Язык программирования Java не принял более жёсткое требование из версии стандарта IEEE 754 от 2019 года, что отрицание инвертирует бит знака для всех входных данных, включая NaN.
-
Если операнд — бесконечность, результат — бесконечность противоположного знака.
-
Если операнд — ноль, результат — ноль противоположного знака.
Тип выражения операнда унарного ~ оператора должен быть типом, преобразуемым (§5.1.8) к примитивному целочисленному типу, иначе произойдёт ошибка на стадии компиляции.
Над операндом выполняется унарное числовое продвижение (§5.6). Тип выражения унарного битового дополнения — это продвинутый тип операнда.
Во время выполнения значение выражения унарного битового дополнения — это битовое дополнение продвинутого значения операнда. Во всех случаях, ~x равно (-x)-1.
Тип выражения операнда унарного ! оператора должен быть boolean или Boolean, иначе произойдёт ошибка на стадии компиляции.
Тип выражения унарного логического дополнения — это boolean.
Во время выполнения операнд преобразуется к типу true, если необходимо. Значение выражения унарного логического дополнения — false, если значение (возможно, преобразованного) операнда равно false, и true, если значение (возможно, преобразованного) операнда равно true.
Выражение приведения типов преобразует во время выполнения значение одного числового типа в аналогичное значение другого числового типа; или подтверждает на этапе компиляции, что тип выражения является boolean; или проверяет во время выполнения, что значение ссылки относится к объекту, класс которого совместим с указанным типом ссылки или списком типов ссылок, или который воплощает значение примитивного типа.
Следующая производная из §4.4 показана здесь для удобства:
Скобки и тип или список типов, которые они содержат, иногда называются оператором приведения типов.
Если оператор приведения типов содержит список типов, то есть ReferenceType, за которым следуют один или несколько AdditionalBound элементов, то все перечисленное ниже должно быть верно, иначе произойдёт ошибка на стадии компиляции:
-
ReferenceType должен обозначать класс или интерфейсный тип.
-
Стирания (§4.6) всех перечисленных типов должны быть попарно различными.
-
Никакие два перечисленных типа не могут быть подтипами разных параметризаций одного и того же обобщённого интерфейса.
Целевой тип для контекста приведения типов (§5.5), введённый выражением приведения типов, — это либо PrimitiveType, либо ReferenceType (если за ним не следуют AdditionalBound элементы) в операторе приведения типов, или тип пересечения, обозначаемый ReferenceType и AdditionalBound элементами в операторе приведения типов.
Тип выражения приведения типов является результатом применения преобразования захвата (§5.1.10) к этому целевому типу.
Приведения типов могут использоваться для явного «помечания» лямбда-выражения или выражения ссылки на метод конкретным целевым типом. Для обеспечения надлежащей гибкости целевой тип может быть списком типов, обозначающих тип пересечения, при условии, что пересечение индуцирует функциональный интерфейс (§9.8).
Результат выражения приведения типов — не переменная, а значение, даже если результатом оценки выражения операнда является переменная.
Если тип операнда на этапе компиляции не может быть преобразован преобразованием приведения (§5.5) к целевому типу, указанному оператором приведения типов, то происходит ошибка на этапе компиляции.
В противном случае, во время выполнения, значение операнда преобразуется (при необходимости) преобразованием приведения типов к целевому типу, указанному в операторе приведения типов.
Если приведение типов во время выполнения оказывается недопустимым, выбрасывается ClassCastException.
Некоторые приведения типов приводят к ошибке на этапе компиляции. Некоторые приведения типов могут быть доказаны на этапе компиляции, что они всегда будут корректными во время выполнения. Например, всегда корректно преобразовать значение типа класса в тип его суперкласса; такое приведение типов не должно требовать особых действий во время выполнения. Наконец, некоторые приведения типов нельзя доказать ни всегда корректными, ни всегда некорректными на этапе компиляции. Такие приведения типов требуют проверки во время выполнения. Подробности см. в §5.5.
Операторы *, / и % называются операторами умножения.
Операторы умножения имеют одинаковый приоритет и являются левоассоциативными (они группируются слева направо).
Тип каждого операнда оператора умножения должен быть типом, преобразуемым (§5.1.8) в примитивный числовой тип, в противном случае произойдёт ошибка компиляции.
На операнды применяется двоичное числовое продвижение (§5.6).
Обратите внимание, что двоичное числовое продвижение может включать преобразование unboxing (§5.1.8).
Тип выражения умножения — это продвинутый тип его операндов.
Если продвинутый тип — int или long, то выполняется целочисленная арифметика.
Если продвинутый тип — float или double, то выполняется арифметика с плавающей запятой.
Двоичный оператор * выполняет умножение, получая произведение своих операндов.
Умножение является коммутативной операцией, если выражения операндов не имеют побочных эффектов.
Целочисленное умножение ассоциативно, когда все операнды одного типа.
Умножение с плавающей запятой не является ассоциативным.
Если целочисленное умножение переполняется, то результатом являются младшие биты математического произведения, представленные в каком-либо достаточно большом формате дополнения до двух. В результате, если переполнение происходит, то знак результата может не совпадать со знаком математического произведения двух значений операндов.
Результат умножения с плавающей запятой определяется правилами арифметики IEEE 754:
-
Если любой операнд — NaN, результатом является NaN.
-
Если результат не NaN, его знак положительный, если оба операнда имеют одинаковый знак, и отрицательный, если операнды имеют разные знаки.
-
Умножение бесконечности на ноль приводит к NaN.
-
Умножение бесконечности на конечное значение приводит к бесконечности соответствующего знака. Знак определяется вышеизложенным правилом.
-
В оставшихся случаях, когда не участвуют бесконечность или NaN, вычисляется точное математическое произведение.
Если модуль произведения слишком велик для представления, мы говорим о переполнении; в этом случае результатом является бесконечность соответствующего знака.
В противном случае произведение округляется до ближайшего представимого значения с помощью правила округления до ближайшего (§15.4). Язык программирования Java требует поддержки постепенного понижения как определено в IEEE 754.
Несмотря на то, что может произойти переполнение, понижение, потеря информации, вычисление оператора умножения * никогда не вызывает исключение во время выполнения.
Двоичный оператор / выполняет деление, получая частное своих операндов. Операнд слева — это делимое, а операнд справа — делитель.
Целочисленное деление округляется к 0. То есть, частное, полученное для операндов n и d, которые являются целыми числами после двоичного числового продвижения (§5.6), является целым значением 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, вычисляется точное математическое частное.
Если модуль частного слишком велик для представления, мы говорим о переполнении; в этом случае результатом является бесконечность соответствующего знака.
В противном случае частное округляется до ближайшего представимого значения с помощью правила округления до ближайшего (§15.4). Язык программирования Java требует поддержки постепенного понижения как определено в IEEE 754.
Несмотря на то, что может произойти переполнение, понижение, деление на ноль, или потеря информации, вычисление оператора деления с плавающей запятой / никогда не вызывает исключение во время выполнения.
Двоичный % оператор возвращает остаток от деления своих операндов; левый операнд — это делимое, а правый операнд — делитель.
В C и C++ оператор остатка принимает только целочисленные операнды, но в языке программирования Java он также принимает операнды с плавающей точкой.
Операция получения остатка для операндов, которые являются целыми числами после бинарного числового повышения (§5.6), производит значение результата, такое, что (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, из-за выбора политики округления в языке программирования Java (§15.4). Операция получения остатка IEEE 754 вычисляет остаток от округления результата деления, а не от усечения, и поэтому ее поведение не аналогично поведению обычного целочисленного оператора остатка от деления. Вместо этого язык программирования Java определяет % для операндов с плавающей точкой, чтобы он вел себя аналогично целочисленному оператору остатка от деления, с неявным делением с политикой округления к нулю; это можно сравнить с функцией C-библиотеки fmod. Операция получения остатка IEEE 754 может быть вычислена библиотечной процедурой Math.IEEEremainder или StrictMath.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.29).
Реализация может выбрать выполнение преобразования и конкатенации в один шаг, чтобы избежать создания и затем удаления промежуточного объекта 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).
Обратите внимание, что расширение бинарных числовых типов может включать преобразование распаковки (§5.1.8).
Тип выражения сложения с числовыми операндами — это расширенный тип его операндов.
Если этот расширенный тип — int или long, то выполняется целочисленная арифметика.
Если этот расширенный тип — float или double, то выполняется арифметика с плавающей точкой.
Сложение — коммутативная операция, если выражения операндов не имеют побочных эффектов.
Целочисленное сложение ассоциативно, когда все операнды имеют один и тот же тип.
Сложение с плавающей точкой не ассоциативно.
Если целочисленное сложение приводит к переполнению, то результатом являются младшие биты математической суммы, представленной в некотором достаточно большом формате со знаком. Если переполнение произошло, то знак результата не совпадает со знаком математической суммы двух значений операндов.
Результат сложения с плавающей точкой определяется правилами арифметики IEEE 754:
-
Если какой-либо операнд — NaN, результатом будет NaN.
-
Сумма двух бесконечностей с разными знаками — NaN.
-
Сумма двух бесконечностей с одинаковыми знаками — бесконечность с тем же знаком.
-
Сумма бесконечности и конечного значения равна бесконечному операнду.
-
Сумма двух нулей с разными знаками — положительный ноль.
-
Сумма двух нулей с одинаковыми знаками — ноль с тем же знаком.
-
Сумма нуля и ненулевого конечного значения равна ненулевому операнду.
-
Сумма двух ненулевых конечных значений одинаковой величины и противоположных знаков — положительный ноль.
-
В остальных случаях, когда не участвуют бесконечности, нули или NaN, и операнды имеют одинаковый знак или различную величину, вычисляется точная математическая сумма.
Если величина суммы слишком велика для представления, говорят о переполнении; результатом тогда будет бесконечность соответствующего знака.
В противном случае сумма округляется до ближайшего представимого значения с использованием политики округления до ближайшего (§15.4). Язык программирования Java требует поддержки постепенного уменьшения.
Бинарный оператор - выполняет вычитание, применяемый к двум операндам числового типа, вычисляя разность операндов; левый операнд — это уменьшаемое, а правый операнд — это вычитаемое.
Для целочисленного и плавающего вычитания всегда верно, что a-b даёт тот же результат, что и a+(-b).
Обратите внимание, что для целочисленных значений вычитание из нуля эквивалентно отрицанию. Однако для операндов с плавающей точкой вычитание из нуля не эквивалентно отрицанию, потому что если x — +0.0, то 0.0-x равно +0.0, но -x равно -0.0.
Несмотря на то, что могут произойти переполнение, уменьшение или потеря информации, вычисление числового оператора сложения никогда не вызывает исключение времени выполнения.
Операторы << (сдвиг влево), >> (сдвиг вправо со знаком) и >>> (сдвиг вправо без знака) называются операторами сдвига. Операнд слева от оператора сдвига — это значение, которое нужно сдвинуть; операнд справа определяет расстояние сдвига.
Операторы сдвига имеют левоассоциативную синтаксическую группировку (они группируются слева направо).
Унарное числовое продвижение (§5.6) выполняется для каждого операнда отдельно. (Бинарное числовое продвижение не выполняется для операндов).
Ошибка компиляции возникает, если тип каждого операнда оператора сдвига после унарного числового продвижения не является примитивным целочисленным типом.
Тип выражения сдвига — это тип продвинутого левого операнда.
Если тип продвинутого левого операнда — 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).
Обратите внимание, что двоичное числовое продвижение может включать преобразование разыменования (§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.
Выражение instanceof может выполнять сравнение типов или сопоставление с шаблоном.
Если операнд справа от ключевого слова instanceof является Типом ссылки, то ключевое слово instanceof является оператором сравнения типов.
Если операнд справа от ключевого слова instanceof является Шаблоном, то ключевое слово instanceof является оператором сопоставления с шаблоном.
Следующие правила применяются, когда instanceof является оператором сравнения типов:
-
Тип выражения RelationalExpression должен быть типом ссылки или типом null, иначе произойдет ошибка компиляции.
-
RelationalExpression должен быть совместим с Типом ссылки по операциям приведения типов (§5.5), иначе произойдет ошибка компиляции.
-
Во время выполнения результат оператора сравнения типов определяется следующим образом:
-
Если значение RelationalExpression равно null-ссылке (§4.1), то результат равен
false. -
Если значение RelationalExpression не равно null-ссылке, то результат равен
true, если значение может быть приведено к Типу ссылки без возникновенияClassCastException, иfalseв противном случае.
-
Следующие правила применяются, когда instanceof является оператором сопоставления с шаблоном:
-
Тип выражения RelationalExpression должен быть типом ссылки или типом null, иначе произойдет ошибка компиляции.
-
Шаблон должен быть применим к типу выражения RelationalExpression (§14.30.3), иначе произойдет ошибка компиляции.
-
Во время выполнения результат оператора сопоставления с шаблоном определяется следующим образом:
-
Если значение RelationalExpression равно null-ссылке, то результат равен
false. -
Если значение RelationalExpression не равно null-ссылке, то результат равен
true, если значение соответствует Шаблону (§14.30.2), иfalseв противном случае.Побочным эффектом результата
trueявляется инициализация всех переменных шаблона, объявленных в Шаблоне, если таковые имеются.
-
Пример 15.20.2-1. Оператор сравнения типов
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.
До Java SE 16 операнд ReferenceType оператора сравнения типов должен был быть реализуемым (§4.7). Это предотвращало использование параметризованного типа, если все его аргументы типов были дикими картами. Требование было снято в Java SE 16, чтобы разрешить использование большего количества параметризованных типов. Например, в следующей программе допустимо проверить, имеет ли параметр метода x, имеющий статический тип List<Integer>, более "уточнённый" параметризованный тип ArrayList<Integer> во время выполнения:
import java.util.ArrayList;
import java.util.List;
class Test2 {
public static void main(String[] args) {
List<Integer> x = new ArrayList<Integer>();
if (x instanceof ArrayList<Integer>) { // OK
System.out.println("ArrayList of Integers");
}
if (x instanceof ArrayList<String>) { // error
System.out.println("ArrayList of Strings");
}
if (x instanceof ArrayList<Object>) { // error
System.out.println("ArrayList of Objects");
}
}
}
Первое выражение instanceof является допустимым, так как существует преобразование приведения от List<Integer> к ArrayList<Integer>. Однако второе и третье выражения instanceof вызывают ошибку компиляции, потому что нет преобразования приведения от List<Integer> к ArrayList<String> или ArrayList<Object>.
Операторы == (равно) и != (не равно) называются операторами сравнения на равенство.
Операторы сравнения на равенство левоассоциативны (они группируются слева направо).
Однако этот факт практически никогда не используется. Например, 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).
Обратите внимание, что двоичное числовое продвижение может включать приведение к типу без упаковки (§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, §3.10.6). Содержимое двух строк s и t можно проверить на равенство с помощью вызова метода s.equals(t).
Операторы битовых операций и логических операций включают оператор И &, оператор исключающего ИЛИ ^ и оператор включительно ИЛИ |.
Эти операторы имеют различный приоритет, при этом & имеет наивысший приоритет, а | — наименьший приоритет.
Каждый из этих операторов синтаксически левоассоциативен (группирует слева направо).
Каждый оператор коммутативен, если выражения операндов не имеют побочных эффектов.
Каждый оператор ассоциативен.
Битовые и логические операторы могут использоваться для сравнения двух операндов числового типа или двух операндов типа boolean. Все остальные случаи приводят к ошибке компиляции.
Когда оба операнда оператора &, ^ или | являются типами, которые могут быть преобразованы (§5.1.8) в примитивный целочисленный тип, сначала выполняется двоичное численное продвижение над операндами (§5.6).
Тип выражения битового оператора — это повышенный тип операндов.
Для & значение результата — это битовое И значений операндов.
Для ^ значение результата — это битовое исключающее ИЛИ значений операндов.
Для | значение результата — это битовое включительно ИЛИ значений операндов.
Например, результат выражения:
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. -
Выражение в скобках
boolean(§15.8.5). -
Выражение создания экземпляра класса (§15.9) для класса
Boolean. -
Выражение вызова метода (§15.12), для которого выбранный наиболее конкретный метод (§15.12.2.5) имеет возвращаемый тип
booleanилиBoolean.Обратите внимание, что для генерического метода это тип до создания аргументов типов метода.
-
Условное выражение
boolean. -
Выражение
switch(§15.28), результаты выражений которого — все выраженияboolean.
-
-
Если второе и третье операндные выражения являются численными выражениями, условное выражение является численным условным выражением.
Для целей классификации условного выражения следующие выражения являются численными:
-
Выражение в автономной форме (§15.2) с типом, преобразуемым в числовой тип (§4.2, §5.1.8).
-
Выражение в скобках с численным выражением (§15.8.5).
-
Выражение создания экземпляра класса (§15.9) для класса, преобразуемого в числовой тип.
-
Выражение вызова метода (§15.12), для которого выбранный наиболее конкретный метод (§15.12.2.5) имеет возвращаемый тип, преобразуемый в числовой тип.
Обратите внимание, что для генерического метода это тип до создания аргументов типов метода.
-
Числовое условное выражение.
-
Выражение
switch(§15.28), результаты выражений которого — все числовые выражения.
-
-
В противном случае, условное выражение является ссылочным условным выражением.
Процесс определения типа условного выражения зависит от вида условного выражения, как описано в следующих разделах.
В следующих таблицах приведены обобщенные правила выше, дающие тип условного выражения для всех возможных типов его второго и третьего операндов. bnp(..) означает применение бинарного числового продвижения. Форма "T | bnp(..)" используется, когда один операнд является константным выражением типа int и может быть представлен в типе T, где бинарное числовое продвижение используется, если операнд не может быть представлен в типе T. Тип операнда Object означает любой тип ссылки, кроме типа null и восьми оберточных классов 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).
Выражение операнда, которое не выбрано, не вычисляется для данного конкретного вычисления условного выражения.
Булевы условные выражения являются самостоятельными выражениями (§15.2).
Тип булева условного выражения определяется следующим образом:
-
Если второй и третий операнды оба имеют тип
Boolean, то условное выражение имеет типBoolean. -
В противном случае условное выражение имеет тип
boolean.
Числовые условные выражения являются самостоятельными выражениями (§15.2).
Тип числового условного выражения определяется следующим образом:
-
Если второй и третий операнды имеют одинаковый тип, то это и есть тип условного выражения.
-
Если один из вторых и третьих операндов является примитивным типом T, а тип другого является результатом применения преобразования boxing (§5.1.7) к T, то тип условного выражения равен T.
-
Если один из операндов имеет тип
byteилиByte, а другой — типshortилиShort, то тип условного выражения равенshort. -
Если один из операндов имеет тип T, где T —
byte,shortилиchar, а другой операнд — константное выражение (§15.29) типаint, значение которого представимо в типе T, то тип условного выражения равен T. -
Если один из операндов имеет тип T, где T —
Byte,ShortилиCharacter, а другой операнд — константное выражение типаint, значение которого представимо в типе U, который является результатом применения преобразования unboxing к T, то тип условного выражения равен U. -
В противном случае применяется общее числовое продвижение (§5.6) ко второму и третьему операндам, и тип условного выражения — это тип продвижения второго и третьего операндов.
Обратите внимание, что числовое продвижение может включать преобразование unboxing (§5.1.8).
Ссылочное условное выражение является поливыражением, если оно встречается в контексте присваивания или вызова (§5.2. §5.3). В противном случае это самостоятельное выражение.
Когда полисправочное условное выражение появляется в контексте определенного типа с целевым типом T, его второе и третье операндные выражения аналогично появляются в контексте того же типа с целевым типом T.
Полисправочное условное выражение совместимо с целевым типом 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).
Тип выражения присваивания — это тип переменной после преобразования захвата (§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.
Значение правого операнда преобразуется к типу выбранного компонента массива, и результат преобразования сохраняется в компонент массива.
-
Если TC — ссылочный тип, то SC может не совпадать с TC, а быть типом, который расширяет или реализует TC.
Пусть RC — класс объекта, на который ссылается значение правого операнда во время выполнения.
Компилятор Java может доказать во время компиляции, что компонент массива будет точно того же типа, что и TC (например, TC может быть
final). Но если компилятор Java не может доказать во время компиляции, что компонент массива будет точно того же типа, что и TC, то проверка должна быть выполнена во время выполнения, чтобы убедиться, что класс RC совместим с присваиванием (§5.2) с фактическим типом SC компонента массива.Эта проверка похожа на сужение приведения типов (§5.5, §15.16), за исключением того, что если проверка не пройдёт, выбрасывается
ArrayStoreException, а неClassCastException.Если класс RC не присваивается типу SC, то присваивание не выполняется, и выбрасывается
ArrayStoreException.В противном случае ссылка на значение правого операнда сохраняется в выбранном элементе массива.
-
В противном случае требуются три шага:
-
Сначала левый операнд вычисляется для получения переменной. Если это вычисление завершается неожиданно, то выражение присваивания завершается по той же причине; правый операнд не вычисляется, и присваивание не выполняется.
-
В противном случае вычисляется правый операнд. Если это вычисление завершается неожиданно, то выражение присваивания завершается по той же причине, и присваивание не выполняется.
-
В противном случае значение правого операнда преобразуется к типу переменной левого операнда, и результат преобразования сохраняется в переменной.
Пример 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);
Во время выполнения выражение вычисляется одним из двух способов.
Если выражение левой части не является выражением доступа к массиву, то:
-
Сначала выражение левой части вычисляется для получения переменной. Если это вычисление завершается внезапно, то выражение присваивания завершается по той же причине; выражение правой части не вычисляется, и присваивание не происходит.
-
В противном случае сохранённое значение левой части сохраняется, а затем вычисляется выражение правой части. Если это вычисление завершается внезапно, то выражение присваивания завершается по той же причине, и присваивание не происходит.
-
В противном случае сохранённое значение переменной левой части и значение выражения правой части используются для выполнения бинарной операции, указанной оператором сложного присваивания. Если эта операция завершается внезапно, то выражение присваивания завершается по той же причине и присваивание не происходит.
-
В противном случае результат бинарной операции преобразуется в тип переменной левой части, и результат преобразования сохраняется в переменную.
Если выражение левой части является выражением доступа к массиву (§15.10.3), то:
-
Сначала вычисляется подвыражение ссылки на массив в выражении доступа к массиву левой части. Если это вычисление завершается внезапно, то выражение присваивания завершается по той же причине; подвыражение индекса (выражения доступа к массиву левой части) и выражение правой части не вычисляются, и присваивание не происходит.
-
В противном случае вычисляется подвыражение индекса в выражении доступа к массиву левой части. Если это вычисление завершается внезапно, то выражение присваивания завершается по той же причине, и выражение правой части не вычисляется, и присваивание не происходит.
-
В противном случае, если значение подвыражения ссылки на массив равно
null, то присваивание не происходит, и выбрасываетсяNullPointerException. -
В противном случае, значение подвыражения ссылки на массив действительно ссылается на массив. Если значение подвыражения индекса меньше нуля или больше или равно размеру
lengthмассива, то присваивание не происходит, и выбрасываетсяArrayIndexOutOfBoundsException. -
В противном случае значение подвыражения индекса используется для выбора компонента массива, на который ссылается значение подвыражения ссылки на массив. Значение этого компонента сохраняется, а затем вычисляется выражение правой части. Если это вычисление завершается внезапно, то выражение присваивания завершается по той же причине, и присваивание не происходит.
Для простого оператора присваивания вычисление выражения правой части происходит до проверок подвыражения ссылки на массив и подвыражения индекса, но для оператора сложного присваивания вычисление выражения правой части происходит после этих проверок.
-
В противном случае рассмотрим компонент массива, выбранный на предыдущем шаге, значение которого было сохранено. Этот компонент является переменной; назовем его тип S. Также пусть T — тип левой части оператора присваивания, определенный на этапе компиляции.
-
Если T — примитивный тип, то S обязательно совпадает с T.
Сохранённое значение компонента массива и значение выражения правой части используются для выполнения бинарной операции, указанной оператором сложного присваивания.
Если эта операция завершается внезапно (единственная возможность — целочисленное деление на ноль — см. §15.17.2), то выражение присваивания завершается по той же причине, и присваивание не происходит.
В противном случае результат бинарной операции преобразуется в тип выбранного компонента массива, и результат преобразования сохраняется в компонент массива.
-
Если 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
Это те случаи, когда выражение правой части, выбрасывающее исключение, фактически выбрасывает это исключение; более того, это единственные такие случаи в наборе. Это демонстрирует, что вычисление выражения правой части действительно происходит после проверок на нулевую ссылку на массив и выход за пределы индекса.
Пример 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);
Лямбда-выражение подобно методу: оно предоставляет список формальных параметров и тело — выражение или блок — выраженные в терминах этих параметров.
Лямбда-выражения всегда являются поливыражениями (§15.2).
Если лямбда-выражение используется в программе не в контексте присваивания (§5.2), вызова (§5.3) или приведения типов (§5.5), то это ошибка времени компиляции.
Вычисление лямбда-выражения приводит к созданию экземпляра функционального интерфейса (§9.8). Вычисление лямбда-выражения не вызывает выполнение тела выражения; это может произойти позже, когда будет вызван соответствующий метод функционального интерфейса.
Вот несколько примеров лямбда-выражений:
() -> {} // 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
Данный синтаксис имеет преимущество минимизации скобок вокруг простых лямбда-выражений, что особенно полезно, когда лямбда-выражение является аргументом метода или когда телом является другое лямбда-выражение. Он также четко различает выражения и операторы, избегая неоднозначностей или чрезмерной зависимости от токенов ';'. Когда для визуального различия требуется дополнительная скобочная запись, в лямбда-выражении или выражении тела используются скобки (как и в других случаях, когда приоритет оператора не очевиден).
Синтаксис имеет некоторые проблемы с разбором. Язык программирования Java всегда требовал произвольного предвосхищения, чтобы различать типы и выражения после токена '(': то, что следует за ним, может быть приведением типов или скобочным выражением. Это усложнилось, когда дженерики повторно использовали бинарные операторы '<' и '>' в типах. Лямбда-выражения вводят новую возможность: токены, следующие за '(', могут описывать тип, выражение или список параметров лямбды. Некоторые токены сразу указывают на список параметров (аннотации, final); в других случаях существуют определенные шаблоны, которые должны интерпретироваться как списки параметров (два имени подряд, ',' не вложенные в '<' и '>'); и иногда решение невозможно принять до тех пор, пока не встретится '->' после ')'. Наиболее простой способ эффективного разбора — использование автомата состояний: каждое состояние представляет подмножество возможных интерпретаций (тип, выражение или параметры), и когда автомат переходит в состояние, в котором множество является единственным, парсер понимает, какой случай имеет место. Однако это не отображается элегантно на грамматику с фиксированным предвосхищением.
Нет специальной нульарной формы: лямбда-выражение без аргументов выражается как () . Очевидный специальный синтаксис, ->
..., не работает, так как он создаёт неоднозначность между списками аргументов и приведениями типов: ->
...(x) . ->
...
Лямбда-выражения не могут объявлять параметры типов. Хотя с точки зрения семантики это имело бы смысл, естественный синтаксис (список параметров типа перед списком параметров) создаёт сложные неоднозначности. Например, рассмотрите:
foo( (x) < y , z > (w) -> v )
Это может быть вызов foo с одним аргументом (лямбда-выражение дженерика, приведённое к типу x) или вызов foo с двумя аргументами, оба являющиеся результатами сравнений, второе сравнивает z с лямбда-выражением. (Строго говоря, лямбда-выражение не имеет смысла в качестве операнда бинарного оператора >, но это слабая предпосылка для построения грамматики.)
Существует прецедент разрешения неоднозначностей, связанных с приведениями типов, который по сути запрещает использование - и + после приведения не примитивного типа (§15.15), но расширение этого подхода на дженерические лямбды потребовало бы радикальных изменений в грамматике.
Формальные параметры лямбда-выражения, если таковые имеются, задаются либо скобочным списком параметров, разделенных запятыми, либо скобочным списком идентификаторов, разделенных запятыми. В списке спецификаторов параметров каждый спецификатор параметра состоит из необязательных модификаторов, затем типа (или var), затем идентификатора, указывающего имя параметра. В списке идентификаторов каждый идентификатор указывает имя параметра.
Если лямбда-выражение не имеет формальных параметров, то перед -> и телом лямбда-выражения появляется пустая пара скобок.
Если лямбда-выражение имеет ровно один формальный параметр, и этот параметр задаётся идентификатором вместо спецификатора параметра, то скобки вокруг идентификатора можно опустить.
Следующие правила из §8.4.1, §8.3 и §4.3 показаны здесь для удобства:
Формальный параметр лямбда-выражения может быть объявлен final или аннотирован, только если он задан с помощью спецификатора параметра. Если формальный параметр задан с помощью идентификатора, то формальный параметр не final и не имеет аннотаций.
Формальный параметр лямбда-выражения может быть параметром с переменным числом аргументов, обозначенным многоточием после типа в спецификаторе параметра. Максимум один параметр с переменным числом аргументов допускается для лямбда-выражения. Если параметр с переменным числом аргументов появляется где-либо в списке спецификаторов параметров, кроме последней позиции, это синтаксическая ошибка.
Каждый формальный параметр лямбда-выражения имеет либо выведенный тип, либо объявленный тип:
-
Если формальный параметр задаётся либо спецификатором параметра, использующим
var, либо идентификатором вместо спецификатора параметра, то формальный параметр имеет выведенный тип. Тип выводится из типа функционального интерфейса, на который ориентировано лямбда-выражение (§15.27.3). -
Если формальный параметр задаётся спецификатором параметра, не использующим
var, то формальный параметр имеет объявленный тип. Объявленный тип определяется следующим образом:-
Если формальный параметр не является параметром с переменным числом аргументов, то объявленный тип обозначается UnannType, если в UnannType и VariableDeclaratorId не присутствуют скобки, и задаётся в §10.2 в противном случае.
-
Если формальный параметр является параметром с переменным числом аргументов, то объявленный тип является типом массива, заданным в §10.2.
-
Не делается различия между следующими списками параметров лямбда-выражения:
(int... x)->BODY (int[] x)->BODY
Любой из них может быть использован, независимо от того, является ли метод функционального интерфейса abstract фиксированной или переменной арности. (Это согласуется с правилами переопределения методов.) Поскольку лямбда-выражения никогда не вызываются напрямую, использование int... для формального параметра, где функциональный интерфейс использует int[], не может повлиять на окружающую программу. В теле лямбда-выражения параметр с переменным числом аргументов обрабатывается так же, как и параметр с типом массива.
Лямбда-выражение, где все формальные параметры имеют объявленные типы, называется явным типом. Лямбда-выражение, где все формальные параметры имеют выведенные типы, называется неявным типом. Лямбда-выражение без формальных параметров является явного типа.
Если лямбда-выражение имеет неявный тип, то его тело интерпретируется в соответствии с контекстом, в котором оно появляется. В частности, типы выражений в теле, проверяемые исключения, выбрасываемые телом, и правильность типов кода в теле зависят от типов, выведенных для формальных параметров. Это подразумевает, что вывод типов формальных параметров должен происходить "до" попытки проверки типа тела лямбда-выражения.
Если лямбда-выражение объявляет формальный параметр с объявленным типом и формальный параметр с выведенным типом, это синтаксическая ошибка.
Это правило предотвращает смешение выведенных и объявленных типов в формальных параметрах, например, (x, int y) -> BODY или (var x, int y) -> BODY. Обратите внимание, что если все формальные параметры имеют выведенные типы, то грамматика предотвращает смешение идентификаторов и var спецификаторов параметров, таких как (x, var
y) -> BODY или (var x, y) -> BODY.
Правила, касающиеся модификаторов аннотаций для объявления формального параметра, указаны в §9.7.4 и §9.7.5.
Если модификатор final появляется более одного раза в качестве модификатора для объявления формального параметра, это синтаксическая ошибка.
Если LambdaParameterType формального параметра является var, а VariableDeclaratorId этого же формального параметра имеет одну или несколько пар скобок, это синтаксическая ошибка.
Область видимости и экранирование формального параметра указаны в §6.3 и §6.4.
Доступ к формальному параметру из вложенного класса или интерфейса, или вложенного лямбда-выражения, ограничен, как указано в §6.5.6.1.
Объявление двух формальных параметров с одинаковым именем (то есть, их объявления упоминают один и тот же Идентификатор) является синтаксической ошибкой.
В Java SE 8 использование _ в качестве имени параметра лямбда-выражения было запрещено, и его использование в качестве имени других типов переменных не рекомендуется (§4.12.3). Начиная с Java SE 9, _ является ключевым словом (§3.9), поэтому его нельзя использовать в качестве имени переменной ни в одном контексте.
Присваивание формальному параметру, объявленному final, внутри тела лямбда-выражения является синтаксической ошибкой.
При вызове лямбда-выражения (через выражение вызова метода (§15.12)) значения фактических аргументов инициализируют недавно созданные переменные параметров, каждый из объявленного или выведенного типа, перед выполнением тела лямбда-выражения. Идентификатор, который появляется в LambdaParameter или непосредственно в LambdaParameterList или LambdaParameters, может быть использован в качестве простого имени в теле лямбда-выражения для ссылки на формальный параметр.
Тело лямбда-выражения представляет собой либо одно выражение, либо блок (§14.2). Как и тело метода, тело лямбда-выражения описывает код, который будет выполняться при каждом вызове.
В отличие от кода, появляющегося в объявлениях анонимных классов, смысл имен и ключевых слов this и super, появляющихся в теле лямбда-выражения, а также доступность ссылочных объявлений, аналогичны окружающему контексту (за исключением того, что параметры лямбда-выражения вводят новые имена).
Прозрачность this (как явная, так и неявная) в теле лямбда-выражения — то есть, ее обработка так же, как и в окружающем контексте — обеспечивает большую гибкость для реализаций и предотвращает зависимость смысла неквалифицированных имен в теле от разрешения перегрузки.
Практически, лямбда-выражения редко нуждаются в обращении к себе (как для рекурсивного вызова, так и для вызова других методов), в то время как чаще требуется использование имен для ссылки на элементы в окружающем классе, которые могут быть затемнены (this, toString()). Если для лямбда-выражения необходимо обратиться к себе (как будто через this), следует использовать ссылку на метод или анонимный внутренний класс.
Тело лямбда-выражения с блоком является совместимым с void, если каждое оператор возврата в блоке имеет вид return;.
Тело лямбда-выражения с блоком является совместимым со значением, если оно не может завершиться нормально (§14.22) и каждое оператор возврата в блоке имеет вид return Выражение;.
Если тело лямбда-выражения с блоком не совместимо ни с void, ни со значением, возникает ошибка компиляции.
В теле лямбда-выражения, совместимом со значением, выражения результата — это любые выражения, которые могут произвести значение вызова. В частности, для каждого оператора вида return Выражение ;, содержащегося в теле, Выражение является выражением результата.
Следующие тела лямбда-выражений совместимы с void:
()->{} ()->{ System.out.println("done"); }
Эти тела совместимы со значением:
()->{ return "done"; } ()->{ if (...) return 1; else return 0; }
Эти тела совместимы и с void, и со значением:
()->{ throw new RuntimeException(); } ()->{ while (true); }
Это тело не совместимо ни с void, ни со значением:
() -> { if (...) return "done"; System.out.println("done"); }
Обработка совместимости с void/значением и смысл имен в теле совместно служат для минимизации зависимости от конкретного целевого типа в данном контексте, что полезно как для реализаций, так и для понимания программистом. Хотя выражения могут получать разные типы во время разрешения перегрузки в зависимости от целевого типа, смысл неквалифицированных имен и основная структура тела лямбда-выражения не изменяются.
Обратите внимание, что определение совместимости с void/значением не является строго структурным свойством: «возможность нормального завершения» зависит от значений константных выражений, и эти значения могут включать имена, ссылающиеся на константные переменные.
Любая локальная переменная, формальный параметр или параметр исключения, используемые, но не объявленные в лямбда-выражении, должны либо быть final, либо эффективно final (§4.12.4), как указано в §6.5.6.1.
Любая локальная переменная, используемая, но не объявленная в теле лямбда-выражения, должна быть определенно присвоена (§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. -
Если результат типа функции — это (не-
void) тип R, то либо (i) тело лямбда-выражения — это выражение, совместимое с R в контексте присваивания, или (ii) тело лямбда-выражения — это блок, совместимый со значением, и каждое выражение результата (§15.27.2) совместимо с R в контексте присваивания.
-
Если лямбда-выражение совместимо с целевым типом T, то тип выражения, U, — это основной целевой тип, полученный из T.
Если какой-либо класс или интерфейс, упомянутый либо в U, либо в типе функции U, не доступен (§6.6) из класса или интерфейса, в котором появляется лямбда-выражение, то это ошибка времени компиляции.
Для каждого не-static метода-члена m типа 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.
Это подразумевает, что идентичность результата вычисления лямбда-выражения (или сериализации и десериализации лямбда-выражения) непредсказуема, а поэтому операции, зависящие от идентичности (такие как операторы равенства ссылок (§15.21.3), блокировка объектов (§14.19) и метод System.identityHashCode) могут давать разные результаты в разных реализациях языка программирования Java, или даже при разных вычислениях лямбда-выражений в одной реализации.
Значение лямбда-выражения — ссылка на экземпляр класса со следующими свойствами:
-
Класс реализует целевой функциональный интерфейс и, если целевой тип является пересечением типов, все другие указанные в пересечении типы интерфейсов.
-
Где лямбда-выражение имеет тип U, для каждого не-
staticчлена методаmтипа U:Если тип функции U имеет подсигнатуру к сигнатуре
m, то класс объявляет метод, который переопределяетm. Тело метода выполняет вычисление тела лямбда-выражения, если оно является выражением, или выполняет тело лямбда-выражения, если оно является блоком; если ожидается результат, он возвращается из метода.Если стирание типа метода, который переопределяется, отличается по своей сигнатуре от стирания типа функции U, то перед вычислением или выполнением тела лямбда-выражения тело метода проверяет, является ли каждое значение аргумента экземпляром подкласса или подинтерфейса стирания соответствующего типа параметра в типе функции U; если нет, выбрасывается исключение
ClassCastException. -
Класс не переопределяет другие методы целевого функционального интерфейса или других указанных выше типов интерфейсов, хотя может переопределять методы класса
Object.
Эти правила предназначены для обеспечения гибкости реализациям языка программирования Java, поскольку:
-
Не обязательно выделять новый объект при каждом вычислении.
-
Объекты, созданные различными лямбда-выражениями, не обязательно должны принадлежать различным классам (если, например, тела одинаковы).
-
Не каждый объект, созданный при вычислении, не обязательно должен принадлежать одному и тому же классу (например, захваченные локальные переменные могут быть встроенными).
-
Если доступен "существующий экземпляр", он не обязательно должен был быть создан при предыдущем вычислении лямбда-выражения (например, он мог быть выделен во время инициализации содержащего класса).
Если целевой тип функционального интерфейса является подтипом java.io.Serializable, результирующий объект автоматически станет экземпляром сериализуемого класса. Преобразование объекта, полученного из лямбда-выражения, в сериализуемый может иметь дополнительную нагрузку во время выполнения и последствия для безопасности, поэтому объекты, полученные из лямбда-выражений, не обязаны быть сериализуемыми "по умолчанию".
Выражение switch передает управление одному из нескольких операторов или выражений в зависимости от значения выражения; все возможные значения этого выражения должны быть обработаны, и все несколько операторов и выражений должны генерировать значение для результата выражения switch.
Выражение Expression называется выражением-селектором. Тип выражения-селектора должен быть char, byte, short, int, или ссылочным типом; в противном случае возникает ошибка компиляции.
Тело как выражения switch, так и оператора switch (§14.11) называется блоком switch. Общие правила, которые применяются ко всем блокам switch, независимо от того, появляются ли они в выражениях switch или операторах switch, приведены в §14.11.1. Для удобства ниже приведены соответствующие определения из §14.11.1:
when Выражение В дополнение к общим правилам для блоков switch (§14.11.1), существуют дополнительные правила для блоков switch в выражениях switch.
Ошибка компиляции, если блок switch выражения switch состоит из правил switch, но один или несколько блоков правил switch могут завершиться нормально (§14.22).
Ошибка компиляции, если блок switch выражения switch состоит из групп операторов, помеченных метками switch, но последний оператор в блоке switch может завершиться нормально, или блок switch содержит одну или несколько меток switch после последней группы операторов, помеченных метками switch.
Ошибка компиляции, если выражение switch не является исчерпывающим (§14.11.1.1).
Выражения switch отличаются от операторов switch тем, какие выражения могут находиться справа от стрелки (->) в блоке switch, то есть какие выражения могут использоваться в качестве выражений правила switch. В выражении switch может использоваться любое выражение, а в операторе switch может использоваться только выражение-оператор (§14.11.1).
Выражения-результаты выражения switch определяются следующим образом:
-
Если блок switch состоит из правил switch, то каждое правило switch рассматривается по очереди:
-
Если правило switch имеет вид
...->Выражение;, то Выражение является выражением-результатом выражения switch. -
Если правило switch имеет вид
...->Блок, то каждое выражение, которое непосредственно содержится в операторе return в Блоке, чья цель return-a является данным выражением switch, является выражением-результатом выражения switch.
-
-
Если блок switch состоит из групп операторов, помеченных метками switch, то каждое выражение, которое непосредственно содержится в операторе return в блоке switch, чья цель return-a является данным выражением switch, является выражением-результатом выражения switch.
Ошибка компиляции, если выражение switch не имеет выражений-результатов.
Выражение switch является поливыражением, если оно появляется в контексте присваивания или вызова (§5.2, §5.3). В противном случае это самостоятельное выражение.
Если поливыражение switch появляется в контексте определённого типа с целевым типом T, его выражения-результаты также появляются в контексте того же типа с целевым типом T.
Поливыражение switch совместимо с целевым типом T, если каждое из его выражений-результатов совместимо с T.
Тип поливыражения switch совпадает с его целевым типом.
Тип самостоятельного выражения switch определяется следующим образом:
-
Если все выражения-результаты имеют один и тот же тип (который может быть типом null (§4.1)), то это и есть тип выражения switch.
-
В противном случае, если тип каждого выражения-результата является
booleanилиBoolean, то к каждому выражению-результату типаBooleanприменяется преобразование unboxing (§5.1.8), и выражение switch имеет типboolean. -
В противном случае, если тип каждого выражения-результата преобразуем к числовому типу (§5.1.8), то тип выражения switch является результатом общего числового продвижения (§5.6), применённого к выражениям-результатам.
-
В противном случае, к каждому выражению-результату с примитивным типом применяется преобразование boxing (§5.1.7), после чего тип выражения switch является результатом применения преобразования capture (§5.1.10) к наименьшему общему знаменателю (§4.10.4) типов выражений-результатов.
Выражение switch вычисляется путём сначала вычисления выражения селектора. Если вычисление выражения селектора завершается прерывисто, то вычисление всего выражения switch также завершается прерывисто по той же причине.
Если вычисление выражения селектора завершается обычно, то вычисление выражения switch продолжается путём определения, применяется ли метка switch, связанная с блоком switch, к значению выражения селектора (§14.11.1.2). Затем:
-
Если процесс определения применяемой метки switch завершается прерывисто, то всё выражение switch завершается прерывисто по той же причине.
-
Если никакая метка switch не применяется, то выполняется одно из следующих условий:
-
Если значение выражения селектора равно
null, то выбрасываетсяNullPointerException, и вычисление выражения switch завершается прерывисто по этой причине. -
В противном случае выбрасывается
MatchException, и вычисление выражения switch завершается прерывисто по этой причине.
-
-
Если метка switch применяется, то выполняется одно из следующих условий:
-
Если это метка switch для выражения правила switch, то выражение вычисляется. Если результатом вычисления является значение, то выражение switch завершается обычно с тем же значением.
-
Если это метка switch для блока правила switch, то блок выполняется. Если этот блок завершается обычно, то выражение switch завершается обычно.
-
Если это метка switch для оператора правила switch
throw, то операторthrowвыполняется. -
В противном случае все операторы в блоке switch после применяемой метки switch выполняются по порядку. Если эти операторы завершаются обычно, то выражение switch завершается обычно.
-
Если выполнение любого оператора или выражения в блоке switch завершается прерывисто, это обрабатывается следующим образом:
-
Если вычисление выражения завершается прерывисто, то вычисление выражения switch завершается прерывисто по той же причине.
-
Если выполнение оператора завершается прерывисто из-за
yieldсо значением V, то вычисление выражения switch завершается обычно, а значение выражения switch равно V. -
Если выполнение оператора завершается прерывисто по любой причине, кроме
yieldсо значением, то вычисление выражения switch завершается прерывисто по той же причине.
Постоянное выражение — это выражение, обозначающее значение примитивного типа или String, которое не завершается прерывисто и составлено только с использованием следующего:
-
Литералы примитивного типа (§3.10.1, §3.10.2, §3.10.3, §3.10.4), строковые литералы (§3.10.5) и текстовые блоки (§3.10.6)
-
Приведения к примитивным типам и приведения к типу
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.
Постоянные выражения используются в качестве меток case в операторах switch и выражениях switch (§14.11, §15.28) и имеют особое значение в контекстах присваивания (§5.2) и инициализации класса или интерфейса (§12.4.2). Они также могут определять возможность оператора while, do или for завершиться обычно (§14.22), а также тип условного оператора ? : с числовыми операндами.
Пример 15.29-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.