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

我在技术testing中遇到了这个问题。 给出以下代码示例:

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

它给出的输出为65534。

此行为仅显示负值; 0和正数产生相同的值,即在SOP中input的值。 这里投的字节不重要, 我没有尝试过

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

在您能够理解这里发生的事情之前,我们需要达成一些先决条件。 了解以下的要点,其余的是简单的推论:

  1. JVM中的所有基本types均表示为一系列位。 inttypes由32位表示, charshorttypes由16位表示, bytetypes由8位表示。

  2. 所有JVM编号都是有符号的, chartypes是唯一的无符号“编号”。 当一个数字被签名时, 最高位用来表示这个数字的符号。 对于这个最高位, 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的表示。 请记住,这一切只是解释一个位模式的问题。 你可以用不同的方式记下负数,但这个负数的倒数符号非常方便,因为它允许一些相当快的变换,因为稍后我们可以看到一个小例子。

    如前所述,这不适用于chartypes。 chartypes表示一个非负的“数值范围”为065535的Unicode字符。 每个这个数字都是指一个16位的Unicode值。

  3. intbyteshortcharbooleantypes之间转换时,JVM需要添加或截断比特。

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

    这个规则的一个例外是扩展一个chartypes,正如我们之前所说的那样,它是无符号的。 一个char的转换总是通过填充附加的位来实现的,因为我们说没有符号,因此不需要反转的符号。 一个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 

    当原始types的位数多于目标types时,附加位仅被截断。 只要原始值适合于目标值,就可以正常工作,例如下面的短语转换为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指定的 ,如果你更喜欢所有这些规则的更正式的定义。

最后一点:types的位大小不一定代表JVM在内存中表示这种types所保留的位数。 事实上,JVM并不区分booleanbyteshortcharint型。 所有这些都由相同的JVMtypes表示,虚拟机只是模拟这些铸件。 在方法的操作数堆栈(即方法内的任何variables)上,所有指定types的值都消耗32位。 但是,对于任何JVM实现者可以随意处理的数组和对象字段而言,情况并非如此。

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

  1. char是无符号的,不能是负数
  2. 根据Java语言规范,首先将一个字节转换为一个字符包含隐式转换为一个int。

因此,将-2强制转换为int可以使我们得到11111111111111111111111111111110.注意二进制补码值是如何用一个符号扩展的; 只发生负值。 当我们把它缩小到一个字符时,int就被截断了

 1111111111111110 

最后,将int 1111111111111110强制转换为一个int值,而不是一个值,因为该值现在被认为是正值(因为字符只能是正值)。 因此,扩大比特使得数值保持不变,但与负值不变的情况不同。 以十进制打印时的二进制值是65534。

一个char的值在0到65535之间,所以当你把一个负值转换为char时,结果与从65536中减去该值相同,结果是65534.如果你把它打印为char ,它会尝试显示任何unicode字符由65534表示,但是当你转换为int ,你实际上得到了65534.如果你以65536以上的数字开始,你会看到类似的“混乱”结果,其中一个大数字(例如65538)会结束(2)。

我认为解释这个最简单的方法就是把它分解成你正在执行的操作顺序

 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 | 
  1. 你只需要一个32位有符号值。
  2. 然后您将其转换为8位有符号值。
  3. 当你试图将其转换为一个16位的无符号值时,编译器偷偷转换成32位有符号值,
  4. 然后将其转换为16位而不保留标志。
  5. 当最终转换为32位时,没有符号,所以该值增加零位来保持值。

所以,是的,当你这样看的时候,字节的转换是非常重要的(学术上讲),虽然结果是微不足道的(编程的乐趣,一个重要的行动可以有一个微不足道的效果)。 在保持标志的同时缩小和扩大的效果。 在哪里,转换为字符变窄,但不扩大标志。

(请注意,我使用了#表示签名位,并且如前所述,char没有签名位,因为它是无符号值)。

我用parens来表示内部实际发生的事情。 数据types实际上是在它们的逻辑块中被中继的,但是如果在int中查看,它们的结果将是parens象征的。

签名值总是随签名位的值而变宽。 无符号总是随着位closures而变宽。

*所以,这个技巧(或陷阱)是,从字节扩展到int,在加宽时保持有符号值。 然后,当它接触到字符时,它就会变窄。 然后closures有符号的位。

如果转换为int没有发生,价值将会是254.但是,它确实如此,事实并非如此。