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

来源:互联网 发布:流程的再造与优化 编辑:程序博客网 时间:2024/05/01 00:49

前几天看有人贴了一个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

 


1 0
原创粉丝点击
热门问题 老师的惩罚 人脸识别 我在镇武司摸鱼那些年 重生之率土为王 我在大康的咸鱼生活 盘龙之生命进化 天生仙种 凡人之先天五行 春回大明朝 姑娘不必设防,我是瞎子 迷失在时间和空间的交界处该怎么办 用卫生巾过后瘙痒起疹子了怎么办 装修公司倒闭了装修保修卡怎么办 丈夫把妻子的车抵押出去了怎么办 亚马逊海外购超过两万的额度怎么办 增值税申报表进项税转出忘填怎么办 一般纳税人注册下来后未营业怎么办 增值税税率把3错开成了5怎么办 在义乌做压痕加工老板拖欠钱怎么办 蓝洞棋牌是赌博输了几万怎么办 夏季来月经用卫生巾外阴瘙痒怎么办 用洗衣机洗衣服忘掏卫生纸了怎么办 剖腹产后一个月了还有血怎么办 剖腹产两个月同房后下面有血怎么办 剖腹产后月子里便秘有血怎么办 产后10天b超检查有血块怎么办 吃完优思明月经没有血块怎么办 刨腹产妇42天还有恶露怎么办 打完孩子第五天同房了出血了怎么办 打完孩子同房了出了一点血怎么办 宫腔镜检查一个月同房流血多怎么办 宫颈活检后三天同房有出血怎么办 顺产侧切两个月之后同房感染怎么办 顺产40天还有暗红色的恶露怎么办 来姨妈了没带卫生棉条去游泳怎么办 母猪肚子里面的小猪下不出来怎么办 刚生小猪的母猪肚子胀怎么办 吃了两天中药肚子还疼怎么办 怀孕期间垫了脚够东西怎么办 月经期垫卫生巾有边红肿有疹怎么办 四十天拉今天恶露特别多怎么办 顺产侧切出院几天后伤口裂开怎么办 产后十几撕裂用卫生巾疼怎么办 婴儿绑肚脐的棉黏在肚脐上怎么办 割完双眼皮第五天了很痒怎么办 自体脂肪丰胸做完半年有团块怎么办 假体隆胸一个月了躺着睡很硬怎么办 假体隆胸半月俩胸大小不一样怎么办 假体隆胸拆线后还是起不来床怎么办 假体隆胸术后6天 天天胀痛怎么办 阴部大腿根长了个疙瘩有点痛怎么办