之前看了一篇文章是关于Integer作为形参传入,之后修改值得问题,感觉有点意思,又做了进一步研究。
案例:
public static void main(String[] args) {
Integer a = 1;
Integer b = 2;
System.out.println("a=" + a + ",b=" + b);
swap(a, b);
System.out.println("a=" + a + ",b=" + b);
}
private static void swap(Integer a, Integer b) {
}
原本的案例是这样: 通过swap方法将a和b的值交换
测试一:
// 1
private static void swap(Integer a, Integer b) {
int temp = a;
a = b;
b = temp;
}
最传统的方式,如果是在main中是可以实现的,但在swap中的结果是:
a=1,b=2
a=1,b=2
解析:
- 这里是地址传参,b将地址赋值给a,在方法外,a的值一定是不变的
- b看似是被一个int类型赋值之后做了自动装箱,但实际上,经过调试发现自动装箱调用的是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);
}
根据源码可以发现,无论是哪个分支都不会影响main方法中的b的值
- 在-128~127这个神奇的区间内是在
IntegerCache.cache
中获取Integer对象,而IntegerCache.cache
是一个静态数组,作为这个区间值的缓存,所以以自动装箱创建的Integer对象是已经创建好的,统一数值其地址相同
private static class IntegerCache {
static final int low = -128;
static final int high;
static final Integer cache[];
- 在上述区间之外,重新创建Integer对象,地址改变,而不是值改变,因此不影响main中b的值。 同理,2的结果是一样的
// 2
private static void swap2(Integer a, Integer b) {
int temp = a.intValue();
a = b.intValue();
b = temp;
}
所以如何改变Integer的值,而不改变地址?之前看到的文章中给出了答案,我也只想到了这种方式:反射
测试六:
// 6
private static void swap6(Integer a, Integer b) {
Integer temp = a.intValue();
try {
Field field = Integer.class.getDeclaredField("value");
field.setAccessible(true);
field.set(a, b);
System.out.println("swap: temp:" + temp);
field.set(b, temp);
} catch (NoSuchFieldException | SecurityException e) {
// TODO Auto-generated catch block
e.printStackTrace();
} catch (IllegalArgumentException e) {
// TODO Auto-generated catch block
e.printStackTrace();
} catch (IllegalAccessException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
结果:
a=1,b=2
a=2,b=2
a已经成功改变,但是b却没有,原因:temp与a的地址相同,在第一次修改值之后a从1变为2,temp也变为2,所以b不会改变
那么temp改为int呢?
// 5
private static void swap5(Integer a, Integer b) {
int temp = a.intValue();
try {
Field field = Integer.class.getDeclaredField("value");
field.setAccessible(true);
field.set(a, b);
System.out.println("swap: temp:" + temp);
Integer c = temp;
System.out.println(c.intValue());
field.set(b, temp);
} catch (NoSuchFieldException | SecurityException e) {
// TODO Auto-generated catch block
e.printStackTrace();
} catch (IllegalArgumentException e) {
// TODO Auto-generated catch block
e.printStackTrace();
} catch (IllegalAccessException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
结果仍然为
a=1,b=2
a=2,b=2
很令人费解,于是在中间添加了
Integer c = temp;
System.out.println(c.intValue());
发现temp输出结果是1,但c却是2?为什么?
a=1,所以temp为1没有问题,关键在于
Integer c = temp;
和上述一样,调用了Integer.valueOf(1);
获取到的是IntegerCache.cache[129]
,即原来a的位置,但是a已经被set为2了,所以IntegerCache.cache
中原来1的位置被替代为2,所以c的value为2.
但是还有另一个因素导致,先看下面的例子:
// 3
private static void swap3(Integer a, Integer b) {
int temp = a.intValue();
try {
Field field = Integer.class.getDeclaredField("value");
field.setAccessible(true);
field.setInt(a, b);
field.setInt(b, temp);
} catch (NoSuchFieldException | SecurityException e) {
// TODO Auto-generated catch block
e.printStackTrace();
} catch (IllegalArgumentException e) {
// TODO Auto-generated catch block
e.printStackTrace();
} catch (IllegalAccessException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
3的结果是:
a=1,b=2
a=2,b=1
是我众多例子中唯一修改了的。
3和5的区别在于field.setInt(b, temp);
和field.set(b, temp);
看一下分别的源码:
Field:
public void set(Object obj, Object value)
throws IllegalArgumentException, IllegalAccessException
{
if (!override) {
if (!Reflection.quickCheckMemberAccess(clazz, modifiers)) {
Class<?> caller = Reflection.getCallerClass();
checkAccess(caller, clazz, obj, modifiers);
}
}
getFieldAccessor(obj).set(obj, value);
}
UnsafeQualifiedIntegerFieldAccessorImpl:
public void set(Object arg0, Object arg1) throws IllegalArgumentException, IllegalAccessException {
this.ensureObj(arg0);
if (this.isReadOnly) {
this.throwFinalFieldIllegalAccessException(arg1);
}
if (arg1 == null) {
this.throwSetIllegalArgumentException(arg1);
}
if (arg1 instanceof Byte) {
unsafe.putIntVolatile(arg0, this.fieldOffset, ((Byte) arg1).byteValue());
} else if (arg1 instanceof Short) {
unsafe.putIntVolatile(arg0, this.fieldOffset, ((Short) arg1).shortValue());
} else if (arg1 instanceof Character) {
unsafe.putIntVolatile(arg0, this.fieldOffset, ((Character) arg1).charValue());
} else if (arg1 instanceof Integer) {
unsafe.putIntVolatile(arg0, this.fieldOffset, ((Integer) arg1).intValue());
} else {
this.throwSetIllegalArgumentException(arg1);
}
}
Field:
public void setInt(Object obj, int i)
throws IllegalArgumentException, IllegalAccessException
{
if (!override) {
if (!Reflection.quickCheckMemberAccess(clazz, modifiers)) {
Class<?> caller = Reflection.getCallerClass();
checkAccess(caller, clazz, obj, modifiers);
}
}
getFieldAccessor(obj).setInt(obj, i);
}
UnsafeQualifiedIntegerFieldAccessorImpl:
public void setInt(Object arg0, int arg1) throws IllegalArgumentException, IllegalAccessException {
this.ensureObj(arg0);
if (this.isReadOnly) {
this.throwFinalFieldIllegalAccessException(arg1);
}
unsafe.putIntVolatile(arg0, this.fieldOffset, arg1);
}
两中方式最终都是执行了UnsafeQualifiedIntegerFieldAccessorImpl的unsafe.putIntVolatile方法,但是结果却是不同的,原因就在于set方法中第二个参数是对象,
unsafe.putIntVolatile(arg0, this.fieldOffset, ((Integer) arg1).intValue());
所以有进行一次强转,然而调试发现强转也是调用的Integer.valueOf();
,所以问题又回到了上面赋值的虽然是1,但是由于使用反射修改了value,导致取出来的值实际上是2,而setInt没有这个过程,所以setInt修改成功,但是set修改失败。
参考文章: