内容简介:在我们学习源码的时候,能经常见到类似于这种操作的场景:前文已经说过,当整型从较窄类型向较宽的类型进行扩展时,除了char类型,都将采用符号扩展:如果原数值是正数,则高位补0;如果原数值是负数,则高位补1;
问题一:让人疑惑的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
小结:
-
整型从窄到宽的扩展中,补符号位,可以保证十进制数据不变;而补符号位,可以保证补码的一致性,也就是二进制数据的一致性,但十进制有可能是会变化的;
-
一般情况下,我们使用
b & 0xff
就是为了保持二进制数据的一致性,说白了就是对低8位数据的复制(可能不是8位); -
很多情况下,我们使用
b & 0xff
的时候会配合逻辑或|
运算符,达到字节拼接的效果;并且也会经常与移位运算符>> <<
等一起使用;
问题二: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之间,其实就相当于对位移数执行
& 0x3f
操作(也就是执行& 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) { // ... }
很显然,我们声明 i
为 NaN
即可。
有关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<=j
和 j<= i
,并且还要 i != j
,对普通的整数来说,看着好像是无解的呢?
解惑5.2:
对一般的常数来说,这的确是的,但不要忘记了Java中还有自动装箱与自动拆箱呢,当比较的对象是包装类的时候,那么 =
操作比较的就不一定是数值了,我们可以声明如下:
Integer i = new Integer(0); Integer j = new Integer(0);
前两个表达式 i <= j
和 j <= 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_VALUE
和Long.MIN_VALUE
取负值不会改变它的值;但对Short.MIN_VALUE
和Byte.MIN_VALUE
则需要取负值后将所产生的int数值再转回short/byte,返回的同样是最开始的值;
5.4 循环4
同样还是循环,我们该如何声明,能够让下面的循环变为一个无限循环:
while (i != 0) { i >>>= 1; }
无符号右移操作,右移的过程中,左侧都是补0;这个看起来有些麻烦,我们来直接看下吧。
解惑5.4:
为了使这里的位移操作合法,这里的 i
必须是一个整数类型。前面有关复合操作符的操作我们了解到: 复合操作符可能会自动的执行窄化原生类型转换
。而依据这个特性,我们可以通过下面的方式实现:
short i = -1;
来简单梳理下实现流程:
-
在执行移位操作的时候,首先就会将
i
提升为int类型, 所有算术操作都会对short,byte和char类型的操作数执行这样的提升 ,这种操作是通过符号扩展拓宽原生类型,不会有信息丢失(11111111 … 11111111); -
无符号右移1位(01111111 … 11111111),最后这个结果被存回
i
中,这时候将int数值存入到short中,会自动丢弃高16位,这样最终又变回了11111111 11111111
,结果还是-1
,然后我们后面还是执行同样的操作,因此就变为了无限循环了。
到这里,循环的内容就告一段落了。
以上就是本文的全部内容,希望本文的内容对大家的学习或者工作能带来一定的帮助,也希望大家多多支持 码农网
猜你喜欢:- Java 解惑系列(三): 让人疑惑的 0xff
- jvm内部缓存选型?一篇文章为你解答疑惑
- 离职后才搞懂vue项目开发流程中的疑惑点
- 答疑解惑之nginx
- 移动端H5解惑-页面适配(二)
- 移动端H5解惑-概念术语(一)
本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。
算法技术手册(原书第2版)
George T. Heineman、Gary Pollice、Stanley Selkow / 杨晨、曹如进 / 机械工业出版社 / 2017-8-1 / 89.00元
本书使用实际代码而非伪代码来描述算法,并以经验主导支撑数学分析,侧重于应用且规范严谨。本书提供了用多种程序设计语言实现的文档化的实际代码解决方案,还介绍了近40种核心算法,其中包括用于计算点集的Voronoi图的Fortune算法、归并排序、多线程快速排序、AVL平衡二叉树实现以及空间算法。一起来看看 《算法技术手册(原书第2版)》 这本书的介绍吧!