Spec-Zone.ru › Java Language Specification 21

Глава 15. Выражения

Содержание

15.1. Оценка, Обозначение и Результат
15.2. Формы Выражений
15.3. Тип Выражения
15.4. Выражения с Плавающей Точкой
15.5. Выражения и Проверки во Время Выполнения
15.6. Нормальное и Прерывистое Завершение Оценки
15.7. Порядок Оценки
15.7.1. Сначала Оценить Левый Операнд
15.7.2. Оценить Операторы Перед Операцией
15.7.3. Оценка Учитывает Скобки и Приоритет
15.7.4. Списки Аргументов Оцениваются Слева Направо
15.7.5. Порядок Оценки для Других Выражений
15.8. Первичные Выражения
15.8.1. Лексические Литералы
15.8.2. Литералы Классов
15.8.3. this
15.8.4. Квалифицированные this
15.8.5. Выражения в Скобках
15.9. Выражения для Создание Экземпляров Классов
15.9.1. Определение Класса, Подлежащего Инициализации
15.9.2. Определение Окружающих Экземпляров
15.9.3. Выбор Конструктора и Его Аргументов
15.9.4. Вычисление Выражений для Создание Экземпляров Классов во Время Выполнения
15.9.5. Объявления Анонимных Классов
15.9.5.1. Анонимные Конструкторы
15.10. Выражения для Создание и Доступа к Массивам
15.10.1. Выражения для Создание Массивов
15.10.2. Вычисление Выражений для Создание Массивов во Время Выполнения
15.10.3. Выражения для Доступа к Элементам Массива
15.10.4. Вычисление Выражений для Доступа к Элементам Массива во Время Выполнения
15.11. Выражения для Доступа к Полям
15.11.1. Доступ к Полям Используя Первичное Выражение
15.11.2. Доступ к Членам Предков Используя super
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.12.4.1. Вычисление Целевой Ссыли (При Необходимости)
15.12.4.2. Оценить Аргументы
15.12.4.3. Проверить Доступность Типа и Метода
15.12.4.4. Найти Вызываемый Метод
15.12.4.5. Создать Фрейм, Синхронизироваться, Передать Управление
15.13. Выражения для Ссылок на Методы
15.13.1. Декларация Ссылки на Метод во Время Компиляции
15.13.2. Тип Ссылки на Метод
15.13.3. Вычисление Ссылок на Методы во Время Выполнения
15.14. Постфиксные Выражения
15.14.1. Имена Выражений
15.14.2. Постфиксный Оператор Инкремента ++
15.14.3. Постфиксный Оператор Декремента --
15.15. Унарные Операторы
15.15.1. Префиксный Оператор Инкремента ++
15.15.2. Префиксный Оператор Декремента --
15.15.3. Унарный Плюс Оператор +
15.15.4. Унарный Минус Оператор -
15.15.5. Битовый Дополнение Оператор ~
15.15.6. Логическое Дополнение Оператор !
15.16. Выражения Приведения Типа
15.17. Мультипликативные Операторы
15.17.1. Оператор Умножения *
15.17.2. Оператор Деления /
15.17.3. Оператор Остатка %
15.18. Аддитивные Операторы
15.18.1. Оператор Конкатенации Строк +
15.18.2. Аддитивные Операторы (+ и -) для Числовых Типов
15.19. Операторы Сдвига
15.20. Операторы Сравнения
15.20.1. Операторы Числового Сравнения <, <=, > и >=
15.20.2. Оператор instanceof
15.21. Операторы Равенства
15.21.1. Операторы Числового Равенства == и !=
15.21.2. Операторы Булевого Равенства == и !=
15.21.3. Операторы Ссылочного Равенства == и !=
15.22. Битовые и Логические Операторы
15.22.1. Битовые Операторы Целых Чисел &, ^ и |
15.22.2. Логические Операторы Булевых Значений &, ^ и |
15.23. Оператор Условного И &&
15.24. Оператор Условного ИЛИ ||
15.25. Условный Оператор ? :
15.25.1. Булевые Условные Выражения
15.25.2. Числовые Условные Выражения
15.25.3. Ссылочные Условные Выражения
15.26. Операторы Присваивания
15.26.1. Оператор Простого Присваивания =
15.26.2. Операторы Сложного Присваивания
15.27. Лямбда-Выражения
END_OF_DOCUMENT_MARKER
15.27.1. Параметры лямбда-выражения
15.27.2. Тело лямбда-выражения
15.27.3. Тип лямбда-выражения
15.27.4. Вычисление лямбда-выражений во время выполнения
15.28. switch Выражения
15.28.1. Блок оператора switch switch выражения
15.28.2. Вычисление switch выражений во время выполнения
15.29. Постоянные выражения

Большая часть работы в программе выполняется путём вычисления выражений, либо для их побочных эффектов, таких как присваивание переменным, либо для их значений, которые могут быть использованы в качестве аргументов или операндов в более крупных выражениях, либо для влияния на последовательность выполнения в операторах, либо и для того, и для другого.

В этом разделе описываются значения выражений и правила их вычисления.

15.1. Вычисление, обозначение и результат

Когда выражение в программе вычисляется (исполняется), результат обозначает одно из трёх:

  • Переменная (§4.12) (в C это называлось бы lvalue)

  • Значение (§4.2, §4.3)

  • Ничего (выражение считается пустым)

Если выражение обозначает переменную, и требуется значение для дальнейшего вычисления, то используется значение этой переменной. В этом контексте, если выражение обозначает переменную или значение, мы можем говорить просто о значении выражения.

Выражение обозначает ничего тогда и только тогда, когда это вызов метода (§15.12), который вызывает метод, не возвращающий значение, то есть метод объявленный void (§8.4). Такое выражение может быть использовано только как оператор выражения (§14.8) или как единственное выражение тела лямбда-выражения (§15.27.2), поскольку любой другой контекст, в котором может появиться выражение, требует, чтобы выражение обозначало что-то. Оператор выражения или тело лямбда-выражения, являющееся вызовом метода, также может вызывать метод, который производит результат; в этом случае возвращаемое значение метода отбрасывается.

Вычисление выражения может иметь побочные эффекты, поскольку выражения могут содержать вложенные присваивания, операторы инкремента, операторы декремента, вызовы методов и, в switch выражениях, произвольные операторы.

Выражение встречается в одном из следующих случаев:

  • Объявление некоторого класса или интерфейса, который объявляется: в инициализаторе поля, в статическом инициализаторе, в инициализаторе экземпляра, в объявлении конструктора, в объявлении метода или в аннотации.

  • Аннотация на объявлении модуля, пакета или верхнего уровня класса или интерфейса.

15.2. Формы выражений

Выражения можно грубо разделить на следующие синтаксические формы:

  • Имена выражений (§6.5.6)

  • Первичные выражения (§15.8 - §15.13)

  • Выражения с унарными операторами (§15.14 - §15.16)

  • Выражения с бинарными операторами (§15.17 - §15.24, и §15.26)

  • Выражения с тернарным оператором (§15.25)

  • Лямбда-выражения (§15.27)

  • switch выражения (§15.28)

Преимущество операторов управляется иерархией грамматических правил. Оператор с наименьшим приоритетом — стрелка лямбда-выражения (->), за ним следуют операторы присваивания. Таким образом, все выражения синтаксически включены в LambdaExpression и AssignmentExpression нетерминалы:

Выражение:
LambdaExpression
AssignmentExpression

Когда некоторые выражения появляются в определённых контекстах, они считаются поли-выражениями. Следующие формы выражений могут быть поли-выражениями:

  • Выражения в скобках (§15.8.5)

  • Выражения создания экземпляра класса (§15.9)

  • Выражения вызова метода (§15.12)

  • Выражения ссылки на метод (§15.13)

  • Условные выражения (§15.25)

  • Лямбда-выражения (§15.27)

  • switch выражения (§15.28)

Правила, определяющие, является ли выражение одного из этих видов поли-выражением, даны в отдельных разделах, определяющих эти формы выражений.

Выражения, которые не являются поли-выражениями, являются самостоятельными выражениями. Самостоятельные выражения — это выражения вышеупомянутых видов, когда они определены не как поли-выражения, а также все выражения всех других видов. Выражения всех других форм называются самостоятельной формой.

Некоторые выражения имеют значение, которое может быть определено на этапе компиляции. Это постоянные выражения (§15.29).

15.3. Тип выражения

Если выражение обозначает переменную или значение, то выражение имеет тип, известный во время компиляции. Тип автономного выражения может быть определён полностью по содержимому выражения; в отличие от этого, тип полиморфного выражения может зависеть от целевого типа выражения (§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.

15.4. Вещественные выражения

Вещественное выражение — это выражение, тип которого является float или double (§4.2.3). Вещественные выражения типа float обозначают значения, которые точно соответствуют значениям, представимым в 32-битном формате IEEE 754 binary32. Вещественные выражения типа double обозначают значения, которые точно соответствуют значениям, представимым в 64-битном формате IEEE 754 binary64.

Многие операторы сравнения и числовые операторы языка программирования Java, которые могут быть использованы для построения вещественных выражений, соответствуют операциям IEEE 754, как и преобразования, которые действуют на значения с плавающей запятой (Таблица 15.4-A).

Таблица 15.4-A. Соответствие операциям IEEE 754

Оператор/Преобразование Операция IEEE 754
Числовые операторы сравнения <, <=, > и >= (§15.20.1) compareQuietLess, compareQuietLessEqual, compareQuietGreater, compareQuietGreaterEqual
Числовые операторы равенства == и != (§15.21.1) compareQuietEqual, compareQuietNotEqual
Унарный оператор минус - (§15.15.4) negate
Мультипликативные операторы * и / (§15.17.1, §15.17.2) умножение, деление
Аддитивные операторы + и - (§15.18.2) сложение, вычитание
Расширяющее преобразование примитивного типа из целочисленного типа (§5.1.2) convertFromInt
Сужающее преобразование примитивного типа к целочисленному типу (§5.1.3) convertToIntegerTowardZero
Преобразование между float и double convertFormat

Оператор остатка от деления для чисел с плавающей запятой % (§15.17.3) не соответствует операции остатка от деления IEEE 754.

Некоторые операции IEEE 754 без соответствующих операторов в языке программирования Java предоставляются через методы в классах Math и StrictMath, включая метод sqrt для операции извлечения квадратного корня IEEE 754, метод fma для операции IEEE 754 fusedMultiplyAdd и метод IEEEremainder для операции остатка от деления IEEE 754.

Язык программирования Java требует поддержки чисел с плавающей запятой strictfp IEEE 754 и strictfp, что облегчает доказательство желательных свойств конкретных числовых алгоритмов. Операции с плавающей запятой не «обнуляются» при подсчете результатов, если полученное значение — float.

Результат оператора с плавающей запятой языка программирования Java должен соответствовать результату соответствующей операции IEEE 754 над теми же операндами. Для конечных результатов это подразумевает, что знак, мантисса и порядок вещественного результата должны быть такими, как указано в IEEE 754.

Требование соответствия знака, мантиссы и порядка исключает некоторые преобразования, которые могли бы быть разрешены, если бы поведение чисел с плавающей запятой было менее точно определено. Например, -x обычно не может быть заменено на (0.0 - x), потому что знак результата будет отличаться, если x является -0.0. Кроме того, другие, возможно, изменяющие значение преобразования, такие как замена (a * b + c) вызовом методу библиотеки fused multiply-accumulate, не допускаются, если результат не может быть доказан как идентичный.

Нет обстоятельств, когда при вычислении вещественного выражения могут быть использованы промежуточные результаты, имеющие большую точность или больший диапазон порядка, чем указано типом выражения.

Операция с плавающей запятой, которая приводит к переполнению, производит бесконечность со знаком.

Операция с плавающей запятой, которая приводит к потере точности, производит поднормальное значение или ноль со знаком.

Операция с плавающей запятой, для которой нет единственного математически определенного результата, производит NaN.

Все числовые операции с NaN в качестве операнда дают NaN в качестве результата.

Поскольку NaN не упорядочен, любая числовая операция сравнения, включающая один или два NaN, возвращает false, любая операция == сравнения, включающая NaN, возвращает false, и любая операция != сравнения, включающая NaN, возвращает true.

Арифметика с плавающей запятой является приближением к действительной арифметике. Хотя существует бесконечное количество вещественных чисел, в конкретном формате с плавающей запятой имеется только конечное число значений. В языке программирования Java правило округления — это функция, используемая для отображения действительного числа в значение с плавающей запятой в заданном формате. Для действительных чисел в диапазоне, представимом в формате с плавающей запятой, непрерывный отрезок числовой оси отображается в одно значение с плавающей запятой. Действительное число, значение которого численно равно значению с плавающей запятой, отображается в это значение с плавающей запятой; например, действительное число 1,5 отображается в значение с плавающей запятой 1,5 в заданном формате. Язык программирования Java определяет два правила округления следующим образом:

  • Правило округления до ближайшего значения применяется ко всем операторам с плавающей запятой, за исключением (i) преобразования в целое значение и (ii) остатка от деления с плавающей запятой. Согласно правилу округления до ближайшего значения, неточные результаты должны быть округлены до ближайшего представимого значения к бесконечно точному результату; если два ближайших представимых значения находятся на одинаковом расстоянии, выбирается значение, у которого младший бит равен нулю.

    Правило округления до ближайшего значения соответствует атрибуту направления округления по умолчанию для двоичной арифметики в IEEE 754, roundTiesToEven.

    Атрибут направления округления roundTiesToEven был известен как режим округления «до ближайшего» в версии стандарта IEEE 754 1985 года. Правило округления в языке программирования Java названо в честь этого режима округления.

  • Правило округления к нулю применяется к (i) преобразованию значения с плавающей запятой в целое значение (§5.1.3) и (ii) остатку от деления с плавающей запятой (§15.17.3). Согласно правилу округления к нулю, неточные результаты округляются до ближайшего представимого значения, которое не больше по модулю, чем бесконечно точный результат. Для преобразования в целое число правило округления к нулю эквивалентно усечению, где дробные биты мантиссы отбрасываются.

    Правило округления к нулю соответствует атрибуту направления округления roundTowardZero для двоичной арифметики в IEEE 754.

    Атрибут направления округления roundTowardZero был известен как режим округления «к нулю» в версии стандарта IEEE 754 1985 года. Правило округления в языке программирования Java названо в честь этого режима округления.

Язык программирования Java требует, чтобы каждый оператор с плавающей запятой округлял свой результат до точности результата. Правило округления, используемое для каждого оператора с плавающей запятой, — это либо округление до ближайшего, либо округление к нулю, как указано выше.

Java 1.0 и 1.1 требовали строгого вычисления вещественных выражений. Строгое вычисление означает, что каждый float операнд соответствует значению, представимому в формате IEEE 754 binary32, каждый double операнд соответствует значению, представимому в формате IEEE 754 binary64, и каждый оператор с плавающей запятой с соответствующей операцией IEEE 754 соответствует результату IEEE 754 для тех же операндов.

Строгое вычисление обеспечивает предсказуемые результаты, но вызывало проблемы с производительностью в реализациях Java Virtual Machine для некоторых семейств процессоров, распространенных в эпоху Java 1.0/1.1. Поэтому в Java 1.2 и до Java SE 16 платформа Java SE разрешила реализации Java Virtual Machine иметь один или два набора значений, связанные с каждым типом с плавающей запятой. Тип float был связан с набором значений float и набором значений float с расширенным порядком, а тип double был связан с набором значений double и набором значений double с расширенным порядком. Набор значений float соответствовал значениям, представимым в формате IEEE 754 binary32; набор значений float с расширенным порядком имел то же количество битов точности, но больший диапазон порядка. Аналогично, набор значений double соответствовал значениям, представимым в формате IEEE 754 binary64; набор значений double с расширенным порядком имел то же количество битов точности, но больший диапазон порядка. Разрешение по умолчанию использования наборов значений с расширенным порядком смягчило проблемы производительности на некоторых семействах процессоров.

Для совместимости, Java 1.2 разрешало программисту запретить использованию расширенных наборов значений показателя степени. Программист выражал это, помещая модификатор strictfp на объявление класса, интерфейса или метода. strictfp ограничивало семантику с плавающей точкой любых вложенных выражений, используя набор значений типа float для float выражений и набор значений типа double для double выражений, гарантируя, что результаты таких выражений были полностью предсказуемы. Код, измененный с помощью strictfp, таким образом, имел ту же семантику с плавающей точкой, что и в Java 1.0 и 1.1.

В Java SE 17 и более поздних версиях платформа Java SE всегда требует строгой оценки выражений с плавающей точкой. Более новые члены семейств процессоров, у которых были проблемы с производительностью при реализации строгой оценки, больше не сталкиваются с этой трудностью. Это спецификация больше не связывает float и double с четырьмя наборами значений, описанными выше, и модификатор strictfp больше не влияет на оценку выражений с плавающей точкой. Для совместимости strictfp остается ключевым словом в Java SE 21 (§3.8) и продолжает иметь ограничения на его использование (§8.4.3, §9.4), хотя компиляторы Java рекомендуются предупреждать программиста о его устаревшем статусе. Будущие версии языка программирования Java могут переопределить или удалить ключевое слово strictfp.

15.5. Выражения и проверки во время выполнения

Если тип выражения — примитивный тип, то значение выражения — того же примитивного типа.

Если тип выражения — ссылочный тип, то класс ссылаемого объекта, или даже является ли значение ссылкой на объект, а не null, не обязательно известен во время компиляции. Есть несколько мест в языке программирования Java, где фактический класс ссылаемого объекта влияет на выполнение программы способом, который нельзя вывести из типа выражения. Они таковы:

  • Вызов метода (§15.12). Конкретный метод, используемый для вызова o.m(...), выбирается на основе методов, которые являются частью класса или интерфейса, который является типом o. Для методов экземпляра класс объекта, на который ссылается фактическое значение o, участвует, потому что подкласс может переопределить определенный метод, уже объявленный в родительском классе, так что вызывается этот переопределенный метод. (Переопределяющий метод может или не может выбрать дальнейшее вызов исходного переопределенного m метода.)

  • Оператор instanceof (§15.20.2). Выражение, тип которого является ссылочным типом, может быть проверено с помощью instanceof, чтобы узнать, может ли класс объекта, на который ссылается фактическое значение выражения, быть преобразован в другой ссылочный тип.

  • Приведение типов (§15.16). Класс объекта, на который ссылается фактическое значение операнда выражения, может не быть совместим с типом, указанным оператором приведения. Для ссылочных типов это может потребовать проверки во время выполнения, которая генерирует исключение, если класс ссылаемого объекта, как определяется во время выполнения, не может быть преобразован в целевой тип.

  • Присвоение компоненту массива ссылочного типа (§10.5, §15.13, §15.26.1). Правила проверки типов позволяют обрабатывать тип массива S[] как подтип T[], если S является подтипом T, но это требует проверки во время выполнения для присваивания компоненту массива, подобной проверке, выполняемой для приведения типов.

  • Обработка исключений (§14.20). Исключение перехватывается блоком catch только в том случае, если класс объекта, выбросившего исключение, является подклассом типа формального параметра блока catch.

Ситуации, когда класс объекта не известен статически, могут привести к ошибкам типа во время выполнения.

Кроме того, существуют ситуации, когда статически известный тип может быть неточным во время выполнения. Такие ситуации могут возникнуть в программе, которая порождает предупреждения о не проверенных во время компиляции типах. Такие предупреждения выдаются в ответ на операции, которые не могут быть гарантированы как безопасные статически и не могут быть немедленно подвергнуты динамической проверке, потому что они включают нереализуемые типы (§4.7). В результате динамические проверки позже в ходе выполнения программы могут обнаружить несоответствия и привести к ошибкам типа во время выполнения.

Ошибка типа во время выполнения может возникнуть только в этих ситуациях:

  • При приведении типов, когда фактический класс объекта, на который ссылается значение выражения-операнда, не совместим с целевым типом, указанным оператором приведения (§5.5, §15.16); в этом случае выбрасывается ClassCastException.

  • При автоматически сгенерированном приведении типов, введенном для обеспечения корректности операции с нереализуемым типом (§4.7).

  • При присваивании компоненту массива ссылочного типа, когда фактический класс объекта, на который ссылается значение, которое должно быть присвоено, не совместим с фактическим типом компонента массива во время выполнения (§10.5, §15.13, §15.26.1); в этом случае выбрасывается ArrayStoreException.

  • Когда исключение не перехватывается никаким блоком catch оператора try (§14.20); в этом случае поток управления, который столкнулся с исключением первым, пытается вызвать обработчик необработанных исключений (§11.3), а затем завершается.

15.6. Нормальное и прерывистое завершение вычисления

Каждый выражение имеет нормальный режим вычисления, в котором выполняются определённые вычислительные шаги. В следующих разделах описывается нормальный режим вычисления для каждого типа выражения.

Если все шаги выполняются без возникновения исключения, то выражение считается завершенным нормально.

Однако, если вычисление выражения вызывает исключение, то выражение считается завершенным прерывисто. Прерывистое завершение всегда имеет связанное основание, которое всегда является 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). Оператор может завершиться прерывисто по различным причинам, а не только из-за выбрасывания исключения.

15.7. Порядок вычисления

Язык программирования Java гарантирует, что операнды операторов, по-видимому, вычисляются в определенном порядке вычисления, а именно слева направо.

Рекомендуется, чтобы код не полагался существенно на это спецификацию. Код обычно более понятен, когда каждое выражение содержит не более одного побочного эффекта, как его внешнюю операцию, и когда код не зависит от того, какая именно исключение возникает в результате левостороннего вычисления выражений.

15.7.1. Вычисление левого операнда в первую очередь

Левый операнд бинарного оператора, по-видимому, полностью вычисляется перед любой частью правого операнда.

Если оператор является оператором сложного присваивания (§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.


15.7.2. Вычисление операндов перед операцией

Язык программирования 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, даже если реализация может определить или вывести, что операция деления обязательно приведет к исключению деления на ноль.


15.7.3. Вычисление учитывает скобки и приоритет

Язык программирования 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. Списки аргументов вычисляются слева направо

В выражении вызова метода или конструктора или создания экземпляра класса выражения аргументов могут появляться в скобках, разделенные запятыми. Каждое выражение аргумента вычисляется полностью, прежде чем любая часть любого выражения аргумента справа от него.

Если вычисление выражения аргумента внезапно завершается, ни одна часть выражения аргумента справа от него, по-видимому, не была вычислена.

Пример 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 не выполняется.


15.7.5. Порядок вычисления для других выражений

Порядок вычисления для некоторых выражений не полностью охвачен этими общими правилами, так как эти выражения могут иногда вызывать исключительные ситуации, которые необходимо указать. См. подробные объяснения порядка вычисления для следующих типов выражений:

  • выражения создания экземпляров классов (§15.9.4)

  • выражения создания массивов (§15.10.2)

  • выражения доступа к элементам массива (§15.10.4)

  • выражения вызова методов (§15.12.4)

  • выражения ссылки на методы (§15.13.3)

  • присваивания, включающие компоненты массива (§15.26)

  • лямбда-выражения (§15.27.4)

15.8. Основные выражения

Основные выражения включают в себя большинство простейших типов выражений, из которых строятся все остальные: литералы, создание объектов, доступ к полям, вызов методов, ссылки на методы, доступ к элементам массива. Выражение в скобках также синтаксически рассматривается как основное выражение.

Основное:
PrimaryNoNewArray
Выражение создания массива
PrimaryNoNewArray:
Литерал
КлассЛитерал
this
ИмяТипа . this
( Выражение )
Выражение создания экземпляра класса
ДоступКПолю
ДоступКЭлементуМассива
ВызовМетода
СсылкаНаМетод

Эта часть грамматики языка программирования Java нестандартна по двум причинам. Во-первых, можно было бы ожидать, что простые имена, такие как имена локальных переменных и параметров методов, будут основными выражениями. По техническим причинам имена объединяются с основными выражениями несколько позже, когда вводятся постфиксные выражения (§15.14).

Технические причины связаны с возможностью левостороннего разбора программ Java с только односимвольным предварительным просмотром. Рассмотрим выражения (z[3]) и (z[]). Первое - это выражение доступа к элементу массива в скобках (§15.10.3), а второе - начало приведения типов (§15.16). В момент, когда символ предварительного просмотра - [, левосторонний разбор сведет z к нетерминалу Имя. В контексте приведения типов мы предпочитаем не сводить имя к Основное, но если Имя было одним из вариантов для Основное, то мы не смогли бы определить, нужно ли выполнять это преобразование (то есть, мы не смогли бы определить, окажется ли текущая ситуация выражением доступа к элементу массива в скобках или выражением приведения типов) без просмотра двух символов вперед, до символа, следующего за [. Приведенная здесь грамматика избегает проблемы, сохраняя Имя и Основное разделенными и разрешая их в определенных других правилах синтаксиса (те, которые относятся к ClassInstanceCreationExpression, MethodInvocation, ArrayAccess и PostfixExpression, но не к FieldAccess, так как оно использует идентификатор непосредственно). Эта стратегия фактически откладывает вопрос о том, следует ли рассматривать Имя как Основное, до тех пор, пока не будет проанализирован больший контекст.

Вторая нестандартная особенность избегает потенциальной синтаксической неоднозначности в выражении "new int[3][3]", которое в Java всегда означает однократное создание многомерного массива, но без соответствующей синтаксической тонкости также могло бы интерпретироваться как эквивалентное "(new int[3])[3]".

Эта неоднозначность устраняется путем разделения ожидаемого определения Основное на Основное и PrimaryNoNewArray. (Это можно сравнить с разделением Утверждение на Утверждение и StatementNoShortIf (§14.5) для устранения проблемы «висящего else».)

15.8.1. Лексические литералы

Литерал (§3.10) обозначает фиксированное, неизменяемое значение.

Следующая производная из §3.10 показана здесь для удобства:

Литерал:
ЦелочисленныйЛитерал
ВещественныйЛитерал
ЛогическийЛитерал
СимвольныйЛитерал
СтроковыйЛитерал
ТекстовыйБлок
НулевойЛитерал

Тип литерала определяется следующим образом:

  • Тип целочисленного литерала (§3.10.1), заканчивающегося на L или l (ell), является long (§4.2.1).

    Тип любого другого целочисленного литерала является int (§4.2.1).

  • Тип вещественного литерала (§3.10.2), заканчивающегося на F или f, является float (§4.2.3).

    Тип любого другого вещественного литерала является double (§4.2.3).

  • Тип логического литерала (§3.10.3) является boolean (§4.2.5).

  • Тип символьного литерала (§3.10.4) является char (§4.2.1).

  • Тип строкового литерала (§3.10.5) или текстового блока (§3.10.6) является String (§4.3.3).

  • Тип нулевого литерала null (§3.10.8) - тип null (§4.1); его значение - нулевая ссылка.

Вычисление лексического литерала всегда завершается успешно.

15.8.2. Литералы классов

Литерал класса — это выражение, состоящее из имени класса, интерфейса, массива, примитивного типа или псевдотипа void, за которым следует «.» и токен class.

ClassLiteral:
ТипИмениКлассa {[ ]} . class
ЧисловойТип {[ ]} . class
boolean {[ ]} . class
void . class

ТипИмениКлассa должен обозначать доступный класс или интерфейс (§6.6). Если ТипИмениКлассa обозначает недоступный класс или интерфейс, или тип переменной, это ошибка времени компиляции.

Тип C.class, где C — имя класса, интерфейса или типа массива (§4.3), — это Class<C>.

Тип p.class, где p — имя примитивного типа (§4.2), — это Class<B>, где B — тип выражения типа p после преобразования к упакованному типу (§5.1.7).

Тип void.class (§8.4.5) — это Class<Void>.

Литерал класса вычисляет значение объекта класса, интерфейса, массива или примитивного типа (или для void), как определено загрузчиком класса, определяющим класс текущего экземпляра (§12.2).

15.8.3. this

Ключевое слово this может использоваться в качестве выражения в следующих контекстах:

  • в теле метода экземпляра класса (§8.4.3.2)

  • в теле конструктора класса (§8.8.7)

  • в инициализаторе экземпляра класса (§8.6)

  • в инициализаторе переменной экземпляра класса (§8.3.2)

  • в теле метода экземпляра интерфейса, то есть, метода по умолчанию или нестатического метода интерфейса (§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.


15.8.4. Квалифицированное this

Любой лексически окружающий экземпляр (§8.1.3) может быть указан путём явного квалифицирования ключевого слова this.

Пусть n — целое число такое, что ТипИмениКлассa обозначает n'ый лексически окружающий класс или интерфейс, объявленный непосредственно перед квалифицированным выражением this.

Значение квалифицированного выражения this ТипИмениКлассa.this — это n'ый лексически окружающий экземпляр this.

Если ТипИмениКлассa обозначает обобщённый класс с параметрами типа F1,...,Fn, тип квалифицированного выражения this — это ТипИмениКлассa<F1,...,Fn>. В противном случае, тип квалифицированного выражения this — это ТипИмениКлассa.

Ошибка времени компиляции возникает, если квалифицированное выражение this появляется в статическом контексте (§8.1.3).

Ошибка времени компиляции возникает, если класс или интерфейс, чьё объявление непосредственно содержит квалифицированное выражение this, не является внутренним классом ТипИмениКлассa или ТипИмениКлассa само по себе.

15.8.5. Выражения в скобках

Выражение в скобках — это выражение-операнд, тип которого совпадает с типом содержащегося выражения, а значение которого во время выполнения равно значению содержащегося выражения. Если содержащееся выражение обозначает переменную, то выражение в скобках также обозначает эту переменную.

Использование скобок влияет только на порядок вычисления, за исключением частного случая, когда (-2147483648) и (-9223372036854775808L) являются допустимыми, но -(2147483648) и -(9223372036854775808L) являются недопустимыми.

Это связано с тем, что десятичные литералы 2147483648 и 9223372036854775808L разрешены только в качестве операнда унарного минуса (§3.10.1).

В частности, наличие или отсутствие скобок вокруг выражения не влияет на то, является ли переменная определённо присвоенной, определённо присвоенной, когда true, определённо присвоенной, когда false, определённо не присвоенной, определённо не присвоенной, когда true или определённо не присвоенной, когда false (§16 (Определённое присвоение)).

Если выражение в скобках появляется в контексте определенного типа с целевым типом T (§5 (Преобразования и контексты)), то содержащееся выражение аналогично появляется в контексте того же типа с целевым типом T.

Если содержащееся выражение является поливыражением (§15.2), то выражение в скобках также является поливыражением. В противном случае это автономное выражение.

Поливыражение в скобках совместимо с целевым типом T, если его содержащееся выражение совместимо с T.

15.9. Выражения создания экземпляров классов

Выражение создания экземпляра класса используется для создания новых объектов, являющихся экземплярами классов.

ClassInstanceCreationExpression:
UnqualifiedClassInstanceCreationExpression
ExpressionName . UnqualifiedClassInstanceCreationExpression
Primary . UnqualifiedClassInstanceCreationExpression
UnqualifiedClassInstanceCreationExpression:
new [TypeArguments] ClassOrInterfaceTypeToInstantiate ( [ArgumentList] ) [ClassBody]
ClassOrInterfaceTypeToInstantiate:
{Annotation} Identifier {. {Annotation} Identifier} [TypeArgumentsOrDiamond]
TypeArgumentsOrDiamond:
TypeArguments
<>

Следующая продукция из §15.12 приведена здесь для удобства:

ArgumentList:
Expression {, Expression}

Выражение создания экземпляра класса указывает класс, который необходимо создать, возможно, с последующими аргументами типов (§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).

15.9.1. Определение класса, подлежащего созданию

Если 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; в противном случае класс не имеет типов аргументов.

15.9.2. Определение окружающих экземпляров

Пусть C — класс, который экземпляризируется, и пусть i — экземпляр, который создаётся. Если C — внутренний класс, то i может иметь непосредственно окружающий экземпляр (§8.1.3), определяемый следующим образом:

  • Если C — анонимный класс, то:

    • Если выражение создания экземпляра класса происходит в статическом контексте, то i не имеет непосредственно окружающего экземпляра.

    • В противном случае, непосредственно окружающий экземпляр i — это this.

  • Если C — внутренний локальный класс, то:

    • Если C встречается в статическом контексте, то i не имеет непосредственно окружающего экземпляра.

    • В противном случае, если выражение создания экземпляра класса происходит в статическом контексте, возникает ошибка компиляции.

    • В противном случае, пусть O — непосредственно окружающее объявление класса или интерфейса для C, а U — непосредственно окружающее объявление класса или интерфейса для выражения создания экземпляра класса.

      Если U не является внутренним классом O или самим O, возникает ошибка компиляции.

      Пусть n — целое число такое, что O — n-й лексически окружающий класс или интерфейс объявления U.

      Непосредственно окружающий экземпляр i — это n-й лексически окружающий экземпляр this.

  • Если C — внутренний член класса, то:

    • Если выражение создания экземпляра класса не квалифицировано, то:

      • Если выражение создания экземпляра класса происходит в статическом контексте, возникает ошибка компиляции.

      • В противном случае, если C не является членом какого-либо класса, объявление которого лексически включает выражение создания экземпляра класса, возникает ошибка компиляции.

      • В противном случае, пусть O — самый внутренний окружающий класс объявления, членом которого является C, а U — непосредственно окружающее объявление класса или интерфейса для выражения создания экземпляра класса.

        Если U не является внутренним классом O или самим O, возникает ошибка компиляции.

        Пусть n — целое число такое, что O — n-й лексически окружающий класс или интерфейс объявления U

        Непосредственно окружающий экземпляр i — это n-й лексически окружающий экземпляр this.

    • Если выражение создания экземпляра класса квалифицировано, то непосредственно окружающий экземпляр i — это объект, который является значением выражения Primary или ExpressionName.

Если C — анонимный класс, а его непосредственный суперкласс S — внутренний класс, то i может иметь непосредственно окружающий экземпляр относительно S, определяемый следующим образом:

  • Если S — внутренний локальный класс, то:

    • Если S встречается в статическом контексте, то i не имеет непосредственно окружающего экземпляра относительно S.

    • В противном случае, если выражение создания экземпляра класса происходит в статическом контексте, возникает ошибка компиляции.

    • В противном случае, пусть O — непосредственно окружающее объявление класса или интерфейса для S, а U — непосредственно окружающее объявление класса или интерфейса для выражения создания экземпляра класса.

      Если U не является внутренним классом O или самим O, возникает ошибка компиляции.

      Пусть n — целое число такое, что O — n-й лексически окружающий класс или интерфейс объявления U.

      Непосредственно окружающий экземпляр i относительно S — это n-й лексически окружающий экземпляр this.

  • Если S — внутренний член класса, то:

    • Если выражение создания экземпляра класса не квалифицировано, то:

      • Если выражение создания экземпляра класса происходит в статическом контексте, возникает ошибка компиляции.

      • В противном случае, если S не является членом какого-либо класса, объявление которого включает выражение создания экземпляра класса, возникает ошибка компиляции.

      • В противном случае, пусть O — самый внутренний окружающий класс объявления, членом которого является S, а U — непосредственно окружающее объявление класса или интерфейса для выражения создания экземпляра класса.

        Если U не является внутренним классом O или самим O, возникает ошибка компиляции.

        Пусть n — целое число такое, что O — n-й лексически окружающий класс или интерфейс объявления U.

        Непосредственно окружающий экземпляр i относительно S — это n-й лексически окружающий экземпляр this.

      • В противном случае, возникает ошибка компиляции.

    • Если выражение создания экземпляра класса квалифицировано, то непосредственно окружающий экземпляр i относительно S — это объект, который является значением выражения Primary или ExpressionName.

15.9.3. Выбор конструктора и его аргументов

Пусть 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 is [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, недоступен в точке вызова.

Тип выражения создания экземпляра класса — это тип возвращаемого значения выбранного конструктора, как определено выше.

15.9.4. Вычисление выражений создания экземпляров класса во время выполнения

Во время выполнения вычисление выражения создания экземпляра класса происходит следующим образом.

Сначала, если выражение создания экземпляра класса является квалифицированным выражением создания экземпляра класса, квалифицирующее выражение-первичный элемент вычисляется. Если квалифицирующее выражение вычисляет значение 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).


15.9.5. Объявления анонимных классов

Анонимный класс неявно объявляется выражением создания экземпляра класса или константой перечисления, которая заканчивается телом класса (§8.9.1).

Анонимный класс никогда не является abstract (§8.1.1.1).

Анонимный класс никогда не является sealed (§8.1.1.2), и, следовательно, у него нет разрешенных непосредственных подклассов (§8.1.6).

Анонимный класс, объявленный выражением создания экземпляра класса, никогда не является final (§8.1.1.2).

Анонимный класс, объявленный константой перечисления, всегда является final.

Анонимный класс, не являющийся final, важен при приведении типов, в частности, при сужении преобразования ссылок, разрешенном для оператора приведения типа (§5.5). С другой стороны, это неактуально для наследования, так как невозможно объявить подкласс анонимного класса (анонимный класс не может быть назван с помощью extends фрагмента), несмотря на то, что анонимный класс не является final.

Анонимный класс всегда является внутренним классом (§8.1.3).

Как и локальный класс или интерфейс (§14.3), анонимный класс не является членом ни одного пакета, класса или интерфейса (§7.1, §8.5).

Тип непосредственного суперкласса или тип непосредственного суперинтерфейса анонимного класса, объявленного выражением создания экземпляра класса, задаётся выражением (§15.9.1), с необходимыми выводами типов аргументов при выборе конструктора (§15.9.3). Если задан тип непосредственного суперинтерфейса, тип непосредственного суперкласса является Object.

Тип непосредственного суперкласса анонимного класса, объявленного константой перечисления, — это тип объявляющего класса перечисления.

ClassBody выражения создания экземпляра класса или константы перечисления объявляет поля (§8.3), методы (§8.4), внутренние классы (§8.5), внутренние интерфейсы (§9.1.1.3), инициализаторы экземпляров (§8.6) и статические инициализаторы (§8.7) анонимного класса. Конструктор анонимного класса всегда неявный (§15.9.5.1).

Если выражение создания экземпляра класса с ClassBody использует алмаз (<>) для типов аргументов класса, подлежащего созданию, то для всех методов, не являющихся private, объявленных в ClassBody, метод объявляется так, как будто он аннотирован @Override (§9.6.4.4).

При использовании <>, выведенные типы аргументов могут не соответствовать ожиданиям программиста. В результате, супертип анонимного класса может не соответствовать ожиданиям, а методы, объявленные в анонимном классе, могут не переопределять методы супертипа как предполагалось. Обращение к таким методам так, как будто они аннотированы @Override (если они не аннотированы явно @Override), помогает избежать создания некорректных программ.

15.9.5.1. Анонимные конструкторы

У анонимного класса не может быть явно объявленного конструктора. Вместо этого для анонимного класса неявно объявляется анонимный конструктор. Форма анонимного конструктора для анонимного класса 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). Это само по себе не приводит к ошибкам ни на этапе компиляции, ни на этапе выполнения.

15.10. Создание массивов и выражения доступа

15.10.1. Выражения создания массивов

Выражение создания массива используется для создания новых массивов (§10 (Массивы)).

ArrayCreationExpression:
ArrayCreationExpressionWithoutInitializer
ArrayCreationExpressionWithInitializer
ArrayCreationExpressionWithoutInitializer:
new PrimitiveType DimExprs [Dims]
new ClassOrInterfaceType DimExprs [Dims]
ArrayCreationExpressionWithInitializer:
new PrimitiveType Dims ArrayInitializer
new ClassOrInterfaceType Dims ArrayInitializer
DimExprs:
DimExpr {DimExpr}
DimExpr:
{Annotation} [ Expression ]

Следующее производство из §4.3 показано здесь для удобства:

Dims:
{Annotation} [ ] {{Annotation} [ ]}

Выражение создания массива создаёт объект, представляющий собой новый массив, элементы которого имеют тип, указанный в 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[][][]

15.10.2. Вычисление выражений создания массивов во время выполнения

При выполнении выражения создания массива происходит следующее:

  • Если нет выражений измерения, то должен быть инициализатор массива. Новый выделенный массив будет инициализирован значениями, предоставленными инициализатором массива, как описано в §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 (int d = 0; d < matrix.length; d++)
    matrix[d] = new float[3];

и:

Age[][][][][] Aquarius = new Age[6][10][8][12][];

эквивалентно:

Age[][][][][] Aquarius = new Age[6][][][][];
for (int d1 = 0; d1 < Aquarius.length; d1++) {
    Aquarius[d1] = new Age[10][][][];
    for (int d2 = 0; d2 < Aquarius[d1].length; d2++) {
        Aquarius[d1][d2] = new Age[8][][];
        for (int d3 = 0; d3 < Aquarius[d1][d2].length; d3++) {
            Aquarius[d1][d2][d3] = new Age[12][];
        }
    }
}

с d, d1, d2 и d3 заменёнными на имена, которые ещё не объявлены локально. Таким образом, одно выражение new фактически создаёт один массив длиной 6, 6 массивов длиной 10, 6x10 = 60 массивов длиной 8 и 6x10x8 = 480 массивов длиной 12. В этом примере пятое измерение, которое содержало бы фактические элементы массива (ссылки на объекты Age), инициализируется только нулевыми ссылками. Эти массивы можно заполнить позже другим кодом, например:

Age[] Hair = { new Age("quartz"), new Age("topaz") };
Aquarius[1][9][6][9] = Hair;

Треугольная матрица может быть создана:

float[][] triang = new float[100][];
for (int i = 0; i < triang.length; i++)
    triang[i] = new float[i+1];

Если при вычислении выражения создания массива оказывается, что памяти недостаточно для выполнения операции создания, то генерируется исключение OutOfMemoryError. Если выражение создания массива не имеет инициализатора массива, эта проверка выполняется только после нормального завершения вычисления всех выражений измерения. Если выражение создания массива имеет инициализатор массива, то исключение OutOfMemoryError может произойти при выделении объекта ссылочного типа во время вычисления выражения инициализатора переменной или при выделении памяти для массива, содержащего значения (возможно, вложенного) инициализатора массива.

Пример 15.10.2-3. OutOfMemoryError и вычисление выражения измерения

class Test3 {
    public static void main(String[] args) {
        int len = 0, oldlen = 0;
        Object[] a = new Object[0];
        try {
            for (;;) {
                ++len;
                Object[] temp = new Object[oldlen = len];
                temp[0] = a;
                a = temp;
            }
        } catch (Error e) {
            System.out.println(e + ", " + (oldlen==len));
        }
    }
}

Эта программа выводит:

java.lang.OutOfMemoryError, true

потому что условие недостатка памяти обнаруживается после вычисления выражения измерения oldlen = len.

Сравните это с выражениями создания экземпляров класса (§15.9), которые обнаруживают условие недостатка памяти до вычисления выражений аргументов (§15.9.4).


15.10.3. Выражения доступа к массивам

Выражение доступа к массиву ссылается на переменную, являющуюся компонентом массива.

ArrayAccess:
ExpressionName [ Выражение ]
PrimaryNoNewArray [ Выражение ]
Инициализированное выражение создания массива [ Выражение ]

Выражение доступа к массиву содержит два подвыражения: выражение ссылки на массив (перед левой квадратной скобкой) и выражение индекса (внутри скобок).

Обратите внимание, что выражение ссылки на массив может быть именем или любым выражением типа primary, которое не является выражением создания массива, за исключением случаев, когда выражение создания массива имеет инициализатор массива (§15.10.1).

Тип выражения ссылки на массив должен быть типом массива (назовём его T[], массивом, компоненты которого имеют тип T), иначе произойдёт ошибка компиляции.

Выражение индекса подвергается числовому продвижению (§5.6). Продвинутый тип должен быть int, иначе произойдёт ошибка компиляции.

Тип выражения доступа к массиву является результатом применения преобразования захвата (§5.1.10) к T.

Результат выражения доступа к массиву — это переменная типа T, а именно переменная в массиве, выбранная значением выражения индекса.

Эта результирующая переменная, являющаяся компонентом массива, никогда не рассматривается как final, даже если выражение ссылки на массив обозначало переменную типа final.

15.10.4. Вычисление выражений доступа к массивам во время выполнения

Во время выполнения вычисление выражения доступа к массиву происходит следующим образом:

  • Сначала вычисляется выражение ссылки на массив. Если это вычисление завершается внезапно, то и доступ к массиву завершается по той же причине, и выражение индекса не вычисляется.

  • В противном случае вычисляется выражение индекса. Если это вычисление завершается внезапно, то и доступ к массиву завершается по той же причине.

  • В противном случае, если значение выражения ссылки на массив равно 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.


15.11. Выражения доступа к полям

Выражение доступа к полю может получить доступ к полю объекта или массива, ссылка на который является значением либо выражения, либо специального ключевого слова super.

FieldAccess:
Primary . Идентификатор
super . Идентификатор
Тип имени . super . Идентификатор

Значение выражения доступа к полю определяется по тем же правилам, что и для квалифицированных имен (§6.5.6.2), но с ограничением, что выражение не может обозначать пакет, тип класса или тип интерфейса.

Также возможно обратиться к полю текущего экземпляра или текущего класса, используя простое имя (§6.5.6.1).

15.11.1. Доступ к полю с помощью Primary

Тип Primary должен быть типом ссылки T, в противном случае произойдет ошибка компиляции.

Значение выражения доступа к полю определяется следующим образом:

  • Если идентификатор называет несколько доступных (§6.6) полей-членов в типе T, то доступ к полю является неоднозначным, и произойдет ошибка компиляции.

  • Если идентификатор не называет доступного поля-члена в типе T, то доступ к полю не определен, и произойдет ошибка компиляции.

  • В противном случае, идентификатор называет единственное доступное поле-член в типе T, и типом выражения доступа к полю является тип поля-члена после преобразования захвата (§5.1.10).

Во время выполнения результат выражения доступа к полю вычисляется следующим образом: (предполагается, что программа корректна с точки зрения анализа определенного назначения, то есть каждой переменной без значения final явно присваивается значение перед доступом)

  • Если поле является static:

    • Выражение Primary оценивается, и результат отбрасывается. Если вычисление выражения Primary завершается прерыванием, выражение доступа к полю завершается прерыванием по той же причине.

    • Если поле является полем-членом непустым final, то результатом является значение указанной переменной класса в классе или интерфейсе, который является типом выражения Primary.

    • Если поле не является final, или является пустым final и доступ к полю происходит в инициализаторе переменной класса (§8.3.2) или статическом инициализаторе (§8.7), то результатом является переменная, а именно указанная переменная класса в классе, который является типом выражения Primary.

  • Если поле не является static:

    • Выражение Primary оценивается. Если вычисление выражения Primary завершается прерыванием, выражение доступа к полю завершается прерыванием по той же причине.

    • Если значение Primary равно null, то выбрасывается NullPointerException.

    • Если поле является непустым final, то результатом является значение названного поля-члена в типе T, найденного в объекте, на который ссылается значение Primary.

    • Если поле не является final, или является пустым final и доступ к полю происходит в инициализаторе переменной экземпляра (§8.3.2), инициализаторе экземпляра (§8.6) или конструкторе (§8.8), то результатом является переменная, а именно, названное поле-член в типе T, найденное в объекте, на который ссылается значение Primary.

Обратите внимание, что для определения используемого поля используется только тип выражения Primary, а не класс фактического объекта, на который ссылаются во время выполнения.

Пример 15.11.1-1. Статическая привязка для доступа к полю

class S           { int x = 0; }
class T extends S { int x = 1; }
class Test1 {
    public static void main(String[] args) {
        T t = new T();
        System.out.println("t.x=" + t.x + when("t", t));
        S s = new S();
        System.out.println("s.x=" + s.x + when("s", s));
        s = t;
        System.out.println("s.x=" + s.x + when("s", s));
    }
    static String when(String name, Object t) {
        return " when " + name + " holds a "
                        + t.getClass() + " at run time.";
    }
}

Эта программа выводит:

t.x=1 when t holds a class T at run time.
s.x=0 when s holds a class S at run time.
s.x=0 when s holds a class T at run time.

Последняя строка показывает, что, действительно, доступное поле не зависит от класса объекта, на который ссылаются во время выполнения; даже если s содержит ссылку на объект класса T, выражение s.x ссылается на поле x класса S, поскольку тип выражения s является S. Объекты класса T содержат два поля с именем x, одно для класса T и одно для его суперкласса S.

Отсутствие динамического поиска для доступа к полям позволяет эффективно выполнять программы с простыми реализациями. Мощность поздней привязки и переопределения доступна, но только при использовании методов экземпляров. Рассмотрим тот же пример с использованием методов экземпляров для доступа к полям:

class S           { int x = 0; int z() { return x; } }
class T extends S { int x = 1; int z() { return x; } }
class Test2 {
    public static void main(String[] args) {
        T t = new T();
        System.out.println("t.z()=" + t.z() + when("t", t));
        S s = new S();
        System.out.println("s.z()=" + s.z() + when("s", s));
        s = t;
        System.out.println("s.z()=" + s.z() + when("s", s));
    }
    static String when(String name, Object t) {
        return " when " + name + " holds a "
                        + t.getClass() + " at run time.";
    }
}

Теперь вывод:

t.z()=1 when t holds a class T at run time.
s.z()=0 when s holds a class S at run time.
s.z()=1 when s holds a class T at run time.

Последняя строка показывает, что, действительно, метод, к которому осуществляется доступ, зависит от класса объекта, на который ссылаются во время выполнения; когда s содержит ссылку на объект класса T, выражение s.z() ссылается на метод z класса T, несмотря на то, что тип выражения s является S. Метод z класса T переопределяет метод z класса S.


Пример 15.11.1-2. Переменная получателя не имеет значения для доступа к полю static

Следующая программа демонстрирует, что нулевая ссылка может использоваться для доступа к переменной класса (static) без выброса исключения:

class Test3 {
    static String mountain = "Chocorua";
    static Test3 favorite(){
        System.out.print("Mount ");
        return null;
    }
    public static void main(String[] args) {
        System.out.println(favorite().mountain);
    }
}

Она компилируется, выполняется и выводит:

Mount Chocorua

Даже если результатом favorite() является null, исключение NullPointerException не выбрасывается. То, что выводится "Mount ", демонстрирует, что выражение Primary действительно полностью оценивается во время выполнения, несмотря на то, что только его тип, а не его значение используется для определения, какое поле использовать (поскольку поле mountain является static).


15.11.2. Доступ к членам суперкласса с помощью super

Форма 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 членам суперкласса.


15.12. Выражения вызова методов

Выражение вызова метода используется для вызова метода класса или экземпляра.

MethodInvocation:
ИмяМетода ( [СписокАргументов] )
ИмяТипа . [АргументыТипов] Идентификатор ( [СписокАргументов] )
ИмяВыражения . [АргументыТипов] Идентификатор ( [СписокАргументов] )
Первичное . [АргументыТипов] Идентификатор ( [СписокАргументов] )
super . [АргументыТипов] Идентификатор ( [СписокАргументов] )
ИмяТипа . super . [АргументыТипов] Идентификатор ( [СписокАргументов] )
СписокАргументов:
Выражение {, Выражение}

Разрешение имени метода во время компиляции сложнее, чем разрешение имени поля, из-за возможности перегрузки методов. Вызов метода во время выполнения также сложнее, чем доступ к полю, из-за возможности переопределения методов экземпляра.

Определение метода, который будет вызван выражением вызова метода, включает несколько шагов. Следующие три раздела описывают обработку выражения вызова метода во время компиляции. Определение типа выражения вызова метода указано в §15.12.3.

Типы исключений, которые может генерировать выражение вызова метода, указаны в §11.2.1.

Синтаксическая ошибка, если имя слева от правого ".", которое встречается до ( в MethodInvocation, нельзя классифицировать как TypeName или ExpressionName (§6.5.2).

Если АргументыТипов присутствуют слева от Идентификатор, то синтаксическая ошибка, если какой-либо из аргументов типа является джокером (§4.5.1).

Выражение вызова метода является поливыражением, если все следующие условия выполняются:

  • Вызов происходит в контексте присваивания или вызова (§5.2, §5.3).

  • Если вызов квалифицирован (то есть любой вид MethodInvocation, кроме первого), то вызов опускает АргументыТипов слева от Идентификатор.

  • Метод, который будет вызван, как определяется в последующих подразделах, является универсальным (§8.4.4) и имеет тип возвращаемого значения, который упоминает как минимум один параметр типа метода.

В противном случае выражение вызова метода является самостоятельным выражением.

15.12.1. Этап компиляции 1: Определение типа для поиска

Первый шаг в обработке вызова метода во время компиляции — определить имя вызываемого метода и тип, для которого необходимо искать определения методов с таким именем.

Имя метода задается ИменемМетода или Идентификатором, непосредственно предшествующим левой круглой скобке ВызоваМетода.

Для типа, по которому следует искать, существуют шесть случаев, в зависимости от формы, предшествующей левой круглой скобке ВызоваМетода:

  • Если форма представляет собой ИменемМетода, то есть просто Идентификатор, то:

    Если Идентификатор присутствует в области видимости объявления метода с таким именем (§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.12.2. Этап компиляции 2: Определение сигнатуры метода

На втором шаге выполняется поиск методов-членов в типе, определенном на предыдущем шаге. Этот шаг использует имя метода и выражения аргументов для поиска методов, которые являются доступными и применимыми, то есть объявления, которые могут быть правильно вызваны с заданными аргументами.

Может быть более одного такого метода, в этом случае выбирается наиболее конкретный. Описание (сигнатура плюс возвращаемый тип) наиболее конкретного метода используется во время выполнения для выполнения диспетчеризации метода.

Некоторые выражения аргументов, содержащие неявные лямбда-выражения (§15.27.1) или неточные ссылки на методы (§15.13.1), игнорируются тестами применимости, потому что их значение нельзя определить до тех пор, пока не будет выбран целевой тип вызова. С другой стороны, только выражения аргументов – а не целевой тип вызова – влияют на тесты применимости, даже если выражение вызова метода является поливыражением.

Процесс определения применимости начинается с определения потенциально применимых методов (§15.12.2.1). Затем, чтобы обеспечить совместимость с языком программирования Java до Java SE 5.0, процесс продолжается в трех фазах:

  1. В первой фазе выполняется разрешение перегрузки без разрешения преобразований boxing или unboxing, или использования вызовов методов с переменным числом аргументов. Если примененный метод не найден на этой фазе, обработка переходит ко второй фазе.

    Это гарантирует, что любые вызовы, которые были допустимы в языке программирования Java до Java SE 5.0, не считаются неоднозначными в результате введения методов с переменным числом аргументов, неявного boxing и/или unboxing. Однако объявление метода с переменным числом аргументов (§8.4.1) может изменить метод, выбранный для данного выражения вызова метода, потому что метод с переменным числом аргументов обрабатывается как метод с фиксированным числом аргументов на первой фазе. Например, объявление m(Object...) в классе, который уже объявляет m(Object), приводит к тому, что m(Object) больше не выбирается для некоторых выражений вызова (таких как m(null)), так как m(Object[]) более конкретный.

  2. На второй фазе выполняется разрешение перегрузки, позволяющее использовать boxing и unboxing, но все еще исключая использование вызовов методов с переменным числом аргументов. Если применимый метод не найден на этой фазе, обработка переходит к третьей фазе.

    Это гарантирует, что метод никогда не выбирается с помощью вызова метода с переменным числом аргументов, если он применим с помощью вызова метода с фиксированным числом аргументов.

  3. На третьей фазе разрешается комбинировать перегрузку с методами с переменным числом аргументов, boxing и unboxing.

Метод применим, если он применим по одному из строгих вызовов (первая фаза, §15.12.2.2), свободных вызовов (вторая фаза, §15.12.2.3) или вызовов с переменным числом аргументов (третья фаза, §15.12.2.4). Определение того, применим ли метод, в случае универсальных методов (§8.4.4), потребует анализа аргументов типа. Аргументы типа могут передаваться явно или неявно; если они передаются неявно, границы аргументов типа должны быть выведены из выражений аргументов (§18 (Выведение типов)).

Если в ходе одного из трех этапов проверки применимости были идентифицированы несколько применимых методов, то выбирается наиболее конкретный из них, как указано в §15.12.2.5.

Для проверки применимости типы аргументов вызова, как правило, не могут быть входными данными анализа. Это связано с тем, что:

  • Аргументы вызова метода могут быть поливыражениями.

  • Поливыражения нельзя типизировать в отсутствие целевого типа.

  • Разрешение перегрузки должно быть завершено, прежде чем будут известны целевые типы аргументов.

Вместо этого входными данными для проверки применимости является список самих аргументов. Аргументы могут быть проверены на совместимость с потенциальными целевыми типами, даже если окончательные типы аргументов неизвестны.

Обратите внимание, что разрешение перегрузки независимо от целевого типа. Это имеет две причины:

  • Во-первых, это делает модель пользователя более доступной и менее подверженной ошибкам. Значение имени метода (т.е., объявление, соответствующее имени) слишком фундаментально для смысла программы, чтобы зависеть от тонких контекстных подсказок. (В отличие от этого, другие поливыражения могут иметь различное поведение в зависимости от целевого типа; но изменения в поведении всегда ограничены и по существу эквивалентны, тогда как о поведении произвольного набора методов, имеющих одинаковое имя и арность, такого гарантии сделать нельзя.)

  • Во-вторых, это позволяет другим свойствам – таким как то, является ли метод поливыражением (§15.12) или как категоризировать условное выражение (§15.25) – зависеть от значения имени метода, даже прежде чем будет известен целевой тип.

Пример 15.12.2-1. Применимость метода

class Doubler {
            static int two()      { return two(1); }
    private static int two(int i) { return 2*i;    }
}
class Test extends Doubler {	
    static long two(long j) { return j+j; }

    public static void main(String[] args) {
        System.out.println(two(3));
        System.out.println(Doubler.two(3)); // compile-time error
    }
}

Для вызова метода two(1) в классе Doubler существуют два доступных метода с именем two, но только второй применим, поэтому он вызывается во время выполнения.

Для вызова метода two(3) в классе Test существуют два применимых метода, но только один в классе Test доступен, поэтому он вызывается во время выполнения (аргумент 3 преобразуется к типу long).

Для вызова метода Doubler.two(3) класс Doubler, а не класс Test, просматривается на предмет методов с именем two; единственный применимый метод недоступен, поэтому этот вызов метода вызывает ошибку компиляции.

Другой пример:

class ColoredPoint {
    int x, y;
    byte color;
    void setColor(byte color) { this.color = color; }
}
class Test {
    public static void main(String[] args) {
        ColoredPoint cp = new ColoredPoint();
        byte color = 37;
        cp.setColor(color);
        cp.setColor(37);  // compile-time error
    }
}

Здесь во время компиляции возникает ошибка для второго вызова setColor, так как во время компиляции нет применимого метода. Тип литерала 37 – int, и int не может быть преобразован в byte с помощью преобразования вызова. Преобразование присваивания, используемое при инициализации переменной color, выполняет неявное преобразование константы из типа int в byte, что разрешено, потому что значение 37 достаточно мало, чтобы быть представлено в типе byte; но такое преобразование не разрешено для преобразования вызова.

Однако, если бы метод setColor был объявлен для принятия int вместо byte, оба вызова метода были бы корректны; первый вызов был бы разрешен, потому что преобразование вызова допускает расширяющее преобразование из byte в int. Однако затем в теле setColor потребовалось бы сужающее приведение:

void setColor(int color) { this.color = (byte)color; }

Вот пример неоднозначности перегрузки. Рассмотрим программу:

class Point { int x, y; }
class ColoredPoint extends Point { int color; }
class Test {
    static void test(ColoredPoint p, Point q) {
        System.out.println("(ColoredPoint, Point)");
    }
    static void test(Point p, ColoredPoint q) {
        System.out.println("(Point, ColoredPoint)");
    }
    public static void main(String[] args) {
        ColoredPoint cp = new ColoredPoint();
        test(cp, cp);  // compile-time error
    }
}

Этот пример генерирует ошибку во время компиляции. Проблема в том, что существует два объявления test, которые применимы и доступны, и ни одно не является более конкретным, чем другое. Поэтому вызов метода является неоднозначным.

Если добавить третье определение test:

static void test(ColoredPoint p, ColoredPoint q) {
    System.out.println("(ColoredPoint, ColoredPoint)");
}

то оно было бы более конкретным, чем два других, и вызов метода больше не был бы неоднозначным.


Пример 15.12.2-2. Тип возврата не учитывается при выборе метода

class Point { int x, y; }
class ColoredPoint extends Point { int color; }
class Test {
    static int test(ColoredPoint p) {
        return p.color;
    }
    static String test(Point p) {
        return "Point";
    }
    public static void main(String[] args) {
        ColoredPoint cp = new ColoredPoint();
        String s = test(cp);  // compile-time error
    }
}

Здесь наиболее конкретное объявление метода test – то, которое принимает параметр типа ColoredPoint. Поскольку тип результата метода – int, возникает ошибка компиляции, так как int не может быть преобразован в String с помощью преобразования присваивания. Этот пример показывает, что типы результатов методов не участвуют в разрешении перегруженных методов, поэтому второй метод test, возвращающий String, не выбирается, даже если его тип результата позволил бы программе скомпилироваться без ошибок.


Пример 15.12.2-3. Выбор наиболее специфичного метода

Наиболее специфичный метод выбирается во время компиляции; его описание определяет, какой метод фактически выполняется во время выполнения. Если к классу добавляется новый метод, то исходный код, который был скомпилирован со старым определением класса, может не использовать новый метод, даже если повторная компиляция приведет к выбору этого метода.

Например, рассмотрим два модуля компиляции, один для класса Point:

package points;
public class Point {
    public int x, y;
    public Point(int x, int y) { this.x = x; this.y = y; }
    public String toString() { return toString(""); }
    public String toString(String s) {
        return "(" + x + "," + y + s + ")";
    }
}

и один для класса ColoredPoint:

package points;
public class ColoredPoint extends Point {
    public static final int
        RED = 0, GREEN = 1, BLUE = 2;
    public static String[] COLORS =
        { "red", "green", "blue" };

    public byte color;
    public ColoredPoint(int x, int y, int color) {
        super(x, y);
        this.color = (byte)color;
    }

    /** Copy all relevant fields of the argument into
        this ColoredPoint object. */
    public void adopt(Point p) { x = p.x; y = p.y; }

    public String toString() {
        String s = "," + COLORS[color];
        return super.toString(s);
    }
}

Теперь рассмотрим третий модуль компиляции, который использует ColoredPoint:

import points.*;
class Test {
    public static void main(String[] args) {
        ColoredPoint cp =
            new ColoredPoint(6, 6, ColoredPoint.RED);
        ColoredPoint cp2 =
            new ColoredPoint(3, 3, ColoredPoint.GREEN);
        cp.adopt(cp2);
        System.out.println("cp: " + cp);
    }
}

Выходные данные:

cp: (3,3,red)

Программист, который написал класс Test, ожидал увидеть слово green, потому что фактический аргумент, ColoredPoint, имеет поле color, и color, по-видимому, является "релевантным полем". (Конечно, документация для пакета points должна была быть гораздо точнее!)

Обратите внимание, что наиболее специфичный метод (на самом деле, единственный применимый метод) для вызова метода adopt имеет сигнатуру, которая указывает на метод с одним параметром, и параметр имеет тип Point. Эта сигнатура становится частью двоичного представления класса Test, созданного компилятором Java, и используется при вызове метода во время выполнения.

Предположим, что программист сообщил об этой ошибке программного обеспечения, и разработчик пакета points после долгих размышлений решил исправить ее, добавив метод в класс ColoredPoint:


public void adopt(ColoredPoint p) {
    adopt((Point)p);
    color = p.color;
}

Если затем программист запустит старый двоичный файл для Test с новым двоичным файлом для ColoredPoint, результат по-прежнему будет:

cp: (3,3,red)

потому что старый двоичный файл для Test всё ещё имеет описание "один параметр, тип которого Point; void" в ассоциации с вызовом метода cp.adopt(cp2). Если исходный код для Test перекомпилировать, то компилятор Java обнаружит, что теперь есть два применимых метода adopt, и что сигнатура более специфичного метода — "один параметр, тип которого ColoredPoint; void"; запуск программы затем даст ожидаемый результат:

cp: (3,3,green)

Проявив прозорливость в отношении таких проблем, разработчик пакета points мог бы исправить класс ColoredPoint, чтобы он работал как с недавно скомпилированным, так и со старым кодом, добавив защитный код в старый метод adopt ради старого кода, который все еще вызывает его с аргументами ColoredPoint:


public void adopt(Point p) {
    if (p instanceof ColoredPoint)
        color = ((ColoredPoint)p).color;
    x = p.x; y = p.y;
}

В идеале, исходный код следует перекомпилировать всякий раз, когда изменяется код, от которого он зависит. Однако в среде, где разные классы поддерживаются разными организациями, это не всегда возможно. Защитное программирование с внимательным отношением к проблемам эволюции классов может сделать обновлённый код гораздо более надёжным. Подробнее о двоичной совместимости и эволюции типов см. §13 (Двоичная совместимость).


15.12.2.1. Определение потенциально применимых методов

Тип, определённый на этапе компиляции 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) потенциально совместимо с любым типом.

Определение потенциальной применимости выходит за рамки простого проверки арности, чтобы также учитывать наличие и «форму» целевых типов функциональных интерфейсов. В некоторых случаях, связанных с выводом типов, выражение лямбда-функции, используемое в качестве аргумента вызова метода, не может быть должным образом типизировано до разрешения перегрузки. Эти правила позволяют по-прежнему учитывать форму выражения лямбда-функции, отбрасывая явно некорректные целевые типы, которые в противном случае могли бы привести к ошибкам неоднозначности.

15.12.2.2. Фаза 1: Определение методов с совпадающей арностью, применимых по строгому вызову

Выражение аргумента считается актуальным для применимости для потенциально применимого метода m, если оно не имеет одного из следующих форм:

  • Выражение лямбда-функции с неявным типом (§15.27.1).

  • Выражение ссылки на метод с неточной спецификацией (§15.13.1).

  • Если m является обобщенным методом, и вызов метода не предоставляет явных аргументов типа, явное выражение лямбда-функции или точное выражение ссылки на метод, для которого соответствующий целевой тип (полученный из сигнатуры m) является параметром типа m.

  • Явное выражение лямбда-функции, тело которой является выражением, не относящимся к применимости.

  • Явное выражение лямбда-функции, тело которой является блоком, где по крайней мере одно выражение результата не относится к применимости.

  • Выражение в скобках (§15.8.5), содержащее выражение, не относящееся к применимости.

  • Условное выражение (§15.25), второй или третий операнд которого не относится к применимости.

Пусть m — потенциально применимый метод (§15.12.2.1) с арностью n и формальными типами параметров F1, ..., Fn, и пусть e1, ..., en — выражения фактических аргументов вызова метода. Тогда:

  • Если m является обобщенным методом, и вызов метода не предоставляет явных аргументов типа, то применимость метода определяется, как указано в §18.5.1.

  • Если m является обобщенным методом, и вызов метода предоставляет явные аргументы типа, то пусть R1, ..., Rp (p ≥ 1) — параметры типа m, Bl — объявленное ограничение Rl (1 ≤ l ≤ p), и U1, ..., Up — явные аргументы типа, заданные в вызове метода. Тогда m является применимым по строгому вызову, если оба следующих условия истинны:

    • Для 1 ≤ i ≤ n, если ei относится к применимости, то ei совместим в контексте строгого вызова с Fi[R1:=U1, ..., Rp:=Up] (§5.3).

    • Для 1 ≤ l ≤ p, Ul <: Bl[R1:=U1, ..., Rp:=Up].

  • Если m не является обобщенным методом, то m является применимым по строгому вызову, если для 1 ≤ i ≤ n, либо ei совместим в контексте строгого вызова с Fi (§5.3), либо ei не относится к применимости.

Если не найден ни один метод, применимый по строгому вызову, поиск применимых методов продолжается на фазе 2 (§15.12.2.3).

В противном случае выбирается наиболее специфичный метод (§15.12.2.5) среди методов, применимых по строгому вызову.

Значение выражения лямбда-функции с неявным типом или выражения ссылки на метод с неточной спецификацией недостаточно определено до разрешения целевого типа, поэтому аргументы, содержащие эти выражения, не считаются актуальными для применимости; они просто игнорируются (за исключением ожидаемой арности) до завершения разрешения перегрузки.

15.12.2.3. Фаза 2: Определение методов с совпадающей арностью, применимых по свободному вызову

Пусть m — потенциально применимый метод (§15.12.2.1) с арностью n и формальными типами параметров F1, ..., Fn, и пусть e1, ..., en — выражения фактических аргументов вызова метода. Тогда:

  • Если m является обобщенным методом, и вызов метода не предоставляет явных аргументов типа, то применимость метода определяется, как указано в §18.5.1.

  • Если m является обобщенным методом, и вызов метода предоставляет явные аргументы типа, то пусть R1, ..., Rp (p ≥ 1) — параметры типа m, Bl — объявленное ограничение Rl (1 ≤ l ≤ p), и U1, ..., Up — явные аргументы типа, заданные в вызове метода. Тогда m является применимым по свободному вызову, если оба следующих условия истинны:

    • Для 1 ≤ i ≤ n, если ei относится к применимости (§15.12.2.2), то ei совместим в контексте свободного вызова с Fi[R1:=U1, ..., Rp:=Up] (§5.3).

    • Для 1 ≤ l ≤ p, Ul <: Bl[R1:=U1, ..., Rp:=Up].

  • Если m не является обобщенным методом, то m является применимым по свободному вызову, если для 1 ≤ i ≤ n, либо ei совместим в контексте свободного вызова с Fi (§5.3), либо ei не относится к применимости.

Если не найден ни один метод, применимый по свободному вызову, поиск применимых методов продолжается на фазе 3 (§15.12.2.4).

В противном случае выбирается наиболее специфичный метод (§15.12.2.5) среди методов, применимых по свободному вызову.

15.12.2.4. Этап 3: Определение методов, применимых по вызову с переменным числом аргументов

Если метод с переменным числом аргументов имеет формальные типы параметров 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).

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 пунктов максимально специфичных методов следующим образом:

    1. Если наиболее специфичный метод является обобщенным, то throws пункты сначала адаптируются к параметрам типа наиболее специфичного метода (§8.4.4).

      Если наиболее специфичный метод не является обобщенным, но по крайней мере один максимально специфичный метод является обобщенным, то throws пункты сначала стираются.

    2. Затем, типы выбрасываемых исключений включают каждый тип E, который удовлетворяет следующим ограничениям:

      • E упоминается в одном из throws пунктов.

      • Для каждого throws пункта, E является подтипом некоторого типа, указанного в этом пункте.

    Эти правила вывода типа единственного метода из группы перегруженных методов также используются для определения типа функции функционального интерфейса (§9.9).

  • В противном случае, вызов метода является неопределённым, и возникает ошибка компиляции.

15.12.2.6. Тип вызова метода

Тип вызова наиболее специфичного доступного и применимого метода — это тип метода (§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).

    • В противном случае, тип вызова такой же, как и тип метода.

15.12.3. Этап компиляции 3: Подходит ли выбранный метод?

Если для вызова метода существует объявление наиболее специфичного метода, оно называется объявлением на этапе компиляции для вызова метода.

Ошибка на этапе компиляции, если аргумент вызова метода не совместим с его целевым типом, полученным из типа вызова объявления на этапе компиляции.

Если объявление на этапе компиляции применимо к вызову с переменным числом аргументов, то в случае, когда тип последнего формального параметра типа вызова метода является Fn[], ошибка на этапе компиляции возникает, если тип, являющийся стиранием Fn, недоступен (§6.6) в момент вызова.

Если объявление на этапе компиляции является void, то вызов метода должен быть выражением верхнего уровня (то есть, выражение в операторе выражения или в части ForInit или ForUpdate for оператора), иначе возникает ошибка на этапе компиляции. Такой вызов метода не производит никакого значения и поэтому должен использоваться только в ситуациях, где значение не требуется.

Кроме того, вопрос о том, является ли объявление на этапе компиляции подходящим, может зависеть от формы выражения вызова метода перед левой скобкой, как следует:

  • Если форма представляет собой ИмяМетода - то есть, просто Идентификатор - и объявление на этапе компиляции является методом экземпляра, то:

    • Ошибка на этапе компиляции, если вызов метода происходит в статическом контексте (§8.1.3).

    • В противном случае пусть T будет классом или интерфейсом для поиска (§15.12.1). Ошибка на этапе компиляции, если внутреннее вложенное объявление класса или интерфейса вызова метода не является T или внутренним классом T.

  • Если форма представляет собой ИмяТипа . [АргументыТипов] Идентификатор, то объявление на этапе компиляции должно быть static, иначе возникает ошибка на этапе компиляции.

  • Если форма представляет собой ИмяВыражения . [АргументыТипов] Идентификатор или Первичное . [АргументыТипов] Идентификатор, то объявление на этапе компиляции не должно быть методом static, объявленным в интерфейсе, иначе возникает ошибка на этапе компиляции.

  • Если форма представляет собой super . [АргументыТипов] Идентификатор, то:

    • Ошибка на этапе компиляции, если объявление на этапе компиляции является abstract.

    • Ошибка на этапе компиляции, если вызов метода происходит в статическом контексте.

  • Если форма представляет собой ИмяТипа . super . [АргументыТипов] Идентификатор, то:

    • Ошибка на этапе компиляции, если объявление на этапе компиляции является abstract.

    • Ошибка на этапе компиляции, если вызов метода происходит в статическом контексте.

    • Если ИмяТипа обозначает класс C, то если объявление класса или интерфейса, непосредственно окружающего вызов метода, не является C или внутренним классом C, возникает ошибка на этапе компиляции.

    • Если ИмяТипа обозначает интерфейс, пусть E будет объявлением класса или интерфейса, непосредственно окружающим вызов метода. Возникает ошибка на этапе компиляции, если существует метод, отличный от объявления на этапе компиляции, который переопределяет (§9.4.1) объявление на этапе компиляции из непосредственного суперкласса или непосредственного суперинтерфейса E.

      В случае, когда суперинтерфейс переопределяет метод, объявленный в интерфейсе предка, это правило предотвращает «пропуск» переопределения дочерним интерфейсом, просто добавив предка в его список непосредственных суперинтерфейсов. Правильный способ доступа к функциональности предка – через непосредственный суперинтерфейс, и только если этот интерфейс решает раскрыть желаемое поведение. (В альтернативном варианте программист свободен определить дополнительный суперинтерфейс, который раскрывает желаемое поведение с помощью super вызова метода.)

Типы параметров на этапе компиляции и результат на этапе компиляции определяются следующим образом:

  • Если объявление на этапе компиляции для вызова метода не является методом полиморфной сигнатуры, то:

    • Типы параметров на этапе компиляции являются типами формальных параметров объявления на этапе компиляции.

    • Результат на этапе компиляции – результат типа вызова объявления на этапе компиляции (§15.12.2.6).

  • Если объявление на этапе компиляции для вызова метода является методом полиморфной сигнатуры, то:

    • Типы параметров на этапе компиляции являются типами выражений фактических аргументов. Выражение аргумента, являющееся нулевым литералом null (§3.10.8), обрабатывается как имеющий тип Void.

    • Результат на этапе компиляции определяется следующим образом:

      • Если метод полиморфной сигнатуры является либо void, или имеет тип возврата, отличный от Object, результат на этапе компиляции – результат типа вызова объявления на этапе компиляции (§15.12.2.6).

      • В противном случае, если выражение вызова метода является оператором выражения, результат на этапе компиляции – void.

      • В противном случае, если выражение вызова метода является операндом выражения приведения типа (§15.16), результат на этапе компиляции – стирание типа выражения приведения типа (§4.6).

      • В противном случае, результат на этапе компиляции – тип возврата метода полиморфной сигнатуры, Object.

Метод является полиморфным по сигнатуре, если все следующие утверждения верны:

  • Он объявлен в классе java.lang.invoke.MethodHandle или классе java.lang.invoke.VarHandle.

  • Он имеет единственный параметр с переменным числом аргументов (§8.4.1), тип которого объявлен как Object[].

  • Он является native.

Затем с вызовом метода связывается следующая информация на этапе компиляции для использования во время выполнения:

  • Имя метода.

  • Квалифицирующий класс или интерфейс вызова метода ( §13.1).

  • Количество параметров и типы параметров во время компиляции, по порядку.

  • Результат времени компиляции.

  • Режим вызова, вычисляемый следующим образом:

    • Если объявление во время компиляции имеет модификатор static, то режим вызова равен static.

    • В противном случае, если часть вызова метода перед левой скобкой имеет форму super . Identifier или форму TypeName . super . Identifier, то режим вызова равен super.

    • В противном случае, если квалифицирующий класс или интерфейс вызова метода на самом деле является интерфейсом, то режим вызова равен interface.

    • В противном случае режим вызова равен virtual.

Если результат типа вызова объявления во время компиляции не является void, то тип выражения вызова метода получается путем применения преобразования захвата (§5.1.10) к типу возвращаемого значения типа вызова объявления во время компиляции.

15.12.4. Run-Time Evaluation of Method Invocation

Во время выполнения вызов метода требует пяти шагов. Во-первых, может быть вычислена целевая ссылка. Во-вторых, вычисляются выражения аргументов. В-третьих, проверяется доступность вызываемого метода. В-четвертых, находится фактический код для выполнения метода. В-пятых, создается новый кадр активации, при необходимости выполняется синхронизация, и управление передается в код метода.

15.12.4.1. Вычисление целевой ссылки (при необходимости)

Есть шесть случаев, которые следует учитывать в зависимости от формы вызова метода:

  • Если форма представляет собой MethodName — то есть просто Identifier — то:

    • Если режим вызова равен static, то целевая ссылка отсутствует.

    • В противном случае, пусть T будет объявлением типа, членом которого является метод, и пусть n будет целым числом таким, что T является n-м лексически охватывающим объявлением типа класса, чье объявление непосредственно содержит вызов метода. Целевой ссылкой является n-й лексически охватывающий экземпляр this.

      Это ошибка времени компиляции, если n-й лексически охватывающий экземпляр this не существует.

  • Если форма представляет собой TypeName . [TypeArguments] Identifier, то целевая ссылка отсутствует.

  • Если форма представляет собой ExpressionName . [TypeArguments] Identifier, то:

    • Если режим вызова равен static, то целевая ссылка отсутствует. ExpressionName вычисляется, но результат затем отбрасывается.

    • В противном случае целевой ссылкой является значение, обозначенное ExpressionName.

  • Если форма представляет собой Primary . [TypeArguments] Identifier, то:

    • Если режим вызова равен static, то целевая ссылка отсутствует. Выражение Primary вычисляется, но результат затем отбрасывается.

    • В противном случае выражение Primary вычисляется, и результат используется в качестве целевой ссылки.

    В любом случае, если вычисление выражения Primary завершается аварийно, то ни одна часть какого-либо выражения аргумента, похоже, не была вычислена, и вызов метода завершается аварийно по той же причине.

  • Если форма представляет собой super . [TypeArguments] Identifier, то целевой ссылкой является значение this.

  • Если форма представляет собой TypeName . super . [TypeArguments] Identifier, то если TypeName обозначает класс, целевой ссылкой является значение TypeName.this; в противном случае целевой ссылкой является значение this.

Пример 15.12.4.1-1. Целевые ссылки и static методы

Когда целевая ссылка вычисляется, а затем отбрасывается, поскольку режим вызова равен static, ссылка не проверяется, чтобы увидеть, является ли она null:

class Test1 {
    static void mountain() {
        System.out.println("Monadnock");
    }
    static Test1 favorite(){
        System.out.print("Mount ");
        return null;
    }
    public static void main(String[] args) {
        favorite().mountain();
    }
}

что выводит:

Mount Monadnock

Здесь favorite() возвращает null, но исключение NullPointerException не выдается.


Пример 15.12.4.1-2. Порядок вычисления при вызове метода

В рамках вызова метода экземпляра (§15.12) существует выражение, обозначающее объект, который будет вызван. Это выражение, похоже, полностью вычисляется до того, как будет вычислена какая-либо часть выражения аргумента для вызова метода.

Итак, например, в:

class Test2 {
    public static void main(String[] args) {
        String s = "one";
        if (s.startsWith(s = "two"))
            System.out.println("oops");
    }
}

вхождение s перед ".startsWith" вычисляется первым, перед выражением аргумента s = "two". Следовательно, ссылка на строку "one" запоминается как целевая ссылка до того, как локальная переменная s будет изменена для ссылки на строку "two". В результате метод startsWith вызывается для целевого объекта "one" с аргументом "two", поэтому результатом вызова является false, поскольку строка "one" не начинается с "two". Из этого следует, что тестовая программа не выводит "oops".


15.12.4.2. Оценка аргументов

Процесс оценки списка аргументов отличается в зависимости от того, является ли вызываемый метод методом с фиксированной арностью или методом с переменной арностью (§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. Далее оценка продолжается, используя значения аргументов, как описано ниже.

15.12.4.3. Проверка доступности типа и метода

В этом разделе:

  • Пусть D - класс, содержащий вызов метода.

  • Пусть Q - квалифицирующий класс или интерфейс вызова метода (§13.1).

  • Пусть m - имя метода, определённое на этапе компиляции (§15.12.3).

Реализация языка программирования 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.4. Поиск вызываемого метода

Как и в предыдущем разделе (§15.12.4.3):

  • Пусть Q — квалифицирующий класс или интерфейс вызова метода (§13.1).

  • Пусть m — метод, найденный в Q или в его суперклассе или суперинтерфейсе. (Обратите внимание, что m был лишь именем метода в предыдущем разделе; здесь это фактическое объявление.)

  • Пусть C — класс или интерфейс, который объявляет m.

Стратегия поиска вызываемого метода зависит от режима вызова:

  • Если режим вызова — static, то ссылка на целевой объект не нужна, и переопределение запрещено. Вызываемым методом будет метод m класса или интерфейса C.

  • В противном случае вызывается метод экземпляра, и есть ссылка на целевой объект. Если ссылка на целевой объект — null, в этот момент будет брошено исключение NullPointerException. В противном случае ссылка на целевой объект относится к целевому объекту и будет использована в качестве значения ключевого слова this в вызываемом методе. Затем рассматриваются три других возможности для режима вызова:

    • Если режим вызова — super, переопределение запрещено. Вызываемым методом будет метод m класса или интерфейса C. Если m равно abstract, будет брошено исключение AbstractMethodError.

    • В противном случае, если режим вызова — virtual, а Q и m совместно указывают на полиморфный метод сигнатуры (§15.12.3), то целевой объект является экземпляром java.lang.invoke.MethodHandle или java.lang.invoke.VarHandle. Целевой объект encapsulates состояние, которое сопоставляется с информацией, связанной с вызовом метода во время компиляции. Подробности этого сопоставления приведены в спецификации Java Virtual Machine, Java SE 21 Edition, и API платформы Java SE. Если сопоставление успешно, то либо метод, на который ссылается экземпляр java.lang.invoke.MethodHandle, вызывается непосредственно и немедленно, либо переменная, представленная экземпляром java.lang.invoke.VarHandle, обращается к ней напрямую и немедленно, и в обоих случаях процедура в §15.12.4.5 не выполняется. Если сопоставление не выполняется, будет брошено исключение java.lang.invoke.WrongMethodTypeException.

    • В противном случае режим вызова — interface или virtual.

      Если метод m класса или интерфейса C — private, то это вызываемый метод.

      В противном случае может произойти переопределение. Используется динамический поиск метода, описанный ниже, для определения вызываемого метода. Процедура поиска начинается с класса R, фактического класса времени выполнения целевого объекта.

      Обратите внимание, что для режима вызова interface, R обязательно реализует Q; для режима вызова virtual, R обязательно равен Q или является подклассом Q. Если целевой объект является массивом, то R — «класс», представляющий тип массива.

Процедура динамического поиска метода следующая. Пусть S — класс для поиска, начиная с R. Тогда:

  1. Если класс S содержит объявление метода, который переопределяет метод m класса или интерфейса C из R (§8.4.8.1), то этот переопределяющий метод является вызываемым методом, и процедура завершается.

  2. В противном случае, если у S есть суперкласс, то шаги 1 и 2 этой процедуры поиска выполняются рекурсивно с использованием непосредственного суперкласса S вместо S; вызываемый метод, если таковой имеется, является результатом рекурсивного вызова этой процедуры поиска.

  3. Если ни один метод не найден на предыдущих двух шагах, то в суперинтерфейсах S ищется подходящий метод.

    Рассматривается набор кандидатов-методов с следующими свойствами: (i) каждый метод объявлен в (прямом или косвенном) суперинтерфейсе R; (ii) каждый метод имеет имя и дескриптор, необходимые для вызова метода; (iii) каждый метод не является не-static и не является не-private; (iv) для каждого метода, где интерфейс объявления метода — I, нет другого метода, удовлетворяющего (i) – (iii), который объявлен в подинтерфейсе I.

    Если этот набор содержит метод по умолчанию, такой метод является вызываемым методом. В противном случае метод, не являющийся методом по умолчанию, из набора выбирается в качестве вызываемого метода.

Динамический поиск метода может вызвать следующие ошибки:

  • Если вызываемый метод — abstract, будет брошено исключение AbstractMethodError.

  • Если вызываемый метод — default, и в наборе кандидатов на шаге 3 выше появляется более одного метода по умолчанию, будет брошено исключение IncompatibleClassChangeError.

  • Если режим вызова — interface, и вызываемый метод не является ни public, ни private, будет брошено исключение IllegalAccessError.

Вышеописанная процедура (если она завершится без ошибок) найдёт не-abstract, доступный метод для вызова, при условии, что все классы и интерфейсы в программе были корректно скомпилированы. Однако, если это не так, могут возникнуть различные ошибки, как указано выше; дополнительные сведения о поведении Java Virtual Machine в этих обстоятельствах приведены в спецификации Java Virtual Machine, Java SE 21 Edition.

Процесс динамического поиска, хотя и описан здесь явно, часто будет реализован неявно, например, как побочный эффект построения и использования таблиц диспетчеризации методов на основе класса или построения других структур на основе класса, используемых для эффективной диспетчеризации.

Пример 15.12.4.4-1. Переопределение и вызов метода

class Point {
    final int EDGE = 20;
    int x, y;
    void move(int dx, int dy) {
        x += dx; y += dy;
        if (Math.abs(x) >= EDGE || Math.abs(y) >= EDGE)
            clear();
    }
    void clear() {
        System.out.println("\tPoint clear");
        x = 0; y = 0;
    }
}
class ColoredPoint extends Point {
    int color;
    void clear() {
        System.out.println("\tColoredPoint clear");
        super.clear();
        color = 0;
    }
}

Здесь подкласс ColoredPoint расширяет абстракцию clear, определённую его суперклассом Point. Это делается путём переопределения метода clear собственным методом, который вызывает метод clear своего суперкласса, используя форму super.clear().

Этот метод затем вызывается всякий раз, когда целевой объект для вызова clear — экземпляр ColoredPoint. Даже метод move в Point вызывает метод clear класса ColoredPoint, когда класс this — ColoredPoint, как показано выводом этой тестовой программы:

class Test1 {
    public static void main(String[] args) {
        Point p = new Point();
        System.out.println("p.move(20,20):");
        p.move(20, 20);

        ColoredPoint cp = new ColoredPoint();
        System.out.println("cp.move(20,20):");
        cp.move(20, 20);

        p = new ColoredPoint();
        System.out.println("p.move(20,20), p colored:");
        p.move(20, 20);
    }
}

что равно:

p.move(20,20):
        Point clear
cp.move(20,20):
        ColoredPoint clear
        Point clear
p.move(20,20), p colored:
        ColoredPoint clear
        Point clear

Переопределение иногда называется «поздняя привязка к себе»; в этом примере это означает, что ссылка на clear в теле Point.move (которая фактически является синтаксическим сокращением для this.clear) вызывает метод, выбранный «поздно» (во время выполнения, на основе класса времени выполнения объекта, на который ссылается this), а не метод, выбранный «раньше» (во время компиляции, только на основе типа this). Это даёт программисту мощный способ расширения абстракций и является ключевой идеей в объектно-ориентированном программировании.


Пример 15.12.4.4-2. Вызов метода с использованием super

Переопределённый метод экземпляра суперкласса может быть вызван, используя ключевое слово super для доступа к членам непосредственного суперкласса, минуя любое переопределение в классе, содержащем вызов метода.

При доступе к переменной экземпляра, super означает то же, что и приведение типа this (§15.11.2), но это равенство неверно для вызова метода. Это демонстрирует пример:

class T1 {
    String s() { return "1"; }
}
class T2 extends T1 {
    String s() { return "2"; }
}
class T3 extends T2 {
    String s() { return "3"; }
    void test() {
        System.out.println("s()=\t\t"          + s());
        System.out.println("super.s()=\t"      + super.s());
        System.out.println("((T2)this).s()=\t" + ((T2)this).s());
        System.out.println("((T1)this).s()=\t" + ((T1)this).s());
    }
}
class Test2 {
    public static void main(String[] args) {
        T3 t3 = new T3();
        t3.test();
    }
}

что даёт вывод:

s()=            3
super.s()=      2
((T2)this).s()= 3
((T1)this).s()= 3

Приведения к типам T1 и T2 не изменяют метод, который вызывается, потому что метод экземпляра для вызова выбирается в соответствии с классом времени выполнения объекта, на который ссылается this. Приведение не изменяет класс объекта; оно только проверяет, что класс совместим с указанным типом.


15.12.4.5. Создание фрейма, синхронизация, передача управления

Метод 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.13. Выражения ссылок на методы

Выражение ссылки на метод используется для ссылки на вызов метода без фактического выполнения вызова. Определенные формы выражения ссылки на метод также позволяют создавать экземпляры класса (§15.9) или создавать массивы (§15.10), как будто это был вызов метода.

MethodReference:
ИмяВыражения :: [АргументыТипов] Идентификатор
Основное :: [АргументыТипов] Идентификатор
ТипСсылка :: [АргументыТипов] Идентификатор
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.13.1. Объявление метода на этапе компиляции

Объявление на этапе компиляции выражения ссылки на метод — это метод, на который ссылается выражение. В особых случаях объявление на этапе компиляции фактически не существует, но является условным методом, представляющим создание экземпляра класса или создание массива. Выбор объявления на этапе компиляции зависит от типа функции, на который направлено выражение, точно так же, как объявление на этапе компиляции вызова метода зависит от аргументов вызова (§15.12.3).

Поиск объявления на этапе компиляции отражает процесс поиска методов для вызовов методов в §15.12.1 и §15.12.2, как показано ниже:

  • Сначала определяется тип для поиска:

    • Если выражение ссылки на метод имеет вид ИмяВыражения :: [АргументыТипов] Идентификатор или ОсновноеВыражение :: [АргументыТипов] Идентификатор, тип для поиска — это тип выражения, предшествующего токену ::.

    • Если выражение ссылки на метод имеет вид ТипСсылка :: [АргументыТипов] Идентификатор, тип для поиска — это результат применения преобразования захвата (§5.1.10) к ТипСсылка.

    • Если выражение ссылки на метод имеет вид super :: [АргументыТипов] Идентификатор, тип для поиска — это тип суперкласса немедленно окружающего объявления класса или интерфейса выражения ссылки на метод.

      Пусть T — это объявление класса или интерфейса, непосредственно окружающее выражение ссылки на метод. Ошибка компиляции, если T — это класс Object или интерфейс.

    • Если выражение ссылки на метод имеет вид ИмяТипа . super :: [АргументыТипов] Идентификатор, то если ИмяТипа обозначает класс, тип для поиска — это тип суперкласса указанного класса; в противном случае ИмяТипа обозначает интерфейс для поиска.

      Ошибка компиляции, если ИмяТипа не является лексически окружающим объявлением класса или интерфейса выражения ссылки на метод, а также непосредственным суперинтерфейсом непосредственно окружающего объявления класса или интерфейса выражения ссылки на метод.

      Ошибка компиляции, если ИмяТипа — это класс Object.

      Ошибка компиляции, если ИмяТипа — это интерфейс, и существует другой непосредственный суперкласс или непосредственный суперинтерфейс непосредственно окружающего объявления класса или интерфейса выражения ссылки на метод, J, такой, что J является подклассом или подинтерфейсом ИмяТипа.

    • Для двух других форм (с участием :: new) метод, на который ссылаются, является условным, и тип для поиска не существует.

  • Во-вторых, для заданного целевого типа функции с n параметрами определяется набор потенциально применимых методов:

    • Если выражение ссылки на метод имеет вид ТипСсылка :: [АргументыТипов] Идентификатор, потенциально применимые методы:

      • методы-члены типа для поиска, которые потенциально применимы (§15.12.2.1) для вызова метода, который называет Идентификатор, имеет арность n, имеет аргументы типа АргументыТипов и появляется в том же классе, что и выражение ссылки на метод; плюс

      • методы-члены типа для поиска, которые потенциально применимы для вызова метода, который называет Идентификатор, имеет арность n-1, имеет аргументы типа АргументыТипов и появляется в том же классе, что и выражение ссылки на метод.

      Рассматриваются две разные арности, n и n-1, чтобы учесть возможность того, что эта форма относится либо к методу-static, либо к методу экземпляра.

    • Если выражение ссылки на метод имеет вид ТипКласса :: [АргументыТипов] new, потенциально применимые методы — это набор условных методов, соответствующих конструкторам ТипКласса.

      Если ТипКласса является сырым типом, но не является не-static членом типа сырого типа, кандидатными методами-членами являются те, которые указаны в §15.9.3 для выражения создания экземпляра класса, которое использует <> для исключения аргументов типа класса. В противном случае, кандидатные методы-члены — это конструкторы ТипКласса, рассматриваемые как методы с типом возврата ТипКласса.

      Среди этих кандидатов потенциально применимые методы — это условные методы, которые были бы потенциально применимы для вызова метода, имеющего арность n, аргументы типа АргументыТипов и находящегося в том же классе, что и выражение ссылки на метод.

    • Если выражение ссылки на метод имеет вид ТипМассива :: new, рассматривается один условный метод. Метод имеет один параметр типа int, возвращает ТипМассива и не имеет throws. Если n = 1, это единственный потенциально применимый метод; в противном случае потенциально применимых методов нет.

    • Для всех остальных форм потенциально применимые методы — это методы-члены типа для поиска, которые потенциально применимы для вызова метода, который называет Идентификатор, имеет арность n, имеет аргумент типа АргументыТипов и появляется в том же классе, что и выражение ссылки на метод.

  • Наконец, если нет потенциально применимых методов, то нет объявления во время компиляции.

    В противном случае, учитывая целевой тип функции с параметрами P1, ..., Pn и набор потенциально применимых методов, объявление во время компиляции выбирается следующим образом:

    • Если выражение ссылки на метод имеет вид ReferenceType :: [TypeArguments] Identifier, то выполняются два поиска наиболее специфичного применимого метода. Каждый поиск выполняется, как указано в §15.12.2.2 по §15.12.2.5 с пояснениями ниже. Каждый поиск генерирует набор применимых методов и, возможно, выделяет наиболее специфичный метод из набора. В случае ошибки, как указано в §15.12.2.4, набор применимых методов пуст. В случае ошибки, как указано в §15.12.2.5, нет наиболее специфичного метода.

      В первом поиске ссылка на метод обрабатывается так, как если бы это было обращение с выражениями аргументов типов P1, ..., Pn. Типовые аргументы, если таковые имеются, задаются выражением ссылки на метод.

      Во втором поиске, если P1, ..., Pn не пусто и P1 является подтипом ReferenceType, то выражение ссылки на метод обрабатывается так, как если бы это было выражение вызова метода с выражениями аргументов типов P2, ..., Pn. Если ReferenceType является сырым типом, и существует параметризация этого типа, G<...>, который является надтипом P1, тип для поиска — результат преобразования захвата (§5.1.10), применённого к G<...>; в противном случае, тип для поиска такой же, как тип первого поиска. Типовые аргументы, если таковые имеются, задаются выражением ссылки на метод.

      Если первый поиск приводит к наиболее специфичному методу, который является static, и набор применимых методов, полученный во втором поиске, не содержит методов, отличных от static, то объявление во время компиляции — это наиболее специфичный метод первого поиска.

      В противном случае, если набор применимых методов, полученный в первом поиске, не содержит static методов, а второй поиск даёт наиболее специфичный метод, который не является static, то объявление во время компиляции — это наиболее специфичный метод второго поиска.

      В противном случае, объявление во время компиляции отсутствует.

    • Для всех других форм выражения ссылки на метод выполняется один поиск наиболее специфичного применимого метода. Поиск выполняется, как указано в §15.12.2.2 по §15.12.2.5 с пояснениями ниже.

      Ссылка на метод обрабатывается так, как если бы это было обращение с выражениями аргументов типов P1, ..., Pn; типовые аргументы, если таковые имеются, задаются выражением ссылки на метод.

      Если поиск приводит к ошибке, как указано в §15.12.2.2 по §15.12.2.5, или если наиболее специфичный применимый метод является static, то объявление во время компиляции отсутствует.

      В противном случае, объявление во время компиляции — это наиболее специфичный применимый метод.

Ошибка во время компиляции, если выражение ссылки на метод имеет вид ReferenceType :: [TypeArguments] Identifier, и объявление во время компиляции является static, и ReferenceType не является простым или квалифицированным именем (§6.2).

Ошибка во время компиляции, если выражение ссылки на метод имеет вид super :: [TypeArguments] Identifier или TypeName . super :: [TypeArguments] Identifier, и объявление во время компиляции является abstract.

Ошибка во время компиляции, если выражение ссылки на метод имеет вид super :: [TypeArguments] Identifier или TypeName . super :: [TypeArguments] Identifier, и выражение ссылки на метод используется в статическом контексте (§8.1.3).

Ошибка во время компиляции, если выражение ссылки на метод имеет вид TypeName . super :: [TypeArguments] Identifier, и TypeName обозначает класс C, а ближайший к выражению ссылки на метод класс или интерфейс объявления метода не является C или внутренним классом C.

Ошибка во время компиляции, если выражение ссылки на метод имеет вид TypeName . super :: [TypeArguments] Identifier, и TypeName обозначает интерфейс, и существует метод, отличный от объявления во время компиляции, который переопределяет объявление во время компиляции из непосредственного суперкласса или непосредственного суперинтерфейса класса или интерфейса, чьё объявление непосредственно включает выражение ссылки на метод (§8.4.8, §9.4.1).

Ошибка во время компиляции, если выражение ссылки на метод имеет вид ClassType :: [TypeArguments] new и произошла бы ошибка во время компиляции при определении включающей инстанции для ClassType, как указано в §15.9.2 (обрабатывая выражение ссылки на метод, как если бы это было выражение создания экземпляра класса без квалификатора).

Выражение ссылки на метод вида ReferenceType :: [TypeArguments] Identifier может интерпретироваться по-разному. Если Identifier относится к методу экземпляра, то неявная лямбда-функция имеет дополнительный параметр по сравнению с тем, если Identifier относится к методу static. Возможно, что ReferenceType имеет оба типа применимых методов, поэтому алгоритм поиска выше идентифицирует их отдельно, так как для каждого случая есть разные типы параметров.

Пример неоднозначности:

interface Fun<T,R> { R apply(T arg); }

class C {
    int size() { return 0; }
    static int size(Object arg) { return 0; }

    void test() {
        Fun<C, Integer> f1 = C::size;
          // Error: instance method size()
          // or static method size(Object)?
    }
}

Эту неоднозначность невозможно разрешить, предоставив применимый метод экземпляра, который более специфичен, чем применимый метод static:

interface Fun<T,R> { R apply(T arg); }

class C {
    int size() { return 0; }
    static int size(Object arg) { return 0; }
    int size(C arg) { return 0; }

    void test() {
        Fun<C, Integer> f1 = C::size;
          // Error: instance method size()
          // or static method size(Object)?
    }
}

Поиск достаточно умен, чтобы игнорировать неоднозначности, в которых все применимые методы (из обоих поисков) являются методами экземпляра:

interface Fun<T,R> { R apply(T arg); }

class C {
    int size() { return 0; }
    int size(Object arg) { return 0; }
    int size(C arg) { return 0; }

    void test() {
        Fun<C, Integer> f1 = C::size;
          // OK: reference is to instance method size()
    }
}

Для удобства, когда имя обобщённого типа используется для ссылки на метод экземпляра (где получатель становится первым параметром), целевой тип используется для определения типовых аргументов. Это облегчает использование, например, Pair::first вместо Pair<String,Integer>::first. Аналогично, ссылка на метод, подобная Pair::new, рассматривается как создание экземпляра "ромба" (new Pair<>()). Поскольку "ромб" неявный, этот вид не создаёт сырой тип; фактически, нет способа выразить ссылку на конструктор сырого типа.

Для некоторых выражений ссылки на метод существует только одно возможное объявление во время компиляции с одним возможным типом вызова (§15.12.2.6), независимо от целевого типа функции. Такие выражения ссылки на метод называются точными. Выражение ссылки на метод, которое не является точным, называется неточным.

Выражение ссылки на метод, заканчивающееся Identifier, является точным, если оно удовлетворяет всем следующим условиям:

  • Если выражение ссылки на метод имеет вид ТипСсылы :: [АргументыТипов] Идентификатор, то ТипСсылы не обозначает необработанный тип.

  • Тип для поиска имеет ровно один метод-член с именем Идентификатор, который доступен классу или интерфейсу, в котором появляется выражение ссылки на метод.

  • Этот метод не является методом с переменным числом аргументов (§8.4.1).

  • Если этот метод является обобщенным (§8.4.4), то выражение ссылки на метод предоставляет АргументыТипов.

Выражение ссылки на метод вида ТипКласса :: [АргументыТипов] new является точным, если оно удовлетворяет всем следующим условиям:

  • Тип, обозначаемый ТипКласса, не является необработанным или является не-static членом типа необработанного типа.

  • Тип, обозначаемый ТипКласса, имеет ровно один конструктор, который доступен классу или интерфейсу, в котором появляется выражение ссылки на метод.

  • Этот конструктор не является конструктором с переменным числом аргументов.

  • Если этот конструктор является обобщенным, то выражение ссылки на метод предоставляет АргументыТипов.

Выражение ссылки на метод вида ТипМассива :: new всегда является точным.

15.13.2. Тип ссылки на метод

Выражение ссылки на метод совместимо в контексте присваивания, вызова или приведения типов с типом назначения T, если T является типом функционального интерфейса (§9.8), и выражение соответствует типу функции базового целевого типа, полученного из T.

Базовый целевой тип выводится из T следующим образом:

  • Если T является типом функционального интерфейса с параметрами-подстановочными знаками, то базовый целевой тип — это неподстановочное параметризованное представление (§9.9) T.

  • В противном случае базовый целевой тип — T.

Выражение ссылки на метод соответствует типу функции, если оба следующих утверждения истинны:

  • Тип функции идентифицирует одно объявление во время компиляции, соответствующее ссылке.

  • Выполняется одно из следующих условий:

    • Результат типа функции — void.

    • Результат типа функции — R, и результат применения преобразования захвата (§5.1.10) к типу возвращаемого значения типа вызова (§15.12.2.6) выбранного объявления во время компиляции — R' (где R — тип назначения, который можно использовать для вывода R'), и ни R, ни R' не являются void, и R' совместим с R в контексте присваивания.

Если для применимости объявления во время компиляции было необходимо преобразование без проверки, и это преобразование вызовет предупреждение без проверки в контексте вызова, то возникает предупреждение без проверки во время компиляции, если оно не подавлено @SuppressWarnings (§9.6.4.5).

Если для совместимости типа возвращаемого значения R' с типом возвращаемого значения типа функции R, описанном выше, было необходимо преобразование без проверки, и это преобразование вызовет предупреждение без проверки в контексте присваивания, то возникает предупреждение без проверки во время компиляции, если оно не подавлено @SuppressWarnings.

Если выражение ссылки на метод совместимо с типом назначения T, то типом выражения U является базовый целевой тип, полученный из T.

Ошибка компиляции, если какой-либо класс или интерфейс, упомянутый в U или в типе функции U, недоступен (§6.6) из класса или интерфейса, в котором появляется выражение ссылки на метод.

Для каждого метода-члена static метода m U, если тип функции U имеет подсигнатуру сигнатуры m, то предполагаемый метод, тип которого — тип функции U, считается переопределяющим m, и могут возникнуть ошибки компиляции или предупреждения без проверки, указанные в §8.4.8.3.

Для каждого типа исключения с проверкой X, указанного в throws разделе типа вызова объявления во время компиляции, X или суперкласс X должен быть указан в throws разделе типа функции U, в противном случае возникнет ошибка компиляции.

Ключевая идея, лежащая в основе определения совместимости, заключается в том, что ссылка на метод совместима тогда и только тогда, когда эквивалентное выражение лямбда-выражения (x, y, z) -> exp.<T1, T2>method(x, y, z) совместимо. (Это неформально, и существуют проблемы, которые затрудняют или делают невозможным формальное определение семантики в терминах такого переписывания.)

Эти правила совместимости предоставляют удобный механизм для преобразования из одного функционального интерфейса в другой:

Task t = () -> System.out.println("hi");
Runnable r = t::invoke;

Реализация может быть оптимизирована таким образом, что при передаче объекта, полученного от лямбда-выражения, и его преобразовании к различным типам это не приведет к появлению большого количества уровней адаптации в теле ядра лямбда-выражения.

В отличие от лямбда-выражения, ссылка на метод может соответствовать обобщенному типу функции (то есть типу функции, имеющему параметры типа). Это связано с тем, что лямбда-выражению потребовалось бы объявление параметров типа, а такая синтаксическая конструкция отсутствует; в то же время для ссылки на метод такое объявление не требуется. Например, следующая программа является корректной:


interface ListFactory {
    <T> List<T> make();
}

ListFactory lf  = ArrayList::new;
List<String> ls = lf.make();
List<Number> ln = lf.make();

15.13.3. Выполнение ссылок на методы во время выполнения

Во время выполнения оценка выражения ссылки на метод аналогична оценке выражения создания экземпляра класса, поскольку нормальное завершение приводит к получению ссылки на объект. Оценка выражения ссылки на метод отличается от вызова самого метода.

Во-первых, если выражение ссылки на метод начинается с 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 являются формальными параметрами метода вызова и где:

    • Включающий экземпляр для нового объекта, если таковой имеется, происходит из сайта выражения ссылки на метод, как указано в §15.9.2.

    • Конструктор, который необходимо вызвать, является конструктором, который соответствует объявлению времени компиляции ссылки на метод (§15.13.1).

  • Если форма имеет вид Type[]k :: new (k ≥ 1), то тело метода вызова имеет тот же эффект, что и выражение создания массива формы new Type [ 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.14. Постфиксные выражения

Постфиксные выражения включают использование постфиксных ++ и -- операторов. Имена не считаются первичными выражениями (§15.8), но обрабатываются отдельно в грамматике для избежания определенных неоднозначностей. Они становятся взаимозаменяемыми только здесь, на уровне приоритета постфиксных выражений.

PostfixExpression:
Первичное
ИмяВыражения
ПостфиксноеУвеличение
ПостфиксноеУменьшение

15.14.1. Имена выражений

Правила оценки имен выражений приведены в §6.5.6.

15.14.2. Постфиксный оператор инкремента ++

Постфиксное выражение, за которым следует оператор ++, является постфиксным выражением инкремента.

PostIncrementExpression:
PostfixExpression ++

Результат постфиксного выражения должен быть переменной, конвертируемой (§5.1.8) в числовой тип, в противном случае возникает ошибка компиляции.

Тип постфиксного выражения инкремента — тип переменной. Результат постфиксного выражения инкремента — это значение, а не переменная.

Во время выполнения, если оценка операндного выражения завершается прерыванием, то постфиксное выражение инкремента завершается прерыванием по той же причине и инкрементация не выполняется. В противном случае значение 1 добавляется к значению переменной, а сумма сохраняется обратно в переменную. Перед добавлением выполняется бинарное числовое продвижение (§5.6) над значением 1 и значением переменной. При необходимости сумма сужается с помощью сужающего примитивного преобразования (§5.1.3) и/или подвергается преобразованию упаковки (§5.1.7) в тип переменной перед сохранением. Значение постфиксного выражения инкремента — значение переменной до сохранения нового значения.

Обратите внимание, что бинарное числовое продвижение, упомянутое выше, может включать преобразование распаковки (§5.1.8).

Переменная, объявленная final, не может быть инкрементирована, потому что при использовании доступа к такой final переменной как выражения результат — это значение, а не переменная. Таким образом, она не может использоваться в качестве операнда постфиксного оператора инкремента.

15.14.3. Постфиксный оператор декремента --

Постфиксное выражение, за которым следует оператор --, является постфиксным выражением декремента.

PostDecrementExpression:
PostfixExpression --

Результат постфиксного выражения должен быть переменной, конвертируемой (§5.1.8) в числовой тип, в противном случае возникает ошибка компиляции.

Тип постфиксного выражения декремента — тип переменной. Результат постфиксного выражения декремента — это значение, а не переменная.

Во время выполнения, если оценка операндного выражения завершается прерыванием, то постфиксное выражение декремента завершается прерыванием по той же причине и декрементация не выполняется. В противном случае значение 1 вычитается из значения переменной, а разность сохраняется обратно в переменную. Перед вычитанием выполняется бинарное числовое продвижение (§5.6) над значением 1 и значением переменной. При необходимости разность сужается с помощью сужающего примитивного преобразования (§5.1.3) и/или подвергается преобразованию упаковки (§5.1.7) в тип переменной перед сохранением. Значение постфиксного выражения декремента — значение переменной до сохранения нового значения.

Обратите внимание, что бинарное числовое продвижение, упомянутое выше, может включать преобразование распаковки (§5.1.8).

Переменная, объявленная final, не может быть декрементирована, потому что при использовании доступа к такой final переменной как выражения результат — это значение, а не переменная. Таким образом, она не может использоваться в качестве операнда постфиксного оператора декремента.

15.15. Унарные операторы

Операторы +, -, ++, --, ~, !, а также оператор приведения типов (§15.16) называются унарными операторами. Унарное выражение — это либо унарный оператор, применяемый к операнду, либо выражение switch (§15.28).

UnaryExpression:
ПрефиксноеУвеличение
ПрефиксноеУменьшение
+ UnaryExpression
- UnaryExpression
UnaryExpressionNotPlusMinus
PreIncrementExpression:
++ UnaryExpression
PreDecrementExpression:
-- UnaryExpression
UnaryExpressionNotPlusMinus:
ПостфиксноеВыражение
~ UnaryExpression
! UnaryExpression
ВыражениеПриведенияТипов
ВыражениеSwitch

Выражения с унарными операторами группируются справа налево, поэтому -~x означает то же, что и -(~x).

Эта часть грамматики содержит некоторые ухищрения, чтобы избежать двух потенциальных синтаксических неоднозначностей.

Первая потенциальная неоднозначность возникает в выражениях, таких как (p)+q, что для программиста на C или C++ может показаться, что оно может быть либо приведением к типу p унарного оператора +, действующего над q, либо бинарным сложением двух величин p и q. В C и C++ анализатор обрабатывает эту проблему, выполняя ограниченный объем семантического анализа во время разбора, чтобы знать, является ли p именем типа или именем переменной.

Java использует другой подход. Результат оператора + должен быть числовым, а все имена типов, участвующие в приведениях числовых значений, являются известными ключевыми словами. Таким образом, если p — это ключевое слово, обозначающее примитивный тип, то (p)+q может иметь смысл только как приведение унарного выражения. Однако, если p не является ключевым словом, обозначающим примитивный тип, то (p)+q может иметь смысл только как бинарная арифметическая операция. Аналогичные замечания относятся к оператору -. Грамматика разделяет CastExpression на несколько случаев, чтобы сделать это различие:

CastExpression:
( ПримитивныйТип ) UnaryExpression
( ТипСсылка {ДополнительноеОграничение} ) UnaryExpressionNotPlusMinus
( ТипСсылка {ДополнительноеОграничение} ) LambdaExpression

Нетерминал UnaryExpression включает все унарные операторы, но нетерминал UnaryExpressionNotPlusMinus исключает использование всех унарных операторов, которые также могут быть бинарными операторами, которые в Java являются + и -.

Вторая потенциальная неоднозначность заключается в том, что выражение (p)++ может показаться программисту на C или C++, как постфиксное инкрементирование выражения в скобках или начало приведения, например, в (p)++q. Как и прежде, анализаторы C и C++ знают, является ли p именем типа или именем переменной. Но анализатор, использующий только предпросмотр одного токена и без семантического анализа во время разбора, не сможет сказать, когда ++ — это токен предпросмотра, является ли (p) выражением Primary или следует оставить его для последующего рассмотрения как части CastExpression.

В Java результат оператора ++ должен быть числовым, а все имена типов, участвующие в приведениях числовых значений, являются известными ключевыми словами. Таким образом, если p — это ключевое слово, обозначающее примитивный тип, то (p)++ может иметь смысл только как приведение выражения с префиксным инкрементом, и обязательно должен быть операнд, например, q, после ++. Однако, если p не является ключевым словом, обозначающим примитивный тип, то (p)++ может иметь смысл только как постфиксное инкрементирование p. Аналогичные замечания относятся к оператору --. Нетерминал UnaryExpressionNotPlusMinus поэтому также исключает использование префиксных операторов ++ и --.

15.15.1. Оператор префиксного инкремента ++

Унарное выражение, предваряемое оператором ++, является выражением префиксного инкремента.

Результат унарного выражения должен быть переменной типа, преобразуемого (§5.1.8) в числовой тип; в противном случае возникает ошибка компиляции.

Тип выражения префиксного инкремента — это тип переменной. Результат выражения префиксного инкремента — это значение, а не переменная.

Во время выполнения, если вычисление выражения-операнда завершается неожиданно, то выражение префиксного инкремента завершается по той же причине, и инкрементирование не выполняется. В противном случае к значению переменной добавляется значение 1, и сумма записывается обратно в переменную. Перед сложением выполняется двоичное числовое продвижение (§5.6) над значением 1 и значением переменной. При необходимости сумма сужается с помощью сужающего преобразования примитивных типов (§5.1.3) и/или подвергается преобразованию упаковывания (§5.1.7) к типу переменной перед записью. Значение выражения префиксного инкремента — это значение переменной после записи нового значения.

Обратите внимание, что двоичное числовое продвижение, упомянутое выше, может включать преобразование распаковки (§5.1.8).

Переменную, объявленную final, нельзя инкрементировать, так как при использовании доступа к такой переменной как выражение, результат является значением, а не переменной. Поэтому она не может использоваться в качестве операнда оператора префиксного инкремента.

15.15.2. Оператор префиксного декремента --

Унарное выражение, предваряемое оператором --, является выражением префиксного декремента.

Результат унарного выражения должен быть переменной типа, преобразуемого (§5.1.8) в числовой тип; в противном случае возникает ошибка компиляции.

Тип выражения префиксного декремента — это тип переменной. Результат выражения префиксного декремента — это значение, а не переменная.

Во время выполнения, если вычисление выражения-операнда завершается неожиданно, то выражение префиксного декремента завершается по той же причине, и декрементирование не выполняется. В противном случае из значения переменной вычитается значение 1, и разность записывается обратно в переменную. Перед вычитанием выполняется двоичное числовое продвижение (§5.6) над значением 1 и значением переменной. При необходимости разность сужается с помощью сужающего преобразования примитивных типов (§5.1.3) и/или подвергается преобразованию упаковывания (§5.1.7) к типу переменной перед записью. Значение выражения префиксного декремента — это значение переменной после записи нового значения.

Обратите внимание, что двоичное числовое продвижение, упомянутое выше, может включать преобразование распаковки (§5.1.8).

Переменную, объявленную final, нельзя декрементировать, так как при использовании доступа к такой переменной как выражение, результат является значением, а не переменной. Поэтому она не может использоваться в качестве операнда оператора префиксного декремента.

15.15.3. Унарный плюс-оператор +

Тип выражения операнда унарного + оператора должен быть типом, преобразуемым (§5.1.8) к примитивному числовому типу, иначе произойдёт ошибка на стадии компиляции.

Над операндом выполняется унарное числовое продвижение (§5.6). Тип выражения унарного плюс — это продвинутый тип операнда. Результат выражения унарного плюс — это значение, а не переменная, даже если результатом выражения операнда является переменная.

Во время выполнения значение выражения унарного плюс — это продвинутое значение операнда.

15.15.4. Унарный минус-оператор -

Тип выражения операнда унарного - оператора должен быть типом, преобразуемым (§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.

  • Если операнд — бесконечность, результат — бесконечность противоположного знака.

  • Если операнд — ноль, результат — ноль противоположного знака.

15.15.5. Битовый дополняющий оператор ~

Тип выражения операнда унарного ~ оператора должен быть типом, преобразуемым (§5.1.8) к примитивному целочисленному типу, иначе произойдёт ошибка на стадии компиляции.

Над операндом выполняется унарное числовое продвижение (§5.6). Тип выражения унарного битового дополнения — это продвинутый тип операнда.

Во время выполнения значение выражения унарного битового дополнения — это битовое дополнение продвинутого значения операнда. Во всех случаях, ~x равно (-x)-1.

15.15.6. Логический дополняющий оператор !

Тип выражения операнда унарного ! оператора должен быть boolean или Boolean, иначе произойдёт ошибка на стадии компиляции.

Тип выражения унарного логического дополнения — это boolean.

Во время выполнения операнд преобразуется к типу true, если необходимо. Значение выражения унарного логического дополнения — false, если значение (возможно, преобразованного) операнда равно false, и true, если значение (возможно, преобразованного) операнда равно true.

15.16. Выражения приведения типов

Выражение приведения типов преобразует во время выполнения значение одного числового типа в аналогичное значение другого числового типа; или подтверждает на этапе компиляции, что тип выражения является boolean; или проверяет во время выполнения, что значение ссылки относится к объекту, класс которого совместим с указанным типом ссылки или списком типов ссылок, или который воплощает значение примитивного типа.

CastExpression:
( PrimitiveType ) UnaryExpression
( ReferenceType {AdditionalBound} ) UnaryExpressionNotPlusMinus
( ReferenceType {AdditionalBound} ) LambdaExpression

Следующая производная из §4.4 показана здесь для удобства:

AdditionalBound:
& InterfaceType

Скобки и тип или список типов, которые они содержат, иногда называются оператором приведения типов.

Если оператор приведения типов содержит список типов, то есть ReferenceType, за которым следуют один или несколько AdditionalBound элементов, то все перечисленное ниже должно быть верно, иначе произойдёт ошибка на стадии компиляции:

  • ReferenceType должен обозначать класс или интерфейсный тип.

  • Стирания (§4.6) всех перечисленных типов должны быть попарно различными.

  • Никакие два перечисленных типа не могут быть подтипами разных параметризаций одного и того же обобщённого интерфейса.

Целевой тип для контекста приведения типов (§5.5), введённый выражением приведения типов, — это либо PrimitiveType, либо ReferenceType (если за ним не следуют AdditionalBound элементы) в операторе приведения типов, или тип пересечения, обозначаемый ReferenceType и AdditionalBound элементами в операторе приведения типов.

Тип выражения приведения типов является результатом применения преобразования захвата (§5.1.10) к этому целевому типу.

Приведения типов могут использоваться для явного «помечания» лямбда-выражения или выражения ссылки на метод конкретным целевым типом. Для обеспечения надлежащей гибкости целевой тип может быть списком типов, обозначающих тип пересечения, при условии, что пересечение индуцирует функциональный интерфейс (§9.8).

Результат выражения приведения типов — не переменная, а значение, даже если результатом оценки выражения операнда является переменная.

Если тип операнда на этапе компиляции не может быть преобразован преобразованием приведения (§5.5) к целевому типу, указанному оператором приведения типов, то происходит ошибка на этапе компиляции.

В противном случае, во время выполнения, значение операнда преобразуется (при необходимости) преобразованием приведения типов к целевому типу, указанному в операторе приведения типов.

Если приведение типов во время выполнения оказывается недопустимым, выбрасывается ClassCastException.

Некоторые приведения типов приводят к ошибке на этапе компиляции. Некоторые приведения типов могут быть доказаны на этапе компиляции, что они всегда будут корректными во время выполнения. Например, всегда корректно преобразовать значение типа класса в тип его суперкласса; такое приведение типов не должно требовать особых действий во время выполнения. Наконец, некоторые приведения типов нельзя доказать ни всегда корректными, ни всегда некорректными на этапе компиляции. Такие приведения типов требуют проверки во время выполнения. Подробности см. в §5.5.

15.17. Операторы умножения

Операторы *, / и % называются операторами умножения.

MultiplicativeExpression:
UnaryExpression
MultiplicativeExpression * UnaryExpression
MultiplicativeExpression / UnaryExpression
MultiplicativeExpression % UnaryExpression

Операторы умножения имеют одинаковый приоритет и являются левоассоциативными (они группируются слева направо).

Тип каждого операнда оператора умножения должен быть типом, преобразуемым (§5.1.8) в примитивный числовой тип, в противном случае произойдёт ошибка компиляции.

На операнды применяется двоичное числовое продвижение (§5.6).

Обратите внимание, что двоичное числовое продвижение может включать преобразование unboxing (§5.1.8).

Тип выражения умножения — это продвинутый тип его операндов.

Если продвинутый тип — int или long, то выполняется целочисленная арифметика.

Если продвинутый тип — float или double, то выполняется арифметика с плавающей запятой.

15.17.1. Оператор умножения *

Двоичный оператор * выполняет умножение, получая произведение своих операндов.

Умножение является коммутативной операцией, если выражения операндов не имеют побочных эффектов.

Целочисленное умножение ассоциативно, когда все операнды одного типа.

Умножение с плавающей запятой не является ассоциативным.

Если целочисленное умножение переполняется, то результатом являются младшие биты математического произведения, представленные в каком-либо достаточно большом формате дополнения до двух. В результате, если переполнение происходит, то знак результата может не совпадать со знаком математического произведения двух значений операндов.

Результат умножения с плавающей запятой определяется правилами арифметики IEEE 754:

  • Если любой операнд — NaN, результатом является NaN.

  • Если результат не NaN, его знак положительный, если оба операнда имеют одинаковый знак, и отрицательный, если операнды имеют разные знаки.

  • Умножение бесконечности на ноль приводит к NaN.

  • Умножение бесконечности на конечное значение приводит к бесконечности соответствующего знака. Знак определяется вышеизложенным правилом.

  • В оставшихся случаях, когда не участвуют бесконечность или NaN, вычисляется точное математическое произведение.

    Если модуль произведения слишком велик для представления, мы говорим о переполнении; в этом случае результатом является бесконечность соответствующего знака.

    В противном случае произведение округляется до ближайшего представимого значения с помощью правила округления до ближайшего (§15.4). Язык программирования Java требует поддержки постепенного понижения как определено в IEEE 754.

Несмотря на то, что может произойти переполнение, понижение, потеря информации, вычисление оператора умножения * никогда не вызывает исключение во время выполнения.

15.17.2. Оператор деления /

Двоичный оператор / выполняет деление, получая частное своих операндов. Операнд слева — это делимое, а операнд справа — делитель.

Целочисленное деление округляется к 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.

Несмотря на то, что может произойти переполнение, понижение, деление на ноль, или потеря информации, вычисление оператора деления с плавающей запятой / никогда не вызывает исключение во время выполнения.

15.17.3. Оператор остатка от деления %

Двоичный % оператор возвращает остаток от деления своих операндов; левый операнд — это делимое, а правый операнд — делитель.

В 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

15.18. Операторы сложения

Операторы + и - называются операторами сложения.

AdditiveExpression:
MultiplicativeExpression
AdditiveExpression + MultiplicativeExpression
AdditiveExpression - MultiplicativeExpression

Операторы сложения имеют одинаковый приоритет и являются левоассоциативными (они группируются слева направо).

Если тип любого операнда оператора + является String, то операция представляет собой конкатенацию строк.

В противном случае тип каждого операнда оператора + должен быть типом, преобразуемым (§5.1.8) в примитивный числовой тип, иначе произойдет ошибка компиляции.

В любом случае, тип каждого операнда бинарного оператора - должен быть типом, преобразуемым (§5.1.8) в примитивный числовой тип, иначе произойдет ошибка компиляции.

15.18.1. Оператор конкатенации строк +

Если только один операнд-выражение имеет тип 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:"

на две части, чтобы избежать неудобно длинной строки в исходном коде.


15.18.2. Операторы сложения (+ и -) для числовых типов

Бинарный оператор + выполняет сложение, когда применяется к двум операндам числового типа, вычисляя сумму операндов.

Бинарный оператор - выполняет вычитание, вычисляя разность двух числовых операндов.

Над операндами выполняется расширение бинарных числовых типов (§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.

Несмотря на то, что могут произойти переполнение, уменьшение или потеря информации, вычисление числового оператора сложения никогда не вызывает исключение времени выполнения.

15.19. Операторы сдвига

Операторы << (сдвиг влево), >> (сдвиг вправо со знаком) и >>> (сдвиг вправо без знака) называются операторами сдвига. Операнд слева от оператора сдвига — это значение, которое нужно сдвинуть; операнд справа определяет расстояние сдвига.

ShiftExpression:
AdditiveExpression
ShiftExpression << AdditiveExpression
ShiftExpression >> AdditiveExpression
ShiftExpression >>> AdditiveExpression

Операторы сдвига имеют левоассоциативную синтаксическую группировку (они группируются слева направо).

Унарное числовое продвижение (§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 значения.

15.20. Операторы отношения

Операторы численного сравнения <, >, <=, и >=, а также оператор instanceof, называются операторами отношения.

RelationalExpression:
ShiftExpression
RelationalExpression < ShiftExpression
RelationalExpression > ShiftExpression
RelationalExpression <= ShiftExpression
RelationalExpression >= ShiftExpression
InstanceofExpression

Операторы отношения являются левоассоциативными (группируются слева направо).

Однако этот факт бесполезен. Например, a<b<c анализируется как (a<b)<c, что всегда является ошибкой компиляции, потому что тип a<b всегда boolean, а < не является оператором для boolean значений.

Тип выражения отношения всегда boolean.

15.20.1. Операторы численного сравнения <, <=, > и >=

Тип каждого из операндов оператора численного сравнения должен быть типом, преобразуемым (§5.1.8) к примитивному числовому типу, в противном случае произойдёт ошибка компиляции.

Для операндов выполняется двоичное числовое продвижение (§5.6).

Обратите внимание, что двоичное числовое продвижение может включать преобразование разыменования (§5.1.8).

Если тип продвинутых операндов — int или long, то выполняется сравнение целых чисел со знаком.

Если тип продвинут до float или double, то выполняется сравнение чисел с плавающей точкой.

Результат сравнения чисел с плавающей точкой, определяемый спецификацией стандарта IEEE 754:

  • Если один из операндов — NaN, то результат — false.

  • Все значения, кроме NaN, упорядочены, при этом отрицательная бесконечность меньше всех конечных значений, а положительная бесконечность больше всех конечных значений.

  • Положительный и отрицательный ноль считаются равными.

    Например, -0.0<0.0 — это false, но -0.0<=0.0 — это true.

    Однако методы Math.min и Math.max рассматривают отрицательный ноль как строго меньший, чем положительный ноль.

Принимая во внимание эти соображения для чисел с плавающей точкой, тогда выполняются следующие правила для целочисленных операндов или для операндов с плавающей точкой, отличных от NaN:

  • Значение, произведённое оператором <, равно true, если значение левого операнда меньше значения правого операнда, и в противном случае — false.

  • Значение, произведённое оператором <=, равно true, если значение левого операнда меньше или равно значению правого операнда, и в противном случае — false.

  • Значение, произведённое оператором >, равно true, если значение левого операнда больше значения правого операнда, и в противном случае — false.

  • Значение, произведённое оператором >=, равно true, если значение левого операнда больше или равно значению правого операнда, и в противном случае — false.

15.20.2. Оператор instanceof

Выражение instanceof может выполнять сравнение типов или сопоставление с шаблоном.

InstanceofExpression:
RelationalExpression instanceof Тип ссылки
RelationalExpression instanceof Шаблон

Если операнд справа от ключевого слова instanceof является Типом ссылки, то ключевое слово instanceof является оператором сравнения типов.

Если операнд справа от ключевого слова instanceof является Шаблоном, то ключевое слово instanceof является оператором сопоставления с шаблоном.

Следующие правила применяются, когда instanceof является оператором сравнения типов:

  • Тип выражения RelationalExpression должен быть типом ссылки или типом null, иначе произойдет ошибка компиляции.

  • RelationalExpression должен быть совместим с Типом ссылки по операциям приведения типов (§5.5), иначе произойдет ошибка компиляции.

  • Во время выполнения результат оператора сравнения типов определяется следующим образом:

    • Если значение RelationalExpression равно null-ссылке (§4.1), то результат равен false.

    • Если значение RelationalExpression не равно null-ссылке, то результат равен true, если значение может быть приведено к Типу ссылки без возникновения ClassCastException, и false в противном случае.

Следующие правила применяются, когда instanceof является оператором сопоставления с шаблоном:

  • Тип выражения RelationalExpression должен быть типом ссылки или типом null, иначе произойдет ошибка компиляции.

  • Шаблон должен быть применим к типу выражения RelationalExpression (§14.30.3), иначе произойдет ошибка компиляции.

  • Во время выполнения результат оператора сопоставления с шаблоном определяется следующим образом:

    • Если значение RelationalExpression равно null-ссылке, то результат равен false.

    • Если значение RelationalExpression не равно null-ссылке, то результат равен true, если значение соответствует Шаблону (§14.30.2), и false в противном случае.

      Побочным эффектом результата true является инициализация всех переменных шаблона, объявленных в Шаблоне, если таковые имеются.

Пример 15.20.2-1. Оператор сравнения типов

class Point   { int x, y; }
class Element { int atomicNumber; }
class Test {
    public static void main(String[] args) {
        Point   p = new Point();
        Element e = new Element();
        if (e instanceof Point) {  // compile-time error
            System.out.println("I get your point!");
            p = (Point)e;  // compile-time error
        }
    }
}

Эта программа приводит к двум ошибкам компиляции. Приведение (Point)e некорректно, так как ни один экземпляр Element или его подклассы (никаких не показано здесь) не может быть экземпляром любого подкласса Point. Выражение instanceof некорректно по той же причине. Если бы, с другой стороны, класс Point был подклассом Element (довольно странная идея в этом примере):


class Point extends Element { int x, y; }

то приведение было бы возможным, хотя оно потребовало бы проверки во время выполнения, и выражение instanceof было бы осмысленным и допустимым. Приведение (Point)e никогда не вызвало бы исключения, потому что оно не выполнялось бы, если значение e не могло быть корректно приведено к типу Point.

До Java SE 16 операнд ReferenceType оператора сравнения типов должен был быть реализуемым (§4.7). Это предотвращало использование параметризованного типа, если все его аргументы типов были дикими картами. Требование было снято в Java SE 16, чтобы разрешить использование большего количества параметризованных типов. Например, в следующей программе допустимо проверить, имеет ли параметр метода x, имеющий статический тип List<Integer>, более "уточнённый" параметризованный тип ArrayList<Integer> во время выполнения:

import java.util.ArrayList;
import java.util.List;

class Test2 {
    public static void main(String[] args) {
        List<Integer> x = new ArrayList<Integer>();
	
        if (x instanceof ArrayList<Integer>) {  // OK
            System.out.println("ArrayList of Integers");
        }
        if (x instanceof ArrayList<String>) {  // error
            System.out.println("ArrayList of Strings");
        }
        if (x instanceof ArrayList<Object>) {  // error
            System.out.println("ArrayList of Objects");
        }
    }
}

Первое выражение instanceof является допустимым, так как существует преобразование приведения от List<Integer> к ArrayList<Integer>. Однако второе и третье выражения instanceof вызывают ошибку компиляции, потому что нет преобразования приведения от List<Integer> к ArrayList<String> или ArrayList<Object>.


15.21. Операторы сравнения на равенство

Операторы == (равно) и != (не равно) называются операторами сравнения на равенство.

EqualityExpression:
RelationalExpression
EqualityExpression == RelationalExpression
EqualityExpression != RelationalExpression

Операторы сравнения на равенство левоассоциативны (они группируются слева направо).

Однако этот факт практически никогда не используется. Например, 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).

15.21.1. Числовые операторы сравнения на равенство == и !=

Если оба операнда оператора сравнения на равенство имеют числовой тип или один имеет числовой тип, а другой может быть приведён (§5.1.8) к числовому типу, то выполняется двоичное числовое продвижение операндов (§5.6).

Обратите внимание, что двоичное числовое продвижение может включать приведение к типу без упаковки (§5.1.8).

Если продвинутый тип операндов — int или long, то выполняется проверка равенства целых чисел.

Если продвинутый тип — float или double, то выполняется проверка равенства чисел с плавающей точкой.

Проверка равенства чисел с плавающей точкой выполняется в соответствии с правилами стандарта IEEE 754:

  • Если один из операндов — NaN, то результат == равен false, а результат != равен true.

    Действительно, проверка x!=x равна true тогда и только тогда, когда значение x равно NaN.

    Методы Float.isNaN и Double.isNaN также могут использоваться для проверки, является ли значение NaN.

  • Положительный и отрицательный ноль считаются равными.

    Например, -0.0==0.0 равно true.

  • В противном случае два различных значения чисел с плавающей точкой считаются неравными операторами сравнения на равенство.

    В частности, есть одно значение, представляющее положительную бесконечность, и одно значение, представляющее отрицательную бесконечность; каждое сравнивается только само с собой, и каждое сравнивается неравно со всеми другими значениями.

С учётом этих соображений для чисел с плавающей точкой, тогда выполняются следующие правила для целых операндов или для операндов с плавающей точкой, отличных от NaN:

  • Значение, вычисленное оператором ==, равно true, если значение левого операнда равно значению правого операнда; в противном случае результатом является false.

  • Значение, вычисленное оператором !=, равно true, если значение левого операнда не равно значению правого операнда; в противном случае результатом является false.

15.21.2. Булевы операторы сравнения на равенство == и !=

Если оба операнда оператора сравнения на равенство имеют тип boolean, или если один операнд имеет тип boolean, а другой — Boolean, то операция — это булево сравнение на равенство.

Булевы операторы сравнения на равенство ассоциативны.

Если один из операндов имеет тип Boolean, он подвергается приведению к типу без упаковки (§5.1.8).

Результат == равен true, если оба операнда (после всех необходимых преобразований в тип без упаковки) равны true или оба равны false; в противном случае результатом является false.

Результат != равен false, если оба операнда равны true или оба равны false; в противном случае результатом является true.

Таким образом, != ведёт себя так же, как ^ (§15.22.2) при применении к boolean операндам.

15.21.3. Операторы сравнения ссылок на равенство == и !=

Если оба операнда оператора сравнения на равенство имеют ссылочный тип или тип 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).

15.22. Битовые и логические операторы

Операторы битовых операций и логических операций включают оператор И &, оператор исключающего ИЛИ ^ и оператор включительно ИЛИ |.

AndExpression:
EqualityExpression
AndExpression & EqualityExpression
ExclusiveOrExpression:
AndExpression
ExclusiveOrExpression ^ AndExpression
InclusiveOrExpression:
ExclusiveOrExpression
InclusiveOrExpression | ExclusiveOrExpression

Эти операторы имеют различный приоритет, при этом & имеет наивысший приоритет, а | — наименьший приоритет.

Каждый из этих операторов синтаксически левоассоциативен (группирует слева направо).

Каждый оператор коммутативен, если выражения операндов не имеют побочных эффектов.

Каждый оператор ассоциативен.

Битовые и логические операторы могут использоваться для сравнения двух операндов числового типа или двух операндов типа boolean. Все остальные случаи приводят к ошибке компиляции.

15.22.1. Целочисленные битовые операторы &, ^ и |

Когда оба операнда оператора &, ^ или | являются типами, которые могут быть преобразованы (§5.1.8) в примитивный целочисленный тип, сначала выполняется двоичное численное продвижение над операндами (§5.6).

Тип выражения битового оператора — это повышенный тип операндов.

Для & значение результата — это битовое И значений операндов.

Для ^ значение результата — это битовое исключающее ИЛИ значений операндов.

Для | значение результата — это битовое включительно ИЛИ значений операндов.

Например, результат выражения:

0xff00 & 0xf0f0

равен:

0xf000

Результат выражения:

0xff00 ^ 0xf0f0

равен:

0x0ff0

Результат выражения:

0xff00 | 0xf0f0

равен:

0xfff0

15.22.2. Булевы логические операторы &, ^ и |

Когда оба операнда оператора &, ^ или | имеют тип boolean или Boolean, тогда тип выражения битового оператора — boolean. Во всех случаях операнды подвергаются преобразованию с разворачиванием (§5.1.8) при необходимости.

Для & значение результата — true, если оба значения операндов — true; в противном случае результат — false.

Для ^ значение результата — true, если значения операндов разные; в противном случае результат — false.

Для | значение результата — false, если оба значения операндов — false; в противном случае результат — true.

15.23. Условный оператор И &&

Условный оператор И && похож на & (§15.22.2), но оценивает свой операнд справа только если значение его левого операнда равно true.

ConditionalAndExpression:
InclusiveOrExpression
ConditionalAndExpression && InclusiveOrExpression

Условный оператор И синтаксически левоассоциативен (группирует слева направо).

Условный оператор И полностью ассоциативен по отношению к побочным эффектам и значению результата. То есть для любых выражений 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.24. Условный оператор ИЛИ ||

Условный оператор ИЛИ || похож на | (§15.22.2), но вычисляет свой правый операнд только если значение его левого операнда равно false.

ConditionalOrExpression:
ConditionalAndExpression
ConditionalOrExpression || ConditionalAndExpression

Условный оператор ИЛИ синтаксически левоассоциативен (группирует слева направо).

Условный оператор ИЛИ полностью ассоциативен по отношению к побочным эффектам и значению результата. То есть для любых выражений 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 операндах. Отличие только в том, что правое выражение операнда вычисляется условно, а не всегда.

15.25. Условный оператор ? :

Условный оператор ? : использует булево значение одного выражения для определения, какое из двух других выражений должно быть вычислено.

ConditionalExpression:
ConditionalOrExpression
ConditionalOrExpression ? Выражение : ConditionalExpression
ConditionalOrExpression ? Выражение : LambdaExpression

Условный оператор синтаксически ассоциативен справа налево (он группирует справа налево). Таким образом, a?b:c?d:e?f:g означает то же самое, что и a?b:(c?d:(e?f:g)).

Условный оператор имеет три операндных выражения. ? находится между первым и вторым выражениями, а : находится между вторым и третьим выражениями.

Первое выражение должно иметь тип boolean или Boolean, в противном случае произойдет ошибка компиляции.

Ошибка компиляции возникает, если либо второе, либо третье операндное выражение является вызовом метода void.

Фактически, по грамматике операторов выражений (§14.8), условное выражение не может находиться в любом контексте, где может находиться вызов метода void.

Существует три вида условных выражений, классифицируемых по второму и третьему операндным выражениям: булевы условные выражения, численные условные выражения и ссылочные условные выражения. Правила классификации следующие:

  • Если второе и третье операндные выражения являются булевыми выражениями, условное выражение является булевым условным выражением.

    Для целей классификации условного выражения следующие выражения являются булевыми:

    • Выражение в автономной форме (§15.2), имеющее тип boolean или Boolean.

    • Выражение в скобках boolean (§15.8.5).

    • Выражение создания экземпляра класса (§15.9) для класса Boolean.

    • Выражение вызова метода (§15.12), для которого выбранный наиболее конкретный метод (§15.12.2.5) имеет возвращаемый тип boolean или Boolean.

      Обратите внимание, что для генерического метода это тип до создания аргументов типов метода.

    • Условное выражение boolean.

    • Выражение switch (§15.28), результаты выражений которого — все выражения boolean.

  • Если второе и третье операндные выражения являются численными выражениями, условное выражение является численным условным выражением.

    Для целей классификации условного выражения следующие выражения являются численными:

    • Выражение в автономной форме (§15.2) с типом, преобразуемым в числовой тип (§4.2, §5.1.8).

    • Выражение в скобках с численным выражением (§15.8.5).

    • Выражение создания экземпляра класса (§15.9) для класса, преобразуемого в числовой тип.

    • Выражение вызова метода (§15.12), для которого выбранный наиболее конкретный метод (§15.12.2.5) имеет возвращаемый тип, преобразуемый в числовой тип.

      Обратите внимание, что для генерического метода это тип до создания аргументов типов метода.

    • Числовое условное выражение.

    • Выражение switch (§15.28), результаты выражений которого — все числовые выражения.

  • В противном случае, условное выражение является ссылочным условным выражением.

Процесс определения типа условного выражения зависит от вида условного выражения, как описано в следующих разделах.

В следующих таблицах приведены обобщенные правила выше, дающие тип условного выражения для всех возможных типов его второго и третьего операндов. bnp(..) означает применение бинарного числового продвижения. Форма "T | bnp(..)" используется, когда один операнд является константным выражением типа int и может быть представлен в типе T, где бинарное числовое продвижение используется, если операнд не может быть представлен в типе T. Тип операнда Object означает любой тип ссылки, кроме типа null и восьми оберточных классов Boolean, Byte, Short, Character, Integer, Long, Float, Double.

Таблица 15.25-A. Тип условного выражения (Примитивный 3-й операнд, Часть I)

3-й → byte short char int
2-й ↓
byte byte short bnp(byte,char) byte | bnp(byte,int)
Byte byte short bnp(Byte,char) byte | bnp(Byte,int)
short short short bnp(short,char) short | bnp(short,int)
Short short short bnp(Short,char) short | bnp(Short,int)
char bnp(char,byte) bnp(char,short) char char | bnp(char,int)
Character bnp(Character,byte) bnp(Character,short) char char | bnp(Character,int)
int byte | bnp(int,byte) short | bnp(int,short) char | bnp(int,char) int
Integer bnp(Integer,byte) bnp(Integer,short) bnp(Integer,char) int
long bnp(long,byte) bnp(long,short) bnp(long,char) bnp(long,int)
Long bnp(Long,byte) bnp(Long,short) bnp(Long,char) bnp(Long,int)
float bnp(float,byte) bnp(float,short) bnp(float,char) bnp(float,int)
Float bnp(Float,byte) bnp(Float,short) bnp(Float,char) bnp(Float,int)
double bnp(double,byte) bnp(double,short) bnp(double,char) bnp(double,int)
Double bnp(Double,byte) bnp(Double,short) bnp(Double,char) bnp(Double,int)
boolean lub(Boolean,Byte) lub(Boolean,Short) lub(Boolean,Character) lub(Boolean,Integer)
Boolean lub(Boolean,Byte) lub(Boolean,Short) lub(Boolean,Character) lub(Boolean,Integer)
null lub(null,Byte) lub(null,Short) lub(null,Character) lub(null,Integer)
Object lub(Object,Byte) lub(Object,Short) lub(Object,Character) lub(Object,Integer)

Таблица 15.25-B. Тип условного выражения (Примитивный 3-й операнд, Часть II)

3-й → long float double boolean
2-й ↓
byte bnp(byte,long) bnp(byte,float) bnp(byte,double) lub(Byte,Boolean)
Byte bnp(Byte,long) bnp(Byte,float) bnp(Byte,double) lub(Byte,Boolean)
short bnp(short,long) bnp(short,float) bnp(short,double) lub(Short,Boolean)
Short bnp(Short,long) bnp(Short,float) bnp(Short,double) lub(Short,Boolean)
char bnp(char,long) bnp(char,float) bnp(char,double) lub(Character,Boolean)
Character bnp(Character,long) bnp(Character,float) bnp(Character,double) lub(Character,Boolean)
int bnp(int,long) bnp(int,float) bnp(int,double) lub(Integer,Boolean)
Integer bnp(Integer,long) bnp(Integer,float) bnp(Integer,double) lub(Integer,Boolean)
long long bnp(long,float) bnp(long,double) lub(Long,Boolean)
Long long bnp(Long,float) bnp(Long,double) lub(Long,Boolean)
float bnp(float,long) float bnp(float,double) lub(Float,Boolean)
Float bnp(Float,long) float bnp(Float,double) lub(Float,Boolean)
double bnp(double,long) bnp(double,float) double lub(Double,Boolean)
Double bnp(Double,long) bnp(Double,float) double lub(Double,Boolean)
boolean lub(Boolean,Long) lub(Boolean,Float) lub(Boolean,Double) boolean
Boolean lub(Boolean,Long) lub(Boolean,Float) lub(Boolean,Double) boolean
null lub(null,Long) lub(null,Float) lub(null,Double) lub(null,Boolean)
Object lub(Object,Long) lub(Object,Float) lub(Object,Double) lub(Object,Boolean)

Таблица 15.25-C. Тип условного выражения (Операнд 3, Часть I)

3-й → Byte Short Character Integer
2-й ↓
byte byte short bnp(byte,Character) bnp(byte,Integer)
Byte Byte short bnp(Byte,Character) bnp(Byte,Integer)
short short short bnp(short,Character) bnp(short,Integer)
Short short Short bnp(Short,Character) bnp(Short,Integer)
char bnp(char,Byte) bnp(char,Short) char bnp(char,Integer)
Character bnp(Character,Byte) bnp(Character,Short) Character bnp(Character,Integer)
int byte | bnp(int,Byte) short | bnp(int,Short) char | bnp(int,Character) int
Integer bnp(Integer,Byte) bnp(Integer,Short) bnp(Integer,Character) Integer
long bnp(long,Byte) bnp(long,Short) bnp(long,Character) bnp(long,Integer)
Long bnp(Long,Byte) bnp(Long,Short) bnp(Long,Character) bnp(Long,Integer)
float bnp(float,Byte) bnp(float,Short) bnp(float,Character) bnp(float,Integer)
Float bnp(Float,Byte) bnp(Float,Short) bnp(Float,Character) bnp(Float,Integer)
double bnp(double,Byte) bnp(double,Short) bnp(double,Character) bnp(double,Integer)
Double bnp(Double,Byte) bnp(Double,Short) bnp(Double,Character) bnp(Double,Integer)
boolean lub(Boolean,Byte) lub(Boolean,Short) lub(Boolean,Character) lub(Boolean,Integer)
Boolean lub(Boolean,Byte) lub(Boolean,Short) lub(Boolean,Character) lub(Boolean,Integer)
null Byte Short Character Integer
Object lub(Object,Byte) lub(Object,Short) lub(Object,Character) lub(Object,Integer)

Таблица 15.25-D. Тип условного выражения (Ссылка на 3-й операнд, Часть II)

3-й → Long Float Double Boolean
2-й ↓
byte bnp(byte,Long) bnp(byte,Float) bnp(byte,Double) lub(Byte,Boolean)
Byte bnp(Byte,Long) bnp(Byte,Float) bnp(Byte,Double) lub(Byte,Boolean)
short bnp(short,Long) bnp(short,Float) bnp(short,Double) lub(Short,Boolean)
Short bnp(Short,Long) bnp(Short,Float) bnp(Short,Double) lub(Short,Boolean)
char bnp(char,Long) bnp(char,Float) bnp(char,Double) lub(Character,Boolean)
Character bnp(Character,Long) bnp(Character,Float) bnp(Character,Double) lub(Character,Boolean)
int bnp(int,Long) bnp(int,Float) bnp(int,Double) lub(Integer,Boolean)
Integer bnp(Integer,Long) bnp(Integer,Float) bnp(Integer,Double) lub(Integer,Boolean)
long long bnp(long,Float) bnp(long,Double) lub(Long,Boolean)
Long Long bnp(Long,Float) bnp(Long,Double) lub(Long,Boolean)
float bnp(float,Long) float bnp(float,Double) lub(Float,Boolean)
Float bnp(Float,Long) Float bnp(Float,Double) lub(Float,Boolean)
double bnp(double,Long) bnp(double,Float) double lub(Double,Boolean)
Double bnp(Double,Long) bnp(Double,Float) Double lub(Double,Boolean)
boolean lub(Boolean,Long) lub(Boolean,Float) lub(Boolean,Double) boolean
Boolean lub(Boolean,Long) lub(Boolean,Float) lub(Boolean,Double) Boolean
null Long Float Double Boolean
Object lub(Object,Long) lub(Object,Float) lub(Object,Double) lub(Object,Boolean)

Таблица 15.25-E. Тип условного выражения (Ссылка на 3-й операнд, часть III)

3-й → null Object
2-й ↓
byte lub(Byte,null) lub(Byte,Object)
Byte Byte lub(Byte,Object)
short lub(Short,null) lub(Short,Object)
Short Short lub(Short,Object)
char lub(Character,null) lub(Character,Object)
Character Character lub(Character,Object)
int lub(Integer,null) lub(Integer,Object)
Integer Integer lub(Integer,Object)
long lub(Long,null) lub(Long,Object)
Long Long lub(Long,Object)
float lub(Float,null) lub(Float,Object)
Float Float lub(Float,Object)
double lub(Double,null) lub(Double,Object)
Double Double lub(Double,Object)
boolean lub(Boolean,null) lub(Boolean,Object)
Boolean Boolean lub(Boolean,Object)
null null lub(null,Object)
Object Object Object

Во время выполнения сначала вычисляется выражение первого операнда условного выражения. При необходимости выполняется преобразование unboxing на результат.

Полученное boolean значение затем используется для выбора либо второго, либо третьего выражения операнда:

  • Если значение первого операнда равно true, то выбирается выражение второго операнда.

  • Если значение первого операнда равно false, то выбирается выражение третьего операнда.

Выбранное выражение операнда затем вычисляется, и полученное значение преобразуется в тип условного выражения в соответствии с правилами, изложенными ниже.

Это преобразование может включать преобразование boxing или unboxing (§5.1.7, §5.1.8).

Выражение операнда, которое не выбрано, не вычисляется для данного конкретного вычисления условного выражения.

15.25.1. Булевы условные выражения

Булевы условные выражения являются самостоятельными выражениями (§15.2).

Тип булева условного выражения определяется следующим образом:

  • Если второй и третий операнды оба имеют тип Boolean, то условное выражение имеет тип Boolean.

  • В противном случае условное выражение имеет тип boolean.

15.25.2. Числовые условные выражения

Числовые условные выражения являются самостоятельными выражениями (§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).

15.25.3. Ссылочные условные выражения

Ссылочное условное выражение является поливыражением, если оно встречается в контексте присваивания или вызова (§5.2. §5.3). В противном случае это самостоятельное выражение.

Когда полисправочное условное выражение появляется в контексте определенного типа с целевым типом T, его второе и третье операндные выражения аналогично появляются в контексте того же типа с целевым типом T.

Полисправочное условное выражение совместимо с целевым типом T, если его второе и третье операндные выражения совместимы с T.

Тип полисправочного условного выражения совпадает с его целевым типом.

Тип самостоятельного ссылочного условного выражения определяется следующим образом:

  • Если у второго и третьего операндов один и тот же тип (который может быть типом null), то это и есть тип условного выражения.

  • Если тип одного из вторых и третьих операндов – тип null, а тип другого операнда – ссылочный тип, то тип условного выражения – этот ссылочный тип.

  • В противном случае второй и третий операнды имеют типы S1 и S2 соответственно. Пусть T1 – тип, полученный при применении преобразования boxing к S1, а T2 – тип, полученный при применении преобразования boxing к S2. Тип условного выражения – результат применения преобразования capture (§5.1.10) к lub(T1, T2).

Поскольку ссылочные условные выражения могут быть поливыражениями, они могут «передавать» контекст своим операндам. Это позволяет использовать лямбда-выражения и выражения ссылки на методы в качестве операндов:

return ... ? (x -> x) : (x -> -x);

Это также позволяет использовать дополнительную информацию для улучшения проверки типов вызовов методов дженериков. До Java SE 8 это присваивание было корректно типизировано:

List<String> ls = Arrays.asList();

но это не было:

List<String> ls = ... ? Arrays.asList() : Arrays.asList("a","b");

Вышеприведенные правила позволяют рассматривать оба присваивания как корректно типизированные.

Обратите внимание, что ссылочное условное выражение не обязательно должно содержать поливыражение в качестве операнда, чтобы быть поливыражением. Это поливыражение просто в силу контекста, в котором оно появляется. Например, в следующем коде условное выражение является поливыражением, и каждый операнд считается находящимся в контексте присваивания, направленного на Class<? super Integer>:


Class<? super Integer> choose(boolean b,
                              Class<Integer> c1,
                              Class<Number> c2) {
    return b ? c1 : c2;
}

Если условное выражение не было бы поливыражением, произошла бы ошибка компиляции, так как его тип был бы lub(Class<Integer>, Class<Number>) = Class<? extends Number>, что несовместимо с возвращаемым типом choose.

15.26. Операторы присваивания

Существует 12 операторов присваивания; все они синтаксически ассоциативны справа налево (группируются справа налево). Таким образом, a=b=c означает a=(b=c), которое присваивает значение c переменной b, а затем присваивает значение b переменной a.

AssignmentExpression:
ConditionalExpression
Assignment
Assignment:
LeftHandSide AssignmentOperator Expression
LeftHandSide:
ExpressionName
Доступ к полю
Доступ к массиву
AssignmentOperator:
(один из)
=  *=  /=  %=  +=  -=  <<=  >>=  >>>=  &=  ^=  |=

Результат первого операнда оператора присваивания должен быть переменной; в противном случае возникает ошибка компиляции.

Этот операнд может быть именованной переменной, такой как локальная переменная или поле текущего объекта или класса, или вычисляемой переменной, которая может быть результатом доступа к полю (§15.11) или доступа к массиву (§15.10.3).

Тип выражения присваивания — это тип переменной после преобразования захвата (§5.1.10).

Во время выполнения результат выражения присваивания — это значение переменной после выполнения присваивания. Результат выражения присваивания сам по себе не является переменной.

Переменной, которая объявлена final, нельзя присвоить значение (если она не явно не определена (§16 (Определённое присваивание))), потому что при использовании такой final переменной как выражения результатом будет значение, а не переменная, и поэтому она не может использоваться в качестве первого операнда оператора присваивания.

15.26.1. Оператор простого присваивания =

Если тип правого операнда не совместим с типом переменной для присваивания (§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.


15.26.2. Операторы сложного присваивания

Выражение сложного присваивания вида 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.27. Лямбда-выражения

Лямбда-выражение подобно методу: оно предоставляет список формальных параметров и тело — выражение или блок — выраженные в терминах этих параметров.

LambdaExpression:
LambdaParameters -> LambdaBody

Лямбда-выражения всегда являются поливыражениями (§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), но расширение этого подхода на дженерические лямбды потребовало бы радикальных изменений в грамматике.

15.27.1. Параметры лямбда-выражения

Формальные параметры лямбда-выражения, если таковые имеются, задаются либо скобочным списком параметров, разделенных запятыми, либо скобочным списком идентификаторов, разделенных запятыми. В списке спецификаторов параметров каждый спецификатор параметра состоит из необязательных модификаторов, затем типа (или var), затем идентификатора, указывающего имя параметра. В списке идентификаторов каждый идентификатор указывает имя параметра.

Если лямбда-выражение не имеет формальных параметров, то перед -> и телом лямбда-выражения появляется пустая пара скобок.

Если лямбда-выражение имеет ровно один формальный параметр, и этот параметр задаётся идентификатором вместо спецификатора параметра, то скобки вокруг идентификатора можно опустить.

LambdaParameters:
( [Список параметров лямбда-выражения] )
Идентификатор
LambdaParameterList:
Параметр лямбда-выражения {, Параметр лямбда-выражения}
Идентификатор {, Идентификатор}
LambdaParameter:
{Модификатор переменной} Тип параметра лямбда-выражения Идентификатор объявляемой переменной
Параметр с переменным числом аргументов
LambdaParameterType:
Немаркированный тип
var

Следующие правила из §8.4.1, §8.3 и §4.3 показаны здесь для удобства:

VariableArityParameter:
{Модификатор переменной} Немаркированный тип {Аннотация} ... Идентификатор
VariableModifier:
Аннотация
final
VariableDeclaratorId:
Идентификатор [Размеры]
Dims:
{Аннотация} [ ] {{Аннотация} [ ]}

Формальный параметр лямбда-выражения может быть объявлен final или аннотирован, только если он задан с помощью спецификатора параметра. Если формальный параметр задан с помощью идентификатора, то формальный параметр не final и не имеет аннотаций.

Формальный параметр лямбда-выражения может быть параметром с переменным числом аргументов, обозначенным многоточием после типа в спецификаторе параметра. Максимум один параметр с переменным числом аргументов допускается для лямбда-выражения. Если параметр с переменным числом аргументов появляется где-либо в списке спецификаторов параметров, кроме последней позиции, это синтаксическая ошибка.

Каждый формальный параметр лямбда-выражения имеет либо выведенный тип, либо объявленный тип:

  • Если формальный параметр задаётся либо спецификатором параметра, использующим var, либо идентификатором вместо спецификатора параметра, то формальный параметр имеет выведенный тип. Тип выводится из типа функционального интерфейса, на который ориентировано лямбда-выражение (§15.27.3).

  • Если формальный параметр задаётся спецификатором параметра, не использующим var, то формальный параметр имеет объявленный тип. Объявленный тип определяется следующим образом:

    • Если формальный параметр не является параметром с переменным числом аргументов, то объявленный тип обозначается UnannType, если в UnannType и VariableDeclaratorId не присутствуют скобки, и задаётся в §10.2 в противном случае.

    • Если формальный параметр является параметром с переменным числом аргументов, то объявленный тип является типом массива, заданным в §10.2.

Не делается различия между следующими списками параметров лямбда-выражения:

(int... x) -> BODY
(int[] x) -> BODY

Любой из них может быть использован, независимо от того, является ли метод функционального интерфейса abstract фиксированной или переменной арности. (Это согласуется с правилами переопределения методов.) Поскольку лямбда-выражения никогда не вызываются напрямую, использование int... для формального параметра, где функциональный интерфейс использует int[], не может повлиять на окружающую программу. В теле лямбда-выражения параметр с переменным числом аргументов обрабатывается так же, как и параметр с типом массива.

Лямбда-выражение, где все формальные параметры имеют объявленные типы, называется явным типом. Лямбда-выражение, где все формальные параметры имеют выведенные типы, называется неявным типом. Лямбда-выражение без формальных параметров является явного типа.

Если лямбда-выражение имеет неявный тип, то его тело интерпретируется в соответствии с контекстом, в котором оно появляется. В частности, типы выражений в теле, проверяемые исключения, выбрасываемые телом, и правильность типов кода в теле зависят от типов, выведенных для формальных параметров. Это подразумевает, что вывод типов формальных параметров должен происходить "до" попытки проверки типа тела лямбда-выражения.

Если лямбда-выражение объявляет формальный параметр с объявленным типом и формальный параметр с выведенным типом, это синтаксическая ошибка.

Это правило предотвращает смешение выведенных и объявленных типов в формальных параметрах, например, (x, int y) -> BODY или (var x, int y) -> BODY. Обратите внимание, что если все формальные параметры имеют выведенные типы, то грамматика предотвращает смешение идентификаторов и var спецификаторов параметров, таких как (x, var y) -> BODY или (var x, y) -> BODY.

Правила, касающиеся модификаторов аннотаций для объявления формального параметра, указаны в §9.7.4 и §9.7.5.

Если модификатор final появляется более одного раза в качестве модификатора для объявления формального параметра, это синтаксическая ошибка.

Если LambdaParameterType формального параметра является var, а VariableDeclaratorId этого же формального параметра имеет одну или несколько пар скобок, это синтаксическая ошибка.

Область видимости и экранирование формального параметра указаны в §6.3 и §6.4.

Доступ к формальному параметру из вложенного класса или интерфейса, или вложенного лямбда-выражения, ограничен, как указано в §6.5.6.1.

Объявление двух формальных параметров с одинаковым именем (то есть, их объявления упоминают один и тот же Идентификатор) является синтаксической ошибкой.

В Java SE 8 использование _ в качестве имени параметра лямбда-выражения было запрещено, и его использование в качестве имени других типов переменных не рекомендуется (§4.12.3). Начиная с Java SE 9, _ является ключевым словом (§3.9), поэтому его нельзя использовать в качестве имени переменной ни в одном контексте.

Присваивание формальному параметру, объявленному final, внутри тела лямбда-выражения является синтаксической ошибкой.

При вызове лямбда-выражения (через выражение вызова метода (§15.12)) значения фактических аргументов инициализируют недавно созданные переменные параметров, каждый из объявленного или выведенного типа, перед выполнением тела лямбда-выражения. Идентификатор, который появляется в LambdaParameter или непосредственно в LambdaParameterList или LambdaParameters, может быть использован в качестве простого имени в теле лямбда-выражения для ссылки на формальный параметр.

15.27.2. Тело лямбда-выражения

Тело лямбда-выражения представляет собой либо одно выражение, либо блок (§14.2). Как и тело метода, тело лямбда-выражения описывает код, который будет выполняться при каждом вызове.

LambdaBody:
Выражение
Блок

В отличие от кода, появляющегося в объявлениях анонимных классов, смысл имен и ключевых слов this и super, появляющихся в теле лямбда-выражения, а также доступность ссылочных объявлений, аналогичны окружающему контексту (за исключением того, что параметры лямбда-выражения вводят новые имена).

Прозрачность this (как явная, так и неявная) в теле лямбда-выражения — то есть, ее обработка так же, как и в окружающем контексте — обеспечивает большую гибкость для реализаций и предотвращает зависимость смысла неквалифицированных имен в теле от разрешения перегрузки.

Практически, лямбда-выражения редко нуждаются в обращении к себе (как для рекурсивного вызова, так и для вызова других методов), в то время как чаще требуется использование имен для ссылки на элементы в окружающем классе, которые могут быть затемнены (this, toString()). Если для лямбда-выражения необходимо обратиться к себе (как будто через this), следует использовать ссылку на метод или анонимный внутренний класс.

Тело лямбда-выражения с блоком является совместимым с void, если каждое оператор возврата в блоке имеет вид return;.

Тело лямбда-выражения с блоком является совместимым со значением, если оно не может завершиться нормально (§14.22) и каждое оператор возврата в блоке имеет вид return Выражение;.

Если тело лямбда-выражения с блоком не совместимо ни с void, ни со значением, возникает ошибка компиляции.

В теле лямбда-выражения, совместимом со значением, выражения результата — это любые выражения, которые могут произвести значение вызова. В частности, для каждого оператора вида return Выражение ;, содержащегося в теле, Выражение является выражением результата.

Следующие тела лямбда-выражений совместимы с void:

() -> {}
() -> { System.out.println("done"); }

Эти тела совместимы со значением:

() -> { return "done"; }
() -> { if (...) return 1; else return 0; }

Эти тела совместимы и с void, и со значением:

() -> { throw new RuntimeException(); }
() -> { while (true); }

Это тело не совместимо ни с void, ни со значением:

() -> { if (...) return "done"; System.out.println("done"); }

Обработка совместимости с void/значением и смысл имен в теле совместно служат для минимизации зависимости от конкретного целевого типа в данном контексте, что полезно как для реализаций, так и для понимания программистом. Хотя выражения могут получать разные типы во время разрешения перегрузки в зависимости от целевого типа, смысл неквалифицированных имен и основная структура тела лямбда-выражения не изменяются.

Обратите внимание, что определение совместимости с void/значением не является строго структурным свойством: «возможность нормального завершения» зависит от значений константных выражений, и эти значения могут включать имена, ссылающиеся на константные переменные.

Любая локальная переменная, формальный параметр или параметр исключения, используемые, но не объявленные в лямбда-выражении, должны либо быть final, либо эффективно final (§4.12.4), как указано в §6.5.6.1.

Любая локальная переменная, используемая, но не объявленная в теле лямбда-выражения, должна быть определенно присвоена (§16 (Определенное присваивание)) перед телом лямбда-выражения, иначе произойдет ошибка компиляции.

Аналогичные правила использования переменных применяются в теле внутреннего класса (§8.1.3). Ограничение на эффективно final переменные запрещает доступ к динамически изменяющимся локальным переменным, чье захват, вероятно, приведет к проблемам с конкурентностью. По сравнению с ограничением final, это снижает административную нагрузку на программистов.

Ограничение на эффективно final переменные включает стандартные переменные цикла, но не переменные расширенного for цикла, которые обрабатываются как отдельные для каждой итерации цикла (§14.14.2).

Следующие тела лямбда-выражений демонстрируют использование эффективно final переменных.


void m1(int x) {
    int y = 1;
    foo(() -> x+y);
    // Legal: x and y are both effectively final.
}

void m2(int x) {
    int y;
    y = 1;
    foo(() -> x+y);
    // Legal: x and y are both effectively final.
}

void m3(int x) {
    int y;
    if (...) y = 1;
    foo(() -> x+y);
    // Illegal: y is effectively final, but not definitely assigned.
}

void m4(int x) {
    int y;
    if (...) y = 1; else y = 2;
    foo(() -> x+y);
    // Legal: x and y are both effectively final.
}


void m5(int x) {
    int y;
    if (...) y = 1;
    y = 2;
    foo(() -> x+y);
    // Illegal: y is not effectively final.
}

void m6(int x) {
    foo(() -> x+1);
    x++;
    // Illegal: x is not effectively final.
}

void m7(int x) {
    foo(() -> x=1);
    // Illegal: x is not effectively final.
}

void m8() {
    int y;
    foo(() -> y=1);
    // Illegal: y is not definitely assigned before the lambda.
}

void m9(String[] arr) {
    for (String s : arr) {
        foo(() -> s);
        // Legal: s is effectively final
        // (it is a new variable on each iteration)
    }
}

void m10(String[] arr) {
    for (int i = 0; i < arr.length; i++) {
        foo(() -> arr[i]);
        // Illegal: i is not effectively final
        // (it is not final, and is incremented)
    }
}

15.27.3. Тип лямбда-выражения

Лямбда-выражение совместимо в контексте присваивания, вызова или приведения типов с целевым типом T, если T является типом функционального интерфейса (§9.8) и выражение совместимо с типом функции основного целевого типа, полученного из T.

Основной целевой тип выводится из T следующим образом:

  • Если T — это функциональный интерфейс с параметризацией с подстановкой шаблонов, и лямбда-выражение имеет явный тип, то основной целевой тип определяется так, как описано в §18.5.3.

  • Если T — это функциональный интерфейс с параметризацией с подстановкой шаблонов, и лямбда-выражение имеет неявный тип, то основной целевой тип — это параметризация T без подстановок шаблонов (§9.9).

  • В противном случае основной целевой тип — это T.

Лямбда-выражение совместимо с типом функции, если выполняются все следующие условия:

  • Тип функции не имеет параметров типа.

  • Количество параметров лямбда-выражения совпадает с количеством типов параметров типа функции.

  • Если лямбда-выражение имеет явный тип, его формальные типы параметров совпадают с типами параметров типа функции.

  • Если предполагается, что параметры лямбда-выражения имеют те же типы, что и типы параметров типа функции, то:

    • Если результат типа функции — void, то тело лямбда-выражения является либо выражением-оператором (§14.8), либо блоком, совместимым с void.

    • Если результат типа функции — это (не-void) тип R, то либо (i) тело лямбда-выражения — это выражение, совместимое с R в контексте присваивания, или (ii) тело лямбда-выражения — это блок, совместимый со значением, и каждое выражение результата (§15.27.2) совместимо с R в контексте присваивания.

Если лямбда-выражение совместимо с целевым типом T, то тип выражения, U, — это основной целевой тип, полученный из T.

Если какой-либо класс или интерфейс, упомянутый либо в U, либо в типе функции U, не доступен (§6.6) из класса или интерфейса, в котором появляется лямбда-выражение, то это ошибка времени компиляции.

Для каждого не-static метода-члена m типа U, если тип функции U имеет подсигнатуру под сигнатурой m, то условный метод, тип которого — тип функции U, считается переопределяющим m, и может возникнуть любая ошибка времени компиляции или предупреждение о неопределённом типе, указанное в §8.4.8.3.

Проверяемое исключение, которое может быть вызвано в теле лямбда-выражения, может привести к ошибке времени компиляции, как указано в §11.2.3.

Типы параметров явно типизированных лямбда-выражений должны точно совпадать с типами параметров типа функции. Хотя было бы возможно быть более гибкими — например, допускать упаковку или контравариантность, такая общность кажется излишней и противоречит тому, как работает переопределение в объявлениях классов. Программист должен точно знать, какой тип функции предназначен, когда он пишет лямбда-выражение, поэтому программист должен знать точно, какая подпись должна быть переопределена. (Это не относится к ссылкам на методы, поэтому при их использовании допускается большая гибкость.) Кроме того, большая гибкость с типами параметров увеличит сложность вывода типов и разрешения перегрузки.

Обратите внимание, что хотя упаковка не допускается в строгом контексте вызова, упаковка результатов лямбда-выражений всегда допускается — то есть выражение результата появляется в контексте присваивания, независимо от контекста, охватывающего лямбда-выражение. Однако, если явно типизированное лямбда-выражение является аргументом перегруженного метода, методу подписи, которая избегает упаковки или распаковки результата лямбда-выражения, отдается предпочтение с помощью проверки максимальной специфичности (§15.12.2.5).

Если тело лямбда-выражения является выражением-оператором (то есть выражением, которое может быть предоставлено в качестве самостоятельного оператора), оно совместимо с типом функции, производящим void; любой результат просто отбрасывается. Например, оба следующих примера являются корректными:

// Predicate has a boolean result
java.util.function.Predicate<String> p = s -> list.add(s);
// Consumer has a void result
java.util.function.Consumer<String> c = s -> list.add(s);

В общем случае лямбда-выражение вида () -> expr, где expr — это выражение-оператор, интерпретируется как либо () -> { return expr; } или () -> { expr; }, в зависимости от целевого типа.

15.27.4. Вычисление лямбда-выражений во время выполнения

Во время выполнения вычисление лямбда-выражения аналогично вычислению выражения создания экземпляра класса, поскольку нормальное завершение приводит к ссылке на объект. Вычисление лямбда-выражения отличается от выполнения тела лямбда-выражения.

Создается и инициализируется новый экземпляр класса с указанными ниже свойствами, или же ссылается на существующий экземпляр класса с такими же свойствами. Если необходимо создать новый экземпляр, но памяти недостаточно для выделения объекта, вычисление лямбда-выражения завершается неожиданно путём выброса исключения OutOfMemoryError.

Это подразумевает, что идентичность результата вычисления лямбда-выражения (или сериализации и десериализации лямбда-выражения) непредсказуема, а поэтому операции, зависящие от идентичности (такие как операторы равенства ссылок (§15.21.3), блокировка объектов (§14.19) и метод System.identityHashCode) могут давать разные результаты в разных реализациях языка программирования Java, или даже при разных вычислениях лямбда-выражений в одной реализации.

Значение лямбда-выражения — ссылка на экземпляр класса со следующими свойствами:

  • Класс реализует целевой функциональный интерфейс и, если целевой тип является пересечением типов, все другие указанные в пересечении типы интерфейсов.

  • Где лямбда-выражение имеет тип U, для каждого не-static члена метода m типа U:

    Если тип функции U имеет подсигнатуру к сигнатуре m, то класс объявляет метод, который переопределяет m. Тело метода выполняет вычисление тела лямбда-выражения, если оно является выражением, или выполняет тело лямбда-выражения, если оно является блоком; если ожидается результат, он возвращается из метода.

    Если стирание типа метода, который переопределяется, отличается по своей сигнатуре от стирания типа функции U, то перед вычислением или выполнением тела лямбда-выражения тело метода проверяет, является ли каждое значение аргумента экземпляром подкласса или подинтерфейса стирания соответствующего типа параметра в типе функции U; если нет, выбрасывается исключение ClassCastException.

  • Класс не переопределяет другие методы целевого функционального интерфейса или других указанных выше типов интерфейсов, хотя может переопределять методы класса Object.

Эти правила предназначены для обеспечения гибкости реализациям языка программирования Java, поскольку:

  • Не обязательно выделять новый объект при каждом вычислении.

  • Объекты, созданные различными лямбда-выражениями, не обязательно должны принадлежать различным классам (если, например, тела одинаковы).

  • Не каждый объект, созданный при вычислении, не обязательно должен принадлежать одному и тому же классу (например, захваченные локальные переменные могут быть встроенными).

  • Если доступен "существующий экземпляр", он не обязательно должен был быть создан при предыдущем вычислении лямбда-выражения (например, он мог быть выделен во время инициализации содержащего класса).

Если целевой тип функционального интерфейса является подтипом java.io.Serializable, результирующий объект автоматически станет экземпляром сериализуемого класса. Преобразование объекта, полученного из лямбда-выражения, в сериализуемый может иметь дополнительную нагрузку во время выполнения и последствия для безопасности, поэтому объекты, полученные из лямбда-выражений, не обязаны быть сериализуемыми "по умолчанию".

15.28. Выражения switch

Выражение switch передает управление одному из нескольких операторов или выражений в зависимости от значения выражения; все возможные значения этого выражения должны быть обработаны, и все несколько операторов и выражений должны генерировать значение для результата выражения switch.

SwitchExpression:
switch ( Выражение ) Блок switch

Выражение Expression называется выражением-селектором. Тип выражения-селектора должен быть char, byte, short, int, или ссылочным типом; в противном случае возникает ошибка компиляции.

Тело как выражения switch, так и оператора switch (§14.11) называется блоком switch. Общие правила, которые применяются ко всем блокам switch, независимо от того, появляются ли они в выражениях switch или операторах switch, приведены в §14.11.1. Для удобства ниже приведены соответствующие определения из §14.11.1:

SwitchBlock:
{ Правило switch {Правило switch} }
{ {Группа операторов блока switch} {Метка switch :} }
SwitchRule:
Метка switch -> Выражение ;
Метка switch -> Блок
Метка switch -> Оператор throw
SwitchBlockStatementGroup:
Метка switch : {Метка switch :} Операторы блока
SwitchLabel:
case Константа case {, Константа case}
case null [, default]
case Шаблон case [Условие]
default
CaseConstant:
Условное выражение
CasePattern:
Шаблон
Guard:
when Выражение

15.28.1. Блок switch выражения switch

В дополнение к общим правилам для блоков switch (§14.11.1), существуют дополнительные правила для блоков switch в выражениях switch.

Ошибка компиляции, если блок switch выражения switch состоит из правил switch, но один или несколько блоков правил switch могут завершиться нормально (§14.22).

Ошибка компиляции, если блок switch выражения switch состоит из групп операторов, помеченных метками switch, но последний оператор в блоке switch может завершиться нормально, или блок switch содержит одну или несколько меток switch после последней группы операторов, помеченных метками switch.

Ошибка компиляции, если выражение switch не является исчерпывающим (§14.11.1.1).

Выражения switch отличаются от операторов switch тем, какие выражения могут находиться справа от стрелки (->) в блоке switch, то есть какие выражения могут использоваться в качестве выражений правила switch. В выражении switch может использоваться любое выражение, а в операторе switch может использоваться только выражение-оператор (§14.11.1).

Выражения-результаты выражения switch определяются следующим образом:

  • Если блок switch состоит из правил switch, то каждое правило switch рассматривается по очереди:

    • Если правило switch имеет вид ... -> Выражение ;, то Выражение является выражением-результатом выражения switch.

    • Если правило switch имеет вид ... -> Блок, то каждое выражение, которое непосредственно содержится в операторе return в Блоке, чья цель return-a является данным выражением switch, является выражением-результатом выражения switch.

  • Если блок switch состоит из групп операторов, помеченных метками switch, то каждое выражение, которое непосредственно содержится в операторе return в блоке switch, чья цель return-a является данным выражением switch, является выражением-результатом выражения switch.

Ошибка компиляции, если выражение switch не имеет выражений-результатов.

Выражение switch является поливыражением, если оно появляется в контексте присваивания или вызова (§5.2, §5.3). В противном случае это самостоятельное выражение.

Если поливыражение switch появляется в контексте определённого типа с целевым типом T, его выражения-результаты также появляются в контексте того же типа с целевым типом T.

Поливыражение switch совместимо с целевым типом T, если каждое из его выражений-результатов совместимо с T.

Тип поливыражения switch совпадает с его целевым типом.

Тип самостоятельного выражения switch определяется следующим образом:

  • Если все выражения-результаты имеют один и тот же тип (который может быть типом null (§4.1)), то это и есть тип выражения switch.

  • В противном случае, если тип каждого выражения-результата является boolean или Boolean, то к каждому выражению-результату типа Boolean применяется преобразование unboxing (§5.1.8), и выражение switch имеет тип boolean.

  • В противном случае, если тип каждого выражения-результата преобразуем к числовому типу (§5.1.8), то тип выражения switch является результатом общего числового продвижения (§5.6), применённого к выражениям-результатам.

  • В противном случае, к каждому выражению-результату с примитивным типом применяется преобразование boxing (§5.1.7), после чего тип выражения switch является результатом применения преобразования capture (§5.1.10) к наименьшему общему знаменателю (§4.10.4) типов выражений-результатов.

15.28.2. Вычисление выражений switch во время выполнения

Выражение switch вычисляется путём сначала вычисления выражения селектора. Если вычисление выражения селектора завершается прерывисто, то вычисление всего выражения switch также завершается прерывисто по той же причине.

Если вычисление выражения селектора завершается обычно, то вычисление выражения switch продолжается путём определения, применяется ли метка switch, связанная с блоком switch, к значению выражения селектора (§14.11.1.2). Затем:

  • Если процесс определения применяемой метки switch завершается прерывисто, то всё выражение switch завершается прерывисто по той же причине.

  • Если никакая метка switch не применяется, то выполняется одно из следующих условий:

    • Если значение выражения селектора равно null, то выбрасывается NullPointerException, и вычисление выражения switch завершается прерывисто по этой причине.

    • В противном случае выбрасывается MatchException, и вычисление выражения switch завершается прерывисто по этой причине.

  • Если метка switch применяется, то выполняется одно из следующих условий:

    • Если это метка switch для выражения правила switch, то выражение вычисляется. Если результатом вычисления является значение, то выражение switch завершается обычно с тем же значением.

    • Если это метка switch для блока правила switch, то блок выполняется. Если этот блок завершается обычно, то выражение switch завершается обычно.

    • Если это метка switch для оператора правила switch throw, то оператор throw выполняется.

    • В противном случае все операторы в блоке switch после применяемой метки switch выполняются по порядку. Если эти операторы завершаются обычно, то выражение switch завершается обычно.

Если выполнение любого оператора или выражения в блоке switch завершается прерывисто, это обрабатывается следующим образом:

  • Если вычисление выражения завершается прерывисто, то вычисление выражения switch завершается прерывисто по той же причине.

  • Если выполнение оператора завершается прерывисто из-за yield со значением V, то вычисление выражения switch завершается обычно, а значение выражения switch равно V.

  • Если выполнение оператора завершается прерывисто по любой причине, кроме yield со значением, то вычисление выражения switch завершается прерывисто по той же причине.

15.29. Постоянные выражения

ConstantExpression:
Выражение

Постоянное выражение — это выражение, обозначающее значение примитивного типа или String, которое не завершается прерывисто и составлено только с использованием следующего:

  • Литералы примитивного типа (§3.10.1, §3.10.2, §3.10.3, §3.10.4), строковые литералы (§3.10.5) и текстовые блоки (§3.10.6)

  • Приведения к примитивным типам и приведения к типу String (§15.16)

  • Унарные операторы +, -, ~ и ! (но не ++ или --) (§15.15.3, §15.15.4, §15.15.5, §15.15.6)

  • Множительные операторы *, / и % (§15.17)

  • Складываемые операторы + и - (§15.18)

  • Сдвиговые операторы <<, >> и >>> (§15.19)

  • Операторы сравнения <, <=, > и >= (но не instanceof) (§15.20)

  • Операторы равенства == и != (§15.21)

  • Битовые и логические операторы &, ^ и | (§15.22)

  • Условный оператор И && и условный оператор ИЛИ || (§15.23, §15.24)

  • Тройной условный оператор ? : (§15.25)

  • Скобочные выражения (§15.8.5), содержащее выражение которых является постоянным выражением.

  • Простые имена (§6.5.6.1), которые ссылаются на константные переменные (§4.12.4).

  • Квалифицированные имена (§6.5.6.2) вида ИмяТипа . Идентификатор, которые ссылаются на константные переменные (§4.12.4).

Постоянные выражения типа String всегда «интернируются» для совместного использования уникальных экземпляров с использованием метода String.intern.

Постоянные выражения используются в качестве меток case в операторах switch и выражениях switch (§14.11, §15.28) и имеют особое значение в контекстах присваивания (§5.2) и инициализации класса или интерфейса (§12.4.2). Они также могут определять возможность оператора while, do или for завершиться обычно (§14.22), а также тип условного оператора ? : с числовыми операндами.

Пример 15.29-1. Постоянные выражения

true
(short)(1*2*3*4*5*6)
Integer.MAX_VALUE / 2
2.0 * Math.PI
"The integer " + Long.MAX_VALUE + " is mighty big."

© Oracle and/or its affiliates. All rights reserved.
Licensed under the Oracle Technology Network License Agreement.

Spec-Zone.ru

Настройки Оффлайн Что нового Помощь О нас
Spec-Zone .ru
спецификации, руководства, описания, API