转载:http://www.importnew.com/2217.html
常见Java面试题 – 第一部分:非可变性(Immutability)和对象引用(Object reference)
英文原文: Java Success,编译:ImportNew - 郑雯
ImportNew注: 本文是ImportNew编译整理的Java面试题系列文章之一。请看此系列相关面试题。你可以从这里查看全部的Java面试系列。
一些比较核心的Java问题经常会用来考验面试者的Java基本知识功底。这篇文章列出来了一些在我的书里面没有的面试题,通过这些面试题读者也可以梳理一下Java基础知识点。
Q1.下面的代码片段会输出什么?
1
2
3
4
|
String
s = "
Hello " ; s
+= "
World " ; s.trim(
); System.out.println(s); |
A1.正确输出是” Hello World “。
由于字符串前后都有空格,因为有些人可能会认为结果是”Hello World”。那么,这个题目想要考察的是什么呢?
1. 字符串对象(String Object)是非可变的(immutable),这个题目容易迷惑人的地方在s.trim( )这一行。
2. 理解对象引用和不可达对象会由垃圾回收器收集。
顺着这个题目,你觉得还可以考察哪些概念呢?
1. 例如,上面的代码中一共会生成几个字符串对象,什么时候这些对象会变成不可达对象从而被垃圾回收器回收。
2. 又比如,上面的代码的效率如何?
最好的解释方式是通过一个图表来说明,如下:
如果需要输出前后没有空格的”Hello World”,那么应该将s.trim( )再赋值给”s”。这个赋值操作可以让s指向新创建出来的字符串对象。
上面的代码也可以改写成如下方式:
1
2
3
|
StringBuilder
sb = new StringBuilder( "
Hello " ); sb.append( "
World " ); System.out.println(sb.toString().trim(
)); |
StringBuilder不是一个线程安全的类,因此仅仅用作本地变量是没有问题的。如果你希望用作实例变量,那么可以选择线程安全的StringBuffer类。想知道字符串操作背后的原理吗?可以点击这里:String concatenation。
常见Java面试题 – 第二部分:equals与==
英文原文: Java Success,编译:ImportNew - 郑雯
ImportNew注: 本文是ImportNew编译整理的Java面试题系列文章之一。你可以从这里查看全部的Java面试系列。
Q2.下面的代码片段的输出是什么?
1
2
3
4
5
6
7
8
|
Object
s1 = new
String( "Hello" ); Object
s2 = new
String( "Hello" ); if (s1
== s2) { System.out.println( "s1
and s2 are ==" ); } else
if
(s1.equals(s2)) { System.out.println( "s1
and s2 are equals()" ); } |
1
|
|
A2.输出结果是:
s1 and s2 are equals()
可以用下面这个图来解释:
因此,上面的问题考察了面试者对”==” 和 “equals( )”在Java对象上如何应用的理解是否正确。前者比较引用,后者则比较对象中真正的值。
接着还可以有下面的问题:
Q.下面代码片段的输出是什么?
1
2
3
4
5
6
7
8
|
Object
s1 = "Hello" ; Object
s2 = "Hello" ; if
(s1 == s2) { System.out.println( "s1
and s2 are ==" ); }
else
if
(s1.equals(s2)) { System.out.println( "s1
and s2 are equals()" ); } |
A.答案是:
s1 and s2 are ==
看上去这个答案和对前面一个问题所做的解释似乎有所违背。事实上,这个例子(或者说,规则)比较特殊,是一个典型的flyweight 模式在字符串对象创建中的应用。这个模式通过减少对象的创建来节约内存。String对象会创建一个字符串池(a pool of string),如果当前准备新创建的字符串对象的值在这个池子中已经存在,那么就不会生成新对象,而是复用池中已有的字符串对象。flyweight 模式的精髓就是对象复用。不过,只有采用Object s = “Hello”方式(而非用”new“关键字)声明String对象的时候这个规则才会被应用。
这是一个非常经常被采用的Java面试问题。
常见Java面试题 – 第三部分:重载(overloading)与重写(overriding)
英文原文: Java Success,编译:ImportNew - 郑雯
ImportNew注: 本文是ImportNew编译整理的Java面试题系列文章之一。你可以从这里查看全部的Java面试系列。
这篇文章介绍的常见面试题是关于重载(overloading)方法和重写(overriding)方法的。
Q.下面代码片段的输出结果是什么?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
|
public
class
MethodOverrideVsOverload { public
boolean
equals( MethodOverrideVsOverload other ) { System.out.println( "MethodOverrideVsOverload
equals method reached"
); return
true ; } public
static
void
main(String[] args) { Object
o1 = new
MethodOverrideVsOverload(); Object
o2 = new
MethodOverrideVsOverload(); MethodOverrideVsOverload
o3 = new
MethodOverrideVsOverload(); MethodOverrideVsOverload
o4 = new
MethodOverrideVsOverload(); if (o1.equals(o2)){ System.out.println( "objects
o1 and o2 are equal" ); } if (o3.equals(o4)){ System.out.println( "objects
o3 and o4 are equal" ); } } } |
A.输出结果是:
MethodOverrideVsOverload equals method reached
objects o3 and o4 are equal
这个问题考察了哪些概念呢?
- Java语言中,一个类只能从一个类中继承出来(也就是,单继承结构),如果没有显式的标明所继承自的类,那么自动继承自Object对象。
- 大多数的非final对象类方法都会被子类重写(overridden):
public boolean equals(Object obj); // make note of this method
public int hashCode();
public String toString();
- 重载方法在编译时起作用(例如,静态绑定),重写方法在运行时起作用(例如,动态绑定)。静态绑定意味着JVM在编译时决定调用的类或方法。而动态绑定时,JVM是在运行时决定调用的类或方法。动态绑定设计是多态的基础。更多了解编译时和运行时.
- 子类中重写父类的对应方法必须遵循下面的规则:
参数 | 不可变(译者注:包括参数类型和个数)。 |
返回类型 | 不可变,除了协变返回类型或其子类型(covariant (subtype) returns)。 |
异常 | 子类中可以抛出更少的异常,但绝对不能抛出父类中没有定义的已检查异常。 |
访问权限 | 比父类中对应方法更宽松。 |
调用 | 运行时(也就是动态绑定),根据对象类型来决定调用的具体方法。 |
现在,再回头看上面的代码,MethodOverrideVsOverload 类中的”equals(MethodOverrideVsOverload other)” 方法并没有重写Object类中的”public boolean equals(Object obj)” 方法。这是因为其违背了参数规则,其中一个是MethodOverrideVsOverload 类型,而另一个是Object类型。因此,这两个方法是重载关系(发生在编译时),而不是重写关系。
因此,当调用o1.equals(o2)时,实际上调用了object类中的public boolean equals(Object obj)方法。这是因为在编译时,o1和o2都是Object类型,而Object类的equals( … )方法是比较内存地址(例如,Object@235f56和Object@653af32)的,因此会返回false。
当调用o3.equals(o4)时,实际上调用了MethodOverrideVsOverload 类中的equals( MethodOverrideVsOverload other )方法。这是因为在编译时,o3和o4都是MethodOverrideVsOverload类型的,因此得到上述结果。
接下来还可以怎么提问呢?
Q.那怎么解决上面的那个问题呢?
A.在Java5中,新增了注解,其中包括很好用的编译时注解(compile time annotations)@override,来保证方法正确的重写了父类方法。如果在上面的代码中添加了注解,那么JVM会抛出一个编译错误。
因此,解决的方法就是给MethodOverrideVsOverload 类的boolean equals( MethodOverrideVsOverload other )方法添加@override注解。这样的话编译时就会有错误抛出来提示开发者某个方法没有正确的重写父类方法。之后,还需要修改方法的参数,将其从MethodOverrideVsOverload变成Object,具体如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
|
public
class
MethodOverrideVsOverload { @Override public
boolean
equals( Object other ) { System.out.println( "MethodOverrideVsOverload
equals method reached"
); return
true ; } public
static
void
main(String[] args) { Object
o1 = new
MethodOverrideVsOverload(); //during
compile time o1 is of type Object //during
runtime o1 is of type MethodOverrideVsOverload Object
o2 = new
MethodOverrideVsOverload(); //during
compile time o2 is of type Object //during
runtime o2 is of type MethodOverrideVsOverload MethodOverrideVsOverload
o3 = new
MethodOverrideVsOverload(); //o3
is of type MethodOverrideVsOverload //
during both compile time and runtime MethodOverrideVsOverload
o4 = new
MethodOverrideVsOverload(); //o4
is of type MethodOverrideVsOverload //
during both compile time and runtime if (o1.equals(o2)){ System.out.println( "objects
o1 and o2 are equal" ); } if (o3.equals(o4)){ System.out.println( "objects
o3 and o4 are equal" ); } } } |
输出为:
MethodOverrideVsOverload equals method reached
objects o1 and o2 are equal
MethodOverrideVsOverload equals method reached
objects o3 and o4 are equal
上面的代码中,运行时equals方法正确的重写了Object中的相应方法。这是一个比较容易混淆的问题,面试的时候需要很详尽的解释相关的概念。
常见Java面试题 – 第四部分:迭代(iteration)和递归(recursion)
英文原文: Java Success,编译:ImportNew - 郑雯
ImportNew注: 本文是ImportNew编译整理的Java面试题系列文章之一。你可以从这里查看全部的Java面试系列。
Q.请写一段代码来计算给定文本内字符“A”的个数。分别用迭代和递归两种方式。
A.假设给定文本为”AAA rating”。迭代方式就很直观,如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
|
public
class
Iteration { public
int
countA(String input) { if
(input == null
|| input.length( ) == 0 )
{ return
0 ; } int
count = 0 ; for
( int
i = 0 ;
i < input.length( ); i++) { if (input.substring(i,
i+ 1 ).equals( "A" )){ count++; } } return
count; } public
static
void
main(String[ ] args) { System.out.println( new
Iteration( ).countA( "AAA
rating" ));
//
Ans.3 } } |
接下来,递归方式的代码如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
|
public
class
RecursiveCall { public
int
countA(String input) { //
exit condition – recursive calls must have an exit condition if
(input == null
|| input.length( ) == 0 )
{ return
0 ; } int
count = 0 ; //check
first character of the input if
(input.substring( 0 ,
1 ).equals( "A" ))
{ count
= 1 ; } //recursive
call to evaluate rest of the input //(i.e.
2nd character onwards) return
count + countA(input.substring( 1 )); } public
static
void
main(String[ ] args) { System.out.println( new
RecursiveCall( ).countA( "AAA
rating" ));
//
Ans. 3 } } |
递归比较难以理解,我们用下面的图来进行说明。
Q.理解递归需要了解哪些概念?
A. 可重入方法(re-entrant method)是可以安全进入的方法,即使同一个方法正在被执行,深入到同一个线程的调用栈里面也不会影响此次执行的安全性。一个非可重入方法则不是可以安全进入的。例如,加入写文件或者向文件中写入日志的方法不是可重入方法时,有可能会毁坏那个文件。
如果一个方法调用了其自身的话,我们称之为递归调用。假定栈空间足够的话,尽管递归调用比较难以调试,在Java语言中实现递归调用也是完全可行的。递归方法是众多算法中替代循环的一个不错选择。所有的递归方法都是可重入的,但是不是所有可重入的方法都是递归的。
栈遵守LIFO(Last In First Out)规则,因此递归调用方法能够记住“调用者”并且知道此轮执行结束之返回至当初的被调用位置。递归利用系统栈来存储方法调用的返回地址。 Java是一种基于栈设计的编程语言。
顺着这个思路还有那些问题可以用来面试?
Q.什么情况下应该采用递归?
A. 上面的例子中其实不必采用递归,循环的方式可以达到目的,但是在某些情况下采用递归方式则代码会更加简短易读。递归方法在循环树结构以及避免丑陋的嵌套循环的情况下是非常好用的。
Q.什么是尾递归,为什么需要尾递归?上面的代码用尾递归方式如何重写?
A. 常规递归方法(亦称,头递归)在上面演示了,这种方式会增加调用栈的大小。每次递归,其入口需要被记录在栈中。方法返回之前需要给countA(input.substring(1)的结果加一个count。假定count大于1,那么返回结果就是count + countA(input.substring(1)),当然事先要算出来countA(input.substring(1))才行。同时,这也意味着直到countA(input.substring(1)计算出来才能得到最终的结果。因此,最后需要做的事其实是加法运算,而非递归本身。
尾递归的好处是什么?
在尾递归中,最后要做的是递归,加法运算在之前就已经完成了。一轮递归调用完毕后就没有其他事情了(除了加法运算),因此调用时生成的信息也就没什么用了。这些无用信息可以丢弃,然后用一组新的参数来调用一次递归方法来产生一个新的结果。这也就是说,栈调用减少带来了内存消耗减少并且程序的性能更好。
尾递归重写的代码如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
|
public
class
TailRecursiveCall { public
int
countA(String input) { //
exit condition – recursive calls must have an exit condition if
(input == null
|| input.length() == 0 )
{ return
0 ; } return
countA(input, 0 )
; } public
int
countA(String input, int
count) { if
(input.length() == 0 )
{ return
count; } //
check first character of the input if
(input.substring( 0 ,
1 ).equals( "A" ))
{ count
= count + 1 ; } //
recursive call is the last call as the count is cumulative return
countA(input.substring( 1 ),
count); } public
static
void
main(String[] args) { System.out.println( new
TailRecursiveCall().countA( "AAA
rating" )); } } |