与IntegerCache有关的一个比较坑的面试题
最近在学习有关常量池的内容,正好看到一篇与之有关的面试题的文章。题目如下:
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的过程中可以查看任何变量的值,你只需要在代码中选中即可。
如果出现显示不全,那么移动到最下面,双击。
可以看到数组中129和130两个位置都是2。
这就说明我们实际上修改第一个位置是完成了。第一个值变成了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一个新的对象。
- JavaScript 教程
- JavaScript 编辑工具
- JavaScript 与HTML
- JavaScript 与Java
- JavaScript 数据结构
- JavaScript 基本数据类型
- JavaScript 特殊数据类型
- JavaScript 运算符
- JavaScript typeof 运算符
- JavaScript 表达式
- JavaScript 类型转换
- JavaScript 基本语法
- JavaScript 注释
- Javascript 基本处理流程
- Javascript 选择结构
- Javascript if 语句
- Javascript if 语句的嵌套
- Javascript switch 语句
- Javascript 循环结构
- Javascript 循环结构实例
- Javascript 跳转语句
- Javascript 控制语句总结
- Javascript 函数介绍
- Javascript 函数的定义
- Javascript 函数调用
- Javascript 几种特殊的函数
- JavaScript 内置函数简介
- Javascript eval() 函数
- Javascript isFinite() 函数
- Javascript isNaN() 函数
- parseInt() 与 parseFloat()
- escape() 与 unescape()
- Javascript 字符串介绍
- Javascript length属性
- javascript 字符串函数
- Javascript 日期对象简介
- Javascript 日期对象用途
- Date 对象属性和方法
- Javascript 数组是什么
- Javascript 创建数组
- Javascript 数组赋值与取值
- Javascript 数组属性和方法
- 《Java从入门到失业》第四章:类和对象(4.4):方法参数及传递
- 《Java从入门到失业》第四章:类和对象(4.3):一个完整的例子带你深入类和对象
- 《Java从入门到失业》第四章:类和对象(4.2):String类
- 《Java从入门到失业》第三章:基础语法及基本程序结构(3.9):数组(数组基本使用、数组的循环、数组拷贝、数组排序、多维数组)
- Establishing SSL connection without server identity verification is not recommended
- 《Java从入门到失业》第三章:基础语法及基本程序结构(3.8):流程控制(循环语句、while语句、for语句)
- 《Java从入门到失业》第三章:基础语法及基本程序结构(3.8):流程控制(选择语句、if-else语句、switch语句)
- 线上服务启动卡死,堆栈分析
- 《Java从入门到失业》第三章:基础语法及基本程序结构(3.7):运算符(自增自减、关系运算、逻辑运算、条件运算、位运算、赋值运算、类型转换)
- JavaWeb——MyBatis框架之多表查询操作实战案例总结(MyBatis1对1查询,MyBatis1对多查询,MyBatis多对多查询)
- kubernetes(二十二) 服务网格化istio入门
- 使用Ngrok将本地服务映射为公网服务
- 前端图片下载
- 使用JDBC连接MySQL数据库--典型案例分析(七)----批量插入员工信息
- NIO删除文件提示文件AccessDeniedException