为什么 '(int)(char)(byte)-2' 在 Java 中产生 65534?

2022-08-31 13:55:42

我在工作的技术测试中遇到了这个问题。给定以下代码示例:

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

它给出的输出为 65534。

此行为仅对负值显示;0 和正数产生相同的值,表示在 SOP 中输入的值。这里的字节转换是微不足道的;我尝试过没有它。

所以我的问题是:这里到底发生了什么?


答案 1

在你们了解这里发生了什么之前,我们需要商定一些先决条件。通过理解以下要点,剩下的就是简单的推演:

  1. JVM 中的所有基元类型都表示为一个位序列。类型由 32 位表示,和 类型由 16 位表示,类型由 8 位表示。intcharshortbyte

  2. 所有 JVM 编号都有符号,其中类型是唯一无符号的“数字”。当一个数字被符号时,最高位用于表示这个数字的符号。对于此最高位,表示非负数(正数或零)并表示负数。此外,对于有符号数字,负值(技术上称为二的补码表示法)被反转为正数的递增顺序。例如,正值以位表示,如下所示:char01byte

    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

    如前所述,这不适用于该类型。该类型表示一个 Unicode 字符,其非负“数字范围”为 to 。此数字中的每一个都引用一个 16 位 Unicode 值。charchar065535

  3. 在 、 、 和 类型之间转换时,JVM 需要添加或截断位。intbyteshortcharboolean

    如果目标类型由比转换它的类型的位更多的位表示,则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实现此功能的情况下,请注意,此模型允许通过廉价的移位操作执行铸造,这显然是有利的。

    此规则的一个例外是扩大一个类型,正如我们之前所说,它是无符号的。 a 的转换总是通过填充额外的位来应用,因为我们说没有符号,因此不需要倒置符号。因此,将 a 转换为 a 时,将执行以下操作:charchar0charint

    |            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
    

    当原始类型的位数多于目标类型时,仅会切断其他位。只要原始值适合目标值,这就可以正常工作,例如,对于以下将 a 转换为 a 的操作:shortbyte

    |     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

如您所见,强制转换是多余的,因为强制转换会切割相同的位。bytechar

所有这些都由JVMS指定,如果您更喜欢所有这些规则的更正式的定义。

最后一点:类型的位大小不一定表示 JVM 为其内存中表示此类型而保留的位数。事实上,JVM 不区分 、 、 和类型。它们都由相同的JVM类型表示,其中虚拟机仅模拟这些转换。在方法的操作数堆栈(即方法中的任何变量)上,命名类型的所有值都消耗 32 位。然而,对于任何JVM实现者都可以随意处理的数组和对象字段,情况并非如此。booleanbyteshortcharint


答案 2

这里有两件重要的事情需要注意,

  1. 字符是无符号的,不能为负数
  2. 根据 Java 语言规范,首先将字节转换为字符涉及对 int 的隐藏转换。

因此,将 -2 转换为 int 会给我们11111111111111111111111111111110。注意两者的补码值是如何用一个符号扩展的;这只发生在负值。然后,当我们将其缩小到字符时,int 被截断为

1111111111111110

最后,将1111111111111110强制转换为 int 的位扩展为零,而不是 1,因为该值现在被认为是正数(因为字符只能是正数)。因此,加宽位使值保持不变,但与负值情况不同,值保持不变。当以十进制打印时,二进制值是65534。


推荐