29

Java 解惑系列(三): 让人疑惑的 0xff

 4 years ago
source link: https://mp.weixin.qq.com/s/A1zNuJAcjR1Haly71LhHJQ
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

问题一:让人疑惑的0xff

在我们学习源码的时候,能经常见到类似于这种操作的场景: b & 0xff ,因为我们平时不经常与十六进制,或者说不经常与逻辑运算符打交道,所以刚看到的时候,或许不太清楚它的具体实现含义,我们这里先来简单分析一下它的实现,然后再以一个示例来说明它的使用场景。

解惑1:

前文已经说过,当整型从较窄类型向较宽的类型进行扩展时,除了char类型,都将采用符号扩展:

如果原数值是正数,则高位补0;如果原数值是负数,则高位补1;

由于计算机是使用补码来进行二进制操作的。正数的补码等于原码;而负数的补码等于反码+1,这些我们前面也已经说过了。对非负数来说,符号扩展与零扩展都是一样的,而对于负数来说,因为符号位的原因,则就不一样了,所以我们这里的举例也都是用负数来举例。

1.1 符号扩展,零扩展与0xff

这里我们以byte类型的-127扩展为int类型来举例:

byte类型 -127 
原码: 11111111 补码: 10000001

符号扩展为32位int类型:
补码: 11111111 11111111 11111111 10000001
原码: 10000000 00000000 00000000 01111111

最终结果:  -127(int类型)

可以看出,从byte到int类型的扩展,保证了十进制数值的一致性;但如果是采用零扩展呢,我们也来看一下:

byte类型-127 
原码: 11111111 补码: 10000001

零扩展为32位int类型:
补码: 00000000 00000000 00000000 10000001
原码: 00000000 00000000 00000000 10000001

最终结果: 129(int类型)

而通过零扩展的话,能够保证二进制数据的一致性。

看完了符号扩展以及零扩展,这时候我们就来看一下我们最开始说的 b & 0xff

首先, 0xff 对应二进制为: 11111111

byte b = -127;
int c = b & 0xff;

b:     = 11111111 11111111 11111111 10000001
& 0xff = 00000000 00000000 00000000 11111111
result = 00000000 00000000 00000000 10000001

可以看到,针对32位的0xff而言,前24位都是补0,0xff 就相当于执行了零扩展,也就相当于保持了二进制数据的一致性。

这里在进行 & 操作时,会先将byte扩展为32位,再与0xff进行操作。

1.2 为什么要用0xff

为什么要用0xff,也就是为什么要保持二进制数据的一致性呢?

原因有很多,我们都知道,很多时候我们需要将各种流转换为byte数组,然后进行数据通信,再然后再将byte数组转换为其他类型,中间的过程中我们是不关心这个byte数组中的值的十进制数值的,我们关心的就是数据传输中二进制数据的一致性。

所以说在比如将byte转换为int的时候,我们就能经常看到 b & 0xff 这样的操作,这种方式说白了就是保持低八位数据在转换的过程中不变,也就是二进制的一致性。

1.3 如说我们想将一个int类型转换为一个byte数组:

/**
 * int -> byte[]
 * @param i  int
 * @return byte[]
 */
public static byte[] intToByteArray(int i) {
    byte[] bytes = new byte[4];
    // 将int从高位依次到低位放入bytes数组
    bytes[0] = (byte) ((i >> 24) & 0xff);
    bytes[1] = (byte) ((i >> 16) & 0xff);
    bytes[2] = (byte) ((i >> 8) & 0xff);
    bytes[3] = (byte) (i & 0xff);
    return bytes;
}

我们来简单看一下 (i >> 24) & 0xff

i         =  00000001 00000011 00000111 00001111
(i >> 24) =  00000000 00000000 00000000 00000001
& 0xff    =  00000000 00000000 00000000 11111111
result    =  00000000 00000000 00000000 00000001

可以看到,恰好将int的高8位获取到,然后低位截取保存到bytes数组中,剩余操作也是类似;

举一反三,知道了如何将int转换为byte数组,那么要将byte数组再转换为int就比较简单了:

/**
 * byte[] -> int
 * @param bytes byte[]
 * @return int
 */
public static int byteArrayToInt(byte[] bytes) {
    int result = 0;
    int length = bytes.length;
    // 依次左移24位,16位,8位,0位
    for (int i = 0; i < length; i ++) {
        result += (bytes[i] & 0xff) << ((length - 1 - i) * 8);
    }
    return result;
}

或者说,我们采用 | 的方式:

public static int byteArrayToInt2(byte[] bytes) {
    int temp0 =(bytes[0] & 0xff) << 24;
    int temp1 =(bytes[1] & 0xff) << 16;
    int temp2 =(bytes[2] & 0xff) << 8;
    int temp3 =bytes[3] & 0xff;
    return temp0 | temp1 | temp2 | temp3;
}

简单优化:

public static int byteArrayToInt3(byte[] bytes) {
    int temp = 0;
    int length = bytes.length;
    for (int i = 0; i < length; i ++) {
        temp |= ((bytes[i] & 0xff) << (length - 1 - i) * 8);
    }
    return temp;
}

有关 0xff 的使用,这里有一个不错的例子可以参考下:是一个通过 0xff 转换ip地址的过程, Convert Decimal to IP Address, with & 0xFF ,地址为:https://mkyong.com/java/java-and-0xff-example

小结:

  1. 整型从窄到宽的扩展中,补符号位,可以保证十进制数据不变;而补符号位,可以保证补码的一致性,也就是二进制数据的一致性,但十进制有可能是会变化的;

  2. 一般情况下,我们使用 b &amp; 0xff 就是为了保持二进制数据的一致性,说白了就是对低8位数据的复制(可能不是8位);

  3. 很多情况下,我们使用 b &amp; 0xff 的时候会配合逻辑或 | 运算符,达到字节拼接的效果;并且也会经常与移位运算符 &gt;&gt; &lt;&lt; 等一起使用;

问题二:Integer.MAX_VALUE的问题

看下面这个程序,最终将会打印什么呢?

public class Main {
    private static final int END = Integer.MAX_VALUE;
    private static final int START = END - 100;

    public static void main(String[] args) {
        int count = 0;
        for (int i = START; i <= END; i++) {
            count++;
        }
        System.out.println(count);
    }
}

这段程序会打印100,还是会打印101呢?很遗憾,它什么都没有打印,并且这个程序不会停止,将一直进入无限循环。

解惑2:

如果我们仔细看的话,就会发现,这和我们平时所使用的循环有点不太一样,因为一般我们使用循环时,都是在循环索引小于终止值时执行程序,而该程序则是在循环索引小于或等于终止值时执行程序,在这个例子中我们的目的是想让循环在 i=Integer.MAX_VALUE 时终止,但按照流程来说,它会在 i= Integer.MAX_VALUE+1 时终止,但遗憾的是它终止不了,因为:

Integer.MAX_VALUE + 1 = Integer.MIN_VALUE:在Java中,当 i 达到 Integer.MAX_VALUE 的时候,如果再次执行增量操作,那么它又绕回了 Integer.MIN_VALUE

这个例子就告诉我们:

无论你在何时操作整数类型,都要意识到整型的边界问题。

至于解决方式,就比较简单了,我们可以指定一个long类型的循环索引:

for (long i = START; i <= END; i++) {

或者借助于 do while 循环:

public static void main(String[] args) {
    int count = 0;
    int i = START;
    do {
        count ++;
    } while (i++ != END);
    System.out.println(count);
}

问题三:移位操作的问题

同样是循环,来看下下面的代码打印什么?

public class Main {
    public static void main(String[] args) {
        int i = 0;
        while (-1 << i != 0) {
            i++;
        }
        System.out.println(i);
    }
}

因为整数类型的 -1 的32位都是1,并且是左移操作,所以正常来说,这个循环将执行32次迭代之后停止,并且会打印32。很遗憾,这个程序也将进入一个无限循环,并且不会打印任何内容。

解惑3:

问题就在于 -1 << 32 的结果是-1,而不是0。那么为什么会是这样的呢?其实,这个在Java开发规范中有说明,我们直接引用下:

If the promoted type of the left-hand operand is int, then only the five lowest-order bits of the right-hand operand are used as the shift distance. It is as if the right-hand operand were subjected to a bitwise logical AND operator & (§15.22.1) with the mask value 0x1f (0b11111). The shift distance actually used is therefore always in the range 0 to 31, inclusive.
If the promoted type of the left-hand operand is long, then only the six lowest-order bits of the right-hand operand are used as the shift distance. It is as if the right-hand operand were subjected to a bitwise logical AND operator & (§15.22.1) with the mask value 0x3f (0b111111). The shift distance actually used is therefore always in the range 0 to 63, inclusive.

简单梳理下,对于位移操作,就是:

  • 如果左侧操作数的类型为int,则仅将右侧操作数的最低5位用作移位长度;无论右侧位移多少,最终位移的范围都将落在0~31之间,其实就相当于对位移数执行 & 0x1f 操作(也就是执行 & 0b11111 ),其实也就相当于对32取余;而如果恰好是32或者32的倍数,自然就是相当于移位距离是0;

  • 如果左侧操作数的类型是long,则仅将右侧操作数的最低6位用作移位长度;无论右侧位移多少,最终位移的范围都将落在0~64之间,其实就相当于对位移数执行 &amp; 0x3f 操作(也就是执行 &amp; 0b111111 ),其实也就相当于对64取余;

看到这我们也就知道这个问题了,如果试图对一个int类型移位32位,或者对一个long类型移位64位,都值会返回这个数值本身。

没有任何移位长度可以让一个int数值丢弃所有的32位,或者是让一个long数值丢弃所有的64位。

那么这个问题的解决方式也就很简单了。我们不再让-1重复的移位不同的位移长度,而是将前一次移位操作的结果保存起来,并且让它在每一次迭代时都向左再移1位:

public static void main(String[] args) {
    int i = 0;
    for (int val = -1; val != 0; val <<= 1) {
        i++;
    }
    System.out.println(i);
}

还有一点可能也需要注意,就是当位移长度是负数的时候,比如对一个int 右移 -1 位,则是相当于右移了 31 位,无论位移长度是正数还是负数,对int而言都是对32取余,对long而言则是对64取余。

问题四:正无穷大的问题

下面需要我们来动动手写写代码了,首先是看下面的代码,我们该如何声明,能够让下面的循环变为一个无限循环呢?

while (i == i + 1) {
    //...
}

什么样的数字会等于它本身加1呢?正常来说这应该是无法实现的,但如果这个数字是无穷大的话又会怎样呢?

解惑4:

Java中强制要求使用 IEEE754浮点数算术运算,它可以让我们用一个double或者float来表示一个无穷大的数字。正如我们在学校里学过的,无穷大加1还是无穷大。对这个问题而言,如果 i 初始的时候就是无穷大,那么 i+1 将依旧是无穷大,所以循环不会终止,比如:

double i = Double.POSITIVE_INFINITY;

4.1 正无穷,负无穷,非数字

在Java中提供了三个特殊的浮点数值:正无穷大、负无穷大、非数字,用于表示溢出或者其他特殊场景:

  • 正无穷大:用一个正浮点数除以0将得到一个正无穷大,通过Double或Float的POSITIVE_INFINITY表示 ;打印的话,会展示:Infinity

  • 负无穷大:用一个负浮点数除以0将得到一个负无穷大,通过Double或Float的NEGATIVE_INFINITY表示 ;打印的话,会展示:-Infinity

  • 非数字:0.0除以0.0或对一个负数开方将得到一个非数字,通过Double或Float的NaN表示;打印的话,会展示:NaN(含义: Not a Number)

  • 所有的正无穷大的数值都是相等的,所有的负无穷大的数值都是相等;而NaN不与任何数值相等,甚至和NaN自身都不相等;

来看下下面的例子:

public static void main(String[] args) {
    double i = Double.POSITIVE_INFINITY;
    float f = Float.POSITIVE_INFINITY;
    System.out.println(i == f);    // output: true
    System.out.println(i);         // output: Infinity

    i = Double.NEGATIVE_INFINITY;
    f = Float.NEGATIVE_INFINITY;
    System.out.println(i == f);    // output: true
    System.out.println(f);         // output: -Infinity

    i = Double.NaN;
    f = Float.NaN;
    System.out.println(i == f);    // output: false
    System.out.println(f);         // output: NaN
}

当然,不必将 i 初始化为无穷大以确保循环永远执行,任何足够大的浮点数都可以实现这一目的:因为一个浮点数值越大,它和其后继数值之间的间隔就越大;对一个足够大的浮点数加1不会改变它的值,因为1不足以 填补它与其后继者之间的空隙

  • 浮点数操作返回的是最接近其精确数学结果的浮点数值,一旦毗邻的浮点数值之间的距离大于2,那么对其中的一个浮点数值加1将不会产生任何效果,因为其结果没有达到两个数值之间的一半;

  • 对Float类型,加1不会产生任何效果的最小基数是2^25,也就是33554432;而对Double类型,最小基数是2^54,大约是1.8*10^16;

简单看下下面的例子,返回的将是true:

public static void main(String[] args) {
    float i = 123456789F;
    System.out.println(i == i + 1);  // output: true
}

毗邻的浮点数值之间的距离被称为一个 ulp ,它是最小单位(unit in the last place)的首字母缩写词,从JDK5.0之后,引入了 Math.ulp 方法来计算float或者double数值的 ulp

因此,我们需要记住:

  • 用一个float或者double的数值是可以用来表示无穷大的;

  • 将一个很小的的浮点数加到一个很大的浮点数上时,将不会改变大浮点数的值;

4.2 非数字

了解了这些问题,那下面的这个例子就比较简单了。我们该如何声明,能够让下面的循环变为一个无限循环:

while (i != i) {
    // ...
}

很显然,我们声明 iNaN 即可。

有关NaN,我们再多说一点:

首先,前面已经说过,NaN不与任何浮点数相等;其次,任何浮点操作,只要它的一个或多个操作数为NaN,那么其结果都是NaN;

public static void main(String[] args) {
    double i = 0.0 / 0.0;
    System.out.println(i  + 1); // output: NaN
}

最后, Java中有关无穷大,非数字的类型,都是基于 IEEE 754 浮点运算规范,有兴趣的可以去翻下该规范。

问题五:还是循环?

5.1 循环1

接着看下面的例子,和上面的例子类似,我们该如何声明,能够让下面的循环变为一个无限循环,但前提是不能声明为浮点数类型:

while (i != i + 0) {
    //...
}

如果不能用浮点类型,那么有能解决该问题的其他数值类型么?

解惑5.1:

很显然,我们想来想去,不通过浮点型,只通过其他数值类型是没有能解决该问题的;那么针对 + 操作,很自然,我们就能想到String操作,因为String中, + 操作符用于字符串连接,所以我们可以将 i 声明为任何字符串。

通常来说,我们程序中见到的 i 都是被声明为了整型变量名;而上面这种方式很明显不是一种可读性很好的方式;所以我们还是应该按照可读性更高的声明方式来声明变量。

5.2 循环2

还是接着来看循环例子,和上面的类似,我们该如何声明,能够让下面的循环变为一个无限循环:

while (i <= j && j <= i && i != j) {
    // ...
}

对这个例子而言, i<=jj<= i ,并且还要 i != j ,对普通的整数来说,看着好像是无解的呢?

解惑5.2:

对一般的常数来说,这的确是的,但不要忘记了Java中还有自动装箱与自动拆箱呢,当比较的对象是包装类的时候,那么 = 操作比较的就不一定是数值了,我们可以声明如下:

Integer i = new Integer(0);
Integer j = new Integer(0);

前两个表达式 i <= jj <= i ,会将对象拆箱成基本数值进行比较;而 i != j 则是在两个对象引用上进行比较。很显然,为什么编程规范没有规定:当 = 操作符作用于装箱的数值对象时,执行值比较。官方给的答案也很简单:兼容性。因为过去的代码如果这么写就是false的,那么新的规范就必须接着保持这个false。

5.3 循环3

还是接着上面来说,我们该如何声明,能够让下面的循环变为一个无限循环:

while (i != 0 && i == -i) {
    // ...
}

因为这里涉及到一元操作符 - ,也就是说这个 i 必须是数值类型,那么问题来了,除了0,还有哪个整数等于它的负值呢?

解惑5.3:

这时候,我们需要寻找一个非0的数字类型数值,它等于自己的负值。先来看浮点数有没有,正常的浮点数肯定是没有的(浮点数:符号位,尾数,指数),那么来看NaN,正无穷大,负无穷大,同样这些都不满足,那又回到了整数。

对int来说,总共存在个偶数个int数值---准确的来说,是2^32个,其中一个用来表示0,剩下奇数个int数值用来表示正整数和负整数,这意味着正的和负的int数值的数量必然不相等。换句话说,这暗示着至少有一个int数值,其负数不能正确的表示为int数值。

没错,恰好就有一个这样的数值,那就是 Integer.MIN_VALUE ,该值的负值就是它本身;当然,还有 Long.MIN_VALUE ,这两个数值都能满足我们的条件。Java对这两个值取负值将会产生溢出,但是Java在整型计算中忽略了溢出,所以这两个数值才能满足我们的要求:

int i = Integer.MIN_VALUE;
  • java使用二进制的补码的算术运算,是不对称的。对于每一种有符号的整数类型(int,long,byte,short),负的数值总是比正的数值多一个,这个多出来的值总是这种类型所能表示的最小值;

  • Inteeger.MIN_VALUELong.MIN_VALUE 取负值不会改变它的值;但对 Short.MIN_VALUEByte.MIN_VALUE 则需要取负值后将所产生的int数值再转回short/byte,返回的同样是最开始的值;

5.4 循环4

同样还是循环,我们该如何声明,能够让下面的循环变为一个无限循环:

while (i != 0) {
    i >>>= 1;
}

无符号右移操作,右移的过程中,左侧都是补0;这个看起来有些麻烦,我们来直接看下吧。

解惑5.4:

为了使这里的位移操作合法,这里的 i 必须是一个整数类型。前面有关复合操作符的操作我们了解到: 复合操作符可能会自动的执行窄化原生类型转换 。而依据这个特性,我们可以通过下面的方式实现:

short i = -1;

来简单梳理下实现流程:

  1. 在执行移位操作的时候,首先就会将 i 提升为int类型, 所有算术操作都会对short,byte和char类型的操作数执行这样的提升 ,这种操作是通过符号扩展拓宽原生类型,不会有信息丢失(11111111 … 11111111);

  2. 无符号右移1位(01111111 … 11111111),最后这个结果被存回 i 中,这时候将int数值存入到short中,会自动丢弃高16位,这样最终又变回了 11111111 11111111 ,结果还是 -1 ,然后我们后面还是执行同样的操作,因此就变为了无限循环了。

到这里,循环的内容就告一段落了。


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK