我在工作的技术测试中遇到了这个问题。给出以下代码示例:
public class Manager {
public static void main (String args[]) {
System.out.println((int) (char) (byte) -2);
}
}
它输出为65534。
此行为仅显示负值; 0和正数产生相同的值,即在SOP中输入的值。这里输入的字节无关紧要;我试过没有它。
所以我的问题是:这到底发生了什么?
答案 0 :(得分:130)
在您了解这里发生的事情之前,我们需要达成一些先决条件。通过理解以下要点,其余的是简单的推论:
JVM中的所有基本类型都表示为位序列。 int
类型由32位表示,char
和short
类型由16位表示,byte
类型由8位表示。
所有JVM号都已签名,其中char
类型是唯一的未签名"号码"。签名后,最高位用于表示此编号的符号。对于此最高位,0
表示非负数(正数或零),1
表示负数。此外,对于带符号的数字,负值是 inverted (技术上称为two's complement notation)到正数的增量顺序。例如,正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-bits 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实现这一点,请注意,此模型允许以廉价的shift operation执行转换,这显然是有利的。
此规则的一个例外是扩展一个char
类型,正如我们之前所说的那样,是无符号的。转换来自 a char
始终通过使用0
填充其他位来应用,因为我们说没有符号,因此不需要反转符号。因此,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检查了数字的范围并且宁愿将不兼容的数字转换为相同符号的最大可表示值,那么它会更直观。但是,这需要branching什么是昂贵的操作。这一点非常重要,因为this two's complement notation允许廉价的算术运算。
通过所有这些信息,我们可以看到示例中的数字-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
的强制转换会削减相同的位。
所有这些也是specified by the JVMS,如果您更喜欢对所有这些规则进行更正式的定义。
最后一句话:类型的位大小不一定代表JVM保留用于在其内存中表示此类型的位数。事实上,JVM不区分boolean
,byte
,short
,char
和int
类型。所有这些都由相同的JVM类型表示,其中虚拟机仅模拟这些类型的铸件。在方法的操作数堆栈(即方法中的任何变量)上,命名类型的所有值都消耗32位。但是,对于任何JVM实现者都可以随意处理的数组和对象字段,情况并非如此。
答案 1 :(得分:35)
这里有两点需要注意,
因此将-2转换为int给出了11111111111111111111111111111110。注意两个补码值是如何用一个符号扩展的;这只发生在负值上。当我们将它缩小为char时,int被截断为
1111111111111110
最后,将1111111111111110转换为int的位是0,而不是1,因为该值现在被认为是正值(因为字符只能为正)。因此,加宽比特会使值保持不变,但与负值值不变的情况不同。以十进制打印时的二进制值为65534。
答案 2 :(得分:30)
char
的值介于0和65535之间,因此当您将否定值转换为char时,结果与从65536中减去该数字相同,结果为65534.如果您将其打印为{{ 1}},它会尝试显示由65534表示的任何unicode字符,但是当你转换为char
时,你实际上得到65534.如果你开始使用的数字高于65536,那么你就是&#d; dd同样看到"令人困惑"结果是一个大数字(例如65538)最终变小(2)。
答案 3 :(得分:6)
我认为最简单的解释方法就是将其分解为您正在执行的操作的顺序
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 |
所以,是的,当你以这种方式看待它时,字节转换是重要的(从学术上讲),虽然结果是微不足道的(编程的乐趣,重要的行动可能会产生微不足道的影响)。在保持符号的同时缩小和扩大的效果。在哪里,转换为char变窄,但不会扩大到签名。
(请注意,我使用#来表示Signed位,如上所述,char没有符号位,因为它是无符号值。)
我用parens来表示内部实际发生的事情。数据类型实际上是在它们的逻辑块中中继,但如果在int中查看,它们的结果将是parens象征的结果。
有符号值总是随着有符号位的值而变宽。无符号总是在位关闭的情况下加宽。
*因此,对此的诀窍(或陷阱)是从byte扩展到int,在扩展时保持有符号值。然后在接触炭的那一刻缩小。然后关闭有符号位。
如果没有发生转换为int,那么该值将为254.但是,确实如此,所以它不是。