Поведение компилятора Java во время сужающего примитивного преобразования

К.Сьерра и Б.Бейтс в своей книге "Руководство по изучению SCJP" пишут

"Следующее допустимо byte b = 27;, но только потому, что компилятор автоматически сужает буквальное значение до байта. Другими словами, компилятор вставляет приведение. Предыдущий код идентичен следующему: byte b = (byte) 27;"

На мой взгляд, это объяснение неверно. Эти две строки кода идентичны?

По факту

byte b = 27;

просто константа. И сужение констант во время компиляции — единственная причина, по которой этот код действителен. Так что бросок не нужен. При сужении компилятор просто проверяет, подходит ли указанное значение к типу переменной. В спецификации говорится:

Сужающее примитивное преобразование может использоваться, если переменная имеет тип byte, short или char, а значение константного выражения равно представим в типе переменной.

Во втором случае

byte b = (byte) 27;

кастинг происходит во время выполнения, и значение примитива вычисляется в соответствии с определенными правилами. Компилятор не заботится о совместимости примитивных типов. Например

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

Это заставляет меня думать, что преобразование расширения/сужения и приведение типов принципиально разные. Но в различных источниках они часто используются взаимозаменяемо. Это правильно? Происходит ли приведение под одеялом в случае неявного сужающего преобразования?

Кто-нибудь может объяснить реальное поведение компилятора в ситуации, описанной в приведенной выше книге?


person Andrei Kapelchik    schedule 25.06.2013    source источник
comment
возможный дубликат примитивного приведения и присваивания в Java   -  person Raedwald    schedule 25.06.2013


Ответы (2)


Это достаточно легко проверить. Поместите следующее в Temp.java:

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

Теперь скомпилируйте его с помощью вашего любимого компилятора:

$ javac Temp.java

Теперь сбросьте байт-код с помощью 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
  }

Теперь замените 27 на (byte)27 и запустите снова. Вы увидите, что нет никакой разницы. На самом деле два файла классов будут иметь одинаковую сумму md5sum.

В байт-коде нет приведения во время выполнения, потому что компилятор понял, что это не нужно, и оптимизировал его.

Я полагаю, вы правы в том, что синтаксически строка byte b = 27 отличается от строки byte b = (byte) 27, но они семантически одинаковы, потому что все стандартные компиляторы достаточно умны, чтобы оптимизировать строку в один байткод.

person Adam Bliss    schedule 25.06.2013

Прежде чем мы начнем, важно отметить, что в java все чисто числовые литералы являются значениями int.

Ключевая фраза, касающаяся допустимых неприведенных констант: представляется в типе переменной. Это просто означает, что константа «находится в диапазоне» типа переменной, поэтому:

  • для byte от -128 до 127
  • для short от -32768 до 32767
  • для char от 0 до 65535

Значения «в пределах диапазона» не будут «терять информацию» при приведении к типу переменной. Это объясняет, почему разрешены константы в пределах диапазона.

Для значений вне диапазона требуется явное приведение, поскольку при выполнении приведения информация будет потеряна. Когда выполняется сужающее приведение, биты, не входящие в область действия типа переменной, просто маскируются — вот что в этих случаях означает «потеря информации».

Это как если бы присвоение константы выглядело так:

byte b = nnn & 0xFF;

Если nnn находится в пределах допустимого диапазона, то маска не изменит значение, поэтому ничего не потеряно, так что проблем нет — компилируется нормально.

Если nnn выходит за пределы допустимого диапазона, информация будет потеряна, поэтому для подтверждения потери требуется явное приведение типов.

Если вы помните, что все целочисленные литералы являются целыми числами, правила на самом деле ничем не отличаются от тех, которые применяются к назначению int более узкому типу переменной, за исключением того, что компилятор не допускает приведения, если он знает, что значение будет «подходить»,

person Bohemian♦    schedule 25.06.2013