javacastingprimitivescjp

Java compiler behaviour during narrowing primitive conversion


K.Sierra and B.Bates in their book "SCJP Study Guide" write

"The following is legal byte b = 27; but only because the compiler automatically narrows the literal value to a byte. In other words, the compiler puts in the cast. The preceding code is identical to the following: byte b = (byte) 27;"

In my opinion this explanation is incorrect. Are these two lines of code identical?

In fact

byte b = 27;

is simply a constant. And the compile-time narrowing of constants is the only reason why this code is valid. So no cast is needed. When narrowing the compiler just checks whether the specified value fits in the type of the variable. The specification says:

A narrowing primitive conversion may be used if the type of the variable is byte, short, or char, and the value of the constant expression is representable in the type of the variable.

In the second case

byte b = (byte) 27;

casting does occur during at runtime and the primitive value is computed according to specific rules. The compiler doesn't care about the compatibility of primitive types. For example

byte b = 5.0; // compile error
byte b = 277777777; // compile error
byte b = (byte) 5.0; // valid!
byte b = (byte) 277777777; // valid!!

This makes me think that widening/narrowing conversion and casting are fundamentally different. But in various sources they are often used interchangeably. Is this correct? Does casting occur under the covers in case of an implicit narrowing conversion?

Can anyone explain the real behavior of the compiler in the situation described in the above book?


Solution

  • It's easy enough to test it out. Put the following in Temp.java:

    class Temp {
      public static void main(String[] argv) {
        byte b = 27;
        System.out.println(b);
      }
    }
    

    Now compile it with your favorite compiler:

    $ javac Temp.java
    

    Now dump the bytecode with javap:

     $ javap -c Temp.class
     Compiled from "Temp.java"
      class Temp {
        Temp();                                                                                                                             
          Code:
             0: aload_0
             1: invokespecial #1                  // Method java/lang/Object."<init>":()V
             4: return
    
        public static void main(java.lang.String[]);                                                                                        
          Code:
             0: bipush        27
             2: istore_1
             3: getstatic     #2                  // Field java/lang/System.out:Ljava/io/PrintStream;                                       
             6: iload_1
             7: invokevirtual #3                  // Method java/io/PrintStream.println:(I)V
            10: return
      }
    

    Now replace 27 with (byte)27 and run again. You'll see there is no difference. In fact the two classfiles will have the same md5sum.

    There is no runtime cast in the bytecode because the compiler figured out it wouldn't be needed, and optimized it away.

    I believe you're correct that syntactically the line byte b = 27 differs from the line byte b = (byte) 27, but they are semantically the same, because all standard compilers are smart enough to optimize the line into a single bytecode.