Java Expression Evaluation - Operator Precedence, Associativity, and Evaluation order

The following is an excerpt from Chapter 6 of OCP Java 11 Part 1 Fundamentals by Hanumant Deshmukh.


I am sure you have come across simple mathematical expressions such as 2 + 6 / 2 at school. You know that this expression evaluates to 5 and not 4 because division has higher precedence than addition and so, 6 / 2 will be grouped together instead of 2 + 6. To change the default grouping, you use brackets (aka parentheses) i.e. (2+6)/2. You have most likely also come across the acronym BODMAS (or PEDMAS, in some countries), which stands for Brackets/Parentheses, Orders/Exponents, Division, Multiplication, Addition, and Subtraction. It helps memorize the conventional precedence order. Conventionally, brackets are evaluated first, followed by powers, and then the rest in that order.

Java expressions are not much different from mathematical expressions. Their evaluation is determined by similar conventions and rules. The only problem is that there are a lot of operators to worry about and, as we saw earlier, the operators are not just mathematical. They are logical, relational, assignment, and so many other types as well. This makes Java expression evaluation a lot more complicated than a mathematical expression. But don't worry, you will not be required to evaluate complicated expressions in the exam. But you still need to know a few basic principles of expression evaluation to analyze code snippets presented in the questions correctly.

Precedence


Precedence, in a conceptual sense, determines which one out of two operators is evaluated "first". Another way to put it is, precedence determines how tightly an operator binds to its operands as compared to the other applicable operator in an expression. For example, in the case of 2 + 6 / 2, the operand 6 can be bound to + or to /. But the division operator, having higher precedence than addition operator, binds to an operand more tightly than the addition operator. The addition operator, therefore, is not able to get hold of 6 as its second operand and has to wait until the division operator is done with it. In an expression involving multiple operators, the operator with highest precedence gets the operand, followed by the operator with second highest precedence, and so on.

The following table shows the precedence of all Java operators -

Operators Precedence
member and array access operators  . and [ ]
cast ()
postfix expr++ expr--
unary ++expr --expr +expr -expr ~ !
multiplicative * / %
additive + -
shift << >> >>>
relational < > <= >= instanceof
equality == !=
bitwise AND &
bitwise exclusive OR ^
bitwise inclusive OR |
logical AND &&
logical OR ||
ternary ? :
assignment = += -= *= /= %= &= ^= |= <<= >>= >>>=
lambda ->

An important thing to observe from the above table is that the access operator and the cast operators have the highest precedence among all while the assignment operators and the lambda operator have the lowest precedence among all.

This explains why the following code doesn't compile:

int i = 0;
byte b = (byte) i + 1;
Since the cast operator has higher precedence than the + operator, i is first cast to byte and then the addition is performed. The end result, therefore, is an int instead of a byte. You need to put i + 1 in parentheses like this: byte b = (byte)(i + 1);

Associativity

Associativity of operators determines the grouping of operators when an expression has multiple operators of same precedence. For example, the value of the expression 2 - 3 + 4  depends on whether it is grouped as (2 - 3) + 4 or as 2 - (3 + 4). The first grouping would be used if - operator is left-associative  and the second grouping would be used if - operator is right-associative. It turns out that operators are usually grouped in the same fashion in which we read the expression i.e. from left to right. In other words, almost all of the operators in Java are defined to be left-associative. The only exceptions are the assignment operators (simple as well as compound) and the ternary operator. Thus, the expression 2 - 3 + 4 will be grouped as (2 - 3) + 4 and will evaluate to 3. But the expression a = b = c = 5; will be grouped as a = ( b = (c = 5)) ; because the assignment operator is right associative. Here is another example that shows the impact of associativity -

String s1 = "hello";
int i = 1;   
String s2 = s1 + 1 + i;
System.out.println(s2); //prints hello11

The above code prints hello11 instead of hello2 because the + operator is left-associative. The expression s2 = s1 + 1 + i; is grouped as s2 = (s1 + 1) + i;. Thus, s1+1 is computed first, resulting in the string hello1, which is then concatenated with 1, producing hello11.

A programming language could easily prohibit ambiguous expressions. There is no technical necessity for accepting the expression 2 + 6 / 2 as valid when it can be interpreted in two different ways. The only reason ambiguous expressions are accepted is because it is considered too onerous for the programmer to resolve all ambiguity by using parenthesis when a convention already exists to evaluate mathematical expressions. Rules of Operator Precedence and Associativity are basically a programming language extension to the same convention that includes all sorts of operators. You can, therefore, imagine that operator precedence and evaluation order are used by the compiler to insert parenthesis in an expression. Thus, when a compiler see 2 + 6 / 2, it converts the expression to 2 + ( 6 / 2 ), which is what the programmer should have written in the first place.

You should always use parenthesis in expressions such as 2 - 3 + 4 where the grouping of operands is not very intuitive.
Parenthesis
You can use parentheses to change how the terms of an expressions are grouped if the default grouping based on precedence and associativity is not what you want. For example, if you don't want 2 - 3 + 4 to be grouped as (2 - 3) + 4, you could specify the parenthesis to change the grouping to 2 - (3 + 4).

Evaluation Order

Once an expression is grouped in accordance with the rules of precedence and associativity, the process of evaluation of the expression starts. This is the step where computation of the terms of the expression happens. In Java, expressions are evaluated from left to right. Thus, if you have an expression getA() - getB() + getC(), the method getA will be invoked first, followed by getB and getC. This means, if the call to getA results in an exception, methods getB and getC will not be invoked.

Java also makes sure that operands of an operator are evaluated fully before the evaluation of the operator itself. Obviously, you can't compute getA() + getB() unless you get the values for getA() and getB() first.

The important point to understand here is that evaluation order of an expression doesn't change with grouping. Even if you use parentheses to change the grouping of getA() - getB() + getC() to getA() - ( getB() + getC() ), getA() will still be invoked before getB() and getC().

Let me show you another example of how the above rules are applied while evaluating an expression. Consider the following code:

public class TestClass{
  static boolean a ;
  static boolean b ;
  static boolean c ;
  public static void main(String[] args) {
     boolean bool = (a = true) || (b = true) && (c = true) ;
     System.out.println(a + ", " + b + ", "+ c );
  }
}
Can you tell the output? It prints true, false, false. Surprised?

Many new programmers think that since && has higher precedence, (b = true) && (c = true) would be evaluated first and so, it would print true, true, true. It would be logical to think so in a Mathematics class. However, evaluating a programming language expression is a two step process. In the first step, you have to use the rules of precedence and associativity to group the terms of the expression to remove ambiguity. Here, the operand (b = true) can be applied to || as well as to &&. However, since && has higher precedence than ||, this operand will be applied to &&. Therefore, the expression will be grouped as (a = true) || ( (b = true) && (c = true) ). After this step, there is no ambiguity left in the expression. Now, in the second step, evaluation of the expression will start, which, in Java, happens from from left to right. So, now, a = true will be evaluated first. The value of this expression is true and it assigns true to a as well. Next, since the first operand of || is true, and since || is a short circuiting operator, the second operand will not be evaluated and so, (b = true) && (c = true) will not be executed.