最近在学习有关常量池的内容,正好看到一篇与之有关的面试题的文章。Java大型互联网公司经典面试题,论JDK源码的重要性的无限思考。题目如下:
public static void main(String[] args) {
Integer a = 1;
Integer b = 2;
System.out.println("before swap: a="+a+" b="+b);
swap(a,b);
System.out.println("before swap: a="+a+" b="+b);
}
private static void swap(Integer a,Integer b) {
}
要求将这两个Integer变量经过swap方法之后完成互换。
对于这个题目,可能一开始想到的就是最简单的办法,直接在swap方法内实现互换即可。不妨尝试一下:
private static void swap(Integer a,Integer b) {
a = a^b;
b = a^b;
a = a^b;
}
但是输出结果如下:
before swap: a=1 b=2
before swap: a=1 b=2
没有成功。也就是说这个题目没有看上去的那么简单。里面肯定有坑。
这里就需要了解下java中引用传递还是值传递了。对于java,如果方法中传递的是基本类型,那么就是值传递。如果方法中是普通对象,那么就是引用传递。但是在java中,有这么一些特殊的类,如String、Integer等,没有set方法的immutable类。实际上也是值传递。
为此我们可以看看Integer的源码:
/**
* The value of the {@code Integer}.
*
* @serial
*/
private final int value;
其中Integer的value是final修饰的。那么final修饰的只能被实例化一次。之后不能修改。因此Integer也没有提供可以修改Integer的set方法。因此Integer是一个与String类型的immutable类。所以也是值传递。
因此基于对反射的了解,我们解决这个问题的方法很容易想到了通过反射来解决。
那么我们修改代码如下:
private static void swap(Integer a,Integer b) {
try {
Field field = Integer.class.getDeclaredField("value");
field.setAccessible(true);
int tmp1 = a.intValue();
int tmp2 = b.intValue();
tmp1 = tmp1^tmp2;
tmp2 = tmp1^tmp2;
tmp1 = tmp1^tmp2;
field.set(a,tmp1);
field.set(b,tmp2);
} catch (Exception e) {
e.printStackTrace();
}
}
再次执行。
结果如下:
before swap: a=1 b=2
before swap: a=2 b=2
这个结果有些让人意外的是,a的结果改成了2,但是b的结果也是2 !!!!
对于这个问题,只能进行单步了。
在debug的时候发现,调用了valeOf方法。也就是说,jvm自动将int的数据通过valueOf方法转换为了Integer。
我们将这个类的class反编译,可以看到:
public static void main(String[] args) {
Integer a = Integer.valueOf(1);
Integer b = Integer.valueOf(2);
System.out.println("before swap: a=" + a + " b=" + b);
swap(a, b);
System.out.println("before swap: a=" + a + " b=" + b);
}
private static void swap(Integer a, Integer b) {
try {
Field field = Integer.class.getDeclaredField("value");
field.setAccessible(true);
int tmp1 = a.intValue();
int tmp2 = b.intValue();
tmp1 ^= tmp2;
tmp2 = tmp1 ^ tmp2;
tmp1 ^= tmp2;
field.set(a, Integer.valueOf(tmp1));
field.set(b, Integer.valueOf(tmp2));
} catch (Exception e) {
e.printStackTrace();
}
}
实际上在jvm中执行的是上述代码。jvm会自动在这些类型需要转换的地方装箱。哪我们再看看valueOf方法具体干了啥:
/**
* Returns an {@code Integer} instance representing the specified
* {@code int} value. If a new {@code Integer} instance is not
* required, this method should generally be used in preference to
* the constructor {@link #Integer(int)}, as this method is likely
* to yield significantly better space and time performance by
* caching frequently requested values.
*
* This method will always cache values in the range -128 to 127,
* inclusive, and may cache other values outside of this range.
*
* @param i an {@code int} value.
* @return an {@code Integer} instance representing {@code i}.
* @since 1.5
*/
public static Integer valueOf(int i) {
if (i >= IntegerCache.low && i <= IntegerCache.high)
return IntegerCache.cache[i + (-IntegerCache.low)];
return new Integer(i);
}
这个方法,会对传入的值进行判断,如果在-128 - 127之间。则直接使用的是IntegerCache的结果。
那么这个题目要将Integer对象值为1的变量a和值为2的变量b互换,如果要成功的话,那就意味着将IntegerCache中的内容互换。
我们继续debug,在第一个set的代码行执行完成之后:
field.set(a, Integer.valueOf(tmp1));
此时我们可以看看这个IntegerCache是多少,为此为了debug方便我们加上一行:
Integer c = 5;
在这行执行valueOf方法的时候,查看IntegerCache中的内容:
idea比较好的就是在debug的过程中可以查看任何变量的值,你只需要在代码中选中即可。
![](https://img.haomeiwen.com/i3237432/1838fddda449279b.png)
如果出现显示不全,那么移动到最下面,双击。
![](https://img.haomeiwen.com/i3237432/7cc82a55c52988d8.png)
可以看到数组中129和130两个位置都是2。
![](https://img.haomeiwen.com/i3237432/c7a59d1d4ed5ce04.png)
这就说明我们实际上修改第一个位置是完成了。第一个值变成了2,但是在第二次set的时候出现了问题,反编译之后的写法,实际上是Integer.ValueOf,那么去从IntegerCache中查找1对应的位置,但是此时已经修改为了2,那么得到2后再修改,任然还是2。
因此,为了解决这个问题,我们的代码做出修改,让set的时候不再走IntegerCache。修改为如下:
private static void swap(Integer a,Integer b) {
try {
Field field = Integer.class.getDeclaredField("value");
field.setAccessible(true);
int tmp1 = a.intValue();
int tmp2 = b.intValue();
tmp1 = tmp1^tmp2;
tmp2 = tmp1^tmp2;
tmp1 = tmp1^tmp2;
field.set(a,new Integer(tmp1));
field.set(b,new Integer(tmp2));
} catch (Exception e) {
e.printStackTrace();
}
}
这样就解决了此问题。我们可以再看一下反编译之后的代码:
private static void swap(Integer a, Integer b) {
try {
Field field = Integer.class.getDeclaredField("value");
field.setAccessible(true);
int tmp1 = a.intValue();
int tmp2 = b.intValue();
tmp1 ^= tmp2;
tmp2 = tmp1 ^ tmp2;
tmp1 ^= tmp2;
field.set(a, new Integer(tmp1));
field.set(b, new Integer(tmp2));
} catch (Exception e) {
e.printStackTrace();
}
}
可以看到通过new一个新对象的方法,就避免了再次进入IntegerCache。
总结:
以上这个面试题的坑不少,主要有:
- 1.java中的方法传递和值传递。对于不可变类型实际上还是值传递。
- 2.反射修改私有变量需要setAccessible(true)。
- 3.自动装箱调用ValueOf方法会走IntegerCache。
- 4.如果不想走IntegerCache,那么new一个新的对象。
网友评论