`

理解java reference

阅读更多

Java世界泰山北斗级大作《Thinking In Java》切入Java就提出“Everything is Object”。在Java这个充满Object的世界中,reference是一切谜题的根源,所有的故事都是从这里开始的。

Reference是什么?

      如果你和我一样在进入Java世界之前曾经浪迹于C/C++世界,就一定不会对指针陌生。谈到指针,往日种种不堪回首的经历一下子涌上心头,这里不是抱怨 的地方,让我们暂时忘记指针的痛苦,回忆一下最初接触指针的甜蜜吧!还记得你看过的教科书中,如何讲解指针吗?留在我印象中的一种说法是,指针就是地址, 如同门牌号码一样,有了地址,你可以轻而易举找到一个人家,而不必费尽心力的大海捞针。

      C++登上历史舞台,reference也随之而来,容我问个小问题,指针和reference区别何在?我的答案来自于在C++世界享誉盛名的《More Effective C++》。

 

  1. 没有null reference。
  2. reference必须有初值。
  3. 使用reference要比使用指针效率高。因为reference不需要测试其有效性。
  4. 指针可以重新赋值,而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 >

分享到:
评论

相关推荐

    拓胜技术专家教你如何深入理解Java四种引用类型

    ava有四种引用类型,strongreference,softreference,weakreference,phantomreference。本篇文档主要的就是进阶的介绍和了解这四种引用类型的异同点,助于你对java的更好的学习理解

    深入理解理解java虚拟机

    垃圾回收重点区域:堆和方法区部分...  基本思想:通过一系列的名为“GC Root”的对象作为起点,从这些节点向下搜索,搜索所走过的路径称为引用链(Reference Chain),当一个对象到GC Root没有任何引用链相连时,则该

    Java线程池文档

    Reference: 《创建Java线程池》[1],《Java线程:新特征-线程池》[2], 《Java线程池学习》[3],《线程池ThreadPoolExecutor使用简介》[4],《Java5中的线程池实例讲解》[5],《ThreadPoolExecutor使用和思考》[6] ...

    理解Java中的弱引用(Weak Reference)

    本篇文章尝试从What、Why、How这三个角度来探索Java中的弱引用,理解Java中弱引用的定义、基本使用场景和使用方法。由于个人水平有限,叙述中难免存在不准确或是不清晰的地方,希望大家可以指出,谢谢大家:)  1....

    Spring Dynamic Modules Reference Guide中文预览版

    Spring Dynamic Modules Reference Guide翻译已经接近完成,但最近项目较紧,故暂停,现将预览版发布,正好供大家边看边审查。 此预览版除了最后的测试和附录仅翻译了部分之外,其余均已完整翻译,供大家一般...

    JAVA性能瓶颈和漏洞检测

    Reference Graph 和 Instance Detail:跟踪内存使用和对象引用; 垃圾回收分析:检测过多的短期对象和垃圾收集详情; Snapshot 比对:确定代码改变对内存使用的影响。 JProbe Coverage *JProbe Coverage 帮助开发...

    Practical Java(中文版(繁体+简体))

    實踐1:引數以by value方式而非by reference方式傳遞1 實踐2:對不變的data和object reference 使用final 3 實踐3:缺省情況㆘所有non-static函數都可被覆寫6 實踐4:在arrays 和Vectors 之間慎重選擇7 實踐5:多態...

    Java 语言基础 —— 非常符合中国人习惯的Java基础教程手册

    在 java 语言中,Java 程序的基本单位是类,也就是说:一个 Java 程序是由多个类组成 的。定义一个类与定义一个数据类型是有区别的。在程序设计语言中,把定义数据类型的能 力作为一种很重要的能力来对待。在面向...

    spring-framework-4-reference.pdf

    利用业余时间对此进行翻译,并在原文的基础上,插入配图,图文并茂方便用户理解。如有 勘误欢迎指正,点此提问。如有兴趣,也可以参与到本翻译工作中来 :) 如果想了解 Spring 5 方面的内容,移步至笔者所著的...

    生鲜配送平台源码java-Python_Reference:Python_Reference

    生鲜配送平台源码java Table of Contents Python语言特性 1 Python的函数参数传递 看两个例子: a = 1 def fun(a): a = 2 fun(a) print a # 1 a = [] def fun(a): a.append(1) fun(a) print a # [1] 所有的变量都可以...

    J2ME the Complete Reference j2me的完全开发参考

    讲解了开发J2ME、Web服务、PDA和移动电话应用程序所需的知识与技术,并且提供了基于J2ME开发应用软件,使用应用程序开发包、数据库以及Web服务等详细例程和应用程序接口的详细参考指南,帮助读者全面理解J2ME的基本...

    Java 虚拟机面试题全面解析(干货)

    Java 虚拟机面试题全面解析,《深入理解Java虚拟机》干货版,自己总结,希望能够帮助大家,免费下载~什么是类加载机制? 虚拟机和物理机的区别是什么? 运行时栈帧结构 Java方法调用 什么是方法调用? Java的方法调用,...

    JAVA性能瓶颈和漏洞检测.JProbe.Suite.v7.0.part2

    Reference Graph 和 Instance Detail:跟踪内存使用和对象引用; 垃圾回收分析:检测过多的短期对象和垃圾收集详情; Snapshot 比对:确定代码改变对内存使用的影响。 JProbe Coverage *JProbe Coverage 帮助...

    JAVA性能瓶颈和漏洞检测].JProbe.Suite.v7.0.part1

    Reference Graph 和 Instance Detail:跟踪内存使用和对象引用; 垃圾回收分析:检测过多的短期对象和垃圾收集详情; Snapshot 比对:确定代码改变对内存使用的影响。 JProbe Coverage *JProbe Coverage 帮助...

    JVM规范--高手总结

    2.5.JVM简单理解 16 2.5.1 Java栈 16 2.5.2 堆 16 2.5.3 堆栈分离的好处 20 2.5.4 堆(heap)和栈(stack) 20 JAVA垃圾收集器 21 3.1 垃圾收集简史 21 3.2 常见的垃圾收集策略 21 3.2.1 Reference Counting(引用计数) ...

    Spring-Reference_zh_CN(Spring中文参考手册)

    9.5.1. 理解Spring的声明式事务管理实现 9.5.2. 第一个例子 9.5.3. 回滚 9.5.4. 为不同的bean配置不同的事务语义 9.5.5. &lt;tx:advice/&gt; 有关的设置 9.5.6. 使用 @Transactional 9.5.6.1. @Transactional 有关的设置 ...

    JavaServer Faces 2.0完全参考手册(JSF2.0中文版) 1/2

    他与其他人合著了JavaServer Faces:The Complete Reference一书,并且是Secrets of the Rock Star Programmers一书的作者。 沙尔克,Chris Schalk是developer advocate,致力于提升Google的API和技术。他当前在国际...

    JavaServer Faces 2.0完全参考手册(JSF2.0中文版).part1

    他与其他人合著了JavaServer Faces:The Complete Reference一书,并且是Secrets of the Rock Star Programmers一书的作者。 沙尔克,Chris Schalk是developer advocate,致力于提升Google的API和技术。他当前在国际...

Global site tag (gtag.js) - Google Analytics