Страницы

Поиск по вопросам

понедельник, 30 декабря 2019 г.

Приоритеты операций в языке программирования Java

#java #переменные #операторы


Речь о приоритете операторов в 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. И да, соглашусь с тем, что лучше использовать обычные скобки для коррекции приоритетов! :) Благодарю всех за внимание! ;)

Комментариев нет:

Отправить комментарий