Подтвердить что ты не робот

Почему '(int) (char) (byte) -2' производит 65534 в Java?

Я столкнулся с этим вопросом в техническом тесте для работы. Учитывая следующий пример кода:

public class Manager {
    public static void main (String args[]) {
        System.out.println((int) (char) (byte) -2);
    }
}

Он выводит результат как 65534.

Это поведение отображается только для отрицательных значений; 0 и положительные числа дают одно и то же значение, то есть значение, введенное в SOP. Байт, приведенный здесь, незначителен; Я пробовал без него.

Итак, мой вопрос: что именно здесь происходит?

4b9b3361

Ответ 1

Есть некоторые предпосылки, о которых нам нужно договориться, прежде чем вы сможете понять, что здесь происходит. С пониманием следующих пунктов, остальное - простой вывод:

  • Все примитивные типы в JVM представлены как последовательность бит. Тип int представлен 32 битами, типы char и short - 16 бит, а тип byte - 8 бит.

  • Все номера JVM подписаны, где тип char является единственным беззнаковым "числом". Когда число подписано, старший бит используется для обозначения знака этого числа. Для этого наивысшего бита 0 представляет неотрицательное число (положительное или нулевое), а 1 обозначает отрицательное число. Кроме того, с подписанными числами отрицательное значение инвертировано (технически известно как двухкомпонентная нотация) к порядку инкремента положительных чисел. Например, положительное значение byte представлено в битах следующим образом:

    00 00 00 00 => (byte) 0
    00 00 00 01 => (byte) 1
    00 00 00 10 => (byte) 2
    ...
    01 11 11 11 => (byte) Byte.MAX_VALUE
    

    в то время как порядок бит для отрицательных чисел инвертируется:

    11 11 11 11 => (byte) -1
    11 11 11 10 => (byte) -2
    11 11 11 01 => (byte) -3
    ...
    10 00 00 00 => (byte) Byte.MIN_VALUE
    

    Эта инвертированная нотация также объясняет, почему отрицательный диапазон может содержать дополнительное число по сравнению с положительным диапазоном, где последний включает представление числа 0. Помните, что это всего лишь вопрос интерпретации битовой диаграммы. Вы можете отметить отрицательные числа по-разному, но эта инвертированная нотация для отрицательных чисел весьма удобна, потому что она допускает некоторые довольно быстрые преобразования, как мы увидим в небольшом примере позже.

    Как уже упоминалось, это не относится к типу char. Тип char представляет символ Unicode с неотрицательным "числовым диапазоном" от 0 до 65535. Каждое из этих чисел относится к значению 16-бит Unicode.

  • При преобразовании между int, byte, short, char и boolean типам JVM необходимо либо добавить, либо усечь биты.

    Если целевой тип представлен больше бит, чем тип, из которого он был преобразован, тогда JVM просто заполняет дополнительные слоты значением наивысшего бита данного значения (которое представляет подпись):

    |     short   |     byte    |
    |             | 00 00 00 01 | => (byte) 1
    | 00 00 00 00 | 00 00 00 01 | => (short) 1
    

    Благодаря перевернутой нотации эта стратегия также работает для отрицательных чисел:

    |     short   |     byte    |
    |             | 11 11 11 11 | => (byte) -1
    | 11 11 11 11 | 11 11 11 11 | => (short) -1
    

    Таким образом, знак значения сохраняется. Не вдаваясь в подробности реализации этого для JVM, обратите внимание на то, что эта модель позволяет выполнить кастинг с помощью дешевой операции переключения, что очевидно выгодно.

    Исключением из этого правила является расширение типа char, который, как мы уже говорили, беззнаковый. Преобразование из a char всегда применяется путем заполнения дополнительных бит 0, потому что мы сказали, что нет знака и, следовательно, нет необходимости в перевернутом обозначении. Следовательно, преобразование a char в int выполняется как:

    |            int            |    char     |     byte    |
    |                           | 11 11 11 11 | 11 11 11 11 | => (char) \uFFFF
    | 00 00 00 00 | 00 00 00 00 | 11 11 11 11 | 11 11 11 11 | => (int) 65535
    

    Если исходный тип имеет больше бит, чем целевой тип, дополнительные биты просто обрезаются. Пока исходное значение будет соответствовать целевому значению, это работает отлично, например, для следующего преобразования short в byte:

    |     short   |     byte    |
    | 00 00 00 00 | 00 00 00 01 | => (short) 1
    |             | 00 00 00 01 | => (byte) 1
    | 11 11 11 11 | 11 11 11 11 | => (short) -1
    |             | 11 11 11 11 | => (byte) -1
    

    Однако, если значение слишком велико или слишком мало, это больше не работает:

    |     short   |     byte    |
    | 00 00 00 01 | 00 00 00 01 | => (short) 257
    |             | 00 00 00 01 | => (byte) 1
    | 11 11 11 11 | 00 00 00 00 | => (short) -32512
    |             | 00 00 00 00 | => (byte) 0
    

    Вот почему сужение отливок иногда приводит к странным результатам. Вы можете удивиться, почему сужение осуществляется таким образом. Вы могли бы утверждать, что было бы более интуитивно, если бы JVM проверил диапазон чисел и скорее поставил бы несовместимое число самому большому представимому значению того же знака. Однако для этого потребуется ветвление, что является дорогостоящей операцией. Это особенно важно, поскольку эта двухкомпонентная нотация позволяет выполнять дешевые арифметические операции.

Со всей этой информацией мы можем увидеть, что происходит с номером -2 в вашем примере:

|           int           |    char     |     byte    |
| 11 11 11 11 11 11 11 11 | 11 11 11 11 | 11 11 11 10 | => (int) -2
|                         |             | 11 11 11 10 | => (byte) -2
|                         | 11 11 11 11 | 11 11 11 10 | => (char) \uFFFE
| 00 00 00 00 00 00 00 00 | 11 11 11 11 | 11 11 11 10 | => (int) 65534

Как вы можете видеть, приведение byte избыточно, поскольку приведение к char приведет к вырезанию одних и тех же бит.

Все это также заданное JVMS, если вы предпочитаете более формальное определение всех этих правил.

Последнее замечание: размер бит типа не обязательно представляет количество бит, зарезервированных JVM для представления этого типа в его памяти. На самом деле JVM не различает типы boolean, byte, short, char и int. Все они представлены одним и тем же JVM-типом, где виртуальная машина просто эмулирует эти отливки. В стеке операндов метода (т.е. Любой переменной внутри метода) все значения именованных типов потребляют 32 бита. Это, однако, неверно для массивов и полей объектов, которые любой исполнитель JVM может обрабатывать по своему усмотрению.

Ответ 2

Здесь есть две важные вещи,

  • a char не имеет знака и не может быть отрицательным
  • литье байта в char сначала включает скрытое приведение в int по Java Language Spec.

Таким образом, литье -2 в int дает нам 11111111111111111111111111111110. Обратите внимание, что два значения дополнения были расширены знаком с одним; это происходит только при отрицательных значениях. Когда мы затем сужаем его до char, int усекается до

1111111111111110

Наконец, отлитие 1111111111111110 к int битовым расширением с нулем, а не с одним, потому что теперь значение считается положительным (поскольку символы могут быть только положительными). Таким образом, расширение битов оставляет значение неизменным, но в отличие от значения отрицательного значения, неизмененного по значению. И это двоичное значение при печати в десятичном формате равно 65534.

Ответ 3

A char имеет значение от 0 до 65535, поэтому, когда вы нанесли отрицательный результат на char, результат будет таким же, как и вычесть это число из 65536, в результате получится 65534. Если вы напечатали его как char, он попытался бы отобразить любой символ юникода, представленный 65534, но затем, когда вы добавили к int, вы действительно получите 65534. Если вы начали с номера, который был выше 65536, вы бы увидели аналогичные "запутывающие" результаты в котором большое число (например, 65538) закончится малым (2).

Ответ 4

Я думаю, что самый простой способ объяснить это просто состоит в том, чтобы разбить его на порядок операций, которые вы выполняете

Instance | #          int            |     char    | #   byte    |    result   |
Source   | 11 11 11 11 | 11 11 11 11 | 11 11 11 11 | 11 11 11 10 | -2          |
byte     |(11 11 11 11)|(11 11 11 11)|(11 11 11 11)| 11 11 11 10 | -2          |
int      | 11 11 11 11 | 11 11 11 11 | 11 11 11 11 | 11 11 11 10 | -2          |
char     |(00 00 00 00)|(00 00 00 00)| 11 11 11 11 | 11 11 11 10 | 65534       |
int      | 00 00 00 00 | 00 00 00 00 | 11 11 11 11 | 11 11 11 10 | 65534       |
  • Вы просто берете 32-битное значение со знаком.
  • Затем вы конвертируете его в 8-значное знаковое значение.
  • Когда вы пытаетесь преобразовать его в 16-разрядное значение без знака, компилятор прокрадывается в быстрое преобразование в 32-разрядное значение со знаком,
  • Затем преобразование его в 16 бит без сохранения знака.
  • Когда происходит окончательное преобразование в 32 бит, нет знака, поэтому значение добавляет нулевые биты для поддержания значения.

Итак, да, когда вы смотрите на это таким образом, байт бросает значительную (академически говоря), хотя результат незначителен (радость программированию, значительное действие может иметь незначительный эффект). Эффект сужения и расширения при сохранении знака. Где конверсия в char сужается, но не расширяется для подписания.

(Обратите внимание: я использовал символ # для обозначения подписанного бита, и, как отмечено, для char нет знакового бита, поскольку это значение без знака).

Я использовал parens для представления того, что на самом деле происходит внутри. Типы данных фактически транкируются в своих логических блоках, но если смотреть как на int, их результаты будут соответствовать символам Parens.

Подписанные значения всегда расширяются со значением подписанного бита. Без знака всегда расширяется с выключенным битом.

* Таким образом, трюк (или gotchas) для этого заключается в том, что расширение до int из байта, поддерживает расширяемое значение. Которая затем сужается в момент касания char. Затем отключается подписанный бит.

Если преобразование в int не произошло, значение было бы 254. Но это так, так что это не так.