行百里er 行百里er
首页
  • 分类
  • 标签
  • 归档
设计模式
  • JVM
  • Java基础
MySQL
Elastic Stack
Redis
  • Kafka
  • RocketMQ
分布式
Spring Cloud Alibaba
云原生
数据结构与算法
关于
GitHub (opens new window)

行百里er

Java程序员一枚
首页
  • 分类
  • 标签
  • 归档
设计模式
  • JVM
  • Java基础
MySQL
Elastic Stack
Redis
  • Kafka
  • RocketMQ
分布式
Spring Cloud Alibaba
云原生
数据结构与算法
关于
GitHub (opens new window)
  • JVM

  • Java基础

    • 【优雅的避坑】从验证码生成代码的优化到 JVM 栈和堆
    • 【优雅的避坑】HashMap正确的避免扩容带来的额外开销
    • 【优雅的避坑】不要轻易使用==比较两个Integer的值
      • 引入
      • 自动装箱与自动拆箱
      • 比较两个Integer的值
      • 避坑
    • 【优雅的避坑】为什么0.1+0.2不等于0.3了!?
    • 【优雅的避坑】不安全!别再共享SimpleDateFormat了
    • new Object在内存中占多少字节?
    • Java 8之Lambda表达式的写法套路
    • Java 8 Stream API可以怎么玩?
    • Java最强大的技术之一:反射
    • 从一道面试题进入Java并发新机制---JUC
  • Java
  • Java基础
行百里er
2020-09-24
目录

【优雅的避坑】不要轻易使用==比较两个Integer的值

作者:行百里er

博客:https://chendapeng.cn (opens new window)

提示

这里是 行百里er 的博客:行百里者半九十,凡事善始善终,吾将上下而求索!

# 引入

没有前戏,直奔代码,来看下面这段代码的运行结果:

@Test
public void test() {
    Integer i = 666;
    int j = i + 1;
    System.out.println("j = " + j);
}
1
2
3
4
5
6

自然,我们都知道会打印 j = 667,曾经我很好奇,i是Integer对象,属于包装类型,而j是int基础数据类型,他俩怎么会在一起运算呢?直到我扒开Integer的外表,直接看到了他的内涵...

当然我是借助于工具看到程序运行的内涵的,IDEA的jclasslib Bytecode viewer这个插件,能够反编译代码,而且,反编译出来的指令,可以直接链接到官网上查看。

安装好插件后,如图所示,就可以看反编译后的代码指令了:

使用Jclasslib插件查看程序字节码

上面这段程序的字节码指令:

那么这些都是什么玩意呢?

# 自动装箱与自动拆箱

自动装箱(auto boxing)和自动拆箱(auto unboxing)是Java 5引入的功能,有了这两个功能,Java在编译阶段,会根据上下文对数据类型自动进行转换,可以保证不同的写法在运行时等价。

自动装箱:将值类型装换成引用类型的过程

自动拆箱:将引用类型转换成值类型的过程

Integer i = 666;
int j = i + 1;
1
2

这两行代码就是就体现了自动装箱与自动拆箱。

来看一下代码编译后的字节码指令:

 0 sipush 666
 3 invokestatic #2 <java/lang/Integer.valueOf>
 6 astore_1
 7 aload_1
 8 invokevirtual #3 <java/lang/Integer.intValue>
11 iconst_1
12 iadd
13 istore_2
14 return
1
2
3
4
5
6
7
8
9

第3行: invokestatic #2 <java/lang/Integer.valueOf>

意思是调用类的静态方法,后面指出了是调用Integer的valueOf这个静态方法,也就是说在编译阶段Java就自动把装箱转换成了Integer.valueOf;

第8行:invokevirtual #3 <java/lang/Integer.intValue>

调用类实例的方法,这个拆箱就是说在编译阶段就调用了Integer的intValue方法。

分别来看一下valueOf和intValue这两个方法源码。

valueOf:

/**
* 返回表示指定int值的整数实例。如果不需要新的Integer实例,
* 那么通常应该优先使用该方法,而不是构造函数Integer(int),
* 因为通过缓存经常请求的值,该方法可能会产生更好的空间和时间性能。
* 此方法将始终缓存范围为(-128,127]的值,并可能缓存此范围之外的其他值。
*/
public static Integer valueOf(int i) {
    if (i >= IntegerCache.low && i <= IntegerCache.high)
        return IntegerCache.cache[i + (-IntegerCache.low)];
    return new Integer(i);
}
1
2
3
4
5
6
7
8
9
10
11

水越来越深了,这时又跑出来个IntegerCache!

IntegerCache:

/**
 * 缓存支持自动装箱为-128,并根据需要通过JLS 127(含)之间的值的对象标识语义。
 * 缓存是在第一次使用初始化的。
 * 缓存的大小可以通过-XX:AutoBoxCacheMax=<size>选项设置。
 * 在虚拟机初始化的过程中,在系统类sun.misc.VM中设置并保存java.lang.Integer.IntegerCache.high
 **/
private static class IntegerCache {
    static final int low = -128;
    static final int high;
    static final Integer cache[];

    static {
        // high value may be configured by property
        int h = 127;
        String integerCacheHighPropValue =
            sun.misc.VM.getSavedProperty("java.lang.Integer.IntegerCache.high");
        if (integerCacheHighPropValue != null) {
            try {
                int i = parseInt(integerCacheHighPropValue);
                i = Math.max(i, 127);
                // Maximum array size is Integer.MAX_VALUE
                h = Math.min(i, Integer.MAX_VALUE - (-low) -1);
            } catch( NumberFormatException nfe) {
                // If the property cannot be parsed into an int, ignore it.
            }
        }
        high = h;

        cache = new Integer[(high - low) + 1];
        int j = low;
        for(int k = 0; k < cache.length; k++)
            cache[k] = new Integer(j++);

        // range [-128, 127] must be interned (JLS7 5.1.7)
        assert IntegerCache.high >= 127;
    }

    private IntegerCache() {}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39

这就是所谓的Integer缓存,在虚拟机初始化的过程中,就已经缓存好了(-128,127]之间的数据(自动装箱)。

再看一下intValue()方法:

//...
// The value of the {@code Integer}.
int private final int value;
//...

/**
* 以整型数的形式返回该Integer的值
*/
public int intValue() {
    return value;
}
1
2
3
4
5
6
7
8
9
10
11

以整型数的形式返回该Integer的值,对应拆箱。

# 比较两个Integer的值

看代码:

@Test
public void test() {
    Integer i1 = 66;
    Integer i2 = 66;
    System.out.println("66 == 66 ? " + (i1 == i2));
    Integer i3 = 666;
    Integer i4 = 666;
    System.out.println("666 == 666 ? " + (i3 == i4));
}
1
2
3
4
5
6
7
8
9

到这大家肯定都知道一个是true,一个是false:

但是,这是为什么呢?被问到这个丝毫不慌,其实前文已经解释了,Integer里面搞了个IntegerCache这个东西,它默认缓存了(-128,127]之间的数据(可以通过 -XX:AutoBoxCacheMax= 设置),并用数组Integer cache[]保存起来了,也就是说在(-128,127]之间的数值都是IntegerCache.cache[] 数组中的同一个Integer对象。

66在(-128,127]之间,666大于127了,所以i1 == i2为true,而i3 == i4为false。

# 避坑

那么怎么正确的比较两个Integer的值呢?用equals()!

equals:

/**
* 将此对象与指定对象进行比较。
* 当且仅当参数不为null且为包含与此对象相同整型值的整数对象时,结果为真。
*/
public boolean equals(Object obj) {
    if (obj instanceof Integer) {
        return value == ((Integer)obj).intValue();
    }
    return false;
}
1
2
3
4
5
6
7
8
9
10

哈哈,equals方法比较的是两个对象的整型值,不用考虑是基础类型还是引用类型了,一律转换成int类型再进行比较!

这也就是阿里Java开发手册上说的强制使用equals方法比较整型包装类对象的值:


首发公众号 行百里er ,欢迎老铁们关注阅读指正。

#Java
上次更新: 2022/10/04, 18:14:30
【优雅的避坑】HashMap正确的避免扩容带来的额外开销
【优雅的避坑】为什么0.1+0.2不等于0.3了!?

← 【优雅的避坑】HashMap正确的避免扩容带来的额外开销 【优雅的避坑】为什么0.1+0.2不等于0.3了!?→

最近更新
01
重要数据不能丢!MySQL数据库定期备份保驾护航!
05-22
02
分布式事务解决方案之 Seata(二):Seata AT 模式
09-09
03
Seata 番外篇:使用 docker-compose 部署 Seata Server(TC)及 K8S 部署 Seata 高可用
09-05
更多文章>
Theme by Vdoing | Copyright © 2020-2023 行百里er | MIT License | 豫ICP备2022020385号-1
  • 跟随系统
  • 浅色模式
  • 深色模式
  • 阅读模式