Глава 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 17 (§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) выбрасывает
IncompatibleClassChangeError, если метка switch не соответствует значению выражения селектора.
Выражение вызова метода также может привести к выбрасыванию исключения, если при выполнении тела метода произойдет внезапное завершение.
Выражение создания экземпляра класса также может привести к выбрасыванию исключения, если при выполнении конструктора произойдет внезапное завершение.
Во время вычисления выражения также могут возникнуть различные ошибки связи и виртуальной машины. По своей природе такие ошибки сложно предсказать и обработать.
Если произойдет исключение, вычисление одного или нескольких выражений может быть прервано до завершения всех шагов их нормального режима вычисления; такие выражения считаются завершенными внезапно.
Если вычисление выражения требует вычисления подвыражения, то внезапное завершение подвыражения всегда вызывает немедленное внезапное завершение самого выражения с тем же основанием, и все последующие шаги в нормальном режиме вычисления не выполняются.
Термины «завершение нормально» и «завершение внезапно» также применяются к выполнению операторов (§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. (Это можно сравнить с разделением Statement на Statement и 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.
ИмяТипа должен обозначать класс или интерфейс, к которому есть доступ (§6.6). Ошибка компиляции возникает, если ИмяТипа обозначает недоступный класс или интерфейс, или переменную типа.
Тип 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>.
Литерал класса вычисляет объект Class для указанного класса, интерфейса, типа массива или примитивного типа (или для 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 — целое число такое, что ИмяТипа обозначает n-ый лексически окружающий класс или интерфейс в объявлении класса или интерфейса, который сразу включает в себя квалифицированное выражение this.
Значение квалифицированного выражения this ИмяТипа.this — это n-ый лексически окружающий экземпляр this.
Если ИмяТипа обозначает обобщенный класс с параметрами типа F1,...,Fn, то тип квалифицированного выражения this равен ИмяТипа<F1,...,Fn>. В противном случае, тип квалифицированного выражения this равен ИмяТипа.
Ошибка компиляции возникает, если квалифицированное выражение this встречается в статическом контексте (§8.1.3).
Ошибка компиляции возникает, если класс или интерфейс, объявление которого непосредственно включает в себя квалифицированное выражение this, не является внутренним классом ИмяТипа или ИмяТипа самому.
Выражение в скобках — это выражение-операнд, тип которого совпадает с типом заключённого в скобки выражения, а значение в момент выполнения равно значению заключённого выражения. Если заключенное выражение обозначает переменную, то выражение в скобках также обозначает эту переменную.
Использование скобок влияет только на порядок вычисления, за исключением частного случая, в котором (-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 вместе с любыми аргументами типа класса в выражении. Для выбора одного из конструкторов T и определения его
throwsиспользуется процесс, описанный в §15.12.2 с модификациями для обработки конструкторов.Если в T нет уникального наиболее конкретного конструктора, который одновременно применим и доступен (§6.6), то возникает ошибка времени компиляции (как при вызовах методов).
В противном случае возвращаемый тип, соответствующий выбранному конструктору, — T.
-
Если C — анонимный класс, то:
Для выбора одного из конструкторов типа непосредственного суперкласса C и определения его
throwsиспользуется процесс, описанный в §15.12.2 с модификациями для обработки конструкторов.Если в типе непосредственного суперкласса 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.θ
j— это[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.
Тип непосредственного суперкласса анонимного класса, объявленного константой перечисления, — это тип объявляющего класса перечисления.
Тело класса выражения создания экземпляра класса или константы перечисления объявляет поля (§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 может именовать любой именованный ссылочный тип, даже абстрактный тип класса (§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).
Выражение доступа к массиву относится к переменной, которая является компонентом массива.
Выражение доступа к массиву содержит два подвыражения: выражение ссылки на массив (перед левой квадратной скобкой) и выражение индекса (внутри квадратных скобок).
Обратите внимание, что выражение ссылки на массив может быть именем или любым выражением-первичным выражением, которое не является выражением создания массива (§15.10).
Тип выражения ссылки на массив должен быть типом массива (назовём его 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, нельзя классифицировать как ИмяТипа или ИмяВыражения (§6.5.2).
Если АргументыТипов присутствуют слева от Идентификатор, то ошибка компиляции, если любой из аргументов типа является условным (§4.5.1).
Выражение вызова метода является поливыражением, если все перечисленные ниже условия являются истинными:
-
Вызов появляется в контексте присваивания или вызова (§5.2, §5.3).
-
Если вызов квалифицированный (то есть любой вид MethodInvocation, кроме первого), то вызов опускает АргументыТипов слева от Идентификатор.
-
Метод, подлежащий вызову, как определено в следующих подразделах, является обобщенным (§8.4.4) и имеет тип возвращаемого значения, который упоминает по крайней мере один из параметров типа метода.
В противном случае выражение вызова метода является самостоятельным выражением.
Первый шаг обработки вызова метода во время компиляции заключается в определении имени вызываемого метода и типа, для которого необходимо искать определения методов с этим именем.
Имя метода задается с помощью ИмениМетода или Идентификатора, непосредственно предшествующего левой скобке вызова метода MethodInvocation.
Для определения типа для поиска необходимо рассмотреть шесть случаев, в зависимости от формы, предшествующей левой скобке вызова метода MethodInvocation:
-
Если форма представляет собой ИмениМетода, то есть просто Идентификатор, то:
Если Идентификатор находится в области видимости объявления метода с таким именем (§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), и выполняется одно из следующих условий:
-
Выражение ссылки на метод имеет форму ТипСсылки
::[TypeArguments] Идентификатор и по крайней мере один потенциально применимый метод либо (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
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.Идентификатор или имеет вид ТипИмя.super.Идентификатор, то режим вызова —super. -
В противном случае, если квалифицирующий класс или интерфейс вызова метода на самом деле является интерфейсом, то режим вызова —
interface. -
В противном случае, режим вызова —
virtual.
-
Если тип результата вызова объявления на этапе компиляции не является void, то тип выражения вызова метода получается путем применения преобразования захвата (§5.1.10) к типу возврата типа вызова объявления на этапе компиляции.
При выполнении вызова метода требуется пять шагов. Во-первых, может быть вычислена ссылка на целевой объект. Во-вторых, вычисляются выражения аргументов. В-третьих, проверяется доступность вызываемого метода. В-четвертых, находится фактический код выполняемого метода. В-пятых, создается новая фреймворк активации, при необходимости выполняется синхронизация, и управление передается коду метода.
Следует рассмотреть шесть случаев в зависимости от формы вызова метода:
-
Если форма — ИмяМетода (то есть просто Идентификатор), то:
-
Если режим вызова —
static, то ссылка на целевой объект отсутствует. -
В противном случае, пусть T — объявление типа, содержащего метод, и пусть n — целое число такое, что T является n-ым лексически вложенным объявлением типа класса, в котором непосредственно содержится вызов метода. Ссылка на целевой объект — это n-ый лексически вложенный экземпляр
this.Ошибка во время компиляции, если n-ый лексически вложенный экземпляр
thisне существует.
-
-
Если форма — ИмяТипа
.[ТипАргументов] Идентификатор, то ссылка на целевой объект отсутствует. -
Если форма — ИмяВыражения
.[ТипАргументов] Идентификатор, то:-
Если режим вызова —
static, то ссылка на целевой объект отсутствует. ИмяВыражения вычисляется, но результат затем отбрасывается. -
В противном случае, ссылка на целевой объект — значение, обозначаемое ИмяВыражения.
-
-
Если форма — ПервичноеВыражение
.[ТипАргументов] Идентификатор, то:-
Если режим вызова —
static, то ссылка на целевой объект отсутствует. Выражение ПервичноеВыражение вычисляется, но результат затем отбрасывается. -
В противном случае, выражение ПервичноеВыражение вычисляется, и результат используется как ссылка на целевой объект.
В любом случае, если вычисление выражения ПервичноеВыражение завершается неожиданно, то ни одна часть выражения аргумента не была вычислена, и вызов метода завершается неожиданно по той же причине.
-
-
Если форма —
super.[ТипАргументов] Идентификатор, то ссылка на целевой объект — значениеthis. -
Если форма — ИмяТипа
.super.[ТипАргументов] Идентификатор, то если ИмяТипа обозначает класс, то ссылка на целевой объект — значение ИмяТипа.this; в противном случае, ссылка на целевой объект — значениеthis.
Пример 15.12.4.1-1. Ссылки на целевой объект и методы static
Когда ссылка на целевой объект вычисляется и затем отбрасывается, потому что режим вызова — static, ссылка не проверяется на то, является ли она null:
class Test1 {
static void mountain() {
System.out.println("Monadnock");
}
static Test1 favorite(){
System.out.print("Mount ");
return null;
}
public static void main(String[] args) {
favorite().mountain();
}
}
что выводит:
Mount Monadnock
Здесь favorite() возвращает null, но исключение NullPointerException не выбрасывается.
Пример 15.12.4.1-2. Порядок вычисления при вызове метода
В рамках вызова метода экземпляра (§15.12) есть выражение, обозначающее вызываемый объект. Это выражение полностью вычисляется до вычисления любой части выражения аргумента вызова метода.
Например, в:
class Test2 {
public static void main(String[] args) {
String s = "one";
if (s.startsWith(s = "two"))
System.out.println("oops");
}
}
встреча s перед ".startsWith" вычисляется первой, перед выражением аргумента s = "two". Таким образом, ссылка на строку "one" запоминается как ссылка на целевой объект до изменения локальной переменной s на ссылку на строку "two". В результате, метод startsWith вызывается для целевого объекта "one" с аргументом "two", поэтому результатом вызова является false, так как строка "one" не начинается со строки "two". Следовательно, тестовая программа не выводит "oops".
Процесс вычисления списка аргументов отличается в зависимости от того, является ли вызываемый метод методом фиксированной арности или методом переменной арности (§8.4.1).
Если вызываемый метод — метод переменной арности m, то он обязательно имеет n > 0 формальных параметров. Последний формальный параметр m обязательно имеет тип T[] для некоторого T, и m обязательно вызывается с k ≥ 0 выражениями фактических аргументов.
Если m вызывается с k ≠ n выражениями фактических аргументов или если m вызывается с k = n выражениями фактических аргументов, и тип k-го выражения аргумента не совместим по присваиванию с T[], то список аргументов (e1, ..., en-1, en, ..., ek) вычисляется так, как если бы он был написан как (e1, ..., en-1, new |T[]| { en, ..., ek }), где |T[]| обозначает стирание (§4.6) T[].
Предыдущий абзац создан для обработки взаимодействия параметризованных типов и массивов, которое происходит в Java Virtual Machine с «стеретыми» дженериками. Иными словами, если элемент типа T переменного параметра массива нереализуем, например, List<String>, то с выражением создания массива (§15.10) необходимо обращаться с особой осторожностью, потому что тип элемента создаваемого массива должен быть реализуем. Стервнием типа массива конечного выражения в списке аргументов, мы гарантируем получение реализуемого типа элемента. Затем, так как выражение создания массива появляется в контексте вызова (§5.3), возможно неявное преобразование из типа массива с реализуемым типом элемента в тип массива с нереализуемым типом элемента, в частности, того переменного параметра массива. Компилятор Java должен выдавать предупреждение о неявном преобразовании на этом этапе. Справочная реализация компилятора Java от Oracle идентифицирует это предупреждение о неявном преобразовании как более информативное создание массивов дженериков без проверок.
Теперь выражения аргументов (возможно, переписанные, как описано выше) вычисляются для получения значений аргументов. Каждое значение аргумента соответствует ровно одному формальному параметру метода.
Выражения аргументов, если таковые имеются, вычисляются в порядке слева направо. Если вычисление любого выражения аргумента завершается неожиданно, то ни одна часть любого выражения аргумента справа от него не вычисляется, и вызов метода завершается неожиданно по той же причине. Результатом вычисления 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, издание Java SE 17 и 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в наборе.
Динамический поиск метода может вызвать следующие ошибки:
-
Если вызываемый метод —
abstract, выбрасывается исключениеAbstractMethodError. -
Если вызываемый метод —
default, и в наборе кандидатов на шаге 3 выше появляется более одного метода по умолчанию, выбрасывается исключениеIncompatibleClassChangeError. -
Если режим вызова —
interfaceи вызываемый метод не является ниpublic, ниprivate, выбрасывается исключениеIllegalAccessError.
Вышеприведённая процедура (если она завершается без ошибок) найдёт не-abstract и доступный метод для вызова, при условии, что все классы и интерфейсы в программе были корректно скомпилированы. Однако, если это не так, могут возникнуть различные ошибки, как указано выше; дополнительные сведения о поведении виртуальной машины Java в таких обстоятельствах приведены в Спецификации виртуальной машины Java, издание Java SE 17.
Процесс динамического поиска, хотя он здесь описан явно, часто реализуется неявно, например, как побочный эффект построения и использования таблиц диспетчеризации методов по классу или построения других структур по классу, используемых для эффективной диспетчеризации.
Пример 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, чтобы учесть возможность, что эта форма относится либо к методу-статику, либо к методу-инстансу.
-
-
Если выражение ссылки на метод имеет вид ТипКласса
::[ТипыАргументов]new, потенциально применимые методы — это набор условных методов, соответствующих конструкторам ТипКласса.Если ТипКласса является необработанным типом, но не является типом-членом необработанного типа, кандидатные методы-члены — это те, что указаны в §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 является функциональным типом интерфейса с параметром-подстановкой, то основной целевой тип — это параметризация T без подстановок (§9.9).
-
В противном случае, основной целевой тип — 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, нельзя инкрементировать, потому что при использовании доступа к такой final переменной в качестве выражения, результатом является значение, а не переменная. Таким образом, её нельзя использовать в качестве операнда оператора префиксного инкремента.
Унарное выражение, предваряемое оператором --, представляет собой выражение префиксного декремента.
Результат унарного выражения должен быть переменной типа, который может быть преобразован (§5.1.8) в числовой тип, в противном случае возникает ошибка компиляции.
Тип выражения префиксного декремента — это тип переменной. Результатом выражения префиксного декремента является не переменная, а значение.
Во время выполнения, если вычисление выражения операнда завершается внезапно, то выражение префиксного декремента завершается по той же причине, и декрементация не выполняется. В противном случае значение 1 вычитается из значения переменной, и разность сохраняется обратно в переменную. Перед вычитанием выполняется бинарное числовое продвижение (§5.6) для значения 1 и значения переменной. При необходимости разность сужается с помощью сужающего примитивного преобразования (§5.1.3) и/или подвергается преобразованию упаковки (§5.1.7) до типа переменной перед сохранением. Значением выражения префиксного декремента является значение переменной после сохранения нового значения.
Обратите внимание, что бинарное числовое продвижение, упомянутое выше, может включать преобразование распаковки (§5.1.8).
Переменную, объявленную final, нельзя декрементировать, потому что при использовании доступа к такой 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.
Во время выполнения операнд подвергается преобразованию распаковки (§5.1.8), если это необходимо. Значение унарного логического дополняющего выражения равно true, если значение операнда (возможно, преобразованное) равно false, и false, если значение операнда (возможно, преобразованное) равно true.
Выражение преобразования типов преобразует во время выполнения значение одного числового типа в аналогичное значение другого числового типа; или подтверждает на этапе компиляции, что тип выражения является boolean; или проверяет во время выполнения, что ссылка на значение относится к объекту, чья класс совместим со специфицированным типом ссылки или списком типов ссылок, или который воплощает значение примитивного типа.
Следующая производная из §4.4 показана здесь для удобства:
Скобки и тип или список типов, которые они содержат, иногда называются оператором приведения типа.
Если оператор приведения типов содержит список типов, то есть ReferenceType за которым следуют один или несколько ДополнительноеОграничение элементов, то должны быть выполнены все следующие условия, в противном случае произойдёт ошибка на этапе компиляции:
-
ReferenceType должен обозначать тип класса или интерфейса.
-
Стертые (§4.6) всех перечисленных типов должны быть попарно различными.
-
Никакие два перечисленных типа не могут быть подтипами разных параметризаций одного и того же обобщенного интерфейса.
Целевой тип для контекста приведения типа (§5.5) введенный выражением приведения типа является либо PrimitiveType, либо ReferenceType (если за ним не следуют ДополнительноеОграничение элементы), либо пересечение типов, обозначаемое ReferenceType и ДополнительноеОграничение элементы, появляющиеся в операторе приведения типа.
Тип выражения приведения типов является результатом применения преобразования захвата (§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
Операторы + и - называются арифметическими операторами сложения.
Арифметические операторы сложения имеют одинаковый приоритет и являются левоассоциативными (группируются слева направо).
Если тип одного из операндов оператора + является строкой, то операция представляет собой конкатенацию строк.
В противном случае тип каждого из операндов оператора + должен быть типом, преобразуемым (§5.1.8) в примитивный числовой тип, иначе произойдет ошибка компиляции.
В любом случае тип каждого из операндов бинарного оператора - должен быть типом, преобразуемым (§5.1.8) в примитивный числовой тип, иначе произойдет ошибка компиляции.
Если хотя бы один операнд выражения имеет тип строка, то на другой операнд применяется преобразование в строку (§5.1.11) для получения строки во время выполнения.
Результат конкатенации строк — ссылка на объект строка, который является конкатенацией двух строковых операндов. Символы левого операнда предшествуют символам правого операнда в новой строке.
Объект строка создаётся заново (§12.5), если выражение не является константным выражением (§15.29).
Реализация может выполнять преобразование и конкатенацию в одном шаге для избежания создания и удаления промежуточного объекта строка. Для повышения производительности при многократной конкатенации строк компилятор Java может использовать класс StringBuilder или аналогичную технику для уменьшения количества создаваемых промежуточных объектов строка во время вычисления выражения.
Для примитивных типов реализация также может оптимизировать создание обёртки, преобразуя непосредственно из примитивного типа в строку.
Пример 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).
Обратите внимание, что расширение числовых типов может включать преобразование unboxing (§5.1.8).
Тип арифметического выражения с числовыми операндами — это тип расширенных операндов.
Если этот тип расширенных операндов — int или long, то выполняется целочисленное сложение.
Если этот тип — float или double, то выполняется операция сложения с плавающей точкой.
Сложение — коммутативная операция, если выражения-операнды не имеют побочных эффектов.
Целочисленное сложение — ассоциативная операция, если все операнды имеют одинаковый тип.
Сложение с плавающей точкой не является ассоциативной операцией.
Если при целочисленном сложении происходит переполнение, то результатом является младшие биты математической суммы, представленной в формате с дополнением до двух достаточной разрядности. При переполнении знак результата не совпадает со знаком математической суммы двух значений операндов.
Результат сложения с плавающей точкой определяется правилами арифметики IEEE 754:
-
Если один из операндов NaN, то результатом является NaN.
-
Сумма двух бесконечностей с разными знаками — NaN.
-
Сумма двух бесконечностей с одинаковыми знаками — бесконечность с тем же знаком.
-
Сумма бесконечности и конечного значения равна бесконечному операнду.
-
Сумма двух нулей с разными знаками — положительный ноль.
-
Сумма двух нулей с одинаковыми знаками — ноль с тем же знаком.
-
Сумма нуля и ненулевого конечного значения равна ненулевому операнду.
-
Сумма двух ненулевых конечных значений с одинаковым модулем и разными знаками — положительный ноль.
-
В остальных случаях, когда не участвуют бесконечности, нули или NaN, и операнды имеют одинаковый знак или разные модули, вычисляется точная математическая сумма.
Если модуль суммы слишком велик для представления, то операция переполняется; результатом является бесконечность с соответствующим знаком.
В противном случае сумма округляется до ближайшего представимого значения с использованием правила округления до ближайшего значения (§15.4). Язык программирования Java требует поддержки постепенного уменьшения.
Бинарный оператор - выполняет вычитание двух числовых операндов, возвращая разность операндов; левый операнд — уменьшаемое, а правый — вычитаемое.
Как для целочисленных, так и для чисел с плавающей точкой всегда справедливо, что a - b равно тому же, что a + (-b).
Обратите внимание, что для целочисленных значений вычитание из нуля равно отрицанию. Однако для операндов с плавающей точкой вычитание из нуля не равно отрицанию, потому что если x — 0, то -x — -0, но -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 должен быть совместим с приведением к типу ReferenceType (§5.5), иначе произойдёт ошибка компиляции.
-
Во время выполнения результат оператора сравнения типов определяется следующим образом:
-
Если значение RelationalExpression равно нулевой ссылке (§4.1), то результат —
false. -
Если значение RelationalExpression не равно нулевой ссылке, то результат —
true, если значение можно привести к типу ReferenceType без вызоваClassCastException, иfalseв противном случае.
-
При использовании instanceof в качестве оператора сопоставления с шаблоном применяются следующие правила:
-
Тип выражения RelationalExpression должен быть типом ссылки или типом null, иначе произойдёт ошибка компиляции.
-
RelationalExpression должен быть совместим с Шаблоном (§14.30.1), иначе произойдёт ошибка компиляции.
-
Если тип RelationalExpression является подтипом типа Шаблон, то произойдёт ошибка компиляции.
-
Во время выполнения результат оператора сопоставления с шаблоном определяется следующим образом:
-
Если значение RelationalExpression равно нулевой ссылке, то результат —
false. -
Если значение RelationalExpression не равно нулевой ссылке, то результат —
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).
Обратите внимание, что бинарное числовое продвижение может включать преобразование unboxing (§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, он подвергается преобразованию unboxing (§5.1.8).
Результат == равен true, если оба операнда (после любого необходимого преобразования unboxing) являются 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.
Тип полисправочного условного выражения совпадает с его целевым типом.
Тип самостоятельного ссылочного условного выражения определяется следующим образом:
-
Если у второго и третьего операндов один и тот же тип (который может быть нулевым типом), то это и будет тип условного выражения.
-
Если тип одного из второго и третьего операндов — нулевой тип, а тип другого операнда — ссылочный тип, то тип условного выражения — этот ссылочный тип.
-
В противном случае второй и третий операнды имеют типы S1 и S2 соответственно. Пусть T1 — тип, полученный применением преобразования упаковки к S1, а T2 — тип, полученный применением преобразования упаковки к S2. Тип условного выражения — результат применения преобразования захвата (§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, нельзя ничего присвоить (если она определенно не unassigned (§16 (Definite Assignment))), потому что, когда доступ к такой 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); }
Это ни с чем не совместимо:
() -> { 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, и могут возникнуть ошибки компиляции или предупреждения об unchecked-типах, как указано в §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);
В общем случае лямбда-выражение вида () -> выражение, где выражение — выражение-оператор, интерпретируется либо как () -> { return выражение; }, либо как () -> { выражение; }, в зависимости от целевого типа.
При выполнении, вычисление лямбда-выражения аналогично созданию экземпляра класса, в том смысле, что нормальное завершение приводит к ссылке на объект. Вычисление лямбда-выражения отличается от выполнения тела лямбда-выражения.
Либо создается и инициализируется новый экземпляр класса с указанными ниже свойствами, либо ссылается на существующий экземпляр класса с этими свойствами. Если новый экземпляр должен быть создан, но памяти недостаточно для выделения объекта, вычисление лямбда-выражения завершается прерыванием сбросом исключения 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, Character, Byte, Short, Integer, String или типом перечисления (§8.9), иначе произойдет ошибка компиляции.
Тело выражения switch и оператора switch (§14.11) называется блоком переключателя. Общие правила, которые применяются ко всем блокам переключателя, появляющимся в выражениях switch или операторах switch, приведены в §14.11.1. Следующие правила из §14.11.1 приведены здесь для удобства:
Помимо общих правил для блоков switch (§14.11.1), для блоков switch в выражениях switch существуют дополнительные правила. А именно, для блока switch выражения switch должны выполняться следующие условия, в противном случае произойдет ошибка компиляции:
-
Если тип выражения-селектора не является типом перечисления, то ровно одна
defaultметка ассоциирована с блоком switch. -
Если тип выражения-селектора является типом перечисления, то (i) набор
caseконстант, связанных с блоком switch, включает все константы перечисления этого типа, и (ii) не более однойdefaultметки ассоциировано с блоком switch.Метка
defaultразрешена, но не обязательна, когдаcaseметки покрывают все константы перечисления. -
Если блок switch состоит из правил switch, то ни один блок правила switch не может завершиться нормально (§14.22).
-
Если блок switch состоит из групп операторов с метками switch, то последний оператор в блоке switch не может завершиться нормально, и блок switch не имеет меток switch после последней группы операторов с метками switch.
Выражения switch не могут иметь пустые блоки switch, в отличие от операторов switch. Кроме того, выражения switch отличаются от операторов switch по типам выражений, которые могут появляться справа от стрелки (->) в блоке switch, то есть по тем выражениям, которые могут использоваться в качестве выражений правил switch. В выражении switch любое выражение может использоваться в качестве выражения правила switch, но в операторе switch может использоваться только выражение оператора (§14.11.1).
Выражения результатов выражения switch определяются следующим образом:
-
Если блок switch состоит из правил switch, то каждое правило switch рассматривается по очереди:
-
Если правило switch имеет вид
...->Выражение;, то Выражение является выражением результата выраженияswitch. -
Если правило switch имеет вид
...->Блок, то каждое выражение, непосредственно содержащееся в оператореyieldв Блоке, цель передачи которого — данное выражениеswitch, является выражением результата выраженияswitch.
-
-
Если блок switch состоит из групп операторов с метками switch, то каждое выражение, непосредственно содержащееся в операторе
yieldв блоке switch, цель передачи которого — данное выражение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завершается внезапно по той же причине. -
В противном случае, если результат вычисления выражения-селектора является
null, то выбрасываетсяNullPointerException, и всё выражениеswitchзавершается внезапно по этой причине. -
В противном случае, если результат вычисления выражения-селектора имеет тип
Character,Byte,ShortилиInteger, к нему применяется преобразование unboxing (§5.1.8). Если это преобразование завершается внезапно, то всё выражениеswitchзавершается внезапно по той же причине.
Если вычисление выражения-селектора завершается нормально, результат — не null, и последующее преобразование unboxing (если есть) завершается нормально, то вычисление выражения switch продолжается путём определения, соответствует ли метка switch, связанная с блоком switch, значению выражения-селектора (§14.11.1). Затем:
-
Если ни одна метка switch не соответствует, то выбрасывается
IncompatibleClassChangeError, и всё выражение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.