购买
下载掌阅APP,畅读海量书库
立即打开
畅读海量书库
扫码下载掌阅APP

第6章

传递与引用

J ava语言明确说明取消了指针,因为指针往往是在带来方便的同时导致代码不安全的根源,而且还会使程序变得非常复杂和难以理解,滥用指针写成的代码不亚于使用早已“臭名昭著”的GOTO语句。Java放弃指针的概念绝对是极其明智的。但这只是在Java语言中没有明确的指针定义,实质上,每一个new语句返回的都是一个指针的引用,只不过在大多数时候Java不用关心如何操作这个“指针”,更不用像在操作C++的指针那样胆战心惊,唯一要多注意的是在给函数传递对象的时候。

传值与引用问题中的静态变量、私有变量、clone等问题也是各大公司的常备考点。本章不对传值与引用基本知识做回顾和分析(请参考其他经典著作),只是通过对各公司面试题目进行全面仔细的解析,帮读者解决其中的难点。

以下的考题来自真实的笔试资料,希望读者先不要看答案,自我解答后再与答案加以比对,找出自己的不足。

6.1 传值与传引用

面试例题1: Explain call by value and call by reference.Which of these two does Java support?(解释:Java中是传值还是传引用)[中国大陆某著名网络公司B2009年9月面试题]

解析: 就像光到底是波还是粒子的问题一样众说纷纭,对于Java参数是传值还是传引用的问题,也有很多错误的理解和认识。我们首先要搞清楚的一点就是,不管Java参数的类型是什么,一律传递参数的副本。对此,thinking in Java一书给出的经典解释是,When you’re passing primitives into a method,you get a distinct copy of the primitive.When you’re passing a reference into a method,you get a copy of the reference.(如果Java是传值,那么传递的是值的副本;如果Java是传引用,那么传递的是引用的副本。)

在Java中,变量分为以下两类。

① 对于基本类型变量(int、long、double、float、byte、boolean、char),Java是传值的副本。(这里Java和C++相同。)

② 对于一切对象型变量,Java都是传引用的副本。其实,传引用副本的实质就是复制指向地址的指针,只不过Java不像C++中有显著的*和&符号。(这里Java和C++不同,在C++中,当参数是引用类型时,传递的是真实引用而不是引用副本。)

需要注意的是:String类型也是对象型变量,所以它必然是传引用副本。不要因为String在Java里面非常易于使用,而且不需要new,就被蒙蔽而把String当作基本变量类型。只不过String是一个非可变类,使得其传值还是传引用显得没什么区别。

对基本类型而言,传值就是把自己复制一份传递,即使自己的副本变了,自己也不变。而对于对象类型而言,它传的引用副本(类似于C++中的指针)指向自己的地址,而不是自己实际值的副本。为什么要这么做呢?因为对象类型是放在堆里的,一方面,速度相对于基本类型比较慢,另一方面,对象类型本身比较大,如果采用重新复制对象值的办法,浪费内存且速度又慢。就像你要张三(张三相当于函数)打开仓库并检查库里面的货物(仓库相当于地址),有必要新建一座仓库(并放入相同货物)给张三吗?没有必要,你只需要把钥匙(引用)复制一把寄给张三就可以了,张三会拿备用钥匙(引用副本,但是有时效性,函数结束,钥匙销毁)打开仓库。

在这里提一下,很多经典书籍包括thinking in Java都是这样解释的:“不管是基本类型还是对象类型,都是传值。”这种说法也不能算错,因为它们把引用副本也当作是一种“值”。但是笔者认为:传值和传引用本来就是两个不同的内容,没必要把两者弄在一起,弄在一起反而更不易理解。

下面看几个例子。

例1:

运行结果:

不难看出,虽然在 test(boolean) 方法中改变了传进来的参数值,但对这个参数源变量本身并没有影响,即对 main (String[]) 方法中的 test 变量没有影响,说明参数类型是简单类型的时候,是按值传递的。以参数形式传递简单类型的变量时,实际上是将参数的值作为一个副本传进方法函数的,那么在方法函数中不管怎么改变其值,其结果都是只改变了副本的值,而不是源值。

例2:

运行结果如下:

test(string)调用了test(StringBuffer)方法,并将string作为参数传递了进去。这里string是一个引用,Java对于引用形式传递对象类型的变量时,实际上是将引用作为一个副本传进方法函数的。那么这个函数里面的引用副本所指向的是什么呢?是对象的地址。通过引用副本(复制的钥匙)找到地址(仓库)并修改地址中的值,也就修改了对象。

例3:

运行结果如下:

为什么会这样呢?这是因为当执行str="World";时,其过程为:首先系统会自动生成一个新String对象,并把这个新对象的值设为"World!",然后把这个对象的引用赋给str(可以理解为str这把钥匙原来是指向"Hello"这个仓库,但是现在要求str这把钥匙重新指向"World"这个仓库)。我们必须清楚的一点是,String类是final类型的,因此,你不可以继承和修改这个类。str="World";其实是隐含地让Java生成一个新的String对象。既然对象都是新的,那就与原来的"Hello"没有任何关系。当函数结束,str作用消失,原来的内存地址上的内容未加改变,所以打印结果仍然是Hello。而例2中的 str.append(",World!");就不同了,StringBuffer是产生一块内存空间,相关的增、删、改操作都在其中进行,所以为其添加一句",World!"仍然是在同一段内存地址上进行,str所指向的引用并没有改变。

答案: 对于基本类型变量,Java是传值的副本;对于一切对象型变量,Java都是传引用的副本。

面试例题2: 下列代码的输出结果是多少,为什么?[美国著名软件公司I2009年11月面试题]

解析: 方法参数有基本类型,如int等,另外一种类型是Object对象。Java方法参数传对象,传的是对这个对象引用的一份副本,即地址值,跟原来的引用都是指向同一个对象。下面我们看一段程序:

上面程序中,数组传值的本质是传地址值的副本。好比说一个仓库有一把钥匙A,传值(传副本)相当于现在重新配了一把一模一样的钥匙B,但是还是指向这个仓库。当ch[0]='C'时,相当于通过这把备用钥匙B改变了仓库里的物资(比如原来装着韭菜,现在换成萝卜)。等备用钥匙的寿命结束(函数结束),再用主钥匙打开仓库时就会发现仓库已经改变(Hello变成了Cello)。

对于数组,函数里“ch[0]='C';”语句的含义是将ch所指向的内存中偏移量是0的内容由"H"换成"C",也就是说,ch所指的对象的内容被改变了,但ch并没有变。

在本题中,对象v也是传一份引用的副本,v.i=20;通过引用的副本改变原对象的值为20。但在语句Value val=new Value();中,new引出了一个新的对象,然后执行v=val;相当于引用的副本指向了一个新的Object。所以v.i=15是改变新的Object的值,而不改变原对象的值。

答案: 15 0 20

6.2 静态变量与私有变量

面试例题1: 关于静态变量的创建,哪一个选项是正确的?()

A.一旦一个静态变量被分配,它就不允许改变

B.一个静态变量在一个方法中创建,它在被调用的时候值保持不变

C.在任意多个类的实例中,一个静态变量的实例只存在一个

D.一个静态的标识符只能被应用于primitive value

解析: 选项A是对常量的描述。选项B是为了迷惑那些习惯使用VB的人。选项D所说的静态标识符可以被应用到类(但只是一个内部类)、方法和变量中。

答案: C

面试例题2: 当编译和运行下列代码时会出现什么情况?

A.编译时错误,court变量定义的是私有变量

B.编译时错误,当System.out.println方法被调用时s没有被初始化

C.编译和执行时没有输出结果

D.编译运行时输出结果是99

解析: 事实上,变量court被定义为私有变量,并不能阻止构造器对它进行初始化,所以court被初始化成99。

答案: D

面试例题3: 当你编译和运行下面的代码时,会出现下面选项中的哪种情况?()

A.编译时错误 B.编译通过并输出结果false

C.编译通过并输出结果true D.编译通过并输出结果null

解析: 定义在类里面的变量会被赋予一个默认的值,而布尔类型的默认初始值是false,所以此题的输出结果是false。

答案: B

面试例题4: 给定下面的程序。

如果用命令行参数Java Sytch Jones Diggle编译和运行上面的程序,会出现下面哪个结果?()

A.编译通过并输出Ms Diggle Please pay $2000结果

B.编译时错误

C.编译通过并输出Ms Jones Please pay $2000

D.编译通过但是运行时错误

解析: 因为main方法是静态的,不能访问一个非静态的变量x。

答案: B

6.3 输入/输出流

面试例题1: 假设任何异常处理已经被创建,下列哪个选项是创建RandomAccessFile类实例的正确选项?()

A.RandomAccessFile raf=new RandomAccessFile("myfile.txt","rw");

B.RandomAccessFile raf=new RandomAccessFile(new Data InputStr-eam());

C.RandomAccessFile raf=new RandomAccessFile("myfile.txt");

D.RandomAccessFile raf=new RandomAccessFile(new File ("myfile.txt"));

解析: 在Java的I/O结构中,RandomAccessFile是比较不寻常的类,它直接继承于Object,它并不属于Streams结构的一部分。

答案: A

面试例题2: 需要读一个比较大的文本文件,这个文件里有很多字节的数据,那么下列最合适读这类文件的选项是哪个?()

A.new FileInputStream("file.name");

B.new InputStreamReader(new FileInputStream("file.name"));

C.new BufferedReader(new InputStreamReader(new FileInputStream("file.name")));

D.new RandomAccessFile raf=new RandomAccessFile("myfile.txt","+rw");

解析: 这道题最主要的是具有很多字节数的文本文件,那么就要考虑到BufferedReader。

答案: C

面试例题3: 请给出一段代码描述字符串写入文件。

答案: 代码如下。

面试例题4: 写一个Java应用程序,从键盘输入两个整数,然后输出它们的平方值及立方值。

解析: 在Java中没有像C语言那样有一个专供接受从键盘输入值的scanf函数,所以一般的做法是从键盘输入一行字符,保存到字符串s中,再将字符组成的字符串s转换为整型数据后返回。

答案: 代码如下。

面试例题5: 8、64、256都是2的阶次方数(例如,8是2的3次方),用Java编写程序来判断一个整数是不是2的阶次方数,并说明哪个方法更好。

解析: 如果一个数是2的阶次方数,那么它的二进制数的首位一般是1,后面接若干个0。比如8就是1000,64是1000000。如果将这个数减1后,再与该数做和(&)运算,则应该全为0(例如,8与7,一个二进制数是1000,一个二进制数是111,它们做和运算后全为0)。所以((d-1)&(d))==0。

答案: 代码如下。

6.4 序列化

面试例题: 如何实现Java的序列化?[英国著名图形软件公司面试题]

解析: Java的“对象序列化”能将一个实现了Serializable接口的对象转换成一组byte,这样日后要用这个对象的时候,就能把这些byte数据恢复出来,并据此重新构建那个对象。这一点甚至在跨网络的环境下也是如此,这就意味着序列化机制能自动补偿操作系统方面的差异。也就是说,可以在Windows机器上创建一个对象,序列化之后,再通过网络传到UNIX机器上,最后在那里进行重建。不用担心在不同的平台上数据是怎样表示的,以及byte顺序怎样,或者别的什么细节。

对象序列化能实现“轻量级的persistence(lightweight persistence)”。所谓persistence,是指对象的生命周期不是由程序是否运行决定的,在程序的两次调用之间对象仍然还活着。通过“将做过序列化处理的对象写入磁盘,等到程序再次运行的时候再把它读出来”,可以达到persistence的效果。之所以说“轻量级”,是因为不能用像“persistent”这样的关键词来直接定义一个对象,然后让系统去处理所有的细节(虽然将来有可能会这样)。相反,必须明确地进行序列化(serialize)和解序列化(deserialize)。

之所以要在语言里加入对象序列化,是因为要用它来实现两个重要的功能。Java的远程方法调用(Remote Method Invocation,简称RMI)能像调用自己机器上的对象那样去调用其他机器上的对象。当向远程对象传递消息的时候,就需要通过对象序列化来传送参数和返回值。对JavaBean来说,对象序列化也是必不可少的。Bean的状态信息通常是在设计时配置的,这些状态信息必须保存起来,供程序启动的时候用,对象序列化就负责这个工作。

答案: 序列化一个对象还是比较简单的,只要让它实现Serializable接口就行了(这是一个“标记接口”,tagging interface,没有任何方法)。但是,当语言引入序列化概念之后,它的很多标准类库的类,包括primitive的wrapper类、所有的容器类,以及别的很多类,都会相应地发生改变,甚至连Class对象都会被序列化。

要想序列化对象,必须先创建一个OutputStream,然后把它嵌进ObjectOutputStream。这时就能用writeObject()方法把对象写入OutputStream。读的时候需要把InputStream嵌到ObjectInputStream中,然后再调用readObject()方法。不过这样读出来的只是一个Object的reference,因此,在用之前,还得先下传。

对象序列化不仅能保存对象的副本,而且还会跟着对象中的reference把它所引用的对象也保存起来,然后再继续跟踪那些对象的reference,以此类推。这种情形常被称为“单个对象所联结的‘对象网’”。这个机制所涵盖的范围不仅包括对象的成员数据,而且还包含数组里面的reference。如果要自己实现对象序列化,那么编写跟踪这些链接的程序将会是一件非常痛苦的任务。但是,Java的对象序列化就能精确无误地做到这一点,毫无疑问,它的遍历算法是做过优化的。 RDqauGaD7fNBpIfC0/J6Q2+HhNmdzVAokb0RRo43btL4npp38LwWJdXTA5Tsk6VU

点击中间区域
呼出菜单
上一章
目录
下一章
×