Речь о приоритете операторов в Java.
Итак, уже не раз наталкиваюсь на такие вот интересные таблички, в которых операторы
выставлены в приоритете их выполнения в программе. Типичный пример: https://introcs.cs.princeton.edu/java/11precedence/
Можно поискать и другие варианты, но все они более-менее схожи. Во всех таблицах,
которые попадались мне на глаза, постфиксные унарные операторы инкремента и декремента
имеют явный приоритет над своими префиксными аналогами. Следовательно, стоило бы ожидать,
что в составном выражении, которое содержит, как постфиксный инкремент/декремент, так
и префиксный, изначально должен вычисляться именно тот инкремент/декремент, который
был записан в постфиксной форме. Хорошо, давайте тогда рассмотрим небольшой пример.
Возьмём следующий кусок кода:
int y = 10;
int z = ++y * y--;
System.out.println(z);
Что мы здесь имеем? Я вижу целых 4 операции в данном составном выражении. Ключевой
операций здесь является операция присваивания, но она будет выполнена в самую последнюю
очередь, так как имеет наименьший приоритет. В таком случае нам необходимо отдельно
рассмотреть правую часть данной операции. Очевидно, что изначально необходимо выполнить
унарные операции, а уже потом переходить к бинарной. Но какую операцию делать в первую
очередь? Это достаточно принципиально, так как от этого зависит результат выполнения
программы. Если верить таблице, то сначала я должен вычислить инкремент/декремент,
а потом уже переходить к их перемножению. Поскольку постфиксная форма имеет более высокий
приоритет, то сначала я провожу операцию y--, а уже потом ++y. В общем, если всё делать
так, как я это понимаю, то в результате должно получиться 100. Пишу данный код в своей
IDE и вывожу его на консоль. Результат: 121. Почему так? Неправильно расписана таблица
приоритетов? Или же я чего-то не понимаю?
Ответы
Ответ 1
Операция ++y выполняется непосредственно перед тем как значение y будет подставлено
в выражение.
Операция y-- сразу после.
Пример 1:
y= 10
z = ++y * y--
----------------------------
y = y + 1
z = 11 * 11 // 11*11 = 121
y = y - 1
Пример 2:
z = (y++) * (++y)
----------------------------
z = 10 * (++y)
y = y + 1
y = y + 1
z = 10 * 12 // 10*12 = 120
UPD: JLS утверждает что существуют первичные выражения
Первичные выражения включают в себя большинство простейших видов
выражений, из которых строятся другие: литералы, создание объектов,
обращения к полям, вызовы методов, ссылки на методы и обращения к
массиву. Выражение в скобках также рассматривается синтаксически как
первичное выражение.
постфиксные выражения
Постфиксные выражения включают использование операторов postfix ++ и
-. Они не считаются первичными выражениями (§15.8), но обрабатываются отдельно
в грамматике, чтобы избежать определенных
неоднозначностей. И становятся взаимозаменяемыми только здесь, на
уровне приоритета постфиксных выражений.
и унарные операторы
Оператор +, -, ++, --, ~, !, и оператор приведения типа (§15.16)
называются унарными операторами.
Видно что постфиксные операции занимают отдельную позицию в структуре выражений Java,
где-то между вызовами методов и унарными операторами. Соответственно им должен быть
назначен определенный уровень приоритета. JLS не объясняет конкретно, почему это было
сделано, оправдываясь "определенными неоднозначностями". Можно предположить, что это
относиться к удобству парсинга выражений или определенной реализации его механизма.
Как выяснили в соседних ответах, столкнуться с конфликтом префиксных и постфиксных
операторов или неоднозначностью выражения с их использованием достаточно сложно, и
на практику написания кода такое разделение уровней приоритетов сильно не влияет.
Ответ 2
Чтобы разобраться, достаточно открыть .class файл и посмотреть как java его декомпилировала:
int y = 10;
int y = y + 1; // 11
int z = y * y--; // 11 * 11, потом y = y - 1, но это уже не важно, т.к. `y` больше
не используется
System.out.println("z = " + z);
Ответ 3
Значение подставляется сразу после выполнения операции. То есть, выполнив ++y оно
вернет значение 11 на место операции, а в месте y-- сначала вернет 11, а потом уменьшит
y на единицу. И выходит что 11*11
Я как-то даже не обращал внимание, что у постфиксных унарных операторов приоритет
выше. Выполняются они все в порядке очереди. Интересно
Почему оно так работает, вроде бы понятно. Приоритет нужен в случае решения конфликта,
когда два оператора разных приоритетов сталкиваются на одном уровне. То есть (a + b*b)
сразу ясно что выполнять первым необходимо умножение, а в случае (a+a == b*b) смысла
выполнять первым именно операцию умножение нет, идя в порядке слева направо получим
тот же результат.
Приоритет операторов ++/-- помог бы решить ситуацию вроде (++a--), указывающий, что
сначала нужно выполнить именно уменьшение переменной, но такая конструкция запрещена,
да и смыла в ней мало. В случае с (++a * a--) выполнятся будет слева направо. А из=за
того, что унарные операции изменяют саму переменную, мы можем проследить какая операция
в действительности выполняется первая, хотя по сути это не должно иметь значения. Их
нужно использовать с осторожностью
Ответ 4
Друзья, спасибо всем за помощь! Мозговой штурм действительно работает, убедился в
этом на своей собственной шкуре. Знаю, что данная тема вводит в ступор многих начинающих
программистов, поэтому решил расписать свои соображения на данный счёт. Надеюсь, что
судить строго не будете.
Итак, давайте начнём разбираться в этом вопросе. В чём основная проблема людей (в
том числе и моя) в понимании данной темы? А проблема в том, что мы путаем «приоритет»
с обычным порядком выполнения операторов. В каком порядке JVM выполняет инструкции?
Очевидно, что все инструкции (а также операторы составных выражений) выполняются в
привычном нам левоассоциативном направлении (слева направо), а когда мы доходим до
конца строки (как правило в конце каждой строки ставится специальный Unicode-символ
(или их комбинация), который является ограничителем строк (в основном это символы,
которые имеют мнемоническое обозначение CR+LF)), то мы переходим на нижестоящую строчку
и выполняем код с самого начала в том же левостороннем направлении (можно представить
это как возврат каретки в самое начало строки в пишущей машинке, а затем перевод той
же каретки в начало следующей строки).
Когда же начинаются основные сложности? Все неурядицы возникают ровным счётом тогда,
когда мы сталкиваемся с некоторой неопределённостью. Что я имею в виду? Давайте рассмотрим
классический пример применения на практике таблицы приоритетов операторов. Возьмём
следующее арифметическое выражение:
/*
* Some code
*/
int z = a + b * c;
Ещё с начальной школы мы знаем, что сперва выполняются мультипликативные операции,
а затем уже аддитивные, так как первые имеют явный приоритет над вторыми. Как это реализовано
в самой Java'е и какая здесь может возникнуть коллизия? Мы видим в правой части операции
присваивания составное выражение, которое состоит из двух бинарных арифметических операторов
и 3-х операндов. Очевидно, что у нас есть два возможных пути, которые будут пересекаться
в одном месте. Мы можем сделать так:
(a + b) * с;
Или же так:
a + (b * c);
Коллизия заключается в том, что в обеих случаях мы захватываем переменную b, которая
одновременно является одним из операндов относительно обеих операторов. Понятное дело,
что конечный результат будет разниться в зависимости от выбранного пути. Вот тут и
вступают в дело приоритеты операторов! Мы прекрасно знаем, что операция умножения будет
выполняться первой, ведь для нас это весьма очевидно и мы даже не обращаем на это внимания
(хотя всё это изначально прописано в "мозгах" самой JVM).
А теперь хотелось бы перейти к более сложному примеру, с которого всё и начиналось.
Почему же инкремент/декремент записанный в постфиксной нотации имеет приоритет над
префиксной формой той же записи? Настолько я понял, то в обеих случаях можно привести
лишь по одному примеру, где может возникнуть явная неопределённость. Приведём эти примеры:
a---b;
a+++b;
Такая форма записи допускает всего 2 возможных варианта без ошибки времени компиляции.
Здесь можно выделить 2 оператора, один из которых будет бинарным оператором (сложения
или вычитания), а также унарный инкремент/декремент (о форме записи пока ничего не
говорим). Здесь уже возникает немного другого рода неопределённость, которая отличается
от первого рассмотренного нами случая. Если в первом случае возникла коллизия на уровне
совместно используемого операнда, то здесь возникает двусмысленность в вопросе унарного
оператора, который может быть постфиксным/префиксным инкрементом/декрементом, как для
переменной a, так и для переменной b. Имеем следующие варианты раскрытия скобок:
(a--)-b;
a-(--b);
(a++)+b;
a+(++b);
Положение бинарного оператора имеет принципиальное значение только в первом случае,
так как вычитание является антикоммутативной операций, а сложение, напротив, коммутативной.
Хотелось бы добавить, что такой приоритет появился совсем не случайно. Чтобы понять
это, необходимо обратиться к JLS:
The longest possible translation is used at each step, even if the
result does not ultimately make a correct program while another
lexical translation would. There is one exception: if lexical
translation occurs in a type context (§4.11) and the input stream has
two or more consecutive > characters that are followed by a non->
character, then each > character must be translated to the token for
the numerical comparison operator >.
The input characters a--b are tokenized (§3.5) as a, --, b, which is
not part of any grammatically correct program, even though the
tokenization a, -, -, b could be part of a grammatically correct
program.
Without the rule for > characters, two consecutive > brackets in a
type such as List> would be tokenized as the signed right
shift operator >>, while three consecutive > brackets in a type such
as List>> would be tokenized as the unsigned right
shift operator >>>. Worse, the tokenization of four or more
consecutive > brackets in a type such as
List>>> would be ambiguous, as various
combinations of >, >>, and >>> tokens could represent the >>>>
characters.
Как мы можем понять, на каждом этапе используется наиболее длинная трансляция, даже
если она приводит к некорректной программе. Скорее всего именно по этой причине постфиксная
форма унарных операторов имеет некоторый приоритет над префиксной, это вписывается
в общую логику лексической транляции, тем более других примеров в которых могла бы
возникнуть коллизия между такого рода операторами придумать не то чтобы трудно, а скорее
всего невозможно (во всяком случае, лично я не вижу других возможных вариантов написания
корректной программы, где можно было бы смоделировать данную ситуацию).
Напоследок можно рассмотреть пример с логическими операторами.
boolean bool = a ^ b & c | d;
где a, b, c, d — переменные логического типа.
Чтобы понять, какая операция будет выполняться первой, было бы неплохо заглянуть
вот сюда. Зная приоритет каждого из операторов находим правильное решение:
boolean bool = ((a ^ (b & c)) | d);
Что хотелось бы сказать напоследок? В глобальной сети можно найти очень много ложных
таблиц, которые могут ввести в заблуждение. Авторы добавляют туда всякую "отсебятину",
по типу разделителей, оператора new, операторы приведения типов и прочую чепуху. Считаю,
что есть единственный источник к которому можно обратиться в данном вопросе, это официальный
сайт корпорации Oracle. И да, соглашусь с тем, что лучше использовать обычные скобки
для коррекции приоритетов! :) Благодарю всех за внимание! ;)