Java Language Basics

Java 101: Evaluate Java expressions with operators

A complete guide to the Java operator types and writing Java expressions in Java 12

1 2 3 4 Page 4
Page 4 of 4

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 JVM (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.

Widening rules in practice

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:


bipush 65 ; Push 8-bit Unicode value for A, which is sign-extended to 32-bit int, onto stack.
istore_1  ; Pop this 32-bit value into a special int variable.
...
bipush 67 ; Push 8-bit Unicode value for C, which is sign-extended to 32-bit int, onto stack.
iload_1   ; Push 32-bit Unicode value for A onto stack.
isub      ; Subtract 65 (A) from 67 (C). Push 32-bit result onto stack.

The bi in bipush stands for the byte integer type; the i in istore_1, iload_1, and isub stands for the 32-bit integer type. The compiler has converted the expression into an int value. It makes sense to do so because of the close relationship between character literals (really, unsigned Unicode integers) and Java's signed integers.

In addition to the previous widening rules, Java provides a special widening rule for use with String objects (e.g., string literals). When either operand of the string concatenation operator is not a string, that operand is converted to a string before the concatenation operation is performed. For example, when confronted with "X" + 3, the compiler generates code to convert 3 to "3" before performing the concatenation.

Using cast operators for type narrowing

Sometimes, you'll need to deliberately narrow a type where information may be lost. For example, you're drawing a mathematical curve with floating-point coordinates (for accuracy). Because the screen's pixels use integer coordinates, you must convert from floating-point to integer before you can plot a pixel. In Java, we can use cast operators to narrow a type. Cast operators are available to perform the following primitive-type conversions:

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

For example, the (float) cast in float circumference = (float) 3.14159 * 10 * 10; is necessary to convert from double precision floating-point to floating-point.

A cast operator isn't always necessary for the above primitive-type conversions. For example, consider conversion from 32-bit integer to 8-bit byte integer. You don't need to supply a cast operation when assigning a 32-bit integer literal that ranges from -128 to 127 to a variable of byte integer type. For example, you could specify byte b = 100; and the compiler wouldn't complain because no information is lost. (This is why I was previously able to specify short x = 0B0011010101110010;, where the binary literal is of 32-bit integer type, without requiring a (short) cast operator, as in short x = (short) 0B0011010101110010;.) However, if you specified int i = 2; byte b = i;, the compiler would complain because i could contain a value outside the valid range of integers that can be assigned to a byte integer variable.

Example application: Primitive-type conversions

A short application should help to clarify all of this theory. Check out Listing 11's Convert source code.

Listing 11. Primitive-type conversions in Java (Convert.java)


class Convert
{
   public static void main(String[] args)
   {
      float f = 1000;
      System.out.println("f = " + f);
      long l = 5000;
      System.out.println("l = " + l);
      System.out.println("'C' - 'A' = " + ('C' - 'A'));
      char base = 'A';
      System.out.println("'C' - base = " + ('C' - base));
      int i = (int) 2.5;
      System.out.println(i);
      byte b = 25;
      System.out.println(b);
      b = (byte) 130;
      System.out.println(b);
      i = 2;
      b = (byte) i;
      System.out.println(b);
   }
}

Listing 11 reveals a good way to identify variables when outputting their values (for debugging or another purpose). Simply concatenate a variable (of arbitrary type) to a string label, as in "f = " + f.

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


f = 1000.0
l = 5000
'C' - 'A' = 2
'C' - base = 2
2
25
-126
2

Conclusion

Java's support for expressions is extensive, and there is a lot of theory to grasp. I encourage you to experiment with the example applications in this tutorial, and modify them to reinforce what you've learned about using operators to write compound Java expressions. Your practice will come in handy for the next tutorial, which wraps up this series on Java's fundamental language features.

1 2 3 4 Page 4
Page 4 of 4