Spec-Zone.ru › Java Language Specification 7

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

Содержание

15.1. Вычисление, обозначение и результат
15.2. Переменные как значения
15.3. Тип выражения
15.4. Выражения FP-строгой строгости
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.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.2.7. Выведение типов аргументов на основе фактических аргументов
15.12.2.8. Выведение неразрешённых типов аргументов
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.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.26. Операторы присваивания
15.26.1. Оператор простого присваивания =
15.26.2. Операторы сложного присваивания
15.27. Выражение
15.28. Постоянные выражения

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

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

15.1. Оценка, Обозначение и Результат

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

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

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

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

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

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

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

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

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

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

15.2. Переменные как Значения

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

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

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

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

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

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

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

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

15.4. Выражения FP-strict

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

Любое выражение компиляционной константы (§15.28) является FP-strict.

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

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

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

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

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

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

END_OF_DOCUMENT_MARKER

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

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

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

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

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

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

  • Присваивание элементу массива ссылочного типа (§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), выражение создания массива (§15.10), выражение инициализации массива (§10.6) или выражение оператора конкатенации строк (§15.18.1) генерирует исключение OutOfMemoryError, если доступно недостаточно памяти.

  • Выражение создания массива (§15.10) генерирует исключение NegativeArraySizeException, если значение любого выражения измерения меньше нуля.

  • Выражение доступа к полю (§15.11) генерирует исключение NullPointerException, если значение выражения ссылки на объект является null.

  • Выражение вызова метода (§15.12), вызывающее метод экземпляра, генерирует исключение NullPointerException, если целевая ссылка является null.

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

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

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

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

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

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

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

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

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


Основное:
ОсновноеБезСозданиеМассива
ВыражениеСозданиеМассива

ОсновноеБезСозданиеМассива:
Литерал
Тип . class
void . class
this
ИмяКласса . this
( Выражение )
ВыражениеСозданиеЭкземпляраКласса
ОбращениеКПолю
ВызовМетода
ОбращениеКМассиву

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

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

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

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

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

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

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


Литерал:
ЦелочисленныйЛитерал
ДробныйЛитерал
БулевыйЛитерал
СимвольныйЛитерал
СтроковыйЛитерал
НулевойЛитерал

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

  • Тип целочисленного литерала (§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 (§3.10.7) является типом null (§4.1); его значение — нулевая ссылка.

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

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

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

Тип 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>.

Ошибка компиляции, если указанный тип — это переменная типа (§4.4) или параметризованный тип (§4.5) или массив, тип элемента которого — переменная типа или параметризованный тип.

Ошибка компиляции, если указанный тип не обозначает доступный (§6.6) и находящийся в области видимости (§6.3) тип в месте появления литерала класса.

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

15.8.3. this

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

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

Тип this — это класс C, в котором встречается ключевое слово this.

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

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

Пример 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.

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

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

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

Если текущий класс не является вложенным классом класса C или самим классом C, это ошибка времени компиляции.

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

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

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

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

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

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

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

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

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


ClassInstanceCreationExpression:
new TypeArgumentsopt TypeDeclSpecifier TypeArgumentsOrDiamondopt
( ArgumentListopt ) ClassBodyopt
Primary . new TypeArgumentsopt Identifier TypeArgumentsOrDiamondopt
( ArgumentListopt ) ClassBodyopt

TypeArgumentsOrDiamond:
TypeArguments
<>

ArgumentList:
Expression
ArgumentList , Expression

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

  • Если выражение создания экземпляра класса является выражением создания экземпляра класса без указания квалификатора, то TypeDeclSpecifier TypeArgumentsOrDiamondopt должно обозначать тип, T (§4.3, §4.5), в противном случае возникает ошибка компиляции.

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

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

    Если T обозначает интерфейс, то объявляется анонимный непосредственный подкласс Object, реализующий интерфейс, указанный T.

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

    Класс, подлежащий инстанцированию, — это анонимный подкласс.

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

    Ошибка компиляции возникает, если Identifier после маркера new не является простым именем (§6.2) доступного (§6.6) не-final внутреннего класса (§8.1.3), являющегося членом времени компиляции типа Primary.

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

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

    Пусть T — тип, указанный Identifier и любыми аргументами типа. Объявляется анонимный непосредственный подкласс класса, указанного T. Тело подкласса — это ClassBody, указанное в выражении создания экземпляра класса.

    Класс, подлежащий инстанцированию, — это анонимный подкласс.

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

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

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

    Если для опускания аргументов типа после имени класса используется "<>", то ошибка компиляции возникает, если класс не является обобщённым.

    Класс, подлежащий инстанцированию, — это класс, обозначенный TypeDeclSpecifier.

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

    Ошибка компиляции возникает, если Identifier после маркера new не является простым именем (§6.2) доступного (§6.6) не-abstract внутреннего класса (§8.1.3) C, являющегося членом времени компиляции типа Primary.

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

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

    Если для опускания аргументов типа после имени используется "<>", то ошибка компиляции возникает, если C не является обобщённым.

    Класс, подлежащий инстанцированию, — C.

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

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

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

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

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

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

Если C — локальный класс (§14.3), то пусть O — самый внутренний лексически окружающий класс C. Пусть n — целое число такое, что O — n-й лексически окружающий класс класса, в котором появляется выражение создания экземпляра класса. Тогда:

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

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

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

В противном случае, C — внутренний член класса (§8.5), и тогда:

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

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

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

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

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

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

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

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

Если S — локальный класс (§14.3), то пусть O — самый внутренний лексически окружающий класс S. Пусть n — целое число такое, что O — n-й лексически окружающий класс класса, в котором появляется выражение создания экземпляра класса. Тогда:

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

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

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

В противном случае, S — внутренний член класса (§8.5), и тогда:

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

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

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

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

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

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

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

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

Пусть C — класс, который нужно создать. Для создания экземпляра C, i, конструктор C выбирается во время компиляции по следующим правилам.

Сначала определяются фактические аргументы вызова конструктора:

  • Если C — анонимный класс, и непосредственный суперкласс C, S, является внутренним классом, то:

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

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

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

Во-вторых, определяется конструктор C и соответствующий тип результата и throws часть:

  • Если выражение создания экземпляра класса использует "<>" для устранения аргументов типа класса, список методов m1...mn определяется для целей разрешения перегрузки и вывода аргументов типа.

    Пусть c1...cn — конструкторы класса C. Пусть #m — автоматически сгенерированное имя, отличное от всех имён конструкторов и методов в C. Для всех j (1 ≤ j ≤ n), mj определяется относительно cj следующим образом:

    • Вначале определяется подстановка θj для инстанцирования типов в cj.

      Пусть F1...Fp — параметры типа C, и пусть G1...Gq — параметры типа (если таковые имеются) cj. Пусть X1...Xp и Y1...Yq — переменные типа с различными именами, которые не находятся в области видимости в теле C.

      θj — [F1:=X1, ..., Fp:=Xp, G1:=Y1, ..., Gq:=Yq].

    • Модификаторы mj — это те же модификаторы, что у cj.

    • Параметры типа mj — X1...Xp,Y1...Yq. Граница каждого параметра, если таковая имеется, — это θj, применённая к соответствующей границе параметра в C или cj.

    • Тип возвращаемого значения mj — это θj, применённый к C<F1,...,Fp>.

    • Имя mj — #m.

    • Список (возможно пустой) типов аргументов mj — это θj, применённый к типам аргументов cj.

    • Список (возможно пустой) типов выбрасываемых исключений mj — это θj, применённый к типам исключений, выбрасываемых cj.

    • Тело mj не имеет значения.

    Для выбора конструктора мы временно считаем m1...mn членами C. Затем выбирается один из m1...mn, как определяется выражениями аргументов выражения создания экземпляра класса, используя процесс, описанный в §15.12.2.

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

    В противном случае, где mj — выбранный метод, cj — выбранный конструктор. Тип результата и throws часть cj совпадают с типом возвращаемого значения и throws частью, определёнными для mj (§15.12.2.6).

  • В противном случае выражение создания экземпляра класса не использует "<>" для устранения аргументов типа класса.

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

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

    В противном случае, тип результата — T.

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

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

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


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

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

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

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

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

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

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

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

    Фактические аргументы выражения создания экземпляра класса используются для определения конструктора S, используя те же правила, что и для вызовов методов (§15.12).

    Тип каждого формального параметра анонимного конструктора должен быть идентичен соответствующему формальному параметру S.

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

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

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

    Фактические аргументы выражения создания экземпляра класса используются для определения конструктора S, используя те же правила, что и для вызовов методов (§15.12).

    Тип каждого формального параметра анонимного конструктора должен быть идентичен соответствующему формальному параметру S.

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

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

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

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

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


ArrayCreationExpression:
new PrimitiveType DimExprs Dimsopt
new ClassOrInterfaceType DimExprs Dimsopt
new PrimitiveType Dims ArrayInitializer
new ClassOrInterfaceType Dims ArrayInitializer

DimExprs:
DimExpr
DimExprs DimExpr

DimExpr:
[ Expression ]

Dims:
[ ]
Dims [ ]

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

Если ClassOrInterfaceType не обозначает реализуемый тип (§4.7), то это ошибка времени компиляции. В противном случае ClassOrInterfaceType может именовать любой именованный тип ссылки, даже абстрактный тип класса (§8.1.1.1) или тип интерфейса (§9).

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

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

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

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

Например, тип выражения создания:

new double[3][3][]

является:

double[][][]

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

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

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

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

    Значение инициализатора массива становится значением выражения создания массива.

  • В противном случае нет инициализатора массива, и:

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

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

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

    • Затем, если появляется единственное DimExpr, создаётся одномерный массив заданной длины, и каждый компонент массива инициализируется своим значением по умолчанию (§4.12.5).

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

      Многомерный массив может не иметь массивов с одинаковой длиной на каждом уровне.

Пример 15.10.1-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.1-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.1-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.11. Выражения доступа к полям

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


FieldAccess:
Primary . Identifier
super . Identifier
ClassName . super . Identifier

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

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

15.11.1. Доступ к полю с помощью первичного выражения

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

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

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

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

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

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

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

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

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

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

  • Если поле не static:

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

    • Если значение выражения Первичное является null, то генерируется NullPointerException.

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

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

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

Пример 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. Переменная получателя не имеет значения для доступа к статическому полю

Следующая программа демонстрирует, что на ссылку 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 " демонстрирует, что выражение Первичное действительно полностью вычисляется во время выполнения, несмотря на то, что только его тип, а не значение, используется для определения доступа к полю (поскольку поле mountain является static).


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

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

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

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

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

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

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

Если выражение доступа к полю T.super.name появляется внутри класса C, а непосредственный предок класса, обозначенного T, — это класс, полное имя которого S, то T.super.name обрабатывается точно так же, как если бы это было выражение this.name в теле класса S. Таким образом, он может получить доступ к полю name, видимому в классе S, даже если это поле скрыто объявлением поля name в классе 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

Обратите внимание, что super.x не указано в терминах приведения типов из-за трудностей с доступом к protected членам предка.


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

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


MethodInvocation:
MethodName ( ArgumentListopt )
Primary . NonWildTypeArgumentsopt Identifier ( ArgumentListopt )
super . NonWildTypeArgumentsopt Identifier ( ArgumentListopt )
ClassName . super . NonWildTypeArgumentsopt Identifier ( ArgumentListopt )
TypeName . NonWildTypeArguments Identifier ( ArgumentListopt )

Определение ArgumentList из §15.9 приведено здесь для удобства:


ArgumentList:
Expression
ArgumentList , Expression

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

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

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

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

  • Если форма — ИмяМетода, то существуют три подслучая:

    • Если это простое имя, то есть только Идентификатор, то имя метода — Идентификатор.

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

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

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

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

    • Если это квалифицированное имя вида ТипИмя . Идентификатор, то имя метода — Идентификатор, а класс для поиска — класс, названный ТипИмя.

      Если ТипИмя — имя интерфейса, а не класса, то возникает ошибка компиляции, так как эта форма может вызывать только static методы, а интерфейсы не имеют static методов.

    • Во всех остальных случаях квалифицированное имя имеет вид ИмяПоля . Идентификатор.

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

  • Если форма — Primary . NonWildTypeArgumentsopt Identifier, то имя метода — Identifier.

    Пусть T — тип выражения Primary. Класс или интерфейс для поиска — T, если T — тип класса или интерфейса, или верхняя граница T, если T — переменная типа.

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

  • Если форма — super . NonWildTypeArgumentsopt Identifier, то имя метода — Identifier, а класс для поиска — суперкласс класса, объявление которого содержит вызов метода.

    Ошибка компиляции, если T — класс Object или T — интерфейс.

  • ...

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

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

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

Метод применим, если он применим по подтипированию (§15.12.2.2), применим с помощью преобразования вызова метода (§15.12.2.3) или это применим метод с переменной арностью (§15.12.2.4).

Процесс определения применимости начинается с определения потенциально применимых методов (§15.12.2.1).

Остальная часть процесса разделена на три фазы, чтобы обеспечить совместимость с версиями языка программирования Java до Java SE 5.0. Фазы следующие:

  1. Первая фаза (§15.12.2.2) выполняет разрешение перегрузки без разрешения преобразования с помощью автоупаковки/распаковки или использования вызова метода с переменной арностью. Если во время этой фазы применимый метод не найден, обработка продолжается во вторую фазу.

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

  2. Вторая фаза (§15.12.2.3) выполняет разрешение перегрузки, позволяя автоупаковку и распаковку, но по-прежнему запрещает использование вызова метода с переменной арностью. Если во время этой фазы применимый метод не найден, обработка продолжается в третью фазу.

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

  3. Третья фаза (§15.12.2.4) позволяет объединить перегрузку с методами переменной арности, автоупаковкой и распаковкой.

Определение того, применим ли метод, в случае с генерическими методами (§8.4.4), потребует определения аргументов типа. Аргументы типа могут передаваться явно или неявно. Если они передаются неявно, они должны быть выведены (§15.12.2.7) из типов выражений аргументов.

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

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

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

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

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

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

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

Ещё один пример:

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

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

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

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

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

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

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

Если добавить третью декларацию test:

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

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


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

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

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


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

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

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

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

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

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

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

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

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

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

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

Результат вывода:

cp: (3,3,red)

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

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

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


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

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

cp: (3,3,red)

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

cp: (3,3,green)

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


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

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


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

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

Кроме того, если вызов метода содержит, перед левой скобкой, ИмяМетода в форме Идентификатор, то процесс поиска также исследует все методы-члены, которые (а) импортированы объявлениями single-static-import (§7.5.3) и объявлениями static-import-on-demand (§7.5.4) в модуле компиляции (§7.3), в котором происходит вызов метода, и (б) не затеняются (§6.4.1) в том месте, где появляется вызов метода, чтобы определить, являются ли они потенциально применимыми.

Метод-член является потенциально применимым к вызову метода, если и только если все следующие условия выполняются:

  • Имя члена идентично имени метода в вызове метода.

  • Член доступен (§6.6) для класса или интерфейса, в котором появляется вызов метода.

    Доступность метода-члена при вызове метода зависит от модификатора доступа (public, нет, protected или private) в объявлении члена и от того, где появляется вызов метода.

  • Если метод-член является методом с переменным числом параметров (arity) n, количество параметров в вызове метода должно быть больше или равно n-1.

  • Если метод-член является методом с фиксированным числом параметров (arity) n, количество параметров в вызове метода должно быть равно n.

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

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

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

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

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

Пусть m - потенциально применимый метод (§15.12.2.1), пусть e1, ..., en - выражения фактических аргументов вызова метода, и пусть Ai - тип ei (1 ≤ i ≤ n). Тогда:

  • Если m - родовой метод, то пусть F1 ... Fn - типы формальных параметров m, и пусть R1 ... Rp (p ≥ 1) - параметры типа m, и пусть Bl - объявленная граница Rl (1 ≤ l ≤ p). Тогда:

    • Если вызов метода не предоставляет явные аргументы типа, то пусть U1 ... Up - аргументы типа, выведенные (§15.12.2.7) для этого вызова m, используя набор начальных ограничений, состоящий из ограничений Ai << Fi (1 ≤ i ≤ n) для каждого выражения фактического аргумента ei, тип которого является ссылочным типом.

    • В противном случае, пусть U1 ... Up - явные аргументы типа, заданные в вызове метода.

    Тогда пусть Si = Fi[R1=U1,...,Rp=Up] (1 ≤ i ≤ n) - типы, выведенные для формальных параметров m.

  • В противном случае, пусть S1 ... Sn - типы формальных параметров m.

Метод m является применимым путем подтипирования, если и только если выполняются оба следующих условия:

  • Для 1 ≤ i ≤ n, либо:

    • Ai <: Si (§4.10), или

    • Ai преобразуется в некоторый тип Ci путем безусловного преобразования (§5.1.9), и Ci <: Si.

  • Если m - родовой метод, как описано выше, то Ul <: Bl[R1=U1,...,Rp=Up] (1 ≤ l ≤ p).

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

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

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

Пусть m - потенциально применимый метод (§15.12.2.1), пусть e1, ..., en - выражения фактических аргументов вызова метода, и пусть Ai - тип ei (1 ≤ i ≤ n). Тогда:

  • Если m - родовой метод, то пусть F1 ... Fn - типы формальных параметров m, и пусть R1 ... Rp (p ≥ 1) - параметры типа m, и пусть Bl - объявленная граница Rl (1 ≤ l ≤ p). Тогда:

    • Если вызов метода не предоставляет явные аргументы типа, то пусть U1 ... Up - аргументы типа, выведенные (§15.12.2.7) для этого вызова m, используя набор начальных ограничений, состоящий из ограничений Ai << Fi (1 ≤ i ≤ n).

    • В противном случае, пусть U1 ... Up - явные аргументы типа, заданные в вызове метода.

    Тогда пусть Si = Fi[R1=U1,...,Rp=Up] (1 ≤ i ≤ n) - типы, выведенные для формальных параметров m.

  • В противном случае, пусть S1 ... Sn - типы формальных параметров m.

Метод m является применимым путем преобразования вызова метода, если и только если выполняются оба следующих условия:

  • Для 1 ≤ i ≤ n, тип ei, Ai, может быть преобразован путем преобразования вызова метода (§5.3) в Si.

  • Если m - родовой метод, как описано выше, то Ul <: Bl[R1=U1,...,Rp=Up] (1 ≤ l ≤ p).

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

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

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

Пусть m — потенциально применимый метод (§15.12.2.1) с переменным числом аргументов, пусть e1, ..., ek — фактические выражения аргументов вызова метода, и пусть Ai — тип ei (1 ≤ i ≤ k). Тогда:

  • Если m — это метод с дженериками, то пусть F1 ... Fn (1 ≤ n ≤ k+1) — типы формальных параметров m, где Fn=T[] для некоторого типа T, и пусть R1 ... Rp (p ≥ 1) — параметры типа m, и пусть Bl — объявленная граница Rl (1 ≤ l ≤ p). Тогда:

    • Если вызов метода не предоставляет явных аргументов типа, то пусть U1 ... Up — аргументы типа, выведенные (§15.12.2.7) для этого вызова m, используя набор начальных ограничений, состоящих из ограничений Ai << Fi (1 ≤ i < n) и ограничений Aj << T (n ≤ j ≤ k).

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

    Тогда пусть Si = Fi[R1=U1,...,Rp=Up] (1 ≤ i ≤ n) — типы, выведенные для формальных параметров m.

  • В противном случае, пусть S1 ... Sn (где n ≤ k+1) — типы формальных параметров m.

Метод m является применимым методом с переменным числом аргументов тогда и только тогда, когда выполняются все следующие условия:

  • Для 1 ≤ i < n, тип ei, Ai, может быть преобразован с помощью преобразования вызова метода к Si.

  • Если k ≥ n, то для n ≤ i ≤ k, тип ei, Ai, может быть преобразован с помощью преобразования вызова метода к компоненту типа Sn.

  • Если k != n, или если k = n и An не может быть преобразован с помощью преобразования вызова метода к Sn[], то тип, являющийся стёртой (§4.6) Sn, доступен в точке вызова.

  • Если m — это метод с дженериками, как описано выше, то Ul <: Bl[R1=U1...,Rp=Up] (1 ≤ l ≤ p).

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

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

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

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

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

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

  • Объявленные типы параметров первого метода члена равны T1, ..., Tn.

  • Объявленные типы параметров другого метода равны U1, ..., Un.

  • Если второй метод является обобщенным, то пусть R1 ... Rp (p ≥ 1) будут его параметрами типа, пусть Bl будет объявленной границей Rl (1 ≤ l ≤ p), пусть A1 ... Ap будут аргументами типа, выведенными (§15.12.2.7) для этого вызова при начальных ограничениях Ti << Ui (1 ≤ i ≤ n), и пусть Si = Ui[R1=A1,...,Rp=Ap] (1 ≤ i ≤ n).

    В противном случае, пусть Si = Ui (1 ≤ i ≤ n).

  • Для всех j от 1 до n, Tj <: Sj.

  • Если второй метод является обобщенным методом, как описано выше, то Al <: Bl[R1=A1,...,Rp=Ap] (1 ≤ l ≤ p).

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

  1. Один метод члена имеет n параметров, а другой — k параметров, где n ≥ k, и:

    • Типы параметров первого метода члена равны T1, ..., Tn-1, Tn[].

    • Типы параметров другого метода равны U1, ..., Uk-1, Uk[].

    • Если второй метод является обобщенным, то пусть R1 ... Rp (p ≥ 1) будут его параметрами типа, пусть Bl будет объявленной границей Rl (1 ≤ l ≤ p), пусть A1 ... Ap будут аргументами типа, выведенными (§15.12.2.7) для этого вызова при начальных ограничениях Ti << Ui (1 ≤ i ≤ k-1) и Ti << Uk (k ≤ i ≤ n), и пусть Si = Ui[R1=A1,...,Rp=Ap] (1 ≤ i ≤ k).

      В противном случае, пусть Si = Ui (1 ≤ i ≤ k).

    • Для всех j от 1 до k-1, Tj <: Sj, и,

    • Для всех j от k до n, Tj <: Sk, и,

    • Если второй метод является обобщенным методом, как описано выше, то Al <: Bl[R1=A1,...,Rp=Ap] (1 ≤ l ≤ p).

  2. Один метод члена имеет k параметров, а другой — n параметров, где n ≥ k, и:

    • Типы параметров первого метода равны U1, ..., Uk-1, Uk[].

    • Типы параметров другого метода равны T1, ..., Tn-1, Tn[].

    • Если второй метод является обобщенным, то пусть R1 ... Rp (p ≥ 1) будут его параметрами типа, пусть Bl будет объявленной границей Rl (1 ≤ l ≤ p), пусть A1 ... Ap будут аргументами типа, выведенными (§15.12.2.7) для этого вызова при начальных ограничениях Ui << Ti (1 ≤ i ≤ k-1) и Uk << Ti (k ≤ i ≤ n), и пусть Si = Ti[R1=A1,...,Rp=Ap] (1 ≤ i ≤ n).

      В противном случае, пусть Si = Ti (1 ≤ i ≤ n).

    • Для всех j от 1 до k-1, Uj <: Sj, и,

    • Для всех j от k до n, Uk <: Sj, и,

    • Если второй метод является обобщенным методом, как описано выше, то Al <: Bl[R1=A1,...,Rp=Ap] (1 ≤ l ≤ p).

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

Метод m1 строго более специфичен, чем другой метод m2, если и только если m1 более специфичен, чем m2, и m2 не более специфичен, чем m1.

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

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

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

  • Если все максимально специфичные методы имеют эквивалентные в плане переопределения (§8.4.2) сигнатуры, то:

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

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

      Однако наиболее специфичный метод считается бросающим проверяемое исключение тогда и только тогда, когда это исключение или его стёртая форма объявлены в throws разделах каждого из максимально специфичных методов.

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

15.12.2.6. Тип результата и типы исключений метода

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

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

  • В противном случае, если для применения метода было необходимо неявное преобразование, тогда типом результата является стёртый тип (§4.6) объявленного типа возвращаемого значения метода.

  • В противном случае, если выбранный метод является дженериком, то для 1 ≤ i ≤ n, пусть Fi — формальные параметры типа метода, Ai — фактические типы аргументов, выведенные для вызова метода, а R — тип возвращаемого значения выбранного метода.

    Тип результата получается путем применения преобразования захвата (§5.1.10) к R[F1=A1,...,Fn=An].

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

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

  • Если для применения метода было необходимо неявное преобразование, то throws раздел состоит из стёртых форм (§4.6) типов в объявленном throws разделе метода.

  • В противном случае, если вызываемый метод является дженериком, то для 1 ≤ i ≤ n, пусть Fi — параметры типа метода, Ai — аргументы типа, выведенные для вызова метода, а Ej (1 ≤ j ≤ m) — типы исключений, объявленные в throws разделе вызываемого метода.

    throws раздел состоит из типов Ej[F1=A1,...,Fn=An].

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

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

15.12.2.7. Вывод типов аргументов на основе фактических аргументов

В этом разделе мы описываем процесс вывода типов аргументов для вызовов методов и конструкторов. Этот процесс вызывается как подпрограмма при проверке применимости метода (или конструктора) (§15.12.2.2, §15.12.2.3, §15.12.2.4).

Процесс вывода типов по своей природе сложен. Поэтому полезно дать общее представление о процессе перед углублением в подробное описание.

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

На основе этих начальных ограничений можно вывести набор ограничений на подтипирование и/или равенство для типов параметров метода или конструктора.

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

Ограничение подтипирования T :> X подразумевает, что решением является один из супертипов X. Учитывая несколько таких ограничений для T, мы можем пересечь наборы супертипов, подразумеваемые каждым из ограничений, так как параметр типа должен быть членом всех из них. Затем мы можем выбрать наиболее специфичный тип, который входит в пересечение.

Вычисление пересечения сложнее, чем можно подумать на первый взгляд. Если параметр типа ограничен быть супертипом двух различных вызовов обобщённого типа, например List<Object> и List<String>, то простое пересечение может дать Object. Однако более сложный анализ даёт набор, содержащий List<?>. Аналогично, если параметр типа T ограничен быть супертипом двух несвязанных интерфейсов I и J, мы можем вывести, что T должен быть Object, или мы можем получить более узкую границу I & J. Эти вопросы обсуждаются более подробно позже в этом разделе.

В этом разделе используются следующие соглашения по обозначениям:

  • Выражения типов представлены буквами A, F, U, V и W. Буква A используется только для обозначения типа фактического аргумента, а F — только для обозначения типа формального параметра.

  • Параметры типа представлены буквами S и T

  • Аргументы параметризованных типов представлены буквами X и Y.

  • Объявления обобщенных типов представлены буквами G и H.

Вывод начинается с набора начальных ограничений вида A << F, A = F или A >> F, где U << V указывает, что тип U преобразуется к типу V с помощью преобразования для вызова метода (§5.3), а U >> V указывает, что тип V преобразуется к типу U с помощью преобразования для вызова метода.

В более простом мире ограничения могли бы быть вида A <: F — просто требование, чтобы типы фактических аргументов были подтипами формальных. Однако реальность более сложная. Как упоминалось ранее, проверка применимости метода состоит из до трёх фаз; это необходимо по соображениям совместимости. Каждая фаза накладывает немного разные ограничения. Если метод применим по подтипированию (§15.12.2.2), ограничения действительно являются ограничениями подтипирования. Если метод применим с помощью преобразования для вызова метода (§15.12.2.3), ограничения подразумевают, что фактический тип преобразуется к формальному типу с помощью преобразования для вызова метода. Ситуация аналогична для третьей фазы (§15.12.2.4), но точная форма ограничений отличается из-за переменного числа аргументов.

Стоит отметить, что ограничение вида A = F никогда не входит в начальные ограничения. Однако оно может возникнуть по мере рекурсии алгоритма. Мы видим это в примере ниже, когда ограничение A << F связывает два параметризованных типа, как в G<V> << G<U>.

Ограничение вида A >> F также возникает при рекурсии алгоритма из-за контравариантных правил подтипирования, связанных с ограниченными снизу дикими картами (таких, как G<? super X>).

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

Затем эти ограничения сводятся к набору более простых ограничений вида T :> X, T = X или T <: X, где T — параметр типа метода. Это сокращение достигается с помощью описанной ниже процедуры.

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

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

Учитывая ограничение вида A << F, A = F или A >> F:

Если F не включает параметр типа Tj, то никакого ограничения для Tj не подразумевается.

В противном случае F включает параметр типа Tj, и необходимо рассмотреть четыре случая.

1. Если A является типом null, то никакого ограничения для Tj не подразумевается.

2. В противном случае, если ограничение имеет вид A << F:

  • Если A — примитивный тип, то A преобразуется в ссылочный тип U посредством преобразования «упаковки», и этот алгоритм применяется рекурсивно к ограничению U << F.

  • В противном случае, если F = Tj, то ограничение Tj :> A подразумевается.

  • Если F = U[], где тип U включает Tj, то если A — массив типа V[] или переменная типа с верхним ограничением, которое является массивом типа V[], где V — ссылочный тип, этот алгоритм применяется рекурсивно к ограничению V << U.

    Это следует из ковариантного отношения подтипов среди типов массивов. Ограничение A << F в этом случае означает, что A << U[]. Таким образом, A обязательно является типом массива V[] или переменной типа, верхнее ограничение которой является типом массива V[] — в противном случае отношение A << U[] никогда не могло бы быть истинным. Следовательно, V[] << U[]. Поскольку подтипизация массивов ковариантна, должно выполняться V << U.

  • Если F имеет вид G<..., Yk-1, U, Yk+1, ...>, где U — выражение типа, которое включает Tj, то если A имеет надтип вида G<..., Xk-1, V, Xk+1, ...>, где V — выражение типа, этот алгоритм применяется рекурсивно к ограничению V = U.

    Для простоты предположим, что G принимает один тип аргумента. Для того, чтобы вызов метода, который рассматривается, был применим, должно выполняться A является подтипом некоторого вызова G. В противном случае A << F никогда не было бы истинным.

    Другими словами, A << F, где F = G<U>, подразумевает, что A << G<V> для некоторого V. Теперь, поскольку U — выражение типа (и, следовательно, U не является аргументом типа-дикой картой), должно выполняться U = V в силу нековариантности обычных параметризованных вызовов типов.

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

  • Если F имеет вид G<..., Yk-1, ? extends U, Yk+1, ...>, где U включает Tj, то если A имеет надтип, который является одним из:

    • G<..., Xk-1, V, Xk+1, ...>, где V — выражение типа. Тогда этот алгоритм применяется рекурсивно к ограничению V << U.

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

      A << F в этом случае означает A << G<? extends U>. Как и выше, должно выполняться A является подтипом некоторого вызова G. Однако, A теперь может быть подтипом либо G<V>, либо G<? extends V>, или G<? super V>. Мы исследуем эти случаи по очереди. Первый вариант описан (обобщён на несколько аргументов) подпунктом непосредственно над текущим. Таким образом, у нас A = G<V> << G<? extends U>. Правила подтипизации для диких карт подразумевают, что V << U.

    • G<..., Xk-1, ? extends V, Xk+1, ...>. Тогда этот алгоритм применяется рекурсивно к ограничению V << U.

      Расширяя анализ выше, имеем A = G<? extends V> << G<? extends U>. Правила подтипизации для диких карт снова подразумевают, что V << U.

    • В противном случае, на Tj нет ограничения.

      Здесь имеем A = G<? super V> << G<? extends U>. В общем случае мы не можем сделать никакого вывода в этом случае. Однако это не обязательно ошибка. Может оказаться, что U в конечном итоге будет выведено как Object, в этом случае вызов может быть действительным. Поэтому мы просто воздерживаемся от наложения любого ограничения на U.

  • Если F имеет вид G<..., Yk-1, ? super U, Yk+1, ...>, где U включает Tj, то если A имеет супертип, являющийся одним из следующих:

    • G<..., Xk-1, V, Xk+1, ...>. Тогда этот алгоритм применяется рекурсивно к ограничению V >> U.

      Как обычно, рассматривается только случай, когда G имеет один тип аргумента.

      A << F в данном случае означает A << G<? super U>. Как и выше, A должен быть подтипом какого-либо вызова G. A теперь может быть подтипом либо G<V>, либо G<? extends V>, или G<? super V>. Мы рассматриваем эти случаи по очереди. Первый вариант описан (обобщен для нескольких аргументов) в подпункте непосредственно выше. Таким образом, у нас есть A = G<V> << G<? super U>. Правила подтипизации для диких карт подразумевают, что V >> U.

    • G<..., Xk-1, ? super V, Xk+1, ...>. Тогда этот алгоритм применяется рекурсивно к ограничению V >> U.

      У нас есть A = G<? super V> << G<? super U>. Правила подтипизации для ограниченных снизу диких карт вновь подразумевают, что V >> U.

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

      Здесь у нас A = G<? extends V> << G<? super U>. В общем случае, мы ничего не можем заключить в этом случае. Однако это не обязательно ошибка. Возможно, U в конечном итоге будет выведено как нулевой тип, в этом случае вызов действительно может быть допустимым. Поэтому мы просто воздерживаемся от наложения каких-либо ограничений на U.

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

3. В противном случае, если ограничение имеет вид A = F:

  • Если F = Tj, то подразумевается ограничение Tj = A.

  • Если F = U[], где тип U включает Tj, то если A является типом массива V[] или переменной типа с верхним пределом, являющимся типом массива V[], где V — тип ссылки, этот алгоритм применяется рекурсивно к ограничению V = U.

    Если типы массивов U[] и V[] одинаковы, их составные типы должны быть одинаковы.

  • Если F имеет вид G<..., Yk-1, U, Yk+1, ...>, где U — выражение типа, включающее Tj, то если A имеет вид G<..., Xk-1, V, Xk+1,...>, где V — выражение типа, этот алгоритм применяется рекурсивно к ограничению V = U.

  • ...

4. В противном случае, если ограничение имеет вид A >> F:

  • Если F = Tj, то ограничение Tj <: A подразумевается.

    Мы не используем такие ограничения в основном теле алгоритма вывода. Однако они используются в §15.12.2.8.

  • Если F = U[], где тип U включает Tj, то если A является массивом типа V[] или переменной типа с верхним пределом, являющимся типом массива V[], где V — ссылка на тип, этот алгоритм применяется рекурсивно к ограничению V >> U. В противном случае на Tj не накладывается ограничение.

    Это следует из ковариантного отношения подтипа между типами массивов. Ограничение A >> F в этом случае означает, что A >> U[]. Следовательно, A обязательно является массивом типа V[] или переменной типа, верхний предел которой — массив типа V[]; в противном случае отношение A >> U[] не могло бы выполняться. Следует, что V[] >> U[]. Поскольку подтипизация массивов ковариантна, то должно быть выполнено V >> U.

  • Если F имеет вид G<..., Yk-1, U, Yk+1, ...>, где U — выражение типа, которое включает Tj, то:

    • Если A является экземпляром негенерического типа, то на Tj не накладывается ограничение.

      В этом случае (снова ограничивая анализ случаем с одним параметром), у нас есть ограничение A >> F = G<U>. A должен быть надтипом обобщённого типа G. Однако, поскольку A не является параметризованным типом, он никак не может зависеть от аргумента типа U. Это надтип G<X> для каждого X, являющегося допустимым аргументом типа для G. Из A нельзя получить осмысленное ограничение для U.

    • Если A — вызов объявления обобщенного типа H, где H — это либо G, либо суперкласс или суперинтерфейс G, то:

      • Если H ≠ G, то пусть S1, ..., Sn — параметры типа G, и пусть H<U1, ..., Ul> — уникальный вызов H, который является супертипом G<S1, ..., Sn>, и пусть V = H<U1, ..., Ul>[Sk=U]. Тогда, если V :> F, этот алгоритм применяется рекурсивно к ограничению A >> V.

        Наша цель здесь — упростить отношение между A и F. Мы стремимся рекурсивно вызвать алгоритм на более простом случае, где известно, что тип аргумента является вызовом того же обобщенного объявления типа, что и формальный.

        Рассмотрим случай, когда у H и G только по одному аргументу типа. Так как у нас есть ограничение A = H<X> >> F = G<U>, где H отличается от G, должно быть так, что H — некоторый надкласс или надинтерфейс G. Должен быть (не дикий) вызов H, являющийся супертипом F = G<U>. Назовём этот вызов V.

        Если мы заменим F на V в ограничении, мы добьёмся цели связать два вызова одного и того же обобщенного типа (в данном случае H).

        Как мы вычислим V? Объявление G должно вводить параметр типа S, и должен быть некоторый (не дикий) вызов H, H<U1>, который является супертипом G<S>. Подстановка выражения типа U вместо S даст (не дикий) вызов H, H<U1>[S=U], который является супертипом G<U>. Например, в самом простом случае U1 может быть S, в этом случае у нас G<S> <: H<S>, и G<U> <: H<U> = H<S>[S=U] = V.

        Может случиться, что H<U1> независимо от S — то есть, S вообще не встречается в U1. Однако, описанная выше подстановка всё равно верна — в этой ситуации V = H<U1>[S=U] = H<U1>. Кроме того, в этой ситуации G<T> <: H<U1> для любого T, и в частности G<U> <: H<U1> = V.

        Независимо от того, зависит ли U1 от S, мы определили тип V, вызов H, который является супертипом G<U>. Теперь мы можем рекурсивно вызвать алгоритм на ограничении H<X> = A >> V = H<U1>[S=U]. Тогда мы сможем связать аргументы типов обоих вызовов H и извлечь соответствующие ограничения из них.

      • В противном случае, если A имеет вид G<..., Xk-1, W, Xk+1, ...>, где W — выражение типа, этот алгоритм применяется рекурсивно к ограничению W = U.

        У нас A = G<W> >> F = G<U> для некоторого выражения типа W. Так как W — выражение типа (а не дикий аргумент типа), то W = U по инвариантности параметризованных типов.

      • В противном случае, если A имеет вид G<..., Xk-1, ? extends W, Xk+1, ...>, этот алгоритм применяется рекурсивно к ограничению W >> U.

        У нас A = G<? extends W> >> F = G<U> для некоторого выражения типа W. Должно быть W >> U по правилам подтипизации для диких типов.

      • В противном случае, если A имеет вид G<..., Xk-1, ? super W, Xk+1, ...>, этот алгоритм применяется рекурсивно к ограничению W << U.

        У нас A = G<? super W> >> F = G<U> для некоторого выражения типа W. Должно быть W << U по правилам подтипизации для диких типов.

      • В противном случае ограничение на Tj не подразумевается.

  • Если F имеет вид G<..., Yk-1, ? extends U, Yk+1, ...>, где U — выражение типа, которое включает Tj, то:

    • Если A является экземпляром негенерического типа, то на Tj нет ограничений.

      Ещё раз ограничивая анализ случаем с одним аргументом, мы имеем ограничение A >> F = G<? extends U>. A должен быть надтипом обобщённого типа G. Однако, так как A не является параметризованным типом, он не может зависеть от U каким-либо образом. Он является надтипом типа G<? extends X> для каждого X, такого что ? extends X является допустимым аргументом типа для G. Никаких осмысленных ограничений на U из A получить нельзя.

    • Если A является вызовом объявления генерического типа H, где H — либо G, либо суперкласс или суперинтерфейс G, то:

      • Если H ≠ G, то пусть S1, ..., Sn — параметры типа G, и пусть H<U1, ..., Ul> — единственный вызов H, который является надтипом G<S1, ..., Sn>, и пусть V = H<? extends U1, ..., ? extends Ul>[Sk=U]. Тогда этот алгоритм применяется рекурсивно к ограничению A >> V.

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

        Предположим, что и H, и G имеют только один аргумент типа. Поскольку у нас есть ограничение A = H<X> >> F = G<? extends U>, где H отличается от G, то H должен быть каким-либо надклассом или надинтерфейсом G. Должен быть вызов H<Y>, такой что H<X> >> H<Y>, который мы можем использовать вместо F = G<? extends U>.

        Как мы вычисляем H<Y>? Как и прежде, обратите внимание, что объявление G должно ввести параметр типа S, и должен быть некоторый (не дикий) вызов H, H<U1>, который является надтипом G<S>. Однако подстановка ? extends U для S обычно не допустима. Чтобы увидеть это, предположим U1 = T[].

        Вместо этого мы получаем вызов H, H<? extends U1>[S=U]. В самом простом случае U1 может быть S, в этом случае мы имеем G<S> <: H<S>, и G<? extends U> <: H<? extends U> = H<? extends S>[S=U] = V.

      • В противном случае, если A имеет вид G<..., Xk-1, ? extends W, Xk+1, ...>, этот алгоритм применяется рекурсивно к ограничению W >> U.

        Мы имеем A = G<? extends W> >> F = G<? extends U> для некоторого выражения типа W. По правилам субординации для подстановок должно выполняться W >> U.

      • В противном случае, на Tj нет ограничений.

  • Если F имеет вид G<..., Yk-1, ? super U, Yk+1, ...>, где U — выражение типа, которое включает Tj, то A может быть:

    • Если A является экземпляром необобщённого типа, то никакие ограничения на Tj не накладываются.

      Ограничивая анализ случаем с одним параметром, получаем ограничение A >> F = G<? super U>. A должен быть супертипом обобщённого типа G. Однако, поскольку A не является параметризованным типом, он не может зависеть от U каким-либо образом. Он является супертипом типа G<? super X> для каждого X, такого что ? super X — допустимый аргумент типа для G. Из A нельзя вывести осмысленное ограничение на U.

    • Если A — вызов объявления обобщённого типа H, где H — либо G, либо его суперкласс или суперинтерфейс, то:

      • Если H ≠ G, то пусть S1, ..., Sn — параметры типа G, и пусть H<U1, ..., Ul> — единственный вызов H, который является супертипом G<S1, ..., Sn>, и пусть V = H<? super U1, ..., ? super Ul>[Sk=U]. Тогда этот алгоритм применяется рекурсивно к ограничению A >> V.

        Здесь подход аналогичен случаю, когда A = G<? extends U>. В нашем примере это даст вызов H<? super U1>[S=U].

      • В противном случае, если A имеет вид G<..., Xk-1, ? super W, ..., Xk+1, ...>, этот алгоритм применяется рекурсивно к ограничению W << U.

        У нас есть A = G<? super W> >> F = G<? super U> для некоторого выражения типа W. Должно выполняться W << U в соответствии с правилами подтипизации для типов с подстановкой.

      • В противном случае, никаких ограничений на Tj не накладывается.

Этот процесс завершает определение ограничений на параметры типа метода.

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

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

Далее, для каждой переменной типа Tj (1 ≤ j ≤ n) разрешаются наложенные ограничения равенства следующим образом.

Для каждого наложенного ограничения равенства Tj = U или U = Tj:

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

  • В противном случае, если U — Tj, то это ограничение не несёт информации и может быть отброшено.

  • В противном случае, ограничение имеет вид Tj = Tk для j ≠ k. Тогда все ограничения, связанные с Tj, переписываются таким образом, что Tj заменяется на Tk, и обработка продолжается со следующей переменной типа.

Затем для каждой оставшейся переменной типа Tj рассматриваются ограничения Tj :> U. Учитывая, что эти ограничения — Tj :> U1 ... Tj :> Uk, тип Tj выводится как lub(U1 ... Uk), вычисляемый следующим образом:

Для типа U мы пишем ST(U) для множества супертипов U и определяем множество супертипов U с учётом стирания:

EST(U) = { V | W в ST(U) и V = |W| } где |W| — стирание (§4.6) W.

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

Например, если T :> List<String> и T :> List<Object>, просто пересечение множеств ST(List<String>) = { List<String>, Collection<String>, Object } и ST(List<Object>) = { List<Object>, Collection<Object>, Object } дало бы множество { Object }, и мы потеряли бы отслеживание того факта, что T безопасно может быть предположено List.

В отличие от этого, пересечение EST(List<String>) = { List, Collection, Object } и EST(List<Object>) = { List, Collection, Object } даёт { List, Collection, Object }, что в конечном итоге позволит нам вывести T = List<?>, как описано ниже.

Множество кандидатов с учётом стирания для параметра типа Tj, EC, — это пересечение всех множеств EST(U) для каждого U в U1 ... Uk.

Минимальное множество кандидатов с учётом стирания для Tj — это:

MEC = { V | V в EC, и для всех W ≠ V в EC, не выполняется W <: V }

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

В нашем примере было EC = { List, Collection, Object }, а теперь MEC = { List }.

Следующим шагом будет восстановление аргументов типа для выведенных типов.

Для любого элемента G из MEC, который является объявлением обобщенного типа, определим соответствующие вызовы G, Inv(G), следующим образом:

Inv(G) = { V | 1 ≤ i ≤ k: V в ST(Ui), V = G<...> }

В нашем примере единственным обобщенным элементом MEC является List, и Inv(List) = { List<String>, List<Object> }. Теперь мы будем искать аргумент типа для List, содержащий (§4.5.1) как String, так и Object.

Это делается с помощью операции наименьшего содержащего вызова (lci), определенной ниже. Первая строка определяет lci() для множества, например, Inv(List), как операцию над списком элементов множества. Следующая строка определяет операцию над такими списками как попарное сокращение элементов списка. Третья строка определяет lci() для пар параметризованных типов, которые, в свою очередь, основаны на понятии наименьшего содержащего аргумента типа (lcta).

lcta() определена для всех шести возможных случаев. Затем CandidateInvocation(G) определяет наиболее специфичный вызов обобщенного типа G, который содержит все вызовы G, известные как надтипы Tj. Это будет наш кандидатский вызов G в границах, которые мы выводим для Tj.

Определим CandidateInvocation(G) = lci(Inv(G)), где lci, наименьший содержащий вызов, определяется следующим образом:

  • lci(S) = lci(e1, ..., en), где ei (1 ≤ i ≤ n) в S

  • lci(e1, ..., en) = lci(lci(e1, e2), e3, ..., en)

  • lci(G<X1, ..., Xn>, G<Y1, ..., Yn>) = G<lcta(X1, Y1), ..., lcta(Xn, Yn)>

  • lci(G<X1, ..., Xn>) = G<lcta(X1), ..., lcta(Xn)>

где lcta() – это функция наименьшего содержащего аргумента типа, определенная (предполагая, что U и V – выражения типа) следующим образом:

  • lcta(U, V) = U если U = V, в противном случае ? extends lub(U, V)

  • lcta(U, ? extends V) = ? extends lub(U, V)

  • lcta(U, ? super V) = ? super glb(U, V)

  • lcta(? extends U, ? extends V) = ? extends lub(U, V)

  • lcta(? extends U, ? super V) = U если U = V, в противном случае ?

  • lcta(? super U, ? super V) = ? super glb(U, V)

  • lcta(U) = ? если верхняя граница U – это Object, в противном случае ? extends lub(U,Object)

где glb() определено в §5.1.10.

Наконец, мы определяем границу для Tj на основе всех элементов минимального набора кандидатов в удаленные типы-надтипы. Если какие-либо из этих элементов являются обобщенными, мы используем функцию CandidateInvocation(), чтобы восстановить информацию об аргументе типа.

Определим Candidate(W) = CandidateInvocation(W), если W обобщенный, иначе W.

Выведенный тип для Tj, lub(U1 ... Uk), равен Candidate(W1) & ... & Candidate(Wr), где Wi (1 ≤ i ≤ r) – элементы MEC.

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

Возможный бесконечный тип обусловлен рекурсивными вызовами lub(). Читатели, знакомые с рекурсивными типами, должны отметить, что бесконечный тип не эквивалентен рекурсивному типу.

15.12.2.8. Выведение неразрешённых аргументов типа

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

  • Если результат метода встречается в контексте, где он будет подвергнут преобразованию присваивания (§5.2) к типу S, то пусть R — объявленный тип результата метода, а R' = R[T1=B(T1) ... Tn=B(Tn)], где B(Ti) — тип, выведенный для Ti в предыдущем разделе, или Ti, если тип не был выведен.

    Если S — ссылка на тип, то пусть S' будет S. В противном случае, если S — примитивный тип, то пусть S' будет результатом применения преобразования упаковывания (§5.1.7) к S.

    Затем создается набор начальных ограничений, состоящий из:

    • ограничение S' >> R', при условии, что R не является void; и

    • дополнительные ограничения Bi[T1=B(T1) ... Tn=B(Tn)] >> Ti, где Bi — объявленная граница Ti,

    • дополнительные ограничения B(Ti) << Bi[T1=B(T1) ... Tn=B(Tn)], где Bi — объявленная граница Ti,

    • для любого ограничения вида V >> Ti, сгенерированного в §15.12.2.7: ограничение V[T1=B(T1) ... Tn=B(Tn)] >> Ti.

    • для любого ограничения вида Ti = V, сгенерированного в §15.12.2.7: ограничение Ti = V[T1=B(T1) ... Tn=B(Tn)].

    и используется для вывода ограничений на аргументы типа с помощью алгоритма из §15.12.2.7.

    Все ограничения равенства разрешаются, а затем для каждого оставшегося ограничения вида Ti <: Uk, аргумент Ti выводится как glb(U1, ..., Uk) (§5.1.10).

    Если Ti появляется как аргумент типа в любом Uk, то Ti выводится как переменная типа X с верхней границей, заданной параметризованным типом glb(U1[Ti=X], ..., Uk[Ti=X]), и нижней границей — типом null.

    Любая оставшаяся переменная типа T, которая ещё не была выведена, выводится как тип Object. Если ранее выведенная переменная типа P использует T, то P выводится как P[T=Object].

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

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

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

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

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

    Причина в том, что вызов метода в этой форме не может быть использован для вызова метода-члена объекта в местах, где this (§15.8.3) не определено.

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

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

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

Причина в том, что вызов метода в этой форме не указывает ссылку на объект, который может служить в качестве this внутри метода-члена объекта.

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

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

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

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

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

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

  • В противном случае пусть C — это класс, обозначаемый ИмяКласса.

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

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

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

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

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

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

  • Он имеет тип возвращаемого значения Object.

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

В Java SE 7 единственными полиморфными по сигнатуре методами являются методы invoke и invokeExact класса java.lang.invoke.MethodHandle.

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

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

  • Типы его параметров — это статические типы выражений фактических аргументов.

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

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

    • Если выражение вызова метода является оператором выражения, метод является void.

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

    • В противном случае тип возвращаемого значения — это объявленный тип возвращаемого значения метода Object.

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

  • Имя метода.

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

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

  • Тип результата или void.

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

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

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

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

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

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

Если объявление во время компиляции для вызова метода не является void, то тип выражения вызова метода — это тип результата, указанный в объявлении во время компиляции.

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

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

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

Следует рассмотреть несколько случаев, в зависимости от того, какая из пяти производных MethodInvocation (§15.12) используется:

  1. Если используется первая производная MethodInvocation, которая включает MethodName, то существуют три подслучаи:

    • Если MethodName — простое имя, то есть просто Identifier, то существуют два подслучая:

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

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

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

    • Если MethodName — квалифицированное имя вида TypeName . Identifier, то ссылка на целевой объект отсутствует.

    • Если MethodName — квалифицированное имя вида FieldName . Identifier, то существуют два подслучая:

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

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

  2. Если используется вторая производная MethodInvocation, которая включает Primary, то существуют два подслучая:

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

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

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

  3. Если используется третья производная MethodInvocation, которая включает ключевое слово super, то ссылка на целевой объект — значение this.

  4. Если используется четвертая производная MethodInvocation, ClassName . super, то ссылка на целевой объект — значение ClassName.this.

  5. Если используется пятая производная MethodInvocation, начинающаяся с TypeName . NonWildTypeArguments, то ссылка на целевой объект отсутствует.

Пример 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.1.9) из типа массива с реализуемым типом элемента в тип массива с нереализуемым типом элемента, то есть в тип переменного параметра арности. Компилятор Java, согласно §5.1.9, должен выдавать предупреждение о неявной конвертации на этапе компиляции. Реализованный в Oracle компилятор Java идентифицирует это предупреждение как более информативное предупреждение создание массива дженериков без проверки.

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

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

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

Пусть C — класс, содержащий вызов метода, T — квалифицируемый тип вызова метода (§13.1), и m — имя метода, определенное на этапе компиляции (§15.12.3).

Реализация языка программирования Java должна гарантировать, что метод m по-прежнему существует в типе T в рамках связи. Если это не так, возникает NoSuchMethodError (который является подклассом IncompatibleClassChangeError).

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

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

  • Для типа T:

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

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

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

  • Для метода m:

    • Если m является public, то m доступен. (Все члены интерфейсов являются public (§9.2).)

    • Если m является protected, то m доступен тогда и только тогда, когда либо T находится в том же пакете, что и C, либо C является T или подклассом T.

    • Если m имеет доступ по умолчанию (пакетный доступ), то m доступен тогда и только тогда, когда T находится в том же пакете, что и C.

    • Если m является private, то m доступен тогда и только тогда, когда C является T или C содержит T, или T содержит C, или T и C оба содержатся третьим классом.

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

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

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

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

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

Если режим вызова является nonvirtual, переопределение запрещено. Вызывается метод m класса T.

В противном случае, если режим вызова является virtual, и T и m совместно указывают на полиморфный метод по сигнатуре (§15.12.3), то целевой объект является экземпляром java.lang.invoke.MethodHandle. Обработчик метода инкапсулирует тип, который сопоставляется с информацией, связанной с вызовом метода во время компиляции (§15.12.3). Подробности этого сопоставления приведены в спецификации виртуальной машины Java, издание Java SE 7, и API платформы Java SE. Если сопоставление успешно, целевой метод, инкапсулированный обработчиком метода, вызывается непосредственно и немедленно, и процедура в §15.12.4.5 не выполняется.

В противном случае, режим вызова является interface, virtual или super, и переопределение может произойти. Используется динамический поиск метода. Процесс динамического поиска начинается с класса S, определяемого следующим образом:

  • Если режим вызова является interface или virtual, то S изначально является фактическим классом R целевого объекта.

    Это верно, даже если целевой объект является массивом. (Обратите внимание, что для режима вызова interface, R обязательно реализует T; для режима вызова virtual, R обязательно является либо T, либо подклассом T.)

  • Если режим вызова является super, то S изначально является квалифицируемым типом (§13.1) вызова метода.

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

Пусть X — тип во время компиляции ссылки на целевой объект вызова метода. Тогда:

  1. Если класс S содержит объявление метода, не являющегося abstract, с именем m с тем же описателем (то же количество параметров, те же типы параметров и тот же возвращаемый тип), что и требуется вызовом метода, как определено во время компиляции (§15.12.3), тогда:

    • Если режим вызова является super или interface, то это метод для вызова, и процедура завершается.

    • Если режим вызова является virtual, и объявление в S переопределяет (§8.4.8.1) X.m, то метод, объявленный в S, является вызываемым методом, и процедура завершается.

    • Если режим вызова является virtual, и объявление в S не переопределяет X.m, и при этом X.m объявлен abstract, то выбрасывается AbstractMethodError.

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

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

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

Пример 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.9, §5.5.2, §8.4.1, §8.4.8.3, §8.4.8.4, §9.4.1.2, §15.12.4.2).

Реализации могут обеспечить эти семантики, создавая мосты методов. В приведённом примере в классе D был бы создан следующий мост метода:

Object id(Object x) { return id((String) x); }

Это метод, который фактически был бы вызван виртуальной машиной Java в ответ на вызов c.id(new Object()), показанный выше, и он выполнит приведение и завершится ошибкой, как требуется.


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

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


ArrayAccess:
ИмяВыражения [ Выражение ]
PrimaryNoNewArray [ Выражение ]

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

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

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

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

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

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

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

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

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

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

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

  • В противном случае, если значение выражения ссылки на массив является null, то генерируется NullPointerException исключение.

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

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

Пример 15.13-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.13-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.13-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.14. Постфиксные выражения

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


ПостфиксноеВыражение:
Первичное
ИмяВыражения
ВыражениеСПостфикснымИнкрементом
ВыражениеСПостфикснымДекрементом

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

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

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

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


ВыражениеСПостфикснымИнкрементом:
ПостфиксноеВыражение ++

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

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


ВыражениеСПостфикснымДекрементом:
ПостфиксноеВыражение --

Результат постфиксного выражения должен быть переменной типа, преобразуемой (§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:
PreIncrementExpression
PreDecrementExpression
+ UnaryExpression
- UnaryExpression
UnaryExpressionNotPlusMinus

PreIncrementExpression:
++ UnaryExpression

PreDecrementExpression:
-- UnaryExpression

UnaryExpressionNotPlusMinus:
PostfixExpression
~ UnaryExpression
! UnaryExpression
CastExpression

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

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:
( PrimitiveType ) UnaryExpression
( ReferenceType ) UnaryExpressionNotPlusMinus

См. §15.15 для обсуждения различий между UnaryExpression и UnaryExpressionNotPlusMinus.

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

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

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

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

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

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

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

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

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

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

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

      • Если тип выражения умножения 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, вычисляется точное математическое частное. Затем выбирается набор значений с плавающей точкой:

    • Если выражение деления строго с плавающей точкой (§15.4):

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

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

    • Если выражение деления не строго с плавающей точкой:

      • Если тип выражения деления — 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 разрядов со знаком расширения. Результирующее значение равно ⌊ 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, результат — ложь.

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

Во время выполнения результат оператора instanceof — true, если значение RelationalExpression не равно null, и ссылка может быть приведена (§15.16) к 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) при применении к булевым операндам.

15.21.3. Операторы равенства ссылок == и !=

Если оба операнда оператора равенства являются либо ссылочным типом, либо типом null, то операция является проверкой равенства объектов.

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

Во время выполнения результат == равен 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

Оператор условного выражения синтаксически ассоциативен справа налево (он группируется справа налево). Таким образом, a?b:c?d:e?f:g означает то же, что и a?b:(c?d:(e?f:g)).

Оператор условного выражения имеет три операнда. ? стоит между первым и вторым выражениями, а : — между вторым и третьим выражениями.

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

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

Фактически, согласно грамматике операторных выражений (§14.8), оператор условного выражения не может использоваться в контексте, где может использоваться вызов метода void.

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

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

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

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

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

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

    • Если один из операндов имеет тип T, где T — byte, short или char, а другой операнд — константное выражение (§15.28) типа int, значение которого представимо в типе T, то тип условного выражения — T.

    • Если один из операндов имеет тип T, где T — Byte, Short или Character, а другой операнд — константное выражение (§15.28) типа int, значение которого представимо в типе U, который является результатом применения преобразования к типу-распаковке к T, то тип условного выражения — U.

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

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

  • В противном случае, второй и третий операнды имеют типы S1 и S2 соответственно. Пусть T1 — тип, полученный из S1 с применением преобразования к типу-упаковке, а T2 — тип, полученный из S2 с применением преобразования к типу-упаковке.

    Тип условного выражения — результат применения преобразования захвата (§5.1.10) к lub(T1, T2) (§15.12.2.7).

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

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

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

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

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

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

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

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

Существует 12 операторов присваивания; все они синтаксически ассоциативны справа налево (они группируются справа налево). Таким образом, a=b=c означает a=(b=c), которое присваивает значение c переменной b, а затем присваивает значение b переменной a.


AssignmentExpression:
ConditionalExpression
Assignment

Assignment:
LeftHandSide AssignmentOperator AssignmentExpression

LeftHandSide:
ExpressionName
FieldAccess
ArrayAccess

AssignmentOperator: один из
= *= /= %= += -= <<= >>= >>>= &= ^= |=

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

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

Тип выражения присваивания — это тип переменной после преобразования захвата (§5.1.10).

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

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

15.26.1. Оператор простого присваивания =

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

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

Если выражение левого операнда является выражением доступа к полю (§15.11) e.f, возможно, заключённым в одной или нескольких парах скобок, то:

  • Сначала вычисляется выражение e. Если вычисление e завершается аварийно, то выражение присваивания завершается аварийно по той же причине.

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

  • Затем, если поле, обозначенное e.f, не является static, и результат вычисления e выше является null, то генерируется исключение NullPointerException.

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

Если левый операнд является выражением доступа к элементу массива (§15.13), возможно, заключённым в одной или нескольких парах скобок, то:

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

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

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

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

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

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

    Этот компонент является переменной; назовем его тип SC. Также пусть TC — это тип левого операнда оператора присваивания, определённый на этапе компиляции. Тогда существует два варианта:

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

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

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

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

      Компилятор Java может доказать на этапе компиляции, что компонент массива будет ровно типа TC (например, TC может быть final). Но если компилятор Java не может доказать на этапе компиляции, что компонент массива будет ровно типа TC, то проверка должна быть выполнена во время выполнения, чтобы убедиться, что класс RC совместим по присваиванию (§5.2) с фактическим типом SC компонента массива.

      Эта проверка похожа на сужение приведения (§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.13), то:

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

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

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

Выражение — это любое выражение присваивания:


Выражение:
ВыражениеПрисваивания

В отличие от C и C++, язык программирования Java не имеет оператора запятой.

15.28. Постоянные выражения


ConstantExpression:
Expression

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

  • Мультипликативные операторы *, / и % (§15.17)

  • Аддитивные операторы + и - (§15.18)

  • Операторы сдвига <<, >> и >>> (§15.19)

  • Операторы сравнения <, <=, > и >= (но не instanceof) (§15.20)

  • Операторы сравнения на равенство == и != (§15.21)

  • Битовые и логические операторы &, ^ и | (§15.22)

  • Условный оператор "и" && и условный оператор "или" || (§15.23, §15.24)

  • Тernary условный оператор ? : (§15.25)

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

  • Простые имена (§6.5.6.1), которые ссылаются на постоянные переменные (§4.12.4).

  • Квалифицированные имена (§6.5.6.2) вида TypeName . Identifier, которые ссылаются на постоянные переменные (§4.12.4).

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