有这样一种说法如今争锋于IT战场的两大势力MS一族偏重于底层实现Java一族偏重于系统架构说法根据无从考证但从两大势力各自的社区力量和图书市场已有佳作不难看出此说法不虚于是事情的另一面让人忽略了偏巧我是一个喜欢探究底层实现的Java程序员虽然我的喜好并非纯正咖啡剑走偏锋却别是一番风味
Reference Java世界泰山北斗级大作《Thinking In Java》切入Java就提出Everything is Object在Java这个充满Object的世界中reference是一切谜题的根源所有的故事都是从这里开始的
Reference是什么?
如果你和我一样在进入Java世界之前曾经浪迹于C/C++世界就一定不会对指针陌生谈到指针往日种种不堪回首的经历一下子涌上心头这里不是抱怨的地方让我们暂时忘记指针的痛苦回忆一下最初接触指针的甜蜜吧!
还记得你看过的教科书中如何讲解指针吗?留在我印象中的一种说法是指针就是地址如同门牌号码一样有了地址你可以轻而易举找到一个人家而不必费尽心力的大海捞针C++登上历史舞台reference也随之而来容我问个小问题指针和reference区别何在?我的答案来自于在C++世界享誉盛名的《More Effective C++》
没有null referencereference必须有初值
使用reference要比使用指针效率高因为reference不需要测试其有效性指针可以重新赋值而reference总是指向它最初获得的对象
设计选择
当你指向你需要指向的某个东西而且绝不会改指向其它东西或是当你实作一个运算符而其语法需要无法有指针达成你就应该选择reference其它任何时候请采用指针
这和Java有什么关系?
初学Java鑒于reference的名称我毫不犹豫的将它和C++中的reference等同起来不过我错了在Java中reference 可以随心所欲的赋值置空对比一下上面列出的差异就不难发现Java的reference如果要与C/C++对应它不过是一个穿着 reference外衣的指针而已于是所有关于C中关于指针的理解方式可以照搬到Java中简而言之reference就是一个地址我们可以把它想象成一个把手抓住它就抓住了我们想要操纵的数据如同掌握C的关键在于掌握指针探索Java的钥匙就是reference
一段小程序
我知道太多的文字总是令人犯困那就来段代码吧!
public class ReferenceTricks
{
public static void main(String[] args)
{
ReferenceTricks r = new ReferenceTricks();
// reset integer
ri = ;
Systemoutprintln
(Before changeInteger: + ri);
changeInteger(r);
Systemoutprintln
(After changeInteger: + ri);
// just for format
Systemoutprintln();
// reset integer
ri = ;
Systemoutprintln
(Before changeReference: + ri);
changeReference(r);
Systemoutprintln
(After changeReference: + ri);
}
private static void
changeReference(ReferenceTricks r)
{
r = new ReferenceTricks();
ri = ;
Systemoutprintln
(In changeReference: + ri);
}
private static void
changeInteger(ReferenceTricks r)
{
ri = ;
Systemoutprintln
(In changeInteger: + ri);
}
public int i;
}
我知道把一个字段设成public是一种不好的编码习惯这里只是为了说明问题如果你有兴趣自己运行一下这个程序你已经运行过了吗?结果如何?是否如你预期?下面是我在自己的机器上运行的结果
Before changeInteger:
In changeInteger:
After changeInteger:
Before changeReference:
In changeReference:
After changeReference:
这里我们关注的是两个changechangeReference和changeInteger从输出的内容中我们可以看出两个方法在调用前和调用中完全一样差异出现在调用后的结果
糊涂的讲解
先让我们来分析一下changeInteger的行为
前面说过了Java中的reference就是一个地址它指向了一个内存空间这个空间存放着一个对象的相关信息这里我们暂时不去关心这个内存具体如何排布只要知道通过地址我们可以找到r这个对象的i字段然后我们给它赋成
既然这个字段的内容得到了修改从函数中返回之后它自然就是改动后的结果了所以调用之后r对象的i字段依然是下图展示了changeInteger调用前后内存变化
让我们把目光转向changeReference从代码上我们可以看出同changeInteger之间的差别仅仅在于多了这么一句
r = new ReferenceTricks();
这条语句的作用是分配一块新的内存然后将r指向它执行完这条语句r就不再是原来的r但它依然是一个ReferenceTricks的对象所以我们依然可以对这个r的i字段赋值到此为止一切都是那么自然
顺着这个思路继续下去的话执行完changeReference输出的r的i字段那么应该是应该是新内存中的i所以应该是至于那块被我们抛弃的内存Java的GC功能自然会替我们善后的
事与愿违
实际的结果我们已经看到了输出的是肯定哪个地方错了究竟是哪个地方呢?
参数传递的秘密
知道方法参数如何传递吗?
记得刚开始学编程那会儿老师教导所谓参数有形式参数和实际参数之分参数列表中写的那些东西都叫形式参数在实际调用的时候它们会被实际参数所替代
编译程序不可能知道每次调用的实际参数都是什么于是写编译器的高手就出个办法让实际参数按照一定顺序放到一个大家都可以找得到的地方以此作为方法调用的一种约定所谓没有规矩不成方圆有了这个规矩大家协作起来就容易多了这个公共数据区现在编译器的选择通常是栈而所谓的顺序就是形式参数声明的顺序
显然程序运行的过程中作为实际参数的变量可能遍布于内存的各个位置而并不一定要老老实实的呆在栈里为了守规矩程序只好将变量复制一份到栈中也就是通常所说的将参数压入栈中
我刚才说什么来着?将变量复制一份到栈中没错复制!
这就是所谓的值传递
C语言的旷世经典《The C Programming Language》开篇的第一章中谈到实际参数时说在C中所有函数的实际参数都是传值的
马上会有人站出来错了还有传地址比如以指针传递就是传地址不错传指针就是传地址在把指针视为地址的时候是否考虑过这样一个问题它也是一个变量前面的讨论中说过了参数传递必须要把参数压入栈中作为地址的指针也不例外所以必须把这个指针也复制一份函数中对于指针操作实际上是对于这个指针副本的操作
Java的reference等于C的指针所以在Java的方法调用中reference也要复制一份压入堆栈在方法中对reference的操作就是对这个reference副本的操作
谜底揭晓
好让我们回到最初的问题上在changeReference中对于reference的赋值实际上是对这个reference的副本进行赋值而对于reference的本尊没有产生丝毫的影响
回到调用点本尊醒来它并不知道自己睡去的这段时间内发生过什么所以只好当作什么都没发生过一般就这样副本消失了在方法中对它的修改也就烟消云散了
也许你会问出这样的问题听了你的解释我反而对changeInteger感到迷惑了既然是对于副本的操作为什么changeInteger可以运作正常?这是很有趣的现象
好那我就用前面的说法解释一下changeInteger的运作所谓复制其结果必然是副本完全等同于本尊reference复制的结果必然是两个reference指向同一块内存空间
虽然在方法中对于副本的操作并不会影响到本尊但对内存空间的修改确实实实在在的回到调用点虽然本尊依然不知道曾经发生过的一切但它按照原来的方式访问内存的时候取到的确是经过方法修改之后的内容于是方法可以把自己的影响扩展到方法之外
多说几句
这个问题起源于我对C/C++中同样问题的思考同C/C++相比在changeReference中对reference赋值可能并不会造成什么很严重的后果而在C/C++中这么做却会造成臭名昭着的内存洩漏根本的原因在于Java拥有了可爱的GC功能即便这样我仍不推荐使用这种的手法毕竟GC已经很忙了我们怎么好意思再麻烦人家
在C/C++中这个问题还可以继续引申既然在函数中对于指针直接赋值行不通那么如何在函数中修改指针呢?答案很简单指针的指针也就是把原来的指针看作一个普通的数据把一个指向它的指针传到函数中就可以了
同样的问题到了Java中就没有那么美妙的解决方案了因为Java中可没有reference的reference这样的语法可能的变通就是将reference进行封装成类至于值不值公道自在人心