Java程序员面试宝典第2版.docx
- 文档编号:3310362
- 上传时间:2022-11-21
- 格式:DOCX
- 页数:12
- 大小:24.44KB
Java程序员面试宝典第2版.docx
《Java程序员面试宝典第2版.docx》由会员分享,可在线阅读,更多相关《Java程序员面试宝典第2版.docx(12页珍藏版)》请在冰豆网上搜索。
Java程序员面试宝典第2版
6
第章
传递与引用
J
ava语言明确说明取消了指针,因为指针往往是在带来方便的同时导致代码不安全的根源,而且还会使程序变得非常复杂和难以理解,滥用指针写成的代码不亚于使用早已臭名昭著的GOTO语句。
Java放弃指针的概念绝对是极其明智的。
但这只是在Java语言中没有明确的指针定义,实质上,每一个new语句返回的都是一个指针的引用,只不过在大多数时候Java不用关心如何操作这个“指针”,更不用像在操作C++的指针那样胆战心惊,唯一要多注意的是在给函数传递对象的时候。
传值与引用问题中的静态变量、私有变量、clone等问题也是各大公司的常备考点。
本章不对传值与引用基本知识做回顾和分析(请参考其他经典著作),只是通过对各公司面试题目进行全面仔细的解析,帮读者解决其中的难点。
以下的考题来自真实的笔试资料,希望读者先不要看答案,自我解答后再与答案加以比对,找出自己的不足。
6.1传值与传引用
面试例题1:
Explaincallbyvalueandcallbyreference.WhichofthesetwodoesJavasupport?
(解释:
Java中是传值还是传引用)[中国大陆某著名网络公司B2009年9月面试题]
解析:
就像光到底是波还是粒子的问题一样众说纷纭,对于Java参数是传值还是传引用的问题,也有很多错误的理解和认识。
我们首先要搞清楚一点就是:
不管Java参数的类型是什么,一律传递参数的副本。
对此,thinkinginJava一书给出的经典解释是Whenyou’repassingprimitivesintoamethod,yougetadistinctcopyoftheprimitive.Whenyou’repassingareferenceintoamethod,yougetacopyofthereference.(如果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++中的指针)指向自己的地址,而不是自己实际值的副本。
为什么要这么做呢?
因为对象类型是放在堆里的,一方面,速度相对于基本类型比较慢,另一方面,对象类型本身比较大,如果采用重新复制对象值的办法,浪费内存且速度又慢。
就像你要张三(张三相当于函数)打开仓库并检查库里面的货物(仓库相当于地址),有必要新建一座仓库(并放入相同货物)给张三么?
没有必要,你只需要把钥匙(引用)复制一把寄给张三就可以了,张三会拿备用钥匙(引用副本,但是有时效性,函数结束,钥匙销毁)打开仓库。
在这里提一下,很多经典书籍包括thinkinginJava都是这样解释的:
“不管是基本类型还是对象类型,都是传值。
”这种说法也不能算错,因为它们把引用副本也当做是一种“值”。
但是笔者认为:
传值和传引用本来就是两个不同的内容,没必要把两者弄在一起,弄在一起反而更不易理解。
下面看几个例子。
例1:
publicclassTest{
publicstaticvoidtest(booleantest){
test=!
test;
System.out.println("Intest(boolean):
test="+test);
}
publicstaticvoidmain(String[]args){
booleantest=true;
System.out.println("Beforetest(boolean):
test="+test);
test(test);
System.out.println("Aftertest(boolean):
test="+test);
}
}
运行结果:
Beforetest(boolean):
test=true
Intest(boolean):
test=false
Aftertest(boolean):
test=true
不难看出,虽然在test(boolean)方法中改变了传进来的参数值,但对这个参数源变量本身并没有影响,即对main(String[])方法中的test变量没有影响,说明参数类型是简单类型的时候,是按值传递的。
以参数形式传递简单类型的变量时,实际上是将参数的值作为一个副本传进方法函数的,那么在方法函数中不管怎么改变其值,其结果都是只改变了副本的值,而不是源值。
例2:
publicclassTest{
publicstaticvoidtest(StringBufferstr){
str.append(",World!
");
}
publicstaticvoidmain(String[]args){
StringBufferstring=newStringBuffer("Hello");
test(string);
System.out.println(string);
}
}
运行结果如下:
Hello,World!
test(string)调用了test(StringBuffer)方法,并将string作为参数传递了进去。
这里string是一个引用,Java对于引用形式传递对象类型的变量时,实际上是将引用作为一个副本传进方法函数的。
那么这个函数里面的引用副本所指向的是什么呢?
是对象的地址。
通过引用副本(复制的钥匙)找到地址(仓库)并修改地址中的值,也就修改了对象。
例3:
publicclassTest{
publicstaticvoidtest(Stringstr){
str="World";
}
publicstaticvoidmain(String[]args){
Stringstring="Hello";
test(string);
System.out.println(string);
}
}
运行结果如下:
Hello
为什么会这样呢?
这是因为当执行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月面试题]
classValue{
publicinti=15;
}
publicclassTest{
publicstaticvoidmain(String[]args){
Testt=newTest();
t.first();
}
publicvoidfirst(){
inti=5;
Valuev=newValue();
v.i=25;
second(v,i);
System.out.println(v.i);
}
publicvoidsecond(Valuev,inti){
i=0;
v.i=20;
Valueval=newValue();
v=val;
System.out.println(v.i+""+i);
}
}
解析:
方法参数有基本类型,如int等,另外一种类型是Object对象。
Java方法参数传对象,传的是对这个对象引用的一份副本,即地址值,跟原来的引用都是指向同一个对象。
下面我们看一段程序:
publicclassTest
{
publicstaticvoidmain(String[]args)
{
charch[]={'H','e','l','l','o'};
change(ch);
System.out.println(ch);
}
publicstaticvoidchange(charch[])
{
ch[0]='C';
}
}
上面程序中,数组传值的本质是传地址值的副本。
好比说一个仓库有一把钥匙A,传值(传副本)相当于现在重新配了一把一模一样的钥匙B,但是还是指向这个仓库。
当ch[0]='C'时,相当于通过这把备用钥匙B改变了仓库里的物资(比如原来装着韭菜,现在换成萝卜)。
等备用钥匙的寿命结束(函数结束),再用主钥匙打开仓库时就会发现仓库已经改变(Hello变成了Cello)。
对于数组,函数里“ch[0]='C';”语句的含义是将ch所指向的内存中偏移是0的内容由"H"换成"C",也就是说,ch所指的对象的内容被改变了,但ch并没有变。
在本题中
publicvoidsecond(Valuev,inti){
i=0;
v.i=20;//通过引用的副本改变原对象的值为20
Valueval=newValue();//new出了一个新的对象
v=val;//引用的副本指向了一个新的Object
System.out.println(v.i+""+i);//打印新对象的值
}
对象v也是传一份引用的副本,v.i=20;通过引用的副本改变原对象的值为20。
但在语句Valueval=newValue();中,new引出了一个新的对象,然后执行v=val;相当于引用的副本指向了一个新的Object。
所以v.i=15是改变新的Object的值,而不改变原对象的值。
答案:
15020
6.2静态变量与私有变量
面试例题1:
关于静态变量的创建,哪一个选项是正确的?
()
A.一旦一个静态变量被分配,它就不允许改变
B.一个静态变量在一个方法中创建,它在被调用的时候值保持不变
C.在任意多个类的实例中,一个静态变量的实例只存在一个
D.一个静态的标识符只能被应用于primitivevalue
解析:
选项A是对常量的描述。
选项B是为了迷惑那些习惯使用VB的人。
选项D所说的静态标识符可以被应用到类(但只是一个内部类)、方法和变量中。
答案:
C
面试例题2:
当编译和运行下列代码时会出现什么情况?
publicclassSandys{
privateintcourt;
publicstaticvoidmain(Stringargv[]){
Sandyss=newSandys(99);
System.out.println(s.court);
}
Sandys(intballcount){
court=ballcount;
}
}
A.编译时错误,court变量定义的是私有变量
B.编译时错误,当System.out.println方法被调用时s没有被初始化
C.编译和执行时没有输出结果
D.编译运行时输出结果是99
解析:
事实上,变量court被定义为私有变量,并不能阻止构造器对它初始化,所以court被初始化成99。
答案:
D
面试例题3:
当你编译和运行下面的代码时,会出现下面选项中的哪种情况?
()
publicclassPvf{
staticbooleanPaddy;
publicstaticvoidmain(Stringargv[]){
System.out.println(Paddy);
}
}
A.编译时错误B.编译通过并输出结果false
C.编译通过并输出结果trueD.编译通过并输出结果null
解析:
定义在类里面的变量会被赋予一个默认的值,而布尔类型的默认初始值是false,所以此题的输出结果是false。
答案:
B
面试例题4:
给定下面的程序。
publicclassSytch
{
intx=2000;
publicstaticvoidmain(Stringargv[])
{
System.out.println("Ms"+argv[1]+"Pleasepay$"+x);
}
}
如果用命令行参数JavaSytchJonesDiggle编译和运行上面的程序,会出现下面哪个结果?
()
A.编译通过并输出MsDigglePleasepay$2000结果
B.编译时错误
C.编译通过并输出MsJonesPleasepay$2000
D.编译通过但是运行时错误
解析:
因为main方法是静态的,不能访问一个非静态的变量x。
答案:
B
6.3输入/输出流
面试例题1:
假设任何异常处理已经被创建,下列哪个选项是创建RandomAccessFile类实例的正确选项?
()
A.RandomAccessFileraf=newRandomAccessFile("myfile.txt","rw");
B.RandomAccessFileraf=newRandomAccessFile(newDataInputStr-eam());
C.RandomAccessFileraf=newRandomAccessFile("myfile.txt");
D.RandomAccessFileraf=newRandomAccessFile(newFile("myfile.txt"));
解析:
在Java的I/O结构中,RandomAccessFile是比较不寻常的类,它直接继承于Object,它并不属于Streams结构的一部分。
答案:
A
面试例题2:
需要读一个比较大的文本文件,这个文件里有很多字节的数据,那么下列最合适读这类文件的选项是哪个?
()
A.newFileInputStream("file.name");
B.newInputStreamReader(newFileInputStream("file.name"));
C.newBufferedReader(newInputStreamReader(newFileInputStream("file.name")));
D.newRandomAccessFileraf=newRandomAccessFile("myfile.txt","+rw");
解析:
这道题最主要的是具有很多字节数的文本文件,那么就要考虑到BufferedReader。
答案:
C
面试例题3:
请给出一段代码描述字符串写入文件。
答案:
代码如下。
importjava.io.*;
publicclassTest
{
publicstaticvoidmain(Stringargs[])
{
try
{
FileOutputStreamout=newFileOutputStream
("FileName.txt");
out.write("字符串写入文件".getBytes());
out.close();
}
catch(IOExceptionioe)
{
}
}//main
}
面试例题4:
写一个Java应用程序,从键盘输入两个整数,然后输出它们的平方值及立方值。
解析:
在Java中没有像C语言那样有一个专供接受从键盘输入值的scanf函数,所以一般的做法是从键盘输入一行字符,保存到字符串s中,再将字符组成的字符串s转换为整型数据后返回。
答案:
代码如下。
//packagecuboid;
importjava.io.*;
classInputData{//定义从键盘输入数据的类
staticprivateStrings="";
staticpublicvoidinput(){//从键盘输入一行字符,保存到字符串s中
BufferedReaderbu=newBufferedReader(
newInputStreamReader(System.in)
);
try{
s=bu.readLine();
}
catch(IOExceptione){}//捕获输入/输出异常
}
staticpublicintgetInt(){//静态方法可直接用类名调用
input();
returnInteger.parseInt(s);//将字符组成的字符串s转换为整型数据后返回
}
}
classResult{
voidprint(intd){
System.out.println(d+"的平方:
"+d*d);
System.out.println(d+"的立方:
"+d*d*d);
}
}
publicclassPrintResult{
publicstaticvoidmain(String[]args){
Resultresult=newResult();
System.out.println("请输入一个整数:
");
inta=InputData.getInt();
result.print(a);
}
}
面试例题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。
答案:
代码如下。
//packagecuboid;
importjava.io.*;
classInputData{//定义从键盘输入数据的类
staticprivateStrings="";
staticpublicvoidinput(){//从键盘输入一行字符,保存到字符串s中
BufferedReaderbu=newBufferedReader(
newInputStreamReader(System.in)
);
try{
s=bu.readLine();
}
catch(IOExceptione){}//捕获输入/输出异常
}
staticpublicintgetInt(){//静态方法可直接用类名调用
input();
returnInteger.parseInt(s);//将字符组成的字符串s转换为整型数据后返回
}
}
classResult{
voidprint(intd){
if(((d-1)&(d))==0&&(d!
=0))
System.out.println("是2的阶次");
else
System.out.println("不是2的阶次");
}
}
publicclassPrintResult{
publicstaticvoidmain(String[]args){
Resultresult=newResult();
System.out.println("请输入一个整数:
");
inta=InputData.getInt();
result.print(a);
}
}
6.4序列化
面试例题:
如何实现Java的序列化?
[英国著名图形软件公司面试题]
解析:
Java的“对象序列化”能将一个实现了Serializable接口的对象转换成一组byte,这样日后要用这个对象的时候,就能把这些byte数据恢复出来,并据此重新构建那个对象。
这一点甚至在跨网络的环境下也是如此,这就意味着序列化机制能自动补偿操作系统方面的差异。
也就是说,可以在Windows机器上创建一个对象,序列化之后,再通过网络传到UNIX机器上,最后在那里进行重建。
不用担心在不同的平台上数据是怎样表示的,以及byte顺序怎样,或者别的什么细节。
对象序列化能实现“轻量级的persistence(lightweightpersistence)”。
所谓persistence,是指对象的生命周期不是由程序是否运行决定的,在程序的两次调用之间对象仍然还活着。
通过“将做过序列化处理的对象写入磁盘,等到程序再次运行的时候再把它读出来”,可以达到persistence的效果。
之所以说“轻量级”,是因为不能用像“persistent”这样的关键词来直接定义一个对象,然后让系统去处理所有的细节(虽然将来有可能会这样)。
相反,必须明确地进行序列化(serialize)和解序列化(deserialize)。
之所以要在语言里加入对象序列化,是因为要用它来实现两个重要的功能。
Java的远程方法调用(RemoteMethodInvocation,简称RMI)能像调用自己机器上的对象那样去调用其他机器上的对象。
当向远程对象传递消息的时候,就需要通过对象序列化来传送参数和返回值。
对JavaBean来说,对象序列化也是必不可少的。
Bean的状态信息通常是在设计时配置的,这些状态信息必须保存起来,供程序启动的时候用,对象序列化就负责这个工作。
答案:
序列化
- 配套讲稿:
如PPT文件的首页显示word图标,表示该PPT已包含配套word讲稿。双击word图标可打开word文档。
- 特殊限制:
部分文档作品中含有的国旗、国徽等图片,仅作为作品整体效果示例展示,禁止商用。设计者仅对作品中独创性部分享有著作权。
- 关 键 词:
- Java 程序员 面试 宝典