1、用最有效率的方法计算 2 乘以 8?
答:
2 << 3(左移 3 位相当于乘以 2 的 3 次方,右移 3 位相当于除以 2 的 3 次方)。
补充:我们为编写的类重写 hashCode 方法时,可能会看到如下所示的代码,其
实我们不太理解为什么要使用这样的乘法运算来产生哈希码(散列码),而且为
什么这个数是个素数,为什么通常选择 31 这个数?前两个问题的答案你可以自己
百度一下,选择 31 是因为可以用移位和减法运算来代替乘法,从而得到更好的性
能。说到这里你可能已经想到了:31 * num 等价于(num << 5) - num,左移 5
位相当于乘以 2 的 5 次方再减去自身就相当于乘以 31,现在的 VM 都能自动完成
这个优化
public class PhoneNumber {
private int areaCode;
private String prefix;
private String lineNumber;
@Override
public int hashCode() {
final int prime = 31;
int result = 1;
result = prime * result + areaCode;
result = prime * result
+ ((lineNumber == null) ? 0 : lineNumber.hashCode());
result = prime * result + ((prefix == null) ? 0 : prefix.hashCode());
return result;
}
@Override
public boolean equals(Object obj) {
if (this == obj)
return true;
if (obj == null)
return false;
if (getClass() != obj.getClass())
return false;
PhoneNumber other = (PhoneNumber) obj;
if (areaCode != other.areaCode)
return false;
if (lineNumber == null) {
if (other.lineNumber != null)
return false;
} else if (!lineNumber.equals(other.lineNumber))
return false;
if (prefix == null) {
if (other.prefix != null)
return false;
} else if (!prefix.equals(other.prefix))
return false;
return true;
}
}
2、在 Java 中,如何跳出当前的多重嵌套循环?
答:
在最外层循环前加一个标记如 A,然后用 break A;可以跳出多重循环。(Java 中
支持带标签的 break 和 continue 语句,作用有点类似于 C 和 C++中的 goto 语
句,但是就像要避免使用 goto 一样,应该避免使用带标签的 break 和 continue,
因为它不会让你的程序变得更优雅,很多时候甚至有相反的作用,所以这种语法
其实不知道更好)
3、构造器(constructor)是否可被重写(override)?
答:
构造器不能被继承,因此不能被重写,但可以被重载。
4、两个对象值相同(x.equals(y) == true),但却可有不同的hash code,这句话对不对?
答:
不对,如果两个对象 x 和 y 满足 x.equals(y) == true,它们的哈希码(hash code)应当相同。Java 对于 eqauls 方法和 hashCode 方法是这样规定的:
(1)如果两个对象相同(equals 方法返回 true),那么它们的 hashCode 值一定要相同;
(2) 如果两个对象的 hashCode 相同,它们并不一定相同。
当然,你未必要按照要求 去做,但是如果你违背了上述原则就会发现在使用容器时,相同的对象可以出现 在 Set 集合中,同时增加新元素的效率会大大下降(对于使用哈希存储的系统,如果哈希码频繁的冲突将会造成存取性能急剧下降)。
5、当一个对象被当作参数传递到一个方法后,此方法可改变 这个对象的属性,并可返回变化后的结果,那么这里到底是值传递还是引用传递?
答:
是值传递。Java 语言的方法调用只支持参数的值传递。当一个对象实例作为一个
参数被传递到方法中时,参数的值就是对该对象的引用。对象的属性可以在被调
用过程中被改变,但对对象引用的改变是不会影响到调用者的。C++和 C#中可以
通过传引用或传输出参数来改变传入的参数的值。在 C#中可以编写如下所示的代
码,但是在 Java 中却做不到
6、String 和 StringBuilder、StringBuffer 的区别?
答:
Java 平台提供了两种类型的字符串:String 和 StringBuffer/StringBuilder,它
们可以储存和操作字符串。其中 String 是只读字符串,也就意味着 String 引用的
字符串内容是不能被改变的。而 StringBuffer/StringBuilder 类表示的字符串对象
可以直接进行修改。StringBuilder 是 Java 5 中引入的,它和 StringBuffer 的方
法完全相同,区别在于它是在单线程环境下使用的,因为它的所有方面都没有被
synchronized 修饰,因此它的效率也比 StringBuffer 要高。
面试题 1 - 什么情况下用+运算符进行字符串连接比调用
StringBuffer/StringBuilder 对象的 append 方法连接字符串性能更好?
面试题 2 - 请说出下面程序的输出。
class StringEqualTest {
public static void main(String[] args) {
String s1 = "Programming";
第 226 页 共 485 页
String s2 = new String("Programming");
String s3 = "Program";
String s4 = "ming";
String s5 = "Program" + "ming";
String s6 = s3 + s4;
System.out.println(s1 == s2);
System.out.println(s1 == s5);
System.out.println(s1 == s6);
System.out.println(s1 == s6.intern());
System.out.println(s2 == s2.intern());
}
}
补充:解答上面的面试题需要清楚两点:1. String 对象的 intern 方法会得到字符
串对象在常量池中对应的版本的引用(如果常量池中有一个字符串与 String 对象
的 equals 结果是 true),如果常量池中没有对应的字符串,则该字符串将被添加
到常量池中,然后返回常量池中字符串的引用;2. 字符串的+操作其本质是创建
了 StringBuilder 对象进行 append 操作,然后将拼接后的 StringBuilder 对象用
toString 方法处理成 String 对象,这一点可以用 javap -c StringEqualTest.class
命令获得 class 文件对应的 JVM 字节码指令就可以看出来
7、描述一下 JVM 加载 class 文件的原理机制?
答:
JVM 中类的装载是由类加载器(ClassLoader)和它的子类来实现的,Java 中的
类加载器是一个重要的 Java 运行时系统组件,它负责在运行时查找和装入类文件
中的类。
由于 Java 的跨平台性,经过编译的 Java 源程序并不是一个可执行程序,而是一
个或多个类文件。当 Java 程序需要使用某个类时,JVM 会确保这个类已经被加载、
连接(验证、准备和解析)和初始化。类的加载是指把类的.class 文件中的数据读
入到内存中,通常是创建一个字节数组读入.class 文件,然后产生与所加载类对应
的 Class 对象。加载完成后,Class 对象还不完整,所以此时的类还不可用。当类
被加载后就进入连接阶段,这一阶段包括验证、准备(为静态变量分配内存并设
置默认的初始值)和解析(将符号引用替换为直接引用)三个步骤。最后 JVM 对
类进行初始化,包括:1)如果类存在直接的父类并且这个类还没有被初始化,那么
就先初始化父类;2)如果类中存在初始化语句,就依次执行这些初始化语句。
类的加载是由类加载器完成的,类加载器包括:根加载器(BootStrap)、扩展加
载器(Extension)、系统加载器(System)和用户自定义类加载器
(java.lang.ClassLoader 的子类)。从 Java 2(JDK 1.2)开始,类加载过程采
取了父亲委托机制(PDM)。PDM 更好的保证了 Java 平台的安全性,在该机制
中,JVM 自带的 Bootstrap 是根加载器,其他的加载器都有且仅有一个父类加载
器。类的加载首先请求父类加载器加载,父类加载器无能为力时才由其子类加载
第 228 页 共 485 页第 229 页 共 485 页
器自行加载。JVM 不会向 Java 程序提供对 Bootstrap 的引用。下面是关于几个类
加载器的说明:
Bootstrap:一般用本地代码实现,负责加载 JVM 基础核心类库(rt.jar);
Extension:从 java.ext.dirs 系统属性所指定的目录中加载类库,它的父
加载器是 Bootstrap;
System:又叫应用类加载器,其父类是 Extension。它是应用最广泛的
类加载器。它从环境变量 classpath 或者系统属性 java.class.path 所指定的目
录中记载类,是用户自定义加载器的默认父加载器。
8、char 型变量中能不能存贮一个中文汉字,为什么?
答:
char 类型可以存储一个中文汉字,因为 Java 中使用的编码是 Unicode(不选择
任何特定的编码,直接使用字符在字符集中的编号,这是统一的唯一方法),一
个 char 类型占 2 个字节(16 比特),所以放一个中文是没问题的
9、抽象类(abstract class)和接口(interface)有什么异同?
答:
抽象类和接口都不能够实例化,但可以定义抽象类和接口类型的引用。一个类如
果继承了某个抽象类或者实现了某个接口都需要对其中的抽象方法全部进行实
现,否则该类仍然需要被声明为抽象类。接口比抽象类更加抽象,因为抽象类中
可以定义构造器,可以有抽象方法和具体方法,而接口中不能定义构造器而且其
中的方法全部都是抽象方法。抽象类中的成员可以是 private、默认、protected、
public 的,而接口中的成员全都是 public 的。抽象类中可以定义成员变量,而接
口中定义的成员变量实际上都是常量。有抽象方法的类必须被声明为抽象类,而
抽象类未必要有抽象方法。
10、静态嵌套类(Static Nested Class)和内部类(Inner Class)的不同?
答:
Static Nested Class 是被声明为静态(static)的内部类,它可以不依赖于外部类
实例被实例化。而通常的内部类需要在外部类实例化后才能实例化,其语法看起
来挺诡异的,如下所示。
/**
* 扑克类(一副扑克)
* @author 骆昊
*
第 230 页 共 485 页
*/
public class Poker {
private static String[] suites = {"黑桃", "红桃", "草花", "方块"};
private static int[] faces = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13};
private Card[] cards;
/**
* 构造器
*
*/
public Poker() {
cards = new Card[52];
for(int i = 0; i < suites.length; i++) {
for(int j = 0; j < faces.length; j++) {
cards[i * 13 + j] = new Card(suites[i], faces[j]);
}
}
}
/**
* 洗牌 (随机乱序)
*
*/
public void shuffle() {
for(int i = 0, len = cards.length; i < len; i++) {
int index = (int) (Math.random() * len);
Card temp = cards[index];
cards[index] = cards[i];
cards[i] = temp;
}
}
/**
* 发牌
* @param index 发牌的位置
*
*/
public Card deal(int index) {
return cards[index];
}
/**
* 卡片类(一张扑克)
* [内部类]
* @author 骆昊
*
*/
public class Card {
private String suite; // 花色
private int face; // 点数
public Card(String suite, int face) {
this.suite = suite;
this.face = face;
}
@Override
public String toString() {
String faceStr = "";
switch(face) {
case 1: faceStr = "A"; break;
第 233 页 共 485 页
case 11: faceStr = "J"; break;
case 12: faceStr = "Q"; break;
case 13: faceStr = "K"; break;
default: faceStr = String.valueOf(face);
}
return suite + faceStr;
}
}
}
class PokerTest {
public static void main(String[] args) {
Poker poker = new Poker();
poker.shuffle(); // 洗牌
Poker.Card c1 = poker.deal(0); // 发第一张牌
// 对于非静态内部类 Card
// 只有通过其外部类 Poker 对象才能创建 Card 对象
Poker.Card c2 = poker.new Card("红心", 1); // 自己创建一张牌
System.out.println(c1); // 洗牌后的第一张
System.out.println(c2); // 打印: 红心 A
}
}
面试题 - 下面的代码哪些地方会产生编译错误?
class Outer {
class Inner {}
public static void foo() { new Inner(); }
public void bar() { new Inner(); }
public static void main(String[] args) {
new Inner();
}
}
注意:Java 中非静态内部类对象的创建要依赖其外部类对象,上面的面试题中 foo
和 main 方法都是静态方法,静态方法中没有 this,也就是说没有所谓的外部类对
象,因此无法创建内部类对象,如果要在静态方法中创建内部类对象,可以这样
做:
new Outer().new Inner();