1.integerԴ?码图?ͼ??
2.画图理解Java Integer的“值传递”
3.第7讲 | int和Integer有什么区别?
4.Integer比较大小用“==”的坑
5.优雅的避坑不要轻易使用==比较两个Integer的值
integerԴ??ͼ??
《Java面试题系列》:深入挖掘面试题中经典内容,剖析源码,码图总结原理,码图形成公众号系列文章,码图不论面试与否,码图均可提升技能。码图阻塞队列原版源码本篇为系列第3篇。码图面试过程中关于Integer的码图比较“==”的问题层出不穷,但了解其底层原理后,码图即可轻松应对。码图根据《阿里巴巴Java开发手册》,码图所有整形包装类对象之间的码图值比较应使用equals方法,对于在-到范围内的码图赋值,Integer对象会在IntegerCache.cache产生并复用,码图这个区间内的码图Integer值可以直接使用“==”进行判断。然而,该区间之外的所有数据会在堆上产生,并不会复用已有对象,这是个陷阱,推荐使用equals方法进行判断。
执行下面的程序,打印结果为true的有几项?
执行程序后,打印结果为:只有C和F项打印为false。读者可能疑惑为什么i1等于i2,i1等于i3,i2等于i4,都为true,根据等号的传递性,i3应该等于i4啊?为什么i1和i3相等,但i5和i6却不相等呢?
在彻底弄清楚问题之前,我们先了解一下基础类型变量、引用类型变量在JVM中的存储。通常变量分为局部变量和全局(成员)变量。局部变量声明在方法内,精美导航源码而全局变量声明在类中。基础类型的变量和值在分配时一起,都在方法区、栈内存或堆内存。而引用类型的变量和值不一定在一起。
局部变量存储在方法栈中,当方法被调用时,Java虚拟机同步创建一个栈帧,局部变量存储其中。方法结束后,栈帧销毁,其中声明的变量也随之结束。因此,局部变量只能在方法中有效。基础类型与引用类型的存储有所不同,基础类型存储在JAVA虚拟机的栈中,引用类型存储在栈中,指向堆中的对象。栈属于线程私有的空间,局部变量的生命周期和作用域一般都很短,为了提高gc效率,所以没必要放在堆里面。
全局变量存储在堆中,不会随着方法结束而销毁。类中声明的变量分为基本类型和引用类型,基本类型存储在堆内存中,引用类型存储在堆中,是一个引用地址,指向所引用的对象。
结合上述理论,我们通过一段代码来分析各种类型所存储的位置。
基础类型的栈内存储,在同一栈帧中,横向统计源码针对int类型的处理模式如下:
假设编译器先处理int a=3,此时会在栈中创建a的引用变量,然后查找栈中是否存在3这个值,如果没有就将3存放进来,然后将a指向3。接着处理int b=3,创建完b的引用变量后,同样进行查找。因为在栈中已经有3这个值,便将b直接指向3。此时,a与b同时指向3这个值,自然是相等的。
对于“==”操作符号,JVM会根据其两边相互比较的操作数的类型,在编译时生成不同的指令。对于boolean、byte、short、int、long这种整形操作数会生成if_icmpne指令,用于比较整形数值是否相等。如果操作数是对象,则生成if_acmpne指令,与if_icmpne相比将i(int)改成了a(object reference)。
学习了底层理论知识,我们得出以下两个int类型比较,直接使用双等号即可;int的包装类Integer对象比较时,使用equals进行比较即可。但结果只能说E项目是正确的,其比较项还涉及到整形的装箱拆箱操作、Integer的缓存。下面逐一分析。龙头疯狂源码
不同创建形式的比较,先看Integer的初始化,有三种创建形式,分别是直接赋值、valueOf方法和new关键字。因为通过new和valueOf创建的是完全两个对象,那么针对题目中的C项,直接比较两个对象的引用肯定是不相等的,因此结果为false。但B项为什么为true呢?后面我们会讲到。
比较中的拆箱,在题目中,我们发现A、D都为true,它们的比较格式都是基础类型与包装类型的对比。针对这种形式的对比,包装类型会进行自动拆箱,变成基础类型(int)。很显然,结果是相等的。
Integer的缓存机制,为什么i1和i3相等,但i5和i6却不相等呢?对应题目中的B和G项。我们已经知道,Integer直接赋值和valueOf是等效的,接下来看一下valueOf及相关的方法。valueOf方法判断数字是否大于low(-)并且小于high(),如果满足条件,则直接从IntegerCache中返回对应数字。IntegerCache用于存储一些常用的数,防止重复创建,在Integer类装入内存时通过静态代码进行初始化。因此,ok源码下载只要是用valueOf或者Integer直接赋值的方式创建的对象,其值小于且大于-的,无论对其进行“==”比较还是equals比较,都是true。
关于Integer的比较核心点有以下三点:引用对象的存储结构、Integer的缓存机制、自动装箱与拆箱。总结Integer在“==”运算时的核心点如下:如果“==”两端有一个是基础类型(int),则会发生自动拆箱操作,这时比较的是值。如果“==”两端都是包装类型(Integer),则不会自动拆箱,首先会面临缓存问题,即便在缓存范围内的数据还会再次面临创建方式的问题,因此强烈建议使用equals方法进行比较。
如果觉得文章写的还不错,就关注一下。下篇文章,我们来讲讲equals和hashcode方法的重写底层逻辑。本文首发来自微信公众号:程序新视界,一个软实力、硬技术同步学习的平台。
画图理解Java Integer的“值传递”
在深入理解Java中"值传递"之前,先看一个简单的Java代码片段,测试能否通过单元测试。答案是否定的,实参 Integer a 的值仍然是。
疑惑点在于方法 change1 的形参 a 接收的是 Integer a 的值还是引用?如果你已经知晓答案,那么本文或许没有必要继续阅读。
结论是:Java中的方法调用都是值传递。对于int类型的局部变量,如int a = ,传递给方法形参的是数值;对于Integer类型的局部变量,如Integer a = ,传递给方法形参的是地址值。Java中并无引用传递。
理解值传递中数值和地址值的差异,需要先理解整数赋值操作的底层机制。整数赋值实际上调用了Integer.valueOf(num)方法。因此,Integer a = 等同于Integer a = Integer.valueOf()。
Integer.valueOf的实现逻辑是,如果整数在-到范围内,直接从池中读取;否则直接new Integer(i)。因此,Integer a = 等同于new Integer()。
深入理解了源码后,就能解释数值和地址值的差异。在数值情况下,赋值不会影响原始变量;而在地址值情况下,赋值会创建新对象并影响实参变量。理解了这一点,就能更好理解代码行为。
使用关键字synchronized可以直观感受值传递中的数值和地址值差异。对于int类型,传递的是数值,synchronized无法加锁,因此编译错误。而对于Integer类型,传递的是地址值,多个线程可以并发操作同一个对象。
内存布局图可以直观展现值传递过程。在数值情况下,实参a指向堆内存中的新对象;在地址值情况下,实参a指向堆内存中的新对象地址,后续操作影响实参变量。
最后,通过单元测试代码,可以进一步确认值传递的影响。测试结果表明,值传递的特性决定了实参和返回值指向不同对象,而方法执行完毕后,为方法分配的栈内存会被回收。
综上所述,Java中的值传递特性决定了实参和返回值指向不同对象,这对于理解Java内存管理和多线程操作具有重要意义。
第7讲 | int和Integer有什么区别?
典型回答:
int是Java中的一种基本数据类型,属于原始数据类型的一种。它是Java编程语言中的8个基本数据类型之一,包括boolean、byte、short、char、int、float、double、long。
Integer是int类型的包装类,它是一个对象,包含一个存储数据的int类型的字段,并提供了一些基本操作,如数学运算、int与字符串之间的转换等。在Java 5中,引入了自动装箱和自动拆箱功能,简化了相关编程。
关于Integer的值缓存,这涉及Java 5中的一个改进。传统上,构建Integer对象的方式是直接调用构造器,创建一个新的对象。但实践表明,大部分数据操作都集中在有限的、较小的数值范围内。因此,在Java 5中引入了静态工厂方法valueOf,它利用一个缓存机制,提高了性能。按照Javadoc,这个值默认缓存范围是-到之间。
知识扩展:
1. 理解自动装箱、拆箱
自动装箱是一种语法糖,它实际上是一种语法上的简化。简单来说,Java平台为我们自动进行了一些转换,以保证不同的写法在运行时等价。这些转换发生在编译阶段,生成的字节码是一致的。
2. 源码分析
考察是否阅读过、是否理解JDK源代码可能是部分面试官的关注点。阅读并实践高质量代码也是程序员成长的必经之路。下面我来分析下Integer的源码。
3. 原始类型线程安全
前面提到了线程安全设计,你可能想过,原始数据类型操作是否线程安全。
4. Java原始数据类型和引用类型局限性
从Java平台发展的角度来看,原始数据类型和对象的局限性和演进是值得关注的。
再扩展:
你知道对象的内存结构是什么样的吗?比如,对象头的结构。如何计算或获取某个Java对象的大小?
Integer比较大小用“==”的坑
让我们通过一个实例来理解Integer比较大小的微妙之处: 通常,许多人可能会认为这两个表达式的输出结果都为true。然而,出乎意料的是,实际情况是: 为何会有这样的差异?为何与我们的直觉不符?这难道意味着我学习的Java有问题吗?让我们深入探究一下背后的原理: Integer类内部有个特性,它会缓存-到范围内的整数,当值超出这个范围时,会新建一个Integer对象。这是为了提升效率,避免频繁创建对象。类加载时,这个缓存就已经建立好了。所以,当我们比较两个在缓存范围内的Integer对象时,由于指向的是同一个对象,结果自然为true;而超出缓存范围的两个对象则是新创建的,比较时会检查地址,地址不同,所以返回false。 那么,如何正确地比较Integer的大小呢? 继续查看源码,你会发现Integer类型的比较实际上转化为基本类型的int比较,这样就避免了对象地址的考虑。 对于Integer以外的整数类型,如Long,一般可以使用equals方法,但需要注意浮点数,如Double和Float,它们的比较更为复杂。 通常,这些类型在常规情况下不会有太大问题,但需要留意以下特殊情况:NaN值
无穷大和无穷小
精度问题
由于本篇主要关注Integer,浮点数的处理将在其他场合详细讨论。优雅的避坑不要轻易使用==比较两个Integer的值
直接进入主题,来看一段代码,让我们探索Integer比较的奥秘:
许多人可能会理所当然地认为这段代码会打印出 j = ,但背后的原理却值得深入探讨。i作为Integer对象,而j为基本类型int,它们如何协同工作呢?这涉及到Java 5引入的自动装箱和拆箱机制。借助IDEA的jclasslib Bytecode viewer插件,我们可以看到程序运行的底层指令:
这段程序的字节码指令揭示了自动装箱和拆箱的过程。第3行调用Integer的valueOf方法进行自动装箱,第8行则调用intValue方法进行自动拆箱,将Integer对象转换为int。
进一步研究valueOf和intValue的源码,我们发现Integer类中有一个IntegerCache机制,它在虚拟机初始化时预加载了(-,]范围内的整数。这解释了为什么i1 == i2为true,而i3 == i4为false:在缓存范围内,而超出了。
为了避免这类陷阱,正确的比较两个Integer值的方法是使用equals()函数,而不是简单的==。equals会比较两个对象的整数值,不受类型影响。
阿里Java开发手册推荐的策略是,当比较整型包装类对象的值时,始终使用equals()方法,以确保准确无误的比较。
2024-11-25 09:34
2024-11-25 09:09
2024-11-25 08:51
2024-11-25 07:52
2024-11-25 07:47
2024-11-25 07:31