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

man*_*Car 69 java casting

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

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

它输出为65534.

此行为仅显示负值; 0和正数产生相同的值,即在SOP中输入的值.这里输入的字节无关紧要; 我试过没有它.

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

Raf*_*ter 130

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

  1. JVM中的所有基本类型都表示为位序列.的int类型是由32位,所表示charshort类型由16位和byte类型由8位表示.

  2. 所有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
    
    Run Code Online (Sandbox Code Playgroud)

    而负数的位顺序是反转的:

    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
    
    Run Code Online (Sandbox Code Playgroud)

    这种反转符号还解释了为什么负范围可以容纳与正范围相比的额外数字,后者包括数字的表示0.请记住,所有这些只是解释一点模式的问题.你可以用不同的方式记录负数,但这个负数的反转符号非常方便,因为它允许一些相当快速的变换,因为我们稍后可以在一个小例子中看到.

    如上所述,这不适用于该char类型.该char类型表示具有0to 的非负"数字范围"的Unicode字符65535.这个数字中的每一个都指的是16位的Unicode值.

  3. 在.,和类型之间进行转换时int,JVM需要添加或截断位.byteshortcharboolean

    如果目标类型由比转换类型更多的位表示,那么JVM只需使用给定值的最高位(表示签名)的值填充其他插槽:

    |     short   |     byte    |
    |             | 00 00 00 01 | => (byte) 1
    | 00 00 00 00 | 00 00 00 01 | => (short) 1
    
    Run Code Online (Sandbox Code Playgroud)

    由于反转符号,此策略也适用于负数:

    |     short   |     byte    |
    |             | 11 11 11 11 | => (byte) -1
    | 11 11 11 11 | 11 11 11 11 | => (short) -1
    
    Run Code Online (Sandbox Code Playgroud)

    这样,值的符号就会保留.在没有详细说明为JVM实现这一点的情况下,请注意,该模型允许通过廉价的移位操作执行铸造,这显然是有利的.

    这条规则的一个例外是扩大了一种char类型,正如我们之前所说的那样,这种类型是无符号的.始终通过填充附加位来应用来自 a 的转换char,0因为我们说没有符号,因此不需要反转符号.因此,执行a char到a的转换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
    
    Run Code Online (Sandbox Code Playgroud)

    当原始类型具有比目标类型更多的位时,附加位仅被切断.只要原始值适合目标值,这就可以正常工作,例如以下转换short为a 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
    
    Run Code Online (Sandbox Code Playgroud)

    但是,如果值太大太小,则不再起作用:

    |     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
    
    Run Code Online (Sandbox Code Playgroud)

    这就是为什么缩小铸件有时会导致奇怪的结果.您可能想知道为什么缩小是以这种方式实现的.你可以争辩说,如果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
Run Code Online (Sandbox Code Playgroud)

正如你所看到的,byte强制转换是多余的,因为转换为char会削减相同的位.

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

最后一点:类型的位大小不一定代表JVM保留用于在其内存中表示此类型的位数.作为事实上,在JVM不区分之间boolean,byte,short,charint类型.所有这些都由相同的JVM类型表示,其中虚拟机仅模拟这些类型的铸件.在方法的操作数堆栈(即方法中的任何变量)上,命名类型的所有值都消耗32位.但是,对于任何JVM实现者可以随意处理的数组和对象字段,情况并非如此.

  • 您可以使用[两个补码]的链接(http://en.wikipedia.org/wiki/Two%27s_complement)(也在[SO](http://stackoverflow.com/q/1049722/2157640)).最大的优点是IMO你可以通过加法来执行减法(`a - b = a +( - b)`).加法的工作方式与无符号整数完全相同. (4认同)

Chr*_*s K 35

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

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

因此,将-2转换为int给出了11111111111111111111111111111110.注意两个补码值是如何用一个符号扩展的; 这只发生在负值上.当我们将它缩小为char时,int被截断为

1111111111111110
Run Code Online (Sandbox Code Playgroud)

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

  • 谢谢@Narmer,一个很好的观点.我已经通过对Java语言规范的引用更新了答案,该语言规范解释了如何将字节转换为char.它通过int. (2认同)

Jac*_*son 30

A char的值介于0和65535之间,因此当您将否定值转换为char时,结果与从65536中减去该数字相同,结果为65534.如果您将其打印为a char,则会尝试显示任何unicode字符是由65534表示,但是当你转换int为时,你实际得到65534.如果你开始使用一个高于65536的数字,你会看到类似的"混乱"结果,其中一个大数字(例如65538)最终会变小( 2).


Roy*_*ker 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       |
Run Code Online (Sandbox Code Playgroud)
  1. 您只需采用32位有符号值.
  2. 然后,您将其转换为8位有符号值.
  3. 当您尝试将其转换为16位无符号值时,编译器会快速转换为32位有符号值,
  4. 然后将其转换为16位而不保持符号.
  5. 当最终转换为32位时,没有符号,因此该值会增加零位以保持值.

所以,是的,当你以这种方式看待它时,字节转换是重要的(从学术上讲),虽然结果是微不足道的(编程的乐趣,重要的行动可能会产生微不足道的影响).在保持符号的同时缩小和扩大的效果.在哪里,转换为char缩小,但不扩大签名.

(请注意,我使用#来表示Signed位,如上所述,char没有符号位,因为它是无符号值).

我用parens来表示内部实际发生的事情.数据类型实际上是在它们的逻辑块中中继,但如果在int中查看,它们的结果将是parens符号化的结果.

有符号值总是随着有符号位的值而变宽.无符号总是随着位关闭而变宽.

*因此,对此的诀窍(或陷阱)是从byte扩展到int,在扩展时保持有符号值.然后在接触到焦炭的那一刻缩小.然后关闭有符号位.

如果没有发生转换为int,则该值将为254.但是,它确实如此,所以它不是.