神奇的 & 0xff


这段时间在解析 lnk 文件的时候,发现了 & 0xff 这个神奇的东西,百思不得其解,

看完佬的的分析后,这才明白过来,现将个人理解分享出来,以做参考。


我们先来看一段代码:

package temp;

public class Test {

    public static void main(String[] args) {

        byte by = -100;

        System.out.println(by);

        int in = by & 0xff;

        System.out.println(in);

    }

}

打印的结果会是什么呢?

乍一看,by 是8位的二进制数,而 0xff 转化成8位二进制就是1111_1111,那 by & 0xff 不还是 by 本身吗?

但是打印的结果却是:

-100
156

我们来简单分析下原因:

我们知道,整数在计算机中是以二进制补码表示的,8位二进制的补码能表示的区间是 -128 到 +127,

156 显然超过了这个区间,故这里化成 16 位进行比较,经计算可知:

-100 的补码是 1111_1111_1001_1100

156 的补码是 0000_0000_1001_1100

细心的同学的可能注意到了,这俩的补码,后8位都是一样的,而前8位不是全0就是全1

问题之一就出现在这个补码上!

附:

进制转换

原码/反码/补码转换


 

一、十进进制数的表示

 

在 Java 中,0x 开头的数字默认被解释为整数类型(int),而不是 byte 或 short 类型。
这是因为 Java 语言规定了整数字面量的默认类型为 int ,即使它们的值可以适应 byte 或 short 类型的范围。
—— CHAT-GPT AI 原话

正因此,我们可以看到在 IDEA 中,把 0xff 赋值给 byte 变量时会爆红,提示我们强转为 byte 类型或改为 int 类型

在这里插入图片描述

而下面的这段代码,则反映了 0xff 在 Java 中的本质更接近数值 255 而非 8 个 1

package temp;

public class Test2 {

    public static void main(String[] args) {

        byte bff = (byte) 0xff;
        short sff = 0xff;
        int iff = 0xff;
        long lff = 0xff;

        System.out.println(bff);
        System.out.println(sff);
        System.out.println(iff);
        System.out.println(lff);

    }

}

输出结果为:

-1
255
255
255

简单分析下:

二进制中的 1111_1111 对应十进制数字 255

但在计算机中,无论是原码还是补码,采用的都是有符号数,即包含负数

在有符号二进数中,原码、反码、补码为 1111_1111 时,

分别表示(十进制的) -127 、0、-1

因此,可以知道,在 Java 中,0xff 是按照无符号数(或者说是非负数)进行解析的,

而存储的时候是按照(有符号数的)补码形式进行存储的,

System.out.println(Integer.toBinaryString(-0x1));

 

二、& 0xff 的本质

 

通过上面 AI 的话可以知道,0xff 本质上是一个整数,类型为整型(int),当 by 与 0xff 进行运算时,

属于是 byte 类型与 int 类型进行运算,因此会发生隐式类型转换

Java 会先将 by 转为整型,再将其与 0xff 进行运算,

而当 by 为负数时,by 前面会补 24 个 1(正数则是补 0)

8位补码:1001_1100

32位补码:1111_1111_1111_1111_1111_1111_1001_1100

而 0xff 是正数,前面刚好是 24 个 0

8位补码:1111_1111

32位补码:0000_0000_0000_0000_0000_0000_1111_1111

根据与 1 不变,与 0 归 0 的运算性质,by 与上 0xff 后,

前 24 位全部 归 0 ,而后 8 位 保持不变!

现在回过头去看 -100 和 156 的补码是不是很清晰了?


以上就是 & 0xff 运算的本质了,吗?

事实上,刚才的解答是存在一定问题的,解释得并不完全,仔细看还是能发现一些不合理的地方,

就比如:0xff 不是整型吗,为什么这行代码中没有类型强转?整数类型直接转 short 应该会报错才对啊

short sff = 0xff;

又比如:0xff 代表 8 个 1,但是在补码中 1111_1111 表示的明明是 -1 啊,为啥说是 255 呢


前面的 4 个 0xff 的代码其实有一定的误导性,

首先明确,数据总是由精度低到精度高方向转换,这一点是没有问题的,如:

byte n1 = 1;
int n2 = n1;

但是反过来则不行,像下面这样就会报错,提示:java : 不兼容的类型 : 从 int 转换到 byte 可能会有损失

int n1 = 1;
byte n2 = n1;

但其实 1 本身也是默认 int 类型的,为啥 byte n1 = 1 就没有报错呢?

这就涉及到一部分知识的盲区了,那就是字面量类型的自动转换

什么是字面量呢?像 12 、-100、3.14 这些就是字面量,

它是一眼就能看出来的量,是直接表示的量,是固定不变的量,

因此,可以理解为字面量就是常量

而诸如:十六进制数 0XD,八进制数 013,二进制数 0B1101,这些都属于字面量

在范围内是字面量是可以自动转换的,如:

package temp;

public class Test3 {

    public static void main(String[] args) {

        byte bin = -0b111_1111;
        byte oct = -0177;
        byte dec = -127;
        byte hex = -0x7f;

        System.out.println(bin);
        System.out.println(oct);
        System.out.println(dec);
        System.out.println(hex);

    }

}

byte 类型的范围是 -128 到 +127 ,所以 上面这些 都可以存入 byte 中


另外,值得一提的是,Java 中,小数默认是 double 类型,

而 float 类型转 double 类型(可能)会有精度损失(哪怕是字面量)(具体涉及到进制间的小数转换,这里就不多说了)

因此,小数字面量赋值给 float 类型的时候会报错,就像下面这样:

在这里插入图片描述

提示:0.3 在二进制中是表示不完的

小数在线进制转换器


跑题了……

所以,0xff 的本质就是 255 !在 Java 中,它是字面量,是整型(int)

而字面量可以自动转换类型,因此可以自动转换为 short 类型,

但是由于 255 已经超过了 byte 的表示范围,所以不能自动转换为 byte !

但是可以强转,代价就是丢失精度(抹除高位)!

而 255 的补码后 8 位就是 8 个 1,因为正数的三码是一样的,这就造成 0XFF 等价于 1111_1111 的错觉!

但,是不是只有 8 个 1,那就不一定了,这得取决于它是有符号还是无符号

无符号的号的话 8 位就够了,所以就是 8 个 1,

而如果是有符号,第一位是符号位,后面真正用于表示的只有 7 位,所以至少要9位,

而 Java 中都是有符号类型,byte 不足以表示 +255,这也就是为什么 0xff 不可以是 byte 类型


关于 0xff 的本质到底是不是 255,以及字面量会不会自动转换,我们可以测试一下:

package temp;

public class Test4 {

    public static void main(String[] args) {

        byte by = -100;
        
		//byte bt = 255; //报错

        short sh = 255;

        int in = 255;

        long lo = 255;

        System.out.println(by & sh);

        System.out.println(by & in);

        System.out.println(by & lo);

        int bin = 0B1111_1111;

        int oct = 0377;

        int hex = 0XFF;

        System.out.println(by & bin);

        System.out.println(by & oct);

        System.out.println(by & hex);

    }

}

上面六个输出结果都是 156

到这里,终于可以正式下结论:

0xFF 的本质就是 255
0xFF 是字面量,在 Java 中是 int 类型,二进制码为 0000_0000_0000_0000_0000_0000_1111_1111
0xFF & byte 变量,在 Java 中,会先将 byte 变量转 int 类型再运算,结果就是原 byte 变量的前 24 位归 0 ,后 8 位不变


 

三、保证补码的一致性

 

到这其实没必要写了,也累了,但既然标题已经开了,就简单说下这个的用途吧。

之前已经说了,整数在计算机中都是以补码的形式存储的,

当一个整数在进行向上转型时(短转长),正数补 0 ,负数补 1,

从无符号数的角度看,补 0 是不影响的,如 123 和 00123 是一样的

而补 1 则不一样,这个数已经发生变化了,如 123 和 11123 、22123、33123 都是不一样的

因此,可以通过 &0xff 可以将高的 24 位置为 0,低的 8 位保持原样,&0xffff 则是高 16 位置 0 ,低 16 位不变


这其实并不是补码的锅,而是符号数的影响,而补码甚至还把这一过程简化了,

如果是原码的话,负数的向上转型后,符号位会移动到最高位,相当于从第二位开始补 0,

而补码和反码,都是从头(第一位)开始补 1,

但其实无论那种,从无符号的角度,数据都发生了变化,

原码相当于 123 变成了 100123,补码和反码相当于 123 变成了 999123,


在 Java 等诸多语言中,都没有无符号类型,只能以有符号的代替,

而有符号数的拓展,会导致其对应的无符号数改变,因为这就是有符号数的特性!(除非符号位放在最后)

因此,如果使用有符号数代替无符号数,那么在进行位数扩展时,需要添加 &0xff 这样的运算来保持原样


下面以我们以一个 byte 类型(有符号)转 dword 类型(无符号)的方法为例稍作讲解:

	private static int bytesToWord(byte byte1, byte byte2) {
		
		return ((byte2 & 0xff) << 8) | (byte1 & 0xff);
		
	}

在这个方法中,左移操作(<<)和 位或操作(|)都会将 byte 类型向上转位 int 类型(Java 特性),

现在我们知道:在进行有符号转无符号数的拓展时,需要将高位置 0

所以这里都加了 & 0xff(位与也会转型,故右边也需要加)

然后将第二个 byte 左移 8 位,相当于是把第二数作为高位

移位操作会补 0 ,所以不能使用位与运算(&)而应该使用位或运算(|)(使用位与的话就全部归 0 了)


至于小标题说保持补码的一致,倒不如说是保持原始字节(或比特)的一致

同样一段比特,如 1111_1111 作为无符号数,就可以认为是 255,因为他们的映射关系的是简单而直接的,

而若作为有符号数,在原码里是 -127,在反码里是0(负0),在补码里是 -1

但是它本身就是 8 个 1 呀,是们是希望它表示的东西不要变,还是希望它自身不要变呢?

就是 0xff 的所做的(保持自身不变)

但往广了说,终究是保持它的意义不变,如果它不被赋予(其所代表的)意义,那也就没有了变换的必要

只不过,1111_1111 与 255 与的这种对应更加自然而纯粹,我们会自然地觉得它们是绑定在一起的


到这里,& 0xff 的原理终于算是讲完了。没想到这么一点东西,却花了我一个整天时间。

不过,现在终于可以好好睡一觉了。


参考文章:

详解 & 0xff 的作用

byte为什么要与上0xff?

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值