//要求写一个swap方法交换ab值输出打印a=2 b=1
public static void main(String[] args) {
Integer a=1,b=2;
swap(a,b);
System.out.println("a="+a+" b="+b);
}
这道题目所涉及到的知识点包括:
1. Integer的自动装箱
java的自动装箱就是自动将原始数据类型(byte,short,char,int,long,float,double,boolean)转换成对应的基本数据类型包装类对象(Byte,Short,Character,Integer,Long,Float,Double,Boolean),比如将int的变量转换成Integer对象
这里的Integer a=1 经过自动装箱以后变为Integer a=Integer.valueOf(1);
那么我们接着查看Integer类中的valueOf()方法如下:
public static Integer valueOf(int i) {
if (i >= IntegerCache.low && i <= IntegerCache.high)
return IntegerCache.cache[i + (-IntegerCache.low)];
return new Integer(i);
}
接着查看IntegerCache这个内部类可以发现java虚拟机在我们首次使用Integer的时候会初始化数值在-128-127之间的整数,而且这个区间范围可以在虚拟机启动的时候自己设定。换句话说,数值在-128-127之间的相同整数都会取自相同的内存具体值。下面例子中a和b都是指向相同的内存地址,而x和y指向两块不同的内存地址。
Integer x=-129;
Integer y=-129;
System.out.println(x==y); //输出false
Integer a=-128;
Integer b=-128;
System.out.println(a==b); //输出true
Integer xx=new Integer(1);
Integer yy=new Integer(1);
System.out.println(xx==yy); //输出false
说到这里必须说一下valueOf(int i)方法最终返回的是new Integer(i);而Integer类的构造函数如下,这里的value是解题的关键,value在Integer类中是一个final的私有成员变量。
public Integer(int value) {
this.value = value;
}
说出正确答案之前还得看看Integer的源码中的toString方法public String toString() { return toString(value); }
可见其返回的真实值就是value,并且value就是在构造函数中初始化的。在swap方法中我们拿到了Integer的引用,那么我们就可以通过反射来改变对应实例的局部变量值。具体代码如下:
private static void swap1(Integer aa, Integer bb) {
try {
Field aaValue = aa.getClass().getDeclaredField("value");
aaValue.setAccessible(true);
aaValue.set(aa,2);
Field bbValue = bb.getClass().getDeclaredField("value");
bbValue.setAccessible(true);
bbValue.set(bb,1);
System.out.println(Integer.valueOf(1));//输出2
} catch (Exception e) {
e.printStackTrace();
}
}
但是运行之后我们发现其结果是:
a=2 b=2
看到这里是不是感觉很不可思议,但是我们距离真相已经很近了。我们疑惑的是为什么b的值没有发生改变,更疑惑的是为什么Integer.valueOf(1)输出的竟然是2。这是因为基本数据类型在-128-127之间的数都会自动装箱并且从缓存中去取,我们反射改变的只是缓存中的1对应的实例的实例变量value值,我们在bbValue.set(bb,1);的时候1会自动装箱去缓存中去找,而这时缓存中的Integer.valueOf(1)已经是2,所以返回的还是2。解决的方法就是不让其去缓存中找,参考代码如下:
//正解
private static void swap1(Integer aa, Integer bb) {
try {
Field aaValue = aa.getClass().getDeclaredField("value");
aaValue.setAccessible(true);
aaValue.set(aa,2);
Field bbValue = bb.getClass().getDeclaredField("value");
bbValue.setAccessible(true);
//这里的1不会自动装箱,当然就不会从缓存中去取值
bbValue.set(bb,new Integer(1));
} catch (Exception e) {
e.printStackTrace();
}
}
这里还是要多说一句,如果a,b变量的值是-128-127之外的整数的话虽然会自动装箱,但是不会从缓存中去取值,那么bbValue.set(bb,-128-127之外的整数);就可以了,是不用专门new Integer()的,参考代码如下:
public static void main(String[] args) {
Integer a=999;
Integer b=888;
swap1(a,b);
System.out.println("a="+a+" b="+b);
}
private static void swap1(Integer aa, Integer bb) {
try {
Field aaValue = aa.getClass().getDeclaredField("value");
aaValue.setAccessible(true);
aaValue.set(aa,888);
Field bbValue = bb.getClass().getDeclaredField("value");
bbValue.setAccessible(true);
bbValue.set(bb,999);//这里没有new Integer(999)
} catch (Exception e) {
e.printStackTrace();
}
}
输出结果为a=888 b=999
2. 错误答案示例
对于这道题目的解法有些同学可能会给出下面的答案,明显是错误的,因为它们具体交换的还是aa和bb所指向的内存地址值,而这一块内存地址值所指向的内存空间的内容并没有改变,所以a和b变量是没有变化的(参考图1)。
private static void swap(Integer aa,Integer bb){
Integer temp;
temp=aa;
bb=temp;
aa=bb;
}
按照我自己的理解,java的值传递和引用传递实际上都是值传递,因为引用传递最终传递的还是引用对象的内存具体值(下图红色线条代表交换后的状态,可见a和b并没有发生变化)
图1.引用传递(红线代表交换后)
3. 题目变种
//对于这道题是不能用反射的,因为是基本数据类型的值传递
public static void main(String[] args) {
int a=1;
int b=2;
swap(a,b);
System.out.println("a="+a+" b="+b);
}
投机取巧的做法如下:
public static void main(String[] args) {
int a=1;
int b=2;
swap(a,b);
System.out.println("a="+a+" b="+b);
}
private static void swap(Integer aa, Integer bb) {
System.out.println("a="+2+" b="+1);
System.exit(0);
}