Integer类型的值交换、IntegerCache缓存
一个很经典的面试题:
private static void swap(Integer x, Integer y)
{
//请实现
}
public static void main(String[] args) {
Integer a = 1;
Integer b = 2;
swap(a, b);
System.out.println("a=" + a + ",b=" + b);
}
题目意思是交换a和b两个Integer类型变量的值,在swap方法中实现
乍一看好像很简单,Integer是int的包装类型,直接通过修改对象的引用实现:
private static void swap(Integer x, Integer y)
{
//请实现
Integer temp = x;
x = y;
y = temp;
}
public static void main(String[] args) {
Integer a = 1;
Integer b = 2;
swap(a, b);
System.out.println("a=" + a + ",b=" + b);
}
输出:
a=1,b=2
显然没有达到预期的效果
这就需要提到形参与实参的区别了:
形参 顾名思义:就是形式参数,用于定义方法的时候使用的参数,是用来接收调用者传递的参数的。 形参只有在方法被调用的时候,虚拟机才会分配内存单元,在方法调用结束之后便会释放所分配的内存单元。 因此,形参只在方法内部有效,所以针对引用对象的改动也无法影响到方法外。
实参 顾名思义:就是实际参数,用于调用时传递给方法的参数。实参在传递给别的方法之前是要被预先赋值的。
在本例中 swap 方法 的x,y就是形参,传递给 swap 方法的 a,b 就是实参
我们可以用图来演示下上面解决方案的过程:
Integer a = 1;
Integer b = 2;
![](https://i-blog.csdnimg.cn/blog_migrate/8ad18ffb1b394f315cb197ed81eae2e4.png)
调用swap方法,创建两个局部变量x和y(形参),分别指向堆中1和2的内存区域,这个过程并不会改变a和b(实参)的引用。
![](https://i-blog.csdnimg.cn/blog_migrate/ca2d35a2ccb36bd08a9b6e6cbbe3efaf.png)
Integer temp = x; 定义temp临时变量,temp的引用和x指向同一内存区域。
![](https://i-blog.csdnimg.cn/blog_migrate/e97eef8685a53ecc39465d4ebf85ba4f.png)
x = y; 改变了x的引用使其指向2的内存
y = temp; 改变y的引用指向temp指向的内存。
![](https://i-blog.csdnimg.cn/blog_migrate/9430625481e30d27f5b2a44f39c32715.png)
方法swap执行结束,我们可以到实参a和b指向的内存始终没有改变,所以并没有达到交换值的目的。
再看下面一种实现:
public static void main(String[] args) {
Integer a = 1;
Integer b = 2;
Integer temp = a;
a = b;
b = temp;
System.out.println("a=" + a + ",b=" + b);
}
输出结果:
a=2,b=1
这样虽然可以达到目的,但改变题目的做法显然不太行
看一个示例:
public class Person {
int age;
public Person(int age) {
this.age = age;
}
static void testPersion(Person person) {
person.age = 40;
}
public static void main(String[] args) {
Person person = new Person(30);
person.testPersion(person);
System.out.println(person.age);
}
}
输出:
40
Integer和Person一样都是引用类型,能否参照Person类改变值的方式来实现swap方法呢?很遗憾不能,我们查看Integer的源码可以看到:
private final int value;
public Integer(int value) {
this.value = value;
}
Integer内部有一个value来记录int基本类型的值,但是没有提供修改它的方法,而且它也是final类型的,类似的还有 String, Float, Double, Short, Byte, Long, Character等,我们均无法访问到其内部的值,因此无法通过常规手段实现来改变它们。
使用反射的方式是可以做到改变私有value的值的。
在讲反射方式之前,先了解一下Java中基本数据类型的装箱和拆箱操作
当我们使用Integer x = 2 的时候,在JVM运行前的编译阶段,该Integer x= 2 将会被编译为Integer x= new Integer(2),此时编译后的这样一个语法是符合JDK运行时的规则的,这种操作就是所谓的装箱操作
接着Integer x = 2; 写上int y = x; 这里就完成了 一个拆箱的操作,即把包装类型转换为基本数据类型。
一般当我们进行对比的时候,编译器会优先把包装类进行自动拆箱:如Integer x1= 2; 和 int x2 = 2; 当执行if(x1 == x2) 时,编译器便会自动的将包装类的x1自动拆箱为int类型进行对比等操作,此处返回true
聊完了拆装箱,我们看一下反射方式实现的几个示例:
示例一:
private static void swap(Integer x, Integer y) throws Exception
{
//请实现
Integer temp = x;
Field field = Integer.class.getDeclaredField("value");//获取成员变量value
field.setAccessible(true);//暴力反射,用于私有成员
field.set(x,y);
field.set(y,temp);//若此处传的temp是int类型(即将第一行Integer temp = x; 改为int temp = x;),会先调用valueOf方法对temp进行装箱操作(public void set(Object obj, Object value))
}
public static void main(String[] args) {
Integer a = 1;
Integer b = 2;
swap(a, b);
System.out.println("a=" + a + ",b=" + b);
}
输出:
a=2,b=2
示例二:
private static void swap(Integer x, Integer y) throws Exception
{
//请实现
Integer temp = x.intValue();
Field field = Integer.class.getDeclaredField("value");//获取成员变量value
field.setAccessible(true);//暴力反射,用于私有成员
field.set(x,y);
field.set(y,temp);
}
public static void main(String[] args) {
Integer a = 1;
Integer b = 2;
swap(a, b);
System.out.println("a=" + a + ",b=" + b);
}
输出:
a=2,b=2
在示例二的基础上,仅仅修改实参a和b的定义,
示例三:
public static void main(String[] args) {
Integer a = 200;
Integer b = 300;
swap(a, b);
System.out.println("a=" + a + ",b=" + b);
}
输出:
a=300,b=200
示例四:
public static void main(String[] args) {
Integer a = new Integer(1);
Integer b = new Integer(2);
swap(a, b);
System.out.println("a=" + a + ",b=" + b);
}
输出:
a=2,b=1
可以看到:示例三和示例四得到了正确的结果,很奇妙!
为什么会出现这种情况呢?终于开始得讲IntegerCache缓存了
上面我们提到过,在自动装箱时,实际上是把 Integer x=2; 在编译时变更成了Integer x= new Integer(2);而在这个装箱的过程中实际上是调用的Integer的valueOf(int i)的方法。
看一下源码实现:
public static Integer valueOf(int i) {//返回的类型是Integer
//自动装箱时,当传入的i值在范围[IntegerCache.low, IntegerCache.high]内,会从缓存中取出对象返回
if (i >= IntegerCache.low && i <= IntegerCache.high)
return IntegerCache.cache[i + (-IntegerCache.low)];
//i超出范围,new一个新的Integer对象返回
return new Integer(i);
}
接下来看一下缓存的实现:
public final class Integer extends Number implements Comparable<Integer> {
private static class IntegerCache {//private修饰的静态内部类
static final int low = -128;//IntegerCache.low被固定为-128
static final int high;
static final Integer cache[];//设置Integer类型的缓存数组,可以看到缓存是存放在常量池中(static修饰)
static {
// high value may be configured by property
int h = 127;
//可以通过设置JVM参数来改变IntegerCache.high,使用integerCacheHighPropValue接收
//-Djava.lang.Integer.IntegerCache.high=xxx 或 -XX:AutoBoxCacheMax=xxx
String integerCacheHighPropValue =
sun.misc.VM.getSavedProperty("java.lang.Integer.IntegerCache.high");
if (integerCacheHighPropValue != null) {
try {
int i = parseInt(integerCacheHighPropValue);
i = Math.max(i, 127);//IntegerCache.high最小应为127
// Maximum array size is Integer.MAX_VALUE
h = Math.min(i, Integer.MAX_VALUE - (-low) -1);//避免Integer最大值溢出,因为是先从负数开始的,看下面a处的cache初始化代码
} 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;
//a IntegerCache.cache初始化,存储了值在[IntegerCache.low, IntegerCache.high]内的Integer对象
for(int k = 0; k < cache.length; k++)
cache[k] = new Integer(j++);
/*
cache[0] = new Integer(-128)
cache[1] = new Integer(-127)
...
cache[128] = new Integer(0)
cache[129] = new Integer(1)
...
*/
// range [-128, 127] must be interned (JLS7 5.1.7)
assert IntegerCache.high >= 127;//IntegerCache.high必须大于等于127
}
private IntegerCache() {}
}
}
而自动拆箱时调用的Integer的intValue()方法:
private final int value;
public int intValue() {
return value;
}
可以看到返回的是基本数据类型。
从上面的源码分析中可以总结几点:
-
在代码中进行Integer的对比时尽可能的使用 .equals()来进行对比,看下面的例子:
Integer x1 = 3; Integer x2 = 3; Integer x3 = new Integer(3); Integer x4 = 200; Integer x5 = 200; System.out.println(x1 == x2); //true System.out.println(x4 == x5); //false System.out.println(x1 == x3); //false
可以看到还是很奇妙,而用equals方法来进行对比则会很保险
-
Integer对象既可能存放在堆中也可能存放在常量池中,由于IntegerCache.cache是位于常量池的,而除缓存里的外新new的Integer对象都是放在堆中的
-
补充一点:
在方法中定义的局部变量:int a = 3 ; a和其值3都是放在线程栈中
对于在类中定义的成员属性来说,比如:static int a =3; 此时是在常量池中(有static修饰),而类的非static成员属性如:int a=3; 则在对象所在的堆中。
介绍完了IntegerCache,就可以解答上面示例一到示例四的问题了,用图解的方式来演示:
示例一:
Integer temp = x; temp指向x所指向的对象
![](https://i-blog.csdnimg.cn/blog_migrate/ed4d6daafcb0cb3d38b2c3faaf7ba803.png)
field.set(x, y); 将x指向对象的value值改为2,temp和x指向的是同一个对象
field.set(y,temp);
![](https://i-blog.csdnimg.cn/blog_migrate/5ae0ef84a943195a245b81659d235fb3.png)
最终:a = 2,b = 2
示例二:
Integer temp = x.intValue(); 该行首先会判断 x.intValue()返回的int值是否在-128~127之间,是则直接从缓存中取相应对象赋给temp,否则才会在堆中分配一块内存。
![](https://i-blog.csdnimg.cn/blog_migrate/c2ae85e6c51e987d1bb6787371e02b88.png)
后面的过程和示例一相同
示例三:
Integer temp = x.intValue(); 由于 x.intValue()返回的int值未在-128~127之间,于是在堆中新分配一块内存,其值是200
![](https://i-blog.csdnimg.cn/blog_migrate/15693814fad8731eab8e48e2a8eab198.png)
field.set(x, y);
field.set(y, temp);
![](https://i-blog.csdnimg.cn/blog_migrate/8cbd2d0435ae8b7c691fc6441b2bed18.png)
示例四与示例三类似,只不过temp指向的是缓存中对象
正确的答案:
private static void swap(Integer x, Integer y) throws Exception
{
//请实现
Integer temp = new Integer(x.intValue());
Field field = Integer.class.getDeclaredField("value");//获取成员变量value
field.setAccessible(true);//暴力反射,用于私有成员
field.set(x,y);
field.set(y,temp);
}
public static void main(String[] args) {
Integer a = 1;
Integer b = 2;
swap(a, b);
System.out.println("a=" + a + ",b=" + b);
}
关键在于不能让temp和x指向堆中的同一个对象
过程与示例三类似,这里不再演示