上篇讲坑,这篇讲 “根”!Java 数据类型底层逻辑全解析

0 阅读6分钟

上篇文章我们介绍了7中基本数据类型的坑,今天让我们来探究下这些坑背后的那些事,知其然知其所以然。

一、坑1:隐式类型转换(小转大)的意外

小范围向大范围转换时,可能因中间计算溢出导致结果错误。

 int a1 = Integer.MAX_VALUE; // 2147483647
 int b1 = a1 + 1; 
 System.out.println("b1="+b1);// 溢出变为 b1=-2147483648(整数溢出回绕)
 long c1 = b1; 
 System.out.println("c1="+c1);// 结果为 c1=-2147483648(错误,因 b 已溢出)

我门来探究下b1为什么等于-2147483648:

1、背景知识

首选 int 是 32(4个字节) 位有符号整数,采用 二进制补码来表示,最高位为符号位,0代表非负数(0和证书),1代表负数。计算公式如下:

image.png

最小值:-2^31 = -2147483648,最大值:2^31 - 1 = 2147483647`

2、二进制表示

Integer.MAX_VALUE = 2147483647 的二进制是:

0111 1111 1111 1111 1111 1111 1111 1111

加 1 后变成:

1000 0000 0000 0000 0000 0000 0000 0000

3、转换为10进制

=-1*2^31+0*2^30+0*2^29+...+0*2^0 
=-1*2^31
=-2^31
=-2147483648

总结:Java 的 int 使用 32 位二进制补码表示,当 2147483647 + 1 超出最大值时,二进制进位使符号位变为 1,结果被解释为补码下的最小负数 -2147483648,即发生“整数溢出回绕”

二、坑2:显式类型转换(大转小)的精度丢失

大范围类型强制转为小范围类型时,直接丢弃高位字节,导致数据失真。

long a2 = 2147483648L; // 超出 int 最大值
int b2 = (int)a2;
System.out.println("b2="+b2);// 结果为 -2147483648(高位丢失,溢出回绕)

1、二进制表示:

背景知识参考坑1,long为8字节,2147483648L在二进制中表示如下:

0000 0000 0000 0000 0000 0000 0000 0000 1000 0000 0000 0000 0000 0000 0000 0000

2、强制类型转换:

long转换为int时,高位截断丢弃变为:

1000 0000 0000 0000 0000 0000 0000 0000

3、转换为十进制:

=-1*2^31+0*2^30+0*2^29+...+0*2^0 
=-1*2^31
=-2^31
=-2147483648

三、坑3:整数运算溢出

int/long 运算结果超出范围时,不会报错,而是按 “模 2^n” 回绕(负数用补码表示)。

int a3 = 1100000000;
int b3 = 1100000000;
int c3 = a3 + b3;
System.out.println("c3="+c3);// 结果为 c3=-2094967296(溢出)

1、转换为二进制:

十进制1100000000转为二进制:

0100 0001 1001 0000 1010 1011 0000 0000
  0100 0001 1001 0000 1010 1011 0000 0000
+
  0100 0001 1001 0000 1010 1011 0000 0000
= 
  1000 0011 0010 0001 0101 0110 0000 0000

3、转为10进制

套用计算公式1000 0011 0010 0001 0101 0110 0000 0000 转换为10进制为-2094967296

四、坑4:浮点数相加,结果不可预测!

double a4 = 0.1;
double b4 = 0.2;
System.out.println(a4 + b4 == 0.3); // false!

问题:为什么0.1和0.2在二进制中无法精确表示(类似 1/3 在十进制中是 0.333...)?

具体分析:

1、背景知识:

10进制小数位转2进制规则如下:

  • 将小数部分不断 × 2;
  • 若结果 ≥ 1 → 记 1,取小数部分继续;
  • 若结果 < 1 → 记 0,用整个结果继续;
  • 重复直到小数部分为 0(有限)或出现循环(无限)。

2、计算过程:

0.1 × 2 = 0.2 → 整数部分:0
0.2 × 2 = 0.4 → 整数部分:0
0.4 × 2 = 0.8 → 整数部分:0
0.8 × 2 = 1.6 → 整数部分:1,小数部分:0.6
0.6 × 2 = 1.2 → 整数部分:1,小数部分:0.2
0.2 × 2 = 0.4 → 整数部分:0 (开始重复)
...

可以看到,从第二步进入循环:0.2 → 0.4 → 0.8 → 0.6 → 0.2...

因此0.1的二进制表示为:

0.0 0011 0011 0011 0011 0011 0011 0011...(无限个0011)

五、坑5:包装类为null 拆箱会报空指针异常!

Integer x = null;
int y = x; //  NullPointerException!

原因分析:

1、背景知识:

java自动装拆箱编译器处理:以int为例,装箱会默认调用valueOf方法,拆箱会调用x.intValue()方法,其他基本数据类型装拆箱类似。

2、原因分析

回到问题:

Integer x = null;

int y = x;
相当于
int y=x.intValue();

我们可以看到,相当于对null值进行方法调用,当然抛空指针异常。

六、坑6:== 比较包装类,结果不可预测!

上篇文章已经讲的比较详细,我们再来回顾下:

Integer a6 = 127;
Integer b6 = 127;
System.out.println(a6 == b6); // true

Integer a66 = 128;
Integer b66 = 128;
System.out.println(a66 == b66); // false

原因分析:Java 对 -128 ~ 127Integer 做了缓存。编译器会在基本类型自动装箱过程调用 valueOf() 方法。

  • 缓存池范围内从缓存中取,返回的是相同对象。
  • 超出范围则每次新建对象,返回的是不同对象。
public static Integer valueOf(int i) {
    //-128 ~ 127均取自IntegerCache,即同一个对象
    if (i >= IntegerCache.low && i <= IntegerCache.high)
        return IntegerCache.cache[i + (-IntegerCache.low)];

    //超出范围,new一个新对象
    return new Integer(i);
}

七、坑7:布尔类型的隐式转换误区

Java 不允许 boolean 与其他基本类型(如 int)相互转换。

boolean a7 = true;
int b7 = (int)a7; // 编译报错(无法强制转换)

这更多的是语言设计理念上的问题,我就不去班门弄斧了。

觉得写的不错的小伙伴 ,点个关注,更新不迷路