Java世界泰山北斗级大作《Thinking In Java》切入Java就提出“Everything is Object”。在Java这个充满Object的世界中,reference是一切谜题的根源,所有的故事都是从这里开始的。
Reference是什么?
如果你和我一样在进入Java世界之前曾经浪迹于C/C++世界,就一定不会对指针陌生。谈到指针,往日种种不堪回首的经历一下子涌上心头,这里不是抱怨
的地方,让我们暂时忘记指针的痛苦,回忆一下最初接触指针的甜蜜吧!还记得你看过的教科书中,如何讲解指针吗?留在我印象中的一种说法是,指针就是地址,
如同门牌号码一样,有了地址,你可以轻而易举找到一个人家,而不必费尽心力的大海捞针。
C++登上历史舞台,reference也随之而来,容我问个小问题,指针和reference区别何在?我的答案来自于在C++世界享誉盛名的《More Effective C++》。
-
没有null reference。
-
reference必须有初值。
-
使用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
r.i = 0;
System.out.println("Before changeInteger:"
+ r.i);
changeInteger(r);
System.out.println("After changeInteger:"
+ r.i);
// just for format
System.out.println();
// reset integer
r.i = 0;
System.out.println("Before changeReference:"
+ r.i);
changeReference(r);
System.out.println("After changeReference:"
+ r.i);
}
private
static
void
changeReference(ReferenceTricks r) {
r = new
ReferenceTricks();
r.i = 5;
System.out.println("In changeReference: "
+ r.i);
}
private
static
void
changeInteger(ReferenceTricks r) {
r.i = 5;
System.out.println("In changeInteger:"
+ r.i);
}
public
int
i;
}
对不起,我知道,把一个字段设成public是一种不好的编码习惯,这里只是为了说明问题。
如果你有兴趣自己运行一下这个程序,我等你!
OK,你已经运行过了吗?结果如何?是否如你预期?下面是我在自己的机器上运行的结果:
Before changeInteger:0
geInteger:5
hangeInteger:5
changeReference:0
geReference: 5
hangeReference:0
这里,我们关注的是两个change——changeReference和changeInteger。从输出的内容中,我们可以看出,两个方法在调用前和调用中完全一样,差异出现在调用后的结果。
糊涂的讲解
先让我们来分析一下changeInteger的行为。
前
面说过了,Java中的reference就是一个地址,它指向了一个内存空间,这个空间存放着一个对象的相关信息。这里我们暂时不去关心这个内存具体如
何排布,只要知道,通过地址,我们可以找到r这个对象的i字段,然后我们给它赋成5。既然这个字段的内容得到了修改,从函数中返回之后,它自然就是改动后
的结果了,所以调用之后,r对象的i字段依然是5。下图展示了changeInteger调用前后内存变化。
Reference +--------+ Reference +--------+
---------->| i = 0 | ---------->| i = 5 |
|--------| |--------|
| Memory | | Memory |
| | | |
| | | |
+--------+ +--------+
调用
changeReference
之前
调用
changeReferenc
之后
让我们把目光转向changeReference。
从代码上,我们可以看出,同changeInteger之间的差别仅仅在于多了这么一句。
r = new ReferenceTricks();
这条语句的作用是分配一块新的内存,然后将r指向它。
执行完这条语句,r就不再是原来的r,但它依然是一个ReferenceTricks的对象,所以我们依然可以对这个r的i字段赋值。到此为止,一切都是那么自然。
Reference +--------+ +--------+
---------->| i = 0 | | i = 0 |
|--------| |--------|
| Memory | | Memory |
| | Reference |--------|
| | ---------->| i = 5 |
+--------+ +--------+
调用
changeReference
之前
调用
changeReferenc
之后
顺着这个思路继续下去的话,执行完changeReference,输出的r的i字段,那么应该是应该是新内存中的i,所以应该是5。至于那块被我们抛弃的内存,Java的GC功能自然会替我们善后的。
事与愿违。
实际的结果我们已经看到了,输出的是0。
肯定哪个地方错了,究竟是哪个地方呢?
参数传递的秘密
知道方法参数如何传递吗?
记得刚开始学编程那会儿,老师教导,所谓参数,有形式参数和实际参数之分,参数列表中写的那些东西都叫形式参数,在实际调用的时候,它们会被实际参数所替代。
编译程序不可能知道每次调用的实际参数都是什么,于是写编译器的高手就出个办法,让实际参数按照一定顺序放到一个大家都可以找得到的地方,以此作为
方法调用的一种约定。所谓"没有规矩,不成方圆",有了这个规矩,大家协作起来就容易多了。这个公共数据区,现在编译器的选择通常是"栈",而所谓的顺序
就是形式参数声明的顺序。
显然,程序运行的过程中,作为实际参数的变量可能遍布于内存的各个位置,而并不一定要老老实实的呆在栈里。为了守"规矩",程序只好将变量复制一份到栈中,也就是通常所说的将参数压入栈中。
打起精神,谜底就要揭晓了。
我刚才说什么来着?将变量复制一份到栈中,没错,"复制"!
这就是所谓的值传递。
C语言的旷世经典《The C Programming Language》开篇的第一章中,谈到实际参数时说,"在C中,所有函数的实际参数都是传‘值'的"。
马上会有人站出来,"错了,还有传地址,比如以指针传递就是传地址"。
不错,传指针就是传地址。在把指针视为地址的时候,是否考虑过这样一个问题,它也是一个变量。前面的讨论中说过了,参数传递必须要把参数压入栈中,作为地址的指针也不例外。所以,必须把这个指针也复制一份。函数中对于指针操作实际上是对于这个指针副本的操作。
Java的reference等于C的指针。所以,在Java的方法调用中,reference也要复制一份压入堆栈。在方法中对reference的操作就是对这个reference副本的操作。
谜底揭晓
好,让我们回到最初的问题上。
在changeReference中对于reference的赋值实际上是对这个reference的副本进行赋值,而对于reference的本尊没有产生丝毫的影响。
回到调用点,本尊醒来,它并不知道自己睡去的这段时间内发生过什么,所以只好当作什么都没发生过一般。就这样,副本消失了,在方法中对它的修改也就烟消云散了。
也许你会问出这样的问题,"听了你的解释,我反而对changeInteger感到迷惑了,既然是对于副本的操作,为什么changeInteger可以运作正常?"
呵呵,很有趣的大脑短路现象。
好,那我就用前面的说法解释一下changeInteger的运作。
所谓复制,其结果必然是副本完全等同于本尊。reference复制的结果必然是两个reference指向同一块内存空间。
虽然在方法中对于副本的操作并不会影响到本尊,但对内存空间的修改确实实实在在的。
回到调用点,虽然本尊依然不知道曾经发生过的一切,但它按照原来的方式访问内存的时候,取到的确是经过方法修改之后的内容。
于是方法可以把自己的影响扩展到方法之外。
demo下载
多说几句
这
个问题起源于我对C/C++中同样问题的思考。同C/C++相比,在changeReference中对reference赋值可能并不会造成什么很严重
的后果,而在C/C++中,这么做却会造成臭名昭著的“内存泄漏”,根本的原因在于Java拥有了可爱的GC功能。即便这样,我仍不推荐使用这种的手法,
毕竟GC已经很忙了,我们怎么好意思再麻烦人家。
在C/C++中,这个问题还可以继续引申。既然在函数中对于指针直接赋值行不通,那么如何在函数中修改指针呢?答案很简单,指针的指针,也就是把原来的指针看作一个普通的数据,把一个指向它的指针传到函数中就可以了。
同样的问题到了Java中就没有那么美妙的解决方案了,因为Java中可没有reference的reference这样的语法。可能的变通就是将reference进行封装成类。至于值不值,公道自在人心。
参考文献
1 《Thinking in Java》
2 《More Effective C++》
3 《The C Programming Language》
转载自 <
http://news.csdn.net/n/20050517/21294.html
>
分享到:
相关推荐
### Neo4j Java Reference 3.0:深入理解图数据库扩展与高级应用 #### 概述 《Neo4j Java Reference 3.0》是一本详细介绍如何使用Java语言来开发和扩展Neo4j图数据库的专业指南。本书不仅覆盖了Neo4j的核心功能,...
在实际编程中,`Reference`类的使用需要谨慎,因为它涉及到对Java内存模型的深入理解。过度依赖软引用可能导致内存泄漏,而滥用虚引用可能导致不必要的复杂性。正确地使用`Reference`可以帮助优化内存使用,提高应用...
《Java The Complete Reference 9th》还会讲解Java语言的面向对象原则,包括类、对象、继承、多态性和封装性等核心概念,这是理解Java编程范式的基础。此外,书中的实践练习和示例代码有助于读者加深对Java编程的...
在Neo4j-java-reference-3.3这份参考指南中,涵盖了多个关于如何在Java环境中使用和扩展Neo4j数据库的高级主题。文档内容涉及到如何嵌入Neo4j到Java应用中、使用Neo4j的遍历框架、手动索引、事务管理、在线备份以及...
Java Reference源码解析 Java Reference是Java语言的一种机制,用于追踪对象的可达性和垃圾...通过了解Java Reference的源码,我们可以更好地理解Java的垃圾回收机制,并且能够更好地应用Java Reference在实际开发中。
理解Java的reference对于深入理解对象、内存管理和方法参数传递至关重要。在面试时,面试官可能会通过考察对reference的理解,来评估候选人的Java基础。例如,可能会问到reference和指针的区别,对象复制与引用复制...
然而,即便有了自动化工具和JavaBeans的支持,深入理解AWT对于Java开发者而言仍然是必不可少的。 #### Java 1.1中的AWT更新:挑战与机遇 面对Java开发者的重大挑战之一是AWT的不断演变。Java 1.0.2正逐渐被Java ...
《Java The Complete Reference 第九版源代码》是一个深入学习Java编程语言的重要资源。这个压缩包包含的源代码是作者在书中讲解各种Java概念和技术时所使用的示例程序,旨在帮助读者更好地理解和应用Java编程知识。...
为了深入理解Java内存管理,了解这四种引用类型是至关重要的。 首先,我们要了解,在JDK1.2版本之前,Java中的引用只有强引用一种,也就是说,如果一个对象有一个强引用指向它,那么这个对象就不会被垃圾收集器回收...
这本书以简洁、实用的方式提供了Java编程的核心信息,旨在帮助读者快速查阅和理解Java语言的关键概念和技术。作为一本桌面速查手册,它覆盖了广泛的Java知识领域,包括语法、API、工具和最佳实践。 首先,本书深入...
### Java™:The Complete Reference, Seventh Edition #### 关于作者:Herbert Schildt Herbert Schildt 是 Java、C、C++ 和 C# 语言领域的权威专家,同时也是 Windows 编程的大师级人物。他的编程书籍在全球范围...
Oracle8i Supplied Java Packages Reference ...5. Oracle8i Supplied Java Packages Reference Release 3 (8.1.7) 对于开发者和使用者来说非常重要,因为它可以帮助他们更好地理解和使用 Oracle8i 中的 Java 包。
Java Primitive 与 Reference 类型详解 Java 作为一种面向对象的语言,数据类型可以分为两类:Primitive 类型和 ...理解 Primitive 类型和 Reference 类型的区别是非常重要的,因为它们对 Java 编程的影响是深远的。
深入理解 Java 虚拟机笔记 Java 虚拟机(JVM)是 Java 语言的运行环境,它负责解释和执行 Java 字节码。下面是 Java 虚拟机相关的知识点: 虚拟机内存结构 Java 虚拟机的内存结构主要包括以下几个部分: * 方法...
这本书面向初学者和经验丰富的开发者,提供了Java语言的深入理解和实践指导。标签“Java”表明了本书的主要内容集中在Java编程领域。 在本书中,读者可以学习到以下几个核心知识点: 1. **Java语言基础**:包括...
Oracle 9i Support for JavaServer Pages Reference Release 2 (9.2) Oracle 9i Support for JavaServer Pages Reference Release 2 (9.2) 是 Oracle 公司发布的一份关于 JavaServer Pages(JSP)的参考指南,该...
Oracle 9i 供给 Java 包参考手册Release 2(9.2)是 Oracle 公司发布的一份关于 Java 包的参考手册,该手册主要面向 Oracle 9i 数据库管理系统的开发者和管理员,旨在帮助他们更好地理解和使用 Oracle 供给的 Java ...
文档提供了一个遍历框架的指南,帮助开发者理解和使用遍历。 ### 手动索引 在某些情况下,开发者可能需要使用显式的索引。文档提供了如何在Neo4j中使用手动索引的详细指南。 ### 事务管理 事务在图形数据库中...