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

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

时间: 2024-08-24 19:57:42

一个简单代码的不简单实现的相关文章

完成一个简单的时间片轮转多道程序内核代码

王康 + 原创作品转载请注明出处 + <Linux内核分析>MOOC课程http://mooc.study.163.com/course/USTC-1000029000 " 分别是1 存储程序计算机工作模型,cpu执行程序的基础流程: 2 函数调用堆栈:各种寄存器和存储主要是为了指令的传取值,通过eip,esp,eax,ebp和程序内存的分区,搭配push pop call return leave等一系列指令完成函数调用操作. 3 中断:多道批程序! 在复习一下上一讲的几个重要指令

生成一个空白BMP的简单代码【转】

转自:http://blog.chinaunix.net/uid-15063109-id-4275395.html 做图像处理时,有时需要临时生成图使用.以下是生成320x240 24位图的一个简单的代码实现: #define WIDTHBYTES(bits) ((DWORD)(((bits)+31) & (~31)) / 8) void makebmp() { int nSize =abs(long(240 * WIDTHBYTES(24 * 320))); char* buff = new

ios开发UI篇—使用纯代码自定义UItableviewcell实现一个简单的微博界面布局

本文转自 :http://www.cnblogs.com/wendingding/p/3761730.html ios开发UI篇—使用纯代码自定义UItableviewcell实现一个简单的微博界面布局 一.实现效果 二.使用纯代码自定义一个tableview的步骤 1.新建一个继承自UITableViewCell的类 2.重写initWithStyle:reuseIdentifier:方法 添加所有需要显示的子控件(不需要设置子控件的数据和frame,  子控件要添加到contentView中

通过反汇编一个简单的C程序,分析汇编代码理解计算机是如何工作的

实验一:通过反汇编一个简单的C程序,分析汇编代码理解计算机是如何工作的 学号:20135114 姓名:王朝宪 注: 原创作品转载请注明出处   <Linux内核分析>MOOC课程http://mooc.study.163.com/course/USTC-1000029000 1 1)实验部分(以下命令为实验楼64位Linux虚拟机环境下适用,32位Linux环境可能会稍有不同) 使用 gcc –S –o main.s main.c -m32 命令编译成汇编代码,如下代码中的数字请自行修改以防与

对一个简单的时间片轮转多道程序内核代码的浅析

这周在网易云课堂上学习了<Linux内核分析>——操作系统是如何工作的.本周学习内容有利用 mykernel 实验模拟计算机平台和利用 mykernel 实验模拟计算机硬件平台两部分内容. 这是实验楼中 mykernel 平台运行的结果: 下面是一段一个简单的时间片轮转多道程序内核代码: 1 /* 2 * linux/mykernel/myinterrupt.c 3 * 4 * Kernel internal my_timer_handler 5 * 6 * Copyright (C) 201

理解计算机的工作方式——通过汇编一个简单的C程序并分析汇编代码

Author: 翁超平 Notice:原创作品转载请注明出处 See also:<Linux内核分析>MOOC课程http://mooc.study.163.com/course/USTC-1000029000  本文通过汇编一个简单的C程序,并分析汇编代码,来理解计算机是如何工作的.整个过程都在实验楼上完成,感兴趣的读者可以通过上面给出的课程链接自行动手学习.以下是实验过程和结果. 一.操作步骤 1.首先在通过vim程序建立main.c文件.代码如下: 图1 2.使用如下命令将main.c编

[软件测试学习]考虑到测试的代码编写/int.parse的非法输入—由一个简单的c#闰年检测程序说起

一个简单的C#的闰年检测程序 1.闰年检测的函数编写 当提起检测平年闰年时候,第一反应写出的代码 1 public static bool isLeapYear(int year){ 2 return ((year % 4 == 0 && year % 100 != 0)||(year % 400 == 0)) 3 } 但是这个并不易于测试和出现错后的修改,更改代码如下 1 public static bool isLeapYear(int year){ 2 bool check = ne

一个统计代码行数的简单方法

安装Git, 到项目目录下右击->Git Bash, 输入命令: find . -name "*.cs" | xargs wc -l 效果如下, 还是挺简便的. 一个统计代码行数的简单方法,布布扣,bubuko.com

Linux下一个简单的日志系统的设计及其C代码实现

1.概述 在大型软件系统中,为了监测软件运行状况及排查软件故障,一般都会要求软件程序在运行的过程中产生日志文件.在日志文件中存放程序流程中的一些重要信息, 包括:变量名称及其值.消息结构定义.函数返回值及其执行情况.脚本执行及调用情况等.通过阅读日志文件,我们能够较快地跟踪程序流程,并发现程序问题. 因此,熟练掌握日志系统的编写方法并快速地阅读日志文件,是对一个软件开发工程师的基本要求. 本文详细地介绍了Linux下一个简单的日志系统的设计方法,并给出了其C代码实现.本文为相关开发项目Linux