面试题系列第3篇:Integer等号判断的内幕,你可能不知道?

简介: 面试题系列第3篇:Integer等号判断的内幕,你可能不知道?

《Java面试题系列》:对面试题中经典内容进行深入挖掘,分析源码、汇总原理,形成公众号系列文章,面试与否均可提升技能。欢迎大家持续关注【程序新视界】。本篇为系列第3篇。


面试过程中关于Integer的比较“==”的问题内容层出不穷,但无论怎么变化,只要了解了其中的底层原理,马上就可以得出答案,再也不用死记硬背考题了。


《阿里巴巴Java开发手册》中有这样一项强制要求:


“所有整形包装类对象之间值的比较,全部使用equals方法比较。说明:对于Integer var= ?在-128到127范围内的赋值,Integer对象在IntegerCache.cache产生,会复用已有对象,这个区间的Integer值可以直接使用==进行判断,但是这个区间之外的所有数据都会在堆上产生,并不会复用已有对象,这是一个大坑,推荐使用equals方法进行判断。”


其实,如果将上面一段话背下来,那么你基本上已经可以答对百分之五十(跟猜的概率差不多)的面试题了。但如果想了解深层原理和剩下的百分之五十的问题,就咱们就继续往下看。


面试题

先来看一道常见的面试题,对照上面的结论,看看能够答对几项。下面代码中打印结果为true的有几项?


@Test
public void test2() {
  Integer i1 = 64;
  int i2 = 64;
  Integer i3 = Integer.valueOf(64);
  Integer i4 = new Integer(64);
  Integer i5 = 256;
  Integer i6 = Integer.valueOf(256);
  System.out.println("A:" + (i1 == i2));
  System.out.println("B:" + (i1 == i3));
  System.out.println("C:" + (i3 == i4));
  System.out.println("D:" + (i2 == i4));
  System.out.println("E:" + (i3.equals(i4)));
  System.out.println("F:" + (i5 == i6));
}

执行上面的程序,打印结果为:

A:true
B:true
C:false
D:true
E:true
F:false

只有C和F项打印为false。你是否疑惑为什么i1等于i2,i1等于i3,i2等于i4,都为true,那么根据等号的传递性,i3应该等于i4啊?


为什么i1和i3相等,但i5和i6却不相等呢?


先保留疑问。下面,我们从int及Integer在JVM中的存储结构来进行分析。掌握了底层存储结构,你会发现无论题面如何变化,都万变不离其宗。


变量在JVM中的存储

在彻底弄清楚上问题之前,我们先来了解一下基础类型变量、引用类型变量在JVM中的存储。


通常变量分为局部变量和全局(成员)变量。局部变量是声明在方法内的变量;全局变量是声明在类中的成员变量。


基础类型的变量和值在分配的时候是在一起的,都在方法区或栈内存或堆内存。而引用类型的变量和值不一定在一起。


局部变量存储在方法栈中

当方法被调用时,Java虚拟机都同步创建一个栈帧,局部变量便存储在其中。当方法结束虚拟机会释放方法栈,其中声明的变量随着栈帧的销毁而结束。因此,局部变量只能在方法中有效。


此过程中,基础类型和引用类型的存储有所区别:


(1)基本类型:变量和对应的值存放在JAVA虚拟机的栈中;


(2)引用类型:变量存储在栈中,是一个内存地址,该地址值指向堆中的对象。


image.png栈属于线程私有的空间,局部变量的生命周期和作用域一般都很短,为了提高gc效率,所以没必要放在堆里面。


全局变量存储在堆中

全局变量存放在堆中,不会随着方法结束而销毁。同样在类中声明的变量也是分为基本类型和引用类型。


(1)基本类型:变量名和值存放在堆内存中。


(2)引用类型:变量是一个引用地址,该地址指向所引用的对象。此时,变量和对象都在堆中。


举个简单的例子,如下代码:


public class Person {

int age = 10;

String name = "Tom";

}

1

2

3

4

对应的age和name的存储结构如下图:


image.png结合上面的理论,我们通过一段代码来分析一下各种类型所存储的位置。

public class DemoTest {
 int y; // 变量和值均在堆上
 public static void main(String[] args) {
     int x = 1; // 变量和值分配在栈上
     String name = new String("cat"); // 数据在堆上,name变量的指针在栈上
     String address = "北京"; // 数据在常量池,属于堆空间,指针在栈上
     Integer price = 4; // 包装类型为引用类型,编译时会自动装拆箱,数据在堆上,指针在栈
 }
}

基础类型的栈内存储

通过上面的实例,基本了解了不同类型的值的内存分配情况。下面我们重点讨论局部变量。


下面先来看看在同一栈帧中,针对int类型的处理模式。


int a = 3;

int b = 3;

1

2

上述代码中a和b均为局部变量。假设编译器先处理int a=3,此时会在栈中创建a的引用变量,然后查找栈中是否存在3这个值,如果没有就将3存放进来,然后将a指向3。


接着处理int b=3,创建完b的引用变量后,同样进行查找。因为在栈中已经有3这个值,便将b直接指向3。


此时,a与b同时指向3这个值,自然是相等的。


关于基础类型与引用类型的底层比较,可稍微延伸一下:对于“==”操作符号,JVM会根据其两边相互比较的操作数的类型,在编译时生成不同的指令:


(1)对于boolean,byte、short、int、long这种整形操作数会生成if_icmpne指令。该指令用于比较整形数值是否相等。


(2)如果操作数是对象的话,编译器则会生成if_acmpne指令,与if_icmpne相比将i(int)改成了a(object reference)。


回归正题

学习了上面的底层理论知识,我们基本上可以得出如下结论:(1)两个int类型比较,直接使用双等号即可;(2)int的包装类Integer对象比较时,使用equals进行比较即可。


但上面的结果只能说E项目是正确的。其比较项还涉及到整形的装箱拆箱操作、Integer的缓存。我们下面逐一分析。


不同创建形式的比较

先看Integer的初始化,根据Integer的内部实现,创建Integer有三种,分别是:


Integer a = new Integer(1); //创建新的类
Integer b = Integer.valueOf(2);  
Integer c = 3; //自动包装,会调用valueOf方法

其中直接赋值底层会调用valueOf方法进行操作的,因此这两种操作效果是一样的。


因为通过new和valueOf创建的是完全两个对象,那么针对题目中的C项,直接比较两个对象的引用肯定是不相等的,因此结果为false。但B项为什么为true呢?后面我们会讲到。


比较中的拆箱

在题目中,我们发现A、D都为true,而且它们的比较格式都是基础类型与包装类型的对比。


针对这种形式的对比,包装类型会进行自动拆箱,变成基础类型(int)。很显然,结果是相等的。


Integer的缓存

为什么i1和i3相等,但i5和i6却不相等呢?对应题目中的B和G项。这里就涉及到Integer的缓存机制。


我们上面已经知道,Integer直接赋值和valueOf是等效的,那先看一下valueOf及相关的方法。


public static Integer valueOf(int i) {
    if (i >= IntegerCache.low && i <= IntegerCache.high)
        return IntegerCache.cache[i + (-IntegerCache.low)];
    return new Integer(i);
}
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() {}
    }

valueOf方法判断数字是否大于low(-128)并且小于high(127),如果满足条件,则直接从IntegerCache中返回对应数字。


IntegerCache用于存储一些常用的数,防止重复创建,在Integer类装入内存时通过静态代码进行初始化。


所以只要是用valueOf或者Integer直接赋值的方式创建的对象,其值小于127且大于-128的,无论对其进行==比较还是equals 比较,都是true。


上面的源码及原理也解释了阿里Java开发手册中所说明的原因。


为什么equals可以规避问题

对于不满足-128到127范围的数,无论通过什么方式创建,都会创建一个新的对象,只能通过equals进行比较。接下来我们再看看equals方法。


public boolean equals(Object obj) {
    if (obj instanceof Integer) {
        return value == ((Integer)obj).intValue();
    }
    return false;
}

equals实现比较简单,先比较类型是否一致,如果不一致,直接返回false;否则,再比较两者的值,相同则返回true。


小结

关于Integer的比较核心点有以下三点:引用对象的存储结构、Integer的缓存机制、自动装箱与拆箱。


Integer在==运算时,总结一下:


(1)如果==两端有一个是基础类型(int),则会发生自动拆箱操作,这时比较的是值。


(2)如果==两端都是包装类型(Integer),则不会自动拆箱,首先会面临缓存问题,即便在缓存范围内的数据还会再次面临创建方式的问题,因此强烈建议使用equals方法进行比较。


如果觉得文章写的还不错,就关注一下。下篇文章,我们来讲讲equals和hashcode方法的重写底层逻辑。



目录
相关文章
|
10天前
|
存储 关系型数据库 分布式数据库
PostgreSQL 18 发布,快来 PolarDB 尝鲜!
PostgreSQL 18 发布,PolarDB for PostgreSQL 全面兼容。新版本支持异步I/O、UUIDv7、虚拟生成列、逻辑复制增强及OAuth认证,显著提升性能与安全。PolarDB-PG 18 支持存算分离架构,融合海量弹性存储与极致计算性能,搭配丰富插件生态,为企业提供高效、稳定、灵活的云数据库解决方案,助力企业数字化转型如虎添翼!
|
9天前
|
存储 人工智能 Java
AI 超级智能体全栈项目阶段二:Prompt 优化技巧与学术分析 AI 应用开发实现上下文联系多轮对话
本文讲解 Prompt 基本概念与 10 个优化技巧,结合学术分析 AI 应用的需求分析、设计方案,介绍 Spring AI 中 ChatClient 及 Advisors 的使用。
401 130
AI 超级智能体全栈项目阶段二:Prompt 优化技巧与学术分析 AI 应用开发实现上下文联系多轮对话
|
3天前
|
存储 安全 前端开发
如何将加密和解密函数应用到实际项目中?
如何将加密和解密函数应用到实际项目中?
197 138
|
9天前
|
人工智能 Java API
AI 超级智能体全栈项目阶段一:AI大模型概述、选型、项目初始化以及基于阿里云灵积模型 Qwen-Plus实现模型接入四种方式(SDK/HTTP/SpringAI/langchain4j)
本文介绍AI大模型的核心概念、分类及开发者学习路径,重点讲解如何选择与接入大模型。项目基于Spring Boot,使用阿里云灵积模型(Qwen-Plus),对比SDK、HTTP、Spring AI和LangChain4j四种接入方式,助力开发者高效构建AI应用。
376 122
AI 超级智能体全栈项目阶段一:AI大模型概述、选型、项目初始化以及基于阿里云灵积模型 Qwen-Plus实现模型接入四种方式(SDK/HTTP/SpringAI/langchain4j)
|
3天前
|
存储 JSON 安全
加密和解密函数的具体实现代码
加密和解密函数的具体实现代码
195 136
|
21天前
|
弹性计算 关系型数据库 微服务
基于 Docker 与 Kubernetes(K3s)的微服务:阿里云生产环境扩容实践
在微服务架构中,如何实现“稳定扩容”与“成本可控”是企业面临的核心挑战。本文结合 Python FastAPI 微服务实战,详解如何基于阿里云基础设施,利用 Docker 封装服务、K3s 实现容器编排,构建生产级微服务架构。内容涵盖容器构建、集群部署、自动扩缩容、可观测性等关键环节,适配阿里云资源特性与服务生态,助力企业打造低成本、高可靠、易扩展的微服务解决方案。
1347 8
|
8天前
|
监控 JavaScript Java
基于大模型技术的反欺诈知识问答系统
随着互联网与金融科技发展,网络欺诈频发,构建高效反欺诈平台成为迫切需求。本文基于Java、Vue.js、Spring Boot与MySQL技术,设计实现集欺诈识别、宣传教育、用户互动于一体的反欺诈系统,提升公众防范意识,助力企业合规与用户权益保护。
|
20天前
|
机器学习/深度学习 人工智能 前端开发
通义DeepResearch全面开源!同步分享可落地的高阶Agent构建方法论
通义研究团队开源发布通义 DeepResearch —— 首个在性能上可与 OpenAI DeepResearch 相媲美、并在多项权威基准测试中取得领先表现的全开源 Web Agent。
1454 87