| CHAPTER 15: Expressions |
Previous |
Java Language |
Index |
Next |
Java guarantees that the operands of operators appear to be evaluated in a specific evaluation order, namely, from left to right.
It is recommended that Java code not rely crucially on this specification. Code is usually clearer when each expression contains at most one side effect, as its outermost operation, and when code does not depend on exactly which exception arises as a consequence of the left-to-right evaluation of expressions.
The left-hand operand of a binary operator appears to be fully evaluated before any part of the right-hand operand is evaluated. For example, if the left-hand operand contains an assignment to a variable and the right-hand operand contains a reference to that same variable, then the value produced by the reference will reflect the fact that the assignment occurred first.
Thus:
class Test {
public static void main(String[] args) {
int i = 2;
int j = (i=3) * i;
System.out.println(j);
}
}
prints:
9
It is not permitted for it to print 6 instead of 9 .
If the operator is a compound-assignment operator (S15.25.2), then evaluation of the left-hand operand includes both remembering the variable that the left-hand operand denotes and fetching and saving that variable's value for use in the implied combining operation. So, for example, the test program:
class Test {
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);
}
}
prints:
12 12
because the two assignment statements both fetch and remember the value of the left-hand operand, which is 9 , before the right-hand operand of the addition is evaluated, thereby setting the variable to 3 . It is not permitted for either example to produce the result 6 . Note that both of these examples have unspecified behavior in C, according to the ANSI/ISO standard.
If evaluation of the left-hand operand of a binary operator completes abruptly, no part of the right-hand operand appears to have been evaluated.
Thus, the test program:
class Test {
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!");
}
}
prints:
java.lang.Exception: I'm outta here! Now j = 1
because the left-hand operand forgetIt() of the operator / throws an exception before the right-hand operand and its embedded assignment of 2 to j occurs.
Java also guarantees that every operand of an operator (except the conditional operators && , || , and ? : ) appears to be fully evaluated before any part of the operation itself is performed.
If the binary operator is an integer division / (S15.16.2) or integer remainder % (S15.16.3), then its execution may raise an ArithmeticException , but this exception is thrown only after both operands of the binary operator have been evaluated and only if these evaluations completed normally.
So, for example, the program:
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!");
}
}
always prints:
java.lang.Exception: Shuffle off to Buffalo!
and not:
java.lang.ArithmeticException: / by zero
since no part of the division operation, including signaling of a divide-by-zero exception, may appear to occur before the invocation of loseBig completes, even though the implementation may be able to detect or infer that the division operation would certainly result in a divide-by-zero exception.
Java implementations must respect the order of evaluation as indicated explicitly by parentheses and implicitly by operator precedence. An implementation may not take advantage of algebraic identities such as the associative law to rewrite expressions into a more convenient computational order unless it can be proven that the replacement expression is equivalent in value and in its observable side effects, even in the presence of multiple threads of execution (using the thread execution model in Chapter 17), for all possible computational values that might be involved.
In the case of floating-point calculations, this rule applies also for infinity and not-a-number (NaN) values. For example, !(x<y) may not be rewritten as x>=y , because these expressions have different values if either x or y is NaN.
Specifically, floating-point calculations that appear to be mathematically associative are unlikely to be computationally associative. Such computations must not be naively reordered. For example, it is not correct for a Java compiler to rewrite 4.0*x*0.5 as 2.0*x ; while roundoff happens not to be an issue here, there are large values of x for which the first expression produces infinity (because of overflow) but the second expression produces a finite result.
So, for example, the test program:
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);
}
}
prints:
Infinity 1.6e+308
because the first expression overflows and the second does not.
In contrast, integer addition and multiplication are provably associative in Java; for example a+b+c , where a , b , and c are local variables (this simplifying assumption avoids issues involving multiple threads and volatile variables), will always produce the same answer whether evaluated as (a+b)+c or a+(b+c) ; if the expression b+c occurs nearby in the code, a smart compiler may be able to use this common subexpression.
In a method or constructor invocation or class instance creation expression, argument expressions may appear within the parentheses, separated by commas. Each argument expression appears to be fully evaluated before any part of any argument expression to its right.
Thus:
class Test {
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);
}
}
always prints:
going, going, gone
because the assignment of the string "gone" to s occurs after the first two arguments to print3 have been evaluated.
If evaluation of an argument expression completes abruptly, no part of any argument expression to its right appears to have been evaluated.
Thus, the example:
class Test {
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 oops() throws Exception {
throw new Exception("oops");
}
static int test(int a, int b, int c) {
return a + b + c;
}
}
prints:
java.lang.Exception: oops, id=1
because the assignment of 3 to id is not executed.
The order of evaluation for some expressions is not completely covered by these general rules, because these expressions may raise exceptional conditions at times that must be specified. See, specifically, the detailed explanations of evaluation order for the following kinds of expressions:
| © 1996 Sun Microsystems, Inc. All rights reserved. |