互联网 Java 工程师面试题(Java 面试题二)

48、运行时异常与受检异常有何异同?

答: 异常表示程序运行过程中可能出现的非正常状态,运行时异常表示虚拟机的通常 操作中可能遇到的异常,是一种常见运行错误,只要程序设计得没有问题通常就 不会发生。受检异常跟程序运行的上下文环境有关,即使程序设计无误,仍然可 能因使用的问题而引发。Java 编译器要求方法必须声明抛出可能发生的受检异常, 但是并不要求必须声明抛出未被捕获的运行时异常。异常和继承一样,是面向对 象程序设计中经常被滥用的东西,在 Effective Java 中对异常的使用给出了以下指 导原则:

  • 不要将异常处理用于正常的控制流(设计良好的 API 不应该强迫它的调 用者为了正常的控制流而使用异常)
  • 对可以恢复的情况使用受检异常,对编程错误使用运行时异常
  • 避免不必要的使用受检异常(可以通过一些状态检测手段来避免异常的发生)
  • 优先使用标准的异常
  • 每个方法抛出的异常都要有文档
  • 保持异常的原子性
  • 不要在 catch 中忽略掉捕获到的异常

49、列出一些你常见的运行时异常?

答:

  • ArithmeticException(算术异常)
  • ClassCastException (类转换异常)
  • IllegalArgumentException (非法参数异常)
  • IndexOutOfBoundsException (下标越界异常)
  • NullPointerException (空指针异常)
  • SecurityException (安全异常)

50、阐述 final、finally、finalize 的区别。

答:

  • final:修饰符(关键字)有三种用法:如果一个类被声明为 final,意味 着它不能再派生出新的子类,即不能被继承,因此它和 abstract 是反义词。将 变量声明为 final,可以保证它们在使用中不被改变,被声明为 final 的变量必须 在声明时给定初值,而在以后的引用中只能读取不可修改。被声明为 final 的方 法也同样只能使用,不能在子类中被重写。
  • finally:通常放在 try…catch…的后面构造总是执行代码块,这就意味着 程序无论正常执行还是发生异常,这里的代码只要 JVM 不关闭都能执行,可以 将释放外部资源的代码写在 finally 块中。
  • finalize:Object 类中定义的方法,Java 中允许使用 finalize()方法在垃 圾收集器将对象从内存中清除出去之前做必要的清理工作。这个方法是由垃圾收 集器在销毁对象时调用的,通过重写 finalize()方法可以整理系统资源或者执行 其他清理工作。

51、类 ExampleA 继承 Exception,类 ExampleB 继承 ExampleA。

有如下代码片断:

1try {
2throw new ExampleB("b")
3} catch(ExampleA e){
4System.out.println("ExampleA");
5} catch(Exception e){
6System.out.println("Exception");
7}

**请问执行此段代码的输出是什么?

答: 输出:ExampleA。(根据里氏代换原则[能使用父类型的地方一定能使用子类型], 抓取 ExampleA 类型异常的 catch 块能够抓住 try 块中抛出的 ExampleB 类型的 异常)

面试题 - 说出下面代码的运行结果。(此题的出处是《Java 编程思想》一书)

 1class Annoyance extends Exception {}
 2class Sneeze extends Annoyance {}
 3class Human {
 4public static void main(String[] args)
 5throws Exception {
 6try {
 7try {
 8throw new Sneeze();
 9}
10catch ( Annoyance a ) {
11System.out.println("Caught Annoyance");
12throw a;
13}
14}
15catch ( Sneeze s ) {
16System.out.println("Caught Sneeze");
17return ;
18}
19finally {
20System.out.println("Hello World!");
21}
22}
23}

52、List、Set、Map 是否继承自 Collection 接口?

答: List、Set 是,Map 不是。Map 是键值对映射容器,与 List 和 Set 有明显的区别, 而 Set 存储的零散的元素且不允许有重复元素(数学中的集合也是如此),List 是线性结构的容器,适用于按数值索引访问元素的情形。

53、阐述 ArrayList、Vector、LinkedList 的存储性能和特性。

答: ArrayList 和 Vector 都是使用数组方式存储数据,此数组元素数大于实际存储的 数据以便增加和插入元素,它们都允许直接按序号索引元素,但是插入元素要涉 及数组元素移动等内存操作,所以索引数据快而插入数据慢,Vector 中的方法由 于添加了 synchronized 修饰,因此 Vector 是线程安全的容器,但性能上ArrayList 差,因此已经是 Java 中的遗留容器。LinkedList 使用双向链表实现存 储(将内存中零散的内存单元通过附加的引用关联起来,形成一个可以按序号索 引的线性结构,这种链式存储方式与数组的连续存储方式相比,内存的利用率更 高),按序号索引数据需要进行前向或后向遍历,但是插入数据时只需要记录项的前后项即可,所以插入速度较快。Vector 属于遗留容器(Java 早期的版本中 提供的容器,除此之外,Hashtable、Dictionary、BitSet、Stack、Properties 都是遗留容器),已经不推荐使用,但是由于 ArrayList 和 LinkedListed 都是非 线程安全的,如果遇到多个线程操作同一个容器的场景,则可以通过工具类 Collections 中的 synchronizedList 方法将其转换成线程安全的容器后再使用(这 是对装潢模式的应用,将已有对象传入另一个类的构造器中创建新的对象来增强 实现)。

补充:遗留容器中的 Properties 类和 Stack 类在设计上有严重的问题,Properties 是一个键和值都是字符串的特殊的键值对映射,在设计上应该是关联一个 Hashtable 并将其两个泛型参数设置为 String 类型,但是 Java API 中的 Properties 直接继承了 Hashtable,这很明显是对继承的滥用。这里复用代码的方式应该是 Has-A 关系而不是 Is-A 关系,另一方面容器都属于工具类,继承工具 类本身就是一个错误的做法,使用工具类最好的方式是 Has-A 关系(关联)或 Use-A 关系(依赖)。同理,Stack 类继承 Vector 也是不正确的。Sun 公司的工 程师们也会犯这种低级错误,让人唏嘘不已。

54、Collection 和 Collections 的区别?

答: Collection 是一个接口,它是 Set、List 等容器的父接口;Collections 是个一个 工具类,提供了一系列的静态方法来辅助容器操作,这些方法包括对容器的搜索、 排序、线程安全化等等。

55、List、Map、Set 三个接口存取元素时,各有什么特点?

答: List 以特定索引来存取元素,可以有重复元素。Set 不能存放重复元素(用对象的 equals()方法来区分元素是否重复)。Map 保存键值对(key-value pair)映射, 映射关系可以是一对一或多对一。Set 和 Map 容器都有基于哈希存储和排序树的 两种实现版本,基于哈希存储的版本理论存取时间复杂度为 O(1),而基于排序树 版本的实现在插入或删除元素时会按照元素或元素的键(key)构成排序树从而达 到排序和去重的效果。

56、TreeMap 和 TreeSet 在排序时如何比较元素? Collections 工具类中的 sort()方法如何比较元素?

答: TreeSet 要求存放的对象所属的类必须实现 Comparable 接口,该接口提供了比 较元素的 compareTo()方法,当插入元素时会回调该方法比较元素的大小。 TreeMap 要求存放的键值对映射的键必须实现 Comparable 接口从而根据键对元 素进行排序。Collections 工具类的 sort 方法有两种重载的形式,第一种要求传入 的待排序容器中存放的对象比较实现 Comparable 接口以实现元素的比较;第二 种不强制性的要求容器中的元素必须可比较,但是要求传入第二个参数,参数是 Comparator 接口的子类型(需要重写 compare 方法实现元素的比较),相当于 一个临时定义的排序规则,其实就是通过接口注入比较元素大小的算法,也是对 回调模式的应用(Java 中对函数式编程的支持)。

例子 1:

 1public class Student implements Comparable {
 2private String name; // 姓名
 3private int age; // 年龄
 4public Student(String name, int age) {
 5this.name = name;
 6this.age = age;
 7}
 8@Override
 9public String toString() {
10return "Student [name=" + name + ", age=" + age + "]";
11}
12@Override
13public int compareTo(Student o) {
14return this.age - o.age; // 比较年龄(年龄的升序)
15}
16}
 1import java.util.Set;
 2import java.util.TreeSet;
 3class Test01 {
 4public static void main(String[] args) {
 5Set set = new TreeSet<>(); // Java 7 的钻石语法
 6(构造器后面的尖括号中不需要写类型)
 7set.add(new Student("Hao LUO", 33));
 8set.add(new Student("XJ WANG", 32));
 9set.add(new Student("Bruce LEE", 60));
10set.add(new Student("Bob YANG", 22));
11for(Student stu : set) {
12System.out.println(stu);
13}
14// 输出结果:
15// Student [name=Bob YANG, age=22]
16// Student [name=XJ WANG, age=32]
17// Student [name=Hao LUO, age=33]
18// Student [name=Bruce LEE, age=60]
19}
20}

例子 2:

 1public class Student {
 2private String name; // 姓名
 3private int age; // 年龄
 4public Student(String name, int age) {
 5this.name = name;
 6this.age = age;
 7}
 8/**
 9* 获取学生姓名
10*/
11public String getName() {
12return name;
13}
14/**
15* 获取学生年龄
16*/
17public int getAge() {
18return age;
19}
20@Override
21public String toString() {
22return "Student [name=" + name + ", age=" + age + "]";
23}
24}
 1import java.util.ArrayList;
 2import java.util.Collections;
 3import java.util.Comparator;
 4import java.util.List;
 5class Test02 {
 6public static void main(String[] args) {
 7List list = new ArrayList<>(); // Java 7 的钻石语法
 8(构造器后面的尖括号中不需要写类型)
 9list.add(new Student("Hao LUO", 33));
10list.add(new Student("XJ WANG", 32));
11list.add(new Student("Bruce LEE", 60));
12list.add(new Student("Bob YANG", 22));
13// 通过 sort 方法的第二个参数传入一个 Comparator 接口对象
14// 相当于是传入一个比较对象大小的算法到 sort 方法中
15// 由于 Java 中没有函数指针、仿函数、委托这样的概念
16// 因此要将一个算法传入一个方法中唯一的选择就是通过接口回调
17Collections.sort(list, new Comparator () {
18@Override
19public int compare(Student o1, Student o2) {
20return o1.getName().compareTo(o2.getName()); //
21比较学生姓名
22}
23});
24for(Student stu : list) {
25System.out.println(stu);
26}
27// 输出结果:
28// Student [name=Bob YANG, age=22]
29// Student [name=Bruce LEE, age=60]
30// Student [name=Hao LUO, age=33]
31// Student [name=XJ WANG, age=32]
32}
33}

57、Thread 类的 sleep()方法和对象的 wait()方法都可以让线 程暂停执行,它们有什么区别?

答: sleep()方法(休眠)是线程类(Thread)的静态方法,调用此方法会让当前线程 暂停执行指定的时间,将执行机会(CPU)让给其他线程,但是对象的锁依然保 持,因此休眠时间结束后会自动恢复(线程回到就绪状态,请参考第 66 题中的线 程状态转换图)。wait()是 Object 类的方法,调用对象的 wait()方法导致当前线 程放弃对象的锁(线程暂停执行),进入对象的等待池(wait pool),只有调用 对象的 notify()方法(或 notifyAll()方法)时才能唤醒等待池中的线程进入等锁池 (lock pool),如果线程重新获得对象的锁就可以进入就绪状态。

补充:可能不少人对什么是进程,什么是线程还比较模糊,对于为什么需要多线 程编程也不是特别理解。简单的说:进程是具有一定独立功能的程序关于某个数 据集合上的一次运行活动,是操作系统进行资源分配和调度的一个独立单位;线 程是进程的一个实体,是 CPU 调度和分派的基本单位,是比进程更小的能独立运 行的基本单位。线程的划分尺度小于进程,这使得多线程程序的并发性高;进程 在执行时通常拥有独立的内存单元,而线程之间可以共享内存。使用多线程的编 程通常能够带来更好的性能和用户体验,但是多线程的程序对于其他程序是不友 好的,因为它可能占用了更多的 CPU 资源。当然,也不是线程越多,程序的性能 就越好,因为线程之间的调度和切换也会浪费 CPU 时间。时下很时髦的 Node.js 就采用了单线程异步 I/O 的工作模式。

58、线程的 sleep()方法和 yield()方法有什么区别?

答:

① sleep()方法给其他线程运行机会时不考虑线程的优先级,因此会给低优先级的 线程以运行的机会;yield()方法只会给相同优先级或更高优先级的线程以运行的 机会;

② 线程执行 sleep()方法后转入阻塞(blocked)状态,而执行 yield()方法后转 入就绪 (ready)状态;

③ sleep()方法声明抛出 InterruptedException,而 yield()方法没有声明任何异 常;

④ sleep()方法比 yield()方法(跟操作系统 CPU 调度相关)具有更好的可移植性。

59、当一个线程进入一个对象的 synchronized 方法 A 之后, 其它线程是否可进入此对象的 synchronized 方法 B?

答: 不能。其它线程只能访问该对象的非同步方法,同步方法则不能进入。因为非静 态方法上的 synchronized 修饰符要求执行方法时要获得对象的锁,如果已经进入 A 方法说明对象锁已经被取走,那么试图进入 B 方法的线程就只能在等锁池(注 意不是等待池哦)中等待对象的锁。

60、请说出与线程同步以及线程调度相关的方法。

答:

  • wait():使一个线程处于等待(阻塞)状态,并且释放所持有的对象的锁;
  • sleep():使一个正在运行的线程处于睡眠状态,是一个静态方法,调用 此方法要处理 InterruptedException 异常;
  • notify():唤醒一个处于等待状态的线程,当然在调用此方法的时候,并 不能确切的唤醒某一个等待状态的线程,而是由 JVM 确定唤醒哪个线程,而且 与优先级无关;
  • notityAll():唤醒所有处于等待状态的线程,该方法并不是将对象的锁给 所有线程,而是让它们竞争,只有获得锁的线程才能进入就绪状态; \

提示:关于 Java 多线程和并发编程的问题,建议大家看我的另一篇文章《关于 Java 并发编程的总结和思考》。 补充:Java 5 通过 Lock 接口提供了显式的锁机制(explicit lock),增强了灵活 性以及对线程的协调。Lock 接口中定义了加锁(lock())和解锁(unlock())的方 法,同时还提供了 newCondition()方法来产生用于线程之间通信的 Condition 对 象;此外,Java 5 还提供了信号量机制(semaphore),信号量可以用来限制对 某个共享资源进行访问的线程的数量。在对资源进行访问之前,线程必须得到信 号量的许可(调用 Semaphore 对象的 acquire()方法);在完成对资源的访问后, 线程必须向信号量归还许可(调用 Semaphore 对象的 release()方法)。

下面的例子演示了 100 个线程同时向一个银行账户中存入 1 元钱,在没有使用同 步机制和使用同步机制情况下的执行情况。

  • 银行账户类:
 1/**
 2* 银行账户
 3* @author 骆昊
 4*
 5*/
 6public class Account {
 7private double balance; // 账户余额
 8/**
 9* 存款
10* @param money 存入金额
11*/
12public void deposit(double money) {
13double newBalance = balance + money;
14try {
15Thread.sleep(10); // 模拟此业务需要一段处理时间
16}
17catch(InterruptedException ex) {
18ex.printStackTrace();
19}
20balance = newBalance;
21}
22/**
23* 获得账户余额
24*/
25public double getBalance() {
26return balance;
27}
28}
  • 存钱线程类:
1/**
2* 存钱线程
3* @author 骆昊
4*
5*/
6public class AddMoneyThread implements Runnable {
7private Account account; // 存入账户
8private double money; // 存入金额
9public AddMoneyThread(Account account, double money) {
10this.account = account;
11this.money = money;
12}
13@Override
14public void run() {
15account.deposit(money);
16}
17}
  • 测试类:
 1import java.util.concurrent.ExecutorService;
 2import java.util.concurrent.Executors;
 3public class Test01 {
 4public static void main(String[] args) {
 5Account account = new Account();
 6ExecutorService service = Executors.newFixedThreadPool(100);
 7for(int i = 1; i <= 100; i++) {
 8service.execute(new AddMoneyThread(account, 1));
 9}
10service.shutdown();
11while(!service.isTerminated()) {}
12System.out.println("账户余额: " + account.getBalance());
13}
14}

在没有同步的情况下,执行结果通常是显示账户余额在 10 元以下,出现这种状况 的原因是,当一个线程 A 试图存入 1 元的时候,另外一个线程 B 也能够进入存款 的方法中,线程 B 读取到的账户余额仍然是线程 A 存入 1 元钱之前的账户余额, 因此也是在原来的余额 0 上面做了加 1 元的操作,同理线程 C 也会做类似的事情, 所以最后 100 个线程执行结束时,本来期望账户余额为 100 元,但实际得到的通 常在 10 元以下(很可能是 1 元哦)。解决这个问题的办法就是同步,当一个线程 对银行账户存钱时,需要将此账户锁定,待其操作完成后才允许其他的线程进行 操作,代码有如下几种调整方案:

  • 在银行账户的存款(deposit)方法上同步(synchronized)关键字
 1/**
 2* 银行账户
 3* @author 骆昊
 4*
 5*/
 6public class Account {
 7private double balance; // 账户余额
 8/**
 9* 存款
10* @param money 存入金额
11*/
12public synchronized void deposit(double money) {
13double newBalance = balance + money;
14try {
15Thread.sleep(10); // 模拟此业务需要一段处理时间
16}
17catch(InterruptedException ex) {
18ex.printStackTrace();
19}
20balance = newBalance;
21}
22/**
23* 获得账户余额
24*/
25public double getBalance() {
26return balance;
27}
28}
  • 在线程调用存款方法时对银行账户进行同步
 1/**
 2* 存钱线程
 3* @author 骆昊
 4*
 5*/
 6public class AddMoneyThread implements Runnable {
 7private Account account; // 存入账户
 8private double money; // 存入金额
 9public AddMoneyThread(Account account, double money) {
10this.account = account;
11this.money = money;
12}
13@Override
14public void run() {
15synchronized (account) {
16account.deposit(money);
17}
18}
19}
  • 通过 Java 5 显示的锁机制,为每个银行账户创建一个锁对象,在存款操 作进行加锁和解锁的操作
 1import java.util.concurrent.locks.Lock;
 2import java.util.concurrent.locks.ReentrantLock;
 3/**
 4* 银行账户
 5*
 6* @author 骆昊
 7*
 8*/
 9public class Account {
10private Lock accountLock = new ReentrantLock();
11private double balance; // 账户余额
12/**
13* 存款
14*
15* @param money
16* 存入金额
17*/
18public void deposit(double money) {
19accountLock.lock();
20try {
21double newBalance = balance + money;
22try {
23Thread.sleep(10); // 模拟此业务需要一段处理时间
24}
25catch (InterruptedException ex) {
26ex.printStackTrace();
27}
28balance = newBalance;
29}
30finally {
31accountLock.unlock();
32}
33}
34/**
35* 获得账户余额
36*/
37public double getBalance() {
38return balance;
39}
40}

按照上述三种方式对代码进行修改后,重写执行测试代码 Test01,将看到最终的 账户余额为 100 元。当然也可以使用 Semaphore 或 CountdownLatch 来实现同步。

61、编写多线程程序有几种实现方式?

答: Java 5 以前实现多线程有两种实现方法:一种是继承 Thread 类;另一种是实现 Runnable 接口。两种方式都要通过重写 run()方法来定义线程的行为,推荐使用 后者,因为 Java 中的继承是单继承,一个类有一个父类,如果继承了 Thread 类 就无法再继承其他类了,显然使用 Runnable 接口更为灵活。

补充:Java 5 以后创建线程还有第三种方式:实现 Callable 接口,该接口中的 call 方法可以在线程执行结束时产生一个返回值,代码如下所示:

 1import java.util.ArrayList;
 2import java.util.List;
 3import java.util.concurrent.Callable;
 4import java.util.concurrent.ExecutorService;
 5import java.util.concurrent.Executors;
 6import java.util.concurrent.Future;
 7class MyTask implements Callable {
 8private int upperBounds;
 9public MyTask(int upperBounds) {
10this.upperBounds = upperBounds;
11}
12@Override
13public Integer call() throws Exception {
14int sum = 0;
15for(int i = 1; i <= upperBounds; i++) {
16sum += i;
17}
18return sum;
19}
20}
21class Test {
22public static void main(String[] args) throws Exception {
23List> list = new ArrayList<>();
24ExecutorService service = Executors.newFixedThreadPool(10);
25for(int i = 0; i < 10; i++) {
26list.add(service.submit(new MyTask((int) (Math.random() *
27100))));
28}
29int sum = 0;
30for(Future future : list) {
31// while(!future.isDone()) ;
32sum += future.get();
33}
34System.out.println(sum);
35}
36}

62、synchronized 关键字的用法?

答: synchronized 关键字可以将对象或者方法标记为同步,以实现对对象和方法的互 斥访问,可以用 synchronized(对象) { … }定义同步代码块,或者在声明方法时 将 synchronized 作为方法的修饰符。在第 60 题的例子中已经展示了 synchronized 关键字的用法。

63、举例说明同步和异步。

答: 如果系统中存在临界资源(资源数量少于竞争资源的线程数量的资源),例如正 在写的数据以后可能被另一个线程读到,或者正在读的数据可能已经被另一个线 程写过了,那么这些数据就必须进行同步存取(数据库操作中的排他锁就是最好 的例子)。当应用程序在对象上调用了一个需要花费很长时间来执行的方法,并 且不希望让程序等待方法的返回时,就应该使用异步编程,在很多情况下采用异 步途径往往更有效率。事实上,所谓的同步就是指阻塞式操作,而异步就是非阻 塞式操作。

64、启动一个线程是调用 run()还是 start()方法?

答: 启动一个线程是调用 start()方法,使线程所代表的虚拟处理机处于可运行状态, 这意味着它可以由 JVM 调度并执行,这并不意味着线程就会立即运行。run()方 法是线程启动后要进行回调(callback)的方法。

65、什么是线程池(thread pool)?

答: 在面向对象编程中,创建和销毁对象是很费时间的,因为创建一个对象要获取内 存资源或者其它更多资源。在 Java 中更是如此,虚拟机将试图跟踪每一个对象, 以便能够在对象销毁后进行垃圾回收。所以提高服务程序效率的一个手段就是尽 可能减少创建和销毁对象的次数,特别是一些很耗资源的对象创建和销毁,这就 是”池化资源”技术产生的原因。线程池顾名思义就是事先创建若干个可执行的 线程放入一个池(容器)中,需要的时候从池中获取线程不用自行创建,使用完 毕不需要销毁线程而是放回池中,从而减少创建和销毁线程对象的开销。 Java 5+中的 Executor 接口定义一个执行线程的工具。它的子类型即线程池接口 是 ExecutorService。要配置一个线程池是比较复杂的,尤其是对于线程池的原理 不是很清楚的情况下,因此在工具类 Executors 面提供了一些静态工厂方法,生 成一些常用的线程池,如下所示:

newSingleThreadExecutor:创建一个单线程的线程池。这个线程池只 有一个线程在工作,也就是相当于单线程串行执行所有任务。如果这个唯一的程因为异常结束,那么会有一个新的线程来替代它。此线程池保证所有任务的执 行顺序按照任务的提交顺序执行。
newFixedThreadPool:创建固定大小的线程池。每次提交一个任务就创 建一个线程,直到线程达到线程池的最大大小。线程池的大小一旦达到最大值就 会保持不变,如果某个线程因为执行异常而结束,那么线程池会补充一个新线程。
newCachedThreadPool:创建一个可缓存的线程池。如果线程池的大小 超过了处理任务所需要的线程,那么就会回收部分空闲(60 秒不执行任务)的 线程,当任务数增加时,此线程池又可以智能的添加新线程来处理任务。此线程 池不会对线程池大小做限制,线程池大小完全依赖于操作系统(或者说 JVM) 能够创建的最大线程大小。
newScheduledThreadPool:创建一个大小无限的线程池。此线程池支 持定时以及周期性执行任务的需求。
newSingleThreadExecutor:创建一个单线程的线程池。此线程池支持 定时以及周期性执行任务的需求。
第 60 题的例子中演示了通过 Executors 工具类创建线程池并使用线程池执行线程 的代码。如果希望在服务器上使用线程池,强烈建议使用 newFixedThreadPool 方法来创建线程池,这样能获得更好的性能。

66、线程的基本状态以及状态之间的关系?

答:

说明:其中 Running 表示运行状态,Runnable 表示就绪状态(万事俱备,只欠 CPU),Blocked 表示阻塞状态,阻塞状态又有多种情况,可能是因为调用 wait() 方法进入等待池,也可能是执行同步方法或同步代码块进入等锁池,或者是调用 了 sleep()方法或 join()方法等待休眠或其他线程结束,或是因为发生了 I/O 中断

67、简述 synchronized 和 java.util.concurrent.locks.Lock 的异同?

答: Lock 是 Java 5 以后引入的新的 API,和关键字 synchronized 相比主要相同点: Lock 能完成 synchronized 所实现的所有功能;主要不同点:Lock 有比 synchronized 更精确的线程语义和更好的性能,而且不强制性的要求一定要获得 锁。synchronized 会自动释放锁,而 Lock 一定要求程序员手工释放,并且最在 finally 块中释放(这是释放外部资源的最好的地方)。

68、Java 中如何实现序列化,有什么意义?

答: 序列化就是一种用来处理对象流的机制,所谓对象流也就是将对象的内容进行流 化。可以对流化后的对象进行读写操作,也可将流化后的对象传输于网络之间。 序列化是为了解决对象流读写操作时可能引发的问题(如果不进行序列化可能会 存在数据乱序的问题)。 要实现序列化,需要让一个类实现 Serializable 接口,该接口是一个标识性接口, 标注该类对象是可被序列化的,然后使用一个输出流来构造一个对象输出流并通 过 writeObject(Object)方法就可以将实现对象写出(即保存其状态);如果需要 反序列化则可以用一个输入流建立对象输入流,然后通过 readObject 方法从流中 读取对象。序列化除了能够实现对象的持久化之外,还能够用于对象的深度克隆 (可以参考第 29 题)。

69、Java 中有几种类型的流?

答: 字节流和字符流。字节流继承于 InputStream、OutputStream,字符流继承Reader、Writer。在 http://java.io 包中还有许多其他的流,主要是为了提高性能和使 用方便。关于 Java 的 I/O 需要注意的有两点:一是两种对称性(输入和输出的对 称性,字节和字符的对称性);二是两种设计模式(适配器模式和装潢模式)。 另外 Java 中的流不同于 C#的是它只有一个维度一个方向。

面试题 - 编程实现文件拷贝。(这个题目在笔试的时候经常出现,下面的代码给 出了两种实现方案)

 1import java.io.FileInputStream;
 2import java.io.FileOutputStream;
 3import java.io.IOException;
 4import java.io.InputStream;
 5import java.io.OutputStream;
 6import java.nio.ByteBuffer;
 7import java.nio.channels.FileChannel;
 8public final class MyUtil {
 9private MyUtil() {
10throw new AssertionError();
11}
12public static void fileCopy(String source, String target) throws
13IOException {
14try (InputStream in = new FileInputStream(source)) {
15try (OutputStream out = new FileOutputStream(target)) {
16byte[] buffer = new byte[4096];
17int bytesToRead;
18while((bytesToRead = in.read(buffer)) != -1) {
19out.write(buffer, 0, bytesToRead);
20}
21}
22}
23}
24public static void fileCopyNIO(String source, String target) throws
25IOException {
26try (FileInputStream in = new FileInputStream(source)) {
27try (FileOutputStream out = new FileOutputStream(target)) {
28FileChannel inChannel = in.getChannel();
29FileChannel outChannel = out.getChannel();
30ByteBuffer buffer = ByteBuffer.allocate(4096);
31while(inChannel.read(buffer) != -1) {
32buffer.flip();
33outChannel.write(buffer);
34buffer.clear();
35}
36}
37}
38}
39}

注意:上面用到 Java 7 的 TWR,使用 TWR 后可以不用在 finally 中释放外部资源 , 从而让代码更加优雅。

70、写一个方法,输入一个文件名和一个字符串,统计这个字 符串在这个文件中出现的次数。

答: 代码如下:

 1import java.io.BufferedReader;
 2import java.io.FileReader;
 3public final class MyUtil {
 4// 工具类中的方法都是静态方式访问的因此将构造器私有不允许创建对象
 5(绝对好习惯)
 6private MyUtil() {
 7throw new AssertionError();
 8}
 9/**
10* 统计给定文件中给定字符串的出现次数
11*
12* @param filename 文件名
13* @param word 字符串
14* @return 字符串在文件中出现的次数
15*/
16public static int countWordInFile(String filename, String word) {
17int counter = 0;
18try (FileReader fr = new FileReader(filename)) {
19try (BufferedReader br = new BufferedReader(fr)) {
20String line = null;
21while ((line = br.readLine()) != null) {
22int index = -1;
23while (line.length() >= word.length() && (index =
24line.indexOf(word)) >= 0) {
25counter++;
26line = line.substring(index + word.length());
27}
28}
29}
30} catch (Exception ex) {
31ex.printStackTrace();
32}
33return counter;
34}
35}

71、如何用 Java 代码列出一个目录下所有的文件?

答:

如果只要求列出当前文件夹下的文件,代码如下所示:

 1import java.io.File;
 2class Test12 {
 3public static void main(String[] args) {
 4File f = new File("/Users/Hao/Downloads");
 5for(File temp : f.listFiles()) {
 6if(temp.isFile()) {
 7System.out.println(temp.getName());
 8}
 9}
10}
11}

如果需要对文件夹继续展开,代码如下所示:

 1import java.io.File;
 2class Test12 {
 3public static void main(String[] args) {
 4showDirectory(new File("/Users/Hao/Downloads"));
 5}
 6public static void showDirectory(File f) {
 7_walkDirectory(f, 0);
 8}
 9private static void _walkDirectory(File f, int level) {
10if(f.isDirectory()) {
11for(File temp : f.listFiles()) {
12_walkDirectory(temp, level + 1);
13}
14}
15else {
16for(int i = 0; i < level - 1; i++) {
17System.out.print("\t");
18}
19System.out.println(f.getName());
20}
21}
22}

在 Java 7 中可以使用 NIO.2 的 API 来做同样的事情,代码如下所示:

 1class ShowFileTest {
 2public static void main(String[] args) throws IOException {
 3Path initPath = Paths.get("/Users/Hao/Downloads");
 4Files.walkFileTree(initPath, new SimpleFileVisitor() {
 5@Override
 6public FileVisitResult visitFile(Path file, BasicFileAttributes
 7attrs)
 8throws IOException {
 9System.out.println(file.getFileName().toString());
10return FileVisitResult.CONTINUE;
11}
12});
13}
14}

72、用 Java 的套接字编程实现一个多线程的回显(echo)服 务器。

答:

 1import java.io.BufferedReader;
 2import java.io.IOException;
 3import java.io.InputStreamReader;
 4import java.io.PrintWriter;
 5import java.net.ServerSocket;
 6import java.net.Socket;
 7public class EchoServer {
 8private static final int ECHO_SERVER_PORT = 6789;
 9public static void main(String[] args) {
10try(ServerSocket server = new
11ServerSocket(ECHO_SERVER_PORT)) {
12System.out.println("服务器已经启动...");
13while(true) {
14Socket client = server.accept();
15new Thread(new ClientHandler(client)).start();
16}
17} catch (IOException e) {
18e.printStackTrace();
19}
20}
21private static class ClientHandler implements Runnable {
22private Socket client;
23public ClientHandler(Socket client) {
24this.client = client;
25}
26@Override
27public void run() {
28try(BufferedReader br = new BufferedReader(new
29InputStreamReader(client.getInputStream()));
30PrintWriter pw = new
31PrintWriter(client.getOutputStream())) {
32String msg = br.readLine();
33System.out.println("收到" + client.getInetAddress() + "
34发送的: " + msg);
35pw.println(msg);
36pw.flush();
37} catch(Exception ex) {
38ex.printStackTrace();
39} finally {
40try {
41client.close();
42} catch (IOException e) {
43e.printStackTrace();
44}
45}
46}
47}
48}

注意:上面的代码使用了 Java 7 的 TWR 语法,由于很多外部资源类都间接的实现了 AutoCloseable 接口(单方法回调接口),因此可以利用 TWR 语法在 try结束的时候通过回调的方式自动调用外部资源类的 close()方法,避免书写冗长的finally 代码块。此外,上面的代码用一个静态内部类实现线程的功能,使用多线程可以避免一个用户 I/O 操作所产生的中断影响其他用户对服务器的访问,简单的说就是一个用户的输入操作不会造成其他用户的阻塞。当然,上面的代码使用线程池可以获得更好的性能,因为频繁的创建和销毁线程所造成的开销也是不可忽视的。

下面是一段回显客户端测试代码:

 1import java.io.BufferedReader;
 2import java.io.InputStreamReader;
 3import java.io.PrintWriter;
 4import java.net.Socket;
 5import java.util.Scanner;
 6public class EchoClient {
 7public static void main(String[] args) throws Exception {
 8Socket client = new Socket("localhost", 6789);
 9Scanner sc = new Scanner(System.in);
10System.out.print("请输入内容: ");
11String msg = sc.nextLine();
12sc.close();
13PrintWriter pw = new PrintWriter(client.getOutputStream());
14pw.println(msg);
15pw.flush();
16BufferedReader br = new BufferedReader(new
17InputStreamReader(client.getInputStream()));
18System.out.println(br.readLine());
19client.close();
20}
21}

如果希望用 NIO 的多路复用套接字实现服务器,代码如下所示。NIO 的操作虽然 带来了更好的性能,但是有些操作是比较底层的,对于初学者来说还是有些难于理解。

 1import java.io.IOException;
 2import java.net.InetSocketAddress;
 3import java.nio.ByteBuffer;
 4import java.nio.CharBuffer;
 5import java.nio.channels.SelectionKey;
 6import java.nio.channels.Selector;
 7import java.nio.channels.ServerSocketChannel;
 8import java.nio.channels.SocketChannel;
 9import java.util.Iterator;
10public class EchoServerNIO {
11private static final int ECHO_SERVER_PORT = 6789;
12private static final int ECHO_SERVER_TIMEOUT = 5000;
13private static final int BUFFER_SIZE = 1024;
14private static ServerSocketChannel serverChannel = null;
15private static Selector selector = null; // 多路复用选择器
16private static ByteBuffer buffer = null; // 缓冲区
17public static void main(String[] args) {
18init();
19listen();
20}
21private static void init() {
22try {
23serverChannel = ServerSocketChannel.open();
24buffer = ByteBuffer.allocate(BUFFER_SIZE);
25serverChannel.socket().bind(new
26InetSocketAddress(ECHO_SERVER_PORT));
27serverChannel.configureBlocking(false);
28selector = Selector.open();
29serverChannel.register(selector, SelectionKey.OP_ACCEPT);
30} catch (Exception e) {
31throw new RuntimeException(e);
32}
33}
34private static void listen() {
35while (true) {
36try {
37if (selector.select(ECHO_SERVER_TIMEOUT) != 0) {
38Iterator it =
39selector.selectedKeys().iterator();
40while (it.hasNext()) {
41SelectionKey key = it.next();
42it.remove();
43handleKey(key);
44}
45}
46} catch (Exception e) {
47e.printStackTrace();
48}
49}
50}
51private static void handleKey(SelectionKey key) throws IOException {
52SocketChannel channel = null;
53try {
54if (key.isAcceptable()) {
55ServerSocketChannel serverChannel =
56(ServerSocketChannel) key.channel();
57channel = serverChannel.accept();
58channel.configureBlocking(false);
59channel.register(selector, SelectionKey.OP_READ);
60} else if (key.isReadable()) {
61channel = (SocketChannel) key.channel();
62buffer.clear();
63if (channel.read(buffer) > 0) {
64buffer.flip();
65CharBuffer charBuffer =
66CharsetHelper.decode(buffer);
67String msg = charBuffer.toString();
68System.out.println("收到" +
69channel.getRemoteAddress() + "的消息:" + msg);
70channel.write(CharsetHelper.encode(CharBuffer.wrap(msg)));
71} else {
72channel.close();
73}
74}
75} catch (Exception e) {
76e.printStackTrace();
77if (channel != null) {
78channel.close();
79}
80}
81}
82}
 1import java.nio.ByteBuffer;
 2import java.nio.CharBuffer;
 3import java.nio.charset.CharacterCodingException;
 4import java.nio.charset.Charset;
 5import java.nio.charset.CharsetDecoder;
 6import java.nio.charset.CharsetEncoder;
 7public final class CharsetHelper {
 8private static final String UTF_8 = "UTF-8";
 9private static CharsetEncoder encoder =
10Charset.forName(UTF_8).newEncoder();
11private static CharsetDecoder decoder =
12Charset.forName(UTF_8).newDecoder();
13private CharsetHelper() {
14}
15public static ByteBuffer encode(CharBuffer in) throws
16CharacterCodingException{
17return encoder.encode(in);
18}
19public static CharBuffer decode(ByteBuffer in) throws
20CharacterCodingException{
21return decoder.decode(in);
22}
23}

73、XML 文档定义有几种形式?它们之间有何本质区别?解析 XML 文档有哪几种方式?

答: XML 文档定义分为 DTD 和 Schema 两种形式,二者都是对 XML 语法的约束,其 本质区别在于 Schema 本身也是一个 XML 文件,可以被 XML 解析器解析,而且 可以为 XML 承载的数据定义类型,约束能力较之 DTD 更强大。对 XML 的解析主 要有 DOM(文档对象模型,Document Object Model)、SAX(Simple API foXML)和 StAX(Java 6 中引入的新的解析 XML 的方式,Streaming API for XML),其中 DOM 处理大型文件时其性能下降的非常厉害,这个问题是由 DOM 树结构用的内存较多造成的,而且 DOM 解析方式必须在解析文件之前把整个文档装入内 存,适合对 XML 的随机访问(典型的用空间换取时间的策略);SAX 是事件驱动 型的 XML 解析方式,它顺序读取 XML 文件,不需要一次全部装载整个文件。当 遇到像文件开头,文档结束,或者标签开头与标签结束时,它会触发一个事件, 用户通过事件回调代码来处理 XML 文件,适合对 XML 的顺序访问;顾名思义, StAX 把重点放在流上,实际上 StAX 与其他解析方式的本质区别就在于应用程能够把 XML 作为一个事件流来处理。将 XML 作为一组事件来处理的想法并不新 颖(SAX 就是这样做的),但不同之处在于 StAX 允许应用程序代码把这些事件逐 个拉出来,而不用提供在解析器方便时从解析器中接收事件的处理程序。

74、你在项目中哪些地方用到了 XML?

答: XML 的主要作用有两个方面:数据交换和信息配置。在做数据交换时,XML 将数 据用标签组装成起来,然后压缩打包加密后通过网络传送给接收者,接收解密与 解压缩后再从 XML 文件中还原相关信息进行处理,XML 曾经是异构系统间交换数 据的事实标准,但此项功能几乎已经被 JSON(JavaScript Object Notation)取 而代之。当然,目前很多软件仍然使用 XML 来存储配置信息,我们在很多项目中 通常也会将作为配置信息的硬代码写在 XML 文件中,Java 的很多框架也是这么做 的,而且这些框架都选择了 dom4j 作为处理 XML 的工具,因为 Sun 公司的官方 API 实在不怎么好用。

补充:现在有很多时髦的软件(如 Sublime)已经开始将配置文件书写成 JSON 格式,我们已经强烈的感受到 XML 的另一项功能也将逐渐被业界抛弃。

你可能感兴趣的:(java,面试技巧,工程师,互联网)