为什么'(int)(char)(byte)-2'用Java生成65534?

时间:2014-07-08 15:35:32

标签: java casting

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

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

它输出为65534。

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

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

4 个答案:

答案 0 :(得分:130)

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

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

  2. 所有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字符" 065535。这个数字中的每一个都是16-bits 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 operation执行转换,这显然是有利的。

    此规则的一个例外是扩展一个char类型,正如我们之前所说的那样,是无符号的。转换来自 a char始终通过使用0填充其他位来应用,因为我们说没有符号,因此不需要反转符号。因此,charint的转换为:

    |            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允许廉价的算术运算。

  4. 通过所有这些信息,我们可以看到示例中的数字-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不区分booleanbyteshortcharint类型。所有这些都由相同的JVM类型表示,其中虚拟机仅模拟这些类型的铸件。在方法的操作数堆栈(即方法中的任何变量)上,命名类型的所有值都消耗32位。但是,对于任何JVM实现者都可以随意处理的数组和对象字段,情况并非如此。

答案 1 :(得分:35)

这里有两点需要注意,

  1. char未签名,不能为负
  2. 首先将一个字节转换为char,然后根据Java Language Spec对int进行隐藏转换。
  3. 因此将-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       |
  1. 您只是采用32位有符号值。
  2. 然后您将其转换为8位有符号值。
  3. 当您尝试将其转换为16位无符号值时,编译器会快速转换为32位有符号值,
  4. 然后将其转换为16位而不保持符号。
  5. 当最终转换为32位时,没有符号,因此该值会增加零位以保持值。
  6. 所以,是的,当你以这种方式看待它时,字节转换是重要的(从学术上讲),虽然结果是微不足道的(编程的乐趣,重要的行动可能会产生微不足道的影响)。在保持符号的同时缩小和扩大的效果。在哪里,转换为char变窄,但不会扩大到签名。

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

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

    有符号值总是随着有符号位的值而变宽。无符号总是在位关闭的情况下加宽。

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

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