Spec-Zone.ru › Java Language Specification 11

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

Оглавление

15.1. Вычисление, Обозначение и Результат
15.2. Формы Выражений
15.3. Тип Выражения
15.4. Выражения FP-strict
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. Lambda Выражения
END_OF_DOCUMENT_MARKER
15.27.1. Параметры лямбда-выражения
15.27.2. Тело лямбда-выражения
15.27.3. Тип лямбда-выражения
15.27.4. Вычисление лямбда-выражений во время выполнения
15.28. Постоянные выражения

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

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

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

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

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

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

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

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

Преобразование множества значений (§5.1.13) применяется к результату каждого выражения, которое производит значение, включая случаи, когда используется значение переменной типа float или double.

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

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

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

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

  • Аннотация на объявлении пакета или на объявлении верхнего уровня типа.

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

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

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

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

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

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

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

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

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

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

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

  • Скобочные выражения (§15.8.5)

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

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

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

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

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

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

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

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

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

Если выражение обозначает переменную или значение, то выражение имеет тип, известный на этапе компиляции. Тип автономного выражения может быть определен полностью из содержимого выражения; в отличие от этого, тип поли выражения может зависеть от целевого типа выражения (§5 (Преобразования и контексты)). Правила определения типа выражения объясняются отдельно ниже для каждого типа выражения.

Значение выражения совместимо с присваиванием (§5.2) с типом выражения, если не происходит загрязнение кучи (§4.12.2).

Аналогично, значение, хранящееся в переменной, всегда совместимо с типом переменной, если не происходит загрязнение кучи.

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

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

15.4. Выражения FP-строгой арифметики

Если тип выражения — float или double, то возникает вопрос, из какого множества значений (§4.2.3) берется значение выражения. Это регулируется правилами преобразования множества значений (§5.1.13); эти правила, в свою очередь, зависят от того, является ли выражение FP-строгим.

Каждое константное выражение (§15.28) является FP-строгим.

Если выражение не является константным, то рассмотрите все объявления классов, интерфейсов и методов, которые содержат это выражение. Если любое такое объявление имеет модификатор strictfp (§8.1.1.3, §8.4.3.5, §9.1.1.2), то выражение является FP-строгим.

Если класс, интерфейс или метод X объявлен strictfp, то X и любой класс, интерфейс, метод, конструктор, инициализатор экземпляра, статический инициализатор или инициализатор переменной внутри X считаются FP-строгими.

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

Следовательно, выражение не является FP-строгим тогда и только тогда, когда оно не является константным и не появляется внутри объявления, имеющего модификатор strictfp.

В выражении FP-строгой арифметики все промежуточные значения должны принадлежать множеству значений чисел с плавающей точкой float или double, что означает, что результаты всех FP-строгих выражений должны соответствовать арифметике IEEE 754 с операндами, представленными в формате single и double.

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

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 только в том случае, если класс объекта исключения, которое было брошено, является instanceof типом формального параметра блока catch.

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

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

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

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

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

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

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

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).

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

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

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

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

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

Термины «завершение нормально» и «завершение прерывисто» также применяются к выполнению операторов (§14.1). Оператор может завершиться прерывисто по различным причинам, а не только из-за выброшенного исключения.

END_OF_DOCUMENT_MARKER

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, для которых первое выражение даёт бесконечность (из-за переполнения), а второе выражение даёт конечный результат.

Например, тестовая программа:

strictfp class Test {
    public static void main(String[] args) {
        double d = 8e+307;
        System.out.println(4.0 * d * 0.5);
        System.out.println(2.0 * d);
    }
}

выводит:

Infinity
1.6e+308

потому что первое выражение переполняется, а второе — нет.

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

Например, a+b+c, где a, b и c являются локальными переменными (это упрощающее предположение избегает проблем, связанных с несколькими потоками и volatile переменными), всегда даст один и тот же ответ, независимо от того, вычисляется ли он как (a+b)+c или как a+(b+c); если выражение b+c встречается поблизости в коде, умный компилятор Java может использовать это общее подвыражение.

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

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

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

Пример 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
ArrayCreationExpression
PrimaryNoNewArray:
Литерал
ClassLiteral
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 показано здесь для удобства:

Литерал:
Целочисленный литерал
Числовой литерал с плавающей точкой
Булевый литерал
Символьный литерал
Строковый литерал
Литерал null

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

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

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

  • Тип числового литерала с плавающей точкой (§3.10.2), заканчивающегося на F или f, является float, и его значение должно быть элементом множества значений типа float (§4.2.3).

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

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

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

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

  • Тип литерала null null (§3.10.7) — тип null (§4.1); его значение — ссылка null.

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

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

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

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

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

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

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

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

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

15.8.3. this

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

  • в теле метода экземпляра или метода по умолчанию (§8.4.7, §9.4.3)

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

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

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

  • для обозначения параметра получателя (§8.4)

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

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

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

Ключевое слово this также используется в явных операциях вызова конструкторов (§8.8.7.1).

Тип this — тип класса или интерфейса T, в котором встречается ключевое слово this.

Методы по умолчанию предоставляют уникальную возможность доступа к this внутри интерфейса. (Все остальные методы интерфейса являются либо abstract, либо static, поэтому не обеспечивают доступа к this). В результате, this может иметь тип интерфейса.

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

Пример 15.8.3-1. Выражение this

class IntVector {
    int[] v;
    boolean equals(IntVector other) {
        if (this == other)
            return true;
        if (v.length != other.v.length)
            return false;
        for (int i = 0; i < v.length; i++) {
            if (v[i] != other.v[i]) return false;
        }
        return true;
    }
}

Здесь класс IntVector реализует метод equals, который сравнивает два вектора. Если другой вектор — тот же объект вектора, что и тот, для которого был вызван метод equals, то проверка может пропустить сравнение длины и значений. Метод equals реализует эту проверку, сравнивая ссылку на другой объект с this.


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

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

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

Значение выражения вида ИмяТипа.this — n-ый лексически окружающий экземпляр this.

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

Ошибка компиляции, если выражение встречается в классе или интерфейсе, который не является вложенным классом класса T или T сам.

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

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

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

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

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

  • выбор множества значений (§4.2.3) для значения выражения типа float или double.

  • является ли переменная определённо присвоенной, определённо присвоенной, когда true, определённо присвоенной, когда false, определённо неприсвоенной, определённо неприсвоенной, когда true или определённо неприсвоенной, когда false (§16 (Определённое присваивание)).

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

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

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

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

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

ClassInstanceCreationExpression:
UnqualifiedClassInstanceCreationExpression
ИмяВыражения . UnqualifiedClassInstanceCreationExpression
Первичное . UnqualifiedClassInstanceCreationExpression
UnqualifiedClassInstanceCreationExpression:
new [АргументыТипов] КлассИлиИнтерфейсДляИнициализации ( [СписокАргументов] ) [ТелоКласса]
ClassOrInterfaceTypeToInstantiate:
{Аннотация} Идентификатор {. {Аннотация} Идентификатор} [АргументыТиповИлиАлмаз]
TypeArgumentsOrDiamond:
АргументыТипов
<>

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

ArgumentList:
Выражение {, Выражение}

Выражение создания экземпляра класса задаёт класс, который должен быть инициализирован, возможно, с аргументами типа (§4.5.1) или алмазом (<>), если класс, который инициализируется, является обобщённым (§8.1.2), за которым следует (возможно, пустой) список фактических аргументов значения для конструктора.

Если список аргументов типа для класса пуст — форма алмаза <> — аргументы типа класса выводятся. Разрешено, хотя и сильно не рекомендуется с точки зрения стиля, оставлять пробелы между «<» и «>» алмаза.

Если конструктор является обобщённым (§8.8.4), аргументы типа для конструктора также могут либо быть выведены, либо переданы явно. Если они переданы явно, аргументы типа для конструктора следуют непосредственно за ключевым словом new.

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

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

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

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

Выражения создания экземпляра класса имеют две формы:

  • Выражения создания экземпляра класса без квалификатора начинаются с ключевого слова new.

    Выражение создания экземпляра класса без квалификатора можно использовать для создания экземпляра класса, независимо от того, является ли класс верхним уровнем (§7.6), членом (§8.5, §9.5), локальным (§14.3) или анонимным классом (§15.9.5).

  • Выражения создания экземпляра класса с квалификатором начинаются с выражения Первичного или ИмяВыражения.

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

И выражения создания экземпляра класса без квалификатора, и выражения создания экземпляра класса с квалификатором могут необязательно завершаться телом класса. Такое выражение создания экземпляра класса объявляет анонимный класс (§15.9.5) и создаёт экземпляр его.

Выражение создания экземпляра класса является поливыражением (§15.2), если оно использует форму алмаза для аргументов типа класса, и оно появляется в контексте присваивания или контексте вызова (§5.2, §5.3). В противном случае это выражение не самодостаточно.

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

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

Если ClassOrInterfaceTypeToInstantiate заканчивается TypeArguments (а не <>), то ClassOrInterfaceTypeToInstantiate должно обозначать правильно сформированный параметризованный тип (§4.5), в противном случае произойдет ошибка компиляции.

Если ClassOrInterfaceTypeToInstantiate заканчивается <>, но тип, обозначаемый Identifier в ClassOrInterfaceTypeToInstantiate, не является обобщённым, то произойдет ошибка компиляции.

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

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

    Identifier в ClassOrInterfaceTypeToInstantiate должно обозначать класс, который доступен, не является final и не является типом перечисления, или интерфейс, который доступен (§6.6). В противном случае произойдёт ошибка компиляции.

    Если 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 должно однозначно обозначать внутренний класс, который доступен, не является final, не является типом перечисления и является членом типа времени компиляции выражения 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. Пусть n — целое число такое, что O является n-м лексически окружающим объявлением типа класса, в котором находится выражение создания экземпляра класса.

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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 clause.

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

      В противном случае тип возврата, соответствующий выбранному конструктору, равен T.

    • Если C является анонимным классом, то:

      Процесс, указанный в §15.12.2, модифицированный для обработки конструкторов, используется для выбора одного из конструкторов непосредственного суперкласса C и определения его throws clause.

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

      В противном случае анонимный конструктор C выбирается в качестве конструктора C (§15.9.5.1). Его тело состоит из явного вызова конструктора конструктора, выбранного в непосредственном суперклассе C.

      throws clause выбранного конструктора включает исключения в throws clause конструктора, выбранного в непосредственном суперклассе 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 clause выбранного конструктора совпадает с throws clause, определенной для mj.

      Тип возврата, соответствующий выбранному конструктору, - это тип возврата, определенный для mj (§15.12.2.6).

    • Если C является анонимным классом, то анонимный конструктор C выбирается в качестве конструктора C. Его тело состоит из явного вызова конструктора cj.

      throws clause выбранного конструктора включает исключения в throws clause, определенную для mj.

      Тип возврата, соответствующий выбранному конструктору, является типом анонимного класса.

    Если выражение создания экземпляра класса является поливыражением, то его совместимость с целевым типом определяется в соответствии с §18.5.2.1, используя mj в качестве выбранного метода m.

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

    Если C является анонимным классом, то его суперкласс или суперинтерфейс (§15.9.5) является типом возврата, определенным для 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) и подробно описан в §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. Объявления анонимных классов

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

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

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

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

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

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

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

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

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

Анонимный класс не может иметь явно объявленного конструктора. Вместо этого для анонимного класса неявно объявляется анонимный конструктор. Форма анонимного конструктора для анонимного класса C с прямым суперклассом S следующая:

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

    Фактические аргументы выражения создания экземпляра класса используются для определения конструктора cs для S, как указано в §15.9.3. Тип каждого формального параметра анонимного конструктора должен быть идентичен соответствующему формальному параметру cs.

    Тело конструктора состоит из явного вызова конструктора (§8.8.7.1) вида super(...), где фактические аргументы являются формальными параметрами конструктора в порядке их объявления. Вызываемый конструктор суперкласса - cs.

  • В противном случае первый формальный параметр конструктора C представляет значение непосредственно окружающего экземпляра i относительно S (§15.9.2, §15.9.3). Тип этого параметра — тип класса, непосредственно окружающего объявление S.

    Конструктор имеет дополнительный формальный параметр для каждого фактического аргумента в выражении создания экземпляра класса, объявившего анонимный класс. n'ый формальный параметр e соответствует n-1'ому фактическому аргументу.

    Фактические аргументы выражения создания экземпляра класса используются для определения конструктора cs для S, как указано в §15.9.3. Тип каждого формального параметра анонимного конструктора должен быть идентичен соответствующему формальному параметру cs.

    Тело конструктора состоит из явного вызова конструктора (§8.8.7.1) вида o.super(...), где o — первый формальный параметр конструктора, а фактические аргументы — последующие формальные параметры конструктора в порядке их объявления. Вызываемый конструктор суперкласса — cs

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

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

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

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

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

ArrayCreationExpression:
new PrimitiveType DimExprs [Dims]
new ClassOrInterfaceType DimExprs [Dims]
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.1). Продвинутый тип должен быть 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:
ИмяВыражения [ Выражение ]
PrimaryNoNewArray [ Выражение ]

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

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

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

Индексное выражение подвергается числовому продвижению (§5.6.1). Продвинутый тип должен быть 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

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

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

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

Mount Chocorua

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


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

Формат super.Идентификатор ссылается на поле с именем Идентификатор текущего объекта, но с текущим объектом, рассматриваемым как экземпляр суперкласса текущего класса.

Формат T.super.Идентификатор ссылается на поле с именем Идентификатор лексически окружающего экземпляра, соответствующего T, но с этим экземпляром, рассматриваемым как экземпляр суперкласса T.

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

Это точно те же ситуации, в которых ключевое слово this может использоваться в объявлении класса (§15.8.3).

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

Предположим, что выражение доступа к полю super.f появляется внутри класса C, и непосредственный суперкласс C — класс S. Если f в S доступен из класса C (§6.6), тогда super.f обрабатывается так, как если бы это было выражение this.f в теле класса S. В противном случае возникает ошибка времени компиляции.

Таким образом, super.f может получить доступ к полю f, которое доступно в классе S, даже если это поле скрыто объявлением поля f в классе C.

Предположим, что выражение доступа к полю T.super.f появляется внутри класса C, и непосредственный суперкласс класса, обозначенного T, — это класс с полным квалифицированным именем S. Если f в S доступен из C, то T.super.f обрабатывается так, как если бы это было выражение this.f в теле класса S. В противном случае возникает ошибка времени компиляции.

Таким образом, T.super.f может получить доступ к полю f, которое доступно в классе S, даже если это поле скрыто объявлением поля f в классе T.

Ошибка времени компиляции, если текущий класс не является внутренним классом класса T или T сам по себе.

Пример 15.11.2-1. Выражение super

interface I           { int x = 0; }
class T1 implements I { int x = 1; }
class T2 extends T1   { int x = 2; }
class T3 extends T2 {
    int x = 3;
    void test() {
        System.out.println("x=\t\t"          + x);
        System.out.println("super.x=\t\t"    + super.x);
        System.out.println("((T2)this).x=\t" + ((T2)this).x);
        System.out.println("((T1)this).x=\t" + ((T1)this).x);
        System.out.println("((I)this).x=\t"  + ((I)this).x);
    }
}
class Test {
    public static void main(String[] args) {
        new T3().test();
    }
}

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

x=              3
super.x=        2
((T2)this).x=   2
((T1)this).x=   1
((I)this).x=    0

Внутри класса T3, выражение super.x имеет тот же эффект, что и ((T2)this).x, когда у x есть доступ к пакету. Обратите внимание, что super.x не указано в терминах приведения типа, из-за трудностей с доступом к protected членам суперкласса.


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

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

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

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

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

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

Ошибка времени компиляции, если имя слева от правого ".", которое встречается до ( в MethodInvocation, нельзя классифицировать как ИмяТипа или ИмяВыражения (§6.5.2).

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

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

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

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

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

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

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

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

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

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

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

    Если Идентификатор находится в области видимости объявления метода с таким именем (§6.3, §6.4.1), то:

    • Если существует охватывающее объявление типа, членом которого является этот метод, пусть T будет самое внутреннее такое объявление типа. Класс или интерфейс для поиска — T.

      Эта стратегия поиска называется «правилом гребня». Она эффективно ищет методы в иерархии суперклассов вложенного класса перед поиском методов в иерархии суперклассов охватывающего класса. См. §6.5.7.1 для примера.

    • В противном случае объявление метода может находиться в области видимости благодаря одному или нескольким объявлениям single-static-import или static-import-on-demand. Класса или интерфейса для поиска нет, так как метод, который будет вызван, определяется позже (§15.12.2.1).

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

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

  • Если форма представляет собой ОсновноеВыражение . [АргументыТипов] Идентификатор, то пусть T будет типом выражения ОсновноеВыражение. Класс или интерфейс для поиска — T, если T является типом класса или интерфейса, или верхняя граница T, если T является переменной типа.

    Ошибка компиляции, если T не является ссылкой на тип.

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

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

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

    • Ошибка компиляции, если ИмяТипа не обозначает ни класс, ни интерфейс.

    • Если ИмяТипа обозначает класс C, то искомый класс — суперкласс C.

      Ошибка компиляции, если C не является охватывающим объявлением типа текущего класса или если C — класс Object.

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

    • В противном случае ИмяТипа обозначает искомый интерфейс I.

      Ошибка компиляции, если I не является прямым суперинтерфейсом T или если существует другой прямой суперкласс или прямой суперинтерфейс T, J, такой, что J является подтипом I.

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

class Superclass {
    void foo() { System.out.println("Hi"); }
}

class Subclass1 extends Superclass {
    void foo() { throw new UnsupportedOperationException(); }

    Runnable tweak = new Runnable() {
        void run() {
            Subclass1.super.foo();  // Gets the 'println' behavior
        }
    };
}

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

interface Superinterface {
    default void foo() { System.out.println("Hi"); }
}

class Subclass2 implements Superinterface {
    void foo() { throw new UnsupportedOperationException(); }

    void tweak() {
        Superinterface.super.foo();  // Gets the 'println' behavior
    }
}

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

class Subclass3 implements Superinterface {
    void foo() { throw new UnsupportedOperationException(); }

    Runnable tweak = new Runnable() {
        void run() {
            Subclass3.Superinterface.super.foo();  // Illegal
        }
    };
}

Решение — ввести метод private в охватывающем объявленном типе, который выполняет вызов интерфейса super.

15.12.2. Шаг компиляции 2: Определение сигнатуры метода

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

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

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

    • Выражение ссылки на метод имеет вид ТипСсылка :: [TypeArguments] Идентификатор и по крайней мере один потенциально применимый метод либо (i) static и поддерживает арность n, или (ii) не static и поддерживает арность n-1.

    • Выражение ссылки на метод имеет другую форму, и по крайней мере один потенциально применимый метод не static.

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

  • Выражение в скобках (§15.8.5) потенциально совместимо с типом, если выражение внутри него потенциально совместимо с этим типом.

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

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

Метод 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).

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

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

  • Если форма — ИмяВыражения . [TypeArguments] Идентификатор или ОсновноеВыражение . [TypeArguments] Идентификатор, то объявление времени компиляции не должно быть static методом, объявленным в интерфейсе, иначе произойдет ошибка времени компиляции.

  • Если форма — super . [TypeArguments] Идентификатор, то:

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

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

  • Если форма — ИмяТипа . super . [TypeArguments] Идентификатор, то:

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

  • Он native.

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

  • Имя метода.

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

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

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

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

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

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

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

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

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

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

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

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

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

  • Если форма — ИмяМетода (т.е., только Идентификатор), то:

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

что выводит:

Mount Monadnock

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


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

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

Например, в:

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

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


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

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

Если вызываемый метод является методом переменной арности m, то он обязательно имеет n > 0 формальных параметров. Последний формальный параметр m обязательно имеет тип T[] для некоторого T, и m обязательно вызывается с k ≥ 0 выражениями аргументов.

Если m вызывается с k ≠ n выражениями аргументов или, если m вызывается с k = n выражениями аргументов, и тип k-го выражения аргумента не совместим с T[], то список аргументов (e1, ..., en-1, en, ..., ek) оценивается так, как если бы он был записан как (e1, ..., en-1, new |T[]| { en, ..., ek }), где |T[]| обозначает стирание (§4.6) T[].

Предшествующий абзац создан для обработки взаимодействия параметризованных типов и массивов, которое происходит в Java Virtual Machine с стиранием дженериков. Иными словами, если элемент типа T параметра переменной длины массива не может быть определен, например, List<String>, тогда особый уход должен быть предоставлен выражению создания массива (§15.10), потому что тип элемента созданного массива должен быть определен. Стиранием типа массива конечного выражения в списке аргументов гарантируется получение определимого типа элемента. Затем, поскольку выражение создания массива появляется в контексте вызова (§5.3), возможна неопределенная конверсия от типа массива с определимым типом элемента к типу массива с неопределимым типом элемента, конкретно к типу параметра переменной длины. Компилятор Java должен выдавать предупреждение об неопределенном преобразовании на этом этапе. Реализация компилятора Java от Oracle идентифицирует это предупреждение как более информативное неопределенное создание массива дженериков.

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

Выражения аргументов, если они есть, оцениваются в порядке следования слева направо. Если оценка любого выражения аргумента завершается внезапно, то ни одна часть выражения аргумента справа от него не оценивается, и вызов метода завершается внезапно по той же причине. Результат оценки j-го выражения аргумента — j-ое значение аргумента, для 1 ≤ j ≤ n. Затем оценка продолжается, используя значения аргументов, как описано ниже.

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

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

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

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

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

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

  • Если T находится в том же пакете, что и D, то T является доступным.

  • Если T находится в другом пакете, чем D, и их пакеты находятся в одном модуле, и T является public или protected, то T является доступным.

  • Если T находится в другом пакете, чем D, и их пакеты находятся в разных модулях, и модуль T экспортирует пакет T в модуль D, и T является public или protected, то T является доступным.

Если T является protected, то он обязательно является вложенным типом, поэтому во время компиляции его доступность зависит от доступности типов, окружающих его объявление. Однако во время линковки его доступность не зависит от доступности типов, окружающих его объявление. Более того, во время линковки protected T так же доступен, как и public T. Эти расхождения в контроле доступа во время компиляции (§6.6) и во время выполнения обусловлены ограничениями виртуальной машины Java.

Реализация должна также гарантировать во время линковки, что метод m всё ещё можно найти в T или в супертипе T. Если m нельзя найти, возникает ошибка NoSuchMethodError (которая является подклассом IncompatibleClassChangeError). Если m можно найти, пусть C — тип, который объявляет m. Реализация должна гарантировать во время линковки, что объявление m в C доступно для D:

  • Если m является public, то m является доступным.

  • Если m является protected, то m является доступным, если (i) либо D находится в том же пакете, что и C, или D является подтипом C или C сам по себе; и (ii) если m — это метод экземпляра protected, то T должен быть подтипом D или D сам по себе.

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

  • Если m имеет доступ к пакету, то m является доступным, если D находится в том же пакете, что и C.

  • Если m является private, то m является доступным, если D равно C, или D включает в себя C, или C включает в себя D, или C и D оба включаются третьим типом.

Если T или m не являются доступными, то возникает ошибка IllegalAccessError (§12.3).

Если режим вызова — interface, то реализация должна проверить, что целевой тип ссылки всё ещё реализует указанный интерфейс. Если целевой тип ссылки больше не реализует интерфейс, то возникает ошибка IncompatibleClassChangeError.

15.12.4.4. Поиск вызываемого метода

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

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

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

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

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

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

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

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

    • В противном случае, если режим вызова — virtual, и T и m совместно указывают на полиморфный метод сигнатуры (§15.12.3), то объект-цель является экземпляром java.lang.invoke.MethodHandle или java.lang.invoke.VarHandle. Объект-цель содержит состояние, которое сопоставляется с информацией, связанной с вызовом метода на этапе компиляции. Подробности этого сопоставления приведены в Спецификации виртуальной машины Java, издание Java SE 11 и 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 обязательно реализует T; для режима вызова virtual R обязательно является T или подклассом T. Если объект-цель является массивом, то 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, выбирается в качестве вызываемого метода.

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

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

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

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

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

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

Пример 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), которое включает любое необходимое преобразование набора значений (§5.1.13).

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

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

Если метод m не synchronized, управление передаётся в тело вызываемого метода m.

Если метод m является synchronized, то перед передачей управления объект должен быть заблокирован. Дальнейшее выполнение не может быть произведено, пока текущий поток не получит блокировку. Если есть целевая ссылка, то целевой объект должен быть заблокирован; в противном случае объект Class для класса S, класса метода m, должен быть заблокирован. Затем управление передаётся в тело вызываемого метода m. Объект автоматически разблокируется, когда выполнение тела метода завершено, будь то нормально или неожиданно. Поведение блокировки и разблокировки точно такое же, как если бы тело метода было вложено в инструкцию synchronized (§14.19).

Пример 15.12.4.5-1. Подпись вызываемого метода имеет другое стирание, чем подпись метода во время компиляции

Рассмотрим объявления:

abstract class C<T> {
    abstract T id(T x);
}
class D extends C<String> {
    String id(String x) { return x; }
}

Теперь, учитывая вызов:

C c = new D();
c.id(new Object());  // fails with a ClassCastException

Стирание фактически вызываемого метода, D.id(), отличается по своей сигнатуре от стирания объявления метода во время компиляции, C.id(). Первый принимает аргумент типа String, а второй — аргумент типа Object. Вызов завершается с исключением ClassCastException перед выполнением тела метода.

Такие ситуации могут возникать только в том случае, если программа вызывает предупреждение о неконтролируемой компиляции (§4.8, §5.1.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 :: [ПараметрыТипов] Идентификатор, пусть T будет объявлением типа, непосредственно окружающим выражение ссылки на метод. Это ошибка времени компиляции, если T — класс Object или T — интерфейс.

Если выражение ссылки на метод имеет вид ИмяТипа . super :: [ПараметрыТипов] Идентификатор, то:

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

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

  • Если ИмяТипа обозначает переменную типа, происходит ошибка времени компиляции.

Если выражение ссылки на метод имеет вид super :: [ПараметрыТипов] Идентификатор или ИмяТипа . super :: [ПараметрыТипов] Идентификатор, то это ошибка времени компиляции, если выражение встречается в статическом контексте.

Если выражение ссылки на метод имеет вид ТипКласса :: [ПараметрыТипов] 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 :: [ТипАргументов] Идентификатор, тип для поиска — тип суперкласса класса, в декларации которого содержится ссылка на метод.

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

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

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

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

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

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

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

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

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

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

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

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

Ошибка во время компиляции, если выражение ссылки на метод имеет вид ТипСсылка :: [АргументыТипов] Идентификатор, и декларация во время компиляции является static, и ТипСсылка не является простым или квалифицированным именем (§6.2).

Ошибка во время компиляции, если выражение ссылки на метод имеет вид super :: [АргументыТипов] Идентификатор или ИмяТипа . super :: [АргументыТипов] Идентификатор, и декларация во время компиляции является abstract.

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

  • Этот метод не имеет переменной арности (§8.4.1).

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

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

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

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

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

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

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

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

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

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

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

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

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

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


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

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

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:
Primary
ExpressionName
PostIncrementExpression
PostDecrementExpression

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

Правила вычисления имён выражений приведены в §6.5.6.

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

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

PostIncrementExpression:
PostfixExpression ++

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

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

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

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

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

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

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

PostDecrementExpression:
PostfixExpression --

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

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

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

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

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

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

Операторы +, -, ++, --, ~, !, и оператор приведения типов (§15.16) называются унарными операторами.

UnaryExpression:
Префиксный оператор инкремента
Префиксный оператор декремента
+ UnaryExpression
- UnaryExpression
UnaryExpressionNotPlusMinus
PreIncrementExpression:
++ UnaryExpression
PreDecrementExpression:
-- UnaryExpression
UnaryExpressionNotPlusMinus:
Постфиксное выражение
~ UnaryExpression
! UnaryExpression
Оператор приведения типов

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

CastExpression:
( Примитивный тип ) UnaryExpression
( Тип ссылки {Дополнительное ограничение} ) UnaryExpressionNotPlusMinus
( Тип ссылки {Дополнительное ограничение} ) Лямбда-выражение

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Для целочисленных значений отрицание равно вычитанию из нуля. Язык программирования Java использует представление в дополнении до двух для целых чисел, и диапазон значений в дополнении до двух несимметричен, поэтому отрицание максимального отрицательного int или long приводит к тому же максимальному отрицательному числу. В этом случае происходит переполнение, но исключение не выбрасывается. Для всех целочисленных значений x, -x равно (~x)+1.

Для чисел с плавающей запятой отрицание не равно вычитанию из нуля, потому что если x является +0.0, то 0.0-x равно +0.0, но -x равно -0.0. Унарный минус просто меняет знак числа с плавающей запятой. Специальные случаи, представляющие интерес:

  • Если операнд — NaN, результат — NaN. (Напомним, что NaN не имеет знака (§4.2.3).)

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

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

15.15.5. Оператор поразрядного дополнения ~

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

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

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

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

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

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

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

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

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

CastExpression:
( ПримитивныйТип ) УнарноеВыражение
( ТипСсылки {ДополнительноеОграничение} ) УнарноеВыражениеБезПлюсМинус
( ТипСсылки {ДополнительноеОграничение} ) LambdaВыражение

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

AdditionalBound:
& ИнтерфейсныйТип

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

Если оператор приведения содержит список типов, то есть ТипСсылки, за которым следует один или несколько ДополнительныхОграничений, то должны выполняться все следующие условия, в противном случае возникает ошибка компиляции:

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

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

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

Целевой тип для контекста приведения (§5.5), введенного выражением приведения типа, является либо ПримитивныйТип, либо ТипСсылки (если за ним не следуют ДополнительныеОграничения), отображающимся в операторе приведения, или пересечение типов, обозначаемое ТипСсылки и ДополнительныеОграничения, отображающиеся в операторе приведения.

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

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

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

Оператор приведения типов не влияет на выбор множества значений (§4.2.3) для значения типа float или типа double. Следовательно, приведение к типу float в выражении, которое не является FP-строгим (§15.4), не обязательно приводит к преобразованию его значения в элемент множества значений с плавающей точкой, а приведение к типу double в выражении, которое не является FP-строгим, не обязательно приводит к преобразованию его значения в элемент множества значений с двойной точностью.

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

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

Бросается ClassCastException, если во время выполнения выясняется, что приведение недопустимо.

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

15.17. Множительные операторы

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

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

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

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

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

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

Тип множительного выражения — это тип его операндов после продвижения.

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

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

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

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

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

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

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

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

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

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

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

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

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

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

    • Если выражение умножения — FP-строгое (§15.4):

      • Если тип выражения умножения — float, то необходимо выбрать множество значений float.

      • Если тип выражения умножения — double, то необходимо выбрать множество значений double.

    • Если выражение умножения не FP-строгое:

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

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

    Далее необходимо выбрать значение из выбранного множества значений для представления произведения.

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

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

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

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

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

Целочисленное деление округляется к 0. То есть частное для операндов n и d, которые являются целыми числами после бинарного числового повышения (§5.6.2), представляет собой целое значение q, модуль которого как можно больше, при условии |d ⋅ q| ≤ |n|. Кроме того, q является положительным, когда |n| ≥ |d| и n и d имеют одинаковый знак, но q является отрицательным, когда |n| ≥ |d| и n и d имеют разные знаки.

Существует один особый случай, который не удовлетворяет этому правилу: если делимое является целым числом с наибольшим возможным модулем для своего типа, а делитель — -1, то происходит переполнение, и результат равен делимому. Несмотря на переполнение, в этом случае исключение не генерируется. С другой стороны, если значение делителя в целочисленном делении равно 0, то генерируется ArithmeticException.

Результат дробного деления определяется правилами арифметики IEEE 754:

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

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

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

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

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

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

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

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

    • Если выражение деления является FP-строгим (§15.4):

      • Если тип выражения деления — float, то должен быть выбран набор значений с плавающей запятой.

      • Если тип выражения деления — double, то должен быть выбран набор значений с двойной точностью.

    • Если выражение деления не FP-строгое:

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

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

    Далее нужно выбрать значение из выбранного набора значений для представления частного.

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

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

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

15.17.3. Оператор остатка %

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

В C и C++ оператор остатка принимает только целочисленные операнды, но в языке программирования Java он также принимает операнды с плавающей запятой.

Операция остатка для операндов, которые являются целыми числами после бинарного числового повышения (§5.6.2), производит значение результата, такое что (a/b)*b+(a%b) равно a.

Это тождество выполняется даже в том особом случае, когда делимое является целым числом с наибольшим возможным модулем для своего типа, а делитель — -1 (остаток — 0).

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

Если значение делителя для оператора целочисленного остатка равно 0, то генерируется ArithmeticException.

Пример 15.17.3-1. Оператор целочисленного остатка

class Test1 {
    public static void main(String[] args) {
        int a = 5%3;  // 2
        int b = 5/3;  // 1
        System.out.println("5%3 produces " + a +
                           " (note that 5/3 produces " + b + ")");

        int c = 5%(-3);  // 2
        int d = 5/(-3);  // -1
        System.out.println("5%(-3) produces " + c +
                           " (note that 5/(-3) produces " + d + ")");

        int e = (-5)%3;  // -2
        int f = (-5)/3;  // -1
        System.out.println("(-5)%3 produces " + e +
                           " (note that (-5)/3 produces " + f + ")");

        int g = (-5)%(-3);  // -2
        int h = (-5)/(-3);  // 1
        System.out.println("(-5)%(-3) produces " + g +
                           " (note that (-5)/(-3) produces " + h + ")");
    }
}

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

5%3 produces 2 (note that 5/3 produces 1)
5%(-3) produces 2 (note that 5/(-3) produces -1)
(-5)%3 produces -2 (note that (-5)/3 produces -1)
(-5)%(-3) produces -2 (note that (-5)/(-3) produces 1)

Результат операции остатка с плавающей запятой, вычисляемой оператором %, не совпадает с результатом, вычисленным по определению операции остатка IEEE 754. Операция остатка IEEE 754 вычисляет остаток от округленного деления, а не от усеченного деления, и поэтому ее поведение не аналогично поведению обычного оператора остатка целых чисел. Вместо этого, язык программирования Java определяет % для операций с плавающей запятой таким образом, чтобы поведение соответствовало поведению оператора остатка целых чисел; это можно сравнить с функцией C-библиотеки fmod. Операция остатка IEEE 754 может быть вычислена процедурой библиотеки Math.IEEEremainder.

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

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

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

  • Если делимое — бесконечность или делитель — ноль, или оба, то результат — NaN.

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

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

  • В остальных случаях, когда не участвуют бесконечность, ноль или NaN, остаток с плавающей запятой r от деления делимого n на делитель d определяется математическим соотношением r = n - (d ⋅ q), где q — целое число, которое является отрицательным только в том случае, если n/d отрицательно и положительным только в том случае, если n/d положительно, а его модуль как можно больше, без превышения модуля истинного математического частного n и d.

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

Пример 15.17.3-2. Оператор остатка с плавающей запятой

class Test2 {
    public static void main(String[] args) {
        double a = 5.0%3.0;  // 2.0
        System.out.println("5.0%3.0 produces " + a);

        double b = 5.0%(-3.0);  // 2.0
        System.out.println("5.0%(-3.0) produces " + b);

        double c = (-5.0)%3.0;  // -2.0
        System.out.println("(-5.0)%3.0 produces " + c);

        double d = (-5.0)%(-3.0);  // -2.0
        System.out.println("(-5.0)%(-3.0) produces " + d);
    }
}

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

5.0%3.0 produces 2.0
5.0%(-3.0) produces 2.0
(-5.0)%3.0 produces -2.0
(-5.0)%(-3.0) produces -2.0

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.28).

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

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

Пример 15.18.1-1. Конкатенация строк

Пример выражения:

"The square root of 2 is " + Math.sqrt(2)

производит результат:

"The square root of 2 is 1.4142135623730952"

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

a + b + c

всегда трактуется как:

(a + b) + c

Поэтому результат выражения:

1 + 2 + " fiddlers"

равен:

"3 fiddlers"

но результат:

"fiddlers " + 1 + 2

равен:

"fiddlers 12"

Пример 15.18.1-2. Конкатенация строк и условные операторы

В этом юмористическом примере:

class Bottles {
    static void printSong(Object stuff, int n) {
        String plural = (n == 1) ? "" : "s";
  loop: while (true) {
            System.out.println(n + " bottle" + plural
                    + " of " + stuff + " on the wall,");
            System.out.println(n + " bottle" + plural
                    + " of " + stuff + ";");
            System.out.println("You take one down "
                    + "and pass it around:");
            --n;
            plural = (n == 1) ? "" : "s";
            if (n == 0)
                break loop;
            System.out.println(n + " bottle" + plural
                    + " of " + stuff + " on the wall!");
            System.out.println();
        }
        System.out.println("No bottles of " +
                    stuff + " on the wall!");
    }

    public static void main(String[] args) {
        printSong("slime", 3);
    }
}

метод printSong будет печатать отрывок детской песни. Популярные значения для stuff включают "pop" и "beer"; наиболее популярное значение для n — 100. Вот вывод, полученный при запуске программы:

3 bottles of slime on the wall,
3 bottles of slime;
You take one down and pass it around:
2 bottles of slime on the wall!

2 bottles of slime on the wall,
2 bottles of slime;
You take one down and pass it around:
1 bottle of slime on the wall!

1 bottle of slime on the wall,
1 bottle of slime;
You take one down and pass it around:
No bottles of slime on the wall!

В коде обратите внимание на внимательное условное формирование единственного числа "bottle" при необходимости вместо множественного числа "bottles"; обратите также внимание на то, как оператор конкатенации строк был использован для разделения длинной константной строки:

"You take one down and pass it around:"

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


15.18.2. Арифметические операторы (+ и -) для числовых типов

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

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

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

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

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

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

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

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

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

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

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

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

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

  • Сумма двух бесконечностей разных знаков — NaN.

  • Сумма двух бесконечностей одного знака — бесконечность того же знака.

  • Сумма бесконечности и конечного значения равна бесконечному операнду.

  • Сумма двух нулей разных знаков — положительный ноль.

  • Сумма двух нулей одного знака — ноль того же знака.

  • Сумма нуля и ненулевого конечного значения равна ненулевому операнду.

  • Сумма двух ненулевых конечных значений одинаковой величины и разных знаков — положительный ноль.

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

    • Если выражение сложения FP-строгое (§15.4):

      • Если тип выражения сложения — float, то должно быть выбрано множество значений с плавающей запятой.

      • Если тип выражения сложения — double, то должно быть выбрано множество значений с двойной запятой.

    • Если выражение сложения не FP-строгое:

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

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

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

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

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

Бинарный оператор - выполняет вычитание, когда применяется к двум операндам числового типа, производя разность операндов; левый операнд — уменьшаемое, а правый операнд — вычитаемое.

Как для целочисленного, так и для вычитания с плавающей запятой, всегда верно, что a-b даёт тот же результат, что и a+(-b).

Обратите внимание, что для целочисленных значений вычитание из нуля то же самое, что и отрицание. Однако для операндов с плавающей запятой вычитание из нуля не то же самое, что отрицание, так как если x — +0.0, то 0.0-x это +0.0, но -x — -0.0.

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

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

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

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

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

Проводится унарное численное продвижение (§5.6.1) для каждого операнда отдельно. (Бинарное численное продвижение (§5.6.2) не применяется к операндам.)

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

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

Если тип левого операнда после продвижения — int, то только пять младших битов правого операнда используются в качестве расстояния сдвига. Это как будто правый операнд был подвергнут оператору побитового логического И & (§15.22.1) с маской 0x1f (0b11111). Расстояние сдвига, фактически используемое, поэтому всегда находится в диапазоне 0 по 31 включительно.

Если тип левого операнда после продвижения — long, то только шесть младших битов правого операнда используются в качестве расстояния сдвига. Это как будто правый операнд был подвергнут оператору побитового логического И & (§15.22.1) с маской 0x3f (0b111111). Расстояние сдвига, фактически используемое, поэтому всегда находится в диапазоне 0 по 63 включительно.

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

Значение n << s — это n, сдвинутое влево на s позиций; это эквивалентно (даже если происходит переполнение) умножению на два в степени s.

Значение n >> s — это n, сдвинутое вправо на s позиций со знаком; результат — floor(n / 2s). Для неотрицательных значений n это эквивалентно усечению целочисленного деления, вычисленного оператором целочисленного деления /, на два в степени s.

Значение n >>> s — это n, сдвинутое вправо на s позиций с расширением нулями.

  • Если n положительно, то результат такой же, как у n >> s.

  • Если n отрицательно и тип левого операнда — int, то результат равен результату выражения (n >> s) + (2 << ~s).

  • Если n отрицательно и тип левого операнда — long, то результат равен результату выражения (n >> s) + (2L << ~s).

Добавленный член (2 << ~s) или (2L << ~s) отменяет распространение знака.

Обратите внимание, что из-за неявного маскирования правого операнда оператора сдвига ~s как расстояние сдвига эквивалентно 31-s при сдвиге значения int и 63-s при сдвиге значения long.

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

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

RelationalExpression:
ShiftExpression
RelationalExpression < ShiftExpression
RelationalExpression > ShiftExpression
RelationalExpression <= ShiftExpression
RelationalExpression >= ShiftExpression
RelationalExpression instanceof ReferenceType

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

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

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

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

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

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

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

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

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

Сравнение выполняется точно для значений с плавающей точкой, независимо от набора значений, из которых были взяты их представляющие значения.

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

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

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

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

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

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

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

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

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

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

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

15.20.2. Оператор сравнения типов instanceof

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

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

Если приведение типа RelationalExpression к ReferenceType было бы отклонено как ошибка компиляции (§15.16), то выражение сравнения instanceof также приводит к ошибке компиляции. В такой ситуации результат выражения instanceof никогда не может быть истинным.

Во время выполнения результат оператора instanceof равен true, если значение RelationalExpression не равно null и ссылка может быть приведена к ReferenceType без повышения ClassCastException. В противном случае результат равен false.

Пример 15.20.2-1. Оператор instanceof

class Point   { int x, y; }
class Element { int atomicNumber; }
class Test {
    public static void main(String[] args) {
        Point   p = new Point();
        Element e = new Element();
        if (e instanceof Point) {  // compile-time error
            System.out.println("I get your point!");
            p = (Point)e;  // compile-time error
        }
    }
}

Эта программа приводит к двум ошибкам компиляции. Приведение (Point)e некорректно, поскольку ни один экземпляр Element или любой из его возможных подклассов (не показанных здесь) не может быть экземпляром любого подкласса Point. Выражение instanceof некорректно по той же причине. Если бы, с другой стороны, класс Point был подклассом Element (что является довольно странной идеей в этом примере):


class Point extends Element { int x, y; }

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


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.2).

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

Если тип продвижения операндов — int или long, то выполняется целочисленное сравнение на равенство.

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

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

Сравнение на равенство с плавающей точкой выполняется в соответствии с правилами стандарта IEEE 754:

  • Если какой-либо операнд является NaN, то результат == равен false, но результат != равен true.

    Действительно, проверка x!=x равна true тогда и только тогда, когда значение x равно NaN.

    Методы Float.isNaN и Double.isNaN также могут использоваться для проверки, является ли значение NaN.

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

    Например, -0.0==0.0 равно true.

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

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

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

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

  • Значение, вычисленное оператором !=, равно true, если значение левого операнда не равно значению правого операнда; в противном случае результат равен false.

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). Содержимое двух строк 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.2).

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

Для & результат — битовое И значений операндов.

Для ^ результат — битовое исключающее ИЛИ значений операндов.

Для | результат — битовое включительно ИЛИ значений операндов.

Например, результат выражения:

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 ? Expression : ConditionalExpression
ConditionalOrExpression ? Expression : 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.

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

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

    • Выражение вызова метода (§15.12) для которого выбранный наиболее специфичный метод (§15.12.2.5) имеет возвращаемый тип boolean или Boolean.

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

    • Условное выражение boolean.

  • Если оба вторые и третий операнды являются численными выражениями, условное выражение является численным условным выражением.

    Для целей классификации условного выражения, следующие выражения являются численными:

    • Выражение в автономной форме (§15.2) с типом, преобразуемым в числовой тип (§4.2, §5.1.8).

    • Численное выражение в скобках (§15.8.5).

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

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

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

    • Численное условное выражение.

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

Процесс определения типа условного выражения зависит от вида условного выражения, как указано в следующих разделах.

Следующие таблицы обобщают вышеуказанные правила, указывая тип условного выражения для всех возможных типов его второго и третьего операндов. bnp(..) означает применение двоичного числового повышения. Форма «T | bnp(..)» используется, когда один операнд является константным выражением типа int и может быть представлен типом T, где двоичное числовое повышение используется, если операнд не может быть представлен типом T. Тип операнда Object означает любой тип ссылки, кроме типа null и восьми обёрток классов 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

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

Полученное boolean значение используется для выбора либо второго, либо третьего выражения операнда:

  • Если значение первого операнда является true, то выбирается второе выражение операнда.

  • Если значение первого операнда является false, то выбирается третье выражение операнда.

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

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

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

15.25.1. Условные выражения типа boolean

Условные выражения типа boolean являются автономными выражениями (§15.2).

Тип условного выражения boolean определяется следующим образом:

  • Если второй и третий операнды оба имеют тип Boolean, то условное выражение имеет тип Boolean.

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

15.25.2. Условные выражения числового типа

Числовые условные выражения являются автономными выражениями (§15.2).

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

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

  • Если один из вторых и третьих операндов является примитивного типа T, а тип другого является результатом применения преобразования упаковки (§5.1.7) к T, то типом условного выражения является T.

  • Если один из операндов имеет тип byte или Byte, а другой имеет тип short или Short, то типом условного выражения является short.

  • Если один из операндов имеет тип T, где T является byte, short или char, а другой операнд является константным выражением (§15.28) типа int, значение которого представимо в типе T, то типом условного выражения является T.

  • Если один из операндов имеет тип T, где T является Byte, Short или Character, а другой операнд — константное выражение типа int, значение которого представимо в типе U, который является результатом применения преобразования распаковки к T, то типом условного выражения является U.

  • В противном случае применяется бинарное численное продвижение (§5.6.2) к типам операндов, и тип условного выражения является продвинутым типом второго и третьего операндов.

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

15.25.3. Ссылочные условные выражения

Ссылочное условное выражение является поливыражением, если оно встречается в контексте присваивания или вызова (§5.2. §5.3). В противном случае, это самостоятельное выражение.

Если полисправочное условное выражение появляется в контексте определённого типа с целевым типом T, то его второе и третье операндные выражения также появляются в контексте того же типа с целевым типом T.

Полисправочное условное выражение совместимо с целевым типом T, если его второе и третье операндные выражения совместимы с T.

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

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

  • Если у второго и третьего операндов один и тот же тип (который может быть нулевым типом), то это и будет типом условного выражения.

  • Если тип одного из вторых и третьих операндов — нулевой тип, а тип другого операнда — ссылочный тип, то тип условного выражения — этот ссылочный тип.

  • В противном случае второй и третий операнды имеют типы S1 и S2 соответственно. Пусть T1 — тип, полученный при применении преобразования к S1, а T2 — тип, полученный при применении преобразования к S2. Тип условного выражения — результат применения преобразования захвата (§5.1.10) к lub(T1, T2).

Поскольку ссылочные условные выражения могут быть поливыражениями, они могут «передавать» контекст своим операндам. Это позволяет лямбда-выражениям и выражениям ссылок на методы появляться в качестве операндов:

return ... ? (x -> x) : (x -> -x);

Это также позволяет использовать дополнительную информацию для улучшения проверки типов универсальных вызовов методов. До Java SE 8 это присваивание было корректным с точки зрения типов:

List<String> ls = Arrays.asList();

но это не было:

List<String> ls = ... ? Arrays.asList() : Arrays.asList("a","b");

Вышеприведённые правила позволяют рассматривать оба присваивания как корректные с точки зрения типов.

Обратите внимание, что ссылочное условное выражение не обязательно содержит поливыражение в качестве операнда, чтобы быть поливыражением. Это поливыражение просто в силу контекста, в котором оно появляется. Например, в следующем коде условное выражение является поливыражением, а каждый операнд рассматривается как находящийся в контексте присваивания к Class<? super Integer>:


Class<? super Integer> choose(boolean b,
                              Class<Integer> c1,
                              Class<Number> c2) {
    return b ? c1 : c2;
}

Если условное выражение не было поливыражением, то возникла бы ошибка компиляции, так как его тип был бы lub(Class<Integer>, Class<Number>) = Class<? extends Number>, который несовместим с возвращаемым типом choose.

15.26. Операторы присваивания

Существует 12 операторов присваивания; все они синтаксически ассоциативны справа налево (они группируются справа налево). Таким образом, a=b=c означает a=(b=c), которое присваивает значение c переменной b, а затем присваивает значение b переменной a.

AssignmentExpression:
ConditionalExpression
Assignment
Assignment:
Левая часть Оператор присваивания Выражение
LeftHandSide:
Имя выражения
Доступ к полю
Доступ к массиву
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.

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

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

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

      Компилятор Java может доказать во время компиляции, что компонент массива будет именно типа TC (например, TC может быть final). Но если компилятор Java не может доказать во время компиляции, что компонент массива будет именно типа TC, то необходимо выполнить проверку во время выполнения, чтобы убедиться, что класс RC совместим с фактическим типом SC компонента массива (§5.2).

      Эта проверка похожа на сужение приведения типов (§5.5, §15.16), за исключением того, что если проверка завершается неудачно, выбрасывается ArrayStoreException, а не ClassCastException.

      Если класс RC не может быть присвоен типу SC, то присваивание не происходит, и выбрасывается ArrayStoreException.

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

В противном случае требуется три шага:

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

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

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

Пример 15.26.1-1. Простое присваивание компоненту массива

class ArrayReferenceThrow extends RuntimeException { }
class IndexThrow          extends RuntimeException { }
class RightHandSideThrow  extends RuntimeException { }

class IllustrateSimpleArrayAssignment {
    static Object[] objects = { new Object(), new Object() };
    static Thread[] threads = { new Thread(), new Thread() };

    static Object[] arrayThrow() {
        throw new ArrayReferenceThrow();
    }
    static int indexThrow() {
        throw new IndexThrow();
    }
    static Thread rightThrow() {
        throw new RightHandSideThrow();
    }
    static String name(Object q) {
        String sq = q.getClass().getName();
        int k = sq.lastIndexOf('.');
        return (k < 0) ? sq : sq.substring(k+1);
    }

    static void testFour(Object[] x, int j, Object y) {
        String sx = x == null ? "null" : name(x[0]) + "s";
        String sy = name(y);
        System.out.println();
        try {
            System.out.print(sx + "[throw]=throw => ");
            x[indexThrow()] = rightThrow();
            System.out.println("Okay!");
        } catch (Throwable e) { System.out.println(name(e)); }
        try {
            System.out.print(sx + "[throw]=" + sy + " => ");
            x[indexThrow()] = y;
            System.out.println("Okay!");
        } catch (Throwable e) { System.out.println(name(e)); }
        try {
            System.out.print(sx + "[" + j + "]=throw => ");
            x[j] = rightThrow();
            System.out.println("Okay!");
        } catch (Throwable e) { System.out.println(name(e)); }
        try {
            System.out.print(sx + "[" + j + "]=" + sy + " => ");
            x[j] = y;
            System.out.println("Okay!");
        } catch (Throwable e) { System.out.println(name(e)); }
    }

    public static void main(String[] args) {
        try {
            System.out.print("throw[throw]=throw => ");
            arrayThrow()[indexThrow()] = rightThrow();
            System.out.println("Okay!");
        } catch (Throwable e) { System.out.println(name(e)); }
        try {
            System.out.print("throw[throw]=Thread => ");
            arrayThrow()[indexThrow()] = new Thread();
            System.out.println("Okay!");
        } catch (Throwable e) { System.out.println(name(e)); }
        try {
            System.out.print("throw[1]=throw => ");
            arrayThrow()[1] = rightThrow();
            System.out.println("Okay!");
        } catch (Throwable e) { System.out.println(name(e)); }
        try {
            System.out.print("throw[1]=Thread => ");
            arrayThrow()[1] = new Thread();
            System.out.println("Okay!");
        } catch (Throwable e) { System.out.println(name(e)); }

        testFour(null, 1, new StringBuffer());
        testFour(null, 9, new Thread());
        testFour(objects, 1, new StringBuffer());
        testFour(objects, 1, new Thread());
        testFour(objects, 9, new StringBuffer());
        testFour(objects, 9, new Thread());
        testFour(threads, 1, new StringBuffer());
        testFour(threads, 1, new Thread());
        testFour(threads, 9, new StringBuffer());
        testFour(threads, 9, new Thread());
    }
}

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

throw[throw]=throw => ArrayReferenceThrow
throw[throw]=Thread => ArrayReferenceThrow
throw[1]=throw => ArrayReferenceThrow
throw[1]=Thread => ArrayReferenceThrow

null[throw]=throw => IndexThrow
null[throw]=StringBuffer => IndexThrow
null[1]=throw => RightHandSideThrow
null[1]=StringBuffer => NullPointerException

null[throw]=throw => IndexThrow
null[throw]=Thread => IndexThrow
null[9]=throw => RightHandSideThrow
null[9]=Thread => NullPointerException

Objects[throw]=throw => IndexThrow
Objects[throw]=StringBuffer => IndexThrow
Objects[1]=throw => RightHandSideThrow
Objects[1]=StringBuffer => Okay!

Objects[throw]=throw => IndexThrow
Objects[throw]=Thread => IndexThrow
Objects[1]=throw => RightHandSideThrow
Objects[1]=Thread => Okay!

Objects[throw]=throw => IndexThrow
Objects[throw]=StringBuffer => IndexThrow
Objects[9]=throw => RightHandSideThrow
Objects[9]=StringBuffer => ArrayIndexOutOfBoundsException

Objects[throw]=throw => IndexThrow
Objects[throw]=Thread => IndexThrow
Objects[9]=throw => RightHandSideThrow
Objects[9]=Thread => ArrayIndexOutOfBoundsException

Threads[throw]=throw => IndexThrow
Threads[throw]=StringBuffer => IndexThrow
Threads[1]=throw => RightHandSideThrow
Threads[1]=StringBuffer => ArrayStoreException

Threads[throw]=throw => IndexThrow
Threads[throw]=Thread => IndexThrow
Threads[1]=throw => RightHandSideThrow
Threads[1]=Thread => Okay!

Threads[throw]=throw => IndexThrow
Threads[throw]=StringBuffer => IndexThrow
Threads[9]=throw => RightHandSideThrow
Threads[9]=StringBuffer => ArrayIndexOutOfBoundsException

Threads[throw]=throw => IndexThrow
Threads[throw]=Thread => IndexThrow
Threads[9]=throw => RightHandSideThrow
Threads[9]=Thread => ArrayIndexOutOfBoundsException

Самый интересный случай из всех — тринадцатый с конца:

Threads[1]=StringBuffer => ArrayStoreException

что указывает на то, что попытка сохранить ссылку на StringBuffer в массив, компоненты которого имеют тип Thread, вызывает исключение ArrayStoreException. Код типобезопасен во время компиляции: у присваивания левый операнд типа Object[] и правый операнд типа Object. Во время выполнения первым фактическим аргументом метода testFour является ссылка на экземпляр "массив Thread", а третьим фактическим аргументом является ссылка на экземпляр класса StringBuffer.


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);

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

Если выражение левой части не является выражением доступа к массиву, то:

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

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

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

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

Если выражение левой части является выражением доступа к массиву (§15.10.3), то:

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

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

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

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

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

    Для простого оператора присваивания вычисление операнда правой части происходит до проверок выражения ссылки на массив и выражения индекса, но для составного оператора присваивания вычисление операнда правой части происходит после этих проверок.

  • В противном случае, рассмотрим компонент массива, выбранный на предыдущем шаге, значение которого было сохранено. Этот компонент является переменной; назовем его тип S. Также, пусть T — тип левого операнда оператора присваивания, определенный во время компиляции.

    • Если T — примитивный тип, то S обязательно такой же, как T.

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

      Если эта операция завершается неожиданно (единственная возможность — целочисленное деление на ноль — см. §15.17.2), то выражение присваивания завершается неожиданно по той же причине и присваивание не происходит.

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

    • Если T — тип ссылки, то он должен быть String. Поскольку класс String — класс final, S также должен быть String.

      Поэтому проверка во время выполнения, которая иногда требуется для простого оператора присваивания, никогда не требуется для составного оператора присваивания.

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

      В противном случае, String результат бинарной операции сохраняется в компоненте массива.

Пример 15.26.2-1. Составное присваивание компоненту массива

class ArrayReferenceThrow extends RuntimeException { }
class IndexThrow          extends RuntimeException { }
class RightHandSideThrow  extends RuntimeException { }

class IllustrateCompoundArrayAssignment {
    static String[] strings = { "Simon", "Garfunkel" };
    static double[] doubles = { Math.E, Math.PI };

    static String[] stringsThrow() {
        throw new ArrayReferenceThrow();
    }
    static double[] doublesThrow() {
        throw new ArrayReferenceThrow();
    }
    static int indexThrow() {
        throw new IndexThrow();
    }
    static String stringThrow() {
        throw new RightHandSideThrow();
    }
    static double doubleThrow() {
        throw new RightHandSideThrow();
    }
    static String name(Object q) {
        String sq = q.getClass().getName();
        int k = sq.lastIndexOf('.');
        return (k < 0) ? sq : sq.substring(k+1);
    }

    static void testEight(String[] x, double[] z, int j) {
        String sx = (x == null) ? "null" : "Strings";
        String sz = (z == null) ? "null" : "doubles";
        System.out.println();
        try {
            System.out.print(sx + "[throw]+=throw => ");
            x[indexThrow()] += stringThrow();
            System.out.println("Okay!");
        } catch (Throwable e) { System.out.println(name(e)); }
        try {
            System.out.print(sz + "[throw]+=throw => ");
            z[indexThrow()] += doubleThrow();
            System.out.println("Okay!");
        } catch (Throwable e) { System.out.println(name(e)); }
        try {
            System.out.print(sx + "[throw]+=\"heh\" => ");
            x[indexThrow()] += "heh";
            System.out.println("Okay!");
        } catch (Throwable e) { System.out.println(name(e)); }
        try {
            System.out.print(sz + "[throw]+=12345 => ");
            z[indexThrow()] += 12345;
            System.out.println("Okay!");
        } catch (Throwable e) { System.out.println(name(e)); }
        try {
            System.out.print(sx + "[" + j + "]+=throw => ");
            x[j] += stringThrow();
            System.out.println("Okay!");
        } catch (Throwable e) { System.out.println(name(e)); }
        try {
            System.out.print(sz + "[" + j + "]+=throw => ");
            z[j] += doubleThrow();
            System.out.println("Okay!");
        } catch (Throwable e) { System.out.println(name(e)); }
        try {
            System.out.print(sx + "[" + j + "]+=\"heh\" => ");
            x[j] += "heh";
            System.out.println("Okay!");
        } catch (Throwable e) { System.out.println(name(e)); }
        try {
            System.out.print(sz + "[" + j + "]+=12345 => ");
            z[j] += 12345;
            System.out.println("Okay!");
        } catch (Throwable e) { System.out.println(name(e)); }
    }

    public static void main(String[] args) {
        try {
            System.out.print("throw[throw]+=throw => ");
            stringsThrow()[indexThrow()] += stringThrow();
            System.out.println("Okay!");
        } catch (Throwable e) { System.out.println(name(e)); }
        try {
            System.out.print("throw[throw]+=throw => ");
            doublesThrow()[indexThrow()] += doubleThrow();
            System.out.println("Okay!");
        } catch (Throwable e) { System.out.println(name(e)); }
        try {
            System.out.print("throw[throw]+=\"heh\" => ");
            stringsThrow()[indexThrow()] += "heh";
            System.out.println("Okay!");
        } catch (Throwable e) { System.out.println(name(e)); }
        try {
            System.out.print("throw[throw]+=12345 => ");
            doublesThrow()[indexThrow()] += 12345;
            System.out.println("Okay!");
        } catch (Throwable e) { System.out.println(name(e)); }
        try {
            System.out.print("throw[1]+=throw => ");
            stringsThrow()[1] += stringThrow();
            System.out.println("Okay!");
        } catch (Throwable e) { System.out.println(name(e)); }
        try {
            System.out.print("throw[1]+=throw => ");
            doublesThrow()[1] += doubleThrow();
            System.out.println("Okay!");
        } catch (Throwable e) { System.out.println(name(e)); }
        try {
            System.out.print("throw[1]+=\"heh\" => ");
            stringsThrow()[1] += "heh";
            System.out.println("Okay!");
        } catch (Throwable e) { System.out.println(name(e)); }
        try {
            System.out.print("throw[1]+=12345 => ");
            doublesThrow()[1] += 12345;
            System.out.println("Okay!");
        } catch (Throwable e) { System.out.println(name(e)); }
        testEight(null, null, 1);
        testEight(null, null, 9);
        testEight(strings, doubles, 1);
        testEight(strings, doubles, 9);
    }
}

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

throw[throw]+=throw => ArrayReferenceThrow
throw[throw]+=throw => ArrayReferenceThrow
throw[throw]+="heh" => ArrayReferenceThrow
throw[throw]+=12345 => ArrayReferenceThrow
throw[1]+=throw => ArrayReferenceThrow
throw[1]+=throw => ArrayReferenceThrow
throw[1]+="heh" => ArrayReferenceThrow
throw[1]+=12345 => ArrayReferenceThrow

null[throw]+=throw => IndexThrow
null[throw]+=throw => IndexThrow
null[throw]+="heh" => IndexThrow
null[throw]+=12345 => IndexThrow
null[1]+=throw => NullPointerException
null[1]+=throw => NullPointerException
null[1]+="heh" => NullPointerException
null[1]+=12345 => NullPointerException

null[throw]+=throw => IndexThrow
null[throw]+=throw => IndexThrow
null[throw]+="heh" => IndexThrow
null[throw]+=12345 => IndexThrow
null[9]+=throw => NullPointerException
null[9]+=throw => NullPointerException
null[9]+="heh" => NullPointerException
null[9]+=12345 => NullPointerException

Strings[throw]+=throw => IndexThrow
doubles[throw]+=throw => IndexThrow
Strings[throw]+="heh" => IndexThrow
doubles[throw]+=12345 => IndexThrow
Strings[1]+=throw => RightHandSideThrow
doubles[1]+=throw => RightHandSideThrow
Strings[1]+="heh" => Okay!
doubles[1]+=12345 => Okay!

Strings[throw]+=throw => IndexThrow
doubles[throw]+=throw => IndexThrow
Strings[throw]+="heh" => IndexThrow
doubles[throw]+=12345 => IndexThrow
Strings[9]+=throw => ArrayIndexOutOfBoundsException
doubles[9]+=throw => ArrayIndexOutOfBoundsException
Strings[9]+="heh" => ArrayIndexOutOfBoundsException
doubles[9]+=12345 => ArrayIndexOutOfBoundsException

Наиболее интересными являются одиннадцатый и двенадцатый с конца:

Strings[1]+=throw => RightHandSideThrow
doubles[1]+=throw => RightHandSideThrow

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


Пример 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. Lambda-выражения

Lambda-выражение похоже на метод: оно предоставляет список формальных параметров и тело — выражение или блок — выраженные через эти параметры.

LambdaExpression:
LambdaParameters -> LambdaBody

Lambda-выражения всегда являются поливыражениями (§15.2).

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

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

Вот несколько примеров lambda-выражений:


() -> {}                // No parameters; result is void
() -> 42                // No parameters, expression body
() -> null              // No parameters, expression body
() -> { return 42; }    // No parameters, block body with return
() -> { System.gc(); }  // No parameters, void block body

() -> {                 // Complex block body with returns
  if (true) return 12;
  else {
    int result = 15;
    for (int i = 1; i < 10; i++)
      result *= i;
    return result;
  }
}                          

(int x) -> x+1              // Single declared-type parameter
(int x) -> { return x+1; }  // Single declared-type parameter
(x) -> x+1                  // Single inferred-type parameter
x -> x+1                    // Parentheses optional for
                            // single inferred-type parameter

(String s) -> s.length()      // Single declared-type parameter
(Thread t) -> { t.start(); }  // Single declared-type parameter
s -> s.length()               // Single inferred-type parameter
t -> { t.start(); }           // Single inferred-type parameter

(int x, int y) -> x+y  // Multiple declared-type parameters
(x, y) -> x+y          // Multiple inferred-type parameters
(x, int y) -> x+y    // Illegal: can't mix inferred and declared types
(x, final y) -> x+y  // Illegal: no modifiers with inferred types

Этот синтаксис имеет преимущество в минимизации скобок вокруг простых lambda-выражений, что особенно полезно, когда lambda-выражение является аргументом метода или когда тело является другим lambda-выражением. Он также чётко различает выражения и операторы, что предотвращает неоднозначности или чрезмерную зависимость от ';' маркеров. Когда для визуального разграничения всего lambda-выражения или его тела требуется дополнительная скобочная конструкция, скобки поддерживаются естественным образом (точно так же, как и в других случаях, когда порядок операций неясен).

Синтаксис имеет некоторые проблемы с парсингом. Язык программирования Java всегда требовал произвольного предвосхищения, чтобы различать типы и выражения после маркера '(': то, что следует, может быть приведением типа или скобочным выражением. Это усугубилось при использовании дженериков, которые повторно использовали бинарные операторы '<' и '>' в типах. Lambda-выражения вводят новую возможность: токены, следующие за '(', могут описывать тип, выражение или список параметров lambda. Некоторые токены сразу указывают на список параметров (аннотации, final); в других случаях существуют определённые шаблоны, которые должны интерпретироваться как списки параметров (два имени подряд, ',' не вложенное в '<' и '>'); и иногда решение невозможно принять до тех пор, пока не будет встречен '->' после ')'. Самый простой способ представить, как это можно эффективно распарсить, — это с помощью конечного автомата: каждый состояние представляет собой подмножество возможных интерпретаций (тип, выражение или параметры), и когда автомат переходит в состояние, в котором множество является единственным, парсер понимает, какой случай имеет место. Однако это не очень элегантно отображается на грамматику с фиксированным предвосхищением.

Нет специальной нулевой формы: lambda-выражение без аргументов выражается как () -> .... Очевидный специальный синтаксис, -> ..., не работает, потому что он создаёт неоднозначность между списками аргументов и приведениями типов: (x) -> ....

Lambda-выражения не могут объявлять параметры типа. Хотя это имело бы смысл с точки зрения семантики, естественный синтаксис (предшествующий списку параметров с списком параметров типа) вводит запутанные неоднозначности. Например, рассмотрим:

foo( (x) < y , z > (w) -> v )

Это может быть вызов foo с одним аргументом (дженерическое lambda-приведение к типу x), или это может быть вызов foo с двумя аргументами, оба являющиеся результатами сравнений, второе сравнивает z с lambda-выражением. (Строго говоря, lambda-выражение не имеет смысла в качестве операнда бинарного оператора >, но это ненадёжное предположение, на основе которого строится грамматика).

Существует прецедент для разрешения неоднозначностей, связанных с приведением типов, который по существу запрещает использование - и + после не примитивного приведения типов (§15.15), но расширение этого подхода на дженерические lambda-выражения потребовало бы значительных изменений в грамматике.

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

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

Декларация двух формальных параметров с одинаковым именем в лямбда-выражении (то есть их декларации упоминают один и тот же Identifier) является ошибкой компиляции.

В Java SE 8 использование _ в качестве имени параметра лямбда-выражения было запрещено, а использование в качестве имени других типов переменных было нежелательно (§4.12.3). С Java SE 9 _ — ключевое слово (§3.9), поэтому оно не может использоваться в качестве имени переменной ни в каком контексте.

Если формальный параметр, объявленный как final, присваивается в теле лямбда-выражения, то это ошибка компиляции.

При вызове лямбда-выражения (через выражение вызова метода (§15.12)) значения выражений фактических аргументов инициализируют вновь созданные переменные параметров, каждый из объявленного или выведенного типа, перед выполнением тела лямбда-выражения. Identifier, который появляется в LambdaParameter или непосредственно в LambdaParameterList или LambdaParameters, может использоваться как простое имя в теле лямбда-выражения для ссылки на формальный параметр.

Формальный параметр лямбда-выражения типа float всегда содержит элемент из набора значений типа float (§4.2.3); аналогично, формальный параметр лямбда-выражения типа double всегда содержит элемент из набора значений типа double. Не допускается, чтобы формальный параметр лямбда-выражения типа float содержал элемент из набора значений с расширенным порядком значений типа float, который не является также элементом набора значений типа float, а также для формального параметра лямбда-выражения типа double содержал элемент из набора значений с расширенным порядком значений типа double, который не является также элементом набора значений типа double.

15.27.2. Тело лямбда-выражения

Тело лямбда-выражения — это либо одно выражение, либо блок (§14.2). Как и тело метода, тело лямбда-выражения описывает код, который будет выполняться при каждом вызове.

LambdaBody:
Выражение
Блок

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

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

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

Тело лямбда-выражения-блока является совместимым с void, если каждая команда return в блоке имеет вид return;.

Тело лямбда-выражения-блока является совместимым со значением, если оно не может завершиться нормально (§14.21) и каждая команда return в блоке имеет вид return Выражение;.

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

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

Следующие тела лямбда-выражений совместимы с void:

() -> {}
() -> { System.out.println("done"); }

Эти совместимы со значением:

() -> { return "done"; }
() -> { if (...) return 1; else return 0; }

Эти оба совместимы:

() -> { throw new RuntimeException(); }
() -> { while (true); }

Это ни то, ни другое:

() -> { if (...) return "done"; System.out.println("done"); }

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

Обратите внимание, что определение совместимости void/значение не является строго структурным свойством: «может завершиться нормально» зависит от значений константных выражений, и эти значения могут включать имена, ссылающиеся на константные переменные.

Любая локальная переменная, формальный параметр или параметр исключения, используемый, но не объявленный в лямбда-выражении, должен быть либо объявлен final, либо быть эффективно final (§4.12.4), иначе возникает ошибка компиляции в месте использования.

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

Аналогичные правила использования переменных применяются в теле внутреннего класса (§8.1.3). Ограничение на эффективно final переменные запрещает доступ к динамически изменяющимся локальным переменным, захват которых, вероятно, приведет к проблемам с конкурентностью. По сравнению с ограничением final, это снижает трудоёмкость программистов.

Ограничение на эффективно final переменные включает стандартные переменные цикла, но не переменные цикла с расширенным for, которые обрабатываются отдельно для каждой итерации цикла (§14.14.2).

Следующие тела лямбда-выражений демонстрируют использование эффективно final переменных.


void m1(int x) {
    int y = 1;
    foo(() -> x+y);
    // Legal: x and y are both effectively final.
}

void m2(int x) {
    int y;
    y = 1;
    foo(() -> x+y);
    // Legal: x and y are both effectively final.
}

void m3(int x) {
    int y;
    if (...) y = 1;
    foo(() -> x+y);
    // Illegal: y is effectively final, but not definitely assigned.
}

void m4(int x) {
    int y;
    if (...) y = 1; else y = 2;
    foo(() -> x+y);
    // Legal: x and y are both effectively final.
}


void m5(int x) {
    int y;
    if (...) y = 1;
    y = 2;
    foo(() -> x+y);
    // Illegal: y is not effectively final.
}

void m6(int x) {
    foo(() -> x+1);
    x++;
    // Illegal: x is not effectively final.
}

void m7(int x) {
    foo(() -> x=1);
    // Illegal: x is not effectively final.
}

void m8() {
    int y;
    foo(() -> y=1);
    // Illegal: y is not definitely assigned before the lambda.
}

void m9(String[] arr) {
    for (String s : arr) {
        foo(() -> s);
        // Legal: s is effectively final
        // (it is a new variable on each iteration)
    }
}

void m10(String[] arr) {
    for (int i = 0; i < arr.length; i++) {
        foo(() -> arr[i]);
        // Illegal: i is not effectively final
        // (it is not final, and is incremented)
    }
}

15.27.3. Тип лямбда-выражения

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

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

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

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

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

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

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

Это означает, что идентичность результата вычисления lambda-выражения (или сериализации и десериализации lambda-выражения) непредсказуема, и поэтому операции, чувствительные к идентичности (такие как проверка равенства ссылок (§15.21.3), блокировка объекта (§14.19) и метод System.identityHashCode) могут давать разные результаты в разных реализациях языка программирования Java или даже при разных вычислениях lambda-выражения в одной реализации.

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

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

  • Где lambda-выражение имеет тип U, для каждого не-static метода-члена m типа U:

    Если функциональный тип U имеет подсигнатуру подписью m, то класс объявляет метод, который переопределяет m. Тело метода выполняет вычисление тела lambda-выражения, если это выражение, или выполняет тело lambda-выражения, если это блок; если ожидается результат, он возвращается из метода.

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

  • Класс не переопределяет никаких других методов целевого функционального интерфейса или других указанных выше типов интерфейсов, хотя он может переопределять методы класса Object.

Эти правила предназначены для предоставления гибкости реализациям языка программирования Java, в том, что:

  • Новый объект не обязательно должен выделяться при каждом вычислении.

  • Объекты, созданные различными lambda-выражениями, не обязательно должны принадлежать различным классам (например, если тела идентичны).

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

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

Если целевой функциональный интерфейс является подтипом java.io.Serializable, результирующий объект автоматически станет экземпляром сериализуемого класса. Преобразование объекта, полученного из lambda-выражения, в сериализуемый объект может иметь дополнительные накладные расходы во время выполнения и последствия для безопасности, поэтому объекты, полученные из lambda-выражений, не обязаны быть сериализуемыми "по умолчанию".

15.28. Константные выражения

ConstantExpression:
Выражение

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

  • Литералы примитивных типов и литералы типа String (§3.10.1, §3.10.2, §3.10.3, §3.10.4, §3.10.5)

  • Приведения к примитивным типам и приведения к типу String (§15.16)

  • Унарные операторы +, -, ~ и ! (но не ++ или --) (§15.15.3, §15.15.4, §15.15.5, §15.15.6)

  • ... (остальные пункты списка)

Константные выражения типа String всегда "интернируются" для совместного использования уникальных экземпляров с помощью метода String.intern.

Константное выражение всегда обрабатывается как FP-строгое (§15.4), даже если оно встречается в контексте, где неконстантное выражение не считалось бы FP-строгим.

Константные выражения используются в качестве меток case в операторах switch (§14.11) и имеют особое значение в контекстах присваивания (§5.2) и инициализации класса или интерфейса (§12.4.2). Они также могут определять возможность оператора while, do или for завершить выполнение нормально (§14.21), и тип условного оператора ? : с числовыми операндами.

Пример 15.28-1. Константные выражения

true
(short)(1*2*3*4*5*6)
Integer.MAX_VALUE / 2
2.0 * Math.PI
"The integer " + Long.MAX_VALUE + " is mighty big."

© Oracle and/or its affiliates. All rights reserved.
Licensed under the Oracle Technology Network License Agreement.

Spec-Zone.ru

Настройки Оффлайн Что нового Помощь О нас
Spec-Zone .ru
спецификации, руководства, описания, API