一个简单代码的不简单实现

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/zgwangbo/article/details/51932728

前几天看有人贴了一个java代码的问题,实在有意思,今天拿出来和大家分享分享。题目是这样的:给定两个Integer类型的变量a和b,要求实现一个函数swap,交换他们的值。代码如下:


====想一想的分割线 ====

大家用30秒钟想想怎么样实现呢?

====时间到的分割线 ====

估摸着好多盆友一看这个题目,第一反应是:擦,这么简单的题,让我来做,是不是在侮辱我的智商!!!

最简单的实现:

这题目初看一眼,确实好简单,可能只需要10秒钟就可以完成(主要时间花在打字上):



好了,这就是实现代码,三行!那我们来看看结果:

before : a= 1, b = 2

after : a = 1, b = 2

怎么样,这个结果你猜对了嘛?就是完全没有交换。那是为什么呢?老王画了一张图:



在我们的main函数里,有两个对象变量a和b,他们分别指向堆上的两个Integer对象,地址分别为:0x1234和0x1265,值分别为1和2。在java里,Object a = new Object()这句话执行类似于c++里面的CObj* c = new CObj(),这里a和c实际都是指针(java称做引用),a和c的值,实际是一个内存地址,而不是1、2、3这样的具体的数字。所以,在做swap函数调用的时候,传递的是值,也就是i1得到的a的值:一个内存地址,指向0x1234。同理,i2得到的也是b的值:另外一个内存地址:0x1265。

好了,现在swap入栈,i1、i2、tmp都是指针:

tmp = i1; // tmp得到i1的值:0x1234

i1 =i2;  // i1得到i2的值:0x1265

i2 = tmp; // i2得到tmp的值:0x1234

 

可以看到,在swap里面,i1和i2做了一个指针交换,最后的结果如下:

 


 

最终,a和b还是指向对应的内存区域,而这个内存区域的值还是不变。所以,swap这个函数等于啥都没干,完全是浪费表情...

 

那这个题目似乎看起来就是无解的,对嘛?(谁这么无聊搞一个无解的题目来浪费表情!!!)

 

换值,解题的曙光:

在准备放弃之前,我们发现了有一个解法似乎可以做:如果把地址0x1234和0x1265中的值1和2对换,a和b的值就变化了,对吧!

 

那我们就聚焦到用什么方法可以改变这个值呢?

 

如果Integer提供一个函数,叫做setIntValue(intvalue),那就万事大吉了。我们可以实现这样的代码:

 

public static void swap(Integer i1, Integer i2)

{

    // 第二种可能的实现

    int tmp = i1.getIntValue()

    i1.setIntValue(i2.getIntValue());

    i2.setIntValue(tmp);

}

 

于是,我们就去查阅java.lang.Integer的代码实现。可惜的是,他没有这个函数...我们的梦想、我们的曙光,就这样破灭了...

 

反射,又燃起新的曙光:

在我们快要绝望的时候,我们突然发现了这个东东:

 

/**

* The value of the {@code Integer}.

*

* @serial

*/

private final int value;

 

java的Integer实现,实际内部将整数值存放在一个叫int类型的value变量里。他虽然有get函数,但是却没有set函数。因为他是final的(不可修改)!

 

那怎么办呢?哦,我们差点忘了java里有一个神器:反射!我们可以用反射把取到这个变量,并赋值给他,对吧!




 

于是,我们写下了如上的代码。我们从Integer类里面,取出value这个属性,然后分别设置上对应的值。哈哈哈,这下总该完美了吧!run一把:

 



sad... 我们得到了这样的异常:私有的、final的成员是不准我们访问的!

 

看起来似乎真的没办法了。

 

老王的绝杀:

这时候,老王从口袋里掏出了以前存起来的绝杀武器:反射访问控制变量:

AccessibleObject.setAccessible(boolean flag)

 

Field这个类是从AccessibleObject继承下来的,而AccessibleObject提供了一个方法,叫做setAccessible,他能让我们改变对于属性的访问控制。

 



他会将override变量设置为我们想要的值,然后在Field类里面:

 



 

只要这个override的只被设置成true,我们就可以顺利调用set函数啦,于是,我们就简单改一下实现代码:

 



就只加了这一句话,我们就成功了!哈哈哈哈!!! 来看结果吧:

 

before : a= 1, b = 2

after  : a = 2, b = 2

 

等等等等, 好像a已经变了,但是b似乎还没变! 这是怎么搞的?同样的实现方法,a变了,b没变,完全说不通啊,难道java虚拟机出问题了?这个时候,心里真是一万头草泥马奔过...

 

看似只差一步,实际还有万里之遥:

那问题到底出在哪儿呢?那我们重头开始看看这段代码。

 



在函数的一开始,我们就定义了两个变量:Integer a = 1; Integer b = 2; 这里1和2是主类型,换句话说他们是int类型,而a和b是Integer类型。他们是等价的嘛?回答是:NO!!!

 

装箱

那如果类型不等价,为啥编译的时候不出错呢?这里就要谈到一个java编译器的一个特性:装箱。这个是个什么东东?

 

按道理说,我们给a赋值的时候,应该是这样写:Integer a =new Integer(1),这才是标准的写法,对吧。不过这样写多麻烦啊,于是,java编译器给大家做了一个方便的事儿,就是你可以Integera = 1这样写,然后由编译器来帮你把剩下的东西补充完整(java编译器真是可爱,他还有很多其他的糖衣,以后有机会老王专门来介绍)。

 

那编译器给我们做了什么事情呢?难道是:

a = 1 === 编译 ===> a = new Integer(1) ?

 

老王最初也认为是这样的,不过后来发现,错了,他做的操作是:

a = 1 === 编译 ===> a = Integer.valueOf(1)

 

上面这个过程像不像把1这个int类型放入到Integer的箱子里呢?

 

这是怎么确认的呢?很简单,我们用javap来查看编译后的Swap.class代码即可:

 



看,我们的main函数第一行,定义Integer a = 1,实际上是做了 Integer a = Integer.valueOf(1)。这个确实是让人出乎意料。那这个函数做了什么事情呢?

 



这个函数的参数是一个int,然后如果这个int在IntegerCache的low和high之间,就从IntegerCache里面获取,只有超出这个范围,才新建一个Integer类型。

 



这是IntegerCache的实现,默认在-128和127之间的数,一开始就被新建了,所以他们只有一个实例。老王画了下面的示意图(为了让大家看的清楚,没有画完所有的内存)

 



我们可以这样来验证:

 

Integer i1= 1;

Integer i2= 1;

      

Integer i3= 128;

Integer i4= 128;

      

System.out.println(i1 == i2);

System.out.println(i3 == i4);

 

大家猜到答案了么? 结果是:true, false

 

因为Integer i1 = 1; 实际是Integer i1 = Integer.valueOf(1),在cache里,我们找到了1对应的对象地址,然后就直接返回了;同理,i2也是cache里找到后直接返回的。这样,他们就有相同的地址,因而双等号的地址比较就是相同的。i3和i4则不在cache里,因此他们分别新建了两个对象,所以地址不同。

 

好了,做了这个铺垫以后,我们再回到最初的问题,看看swap函数的实现。

 



这个函数的入参:i1和i2分别指向a和b对应的内存地址,这个时候,将i1的值(也就是value)传递给int型的tmp,则tmp的值为整数值1,然后我们想把i2的整数值2设置给i1:f.set(i1, i2.intValue()); 这个地方看起来很正常吧?

 

我们来看看这个函数的原型吧:public voidset(Object obj, Object value) 他需要的传入参数是两个Object,而我们传入的是什么呢? Integer的i1,和int的i2.intValue()。对于第一个参数,是完全没问题的;而第二个参数,编译器又给我们做了一次装箱,最终转化出来的代码就像这样:

 

i1.value =Integer.valueOf(i2.intValue()).intValue();

 

那我们手动执行一下,

 

a、i2.intValue() -> 2

b、Integer.valueOf(2) -> 0x1265

c、0x1265.intValue() -> 2

d、i1.value -> 2

 

所以这个时候,内存里的数据就是这样的了:0x1234被改成2了!!!

 



接着,我们执行下一句:f.set(i2,tmp); 按照上面的步骤,我们先展开:

 

i2.value =Integer.valueOf(tmp).intValue();

 

这里tmp等于1,于是分步执行如下:

 

a、Integer.valueOf(1) -> 0x1234

b、0x1234.intValue() -> 2

c、i2.value -> 2

 

注意步骤b的值就是上一步从1改成2的那个值,因此最终内存的值就是:

 



所以,我们才看到最后a和b输出的都是2。终于终于,我们分析清楚了结果了~~

 

那要达到最后我们要求的交换,怎么样修改呢?我们有两种方法

 

1、不要让Integer.valueOf装箱发挥作用,避免使用cache,因此可以这样写:

 



我们用newInteger代替了Integer.valueOf的自动装箱,这样tmp就分配到了一个不同的地址;

 

2、我们使用setInt函数代替set函数,这样,需要传入的就是int型,而不是Integer,就不会发生自动装箱

 



so...问题解决了!

 

==== 总结的分割线 ====

 

看看,就是这么简单的一个代码实现,却隐藏了这么不简单的实现,包含了:

1、函数调用的值传递;

2、对象引用的值乃是内存地址;

3、反射的可访问性;

4、java编译器的自动装箱;

5、Integer装箱的对象缓存。

 

这么好几个隐含的问题。怎么样,你看懂了嘛?

 

如果觉得老王讲的不错,下周日下午继续关注老王的微信吧(simplemain

 


猜你喜欢

转载自blog.csdn.net/zgwangbo/article/details/51932728