详解java数值类型核心知识点
# 写在文章开头
这篇文章是笔者将早期的一篇稿子进行重新梳理总结的,涉及到数值类型中方方面面的知识点,希望对你有帮助。

Hi,我是 sharkChili ,是个不断在硬核技术上作死的技术人,是 CSDN的博客专家 ,也是开源项目 Java Guide 的维护者之一,熟悉 Java 也会一点 Go ,偶尔也会在 C源码 边缘徘徊。写过很多有意思的技术博客,也还在研究并输出技术的路上,希望我的文章对你有帮助,非常欢迎你关注我的公众号: 写代码的SharkChili 。
同时也非常欢迎你star我的开源项目mini-redis:https://github.com/shark-ctrl/mini-redis (opens new window)
因为近期收到很多读者的私信,所以也专门创建了一个交流群,感兴趣的读者可以通过上方的公众号获取笔者的联系方式完成好友添加,点击备注 “加群” 即可和笔者和笔者的朋友们进行深入交流。

# 详解计算机中进制转换的概念
# 十进制转二进制
在计算机中数据都是以2进制存储的,但为了更方便的表述数据,又出现了八进制、十六进制。
我们先来说说十进制如何转二进制,我们已十进制6转为二进制的运算为例,计算过程为:
- 首先是6除以2,得数3,余数0,2继续向下运算。
- 然后3除以2,得数1,余数也是1,得数1继续向下运算。
- 最后是1除以2,得数0,余数1,因为得数为0所以,停止运算。
最终结果自下向上拼接,得110,具体过程参见下图。

# 二进制转十进制
我们还是以上文得到的二进制110,来演示2进制转为10进制的过程:
- 高位
1*2的2次方。 - 中间次方递减,
1*2的1次方。 - 最后
0*2的0次方。 - 3者相加,得出6。
输出结果如下:

# 其他进制间的转换
我们以2进制转为16进制为例,我们都知道16进制可以表示1-16之间的数据,而2进制4位才能表示16以内的数字。所以以二进制01011010为例,以4位为代表一个16进制。所以我们会将数字切割为:0101 1010。
随后0101转为10进制即5,而1010转为10用16进制表示即A。最终结果为5A。

同理2进制转8进制也是一样的,2进制的3位看作一个8进制数。 而任何进制转2进制也比较容易,只需转为10进制再转2进制即可,这里就不多做赘述了。
# 负数的二进制表现形式
我们还是以2进制6为例,它的2进制表示如下:

为了得到-6的2进制的表示形式,我们首先将6的2进制取反。最后再加1,所以-6的2进制表示如下,这就是为什么正数高位为0,负数高位为1的原因。

# 详解java基本类型相关知识点
# 基本类型和包装类型的区别
从默认值的角度来说:包装类型成员变量默认为null,成员变量默认为该类型默认值,例如int成员变量默认为0。
private static Integer integer;
private static int number;
public static void main(String[] args) {
System.out.println(integer);//null
System.out.println(number);//0
}
2
3
4
5
6
7
从内存分配的角度来说:基本类型的局部变量会被存放在虚拟机栈的局部变量表中,而非static的基本类型成员变量都是存放在堆中。除非包装类型在HotSpot 逃逸分析发现并没有逃逸的外部时会避免分配在堆上,其他情况都会分配上堆区。
从使用范围角度:包装类型可以作为泛型,基本类型不可作为泛型。
# 包装类型的缓存机制
Java会对4种基本整数类型(short、 int、 long、 byte)设置缓存数据,如下Integer 源码所示,可以看到我们尝试使用valueOf生成整型包装类时,如果传入的数值在IntegerCache以内则会从缓存中返回,反之则是创建一个全新的整型包装类对象。
public static Integer valueOf(int i) {
//如果在缓存范围以内,则
if (i >= IntegerCache.low && i <= IntegerCache.high)
return IntegerCache.cache[i + (-IntegerCache.low)];
return new Integer(i);
}
2
3
4
5
6
所以,如果我们在缓存范围内用valueOf,用==比较是会返回true的。
public static void main(String[] args) {
Integer i = Integer.valueOf(1);
Integer i2 = Integer.valueOf(1);
System.out.println(i == i2);
}
2
3
4
5
输出结果:
true
一旦超过整型包装类的缓存范围(默认为127),因为生成的对象并非来缓存,所以用==比较结果为false。
public static void main(String[] args) {
Integer i = Integer.valueOf(128);
Integer i2 = Integer.valueOf(128);
System.out.println(i == i2);
}
2
3
4
5
输出结果:
false
所以,一般情况下,我们建议包装类的比较一律使用equals,从下面的源码我们就可以看出,包装类的equals被重写,比较的结果是获取value值进行==比较的。
public boolean equals(Object obj) {
//判断类型是否一致,若一致则拆箱进行比对
if (obj instanceof Integer) {
return value == ((Integer)obj).intValue();
}
return false;
}
2
3
4
5
6
7
修改后的代码如下:
public static void main(String[] args) {
Integer i = Integer.valueOf(128);
Integer i2 = Integer.valueOf(128);
System.out.println(i.equals(i2));
}
2
3
4
5
输出结果也会true了:
true
注意:float和double没有实现缓存机制,原因也很简单,对于小数而言即使是一个很小的区间,要保存的小数也有很多,如果为每一个小数都设置缓存,会占用大量内存空间,所以设计者就没有考虑对这些高精度小数做缓存:
Double d = 1.2;
Double d2 = 1.2;
System.out.println(d == d2);//false
System.out.println(d.equals(d2));//true
2
3
4
# 自动装箱和自动拆箱
如下所示,将基本类型赋值给包装类型会触发自动装箱,调用valueOf返回一个对象实例。而自动拆箱则是调用xxxValue将包装类的值取出赋值给基本类型。
下面这段代码就是自动装箱:
Integer i = 10;
查看字节码,可以看到装箱操作相当于调用valueOf从返回一个对象实例。
INVOKESTATIC java/lang/Integer.valueOf (I)Ljava/lang/Integer;
2
拆箱代码如下所示:
Integer i = 10;
int i2 = i;
2
自动拆箱,查看字节码相当于调用了intValue,这一点我们查看字节码文件也可以看出:
INVOKEVIRTUAL java/lang/Integer.intValue ()I
2
# 包装类的比较
如下代码i是通过装箱通过valueOf从缓存中取得,而i2则是自己从堆区创建的一个对象,所以两者返回false,要想比较数值必须使用equals
@Test
public void integerQuestion() {
Integer i = 10;//相当于Integer.valueOf(10) 从缓存中拿值
Integer i2 = new Integer(10); // 创建一个新的对象,所以两者值不相等
System.out.println(i == i2);//false
System.out.println(i.equals(i2));//true
}
2
3
4
5
6
7
这个比较器也是同理,因为new Integer做的是在堆区创建一个对象,==比较的是两个引用的地址,所以返回1
@Test
public void BoxedCompareTest() {
Comparator<Integer> naturalOrder = (i, j) -> (i < j) ? -1 : (i == j ? 0 : 1);
int compare = naturalOrder.compare(new Integer(12), new Integer(12));
System.out.println(compare);//返回1 因为integer new出来的对象比较的是地址值
}
2
3
4
5
6
正确做法是将传入的Integer对象进行拆箱进行比较
/**
* 正确比较包装对象做法是进行手动拆箱
*/
@Test
public void BoxedCompareTest2() {
Comparator<Integer> naturalOrder = (i, j) -> {
int compaer1 = Integer.valueOf(i);
int compaer2 = Integer.valueOf(j);
return (compaer1 < compaer2) ? -1 : (compaer1 == compaer2 ? 0 : 1);
};
int compare = naturalOrder.compare(new Integer(12), new Integer(12));
System.out.println(compare);//返回1 因为integer new出来的对象比较的是地址值
}
2
3
4
5
6
7
8
9
10
11
12
13
# 包装类使用时可能会出现的性能问题
使用包装类和基本类型运算会会导致频繁拆装箱,通过查看字节码,我们可以看到第一段 sum += i;实际上会进行INVOKESTATIC java/lang/Long.valueOf (J)Ljava/lang/Long;进行装箱操作。所以我们应该避免这种情况,在进行大量的数值计算时尽量使用基本类型
/**
* 频繁拆装箱会导致性能问题
*/
@Test
public void boxedSum() {
long start = System.currentTimeMillis();
Long sum = 0l;
for (int i = 0; i < Integer.MAX_VALUE; i++) {
//会导致频繁装箱
// INVOKESTATIC java/lang/Long.valueOf (J)Ljava/lang/Long;
sum += i;
}
long end = System.currentTimeMillis();
System.out.println(end - start);//4723
long sum1 = 0l;
start = System.currentTimeMillis();
for (int i = 0; i < Integer.MAX_VALUE; i++) {
sum1 += i;
}
end = System.currentTimeMillis();
System.out.println(end - start);//519
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
# 空指针问题
由于自动拆装箱机制,下面这段代码会进行自动拆箱,unbelievable 会调用intValue转为整数再和12进行比较,而导致空指针异常:
public class Unbelievable {
static Integer unbelievable;
public static void main(String[] args) {
// 包装类型会进行自动拆箱 调用valueOf
// INVOKEVIRTUAL java/lang/Integer.intValue ()I
// 所以很可能导致空指针
if (unbelievable == 12) {
System.out.println("Unbelievable");
}
}
}
2
3
4
5
6
7
8
9
10
11
12
13
# Java中数据精度丢失的问题
可以看到进行+2操作时候报错了,因为byte+int类型时会自动转为高精度的类型,即int,而byte无法容纳int的数字,所以报错了。

那为何byte b=3可以通过呢?在进行赋值操作时,编译器会检查赋的值的取值范围是否在数据类型以内,题目中值的范围符合要求。
所以如果我们要实现加整型的解决方案就只能进行强转。
byte b=(byte)(b+2);
# 浮点数运算错误
如下代码所示,计算机使用二进制表示小数时可能会出现计算循环,由于精度截断很可能导致计算结果错误
/**
* 小数精度丢失原因
* 0.2 转换为二进制数的过程为,不断乘以 2,直到不存在小数为止,
* 在这个计算过程中,得到的整数部分从上到下排列就是二进制的结果。
* 0.2 * 2 = 0.4 -> 0
* 0.4 * 2 = 0.8 -> 0
* 0.8 * 2 = 1.6 -> 1
* 0.6 * 2 = 1.2 -> 1
* 0.2 * 2 = 0.4 -> 0(发生循环)
* ...
*/
float result1 = 3.0f - 2.9f;
float result2 = 2.9f - 2.8f;
System.out.println(result1);
System.out.println(result2);
System.out.println(result1 == result2);
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
# 如何解决浮点数精度丢失问题
对于高精度运算,JDK已经为我们提供了一个不错的计算工具BigDecimal :
BigDecimal num1 = new BigDecimal("3.0");
BigDecimal num2 = new BigDecimal("2.9");
BigDecimal num3 = new BigDecimal("2.8");
//减法
BigDecimal result1 = num1.subtract(num2);
BigDecimal result2 = num2.subtract(num3);
System.out.println(result1);//0.1
System.out.println(result2);//0.1
System.out.println(result1.equals(result2));//true
2
3
4
5
6
7
8
9
10
11
# 超过long类型的整数如何标识
如下所示,使用常规类型计算超过long的值会造成计算错误,所以我们可以使用bigInteger解决问题
long maxVal = Long.MAX_VALUE;
System.out.println(maxVal);
/**
* 加1后高位变1成为负数
*/
long maxValAdd = maxVal + 1;
System.out.println(maxValAdd);
System.out.println(maxValAdd == Long.MIN_VALUE);
//超过long类型的计算方式
BigInteger bigInteger = new BigInteger("9223372036854775807");
System.out.println(bigInteger.add(new BigInteger("1")));
2
3
4
5
6
7
8
9
10
11
12
13
# 高性能精度运算技巧
我们来看一个例子,代码如下所示,我们想看看一块钱可以把多少块糖果,注意糖果每次循环结果会涨0.1元。
首先我们使用常规double类型运算:
double funds = 1.00;
int itemCount = 0;
for (double price = 0.10; funds >= price; price += 0.10) {
System.out.println("funds:" + funds + " price:" + price);
funds -= price;
itemCount++;
System.out.println("remain:" + funds + " itemCount:" + itemCount);
2
3
4
5
6
7
8
9
输出结果,可以看到精度计算异常了
funds:1.0 price:0.1
remain:0.9 itemCount:1
funds:0.9 price:0.2
remain:0.7 itemCount:2
funds:0.7 price:0.30000000000000004
remain:0.3999999999999999 itemCount:3
2
3
4
5
6
7
我们将计算方式修改为使用BigDecimal,虽然可以解决问题,但是性能差、操作不便:
final BigDecimal TEN_CENT = new BigDecimal("0.10");
BigDecimal funds = new BigDecimal("1.00");
int itemCount = 0;
for (BigDecimal price = TEN_CENT;
funds.compareTo(price) >= 0;
price = price.add(new BigDecimal("0.10"))) {
System.out.println("funds:" + funds + " price:" + price);
funds = funds.subtract(price);
itemCount++;
System.out.println("remain:" + funds + " itemCount:" + itemCount);
}
2
3
4
5
6
7
8
9
10
11
12
我们比较推荐结合业务,小数变为整数进行计算,代码如下所示,我们完全可以结合业务场景决定将这段代码转为整数完成计算,如下所示,笔者将所有数值都乘10,这样一来所有计算都是整数计算,从而避免BigDecimal 运算的开销:
int funds = 100;
int itemCount = 0;
for (double price = 10; funds >= price; price += 10) {
System.out.println("funds:" + funds + " price:" + price);
funds -= price;
itemCount++;
System.out.println("remain:" + funds + " itemCount:" + itemCount);
}
2
3
4
5
6
7
8
9
10
11
# 小结
自此我们将java常见数值类型的知识点进行体系化的梳理和总结,希望对你有帮助。
我是 sharkchili ,CSDN Java 领域博客专家,mini-redis的作者,我想写一些有意思的东西,希望对你有帮助,如果你想实时收到我写的硬核的文章也欢迎你关注我的公众号: 写代码的SharkChili 。
同时也非常欢迎你star我的开源项目mini-redis:https://github.com/shark-ctrl/mini-redis (opens new window)
因为近期收到很多读者的私信,所以也专门创建了一个交流群,感兴趣的读者可以通过上方的公众号获取笔者的联系方式完成好友添加,点击备注 “加群” 即可和笔者和笔者的朋友们进行深入交流。

# 参考
Java transient关键字:https://cloud.tencent.com/developer/article/1179627#:~:text=transien,么情况下会失效。 (opens new window)
java中native的用法:https://www.cnblogs.com/b3051/p/7484501.html (opens new window)
Java基础常见面试题总结(上):https://javaguide.cn/java/basis/java-basic-questions-01.html#基本语法 (opens new window)
Java系统性能优化实战:https://book.douban.com/subject/34879022/#:~:text=《Java系统性,能Java系统。 (opens new window)
Effective Java中文版(第3版):https://book.douban.com/subject/30412517/ (opens new window)
面渣逆袭(Java基础篇面试题八股文):https://tobebetterjavaer.com/sidebar/sanfene/javase.html#_7-java-有哪些数据类型 (opens new window)