Java 101: Foundations

Java 101: Evaluate Java expressions with operators

Learn about simple expressions and operator-based compound expressions

1 2 3 4 Page 3
Page 3 of 4
  • Dividing a floating-point/double precision floating-point value by 0 causes the operator to return one of the following special values: +infinity (the dividend is positive), -infinity (the dividend is negative), or NaN -- Not a Number -- (the dividend and divisor are both 0).
  • Dividing an integer value by integer 0 causes the operator to throw an ArithmeticException object. We'll explore exceptions in a future Java 101 article.

Compile Listing 7 (javac MulOp.java) and run the application (java MulOp). You should observe the following output:

192.0
21
1
Infinity
-Infinity
NaN
Exception in thread "main" java.lang.ArithmeticException: / by zero
	at MulOp.main(MulOp.java:11)

Object creation operator

The object creation operator (new) is used to create an object from a class or to create an array. This operator is formally defined below:

  • Given new identifier(argument list), allocate memory for object and call constructor specified as identifier(argument list). Example: new String("ABC")
  • Given new identifier[integer size], allocate a one-dimensional array of values. Example: new int[5]

    To create a two-dimensional array, the syntax changes to identifier[integer size][integer size] (e.g., new double[5][5]). For additional dimensions, append an [integer size] per dimension.

Object and array creation is a rich topic, so we'll dive into it another time.

Relational operators

The relational operators impose an ordering on their operands by determining which operand is greater, lesser, and so on. These operators include greater than (>), greater than or equal to (>=), less than (<), and less than or equal to (<=). Type checking (instanceof) is also considered to be relational. These operators are formally defined below:

  • Greater than: Given operand1 > operand2, where each operand must be of character or numeric type, return true when operand1 is greater than operand2. Otherwise, return false. Example: 65.3 > 22.5
  • Greater than or equal to: Given operand1 >= operand2, where each operand must be of character or numeric type, return true when operand1 is greater than or equal to operand2. Otherwise, return false. Example: 0 >= 0
  • Less than: Given operand1 < operand2, where each operand must be of character or numeric type, return true when operand1 is less than operand2. Otherwise, return false. Example: x < 15
  • Less than or equal to: Given operand1 <= operand2, where each operand must be of character or numeric type, return true when operand1 is less than or equal to operand2. Otherwise, return false. Example: 0 <= 0
  • Type checking: Given operand1 instanceof operand2, where operand1 is an object and operand2 is a class (or other user-defined type), return true when operand1 is an instance of operand2. Otherwise, return false.

Listing 8 presents the source code to a RelOp application that lets you play with the relational operators.

Listing 8. RelOp.java

class RelOp
{
   public static void main(String[] args)
   {
      int x = 10;
      System.out.println(x > 10);
      System.out.println(x >= 10);
      System.out.println(x < 10);
      System.out.println(x <= 10);
      System.out.println("A" instanceof String);
   }
}

Compile Listing 8 (javac RelOp.java) and run the application (java RelOp). You should observe the following output:

false
true
false
true
true

The final output line is interesting because it proves that a string literal (e.g., "A") is in fact a String object.

Shift operators

The shift operators let you shift an integral value left or right by a specific number of bit positions. These operators include left shift (<<), signed right shift (>>), and unsigned right shift (>>>); and are formally defined below:

  • Left shift: Given operand1 << operand2, where each operand must be of character or integer type, shift operand1's binary representation left by the number of bits that operand2 specifies. For each shift, a 0 is shifted into the rightmost bit and the leftmost bit is discarded. Only the five low-order bits of operand2 are used when shifting a 32-bit integer (to prevent shifting more than the number of bits in a 32-bit integer). Only the six low-order bits of operand2 are used when shifting a 64-bit integer (to prevent shifting more than the number of bits in a 64-bit integer). The shift preserves negative values. Furthermore, it's equivalent to (but faster than) multiplying by a multiple of 2. Example: 3 << 2
  • Signed right shift: Given operand1 >> operand2, where each operand must be of character or integer type, shift operand1's binary representation right by the number of bits that operand2 specifies. For each shift, a copy of the sign bit (the leftmost bit) is shifted to the right and the rightmost bit is discarded. Only the five low-order bits of operand2 are used when shifting a 32-bit integer (to prevent shifting more than the number of bits in a 32-bit integer). Only the six low-order bits of operand2 are used when shifting a 64-bit integer (to prevent shifting more than the number of bits in a 64-bit integer). The shift preserves negative values. Furthermore, it's equivalent to (but faster than) dividing by a multiple of 2. Example: -5 >> 2
  • Unsigned right shift: Given operand1 >>> operand2, where each operand must be of character or integer type, shift operand1's binary representation right by the number of bits that operand2 specifies. For each shift, a zero is shifted into the leftmost bit and the rightmost bit is discarded. Only the five low-order bits of operand2 are used when shifting a 32-bit integer (to prevent shifting more than the number of bits in a 32-bit integer). Only the six low-order bits of operand2 are used when shifting a 64-bit integer (to prevent shifting more than the number of bits in a 64-bit integer). The shift doesn't preserve negative values. Furthermore, it's equivalent to (but faster than) dividing by a multiple of 2. Example: 42 >>> 2

Listing 9 presents the source code to a ShiftOp application that lets you play with the shift operators.

Listing 9. ShiftOp.java

class ShiftOp
{
   public static void main(String[] args)
   {
      System.out.println(1 << 8);
      System.out.println(8 >> 2);
      System.out.println(-1 >> 1);
      System.out.println(-1 >>> 1);
   }
}

Compile Listing 9 (javac ShiftOp.java) and run the application (java ShiftOp). You should observe the following output:

256
2
-1
2147483647

The output reveals that bit shifting is equivalent to multiplying or dividing by multiples of 2 (but is faster). The first output line is equivalent to the value derived from 2 * 2 * 2 * 2 * 2 * 2 * 2 * 2 and the second output line is equivalent to the value derived from 8 / 4. The final two output lines show the difference between preserving and not preserving the sign bit where negative values are concerned.

Unary minus/plus operators

The final operators that Java supports are unary minus (-) and unary plus (+). Unary minus returns the negative of its operand (e.g., -8 returns -8 and --8 returns 8), whereas unary plus returns its operand unchanged (e.g., +8 returns 8 and +-8 returns -8). Unary plus is not commonly used, but is included in Java's set of operators for completeness.

Precedence and associativity

I previously mentioned that Java's rules of precedence (priority in order) dictate the order in which compound expressions are evaluated. For the common arithmetic operators (e.g., addition and multiplication), Java follows the established precedence conventions (e.g., multiplication first and then addition). For other operators, order of evaluation isn't as clear. For example, how does Java evaluate 6 > 3 * 2? Does the comparison precede multiplication or vice-versa?

The following list shows you the precedence of Java's operators. Operators closer to the top have higher precedence than operators lower down. In other words, operators higher up in the list are performed first. Operators that have the same precedence are listed on the same line. When the Java compiler encounters multiple operators with the same precedence in the same compound expression, it generates code to perform the operations according to their associativity:

  • Array index, member access, method call, postdecrement, postincrement
  • Bitwise complement, cast, logical complement, object creation, predecrement, preincrement, unary minus, unary plus
  • Division, multiplication, remainder
  • Addition, string concatenation, subtraction
  • Left shift, signed right shift, unsigned right shift
  • Greater than, greater than or equal to, less than, less than or equal to, type checking
  • Equality, inequality
  • Bitwise AND, logical AND
  • Bitwise exclusive OR, logical exclusive OR
  • Bitwise inclusive OR, logical inclusive OR
  • Conditional AND
  • Conditional OR
  • Conditional
  • Assignment, compound assignment

You won't always want to follow this order. For example, you might want to perform addition before multiplication. Java lets you violate precedence by placing subexpressions between round brackets (parentheses). A parenthesized subexpression is evaluated first. Parentheses can be nested, in which a parenthesized subexpression can be located within a parenthesized subexpression. In this case, the innermost parenthesized subexpression is evaluated first.

During evaluation, operators with the same precedence level (e.g., addition and subtraction) are processed according to their associativity (how operators having the same precedence are grouped when parentheses are absent). For example, 10 * 4 / 2 is evaluated as if it was (10 * 4) / 2 because * and / are left-to-right associative operators. In contrast, a = b = c = 50; is evaluated as if it was a = (b = (c = 50)); (50 is assigned to c, c's value is assigned to b, and b's value is assigned to a -- all three variables contain 50) because = is a right-to-left associative operator.

Most of Java's operators are left-to-right associative. Right-to-left associative operators include assignment, bitwise complement, cast, compound assignment, conditional, logical complement, object creation, predecrement, preincrement, unary minus, and unary plus.

I've created a small application for playing with precedence and associativity. Listing 10 presents its source code.

Listing 10. PA.java

class PA
{
   public static void main(String[] args)
   {
      System.out.println(10 * 4 + 2);
      System.out.println(10 * (4 + 2));
      int a, b, c;
      a = b = c = 50;
      System.out.println(a);
      System.out.println(b);
      System.out.println(c);
   }
}

Compile Listing 10 (javac PA.java) and run the application (java PA). You should observe the following output:

42
60
50
50
50

In Listing 10, suppose I specified (a = b) = c = 50; instead of a = b = c = 50; because I want a = b to be evaluated first. How would the compiler respond -- and why?

Converting between types

My previous binary and ternary operator examples presented operands having the same type (e.g., each of 6 * 5's operands is an int). In many cases, operands will not have the same type, and the Java compiler will need to generate bytecode that converts an operand from one type to another before generating bytecode that performs the operation. For example, when confronted by 5.1 + 8, the compiler generates bytecode to convert 32-bit integer 8 to its double precision floating-point equivalent followed by bytecode to add these double precision values. (In the example, the compiler would generate an i2d instruction to convert from int to double and then a dadd instruction to add the two doubles.)

How does the compiler know which operand to convert? For primitive-type operands, its choice is based on the following widening rules, which essentially convert from a type with a narrower set of values to a type with a wider set of values:

  • Convert byte integer to short integer, integer, long integer, floating-point, or double precision floating-point.
  • Convert short integer to integer, long integer, floating-point, or double precision floating-point.
  • Convert character to integer, long integer, floating-point, or double precision floating-point.
  • Convert integer to long integer, floating-point, or double precision floating-point.
  • Convert long integer to floating-point or double precision floating-point.
  • Convert floating-point to double precision floating-point.

Regarding expression 5.1 + 8, we can see that the compiler chooses to convert 8 to a double based on the rule for converting an integer to double precision floating-point. If it converted 5.1 to an int, which is a narrower type, information would be lost because the fractional part would be effectively truncated. Therefore, the compiler always chooses to widen a type so information isn't lost.

These rules also help to explain why, in BitwiseOp.java, the binary values resulting from expressions such as System.out.println(~x); were 32 bits long instead of 16 bits long. The compiler converts the short integer in x to a 32-bit integer value before performing bitwise complement, via iconst_m1 and ixor instructions -- exclusive OR the 32-bit integer value with 32-bit integer -1 and produce a 32-bit integer result. The Java virtual machine provides no sconst_m1 and sxor instructions for performing bitwise complement on short integers. Byte integers and short integers are always widened to 32-bit integers.

Earlier, I mentioned that you would discover why 'C' - 'A' in (grades['C' - 'A']) produces an integer index. Character literals 'C' and 'A' are represented in memory by their Unicode values, which are unsigned 16-bit integers. When it encounters this expression, the Java compiler generates an iconst_2 instruction, which is int value 2. In this case, no subtraction is performed because of optimization. However, if I replaced 'C' - 'A' with 'C' - base, where base is a char variable initialized to 'A', the compiler would generate the following bytecode:

1 2 3 4 Page 3
Page 3 of 4