Глава 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 24 (§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только в том случае, если класс объекта сброшенного исключения являетсяinstanceofтипом формального параметра блокаcatch.
Ситуации, когда класс объекта не известен статически, могут привести к ошибкам типа во время выполнения.
Кроме того, существуют ситуации, когда статически известный тип может быть неверен во время выполнения. Такие ситуации могут возникнуть в программе, которая порождает не проверяемые во время компиляции предупреждения. Такие предупреждения выдаются в ответ на операции, безопасность которых не может быть статически гарантирована и которые не могут быть немедленно подвергнуты динамической проверке, потому что они включают нереализуемые типы (§4.7). В результате, динамические проверки в ходе выполнения программы могут обнаружить несоответствия и привести к ошибкам типа во время выполнения.
Ошибка типа во время выполнения может возникнуть только в этих ситуациях:
-
При приведении типов, когда фактический класс объекта, на который ссылается значение выражения операнда, несовместим с целевым типом, указанным оператором приведения (§5.5, §15.16); в этом случае генерируется исключение
ClassCastException. -
При автоматически генерируемом приведении типов, введенном для обеспечения корректности операции с нереализуемым типом (§4.7).
-
При присваивании компоненту массива ссылочного типа, когда фактический класс объекта, значение которого нужно присвоить, несовместим с фактическим типом компонента массива во время выполнения (§10.5, §15.13, §15.26.1); в этом случае генерируется исключение
ArrayStoreException. -
Когда исключение не перехватывается ни одним блоком
catchоператораtry(§14.20); в этом случае поток управления, который сначала столкнулся с исключением, пытается вызвать обработчик неперехваченного исключения (§11.3), а затем завершается.
У каждого выражения есть нормальный режим вычисления, в котором выполняются определенные вычислительные шаги. В следующих разделах описывается нормальный режим вычисления для каждого типа выражения.
Если все шаги выполняются без возникновения исключения, то говорят, что выражение завершается нормально.
Однако, если вычисление выражения вызывает исключение, то говорят, что выражение завершается прерывисто. Прерывистое завершение всегда имеет связанную причину, которая всегда является throw с заданным значением.
Исключения во время выполнения генерируются предопределенными операторами следующим образом:
-
Выражение создания экземпляра класса (§15.9.4), выражение создания массива (§15.10.2), выражение ссылки на метод (§15.13.3), выражение инициализатора массива (§10.6), выражение оператора конкатенации строк (§15.18.1) или лямбда-выражение (§15.27.4) генерирует
OutOfMemoryError, если доступно недостаточно памяти. -
Выражение создания массива (§15.10.2) генерирует
NegativeArraySizeException, если значение любого выражения размерности меньше нуля. -
Выражение доступа к элементу массива (§15.10.4) генерирует
NullPointerException, если значение выражения ссылки на массив являетсяnull. -
Выражение доступа к элементу массива (§15.10.4) генерирует
ArrayIndexOutOfBoundsException, если значение выражения индекса массива отрицательно или больше или равноlengthмассива. -
Выражение доступа к полю (§15.11) генерирует
NullPointerException, если значение выражения ссылки на объект являетсяnull. -
Выражение вызова метода (§15.12), вызывающее метод экземпляра, генерирует
NullPointerException, если целевая ссылка являетсяnull. -
Выражение приведения типов (§15.16) генерирует
ClassCastException, если приведение типов признано недопустимым во время выполнения. -
Оператор целочисленного деления (§15.17.2) или целочисленного остатка (§15.17.3) генерирует
ArithmeticException, если значение выражения правого операнда равно нулю. -
Присвоение элементу массива ссылочного типа (§15.26.1), выражение вызова метода (§15.12) или префиксный или постфиксный операторы инкремента (§15.14.2, §15.15.1) или декремента (§15.14.3, §15.15.2) могут сгенерировать
OutOfMemoryErrorв результате преобразования к объектной форме (§5.1.7). -
Присвоение элементу массива ссылочного типа (§15.26.1) генерирует
ArrayStoreException, когда присваиваемое значение несовместимо с типом компонента массива (§10.5). -
Выражение
switch(§15.28) или расширенное выражениеswitch(§14.11.2) генерируетMatchException, если метка переключателя не применима к значению выражения селектора.
Выражение вызова метода также может привести к генерации исключения, если возникает исключение, которое приводит к прерывистому завершению выполнения тела метода.
Выражение создания экземпляра класса также может привести к генерации исключения, если возникает исключение, которое приводит к прерывистому завершению выполнения конструктора.
Во время вычисления выражения также могут возникнуть различные ошибки связывания и виртуальной машины. По своей природе такие ошибки трудно предсказать и трудно обработать.
Если возникает исключение, то вычисление одного или нескольких выражений может быть прервано до завершения всех шагов их нормального режима вычисления; такие выражения говорят, что завершаются прерывисто.
Если вычисление выражения требует вычисления подвыражения, то прерывистое завершение подвыражения всегда вызывает немедленное прерывистое завершение самого выражения с той же причиной, и все последующие шаги в нормальном режиме вычисления не выполняются.
Термины «завершается нормально» и «завершается прерывисто» также применяются к выполнению операторов (§14.1). Оператор может завершиться прерывисто по различным причинам, а не только из-за возникновения исключения.
Язык программирования Java гарантирует, что операнды операторов, по-видимому, вычисляются в определенном порядке вычисления, а именно слева направо.
Рекомендуется, чтобы код не полагался существенно на это спецификацию. Код обычно более понятен, когда каждое выражение содержит не более одного побочного эффекта, как его внешнюю операцию, и когда код не зависит от того, какая именно исключение возникает в результате левостороннего вычисления выражений.
Левый операнд бинарного оператора, по-видимому, полностью вычисляется перед любой частью правого операнда.
Если оператор является оператором сложного присваивания (§15.26.2), то вычисление левого операнда включает в себя как запоминание переменной, которую обозначает левый операнд, так и извлечение и сохранение значения этой переменной для использования в подразумеваемой бинарной операции.
Если вычисление левого операнда бинарного оператора завершается внезапно, ни одна часть правого операнда, по-видимому, не была вычислена.
Пример 15.7.1-1. Левый операнд вычисляется в первую очередь
В следующей программе оператор * имеет левый операнд, содержащий присвоение переменной, и правый операнд, содержащий ссылку на ту же переменную. Значение, полученное по ссылке, отразит тот факт, что присвоение произошло в первую очередь.
class Test1 {
public static void main(String[] args) {
int i = 2;
int j = (i=3) * i;
System.out.println(j);
}
}
Эта программа выводит:
9
Не разрешается, чтобы вычисление оператора * производило 6 вместо 9.
Пример 15.7.1-2. Неявный левый операнд в операторе сложного присваивания
В следующей программе оба оператора присваивания сначала извлекают и запоминают значение левого операнда, которое равно 9, прежде чем вычисляется правый операнд оператора сложения, в этот момент переменная устанавливается в значение 3.
class Test2 {
public static void main(String[] args) {
int a = 9;
a += (a = 3); // first example
System.out.println(a);
int b = 9;
b = b + (b = 3); // second example
System.out.println(b);
}
}
Эта программа выводит:
12 12
Не разрешается, чтобы ни один оператор присваивания (сложный для a, простой для b) производил результат 6.
См. также пример в §15.26.2.
Пример 15.7.1-3. Преждевременное завершение вычисления левого операнда
class Test3 {
public static void main(String[] args) {
int j = 1;
try {
int i = forgetIt() / (j = 2);
} catch (Exception e) {
System.out.println(e);
System.out.println("Now j = " + j);
}
}
static int forgetIt() throws Exception {
throw new Exception("I'm outta here!");
}
}
Эта программа выводит:
java.lang.Exception: I'm outta here! Now j = 1
То есть, левый операнд forgetIt() оператора / генерирует исключение, прежде чем вычисляется правый операнд, и происходит встроенное присвоение 2 переменной j.
Язык программирования Java гарантирует, что каждый операнд оператора (кроме условных операторов &&, || и ? :) вычисляется полностью, прежде чем любая часть самой операции будет выполнена.
Если бинарный оператор является оператором целочисленного деления / (§15.17.2) или целочисленным остатком % (§15.17.3), то его выполнение может вызвать ArithmeticException, но это исключение выбрасывается только после того, как оба операнда бинарного оператора были вычислены и только если эти вычисления завершились нормально.
Пример 15.7.2-1. Вычисление операндов перед операцией
class Test {
public static void main(String[] args) {
int divisor = 0;
try {
int i = 1 / (divisor * loseBig());
} catch (Exception e) {
System.out.println(e);
}
}
static int loseBig() throws Exception {
throw new Exception("Shuffle off to Buffalo!");
}
}
Эта программа выводит:
java.lang.Exception: Shuffle off to Buffalo!
и не:
java.lang.ArithmeticException: / by zero
так как ни одна часть операции деления, включая сигнализацию исключения деления на ноль, не может, по-видимому, произойти до завершения вызова loseBig, даже если реализация может определить или вывести, что операция деления обязательно приведет к исключению деления на ноль.
Язык программирования Java соблюдает порядок вычисления, указанный явно скобками и неявно приоритетом операторов.
Реализация языка программирования Java не может использовать алгебраические тождества, такие как ассоциативный закон, для переписывания выражений в более удобном порядке вычисления, если не может быть доказано, что выражение-замена эквивалентно по значению и по наблюдаемым побочным эффектам, даже при наличии нескольких потоков выполнения (используя модель выполнения потоков в §17 (Потоки и блокировки)), для всех возможных вычислительных значений, которые могут быть вовлечены.
В случае вычислений с плавающей точкой это правило также применяется к бесконечности и значениям не-числа (NaN).
Например, !(x<y) не может быть переписан как x>=y, потому что эти выражения имеют разные значения, если либо x, либо y равны NaN, или оба равны NaN.
Конкретно, вычисления с плавающей точкой, которые кажутся математически ассоциативными, вряд ли будут вычислительно ассоциативными. Такие вычисления не должны быть переупорядочены бездумно.
Например, неверно, чтобы компилятор Java переписывал 4.0*x*0.5 как 2.0*x; хотя здесь округление не является проблемой, существуют большие значения x, для которых первое выражение производит бесконечность (из-за переполнения), а второе выражение производит конечный результат.
Например, тестовая программа:
class Test {
public static void main(String[] args) {
double d = 8E307;
System.out.println(4.0 * d * 0.5);
System.out.println(2.0 * d);
}
}
выводит:
Infinity 1.6E308
потому что первое выражение переполняется, а второе — нет.
В отличие от этого, целочисленное сложение и умножение являются доказуемо ассоциативными в языке программирования Java.
Например, a+b+c, где a, b и c являются локальными переменными (это упрощающее предположение избегает проблем, связанных с несколькими потоками и volatile переменными), всегда будет давать одинаковый ответ, независимо от того, вычисляется ли он как (a+b)+c или a+(b+c); если выражение b+c появляется поблизости в коде, умный компилятор Java может использовать эту общую подвыражение.
В выражении вызова метода или конструктора или создания экземпляра класса выражения аргументов могут появляться в скобках, разделенные запятыми. Каждое выражение аргумента вычисляется полностью, прежде чем любая часть любого выражения аргумента справа от него.
Если вычисление выражения аргумента внезапно завершается, ни одна часть выражения аргумента справа от него, по-видимому, не была вычислена.
Пример 15.7.4-1. Порядок вычисления при вызове метода
class Test1 {
public static void main(String[] args) {
String s = "going, ";
print3(s, s, s = "gone");
}
static void print3(String a, String b, String c) {
System.out.println(a + b + c);
}
}
Эта программа выводит:
going, going, gone
потому что присвоение строки "gone" переменной s происходит после того, как первые два аргумента для print3 были вычислены.
Пример 15.7.4-2. Преждевременное завершение выражения аргумента
class Test2 {
static int id;
public static void main(String[] args) {
try {
test(id = 1, oops(), id = 3);
} catch (Exception e) {
System.out.println(e + ", id=" + id);
}
}
static int test(int a, int b, int c) {
return a + b + c;
}
static int oops() throws Exception {
throw new Exception("oops");
}
}
Эта программа выводит:
java.lang.Exception: oops, id=1
потому что присвоение 3 переменной id не выполняется.
Порядок вычисления некоторых выражений не полностью охватывается этими общими правилами, потому что эти выражения иногда могут вызывать исключительные ситуации, которые необходимо указать. См. подробные объяснения порядка вычисления для следующих типов выражений:
Первичные выражения включают в себя большинство простейших типов выражений, из которых строятся все остальные: литералы, создания объектов, доступ к полям, вызовы методов, ссылки на методы и доступ к элементам массива. Выражение в скобках также синтаксически обрабатывается как первичное выражение.
Эта часть грамматики языка программирования Java необычна по двум причинам. Во-первых, можно ожидать, что простые имена, такие как имена локальных переменных и параметров методов, будут первичными выражениями. По техническим причинам имена объединяются с первичными выражениями чуть позже, когда вводятся постфиксные выражения (§15.14).
Технические причины связаны с разрешением левостороннего разбора программ Java с только одним токеном предварительного просмотра. Рассмотрите выражения (z[3]) и (z[]). Первое — это скобочное обращение к элементу массива (§15.10.3), а второе — начало приведения типа (§15.16). В момент, когда символом предварительного просмотра является [, левосторонний разбор сведет z к нетерминалу Имя. В контексте приведения типа нам не нужно сводить имя к Первичному выражению, но если Имя было одним из альтернатив для Первичного выражения, то мы не смогли бы определить, следует ли выполнить сводку (то есть, мы не смогли бы определить, является ли текущая ситуация скобочным доступом к массиву или приведением типа) без просмотра двух токенов, токена, следующего за [. Здесь приведенная грамматика избегает проблемы, сохраняя Имя и Первичное выражение разделенными и разрешая их в некоторых других синтаксических правилах (тех, что для ClassInstanceCreationExpression, MethodInvocation, ArrayAccess и PostfixExpression, но не FieldAccess, так как он использует идентификатор непосредственно). Эта стратегия фактически откладывает вопрос о том, следует ли рассматривать Имя как Первичное выражение, пока не будет рассмотрен больше контекста.
Вторая необычная особенность избегает потенциальной грамматической неоднозначности в выражении "new int[3][3]", которое в Java всегда означает одиночное создание многомерного массива, но без надлежащего грамматического изящества можно было бы интерпретировать и как означающее то же, что и "(new int[3])[3]".
Эта неоднозначность устраняется путем разделения ожидаемого определения Первичного выражения на Первичное выражение и Первичное выражение без создания массива. (Это можно сравнить с разделением Утверждения на Утверждение и Утверждение без краткого if (§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.5) или текстового блока (§3.10.6) равен
String(§4.3.3). -
Тип литерала null
null(§3.10.8) равен типу null (§4.1); его значение — ссылка null.
Оценка лексического литерала всегда завершается нормально.
Литерал класса — это выражение, состоящее из имени класса, интерфейса, типа массива или примитивного типа, или псевдотипа 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>.
Литерал класса вычисляет значение, представляющее объект для указанного класса, интерфейса, типа массива или примитивного типа (или для void), как определено загрузчиком классов, определяющим класс текущего экземпляра (§12.2).
Ключевое слово this может использоваться в качестве выражения в следующих контекстах:
-
в теле метода экземпляра класса (§8.4.3.2)
-
в теле конструктора класса (§8.8.7)
-
в инициализаторе экземпляра класса (§8.6)
-
в инициализаторе переменной экземпляра класса (§8.3.2)
-
в теле метода экземпляра интерфейса, то есть в методе по умолчанию или методе интерфейса, не являющемся
staticprivate(§9.4)
Когда используется как выражение, ключевое слово 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нет непосредственно окружающего экземпляра.Если непосредственный суперкласс C — вложенный локальный класс
L, который встречается в статическом контексте, то пусть S будет ближайшимstaticметодом,staticобъявлением поля или статическим инициализатором, который окружает объявлениеL. Если ближайшийstaticметод,staticобъявление поля или статический инициализатор, который окружает выражение создания экземпляра класса, не является S, то возникает ошибка компиляции. -
В противном случае, непосредственно окружающим экземпляром
iявляетсяthis.
-
-
Если C — вложенный локальный класс, то:
-
Если C встречается в статическом контексте, то у
iнет непосредственно окружающего экземпляра.Пусть S будет ближайшим
staticметодом,staticобъявлением поля или статическим инициализатором, который окружает объявление C. Если ближайшийstaticметод,staticобъявление поля или статический инициализатор, который окружает выражение создания экземпляра класса, не является S, то возникает ошибка компиляции. -
В противном случае, если выражение создания экземпляра класса встречается в статическом контексте, то возникает ошибка компиляции.
-
В противном случае, пусть O — непосредственно окружающее объявление класса или интерфейса C, а U — непосредственно окружающее объявление класса или интерфейса выражения создания экземпляра класса.
Если U не является вложенным классом O или O самим по себе, то возникает ошибка компиляции.
Пусть n — целое число такое, что O — n-е лексически окружающее объявление класса или интерфейса U.
Непосредственно окружающим экземпляром
iявляется n-й лексически окружающий экземплярthis.
-
-
Если C — вложенный членский класс, то:
-
Если выражение создания экземпляра класса неквалифицировано, то:
-
Если выражение создания экземпляра класса встречается в статическом контексте, то возникает ошибка компиляции.
-
В противном случае, если C не является членом любого класса, объявление которого лексически окружает выражение создания экземпляра класса, то возникает ошибка компиляции.
-
В противном случае, пусть O — самое внутреннее окружающее объявление класса, членом которого является C, а U — непосредственно окружающее объявление класса или интерфейса выражения создания экземпляра класса.
Если U не является вложенным классом O или O самим по себе, то возникает ошибка компиляции.
Пусть n — целое число такое, что O — n-е лексически окружающее объявление класса или интерфейса U.
Непосредственно окружающим экземпляром
iявляется n-й лексически окружающий экземплярthis.
-
-
Если выражение создания экземпляра класса квалифицировано, то непосредственно окружающим экземпляром
iявляется объект, который является значением выражения Primary или ExpressionName.
-
Если C — анонимный класс, а его непосредственный суперкласс S — вложенный класс, то i может иметь непосредственно окружающий экземпляр относительно S, определяемый следующим образом:
-
Если S — вложенный локальный класс, то:
-
Если S встречается в статическом контексте, то у
iнет непосредственно окружающего экземпляра относительно S. -
В противном случае, если выражение создания экземпляра класса встречается в статическом контексте, то возникает ошибка компиляции.
-
В противном случае, пусть O — непосредственно окружающее объявление класса или интерфейса S, а U — непосредственно окружающее объявление класса или интерфейса выражения создания экземпляра класса.
Если U не является вложенным классом O или O самим по себе, то возникает ошибка компиляции.
Пусть n — целое число такое, что O — n-е лексически окружающее объявление класса или интерфейса U.
Непосредственно окружающим экземпляром
iотносительно S является n-й лексически окружающий экземплярthis.
-
-
Если S — вложенный членский класс, то:
-
Если выражение создания экземпляра класса неквалифицировано, то:
-
Если выражение создания экземпляра класса встречается в статическом контексте, то возникает ошибка компиляции.
-
В противном случае, если S не является членом любого класса, объявление которого окружает выражение создания экземпляра класса, то возникает ошибка компиляции.
-
В противном случае, пусть O — самое внутреннее окружающее объявление класса, членом которого является S, а U — непосредственно окружающее объявление класса или интерфейса выражения создания экземпляра класса.
Если U не является вложенным классом O или O самим по себе, то возникает ошибка компиляции.
Пусть n — целое число такое, что O — n-е лексически окружающее объявление класса или интерфейса U.
Непосредственно окружающим экземпляром
iотносительно S является n-й лексически окружающий экземплярthis. -
В противном случае, возникает ошибка компиляции.
-
-
Если выражение создания экземпляра класса квалифицировано, то непосредственно окружающим экземпляром
iотносительно S является объект, который является значением выражения Primary или ExpressionName.
-
Пусть C — класс, который необходимо создать. Для создания экземпляра C, i, конструктор C выбирается во время компиляции по следующим правилам.
Сначала определяются фактические аргументы вызова конструктора:
-
Если C — анонимный класс с прямым суперклассом S, то:
-
Если S не является внутренним классом или если S — локальный класс, который встречается в статическом контексте, то аргументы конструктора — это аргументы в списке аргументов выражения создания экземпляра класса, если таковые имеются, в порядке их появления в выражении.
-
В противном случае, первым аргументом конструктора является непосредственно окружающий экземпляр
iотносительно S (§15.9.2), а последующие аргументы конструктора — это аргументы в списке аргументов выражения создания экземпляра класса, если таковые имеются, в порядке их появления в выражении создания экземпляра класса.
-
-
Если C — локальный класс или
privateвнутренний член класса, то аргументы конструктора — это аргументы в списке аргументов выражения создания экземпляра класса, если таковые имеются, в порядке их появления в выражении создания экземпляра класса. -
Если C — не-
privateвнутренний член класса, то первым аргументом конструктора является непосредственно окружающий экземплярi(§8.8.1, §15.9.2), а последующие аргументы его конструктора — это аргументы в списке аргументов выражения создания экземпляра класса, если таковые имеются, в порядке их появления в выражении создания экземпляра класса. -
В противном случае, аргументы конструктора — это аргументы в списке аргументов выражения создания экземпляра класса, если таковые имеются, в порядке их появления в выражении.
Во-вторых, определяется конструктор C и соответствующая throws клауза и тип возвращаемого значения:
-
Если выражение создания экземпляра класса не использует
<>, то:-
Если C — не анонимный класс, то:
Пусть T — тип, обозначаемый C со всеми аргументами типа класса в выражении. Процесс, описанный в §15.12.2, изменённый для обработки конструкторов, используется для выбора одного из конструкторов T и определения его
throwsфрагмента.Если в T нет уникального наиболее специфичного конструктора, который является применимым и доступным (§6.6), то возникает ошибка компиляции (как при вызовах методов).
В противном случае, тип возвращаемого значения, соответствующий выбранному конструктору, — T.
-
Если C — анонимный класс, то:
Процесс, описанный в §15.12.2, изменённый для обработки конструкторов, используется для выбора одного из конструкторов непосредственного суперкласса типа C и определения его
throwsфрагмента.Если в непосредственном суперклассе типа C нет уникального наиболее специфичного конструктора, который является применимым и доступным, то возникает ошибка компиляции (как при вызовах методов).
В противном случае, анонимный конструктор C выбирается как конструктор C (§15.9.5.1). Его тело состоит из явного вызова конструктора (§8.8.7.1) конструктора, выбранного в непосредственном суперклассе типа C.
throwsфрагмент выбранного конструктора включает исключения вthrowsфрагменте конструктора, выбранного в непосредственном суперклассе типа C.Тип возвращаемого значения, соответствующий выбранному конструктору, — тип анонимного класса.
-
-
Если выражение создания экземпляра класса использует
<>, то:Если C — не анонимный класс, пусть D будет таким же, как C. Если C — анонимный класс, пусть D — суперкласс или суперинтерфейс C, указанный в выражении создания экземпляра класса.
Если D — класс, пусть
c1, ...,cn— конструкторы класса D. Если D — интерфейс, пустьc1, ...,cn— список из одного элемента (n = 1), содержащий конструктор без аргументов классаObject.Список методов
m1, ...,mnопределён для целей разрешения перегрузки и вывода аргументов типа. Для всех j (1 ≤ j ≤ n),mjопределяется в терминахcjследующим образом:-
Сначала определяется подстановка θ
jдля инстанцирования типов вcj.Пусть F1, ..., Fp — параметры типа D, и пусть G1, ..., Gq — параметры типа (если таковые имеются)
cj. Пусть X1, ..., Xp и Y1, ..., Yq — переменные типа с разными именами, которые не находятся в области видимости в теле D.θ
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 использует ромб (<>) для типов аргументов создаваемого класса, то для всех методов, объявленных в ClassBody, не являющихся private, метод объявляется как будто он аннотирован с @Override (§9.6.4.4).
При использовании <>, выведенные типы аргументов могут не соответствовать ожиданиям программиста. Вследствие этого, супертип анонимного класса может не соответствовать ожиданиям, а методы, объявленные в анонимном классе, могут не переопределять методы супертипа так, как предполагалось. Обращение к таким методам, как будто они аннотированы @Override (если они не явным образом аннотированы @Override), помогает избежать неявных ошибок в программах.
Анонимный класс не может иметь явно объявленного конструктора. Вместо этого для анонимного класса неявно объявляется анонимный конструктор. Форма анонимного конструктора для анонимного класса C с непосредственным суперклассом S следующая:
-
Если S не является внутренним классом или если S является локальным классом, который встречается в статическом контексте, то анонимный конструктор имеет один формальный параметр для каждого фактического аргумента выражения создания экземпляра класса или константы перечисления, которые объявляют C.
Фактические аргументы выражения создания экземпляра класса или константы перечисления используются для определения конструктора
xкласса S, как указано в §15.9.3. Тип каждого формального параметра анонимного конструктора идентифицируется с соответствующим формальным параметромx.Тело анонимного конструктора состоит из явного вызова конструктора (§8.8.7.1) вида
super(...), где фактические аргументы являются формальными параметрами анонимного конструктора в порядке их объявления. Вызываемый конструктор суперкласса являетсяx. -
В противном случае, первый формальный параметр анонимного конструктора представляет значение непосредственно окружающего экземпляра
iотносительно S (§15.9.2). Тип этого параметра — это тип класса, непосредственно окружающий объявление S.Анонимный конструктор имеет дополнительный формальный параметр для каждого фактического аргумента выражения создания экземпляра класса, объявившего анонимный класс. n'ый формальный параметр соответствует n-1'ому фактическому аргументу.
Фактические аргументы выражения создания экземпляра класса используются для определения конструктора
xкласса S, как указано в §15.9.3. Тип каждого формального параметра анонимного конструктора идентифицируется с соответствующим формальным параметромx.Тело анонимного конструктора состоит из явного вызова конструктора вида
o.super(...), гдеo— это первый формальный параметр анонимного конструктора, а фактические аргументы — последующие формальные параметры конструктора в порядке их объявления. Вызываемый конструктор суперкласса являетсяx.
Во всех случаях, раздел throws анонимного конструктора перечисляет все исключения, обрабатываемые в явном операторе вызова конструктора, содержащемся в анонимном конструкторе, как указано в §15.9.3, и все исключения, обрабатываемые в инициализаторах экземпляров или инициализаторах переменных экземпляра анонимного класса.
Обратите внимание, что возможно, что сигнатура анонимного конструктора относится к недоступному типу (например, если такой тип встречается в сигнатуре конструктора суперкласса x). Это само по себе не вызывает ошибок во время компиляции или выполнения.
Выражение создания массива используется для создания новых массивов (§10 (Массивы)).
Следующая продукция из §4.3 приведена здесь для удобства:
Выражение создания массива создает объект, являющийся новым массивом, элементы которого имеют тип, указанный PrimitiveType или ClassOrInterfaceType.
Ошибка компиляции, если ClassOrInterfaceType не обозначает реифицируемый тип (§4.7). В противном случае ClassOrInterfaceType может именовать любой именованный ссылочный тип, даже тип abstract класса (§8.1.1.1) или интерфейса.
Приведенные выше правила подразумевают, что тип элемента в выражении создания массива не может быть параметризованным типом, если все аргументы типа параметризованного типа не являются неограниченными джойлвайндами.
Тип каждого выражения измерения в DimExpr должен быть типом, преобразуемым (§5.1.8) в целочисленный тип, в противном случае возникает ошибка компиляции.
Каждое выражение измерения подвергается численному продвижению с унарным преобразованием (§5.6). Продвинутый тип должен быть int, в противном случае возникает ошибка компиляции.
Тип выражения создания массива — это массивный тип, который может быть обозначен копией выражения создания массива, из которого удалены ключевое слово new и каждое выражение DimExpr и инициализатор массива.
Например, тип выражения создания:
new double[3][3][]
является:
double[][][]
Во время выполнения выражение создания массива ведет себя следующим образом:
-
Если нет выражений измерения, то должен быть инициализатор массива. Новый выделенный массив будет инициализирован значениями, предоставленными инициализатором массива, как описано в §10.6. Значение инициализатора массива становится значением выражения создания массива.
-
В противном случае нет инициализатора массива, и:
-
Сначала выражения измерения вычисляются слева направо. Если вычисление любого из выражений завершается прерыванием, выражения справа от него не вычисляются.
-
Далее проверяются значения выражений измерения. Если значение любого выражения DimExpr меньше нуля, то бросается
NegativeArraySizeException. -
Далее выделяется место для нового массива. Если памяти недостаточно для выделения массива, вычисление выражения создания массива завершается прерыванием сбросом
OutOfMemoryError. -
Затем, если появляется одно DimExpr, создается одномерный массив указанной длины, и каждый компонент массива инициализируется его значением по умолчанию (§4.12.5).
-
В противном случае, если появляется n выражений DimExpr, создание массива эффективно выполняет набор вложенных циклов глубиной n
-1для создания подразумеваемых массивов массивов.Многомерный массив не обязательно должен иметь массивы одинаковой длины на каждом уровне.
-
Пример 15.10.2-1. Вычисление создания массива
В выражении создания массива с одним или несколькими выражениями измерения каждое выражение измерения полностью вычисляется до начала вычисления любой части любого выражения измерения справа от него. Таким образом:
class Test1 {
public static void main(String[] args) {
int i = 4;
int[][] ia = new int[i][i=3];
System.out.println(
"[" + ia.length + "," + ia[0].length + "]");
}
}
выводит:
[4,3]
потому что первое измерение вычисляется как 4, прежде чем выражение второго измерения устанавливает i в 3.
Если вычисление выражения измерения завершается прерыванием, никакая часть любого выражения измерения справа от него не будет как будто вычислена. Таким образом:
class Test2 {
public static void main(String[] args) {
int[][] a = { { 00, 01 }, { 10, 11 } };
int i = 99;
try {
a[val()][i = 1]++;
} catch (Exception e) {
System.out.println(e + ", i=" + i);
}
}
static int val() throws Exception {
throw new Exception("unimplemented");
}
}
выводит:
java.lang.Exception: unimplemented, i=99
потому что вложенное присваивание, которое устанавливает i в 1, никогда не выполняется.
Пример 15.10.2-2. Создание многомерного массива
Объявление:
float[][] matrix = new float[3][3];
эквивалентно по поведению:
float[][] matrix = new float[3][]; for (intd= 0;d< matrix.length;d++) matrix[d] = new float[3];
и:
Age[][][][][] Aquarius = new Age[6][10][8][12][];
эквивалентно:
Age[][][][][] Aquarius = new Age[6][][][][]; for (intd1= 0;d1< Aquarius.length;d1++) { Aquarius[d1] = new Age[10][][][]; for (intd2= 0;d2< Aquarius[d1].length;d2++) { Aquarius[d1][d2] = new Age[8][][]; for (intd3= 0;d3< Aquarius[d1][d2].length;d3++) { Aquarius[d1][d2][d3] = new Age[12][]; } } }
с d, d1, d2 и d3 заменены именами, которые уже не объявлены локально. Таким образом, одно выражение new фактически создает один массив длиной 6, 6 массивов длиной 10, 6x10 = 60 массивов длиной 8 и 6x10x8 = 480 массивов длиной 12. Этот пример оставляет пятое измерение, которое представляло бы массивы, содержащие фактические элементы массива (ссылки на Age объекты), инициализированными только ссылками null. Эти массивы можно заполнить позже другим кодом, например:
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.1).
Тип выражения ссылки на массив должен быть типом массива (назовем его T[], массив, чьи компоненты имеют тип T), в противном случае произойдет ошибка компиляции.
Выражение индекса претерпевает числовое продвижение (§5.6). Продвинутый тип должен быть int, в противном случае произойдет ошибка компиляции.
Тип выражения доступа к элементу массива является результатом применения преобразования захвата (§5.1.10) к T.
Результат выражения доступа к элементу массива — это переменная типа T, а именно переменная внутри массива, выбранная значением выражения индекса.
Эта результирующая переменная, являющаяся компонентом массива, никогда не рассматривается final, даже если выражение ссылки на массив обозначало переменную final.
Во время выполнения вычисление выражения доступа к элементу массива происходит следующим образом:
-
Сначала вычисляется выражение ссылки на массив. Если это вычисление завершается внезапно, то доступ к элементу массива также завершается по той же причине, и выражение индекса не вычисляется.
-
В противном случае вычисляется выражение индекса. Если это вычисление завершается внезапно, то доступ к элементу массива также завершается по той же причине.
-
В противном случае, если значение выражения ссылки на массив равно
null, выбрасываетсяNullPointerException. -
В противном случае, значение выражения ссылки на массив действительно ссылается на массив. Если значение выражения индекса меньше нуля или больше или равно размеру массива
length, то выбрасываетсяArrayIndexOutOfBoundsException. -
В противном случае результатом доступа к элементу массива является переменная типа T, внутри массива, выбранная значением выражения индекса.
Пример 15.10.4-1. Ссылка на массив вычисляется первой
При доступе к элементу массива выражение слева от квадратных скобок полностью вычисляется до вычисления какой-либо части выражения в квадратных скобках. Например, в (несомненно, чудовищном) выражении a[(a=b)[3]], выражение a полностью вычисляется до вычисления выражения (a=b)[3]; это означает, что исходное значение a извлекается и запоминается, в то время как вычисляется выражение (a=b)[3]. Этот массив, на который ссылается исходное значение a, затем индексируется значением, которое является элементом 3 другого массива (возможно, того же массива), на который ссылался b и теперь также ссылается на a.
Таким образом, программа:
class Test1 {
public static void main(String[] args) {
int[] a = { 11, 12, 13, 14 };
int[] b = { 0, 1, 2, 3 };
System.out.println(a[(a=b)[3]]);
}
}
выводит:
14
потому что значение чудовищного выражения эквивалентно a[b[3]] или a[3] или 14.
Пример 15.10.4-2. Прерывистое завершение вычисления ссылки на массив
Если вычисление выражения слева от квадратных скобок завершается внезапно, ни одна часть выражения в квадратных скобках не будет вычислена. Таким образом, программа:
class Test2 {
public static void main(String[] args) {
int index = 1;
try {
skedaddle()[index=2]++;
} catch (Exception e) {
System.out.println(e + ", index=" + index);
}
}
static int[] skedaddle() throws Exception {
throw new Exception("Ciao");
}
}
выводит:
java.lang.Exception: Ciao, index=1
потому что встроенное присваивание 2 переменной index никогда не выполняется.
Пример 15.10.4-3. null Ссылка на массив
Если выражение ссылки на массив производит null вместо ссылки на массив, то во время выполнения будет выброшено NullPointerException, но только после того, как все части выражения доступа к элементу массива будут вычислены и только если эти вычисления завершились нормально. Таким образом, программа:
class Test3 {
public static void main(String[] args) {
int index = 1;
try {
nada()[index=2]++;
} catch (Exception e) {
System.out.println(e + ", index=" + index);
}
}
static int[] nada() { return null; }
}
выводит:
java.lang.NullPointerException, index=2
потому что встроенное присваивание 2 переменной index происходит до проверки на наличие null ссылки на массив. В качестве связанного примера, программа:
class Test4 {
public static void main(String[] args) {
int[] a = null;
try {
int i = a[vamoose()];
System.out.println(i);
} catch (Exception e) {
System.out.println(e);
}
}
static int vamoose() throws Exception {
throw new Exception("Twenty-three skidoo!");
}
}
всегда выводит:
java.lang.Exception: Twenty-three skidoo!
NullPointerException никогда не происходит, потому что выражение индекса должно быть полностью вычислено перед выполнением любой другой части доступа к элементу массива, что включает в себя проверку, является ли значение выражения ссылки на массив null.
Выражение доступа к полю может получить доступ к полю объекта или массива, ссылка на который является значением либо выражения, либо специального ключевого слова super.
Значение выражения доступа к полю определяется по тем же правилам, что и для квалифицированных имен (§6.5.6.2), но ограничено тем, что выражение не может обозначать пакет, тип класса или тип интерфейса.
Также можно обратиться к полю текущего экземпляра или текущего класса, используя простое имя (§6.5.6.1).
Тип Primary должен быть типом ссылки T, в противном случае возникает ошибка компиляции.
Значение выражения доступа к полю определяется следующим образом:
-
Если идентификатор указывает на несколько доступных (§6.6) полей-членов в типе T, то доступ к полю является неоднозначным и возникает ошибка компиляции.
-
Если идентификатор не указывает на доступное поле-член в типе T, то доступ к полю не определен и возникает ошибка компиляции.
-
В противном случае, идентификатор указывает на единственное доступное поле-член в типе T, и тип выражения доступа к полю — это тип поля-члена после преобразования захвата (§5.1.10).
Во время выполнения результат выражения доступа к полю вычисляется следующим образом: (предполагая, что программа верна с точки зрения анализа определенного присваивания, то есть каждая пустая final переменная определённо присваивается перед доступом)
-
Если поле является
static:-
Вычисляется выражение Primary, и результат отбрасывается. Если вычисление выражения Primary завершается прерывисто, выражение доступа к полю также завершается прерывисто по той же причине.
-
Если поле является непустым
finalполем, то результатом является значение указанной переменной класса в классе или интерфейсе, который является типом выражения Primary. -
Если поле не является
final, или является пустымfinal, и доступ к полю происходит в инициализаторе переменной класса (§8.3.2) или статическом инициализаторе (§8.7), то результатом является переменная, а именно, указанная переменная класса в классе, который является типом выражения Primary.
-
-
Если поле не является
static:-
Вычисляется выражение Primary. Если вычисление выражения Primary завершается прерывисто, выражение доступа к полю также завершается прерывисто по той же причине.
-
Если значение Primary равно
null, то выбрасываетсяNullPointerException. -
Если поле является непустым
final, то результатом является значение указанного поля-члена в типе T, найденного в объекте, на который ссылается значение выражения Primary. -
Если поле не является
final, или является пустымfinal, и доступ к полю происходит в инициализаторе переменной экземпляра (§8.3.2), инициализаторе экземпляра (§8.6) или конструкторе (§8.8), то результатом является переменная, а именно, указанное поле-член в типе T, найденное в объекте, на который ссылается значение выражения Primary.
-
Обратите внимание, что для определения используемого поля используется только тип выражения Primary, а не класс фактического объекта, на который ссылаются во время выполнения.
Пример 15.11.1-1. Статическая привязка для доступа к полю
class S { int x = 0; }
class T extends S { int x = 1; }
class Test1 {
public static void main(String[] args) {
T t = new T();
System.out.println("t.x=" + t.x + when("t", t));
S s = new S();
System.out.println("s.x=" + s.x + when("s", s));
s = t;
System.out.println("s.x=" + s.x + when("s", s));
}
static String when(String name, Object t) {
return " when " + name + " holds a "
+ t.getClass() + " at run time.";
}
}
Эта программа выводит:
t.x=1 when t holds a class T at run time. s.x=0 when s holds a class S at run time. s.x=0 when s holds a class T at run time.
Последняя строка показывает, что поле, к которому осуществляется доступ, действительно не зависит от класса объекта, на который ссылаются во время выполнения; даже если s содержит ссылку на объект класса T, выражение s.x ссылается на поле x класса S, поскольку тип выражения s — S. Объекты класса T содержат два поля с именем x, одно для класса T и одно для его суперкласса S.
Это отсутствие динамического поиска для доступа к полям позволяет программам выполняться эффективно с простыми реализациями. Возможности поздней привязки и переопределения доступны, но только когда используются методы экземпляра. Рассмотрим тот же пример с использованием методов экземпляра для доступа к полям:
class S { int x = 0; int z() { return x; } }
class T extends S { int x = 1; int z() { return x; } }
class Test2 {
public static void main(String[] args) {
T t = new T();
System.out.println("t.z()=" + t.z() + when("t", t));
S s = new S();
System.out.println("s.z()=" + s.z() + when("s", s));
s = t;
System.out.println("s.z()=" + s.z() + when("s", s));
}
static String when(String name, Object t) {
return " when " + name + " holds a "
+ t.getClass() + " at run time.";
}
}
Теперь вывод:
t.z()=1 when t holds a class T at run time. s.z()=0 when s holds a class S at run time. s.z()=1 when s holds a class T at run time.
Последняя строка показывает, что метод, к которому осуществляется доступ, зависит от класса объекта, на который ссылаются во время выполнения; когда s содержит ссылку на объект класса T, выражение s.z() ссылается на метод z класса T, несмотря на то, что тип выражения s — S. Метод z класса T переопределяет метод z класса S.
Пример 15.11.1-2. Переменная получателя не имеет значения для доступа к статическому полю
Следующая программа демонстрирует, что нулевая ссылка может использоваться для доступа к переменной класса (static) без выброса исключения:
class Test3 {
static String mountain = "Chocorua";
static Test3 favorite(){
System.out.print("Mount ");
return null;
}
public static void main(String[] args) {
System.out.println(favorite().mountain);
}
}
Она компилируется, выполняется и выводит:
Mount Chocorua
Даже если результатом favorite() является null, исключение NullPointerException не выбрасывается. Вывод "Mount " демонстрирует, что выражение Primary действительно полностью вычисляется во время выполнения, несмотря на то, что только его тип, а не значение, используется для определения, к какому полю обратиться (поскольку поле mountain является static).
Форма super.Идентификатор относится к полю с именем Идентификатор текущего объекта, но с текущим объектом, рассматриваемым как экземпляр суперкласса текущего класса.
Форма T.super.Идентификатор относится к полю с именем Идентификатор лексически окружающего экземпляра, соответствующего T, но с этим экземпляром, рассматриваемым как экземпляр суперкласса T.
Формы, использующие ключевое слово super, могут использоваться в тех местах объявления класса, где разрешено ключевое слово this в качестве выражения (§15.8.3).
Ошибка компиляции, если выражение доступа к полю с ключевым словом super появляется в статическом контексте (§8.1.3).
Для выражения доступа к полю вида super.Идентификатор:
-
Ошибка компиляции, если непосредственное окружающее объявление класса или интерфейса выражения доступа к полю — это класс
Objectили интерфейс.
Для выражения доступа к полю вида T.super.Идентификатор:
-
Ошибка компиляции, если T — это класс
Objectили интерфейс. -
Пусть U — непосредственно окружающее объявление класса или интерфейса выражения доступа к полю. Ошибка компиляции, если U не является вложенным классом T или T само по себе.
Предположим, что выражение доступа к полю super.f появляется внутри класса C, и непосредственный суперкласс C — класс S. Если f в S доступен из класса C (§6.6), то super.f обрабатывается так, как если бы это было выражение this.f в теле класса S. В противном случае возникает ошибка компиляции.
Таким образом, super.f может получить доступ к полю f, которое доступно в классе S, даже если это поле скрыто объявлением поля f в классе C.
Предположим, что выражение доступа к полю T.super.f появляется внутри класса C, а непосредственный суперкласс класса, обозначенного T, — класс, полное имя которого S. Если f в S доступен из C, то T.super.f обрабатывается так, как если бы это было выражение this.f в теле класса S. В противном случае возникает ошибка компиляции.
Таким образом, T.super.f может получить доступ к полю f, которое доступно в классе S, даже если это поле скрыто объявлением поля f в классе T.
Пример 15.11.2-1. Выражение super
interface I { int x = 0; }
class T1 implements I { int x = 1; }
class T2 extends T1 { int x = 2; }
class T3 extends T2 {
int x = 3;
void test() {
System.out.println("x=\t\t" + x);
System.out.println("super.x=\t\t" + super.x);
System.out.println("((T2)this).x=\t" + ((T2)this).x);
System.out.println("((T1)this).x=\t" + ((T1)this).x);
System.out.println("((I)this).x=\t" + ((I)this).x);
}
}
class Test {
public static void main(String[] args) {
new T3().test();
}
}
Эта программа выводит:
x= 3 super.x= 2 ((T2)this).x= 2 ((T1)this).x= 1 ((I)this).x= 0
Внутри класса T3, выражение super.x имеет тот же эффект, что и ((T2)this).x, когда x имеет доступ к пакету. Обратите внимание, что super.x не задается в терминах приведения типов из-за трудностей доступа к protected членам суперкласса.
Выражение вызова метода используется для вызова метода класса или экземпляра.
( [СписокАргументов] ) ИмяТипа
. [АргументыТипов] Идентификатор ( [СписокАргументов] ) ИмяВыражения
. [АргументыТипов] Идентификатор ( [СписокАргументов] ) Первичное
. [АргументыТипов] Идентификатор ( [СписокАргументов] ) super . [АргументыТипов] Идентификатор ( [СписокАргументов] ) ИмяТипа
. super . [АргументыТипов] Идентификатор ( [СписокАргументов] ) Разрешение имени метода во время компиляции более сложно, чем разрешение имени поля, из-за возможности перегрузки методов. Вызов метода во время выполнения также сложнее, чем доступ к полю, из-за возможности переопределения методов экземпляра.
Определение метода, который будет вызван выражением вызова метода, включает несколько шагов. Следующие три раздела описывают обработку выражения вызова метода во время компиляции. Определение типа выражения вызова метода указано в §15.12.3.
Типы исключений, которые может генерировать выражение вызова метода, указаны в §11.2.1.
Ошибка компиляции, если имя слева от правого ".", который встречается до ( в MethodInvocation, нельзя классифицировать как TypeName или ExpressionName (§6.5.2).
Если TypeArguments присутствует слева от Identifier, то ошибка компиляции, если любой из аргументов типа является шаблонами (§4.5.1).
Выражение вызова метода является поливыражением, если все из перечисленного ниже верно:
-
Вызов появляется в контексте присваивания или вызова (§5.2, §5.3).
-
Если вызов квалифицированный (т.е. любой вид MethodInvocation кроме первого), то вызов опускает TypeArguments слева от Identifier.
-
Метод, который будет вызван, как определяется в следующих подразделах, является обобщенным (§8.4.4) и имеет тип возврата, который упоминает по крайней мере один параметр типа метода.
В противном случае выражение вызова метода является автономным выражением.
Первый шаг при обработке вызова метода во время компиляции заключается в определении имени вызываемого метода и типа, для которого следует искать определения методов с этим именем.
Имя метода задается ИменемМетода или Идентификатором, которые непосредственно предшествуют левой круглой скобке ВызоваМетода.
Для типа, для которого необходимо искать, существуют шесть случаев, в зависимости от формы, предшествующей левой круглой скобке ВызоваМетода:
-
Если форма — ИмяМетода, то есть просто Идентификатор, то:
Если Идентификатор присутствует в области видимости объявления метода с таким именем (§6.3, §6.4.1), то:
-
Если существует содержащий класс или интерфейс, членом которого является этот метод, пусть E будет самым внутренним таким классом или интерфейсом. Тип для поиска — это тип E
.this(§15.8.4).Эта политика поиска называется "правилом гребня". Она фактически ищет методы в иерархии суперклассов вложенного класса перед поиском методов в иерархии суперклассов содержащего класса. Пример см. в §6.5.7.1.
-
В противном случае объявление метода может находиться в области видимости благодаря одному или нескольким объявлениям single-static-import или static-import-on-demand. Тип для поиска отсутствует, так как метод для вызова определяется позднее (§15.12.2.1).
-
-
Если форма — ИмяТипа
.[АргументыТипов] Идентификатор, то тип для поиска — это (возможно, примитивный) тип, обозначаемый ИмяТипа. -
Если форма — ИмяВыражения
.[АргументыТипов] Идентификатор, то тип для поиска — это тип T объявленной переменной, обозначаемой ИмяВыражения, если T — тип класса или интерфейса, или верхняя граница T, если T — переменная типа. -
Если форма — Первичное
.[АргументыТипов] Идентификатор, то пусть T — тип выражения Первичное. Тип для поиска — T, если T — тип класса или интерфейса, или верхняя граница T, если T — переменная типа.Ошибка времени компиляции, если T не является ссылочным типом.
-
Если форма —
super.[АргументыТипов] Идентификатор, то тип для поиска — прямой тип суперкласса класса, в объявлении которого содержится вызов метода.Пусть E — класс или интерфейс, непосредственно окружающий вызов метода. Ошибка времени компиляции, если E — класс
Objectили интерфейс. -
Если форма — ИмяТипа
.super.[АргументыТипов] Идентификатор, то:-
Ошибка времени компиляции, если ИмяТипа обозначает ни класс, ни интерфейс.
-
Если ИмяТипа обозначает класс C, то тип для поиска — прямой тип суперкласса C.
Ошибка времени компиляции, если C не является лексически содержащим классом объявления метода вызова, или если C — класс
Object.Пусть E — класс или интерфейс, непосредственно окружающий вызов метода. Ошибка времени компиляции, если E — класс
Object. -
В противном случае ИмяТипа обозначает интерфейс I.
Пусть E — класс или интерфейс, непосредственно окружающий вызов метода. Ошибка времени компиляции, если I не является прямым суперинтерфейсом E, или если существует другой прямой суперкласс или прямой суперинтерфейс E, J, такой что J является подклассом или подинтерфейсом I.
Тип для поиска — это тип I, который является прямым типом суперинтерфейса E.
-
Синтаксис ИмяТипа . super перегружен: традиционно ИмяТипа относится к лексически содержащему классу объявления, а целевой является суперкласс этого класса, как если бы вызов был неквалифицированным super в лексически содержащем классе объявления.
class Superclass {
void foo() { System.out.println("Hi"); }
}
class Subclass1 extends Superclass {
void foo() { throw new UnsupportedOperationException(); }
Runnable tweak = new Runnable() {
void run() {
Subclass1.super.foo(); // Gets the 'println' behavior
}
};
}
Для поддержки вызова методов по умолчанию в суперинтерфейсах ИмяТипа также может относиться к прямому суперинтерфейсу текущего класса или интерфейса, а целевой является этот суперинтерфейс.
interface Superinterface {
default void foo() { System.out.println("Hi"); }
}
class Subclass2 implements Superinterface {
void foo() { throw new UnsupportedOperationException(); }
void tweak() {
Superinterface.super.foo(); // Gets the 'println' behavior
}
}
Никакой синтаксис не поддерживает комбинацию этих форм, то есть вызов метода суперинтерфейса лексически содержащего класса объявления, как если бы вызов был в форме ИмяИнтерфейса . super в лексически содержащем классе объявления.
class Subclass3 implements Superinterface {
void foo() { throw new UnsupportedOperationException(); }
Runnable tweak = new Runnable() {
void run() {
Subclass3.Superinterface.super.foo(); // Illegal
}
};
}
Обходным путём является введение метода private в лексически содержащем классе объявления, который выполняет вызов интерфейса super.
На втором шаге выполняется поиск методов-членов в типе, определенном на предыдущем шаге. На этом шаге используются имя метода и выражения аргументов для поиска методов, которые являются доступными и применимыми, то есть объявления, которые могут быть корректно вызваны с заданными аргументами.
Может быть найдено более одного такого метода, в этом случае выбирается наиболее специфичный. Дескриптор (сигнатура плюс возвращаемый тип) наиболее специфичного метода используется во время выполнения для выполнения диспетчеризации метода.
Некоторые выражения аргументов, содержащие неявные лямбда-выражения (§15.27.1) или неточные ссылки на методы (§15.13.1), игнорируются тестами применимости, так как их значение невозможно определить до выбора целевого типа вызова. С другой стороны, только выражения аргументов, а не целевой тип вызова, влияют на тесты применимости, даже если выражение вызова метода является поливыражением.
Процесс определения применимости начинается с определения потенциально применимых методов (§15.12.2.1). Затем, для обеспечения совместимости с языком программирования Java до Java SE 5.0, процесс продолжается в трех фазах:
-
На первой фазе выполняется разрешение перегрузки без разрешения преобразования boxing или unboxing, или использования вызова метода с переменным числом аргументов. Если применённый метод не найден на этой фазе, обработка переходит ко второй фазе.
Это гарантирует, что любые вызовы, которые были допустимы в языке программирования Java до Java SE 5.0, не считаются неоднозначными в результате введения методов с переменным числом аргументов, неявного boxing и/или unboxing. Однако объявление метода с переменным числом аргументов (§8.4.1) может изменить метод, выбранный для данного выражения вызова метода, потому что метод с переменным числом аргументов обрабатывается как метод с фиксированным числом аргументов на первой фазе. Например, объявление
m(Object...)в классе, который уже объявляетm(Object), приводит к тому, чтоm(Object)больше не выбирается для некоторых выражений вызова (например,m(null)), так какm(Object[])более специфичен. -
На второй фазе выполняется разрешение перегрузки, разрешая boxing и unboxing, но всё ещё исключая использование вызова метода с переменным числом аргументов. Если применимый метод не найден на этой фазе, обработка переходит к третьей фазе.
Это гарантирует, что метод никогда не выбирается с помощью вызова метода с переменным числом аргументов, если он применим с помощью вызова метода с фиксированным числом аргументов.
-
На третьей фазе разрешается объединение перегрузки с методами переменного числа аргументов, boxing и unboxing.
Метод считается применимым, если он применим по одному из строгих вызовов (первая фаза, §15.12.2.2), свободных вызовов (вторая фаза, §15.12.2.3) или вызовов с переменным числом аргументов (третья фаза, §15.12.2.4). Определение применимости метода, в случае обобщенных методов (§8.4.4), потребует анализа аргументов типа. Аргументы типа могут передаваться явно или неявно; если они передаются неявно, то границы аргументов типа должны быть выведены из выражений аргументов (§18 (Выведение типов)).
Если в ходе одного из трёх этапов проверки применимости были идентифицированы несколько применимых методов, то выбирается наиболее специфичный, как указано в §15.12.2.5.
Для проверки применимости типы аргументов вызова, как правило, не могут быть входными данными для анализа. Это связано с тем, что:
-
Аргументы вызова метода могут быть поливыражениями.
-
Поливыражения не могут быть типизированы без целевого типа.
-
Разрешение перегрузки должно быть завершено до того, как будут известны целевые типы аргументов.
Вместо этого входными данными для проверки применимости является список самих аргументов. Аргументы могут быть проверены на совместимость с потенциальными целевыми типами, даже если окончательные типы аргументов неизвестны.
Обратите внимание, что разрешение перегрузки не зависит от целевого типа. Это по двум причинам:
-
Во-первых, это делает модель пользователя более доступной и менее подверженной ошибкам. Значение имени метода (т. е. объявление, соответствующее имени) слишком фундаментально для понимания программы, чтобы зависеть от тонких контекстных подсказок. (В отличие от этого, другие поливыражения могут иметь различное поведение в зависимости от целевого типа; но изменения в поведении всегда ограничены и в сущности эквивалентны, в то время как не может быть таких гарантий в отношении поведения произвольного набора методов, имеющих одинаковое имя и количество аргументов.)
-
Во-вторых, это позволяет другим свойствам — таким как то, является ли метод поливыражением (§15.12) или как классифицировать условное выражение (§15.25) — зависеть от значения имени метода даже до того, как станет известен целевой тип.
Пример 15.12.2-1. Применимость метода
class Doubler {
static int two() { return two(1); }
private static int two(int i) { return 2*i; }
}
class Test extends Doubler {
static long two(long j) { return j+j; }
public static void main(String[] args) {
System.out.println(two(3));
System.out.println(Doubler.two(3)); // compile-time error
}
}
Для вызова метода two(1) в классе Doubler существует два доступных метода с именем two, но только второй применим, и поэтому он вызывается во время выполнения.
Для вызова метода two(3) в классе Test существует два применимых метода, но только один в классе Test является доступным, и поэтому он вызывается во время выполнения (аргумент 3 преобразуется в тип long).
Для вызова метода Doubler.two(3) в классе Doubler, а не в классе Test, ищутся методы с именем two; единственный применимый метод недоступен, поэтому этот вызов метода приводит к ошибке компиляции.
Другой пример:
class ColoredPoint {
int x, y;
byte color;
void setColor(byte color) { this.color = color; }
}
class Test {
public static void main(String[] args) {
ColoredPoint cp = new ColoredPoint();
byte color = 37;
cp.setColor(color);
cp.setColor(37); // compile-time error
}
}
Здесь ошибка компиляции возникает для второго вызова setColor, потому что во время компиляции не удаётся найти применимый метод. Тип литерала 37 — int, а int не может быть преобразован в byte с помощью преобразования вызова. Преобразование присваивания, используемое при инициализации переменной color, выполняет неявное преобразование константы из типа int в byte, что разрешено, потому что значение 37 достаточно мало, чтобы быть представленным в типе byte; но такое преобразование не разрешено для преобразования вызова.
Однако, если бы метод setColor был объявлен с приёмом int вместо byte, оба вызова метода были бы правильными; первый вызов был бы разрешён, потому что преобразование вызова разрешает расширяющее преобразование от byte к int. Однако в теле setColor тогда потребовался бы сужающий оператор приведения.
void setColor(int color) { this.color = (byte)color; }
Вот пример неоднозначности перегрузки. Рассмотрим программу:
class Point { int x, y; }
class ColoredPoint extends Point { int color; }
class Test {
static void test(ColoredPoint p, Point q) {
System.out.println("(ColoredPoint, Point)");
}
static void test(Point p, ColoredPoint q) {
System.out.println("(Point, ColoredPoint)");
}
public static void main(String[] args) {
ColoredPoint cp = new ColoredPoint();
test(cp, cp); // compile-time error
}
}
Этот пример приводит к ошибке во время компиляции. Проблема в том, что существует два объявления test, которые применимы и доступны, и ни одно не является более специфичным, чем другое. Поэтому вызов метода является неоднозначным.
Если добавить третье определение test:
static void test(ColoredPoint p, ColoredPoint q) {
System.out.println("(ColoredPoint, ColoredPoint)");
}
то оно будет более специфичным, чем другие два, и вызов метода больше не будет неоднозначным.
Пример 15.12.2-2. Тип результата не рассматривается при выборе метода
class Point { int x, y; }
class ColoredPoint extends Point { int color; }
class Test {
static int test(ColoredPoint p) {
return p.color;
}
static String test(Point p) {
return "Point";
}
public static void main(String[] args) {
ColoredPoint cp = new ColoredPoint();
String s = test(cp); // compile-time error
}
}
Здесь наиболее специфичное объявление метода test — то, которое принимает параметр типа ColoredPoint. Поскольку возвращаемый тип метода — int, возникает ошибка компиляции, поскольку int не может быть преобразован в String с помощью преобразования присваивания. Этот пример показывает, что возвращаемые типы методов не участвуют в разрешении перегруженных методов, так что второй метод test, возвращающий String, не выбирается, даже несмотря на то, что у него есть возвращаемый тип, который позволил бы программе скомпилироваться без ошибок.
Пример 15.12.2-3. Выбор самого специфичного метода
Самый специфичный метод выбирается во время компиляции; его дескриптор определяет, какой метод фактически выполняется во время выполнения. Если к классу добавляется новый метод, то исходный код, скомпилированный со старым определением класса, может не использовать новый метод, даже если повторная компиляция приведет к выбору этого метода.
Таким образом, например, рассмотрим два модуля компиляции, один для класса Point:
package points;
public class Point {
public int x, y;
public Point(int x, int y) { this.x = x; this.y = y; }
public String toString() { return toString(""); }
public String toString(String s) {
return "(" + x + "," + y + s + ")";
}
}
и один для класса ColoredPoint:
package points;
public class ColoredPoint extends Point {
public static final int
RED = 0, GREEN = 1, BLUE = 2;
public static String[] COLORS =
{ "red", "green", "blue" };
public byte color;
public ColoredPoint(int x, int y, int color) {
super(x, y);
this.color = (byte)color;
}
/** Copy all relevant fields of the argument into
this ColoredPoint object. */
public void adopt(Point p) { x = p.x; y = p.y; }
public String toString() {
String s = "," + COLORS[color];
return super.toString(s);
}
}
Теперь рассмотрим третий модуль компиляции, который использует ColoredPoint:
import points.*;
class Test {
public static void main(String[] args) {
ColoredPoint cp =
new ColoredPoint(6, 6, ColoredPoint.RED);
ColoredPoint cp2 =
new ColoredPoint(3, 3, ColoredPoint.GREEN);
cp.adopt(cp2);
System.out.println("cp: " + cp);
}
}
Вывод:
cp: (3,3,red)
Программист, который кодировал класс Test, ожидал увидеть слово green, потому что фактический аргумент, a ColoredPoint, имеет поле color, и color, казалось бы, является «релевантным полем». (Конечно, документация для пакета points должна была быть гораздо более точной!)
Обратите внимание, что самый специфичный метод (и, на самом деле, единственный применимый метод) для вызова метода adopt имеет сигнатуру, указывающую на метод с одним параметром, а параметр имеет тип Point. Эта сигнатура становится частью двоичного представления класса Test, созданного компилятором Java, и используется вызовом метода во время выполнения.
Предположим, что программист сообщил об этой ошибке программного обеспечения, и системный администратор пакета points, после долгих размышлений, решил исправить её, добавив метод в класс ColoredPoint:
public void adopt(ColoredPoint p) {
adopt((Point)p);
color = p.color;
}
Если затем программист запустит старый двоичный файл для Test с новым двоичным файлом для ColoredPoint, вывод будет таким же:
cp: (3,3,red)
потому что старый двоичный файл для Test по-прежнему имеет дескриптор «один параметр, тип которого Point; void», связанный с вызовом метода cp.adopt(cp2). Если исходный код для Test перекомпилировать, компилятор Java обнаружит, что теперь есть два применимых adopt метода и что сигнатура более специфичного метода — «один параметр, тип которого ColoredPoint; void»; запуск программы затем даст ожидаемый результат:
cp: (3,3,green)
С предусмотрительностью относительно таких проблем, системный администратор пакета points мог бы исправить класс ColoredPoint, чтобы он работал как с новыми, так и со старыми компиляциями, добавив защитный код в старый метод adopt ради старого кода, который всё ещё вызывает его с аргументами ColoredPoint:
public void adopt(Point p) {
if (p instanceof ColoredPoint)
color = ((ColoredPoint)p).color;
x = p.x; y = p.y;
}
В идеале, исходный код необходимо перекомпилировать всякий раз, когда изменяется код, от которого он зависит. Однако в среде, где разные классы поддерживаются различными организациями, это не всегда возможно. Защитное программирование с тщательным вниманием к проблемам эволюции классов может сделать обновлённый код гораздо более надёжным. Подробное обсуждение двоичной совместимости и эволюции типов см. в §13 (Двоичная совместимость).
Тип, определённый на этапе компиляции 1 (§15.12.1), ищется для всех методов-членов, которые потенциально могут применяться к этому вызову метода; члены, унаследованные от суперклассов и суперинтерфейсов, также включаются в этот поиск.
Кроме того, если форма выражения вызова метода — ИмяМетода (то есть, один Идентификатор) — то поиск потенциально подходящих методов также проверяет все методы-члены, которые импортированы объявлениями single-static-import и static-import-on-demand в единице компиляции, где происходит вызов метода (§7.5.3, §7.5.4), и которые не скрываются в точке, где появляется вызов метода.
Метод-член является потенциально подходящим для вызова метода, если и только если выполняются все следующие условия:
-
Имя члена идентично имени метода в вызове метода.
-
Член доступен (§6.6) для класса или интерфейса, в котором появляется вызов метода.
Доступность метода-члена в вызове метода зависит от модификатора доступа (
public,protected, нет модификатора (доступ по пакетам) илиprivate) в объявлении члена, и от наследования члена классом или интерфейсом, определяемым этапом компиляции 1, и от того, где появляется вызов метода. -
Если член — метод с фиксированной арностью n, арность вызова метода равна n, и для всех i (1 ≤ i ≤ n) i'ый аргумент вызова метода потенциально совместим с типом i'ого параметра метода, как определено ниже.
-
Если член — метод с переменной арностью n, то для всех i (1 ≤ i ≤ n-1), i'ый аргумент вызова метода потенциально совместим с типом i'ого параметра метода; и, где nый параметр метода имеет тип T
[], выполняется одно из следующих условий:-
Арность вызова метода равна n-1.
-
Арность вызова метода равна n, и n'ый аргумент вызова метода потенциально совместим с типом T или T
[]. -
Арность вызова метода равна m, где m > n, и для всех i (n ≤ i ≤ m), i'ый аргумент вызова метода потенциально совместим с типом T.
-
-
Если в вызове метода есть явные типы аргументов, и член — обобщённый метод, то количество типов аргументов равно количеству типов параметров метода.
Это условие подразумевает, что необобщённый метод может быть потенциально подходящим для вызова, который предоставляет явные типы аргументов. Действительно, он может оказаться подходящим. В таком случае типы аргументов просто будут проигнорированы.
Это правило вытекает из вопросов совместимости и принципов заменяемости. Поскольку интерфейсы или суперклассы могут быть обобщены независимо от своих подтипов, мы можем переопределить обобщённый метод необобщённым. Однако переопределяемый (не обобщённый) метод должен быть применим к вызовам обобщённого метода, включая вызовы, которые явно передают типы аргументов. В противном случае подтип не будет заменяем на свой обобщённый супертип.
Если поиск не приводит к по крайней мере одному потенциально подходящему методу, возникает ошибка компиляции.
Выражение потенциально совместимо с целевым типом в соответствии со следующими правилами:
-
Выражение лямбда-функции (§15.27) потенциально совместимо с функциональным интерфейсом типа T (§9.8), если выполняются все следующие условия:
-
Арность типа функции T (§9.9) совпадает с арностью лямбда-выражения.
-
Если тип функции T имеет
voidвозврат, то тело лямбда-выражения является либо выражением-инструкцией (§14.8), либо блоком, совместимым с void (§15.27.2). -
Если тип функции T имеет (не-
void) тип возврата, то тело лямбда-выражения является либо выражением, либо блоком, совместимым со значением (§15.27.2).
-
-
Выражение ссылки на метод (§15.13) потенциально совместимо с функциональным интерфейсом типа T, если, где арность типа функции T равна n, существует по крайней мере один потенциально подходящий метод, когда выражение ссылки на метод направлено на тип функции с арностью n (§15.13.1), и выполняется одно из следующих условий:
-
Выражение ссылки на метод имеет вид ТипСсылок
::[ТипАргументов] Идентификатор и по крайней мере один потенциально подходящий метод либо (i)staticи поддерживает арность n, или (ii) неstaticи поддерживает арность n-1. -
Выражение ссылки на метод имеет другую форму, и по крайней мере один потенциально подходящий метод не
static.
-
-
Лямбда-выражение или выражение ссылки на метод потенциально совместимо с переменной типа, если переменная типа является параметром типа кандидатного метода.
-
Выражение в скобках (§15.8.5) потенциально совместимо с типом, если содержащееся в нём выражение потенциально совместимо с этим типом.
-
Условное выражение (§15.25) потенциально совместимо с типом, если каждое из его второго и третьего операндов потенциально совместимо с этим типом.
-
Выражение switch (§15.28) потенциально совместимо с типом, если все его выражения результата потенциально совместимы с этим типом.
-
Выражение создания экземпляра класса, выражение вызова метода или выражение автономной формы (§15.2) потенциально совместимо с любым типом.
Определение потенциальной применимости выходит за рамки простого проверки арности, чтобы также учитывать наличие и «форму» целевых типов функциональных интерфейсов. В некоторых случаях, связанных с выводом типа, лямбда-выражение, появляющееся в качестве аргумента вызова метода, не может быть корректно типизировано до тех пор, пока не будет завершена разрешение перегрузки. Эти правила позволяют учитывать форму лямбда-выражения, отбрасывая очевидно неправильные целевые типы, которые в противном случае могли бы вызвать ошибки неоднозначности.
Выражение аргумента считается применимым к применимости для потенциально применимого метода m, если оно не имеет одного из следующих форм:
-
Явно типизированное лямбда-выражение (§15.27.1).
-
Неточное выражение ссылки на метод (§15.13.1).
-
Если
mявляется обобщенным методом, а вызов метода не предоставляет явные аргументы типа, явно типизированное лямбда-выражение или точное выражение ссылки на метод, для которого соответствующий целевой тип (полученный из сигнатурыm) является параметром типаm. -
Явно типизированное лямбда-выражение, тело которого является выражением, не относящимся к применимости.
-
Явно типизированное лямбда-выражение, тело которого является блоком, где по крайней мере одно выражение результата не относится к применимости.
-
Выражение в скобках (§15.8.5), содержащее выражение, не относящееся к применимости.
-
Условное выражение (§15.25), второй или третий операнд которого не относится к применимости.
Пусть m — потенциально применимый метод (§15.12.2.1) с арностью n и формальными типами параметров F1, ..., Fn, и пусть e1, ..., en — фактические выражения аргументов вызова метода. Тогда:
-
Если
m— обобщенный метод, а вызов метода не предоставляет явные аргументы типа, то применимость метода определяется, как указано в §18.5.1. -
Если
m— обобщенный метод, а вызов метода предоставляет явные аргументы типа, то пусть R1, ..., Rp (p ≥ 1) — параметры типаm, пусть Bl — объявленная граница Rl (1 ≤ l ≤ p), и пусть U1, ..., Up — явные аргументы типа, заданные в вызове метода. Тогдаmявляется применимым по строгому вызову, если оба следующих условия истинны:-
Для 1 ≤ i ≤ n, если
eiотносится к применимости, тоeiсовместим в контексте строгого вызова с Fi[R1:=U1, ..., Rp:=Up](§5.3). -
Для 1 ≤ l ≤ p, Ul
<:Bl[R1:=U1, ..., Rp:=Up].
-
-
Если
mне является обобщенным методом, тоmявляется применимым по строгому вызову, если для 1 ≤ i ≤ n, либоeiсовместим в контексте строгого вызова с Fi (§5.3), либоeiне относится к применимости.
Если метод, применимый по строгому вызову, не найден, поиск применимых методов продолжается со второй фазы (§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не относится к применимости.
Если метод, применимый по свободному вызову, не найден, поиск применимых методов продолжается с третьей фазы (§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 формальных параметров, и тип n-го параметра типа вызова (§15.12.2.6) m обязательно имеет тип T[] для некоторого T. Пусть k — количество фактических выражений аргументов в вызове метода: если k ≠ n или если k = n и тип k-го выражения аргумента не совместим по присваиванию с T[], то m был признан применимым путём вызова переменной арности (§15.12.2.4). В этом случае список аргументов (e1, ..., en-1, en, ..., ek) оценивается так, как если бы он был написан как (e1, ..., en-1, new |T[]| { en, ..., ek }), где |T[]| обозначает стирание (§4.6) T[].
Предыдущий абзац составлен для обработки взаимодействия параметризованных типов и типов массивов, происходящих в виртуальной машине Java с потёртыми дженериками. Иными словами, если тип элемента T параметра переменной арности массива не может быть уточнён, например, List<String>, то следует соблюдать особую осторожность при выражении создания массива (§15.10), поскольку тип элемента создаваемого массива должен быть уточнён. Стиранием типа массива конечного выражения в списке аргументов гарантируется получение уточняемого типа элемента. Затем, так как выражение создания массива появляется в контексте вызова (§5.3), возможно неявное преобразование от типа массива с уточняемым типом элемента к типу массива с неуточняемым типом элемента, в частности, к типу параметра переменной арности. Компилятор Java должен выдать предупреждение о неявном преобразовании на этапе компиляции. Справочная реализация компилятора Java от Oracle идентифицирует это предупреждение о неявном преобразовании как более информативное создание неявных массивов дженериков.
Выражения аргументов (возможно, переписанные, как описано выше) теперь оцениваются для получения значений аргументов. Каждое значение аргумента соответствует ровно одному из n формальных параметров метода.
Выражения аргументов, если таковые имеются, оцениваются в порядке следования слева направо. Если вычисление любого выражения аргумента завершается внезапно, то никакая часть любого выражения аргумента справа от него, кажется, не была вычислена, и вызов метода завершается внезапно по той же причине. Результат оценки j-го выражения аргумента — j-е значение аргумента, при 1 ≤ j ≤ n. Вычисление затем продолжается с использованием значений аргументов, как описано ниже.
В этом разделе:
Реализация языка программирования Java должна гарантировать, что класс или интерфейс Q доступен в ходе привязки:
-
Если Q находится в том же пакете, что и D, то Q доступен.
-
Если Q находится в другом пакете, чем D, и их пакеты находятся в одном модуле, и Q является
publicилиprotected, то Q доступен. -
Если Q находится в другом пакете, чем D, и их пакеты находятся в разных модулях, и модуль Q экспортирует пакет Q в модуль D, и Q является
publicилиprotected, то Q доступен.
Если Q является protected, он обязательно является вложенным классом или интерфейсом, поэтому на этапе компиляции его доступность зависит от доступности классов и интерфейсов, окружающих его объявление. Однако во время привязки его доступность не зависит от доступности классов и интерфейсов, окружающих его объявление. Кроме того, во время привязки protected Q так же доступен, как и public Q. Эти расхождения в контроле доступа на этапе компиляции (§6.6) и контроле доступа во время выполнения обусловлены ограничениями виртуальной машины Java.
Реализация также должна гарантировать во время привязки, что метод m всё ещё может быть найден в Q или в суперклассе или суперинтерфейсе Q. Если m не может быть найден, то происходит NoSuchMethodError (который является подклассом IncompatibleClassChangeError). Если m может быть найден, то пусть C — класс или интерфейс, объявляющий m. Реализация должна гарантировать во время привязки, что объявление m в C доступно для D:
-
Если
mявляетсяpublic, тоmдоступен. -
Если
mявляетсяprotected, тоmдоступен, если (i) либо D находится в том же пакете, что и C, либо D является подклассом C или сам C; и (ii) еслиm— это метод экземпляра классаprotected, то Q должен быть подклассом D или сам D.Это единственное место, где Q участвует в проверках на
m, потому что метод экземпляра классаprotectedможет вызываться только через квалифицирующий класс или интерфейс, соответствующий типу вызывающего. -
Если у
mесть доступ к пакету, тоmдоступен, если D находится в том же пакете, что и C. -
Если
mявляетсяprivate, тоmдоступен, если D равно C, или D содержит C, или C содержит D, или C и D оба содержатся в третьем классе или интерфейсе.
Если Q или m недоступны, то происходит IllegalAccessError (§12.3).
Если режим вызова является interface, то реализация должна проверить, что целевой класс ссылки всё ещё реализует указанный интерфейс. Если целевой класс ссылки больше не реализует интерфейс, то происходит IncompatibleClassChangeError.
Как и в предыдущем разделе (§15.12.4.3):
-
Пусть Q — квалифицируемый класс или интерфейс вызова метода (§13.1).
-
Пусть
m— метод, найденный в Q или в его суперклассе или суперинтерфейсе. (Обратите внимание, чтоmбыл просто именем метода в предыдущем разделе; здесь это фактическое объявление.) -
Пусть C — класс или интерфейс, объявляющий
m.
Стратегия поиска вызываемого метода зависит от режима вызова:
-
Если режим вызова —
static, то ссылка на целевой объект не требуется, а переопределение запрещено. Вызываемым методом будет методmкласса или интерфейса C. -
В противном случае вызывается метод экземпляра, и есть ссылка на целевой объект. Если ссылка на целевой объект —
null, в этот момент возникает ошибкаNullPointerException. В противном случае ссылка на целевой объект ссылается на целевой объект и будет использоваться как значение ключевого словаthisв вызываемом методе. Затем рассматриваются три других возможных режима вызова:-
Если режим вызова —
super, переопределение запрещено. Вызываемым методом будет методmкласса или интерфейса C. Еслиmравноabstract, генерируется ошибкаAbstractMethodError. -
В противном случае, если режим вызова —
virtual, и Q иmвместе указывают на полиморфный метод подписи (§15.12.3), то целевой объект является экземпляромjava.lang.invoke.MethodHandleилиjava.lang.invoke.VarHandle. Целевой объект инкапсулирует состояние, которое сопоставляется с информацией, связанной с вызовом метода во время компиляции. Подробности этого сопоставления приведены в спецификации виртуальной машины Java, издание Java SE 24, и API платформы Java SE. Если сопоставление успешно, то либо метод, на который ссылается экземплярjava.lang.invoke.MethodHandle, вызывается непосредственно и сразу, либо переменная, представленная экземпляромjava.lang.invoke.VarHandle, обращается непосредственно и сразу, и в обоих случаях процедура в §15.12.4.5 не выполняется. Если сопоставление не выполнено, генерируется ошибкаjava.lang.invoke.WrongMethodTypeException. -
В противном случае, режим вызова —
interfaceилиvirtual.Если метод
mкласса или интерфейса C являетсяprivate, то это вызываемый метод.В противном случае, возможно переопределение. Для поиска вызываемого метода используется динамический поиск метода, описанный ниже. Процедура поиска начинается с класса R, фактического класса целевого объекта.
Обратите внимание, что для режима вызова
interface, R обязательно реализует Q; для режима вызоваvirtual, R обязательно равен Q или является подклассом Q. Если целевой объект — массив, то R — "класс", представляющий тип массива.
-
Процедура динамического поиска метода следующая. Пусть S — класс для поиска, начиная с R. Тогда:
-
Если класс S содержит объявление метода, который переопределяет метод
mкласса или интерфейса C из R (§8.4.8.1), то этот переопределенный метод является вызываемым методом, и процедура завершается. -
В противном случае, если у S есть суперкласс, шаги 1 и 2 этой процедуры поиска выполняются рекурсивно, используя непосредственный суперкласс S вместо S; вызываемый метод, если таковой существует, является результатом рекурсивного вызова этой процедуры поиска.
-
Если ни один метод не найден на предыдущих двух шагах, то проверяются суперинтерфейсы S для поиска подходящего метода.
Рассматривается набор кандидатов с следующими свойствами: (i) каждый метод объявлен в (прямом или косвенном) суперинтерфейсе R; (ii) каждый метод имеет имя и дескриптор, требуемый вызовом метода; (iii) каждый метод не является
staticи не являетсяprivate; (iv) для каждого метода, где объявляющий интерфейс — I, не существует другого метода, удовлетворяющего (i)-(iii), который объявлен в подинтерфейсе I.Если этот набор содержит метод по умолчанию, то один из таких методов — вызываемый метод. В противном случае вызываемый метод выбирается из набора как метод по умолчанию.
Динамический поиск метода может привести к следующим ошибкам:
-
Если вызываемый метод —
abstract, генерируется ошибкаAbstractMethodError. -
Если вызываемый метод —
default, и в наборе кандидатов на шаге 3 выше появляется более одного метода по умолчанию, генерируется ошибкаIncompatibleClassChangeError. -
Если режим вызова —
interface, а вызываемый метод не является ниpublic, ниprivate, генерируется ошибкаIllegalAccessError.
Вышеописанная процедура (если она завершается без ошибок) найдет доступный не-abstract метод для вызова, при условии, что все классы и интерфейсы в программе были последовательно скомпилированы. Однако, если это не так, могут возникнуть различные ошибки, как указано выше; дополнительные сведения о поведении виртуальной машины Java в этих обстоятельствах приведены в спецификации виртуальной машины Java, издание Java SE 24.
Процесс динамического поиска, хотя и описан здесь явно, часто будет реализован неявно, например, как побочный эффект построения и использования таблиц диспетчеризации методов по классам или построения других структур по классам, используемых для эффективной диспетчеризации.
Пример 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, то потенциально применимые методы — это набор условных методов, соответствующих конструкторам ТипКласса.Если ТипКласса является сырым типом, но не является не-
staticчленом типа сырого типа, кандидаты на методы-члены — это те, что указаны в §15.9.3 для выражения создания экземпляра класса, который использует<>для устранения аргументов типа для класса. В противном случае кандидаты на методы-члены — это конструкторы ТипКласса, рассматриваемые так, как если бы они были методами с типом возврата ТипКласса.Среди этих кандидатов потенциально применимые методы — это условные методы, которые были бы потенциально применимыми для вызова метода, имеющего арность n, имеющего аргументы типа АргументыТипов и появляющегося в том же классе, что и выражение ссылки на метод.
-
Если выражение ссылки на метод имеет вид ТипМассива
::new, рассматривается один условный метод. Метод имеет один параметр типаint, возвращает ТипМассива и не имеетthrowsуказателей. Если n = 1, это единственный потенциально применимый метод; в противном случае потенциально применимых методов нет. -
Для всех других форм потенциально применимые методы — это методы-члены типа для поиска, которые потенциально применим для вызова метода, который называет Идентификатор, имеет арность n, имеет аргумент типа АргументыТипов и появляется в том же классе, что и выражение ссылки на метод.
-
-
Наконец, если нет потенциально применимых методов, то объявление на этапе компиляции отсутствует.
В противном случае, для заданного целевого типа функции с параметрами P1, ..., Pn и набором потенциально применимых методов объявление на этапе компиляции выбирается следующим образом:
-
Если выражение ссылки на метод имеет вид ТипСсылки
::[АргументыТипа] Идентификатор, то выполняются два поиска наиболее специфичного применимого метода. Каждый поиск выполняется, как указано в §15.12.2.2 до §15.12.2.5, с указанными ниже уточнениями. Каждый поиск производит набор применимых методов и, возможно, определяет наиболее специфичный метод из набора. В случае ошибки, как указано в §15.12.2.4, набор применимых методов пуст. В случае ошибки, как указано в §15.12.2.5, наиболее специфичного метода нет.В первом поиске ссылка на метод рассматривается так, как если бы она была вызовом с выражениями аргументов типов P1, ..., Pn. Аргументы типа, если таковые имеются, задаются выражением ссылки на метод.
Во втором поиске, если P1, ..., Pn не пусто и P1 является подтипом ТипСсылки, то выражение ссылки на метод рассматривается как выражение вызова метода с выражениями аргументов типов P2, ..., Pn. Если ТипСсылки является нетипизированным типом, и существует параметризация этого типа, 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, объявление на этапе компиляции отсутствует.В противном случае объявление на этапе компиляции является наиболее специфичным применимым методом.
-
Ошибка компиляции, если выражение ссылки на метод имеет вид ТипСсылки :: [АргументыТипа] Идентификатор, и объявление на этапе компиляции является static, и ТипСсылки не является простым или квалифицированным именем (§6.2).
Ошибка компиляции, если выражение ссылки на метод имеет вид super :: [АргументыТипа] Идентификатор или ТипИмя . super :: [АргументыТипа] Идентификатор, и объявление на этапе компиляции является abstract.
Ошибка компиляции, если выражение ссылки на метод имеет вид super :: [АргументыТипа] Идентификатор или ТипИмя . super :: [АргументыТипа] Идентификатор, и выражение ссылки на метод встречается в статическом контексте (§8.1.3).
Ошибка компиляции, если выражение ссылки на метод имеет вид ТипИмя . super :: [АргументыТипа] Идентификатор, и ТипИмя обозначает класс C, и непосредственно окружающее объявление класса или интерфейса выражения ссылки на метод не является C или вложенным классом C.
Ошибка компиляции, если выражение ссылки на метод имеет вид ТипИмя . super :: [АргументыТипа] Идентификатор, и ТипИмя обозначает интерфейс, и существует метод, отличный от объявления на этапе компиляции, который переопределяет объявление на этапе компиляции из непосредственного суперкласса или непосредственного суперинтерфейса класса или интерфейса, объявление которого непосредственно включает выражение ссылки на метод (§8.4.8, §9.4.1).
Ошибка компиляции, если выражение ссылки на метод имеет вид ТипКласса :: [АргументыТипа] new, и ошибка компиляции произошла бы при определении окружающего экземпляра для ТипКласса, как указано в §15.9.2 (обращаясь к выражению ссылки на метод, как если бы это было выражение создания экземпляра класса без квалификатора).
Выражение ссылки на метод вида ТипСсылки :: [АргументыТипа] Идентификатор может интерпретироваться по-разному. Если Идентификатор относится к методу экземпляра, то неявное лямбда-выражение имеет дополнительный параметр по сравнению с тем, если Идентификатор относится к методу static. Возможна ситуация, когда у ТипСсылки есть оба вида применимых методов, поэтому алгоритм поиска, описанный выше, идентифицирует их отдельно, так как для каждого случая есть разные типы параметров.
Пример неоднозначности:
interface Fun<T,R> { R apply(T arg); }
class C {
int size() { return 0; }
static int size(Object arg) { return 0; }
void test() {
Fun<C, Integer> f1 = C::size;
// Error: instance method size()
// or static method size(Object)?
}
}
Эту неоднозначность нельзя разрешить, предоставив применимый метод экземпляра, который более специфичен, чем применимый метод static:
interface Fun<T,R> { R apply(T arg); }
class C {
int size() { return 0; }
static int size(Object arg) { return 0; }
int size(C arg) { return 0; }
void test() {
Fun<C, Integer> f1 = C::size;
// Error: instance method size()
// or static method size(Object)?
}
}
Поиск достаточно умен, чтобы проигнорировать неоднозначности, в которых все применимые методы (из обоих поисков) являются методами экземпляров:
interface Fun<T,R> { R apply(T arg); }
class C {
int size() { return 0; }
int size(Object arg) { return 0; }
int size(C arg) { return 0; }
void test() {
Fun<C, Integer> f1 = C::size;
// OK: reference is to instance method size()
}
}
Для удобства, когда используется имя обобщённого типа для ссылки на метод экземпляра (где получатель становится первым параметром), целевой тип используется для определения аргументов типа. Это облегчает использование, например, Pair::first вместо Pair<String,Integer>::first. Аналогично, ссылка на метод типа Pair::new обрабатывается как "ромовидное" создание экземпляра (new
Pair<>()). Поскольку "ромб" неявный, эта форма не создаёт нетипизированный тип; на самом деле, нет способа выразить ссылку на конструктор нетипизированного типа.
Для некоторых выражений ссылок на метод существует только одно возможное объявление на этапе компиляции с единственным возможным типом вызова (§15.12.2.6), независимо от целевого типа функции. Такие выражения ссылок на методы называются точными. Выражение ссылки на метод, которое не является точным, называется неточным.
Выражение ссылки на метод, заканчивающееся Идентификатор, является точным, если оно удовлетворяет всем следующим условиям:
-
Если выражение ссылки на метод имеет вид ТипСсылка
::[АргументыТипов] Идентификатор, то ТипСсылка не обозначает примитивный тип. -
Тип для поиска имеет ровно один метод-член с именем Идентификатор, доступный для класса или интерфейса, в котором появляется выражение ссылки на метод.
-
Этот метод не является методом с переменным числом аргументов (§8.4.1).
-
Если этот метод является обобщённым (§8.4.4), то выражение ссылки на метод предоставляет АргументыТипов.
Выражение ссылки на метод вида ТипКласса :: [АргументыТипов] new является точным, если оно удовлетворяет всем следующим условиям:
-
Тип, обозначаемый ТипКласса, не является примитивным типом или является не-
staticчленом типа примитивного типа. -
Тип, обозначаемый ТипКласса, имеет ровно один конструктор, доступный для класса или интерфейса, в котором появляется выражение ссылки на метод.
-
Этот конструктор не является конструктором с переменным числом аргументов.
-
Если этот конструктор является обобщённым, то выражение ссылки на метод предоставляет АргументыТипов.
Выражение ссылки на метод вида ТипМассива :: new всегда является точным.
Выражение ссылки на метод совместимо в контексте присваивания, контексте вызова или контексте приведения к типу T, если T является типом функционального интерфейса (§9.8) и выражение является совместимым с типом функции основного целевого типа, полученного из T.
Основной целевой тип выводится из T следующим образом:
-
Если T является типом функционального интерфейса с параметрами-подстановочными знаками, то основной целевой тип — это параметризация T без подстановочных знаков (§9.9).
-
В противном случае, основной целевой тип — T.
Выражение ссылки на метод совместимо с типом функции, если оба следующих условия истинны:
-
Тип функции идентифицирует одну декларацию во время компиляции, соответствующую ссылке.
-
Выполняется одно из следующих условий:
-
Результат типа функции является
void. -
Результат типа функции — R, и результат применения преобразования захвата (§5.1.10) к типу возвращаемого значения типа вызова (§15.12.2.6) выбранной декларации во время компиляции — R' (где R — целевой тип, который можно использовать для вывода R'), и ни R, ни R' не являются
void, и R' совместим с R в контексте присваивания.
-
Если для применения декларации во время компиляции потребовалось неявное преобразование, и это преобразование вызовет предупреждение во время компиляции в контексте вызова, то возникает предупреждение о неявном преобразовании во время компиляции, если оно не подавлено с помощью @SuppressWarnings (§9.6.4.5).
Если для совместимости типа возвращаемого значения R' с типом возвращаемого значения типа функции R, описанным выше, потребовалось неявное преобразование, и это преобразование вызовет предупреждение во время компиляции в контексте присваивания, то возникает предупреждение о неявном преобразовании во время компиляции, если оно не подавлено с помощью @SuppressWarnings.
Если выражение ссылки на метод совместимо с целевым типом T, то тип выражения U — это основной целевой тип, выведенный из T.
Возникает ошибка компиляции, если какой-либо класс или интерфейс, упомянутый либо в U, либо в типе функции U, недоступен (§6.6) из класса или интерфейса, в котором появляется выражение ссылки на метод.
Для каждого метода-члена U, который не является static членом, m, если тип функции U имеет подсигнатуру под сигнатурой m, то предполагаемый метод, тип метода которого — тип функции U, считается переопределяющим m, и может возникнуть любая ошибка компиляции или предупреждение, указанные в §8.4.8.3.
Для каждого типа исключения X, указанного в разделе throws типа вызова декларации во время компиляции, X или суперкласс X должен быть упомянут в разделе throws типа функции U, иначе возникнет ошибка компиляции.
Ключевая идея, лежащая в основе определения совместимости, заключается в том, что ссылка на метод совместима тогда и только тогда, когда эквивалентное лямбда-выражение (x, y, z) совместимо. (Это неформальное описание, и есть проблемы, которые затрудняют или делают невозможным формальное определение семантики в терминах такого переписывания.) -> exp.<T1,
T2>method(x, y, z)
Эти правила совместимости обеспечивают удобный инструмент для преобразования из одного функционального интерфейса в другой:
Task t = () -> System.out.println("hi");
Runnable r = t::invoke;
Реализация может быть оптимизирована таким образом, что при передаче объекта, полученного из лямбда-выражения, и его преобразовании к различным типам это не приводит к многоуровневой логике адаптации вокруг основного тела лямбда-выражения.
В отличие от лямбда-выражения, ссылка на метод может быть совместима с обобщённым типом функции (то есть типом функции, имеющим параметры типа). Это связано с тем, что лямбда-выражение должно было бы уметь объявлять параметры типа, но синтаксис этого не поддерживает; в то время как для ссылки на метод такая декларация не требуется. Например, следующая программа является допустимой:
interface ListFactory {
<T> List<T> make();
}
ListFactory lf = ArrayList::new;
List<String> ls = lf.make();
List<Number> ln = lf.make();
Во время выполнения оценка выражения ссылки на метод аналогична оценке выражения создания экземпляра класса, поскольку нормальное завершение приводит к ссылке на объект. Выполнение выражения ссылки на метод отличается от вызова самого метода.
Во-первых, если выражение ссылки на метод начинается с ExpressionName или Primary, это подвыражение вычисляется. Если подвыражение вычисляется в null, возникает NullPointerException, и выражение ссылки на метод завершается аварийно. Если подвыражение завершается аварийно, выражение ссылки на метод завершается аварийно по той же причине.
Далее либо выделяется и инициализируется новый экземпляр класса со свойствами, указанными ниже, либо ссылается на существующий экземпляр класса со свойствами, указанными ниже. Если новый экземпляр должен быть создан, но для выделения объекта недостаточно места, вычисление выражения ссылки на метод завершается аварийно, вызывая OutOfMemoryError.
Значением выражения ссылки на метод является ссылка на экземпляр класса со следующими свойствами:
-
Класс реализует целевой функциональный интерфейсный тип и, если целевой тип является типом пересечения, все другие интерфейсные типы, упомянутые в пересечении.
-
Где выражение ссылки на метод имеет тип U, для каждого не-
staticметода-членаmиз U:Если функциональный тип U имеет подпись подписи
m, то класс объявляет метод вызова, который переопределяетm. Тело метода вызова вызывает ссылочный метод, создает экземпляр класса или создает массив, как описано ниже. Если результат метода вызова не являетсяvoid, то тело возвращает результат вызова метода или создания объекта после любых необходимых преобразований присваивания (§5.2).Если стирание типа переопределяемого метода отличается по своей сигнатуре от стирания функционального типа U, то перед вызовом метода или созданием объекта тело метода вызова проверяет, что каждое значение аргумента является экземпляром подкласса или подинтерфейса стирания соответствующего типа параметра в функциональном типе U; в противном случае возникает
ClassCastException. -
Класс не переопределяет другие методы функционального интерфейсного типа или другие интерфейсные типы, упомянутые выше, хотя он может переопределять методы класса
Object.
Тело метода вызова зависит от формы выражения ссылки на метод следующим образом:
-
Если форма имеет вид ExpressionName
::[TypeArguments] Identifier или Primary::[TypeArguments] Identifier, то тело метода вызова имеет эффект выражения вызова метода для объявления времени компиляции, которое является объявлением времени компиляции выражения ссылки на метод. Выполнение выражения вызова метода во время выполнения указано в §15.12.4.3, §15.12.4.4 и §15.12.4.5, где:-
Режим вызова выводится из объявления времени компиляции, как указано в §15.12.3.
-
Целевой ссылкой является значение ExpressionName или Primary, как определено при вычислении выражения ссылки на метод.
-
Аргументами выражения вызова метода являются формальные параметры метода вызова.
-
-
Если форма имеет вид ReferenceType
::[TypeArguments] Identifier, тело метода вызова аналогичным образом имеет эффект выражения вызова метода для объявления времени компиляции, которое является объявлением времени компиляции выражения ссылки на метод. Выполнение выражения вызова метода во время выполнения указано в §15.12.4.3, §15.12.4.4 и §15.12.4.5, где:-
Режим вызова выводится из объявления времени компиляции, как указано в §15.12.3.
-
Если объявление времени компиляции является методом экземпляра, то целевой ссылкой является первый формальный параметр метода вызова. В противном случае целевой ссылки нет.
-
Если объявление времени компиляции является методом экземпляра, то аргументами выражения вызова метода (если таковые имеются) являются второй и последующие формальные параметры метода вызова. В противном случае аргументами выражения вызова метода являются формальные параметры метода вызова.
-
-
Если форма имеет вид
super::[TypeArguments] Identifier или TypeName.super::[TypeArguments] Identifier, то тело метода вызова имеет эффект выражения вызова метода для объявления времени компиляции, которое является объявлением времени компиляции выражения ссылки на метод. Выполнение выражения вызова метода во время выполнения указано в §15.12.4.3, §15.12.4.4 и §15.12.4.5, где:-
Режим вызова —
super. -
Если выражение ссылки на метод начинается с TypeName, который называет класс, целевой ссылкой является значение TypeName
.thisв точке, в которой вычисляется ссылка на метод. В противном случае целевой ссылкой является значениеthisв точке, в которой вычисляется ссылка на метод. -
Аргументами выражения вызова метода являются формальные параметры метода вызова.
-
-
Если форма имеет вид ClassType
::[TypeArguments]new, тело метода вызова имеет эффект выражения создания экземпляра класса формыnew[TypeArguments] ClassType(A1, ..., An), где аргументы A1, ..., An — формальные параметры метода вызова, и где: -
Если форма имеет вид Type
[]k::new(k ≥ 1), то тело метода вызова имеет тот же эффект, что и выражение создания массива формыnewType[size][]k-1, где size — единственный параметр метода вызова. (Обозначение[]k указывает последовательность из k пар квадратных скобок.)
Если тело метода вызова имеет эффект выражения вызова метода, то типы параметров времени компиляции и результат времени компиляции вызова метода определяются, как указано в §15.12.3. Для определения результата времени компиляции выражение вызова метода является оператором выражения, если результат метода вызова — void, и Выражение оператора return, если результат метода вызова не является void.
Время вычисления выражения ссылки на метод сложнее, чем у лямбда-выражений (§15.27.4). Когда выражение ссылки на метод имеет выражение (а не тип) перед разделителем ::, это подвыражение вычисляется немедленно. Результат вычисления сохраняется до вызова метода соответствующего типа функционального интерфейса; в этот момент результат используется в качестве целевой ссылки для вызова. Это означает, что выражение перед разделителем :: вычисляется только при обнаружении программой выражения ссылки на метод и не перевычисляется при последующих вызовах функционального интерфейса.
Интересно сравнить обработку null здесь с её обработкой во время вызова метода. При вычислении выражения вызова метода возможно, что Основной элемент, который квалифицирует вызов, вычисляется как null, но не возникает никаких NullPointerException. Это происходит, когда вызываемый метод является static (несмотря на синтаксис вызова, предполагающий метод экземпляра). Поскольку применимый метод для выражения ссылки на метод, квалифицированного Основным элементом, запрещено быть static (§15.13.1), вычисление выражения ссылки на метод проще — null Основной элемент всегда вызывает NullPointerException.
Постфиксные выражения включают использование постфиксных ++ и -- операторов. Имена не считаются первичными выражениями (§15.8), но обрабатываются отдельно в грамматике, чтобы избежать определённых неоднозначностей. Они становятся взаимозаменяемыми только здесь, на уровне приоритета постфиксных выражений.
Правила вычисления имён выражений приведены в §6.5.6.
Постфиксное выражение, за которым следует оператор ++, является постфиксным выражением инкремента.
Результат постфиксного выражения должен быть переменной типа, преобразуемого (§5.1.8) к числовому типу, иначе происходит ошибка времени компиляции.
Тип постфиксного выражения инкремента — тип переменной. Результатом постфиксного выражения инкремента является не переменная, а значение.
Во время выполнения, если вычисление операндного выражения завершается внезапно, то постфиксное выражение инкремента завершается по той же причине, и инкрементация не происходит. В противном случае значение 1 добавляется к значению переменной, и сумма сохраняется обратно в переменную. Перед добавлением выполняется двоичное числовое продвижение (§5.6) над значением 1 и значением переменной. При необходимости сумма сужается с помощью сужающего примитивного преобразования (§5.1.3) и/или подвергается преобразованию в объект (§5.1.7) к типу переменной перед сохранением. Значение постфиксного выражения инкремента — значение переменной до сохранения нового значения.
Обратите внимание, что двоичное числовое продвижение, упомянутое выше, может включать преобразование из объекта в примитивный тип (§5.1.8).
Переменная, объявленная final, не может быть инкрементирована, потому что при использовании доступа к такой final переменной как выражения, результатом является значение, а не переменная. Поэтому она не может использоваться в качестве операнда постфиксного оператора инкремента.
Постфиксное выражение, за которым следует оператор --, является постфиксным выражением декремента.
Результат постфиксного выражения должен быть переменной типа, преобразуемого (§5.1.8) к числовому типу, иначе происходит ошибка времени компиляции.
Тип постфиксного выражения декремента — тип переменной. Результатом постфиксного выражения декремента является не переменная, а значение.
Во время выполнения, если вычисление операндного выражения завершается внезапно, то постфиксное выражение декремента завершается по той же причине, и декрементация не происходит. В противном случае значение 1 вычитается из значения переменной, и разность сохраняется обратно в переменную. Перед вычитанием выполняется двоичное числовое продвижение (§5.6) над значением 1 и значением переменной. При необходимости разность сужается с помощью сужающего примитивного преобразования (§5.1.3) и/или подвергается преобразованию в объект (§5.1.7) к типу переменной перед сохранением. Значение постфиксного выражения декремента — значение переменной до сохранения нового значения.
Обратите внимание, что двоичное числовое продвижение, упомянутое выше, может включать преобразование из объекта в примитивный тип (§5.1.8).
Переменная, объявленная final, не может быть декрементирована, потому что при использовании доступа к такой final переменной как выражения, результатом является значение, а не переменная. Поэтому она не может использоваться в качестве операнда постфиксного оператора декремента.
Операторы +, -, ++, --, ~, ! и оператор приведения типов (§15.16) называются унарными операторами. Унарное выражение — это либо унарный оператор, применённый к операнду, либо выражение switch (§15.28).
Выражения с унарными операторами группируются справа налево, так что -~x означает то же, что и -(~x).
Данная часть грамматики содержит некоторые уловки для избежания двух потенциальных синтаксических неоднозначностей.
Первая потенциальная неоднозначность возникает в выражениях, таких как (p)+q, что, с точки зрения программиста на C или C++, похоже на то, что это может быть либо приведение к типу p унарного оператора +, действующего над q, либо бинарное сложение двух величин p и q. В C и C++ анализатор обрабатывает эту проблему, выполняя ограниченный объём семантического анализа при анализе, чтобы знать, является ли p именем типа или именем переменной.
Java использует другой подход. Результат оператора + должен быть числовым, и все имена типов, участвующие в приведениях числовых значений, являются известными ключевыми словами. Таким образом, если p — это ключевое слово, обозначающее примитивный тип, то (p)+q может иметь смысл только как приведение унарного выражения. Однако, если p не является ключевым словом, обозначающим примитивный тип, то (p)+q может иметь смысл только как бинарная арифметическая операция. Аналогичные замечания относятся к оператору -. Грамматика разделяет CastExpression на несколько случаев, чтобы сделать это различие:
Нетерминал UnaryExpression включает все унарные операторы, но нетерминал UnaryExpressionNotPlusMinus исключает использование унарных операторов, которые также могут быть бинарными, а в Java это + и -.
Вторая потенциальная неоднозначность заключается в том, что выражение (p)++ может, с точки зрения программиста на C или C++, казаться либо постфиксным инкрементом выражения в скобках, либо началом приведения типов, например, в (p)++q. Как и прежде, анализаторы для C и C++ знают, является ли p именем типа или именем переменной. Но анализатор, использующий только предварительный просмотр одного токена и не выполняющий семантический анализ во время анализа, не сможет определить, когда ++ является предстоящим токеном, является ли (p) выражением Primary или оставить его для последующего рассмотрения как части CastExpression.
В Java результат оператора ++ должен быть числовым, а все имена типов, используемые в приведениях числовых значений, являются известными ключевыми словами. Таким образом, если p — это ключевое слово, обозначающее примитивный тип, то (p)++ может иметь смысл только как приведение префиксного инкрементного выражения, и обязательно должен быть такой операнд, как q, следующий за ++. Однако, если p не является ключевым словом, обозначающим примитивный тип, то (p)++ может иметь смысл только как постфиксный инкремент p. Аналогичные замечания относятся к оператору --. Следовательно, нетерминал UnaryExpressionNotPlusMinus также исключает использование префиксных операторов ++ и --.
Унарное выражение, предваряемое оператором ++, является выражением префиксного инкремента.
Результат унарного выражения должен быть переменной типа, преобразуемой (§5.1.8) к числовому типу, в противном случае происходит ошибка компиляции.
Тип выражения префиксного инкремента — это тип переменной. Результат выражения префиксного инкремента — это не переменная, а значение.
Во время выполнения, если вычисление выражения операнда завершается неожиданно, то выражение префиксного инкремента завершается по той же причине, и инкрементирование не происходит. В противном случае к значению переменной добавляется значение 1, и сумма сохраняется обратно в переменную. Перед сложением выполняется бинарное числовое продвижение (§5.6) для значения 1 и значения переменной. При необходимости сумма сужается с помощью сужающего преобразования примитивного типа (§5.1.3) и/или подвергается преобразованию упаковки (§5.1.7) до типа переменной перед сохранением. Значение выражения префиксного инкремента — это значение переменной после сохранения нового значения.
Обратите внимание, что бинарное числовое продвижение, упомянутое выше, может включать преобразование распаковки (§5.1.8).
Переменную, объявленную final, нельзя инкрементировать, потому что при использовании такой переменной в выражении результатом является значение, а не переменная. Таким образом, она не может использоваться в качестве операнда оператора префиксного инкремента.
Унарное выражение, предваряемое оператором --, является выражением префиксного декремента.
Результат унарного выражения должен быть переменной типа, преобразуемой (§5.1.8) к числовому типу, в противном случае происходит ошибка компиляции.
Тип выражения префиксного декремента — это тип переменной. Результат выражения префиксного декремента — это не переменная, а значение.
Во время выполнения, если вычисление выражения операнда завершается неожиданно, то выражение префиксного декремента завершается по той же причине, и декрементирование не происходит. В противном случае от значения переменной вычитается значение 1, и разность сохраняется обратно в переменную. Перед вычитанием выполняется бинарное числовое продвижение (§5.6) для значения 1 и значения переменной. При необходимости разность сужается с помощью сужающего преобразования примитивного типа (§5.1.3) и/или подвергается преобразованию упаковки (§5.1.7) до типа переменной перед сохранением. Значение выражения префиксного декремента — это значение переменной после сохранения нового значения.
Обратите внимание, что бинарное числовое продвижение, упомянутое выше, может включать преобразование распаковки (§5.1.8).
Переменную, объявленную final, нельзя декрементировать, потому что при использовании такой переменной в выражении результатом является значение, а не переменная. Таким образом, она не может использоваться в качестве операнда оператора префиксного декремента.
Тип операнда выражения унарного + оператора должен быть типом, преобразуемым (§5.1.8) к примитивному числовому типу, иначе произойдёт ошибка компиляции.
На операнде выполняется унарное числовое продвижение (§5.6). Тип выражения унарного плюса — это тип продвинутого операнда. Результат выражения унарного плюса не является переменной, а является значением, даже если результат выражения операнда является переменной.
Во время выполнения значение выражения унарного плюса — это продвинутое значение операнда.
Тип операнда выражения унарного - оператора должен быть типом, преобразуемым (§5.1.8) к примитивному числовому типу, иначе произойдёт ошибка компиляции.
На операнде выполняется унарное числовое продвижение (§5.6).
Тип выражения унарного минуса — это тип продвинутого операнда.
Во время выполнения значение выражения унарного минуса — это арифметическое отрицание продвинутого значения операнда.
Для целочисленных значений отрицание равно вычитанию из нуля. Язык программирования Java использует представление целых чисел в дополнительном коде, и диапазон значений в дополнительном коде несимметричен, поэтому отрицание максимального отрицательного int или long приводит к тому же максимальному отрицательному числу. В этом случае происходит переполнение, но исключение не генерируется. Для всех целочисленных значений x, -x равно (~x)+1.
Для значений с плавающей точкой отрицание не равно вычитанию из нуля, потому что если x равно +0.0, то 0.0-x равно +0.0, но -x равно -0.0. Унарный минус просто инвертирует знак числа с плавающей точкой. Важные особые случаи:
-
Если операнд — NaN, результат — NaN. (Напомним, что NaN не имеет знака (§4.2.3).)
Язык программирования Java не принял более строгие требования из версии IEEE 754 стандарта 2019 года, что отрицание инвертирует бит знака для всех входных данных, включая NaN.
-
Если операнд — бесконечность, результат — бесконечность противоположного знака.
-
Если операнд — ноль, результат — ноль противоположного знака.
Тип операнда выражения унарного ~ оператора должен быть типом, преобразуемым (§5.1.8) к примитивному целочисленному типу, иначе произойдёт ошибка компиляции.
На операнде выполняется унарное числовое продвижение (§5.6). Тип выражения унарного побитового дополнения — это тип продвинутого операнда.
Во время выполнения значение выражения унарного побитового дополнения — это побитовое дополнение продвинутого значения операнда. Во всех случаях, ~x равно (-x)-1.
Тип операнда выражения унарного ! оператора должен быть типом boolean или Boolean, иначе произойдёт ошибка компиляции.
Тип выражения унарного логического дополнения — boolean.
Во время выполнения, если необходимо, операнд претерпевает преобразование разыменования (§5.1.8). Значение выражения унарного логического дополнения равно true, если значение операнда (возможно, преобразованное) равно false, и равно false, если значение операнда (возможно, преобразованное) равно true.
Выражение приведения типов преобразует во время выполнения значение одного числового типа в подобное значение другого числового типа; или подтверждает во время компиляции, что тип выражения — boolean; или проверяет во время выполнения, что ссылка на значение ссылается на объект, чья класс совместим со специфицированным типом ссылки или списком типов ссылок, или который воплощает значение примитивного типа.
Следующее производство из §4.4 показано здесь для удобства:
Скобки и содержащийся в них тип или список типов иногда называются оператором приведения типа.
Если оператор приведения типа содержит список типов, то есть ReferenceType, за которым следует одна или несколько AdditionalBound терминов, то все нижеперечисленное должно быть истинным, иначе произойдёт ошибка компиляции:
-
ReferenceType должен обозначать класс или тип интерфейса.
-
Стирания (§4.6) всех перечисленных типов должны быть попарно различными.
-
Ни два перечисленных типа не должны быть подтипами разных параметризаций одного и того же обобщённого интерфейса.
Целевой тип для контекста приведения (§5.5), введённого выражением приведения типа, — это либо PrimitiveType, либо ReferenceType (если за ним не следуют AdditionalBound термины), либо пересечение типов, обозначенное ReferenceType и AdditionalBound терминами, появляющимися в операторе приведения типа.
Тип выражения приведения типа — это результат применения преобразования захвата (§5.1.10) к этому целевому типу.
Приведения типов могут использоваться для явного «метки» лямбда-выражения или выражения ссылки на метод с определённым целевым типом. Для обеспечения надлежащей гибкости целевой тип может быть списком типов, обозначающим тип пересечения, при условии, что пересечение индуцирует функциональный интерфейс (§9.8).
Результат выражения приведения типа не является переменной, а является значением, даже если результат вычисления выражения операнда является переменной.
Если тип операнда во время компиляции не может быть преобразован приведением типов (§5.5) к целевому типу, указанному оператором приведения типа, то произойдёт ошибка компиляции.
В противном случае во время выполнения значение операнда преобразуется (при необходимости) приведением типа к целевому типу, указанному оператором приведения типа.
Выбрасывается ClassCastException, если приведение во время выполнения оказывается недопустимым.
Некоторые приведения типов приводят к ошибке во время компиляции. Некоторые приведения типов могут быть доказаны во время компиляции, что всегда будут правильными во время выполнения. Например, всегда правильно преобразовать значение типа класса к типу его суперкласса; такое приведение не должно требовать специальных действий во время выполнения. Наконец, некоторые приведения типов нельзя доказать, что они всегда верны или всегда неверны во время компиляции. Такие приведения требуют проверки во время выполнения. Смотрите §5.5 для получения подробной информации.
Операторы *, / и % называются множительными операторами.
Множительные операторы имеют одинаковый приоритет и являются левоассоциативными (они группируются слева направо).
Тип каждого из операндов множительного оператора должен быть типом, преобразуемым (§5.1.8) в примитивный числовой тип; в противном случае возникает ошибка времени компиляции.
Над операндами выполняется двоичное числовое продвижение (§5.6).
Обратите внимание, что двоичное числовое продвижение может включать преобразование в примитивный тип (§5.1.8).
Тип множительного выражения — это тип его операндов после продвижения.
Если тип после продвижения — int или long, то выполняется целочисленная арифметика.
Если тип после продвижения — float или double, то выполняется арифметика с плавающей точкой.
Двоичный оператор * выполняет умножение, получая произведение своих операндов.
Умножение является коммутативной операцией, если выражения операндов не имеют побочных эффектов.
Целочисленное умножение ассоциативно, когда все операнды имеют одинаковый тип.
Умножение с плавающей точкой не является ассоциативным.
Если целочисленное умножение приводит к переполнению, то результатом являются младшие биты математического произведения, представленные в формате дополнительного кода с достаточно большой разрядностью. В результате, при переполнении знак результата может не совпадать со знаком математического произведения значений двух операндов.
Результат умножения с плавающей точкой определяется правилами арифметики IEEE 754:
-
Если хотя бы один операнд — NaN, то результатом является NaN.
-
Если результат не NaN, то его знак положителен, если оба операнда имеют одинаковый знак, и отрицателен, если операнды имеют разные знаки.
-
Умножение бесконечности на ноль приводит к NaN.
-
Умножение бесконечности на конечное значение приводит к знаковой бесконечности. Знак определяется указанным выше правилом.
-
В остальных случаях, где не участвуют бесконечность или NaN, вычисляется точное математическое произведение.
Если значение произведения слишком велико для представления, то говорят о переполнении; результатом тогда является бесконечность соответствующего знака.
В противном случае произведение округляется до ближайшего представимого значения с помощью политики округления до ближайшего значения (§15.4). Язык программирования Java требует поддержки постепенного переполнения, как определено в IEEE 754.
Несмотря на то, что может произойти переполнение, недополнение или потеря информации, вычисление оператора умножения * никогда не приводит к выбрасыванию исключения во время выполнения.
Двоичный оператор / выполняет деление, получая частное от деления своих операндов. Левый операнд — это делимое, а правый операнд — делитель.
Целочисленное деление округляется к 0. То есть, частное, полученное для операндов n и d, являющихся целыми числами после двоичного числового продвижения (§5.6), — это целочисленное значение q, модуль которого как можно больше удовлетворяет условию |d ⋅ q| ≤ |n|. Более того, q положительно, когда |n| ≥ |d| и n и d имеют одинаковый знак, но q отрицательно, когда |n| ≥ |d| и n и d имеют разные знаки.
Есть один особый случай, который не удовлетворяет этому правилу: если делимое — это целое число с максимально возможным модулем для своего типа, а делитель — -1, то происходит переполнение, и результат равен делимому. Несмотря на переполнение, исключение в этом случае не выбрасывается. С другой стороны, если значение делителя в целочисленном делении равно 0, то выбрасывается ArithmeticException.
Результат деления с плавающей точкой определяется правилами арифметики IEEE 754:
-
Если хотя бы один операнд — NaN, то результатом является NaN.
-
Если результат не NaN, то его знак положителен, если оба операнда имеют одинаковый знак, и отрицателен, если операнды имеют разные знаки.
-
Деление бесконечности на бесконечность приводит к NaN.
-
Деление бесконечности на конечное значение приводит к знаковой бесконечности. Знак определяется указанным выше правилом.
-
Деление конечного значения на бесконечность приводит к знаковому нулю. Знак определяется указанным выше правилом.
-
Деление нуля на ноль приводит к NaN; деление нуля на любое другое конечное значение приводит к знаковому нулю. Знак определяется указанным выше правилом.
-
Деление ненулевого конечного значения на ноль приводит к знаковой бесконечности. Знак определяется указанным выше правилом.
-
В остальных случаях, где не участвуют бесконечность или NaN, вычисляется точное математическое частное.
Если значение частного слишком велико для представления, то говорят о переполнении; результатом тогда является бесконечность соответствующего знака.
В противном случае частное округляется до ближайшего представимого значения с помощью политики округления до ближайшего значения (§15.4). Язык программирования Java требует поддержки постепенного недополнения, как определено в IEEE 754.
Несмотря на то, что может произойти переполнение, недополнение, деление на ноль или потеря информации, вычисление оператора деления с плавающей точкой / никогда не приводит к выбрасыванию исключения во время выполнения.
Двоичный % оператор возвращает остаток от деления своих операндов; левый операнд — это делимое, а правый операнд — делитель.
В C и C++ оператор остатка принимает только целочисленные операнды, но в языке программирования Java он также принимает операнды с плавающей точкой.
Операция получения остатка для операндов, которые являются целыми числами после двоичного числового продвижения (§5.6), производит значение результата таким образом, что (a/b)*b+(a%b) равно a.
Это тождество выполняется даже в том специальном случае, когда делимое — это отрицательное целое число наибольшего возможного значения для его типа, а делитель — -1 (остаток — 0).
Из этого правила следует, что результат операции остатка может быть отрицательным только в том случае, если делимое отрицательное, и может быть положительным только в том случае, если делимое положительное. Кроме того, величина результата всегда меньше величины делителя.
Если значение делителя для целочисленного оператора остатка — 0, то выбрасывается ArithmeticException.
Пример 15.17.3-1. Оператор остатка для целых чисел
class Test1 {
public static void main(String[] args) {
int a = 5%3; // 2
int b = 5/3; // 1
System.out.println("5%3 produces " + a +
" (note that 5/3 produces " + b + ")");
int c = 5%(-3); // 2
int d = 5/(-3); // -1
System.out.println("5%(-3) produces " + c +
" (note that 5/(-3) produces " + d + ")");
int e = (-5)%3; // -2
int f = (-5)/3; // -1
System.out.println("(-5)%3 produces " + e +
" (note that (-5)/3 produces " + f + ")");
int g = (-5)%(-3); // -2
int h = (-5)/(-3); // 1
System.out.println("(-5)%(-3) produces " + g +
" (note that (-5)/(-3) produces " + h + ")");
}
}
Эта программа выводит:
5%3 produces 2 (note that 5/3 produces 1) 5%(-3) produces 2 (note that 5/(-3) produces -1) (-5)%3 produces -2 (note that (-5)/3 produces -1) (-5)%(-3) produces -2 (note that (-5)/(-3) produces 1)
Результат операции остатка с плавающей точкой, вычисленный оператором %, не такой же, как результат, вычисленный операцией остатка в IEEE 754, из-за выбора политики округления в языке программирования Java (§15.4). Операция остатка IEEE 754 вычисляет остаток от округления деления, а не от усечения, и поэтому ее поведение не аналогично поведению обычного целочисленного оператора остатка. Вместо этого язык программирования Java определяет % для операндов с плавающей точкой, чтобы он вел себя аналогично целочисленному оператору остатка с неявным делением с политикой округления к нулю; это можно сравнить с функцией библиотеки C fmod. Операцию остатка IEEE 754 можно вычислить с помощью библиотечной функции Math.IEEEremainder или StrictMath.IEEEremainder.
Результат операции остатка с плавающей точкой определяется следующими правилами, которые соответствуют арифметике IEEE 754, за исключением того, как вычисляется неявное деление:
-
Если какой-либо операнд — NaN, результат — NaN.
-
Если результат не NaN, знак результата равен знаку делимого.
-
Если делимое — бесконечность, или делитель — ноль, или оба, результат — NaN.
-
Если делимое конечно, а делитель — бесконечность, результат равен делимому.
-
Если делимое — ноль, а делитель — конечное число, результат равен делимому.
-
В остальных случаях, когда не участвует бесконечность, ноль или NaN, остаток с плавающей точкой r от деления делимого n на делитель d определяется математическим соотношением r = n - (d ⋅ q), где q — целое число, которое отрицательно только если n/d отрицательно и положительно только если n/d положительно, и его величина наибольшая, не превышающая величины истинного математического частного от n и d.
Вычисление оператора остатка с плавающей точкой % никогда не вызывает исключения во время выполнения, даже если правый операнд равен нулю. Переполнение, недополнение или потеря точности не могут произойти.
Пример 15.17.3-2. Оператор остатка с плавающей точкой
class Test2 {
public static void main(String[] args) {
double a = 5.0%3.0; // 2.0
System.out.println("5.0%3.0 produces " + a);
double b = 5.0%(-3.0); // 2.0
System.out.println("5.0%(-3.0) produces " + b);
double c = (-5.0)%3.0; // -2.0
System.out.println("(-5.0)%3.0 produces " + c);
double d = (-5.0)%(-3.0); // -2.0
System.out.println("(-5.0)%(-3.0) produces " + d);
}
}
Эта программа выводит:
5.0%3.0 produces 2.0 5.0%(-3.0) produces 2.0 (-5.0)%3.0 produces -2.0 (-5.0)%(-3.0) produces -2.0
Операторы + и - называются арифметическими операторами.
Арифметические операторы имеют одинаковый приоритет и являются левоассоциативными (они группируются слева направо).
Если тип одного из операндов оператора + равен String, то операция представляет собой конкатенацию строк.
В противном случае, тип каждого из операндов оператора + должен быть типом, преобразуемым (§5.1.8) к примитивному числовому типу, иначе произойдёт ошибка компиляции.
В любом случае, тип каждого из операндов бинарного оператора - должен быть типом, преобразуемым (§5.1.8) к примитивному числовому типу, иначе произойдёт ошибка компиляции.
Если только один из операндов имеет тип String, то на другой операнд выполняется преобразование в строку (§5.1.11) для получения строки во время выполнения.
Результат конкатенации строк — ссылка на объект String, который представляет собой конкатенацию двух строковых операндов. Символы левого операнда предшествуют символам правого операнда в новой строке.
Объект String создаётся заново (§12.5), если выражение не является константным выражением (§15.29).
Реализация может выбрать выполнение преобразования и конкатенации одним шагом, чтобы избежать создания и последующего удаления промежуточного объекта String. Для повышения производительности при многократной конкатенации строк компилятор Java может использовать класс StringBuffer или аналогичную технику, чтобы уменьшить количество промежуточных объектов String, создаваемых при вычислении выражения.
Для примитивных типов реализация также может оптимизировать создание объекта-обёртки, преобразуя примитивный тип непосредственно в строку.
Пример 15.18.1-1. Конкатенация строк
Приведённое выражение:
"The square root of 2 is " + Math.sqrt(2)
дает результат:
"The square root of 2 is 1.4142135623730952"
Оператор + является левоассоциативным, независимо от того, представляет ли он конкатенацию строк или числовое сложение по результатам анализа типа. В некоторых случаях требуется осторожность для получения желаемого результата. Например, выражение:
a + b + c
всегда интерпретируется как:
(a + b) + c
Поэтому результат выражения:
1 + 2 + " fiddlers"
равен:
"3 fiddlers"
но результат:
"fiddlers " + 1 + 2
равен:
"fiddlers 12"
Пример 15.18.1-2. Конкатенация строк и условные операторы
В этом шутливом примере:
class Bottles {
static void printSong(Object stuff, int n) {
String plural = (n == 1) ? "" : "s";
loop: while (true) {
System.out.println(n + " bottle" + plural
+ " of " + stuff + " on the wall,");
System.out.println(n + " bottle" + plural
+ " of " + stuff + ";");
System.out.println("You take one down "
+ "and pass it around:");
--n;
plural = (n == 1) ? "" : "s";
if (n == 0)
break loop;
System.out.println(n + " bottle" + plural
+ " of " + stuff + " on the wall!");
System.out.println();
}
System.out.println("No bottles of " +
stuff + " on the wall!");
}
public static void main(String[] args) {
printSong("slime", 3);
}
}
метод printSong напечатает строку детской песенки. Популярные значения для stuff включают "pop" и "beer"; самое популярное значение для n — 100. Вот результат выполнения программы:
3 bottles of slime on the wall, 3 bottles of slime; You take one down and pass it around: 2 bottles of slime on the wall! 2 bottles of slime on the wall, 2 bottles of slime; You take one down and pass it around: 1 bottle of slime on the wall! 1 bottle of slime on the wall, 1 bottle of slime; You take one down and pass it around: No bottles of slime on the wall!
В коде обратите внимание на тщательное условное формирование единственного числа "bottle" при необходимости, а не множественного числа "bottles"; обратите также внимание, как оператор конкатенации строк использовался для разделения длинной константной строки:
"You take one down and pass it around:"
на две части, чтобы избежать неудобной длинной строки в исходном коде.
Бинарный оператор + выполняет сложение, когда применяется к двум операндам числового типа, возвращая сумму операндов.
Бинарный оператор - выполняет вычитание, возвращая разность двух числовых операндов.
Над операндами выполняется расширение числовых типов (§5.6).
Обратите внимание, что расширение числовых типов может включать в себя преобразование разворачивания (§5.1.8).
Тип арифметического выражения с числовыми операндами — это тип расширенных операндов.
Если этот расширенный тип равен int или long, то выполняется целочисленная арифметика.
Если этот расширенный тип равен float или double, то выполняется арифметика с плавающей точкой.
Сложение является коммутативной операцией, если выражения операндов не имеют побочных эффектов.
Целочисленное сложение ассоциативно, когда все операнды одного типа.
Сложение с плавающей точкой не ассоциативно.
Если целочисленное сложение приводит к переполнению, то результатом являются младшие биты математической суммы, представленные в формате дополнения до двух достаточной длины. При переполнении знак результата не совпадает со знаком математической суммы двух значений операндов.
Результат сложения с плавающей точкой определяется правилами арифметики IEEE 754:
-
Если один из операндов равен NaN, результат равен NaN.
-
Сумма двух бесконечностей с разными знаками равна NaN.
-
Сумма двух бесконечностей с одинаковыми знаками равна бесконечности того же знака.
-
Сумма бесконечности и конечного значения равна бесконечному операнду.
-
Сумма двух нулей с разными знаками равна положительному нулю.
-
Сумма двух нулей с одинаковыми знаками равна нулю того же знака.
-
Сумма нуля и ненулевого конечного значения равна ненулевому операнду.
-
Сумма двух ненулевых конечных значений одинаковой величины и разных знаков равна положительному нулю.
-
В оставшихся случаях, когда ни бесконечность, ни ноль, ни NaN не участвуют, и операнды имеют одинаковые знаки или разные величины, вычисляется точная математическая сумма.
Если величина суммы слишком велика для представления, мы говорим о переполнении; в таком случае результат — бесконечность соответствующего знака.
В противном случае сумма округляется до ближайшего представимого значения с помощью политики округления до ближайшего значения (§15.4). Язык программирования Java требует поддержки постепенного уменьшения.
Бинарный оператор - выполняет вычитание, когда применяется к двум числовым операндам, возвращая разность операндов; левый операнд является уменьшаемым, а правый — вычитаемым.
Как для целочисленного, так и для плавающего вычитания, всегда верно, что a-b даёт такой же результат, что и a+(-b).
Обратите внимание, что для целочисленных значений вычитание из нуля то же самое, что и отрицание. Однако для операндов с плавающей точкой вычитание из нуля не эквивалентно отрицанию, так как если x равно +0.0, то 0.0-x равно +0.0, но -x равно -0.0.
Несмотря на то, что может произойти переполнение, недополнение или потеря информации, вычисление арифметического оператора никогда не выбросит исключение во время выполнения.
Операторы << (сдвиг влево), >> (сдвиг вправо со знаком) и >>> (сдвиг вправо без знака) называются операторами сдвига. Операнд слева от оператора сдвига — это значение, подлежащее сдвигу; операнд справа указывает расстояние сдвига.
Операторы сдвига имеют левоассоциативную синтаксическую группировку (они группируются слева направо).
Унарное числовое продвижение (§5.6) выполняется для каждого операнда отдельно. (Двоичное числовое продвижение не выполняется для операндов.)
Ошибка компиляции, если тип каждого из операндов оператора сдвига после унарного числового продвижения не является примитивным целочисленным типом.
Тип выражения сдвига — это продвинутый тип левого операнда.
Если продвинутый тип левого операнда — int, то используются только пять младших битов правого операнда в качестве расстояния сдвига. Это как если бы правый операнд был подвергнут оператору поразрядного логического И & (§15.22.1) с маской 0x1f (0b11111). Расстояние сдвига, фактически используемое, поэтому всегда находится в диапазоне 0 до 31 включительно.
Если продвинутый тип левого операнда — long, то используются только шесть младших битов правого операнда в качестве расстояния сдвига. Это как если бы правый операнд был подвергнут оператору поразрядного логического И & (§15.22.1) с маской 0x3f (0b111111). Расстояние сдвига, фактически используемое, поэтому всегда находится в диапазоне 0 до 63 включительно.
Во время выполнения операции сдвига выполняются на представлении значения левого операнда в дополнительном коде к двум.
Значение n << s — это n, сдвинутый влево на s позиций; это эквивалентно (даже при переполнении) умножению на два в степени s.
Значение n >> s — это n, сдвинутый вправо на s позиций со знаком расширения. Результирующее значение равно floor(n / 2s). Для неотрицательных значений n это эквивалентно усечению целочисленного деления, вычисленного оператором целочисленного деления /, на два в степени s.
Значение n >>> s — это n, сдвинутый вправо на s позиций с нулевым расширением, где:
-
Если n положительно, то результат совпадает с результатом n
>>s. -
Если n отрицательно и тип левого операнда —
int, то результат равен результату выражения(n>>s)+(2<<~s). -
Если n отрицательно и тип левого операнда —
long, то результат равен результату выражения(n>>s)+(2L<<~s).
Добавленный член (2 << ~s) или (2L << ~s) отменяет распространяемый бит знака.
Обратите внимание, что из-за неявного маскирования правого операнда оператора сдвига ~s как расстояние сдвига эквивалентно 31-s при сдвиге значения int и 63-s при сдвиге значения long.
Числовые операторы сравнения <, >, <=, и >=, а также оператор instanceof, называются операторами отношения.
Операторы отношения являются левоассоциативными (они группируются слева направо).
Однако, этот факт бесполезен. Например, a<b<c разбирается как (a<b)<c, что всегда является ошибкой на этапе компиляции, потому что тип a<b всегда boolean, а < не является оператором для boolean значений.
Тип выражения отношения всегда boolean.
Тип каждого из операндов числового оператора сравнения должен быть типом, преобразуемым (§5.1.8) к примитивному числовому типу, в противном случае возникает ошибка компиляции.
На операндах выполняется двоичное числовое продвижение (§5.6).
Обратите внимание, что двоичное числовое продвижение может включать преобразование разупаковки (§5.1.8).
Если тип продвижения операндов — int или long, выполняется сравнение целых чисел со знаком.
Если тип продвижения — float или double, выполняется сравнение чисел с плавающей точкой.
Результат сравнения чисел с плавающей точкой, как определено спецификацией стандарта IEEE 754:
-
Если любой из операндов — NaN, результат — ложь.
-
Все значения, кроме NaN, упорядочены, при этом отрицательная бесконечность меньше всех конечных значений, а положительная бесконечность больше всех конечных значений.
-
Положительный и отрицательный нули считаются равными.
Например,
-0.0<0.0этоfalse, но-0.0<=0.0этоtrue.Однако методы
Math.minиMath.maxрассматривают отрицательный ноль как строго меньший, чем положительный ноль.
При соблюдении этих соображений для чисел с плавающей точкой, следующие правила применяются для целочисленных операндов или для операндов с плавающей точкой, отличных от NaN:
-
Значение, произведённое оператором
<, равноtrue, если значение левого операнда меньше значения правого операнда, и в противном случае равноfalse. -
Значение, произведённое оператором
<=, равноtrue, если значение левого операнда меньше или равно значению правого операнда, и в противном случае равноfalse. -
Значение, произведённое оператором
>, равноtrue, если значение левого операнда больше значения правого операнда, и в противном случае равноfalse. -
Значение, произведённое оператором
>=, равноtrue, если значение левого операнда больше или равно значению правого операнда, и в противном случае равноfalse.
Выражение instanceof может выполнять сравнение типов или сопоставление с шаблоном.
Если операнд справа от ключевого слова instanceof является Типом ссылки, то ключевое слово instanceof является оператором сравнения типов.
Если операнд справа от ключевого слова instanceof является Шаблоном, то ключевое слово instanceof является оператором сопоставления с шаблоном.
Следующие правила применяются, когда instanceof является оператором сравнения типов:
-
Тип выражения RelationalExpression должен быть типом ссылки или типом null, иначе произойдет ошибка компиляции.
-
RelationalExpression должен быть совместим с Типом ссылки при приведении типов (§5.5), иначе произойдет ошибка компиляции.
-
Во время выполнения результат оператора сравнения типов определяется следующим образом:
-
Если значение RelationalExpression является нулевой ссылкой (§4.1), то результат —
false. -
Если значение RelationalExpression не является нулевой ссылкой, то результат —
true, если значение можно привести к Тип ссылки без вызоваClassCastException, иfalseв противном случае.
-
Следующие правила применяются, когда instanceof является оператором сопоставления с шаблоном:
-
Тип выражения RelationalExpression должен быть типом ссылки или типом null, иначе произойдет ошибка компиляции.
-
Шаблон должен быть применим к типу выражения RelationalExpression (§14.30.3), иначе произойдет ошибка компиляции.
-
Во время выполнения результат оператора сопоставления с шаблоном определяется следующим образом:
-
Если значение 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) при применении к булевым операндам.
Если оба операнда оператора сравнения на равенство являются ссылочными типами или типом 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. -
Операция взятия выражения в скобки (parenthesized)
booleanвыражения (§15.8.5). -
Выражение создания экземпляра класса (§15.9) для класса
Boolean. -
Выражение вызова метода (§15.12) для которого выбранный наиболее конкретный метод (§15.12.2.5) имеет тип возвращаемого значения
booleanилиBoolean.Обратите внимание, что для обобщённого метода это тип перед подстановкой аргументов типа метода.
-
Условное выражение
boolean. -
Выражение
switch(§15.28), результаты которого являютсяbooleanвыражениями.
-
-
Если и второй, и третий операнды являются числовыми выражениями, условное выражение является числовым условным выражением.
Для целей классификации условного выражения следующие выражения являются числовыми:
-
Выражение в автономной форме (§15.2) с типом, преобразуемым к числовому типу (§4.2, §5.1.8).
-
Операция взятия числового выражения в скобки (§15.8.5).
-
Выражение создания экземпляра класса (§15.9) для класса, преобразуемого к числовому типу.
-
Выражение вызова метода (§15.12) для которого выбранный наиболее конкретный метод (§15.12.2.5) имеет тип возвращаемого значения, преобразуемый к числовому типу.
Обратите внимание, что для обобщённого метода это тип перед подстановкой аргументов типа метода.
-
Числовое условное выражение.
-
Выражение
switch(§15.28), результаты которого являются числовыми выражениями.
-
-
В противном случае, условное выражение является ссылочным условным выражением.
Процесс определения типа условного выражения зависит от вида условного выражения, как описано в следующих разделах.
Следующие таблицы обобщают вышеуказанные правила, предоставляя тип условного выражения для всех возможных типов его вторых и третьих операндов. bnp(..) означает применение двоичного числового повышения. Форма "T | bnp(..)" используется, когда один операнд является константным выражением типа int и может быть представлен в типе T, где двоичное числовое повышение используется, если операнд не может быть представлен в типе T. Тип операнда Object обозначает любой ссылочный тип, отличный от типа null и восьми оберточных классов Boolean, Byte, Short, Character, Integer, Long, Float, Double.
Таблица 15.25-A. Тип условного выражения (Примитивный 3-й операнд, Часть I)
| 3-й → | byte | short | char | int |
|---|---|---|---|---|
| 2-й ↓ | ||||
byte | byte | short | bnp(byte,char) |
byte | bnp(byte,int) |
Byte | byte | short | bnp(Byte,char) |
byte | bnp(Byte,int) |
short | short | short | bnp(short,char) |
short | bnp(short,int) |
Short | short | short | bnp(Short,char) |
short | bnp(Short,int) |
char | bnp(char,byte) | bnp(char,short) | char |
char | bnp(char,int) |
Character | bnp(Character,byte) | bnp(Character,short) | char |
char | bnp(Character,int) |
int |
byte | bnp(int,byte) |
short | bnp(int,short) |
char | bnp(int,char) | int |
Integer | bnp(Integer,byte) | bnp(Integer,short) | bnp(Integer,char) | int |
long | bnp(long,byte) | bnp(long,short) | bnp(long,char) | bnp(long,int) |
Long | bnp(Long,byte) | bnp(Long,short) | bnp(Long,char) | bnp(Long,int) |
float | bnp(float,byte) | bnp(float,short) | bnp(float,char) | bnp(float,int) |
Float | bnp(Float,byte) | bnp(Float,short) | bnp(Float,char) | bnp(Float,int) |
double | bnp(double,byte) | bnp(double,short) | bnp(double,char) | bnp(double,int) |
Double | bnp(Double,byte) | bnp(Double,short) | bnp(Double,char) | bnp(Double,int) |
boolean | lub(Boolean,Byte) | lub(Boolean,Short) | lub(Boolean,Character) | lub(Boolean,Integer) |
Boolean | lub(Boolean,Byte) | lub(Boolean,Short) | lub(Boolean,Character) | lub(Boolean,Integer) |
null | lub(null,Byte) | lub(null,Short) | lub(null,Character) | lub(null,Integer) |
Object | lub(Object,Byte) | lub(Object,Short) | lub(Object,Character) | lub(Object,Integer) |
Таблица 15.25-B. Тип условного выражения (примитивный 3-й операнд, часть II)
| 3-й → | long | float | double | boolean |
|---|---|---|---|---|
| 2-й ↓ | ||||
byte | bnp(byte,long) | bnp(byte,float) | bnp(byte,double) | lub(Byte,Boolean) |
Byte | bnp(Byte,long) | bnp(Byte,float) | bnp(Byte,double) | lub(Byte,Boolean) |
short | bnp(short,long) | bnp(short,float) | bnp(short,double) | lub(Short,Boolean) |
Short | bnp(Short,long) | bnp(Short,float) | bnp(Short,double) | lub(Short,Boolean) |
char | bnp(char,long) | bnp(char,float) | bnp(char,double) | lub(Character,Boolean) |
Character | bnp(Character,long) | bnp(Character,float) | bnp(Character,double) | lub(Character,Boolean) |
int | bnp(int,long) | bnp(int,float) | bnp(int,double) | lub(Integer,Boolean) |
Integer | bnp(Integer,long) | bnp(Integer,float) | bnp(Integer,double) | lub(Integer,Boolean) |
long | long | bnp(long,float) | bnp(long,double) | lub(Long,Boolean) |
Long | long | bnp(Long,float) | bnp(Long,double) | lub(Long,Boolean) |
float | bnp(float,long) | float | bnp(float,double) | lub(Float,Boolean) |
Float | bnp(Float,long) | float | bnp(Float,double) | lub(Float,Boolean) |
double | bnp(double,long) | bnp(double,float) | double | lub(Double,Boolean) |
Double | bnp(Double,long) | bnp(Double,float) | double | lub(Double,Boolean) |
boolean | lub(Boolean,Long) | lub(Boolean,Float) | lub(Boolean,Double) | boolean |
Boolean | lub(Boolean,Long) | lub(Boolean,Float) | lub(Boolean,Double) | boolean |
null | lub(null,Long) | lub(null,Float) | lub(null,Double) | lub(null,Boolean) |
Object | lub(Object,Long) | lub(Object,Float) | lub(Object,Double) | lub(Object,Boolean) |
Таблица 15.25-C. Тип условного выражения (Операнд 3-й части, Часть I)
| 3-й → | Byte | Short | Character | Integer |
|---|---|---|---|---|
| 2-й ↓ | ||||
byte | byte | short | bnp(byte,Character) | bnp(byte,Integer) |
Byte | Byte | short | bnp(Byte,Character) | bnp(Byte,Integer) |
short | short | short | bnp(short,Character) | bnp(short,Integer) |
Short | short | Short | bnp(Short,Character) | bnp(Short,Integer) |
char | bnp(char,Byte) | bnp(char,Short) | char | bnp(char,Integer) |
Character | bnp(Character,Byte) | bnp(Character,Short) | Character | bnp(Character,Integer) |
int |
byte | bnp(int,Byte) |
short | bnp(int,Short) |
char | bnp(int,Character) | int |
Integer | bnp(Integer,Byte) | bnp(Integer,Short) | bnp(Integer,Character) | Integer |
long | bnp(long,Byte) | bnp(long,Short) | bnp(long,Character) | bnp(long,Integer) |
Long | bnp(Long,Byte) | bnp(Long,Short) | bnp(Long,Character) | bnp(Long,Integer) |
float | bnp(float,Byte) | bnp(float,Short) | bnp(float,Character) | bnp(float,Integer) |
Float | bnp(Float,Byte) | bnp(Float,Short) | bnp(Float,Character) | bnp(Float,Integer) |
double | bnp(double,Byte) | bnp(double,Short) | bnp(double,Character) | bnp(double,Integer) |
Double | bnp(Double,Byte) | bnp(Double,Short) | bnp(Double,Character) | bnp(Double,Integer) |
boolean | lub(Boolean,Byte) | lub(Boolean,Short) | lub(Boolean,Character) | lub(Boolean,Integer) |
Boolean | lub(Boolean,Byte) | lub(Boolean,Short) | lub(Boolean,Character) | lub(Boolean,Integer) |
null | Byte | Short | Character | Integer |
Object | lub(Object,Byte) | lub(Object,Short) | lub(Object,Character) | lub(Object,Integer) |
Таблица 15.25-D. Тип условного выражения (Операнд 3-й аргумент, Часть II)
| 3-й → | Long | Float | Double | Boolean |
|---|---|---|---|---|
| 2-й ↓ | ||||
byte | bnp(byte,Long) | bnp(byte,Float) | bnp(byte,Double) | lub(Byte,Boolean) |
Byte | bnp(Byte,Long) | bnp(Byte,Float) | bnp(Byte,Double) | lub(Byte,Boolean) |
short | bnp(short,Long) | bnp(short,Float) | bnp(short,Double) | lub(Short,Boolean) |
Short | bnp(Short,Long) | bnp(Short,Float) | bnp(Short,Double) | lub(Short,Boolean) |
char | bnp(char,Long) | bnp(char,Float) | bnp(char,Double) | lub(Character,Boolean) |
Character | bnp(Character,Long) | bnp(Character,Float) | bnp(Character,Double) | lub(Character,Boolean) |
int | bnp(int,Long) | bnp(int,Float) | bnp(int,Double) | lub(Integer,Boolean) |
Integer | bnp(Integer,Long) | bnp(Integer,Float) | bnp(Integer,Double) | lub(Integer,Boolean) |
long | long | bnp(long,Float) | bnp(long,Double) | lub(Long,Boolean) |
Long | Long | bnp(Long,Float) | bnp(Long,Double) | lub(Long,Boolean) |
float | bnp(float,Long) | float | bnp(float,Double) | lub(Float,Boolean) |
Float | bnp(Float,Long) | Float | bnp(Float,Double) | lub(Float,Boolean) |
double | bnp(double,Long) | bnp(double,Float) | double | lub(Double,Boolean) |
Double | bnp(Double,Long) | bnp(Double,Float) | Double | lub(Double,Boolean) |
boolean | lub(Boolean,Long) | lub(Boolean,Float) | lub(Boolean,Double) | boolean |
Boolean | lub(Boolean,Long) | lub(Boolean,Float) | lub(Boolean,Double) | Boolean |
null | Long | Float | Double | Boolean |
Object | lub(Object,Long) | lub(Object,Float) | lub(Object,Double) | lub(Object,Boolean) |
Таблица 15.25-E. Тип условного выражения (Операнд 3-го типа, Часть III)
| 3-й → | null | Object |
|---|---|---|
| 2-й ↓ | ||
byte | lub(Byte,null) | lub(Byte,Object) |
Byte | Byte | lub(Byte,Object) |
short | lub(Short,null) | lub(Short,Object) |
Short | Short | lub(Short,Object) |
char | lub(Character,null) | lub(Character,Object) |
Character | Character | lub(Character,Object) |
int | lub(Integer,null) | lub(Integer,Object) |
Integer | Integer | lub(Integer,Object) |
long | lub(Long,null) | lub(Long,Object) |
Long | Long | lub(Long,Object) |
float | lub(Float,null) | lub(Float,Object) |
Float | Float | lub(Float,Object) |
double | lub(Double,null) | lub(Double,Object) |
Double | Double | lub(Double,Object) |
boolean | lub(Boolean,null) | lub(Boolean,Object) |
Boolean | Boolean | lub(Boolean,Object) |
null | null | lub(null,Object) |
Object | Object | Object |
Во время выполнения сначала оценивается выражение первого операнда условного выражения. При необходимости выполняется преобразование unboxing для результата.
Полученное значение boolean затем используется для выбора либо выражения второго, либо третьего операнда:
-
Если значение первого операнда равно
true, то выбирается выражение второго операнда. -
Если значение первого операнда равно
false, то выбирается выражение третьего операнда.
Затем выбранное выражение операнда оценивается, и полученное значение преобразуется к типу условного выражения в соответствии с правилами, изложенными ниже.
Это преобразование может включать преобразования boxing или unboxing (§5.1.7, §5.1.8).
Выражение операнда, не выбранное для оценки, не оценивается для данной конкретной оценки условного выражения.
Булевы условные выражения являются самостоятельными выражениями (§15.2).
Тип булева условного выражения определяется следующим образом:
-
Если второй и третий операнды оба имеют тип
Boolean, то условное выражение имеет типBoolean. -
В противном случае условное выражение имеет тип
boolean.
Числовые условные выражения являются самостоятельными выражениями (§15.2).
Тип числового условного выражения определяется следующим образом:
-
Если второй и третий операнды имеют одинаковый тип, то это и есть тип условного выражения.
-
Если один из вторых и третьих операндов имеет примитивный тип T, а тип другого является результатом применения преобразования boxing (§5.1.7) к T, то тип условного выражения есть T.
-
Если один из операндов имеет тип
byteилиByte, а другой — типshortилиShort, то тип условного выражения естьshort. -
Если один из операндов имеет тип T, где T есть
byte,shortилиchar, а другой операнд — константное выражение (§15.29) типаint, значение которого представимо в типе T, то тип условного выражения есть T. -
Если один из операндов имеет тип T, где T есть
Byte,ShortилиCharacter, а другой операнд — константное выражение типаint, значение которого представимо в типе U, который является результатом применения преобразования unboxing к T, то тип условного выражения есть U. -
В противном случае применяется общее числовое продвижение (§5.6) ко второму и третьему операндам, а тип условного выражения — это продвинутый тип второго и третьего операндов.
Обратите внимание, что числовое продвижение может включать преобразование unboxing (§5.1.8).
Справочное условное выражение является поливыражением, если оно появляется в контексте присваивания или контексте вызова (§5.2. §5.3). В противном случае это автономное выражение.
Когда полисправочное условное выражение появляется в контексте определенного типа с целевым типом T, его второе и третье операндные выражения аналогичным образом появляются в контексте того же типа с целевым типом T.
Полисправочное условное выражение совместимо с целевым типом T, если его второе и третье операндные выражения совместимы с T.
Тип полисправочного условного выражения такой же, как и его целевой тип.
Тип автономного справочного условного выражения определяется следующим образом:
-
Если у второго и третьего операндов один и тот же тип (который может быть типом null), то это и будет типом условного выражения.
-
Если тип одного из второго и третьего операндов — тип null, а тип другого операнда — ссылочный тип, то типом условного выражения будет этот ссылочный тип.
-
В противном случае второй и третий операнды имеют типы S1 и S2 соответственно. Пусть T1 — тип, полученный путем применения преобразования к ящику для 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, не может быть присвоена (если она определенно не неинициализирована (§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), но расширение этого подхода к дженерик-лямбда-выражениям потребовало бы внесения существенных изменений в грамматику.
Формальные параметры лямбда-выражения, если таковые имеются, указываются либо в виде списка в скобках, разделённых запятыми, обычных спецификаторов параметров, либо в виде списка в скобках, разделённых запятыми, кратких спецификаторов параметров, либо в виде одного краткого спецификатора параметра без скобок.
Таким образом, если лямбда-выражение имеет ровно один формальный параметр, он может быть указан в виде списка из одного элемента (int x) или (x), или путём опускания скобок, как в x.
Обычный спецификатор параметра состоит из необязательных модификаторов, типа (или var) и либо идентификатора, либо ключевого слова _ (подчёркивание). Если присутствует идентификатор, он задаёт имя формального параметра. Если присутствует ключевое слово _, к формальному параметру нельзя обратиться по имени.
Краткий спецификатор параметра состоит из идентификатора или ключевого слова _. Если присутствует идентификатор, он задаёт имя формального параметра. Если присутствует ключевое слово _, к формальному параметру нельзя обратиться по имени.
Формальный параметр лямбда-выражения, к которому нельзя обратиться по имени, называется безымянным параметром лямбда-выражения.
Если лямбда-выражение не имеет формальных параметров, то перед -> и телом лямбда-выражения появляется пустая пара скобок.
Следующие правила из §8.4.1, §8.3 и §4.3 представлены здесь для удобства:
Формальный параметр лямбда-выражения может быть объявлен final или аннотирован, только если это указано обычным спецификатором параметра. Если формальный параметр указан кратким спецификатором формального параметра, то формальный параметр не final и не имеет аннотаций.
Формальный параметр лямбда-выражения может быть параметром переменной арности, указанным эллипсом после типа в обычном спецификаторе параметра. Для лямбда-выражения допускается не более одного параметра переменной арности. Если параметр переменной арности появляется где-либо в списке обычных спецификаторов параметров, кроме последнего места, это ошибка компиляции.
Каждый формальный параметр лямбда-выражения имеет либо выведенный тип, либо объявленный тип:
-
Если формальный параметр указан либо обычным спецификатором параметра, использующим
var, либо кратким спецификатором параметра, то формальный параметр имеет выведенный тип. Тип выводится из типа функционального интерфейса, на который направлено лямбда-выражение (§15.27.3). -
Если формальный параметр указан обычным спецификатором параметра, не использующим
var, то формальный параметр имеет объявленный тип. Объявленный тип определяется следующим образом:-
Если формальный параметр не является параметром переменной арности, то объявленный тип обозначается ТипБезАннотаций, если в ТипБезАннотаций и ИдентификаторДекларатораПеременной нет пар скобок, и задаётся §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 появляется более одного раза в объявлении формального параметра, это ошибка компиляции.
Если ТипПараметраЛямбда формального параметра является var, а ИдентификаторДекларатораПеременной того же формального параметра имеет одну или несколько пар скобок, это ошибка компиляции.
Область действия и перекрытие формального параметра указаны в §6.3 и §6.4.
Ссылки на формальный параметр из вложенного класса или интерфейса, или вложенного лямбда-выражения, ограничены, как указано в §6.5.6.1.
Объявление двух формальных параметров лямбда-выражения с одинаковым именем является ошибкой компиляции. (То есть, в их объявлениях упоминается один и тот же Идентификатор.)
Обратите внимание, что, в отличие от этого, возможно объявить более одного безымянного параметра лямбда-выражения, например, (_, _) -> BODY или (String _, int _) -> BODY.
Если формальный параметр, который объявлен final, присваивается внутри тела лямбда-выражения, то это ошибка времени компиляции.
Когда лямбда-выражение вызывается (через выражение вызова метода (§15.12)), значения выражений фактических аргументов инициализируют вновь созданные переменные параметров, каждая из которых имеет объявленный или выведенный тип, перед выполнением тела лямбда-выражения. Идентификатор, который появляется в NormalLambdaParameter или ConciseLambdaParameter, может использоваться как простое имя в теле лямбда-выражения для ссылки на формальный параметр.
Тело лямбда-выражения — это либо единственное выражение, либо блок (§14.2). Как и тело метода, тело лямбда-выражения описывает код, который будет выполняться при каждом вызове.
В отличие от кода, появляющегося в объявлениях анонимных классов, смысл имен и this и super ключевых слов, появляющихся в теле лямбда-выражения, а также доступность ссылочных объявлений такие же, как и в окружающем контексте (за исключением того, что параметры лямбда-выражения могут вводить новые имена).
Прозрачность this (как явного, так и неявного) в теле лямбда-выражения — то есть, его обработка так же, как и в окружающем контексте — обеспечивает большую гибкость для реализаций и предотвращает зависимость смысла неопределенных имен в теле от разрешения перегрузки.
Практически, лямбда-выражениям редко нужно ссылаться на себя (либо для рекурсивного вызова, либо для вызова других методов), в то время как чаще требуется использование имен для ссылки на элементы в окружающем классе, которые в противном случае могут быть скрыты (this, toString()). Если лямбда-выражению необходимо сослаться на себя (как будто через this), то вместо этого следует использовать ссылку на метод или анонимный внутренний класс.
Тело лямбда-выражения с блоком совместимо с void, если каждое оператор `return` в блоке имеет вид return;.
Тело лямбда-выражения с блоком совместимо со значением, если оно не может завершиться нормально (§14.22) и каждое оператор `return` в блоке имеет вид return Выражение;.
Если тело лямбда-выражения с блоком не совместимо ни с void, ни со значением, то это ошибка времени компиляции.
В теле лямбда-выражения с блоком, совместимом со значением, результатные выражения — это любые выражения, которые могут произвести значение вызова. В частности, для каждого оператора вида return Выражение ;, содержащегося в теле, Выражение является результатным выражением.
Следующие тела лямбда-выражений совместимы с void:
()->{} ()->{ System.out.println("done"); }
Эти совместимы со значением:
()->{ return "done"; } ()->{ if (...) return 1; else return 0; }
Эти оба:
()->{ throw new RuntimeException(); } ()->{ while (true); }
Это ни то, ни другое:
() -> { if (...) return "done"; System.out.println("done"); }
Обработка совместимости с void/значением и смысл имен в теле совместно служат для минимизации зависимости от конкретного целевого типа в заданном контексте, что полезно как для реализаций, так и для понимания программиста. В то время как выражения могут иметь разные типы при разрешении перегрузки в зависимости от целевого типа, смысл неопределенных имен и основная структура тела лямбда-выражения не меняются.
Обратите внимание, что определение совместимости с void/значением не является строго структурным свойством: «может завершиться нормально» зависит от значений константных выражений, и эти значения могут включать имена, ссылающиеся на константные переменные.
Любая локальная переменная, формальный параметр или параметр исключения, используемые, но не объявленные в лямбда-выражении, должны быть либо final, либо эффективно постоянными (§4.12.4), как указано в §6.5.6.1.
Любая локальная переменная, используемая, но не объявленная в теле лямбда-выражения, должна быть определённо присвоена (§16 (Определённое присваивание)) перед телом лямбда-выражения, в противном случае произойдет ошибка компиляции.
Аналогичные правила использования переменных применяются в теле внутреннего класса (§8.1.3). Ограничение на эффективно постоянные переменные запрещает доступ к динамически изменяющимся локальным переменным, чьё захват, вероятно, введёт проблемы с конкурентностью. По сравнению с ограничением final, это снижает административную нагрузку на программистов.
Ограничение на эффективно постоянные переменные включает стандартные переменные циклов, но не переменные улучшенных for циклов, которые рассматриваются как отдельные для каждой итерации цикла (§14.14.2).
Следующие тела лямбда-выражений демонстрируют использование эффективно постоянных переменных.
void m1(int x) {
int y = 1;
foo(() -> x+y);
// Legal: x and y are both effectively final.
}
void m2(int x) {
int y;
y = 1;
foo(() -> x+y);
// Legal: x and y are both effectively final.
}
void m3(int x) {
int y;
if (...) y = 1;
foo(() -> x+y);
// Illegal: y is effectively final, but not definitely assigned.
}
void m4(int x) {
int y;
if (...) y = 1; else y = 2;
foo(() -> x+y);
// Legal: x and y are both effectively final.
}
void m5(int x) {
int y;
if (...) y = 1;
y = 2;
foo(() -> x+y);
// Illegal: y is not effectively final.
}
void m6(int x) {
foo(() -> x+1);
x++;
// Illegal: x is not effectively final.
}
void m7(int x) {
foo(() -> x=1);
// Illegal: x is not effectively final.
}
void m8() {
int y;
foo(() -> y=1);
// Illegal: y is not definitely assigned before the lambda.
}
void m9(String[] arr) {
for (String s : arr) {
foo(() -> s);
// Legal: s is effectively final
// (it is a new variable on each iteration)
}
}
void m10(String[] arr) {
for (int i = 0; i < arr.length; i++) {
foo(() -> arr[i]);
// Illegal: i is not effectively final
// (it is not final, and is incremented)
}
}
Лямбда-выражение совместимо в контексте присваивания, вызова или приведения типов с целевым типом T, если T является функциональным интерфейсом (§9.8) и выражение совместимо с типом функции основного целевого типа, полученного из T.
Основной целевой тип выводится из T следующим образом:
-
Если T является функциональным интерфейсом с параметризацией с подстановкой и лямбда-выражение явно типизировано, то основной целевой тип определяется, как описано в §18.5.3.
-
Если T является функциональным интерфейсом с параметризацией с подстановкой и лямбда-выражение неявно типизировано, то основной целевой тип — это параметризация T без подстановок (§9.9).
-
В противном случае, основной целевой тип — T.
Лямбда-выражение совместимо с типом функции, если выполняются все следующие условия:
-
Тип функции не имеет параметров типа.
-
Количество параметров лямбда-выражения равно количеству типов параметров типа функции.
-
Если лямбда-выражение явно типизировано, его формальные типы параметров совпадают с типами параметров типа функции.
-
Если предполагается, что параметры лямбда-выражения имеют те же типы, что и типы параметров типа функции, то:
-
Если результат типа функции —
void, тело лямбда-выражения — это выражение-оператор (§14.8) или блок, совместимый сvoid. -
Если результат типа функции — тип R (не
void), то либо (i) тело лямбда-выражения — это выражение, совместимое с R в контексте присваивания, либо (ii) тело лямбда-выражения — это блок, совместимый по значению, и каждое выражение результата (§15.27.2) совместимо с R в контексте присваивания.
-
Если лямбда-выражение совместимо с целевым типом T, то тип выражения, U, — это основной целевой тип, полученный из T.
Ошибка компиляции, если какой-либо класс или интерфейс, упомянутый в U или в типе функции U, недоступен (§6.6) из класса или интерфейса, в котором находится лямбда-выражение.
Для каждого метода-члена m в U, не являющегося static, если тип функции U имеет подсигнатуру сигнатуры m, то предполагаемый метод с типом метода, который является типом функции U, считается переопределяющим m, и может произойти ошибка компиляции или предупреждение о неявном типе, указанное в §8.4.8.3.
Проверяемое исключение, которое может быть брошено в теле лямбда-выражения, может вызвать ошибку компиляции, как указано в §11.2.3.
Типы параметров явно типизированных лямбда-выражений должны точно совпадать с типами параметров типа функции. Хотя было бы возможно быть более гибкими — например, разрешить упаковку или контравариантность, — такая общность кажется излишней и не согласуется со способом работы переопределения в объявлениях классов. Программист должен точно знать, какой тип функции нацелен при написании лямбда-выражения, а значит, точно знать, какую сигнатуру нужно переопределить. (Это не относится к ссылкам на методы, поэтому при их использовании допускается большая гибкость.) Кроме того, большая гибкость с типами параметров увеличит сложность вывода типов и разрешения перегрузки.
Обратите внимание, что, хотя упаковка недопустима в строгом контексте вызова, упаковка выражений результата лямбда-выражений всегда разрешена — то есть, выражение результата появляется в контексте присваивания, независимо от контекста, окружающего лямбда-выражение. Однако, если явно типизированное лямбда-выражение является аргументом перегруженного метода, сигнатура метода, избегающая упаковки или распаковки результата лямбда-выражения, предпочтительнее по проверке наиболее специфического метода (§15.12.2.5).
Если тело лямбда-выражения — это выражение-оператор (то есть выражение, которое может стоять самостоятельно как оператор), оно совместимо с типом функции, производящей void; любой результат просто отбрасывается. Например, оба приведенных ниже примера допустимы:
// Predicate has abooleanresult java.util.function.Predicate<String> p = s->list.add(s); // Consumer has avoidresult java.util.function.Consumer<String> c = s->list.add(s);
В общем случае лямбда-выражение вида () -> expr, где expr — выражение-оператор, интерпретируется либо как () -> { return expr; }, либо как () -> { expr; }, в зависимости от целевого типа.
При выполнении вычисление лямбда-выражения аналогично вычислению выражения создания экземпляра класса, поскольку нормальное завершение приводит к ссылке на объект. Вычисление лямбда-выражения отличается от выполнения тела лямбда-выражения.
Создается и инициализируется новый экземпляр класса с указанными ниже свойствами или используется существующий экземпляр класса с этими свойствами. Если необходимо создать новый экземпляр, но места для выделения объекта недостаточно, вычисление лямбда-выражения прерывается сбросом исключения OutOfMemoryError.
Это означает, что идентификатор результата вычисления лямбда-выражения (или сериализации и десериализации лямбда-выражения) непредсказуем, а поэтому операции, чувствительные к идентификатору (такие как операторы равенства ссылок (§15.21.3), блокировка объектов (§14.19) и метод System.identityHashCode) могут давать разные результаты в разных реализациях языка программирования Java, или даже при различных вычислениях лямбда-выражений в одной реализации.
Значение лямбда-выражения — ссылка на экземпляр класса с следующими свойствами:
-
Класс реализует целевой функциональный интерфейс и, если целевой тип является пересечением типов, все другие указанные в пересечении типы интерфейсов.
-
Если лямбда-выражение имеет тип U, для каждого не-
staticметода-членаmтипа U:Если функциональный тип U имеет подсигнатуру сигнатуры
m, то класс объявляет метод, который переопределяетm. Тело метода выполняет вычисление тела лямбда-выражения, если это выражение, или выполняет тело лямбда-выражения, если это блок; если ожидается результат, он возвращается методом.Если стирание типа переопределяемого метода отличается по сигнатуре от стирания функционального типа U, то перед вычислением или выполнением тела лямбда-выражения тело метода проверяет, что каждое значение аргумента является экземпляром подкласса или подинтерфейса стирания соответствующего типа параметра в функциональном типе U; если нет, выбрасывается исключение
ClassCastException. -
Класс не переопределяет другие методы целевого функционального типа интерфейса или других указанных выше типов интерфейсов, хотя он может переопределять методы класса
Object.
Эти правила предназначены для гибкости при реализации языка программирования Java, в том числе:
-
При каждом вычислении не обязательно выделять новый объект.
-
Объекты, созданные разными лямбда-выражениями, не обязательно должны принадлежать разным классам (если тела одинаковы, например).
-
Каждый объект, созданный в результате вычисления, не обязательно должен принадлежать одному и тому же классу (локальные переменные, например, могут быть встроены).
-
Если доступен «существующий экземпляр», он не обязательно должен был быть создан при предыдущем вычислении лямбда-выражения (например, он мог быть выделен во время инициализации содержащего класса).
Если целевой функциональный тип интерфейса является подтипом java.io.Serializable, результирующий объект автоматически станет экземпляром сериализуемого класса. Предоставление сериализуемости объекта, полученного из лямбда-выражения, может иметь дополнительные затраты во время выполнения и последствия для безопасности, поэтому объекты, полученные из лямбда-выражений, не обязаны быть сериализуемыми «по умолчанию».
Выражение switch передает управление одному из нескольких операторов или выражений в зависимости от значения выражения; все возможные значения этого выражения должны быть обработаны, и все несколько операторов и выражений должны производить значение для результата выражения switch.
Выражение Expression называется выражением-селектором. Тип выражения-селектора должен быть char, byte, short, int или типом ссылки, в противном случае возникает ошибка компиляции.
Тело как выражения switch, так и оператора switch (§14.11) называется блоком switch. Общие правила, которые применяются ко всем блокам switch, независимо от того, появляются ли они в выражениях switch или операторах switch, приведены в §14.11.1. Следующие производства из §14.11.1 приведены здесь для удобства:
case Константа case {, Константа case} case null [, default] case Образец case {, Образец case} [Условие] default when Выражение Помимо общих правил для блоков switch (§14.11.1), существуют дополнительные правила для блоков switch в выражениях switch.
Ошибка компиляции, если блок switch выражения switch состоит из правил switch, но один или несколько блоков правил switch могут завершиться нормально (§14.22).
Ошибка компиляции, если блок switch выражения switch состоит из групп операторов с метками switch, но последний оператор в блоке switch может завершиться нормально или блок switch имеет одну или несколько меток switch после последней группы операторов с метками switch.
Ошибка компиляции, если выражение switch не является исчерпывающим (§14.11.1.1).
Выражения switch отличаются от операторов switch по типу выражений, которые могут находиться справа от стрелки (->) в блоке switch, то есть выражения, которые могут использоваться как выражения правил switch. В выражении switch любое выражение может использоваться как выражение правила switch, но в операторе 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применяется преобразование разыменования (§5.1.8), и выражениеswitchимеет типboolean. -
В противном случае, если тип каждого выражения-результата преобразуем к числовому типу (§5.1.8), то тип выражения
switchявляется результатом общего числового преобразования (§5.6) примененного к выражениям-результатам. -
В противном случае к каждому выражению-результату с примитивным типом применяется преобразование упаковки (§5.1.7), после чего тип выражения
switchявляется результатом применения преобразования захвата (§5.1.10) к наименьшему общему знаменателю (§4.10.4) типов выражений-результатов.
Выражение switch оценивается путем сначала оценки выражения селектора. Если оценка выражения селектора завершается внезапно, то оценка всего выражения switch завершается внезапно по той же причине.
Если оценка выражения селектора завершается нормально, то оценка выражения switch продолжается путем определения, применяется ли метка switch, связанная с блоком switch, к значению выражения селектора (§14.11.1.2). Затем:
-
Если процесс определения того, какая метка switch применяется, завершается внезапно, то все выражение
switchзавершается внезапно по той же причине. -
Если ни одна метка switch не применяется, то выполняется одно из следующих условий:
-
Если значение выражения селектора равно
null, то возникаетNullPointerException, и оценка выраженияswitchзавершается внезапно по этой причине. -
В противном случае возникает
MatchException, и оценка выраженияswitchзавершается внезапно по этой причине.
-
-
Если метка switch применяется, то выполняется одно из следующих условий:
-
Если это метка switch для выражения правила switch, то выражение оценивается. Если результатом оценки является значение, то выражение
switchзавершается нормально с тем же значением. -
Если это метка switch для блока правила switch, то блок выполняется. Если этот блок завершается нормально, то выражение
switchзавершается нормально. -
Если это метка switch для оператора
throwправила switch, то выполняется оператор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) вида TypeName
.Identifier, которые относятся к константным переменным (§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.