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


70

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

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

输出为65534。

此行为仅显示负值;0和正数产生相同的值,表示在SOP中输入的那个。此处的字节无关紧要;我尝试过没有它。

所以我的问题是:这到底是怎么回事?


事实上,在byte投不改变的结果,并不意味着它没有做任何事情...
纳尔迈

char cast在这里做所有的事情,我不知道什么字节转换是由...决定的,你能告诉我它在做什么吗?
mangoCar 2014年

3
尝试System.out.println((int)(char)(byte)-130)看看它是否只是“ 65536-130”。然后阅读@Chris K答案并解决!:)
Narmer 2014年

哦,不用byte演员就可以重新运行它!
纳默2014年

@Narmer在这里(byte)确实改变了结果,所以情况有所不同。
glglgl 2014年

Answers:


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
    

    负数的位顺序相反:

    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字符,其非负“数值范围”为0to 65535。每个数字都引用一个16位Unicode值。

  3. 当之间进行转换intbyteshortcharboolean类型的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操作执行转换,这显然是有利的。

    如前所述,该规则的一个例外是扩展了一个char无符号类型。由于我们说没有符号,因此也不需要反转符号,因此总是通过用填充附加位来应用a进行的转换。的A转换到因此被执行为:char0charint

    |            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将a转换为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
    

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

    |     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不会区分booleanbyteshortcharint类型。它们全部由相同的JVM类型表示,其中虚拟机仅模拟这些转换。在方法的操作数堆栈(即方法中的任何变量)上,所有已命名类型的值都占用32位。但是,对于任何JVM实现者都可以随意处理的数组和对象字段,情况并非如此。


4
您可以使用指向二进制补码的链接(也在SO上)。IMO的最大优点是您可以通过加法(a - b = a + (-b))进行减法。加法的工作方式与无符号整数完全相同。
Palec 2014年

1
您应该不在上一张表中书写(char) 65534还是(char) 0xFFFE代替(char) 0x65534
FrankPl 2014年

@FrankPI:感谢您的提示,我的意思是编写unicode表示法。我还添加了链接。一般而言,如果您认为有改进,只需编辑我的帖子。
拉斐尔·温特豪德2014年

1
这条线可能有误:00 00 00 00 | => (byte) -1
Ben Voigt 2014年

铸造工作原理的总结。在这些便宜的内存时代,人们忘记了类型大小的真正含义。
Michael Shopsin 2014年

35

这里有两件事要注意:

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

因此,将-2强制转换为int会给我们11111111111111111111111111111111110。只有负值才会发生这种情况。当我们将其缩小为一个char时,int被截断为

1111111111111110

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


为什么将8位转换byte为16位char会产生16位的-2的两个补数,从而解析为65534 int?这都与两个补语有关吗?我的意思是,char演员表中的1填充是如何完成的?
纳默2014年

2
谢谢@Narmer,非常好。我已经更新了答案,并参考了Java语言规范,该规范解释了如何将字节强制转换为char。它通过一个整数。
克里斯·K

是的,您是最有启发性和最明确的答案,应该是该问题的答案。
Narmer 2014年

在这种情况下,所有数字都会出现符号扩展名。碰巧的是,当您有一个正数时,符号位为0。对于负数没有特殊的规则。
个人

@indiv,我对答案进行了修正,以使零和一的位扩展更清晰。
克里斯K

30

Achar的值介于0到65535之间,因此当您将负数转换为char时,结果与从65536中减去该数字相同,结果为65534。如果将其打印为a char,它将尝试显示任何Unicode字符由65534表示,但是当您转换为时,int实际上得到65534。如果您以大于65536的数字开头,您会看到类似的“令人困惑”的结果,其中大的数字(例如65538)最终会变小( 2)。


字符范围不是0-65535吗?
JamesB

您说对了-改变了这一点。减法是从总范围内,这是65536,但是这意味着,高端为65535
雅各布马蒂森

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       |
  1. 您只是采用32位带符号的值。
  2. 然后,您将其转换为8位带符号的值。
  3. 当您尝试将其转换为16位无符号值时,编译器会悄悄地转换为32位有符号值,
  4. 然后将其转换为16bit,而无需保持符号。
  5. 当最终转换为32位时,没有符号,因此该值加零位以保持值。

因此,是的,当您以这种方式看待它时,字节转换是重要的(从学术上来讲),尽管结果是无关紧要的(喜欢编程,重要的动作可能不会产生明显的影响)。在保持符号的同时变窄和变宽的效果。在哪里,到char的转换变窄了,但没有扩大符号。

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

我用括号来表示内部实际发生的事情。数据类型实际上存储在它们的逻辑块中,但是如果以int形式查看,它们的结果将是parens所象征的。

带符号的值总是随着带符号的位的值而变宽。无符号总是随着位的增加而加宽。

*因此,窍门(或陷阱)是从字节扩展到int,在加宽时保持有符号值。然后在触及到字符时就缩小了。然后,这将关闭带符号的位。

如果没有发生向int的转换,则该值为254。但是,确实如此,所以没有。

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.