Java程序员面试题集2

Posted strong

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Java程序员面试题集2相关的知识,希望对你有一定的参考价值。

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

有如下代码片断:

 

[java] view plain copy
 
 技术分享技术分享
  1. try{  
  2.     throw new ExampleB("b")  
  3. }catch(ExampleA e){  
  4.     System.out.println("ExampleA");  
  5. }catch(Exception e){  
  6.     System.out.println("Exception");  
  7. }  

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

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

补充:比此题略复杂的一道面试题如下所示(此题的出处是《Java编程思想》),说出你的答案吧!

 

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


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

 

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

 

53、说出ArrayList、Vector、LinkedList 的存储性能和特性?

  ArrayList 和Vector都是使用数组方式存储数据,此数组元素数大于实际存储的数据以便增加和插入元素,它们都允许直接按序号索引元素,但是插入元素要涉及数组元素移动等内存操作,所以索引数据快而插入数据慢,Vector由于使用了synchronized 方法(线程安全),通常性能上较ArrayList 差,而LinkedList 使用双向链表实现存储(将内存中零散的内存单元通过附加的引用关联起来,形成一个可以按序号索引的线性结构,这种链式存储方式与数组的连续存储方式相比,其实对内存的利用率更高),按序号索引数据需要进行前向或后向遍历,但是插入数据时只需要记录本项的前后项即可,所以插入速度较快。Vector属于遗留容器(早期的JDK中使用的容器,除此之外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也是不正确的。

 

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方法实现元素的比较),相当于一个临时定义的排序规则,其实就是是通过接口注入比较元素大小的算法,也是对回调模式的应用。

 例子1:

Student.java

 

[java] view plain copy
 
 技术分享技术分享
  1. package com.lovo.demo;  
  2.   
  3. public class Student implements Comparable<Student> {  
  4.     private String name;        // 姓名  
  5.     private int age;            // 年龄  
  6.   
  7.     public Student(String name, int age) {  
  8.         this.name = name;  
  9.         this.age = age;  
  10.     }  
  11.   
  12.     @Override  
  13.     public String toString() {  
  14.         return "Student [name=" + name + ", age=" + age + "]";  
  15.     }  
  16.   
  17.     @Override  
  18.     public int compareTo(Student o) {  
  19.         return this.age - o.age; // 比较年龄(年龄的升序)  
  20.     }  
  21.   
  22. }  

Test01.java

[java] view plain copy
 
 技术分享技术分享
  1. package com.lovo.demo;  
  2.   
  3. import java.util.Set;  
  4. import java.util.TreeSet;  
  5.   
  6. class Test01 {  
  7.   
  8.     public static void main(String[] args) {  
  9.         Set<Student> set = new TreeSet<>();     // Java 7的钻石语法(构造器后面的尖括号中不需要写类型)  
  10.         set.add(new Student("Hao LUO", 33));  
  11.         set.add(new Student("XJ WANG", 32));  
  12.         set.add(new Student("Bruce LEE", 60));  
  13.         set.add(new Student("Bob YANG", 22));  
  14.           
  15.         for(Student stu : set) {  
  16.             System.out.println(stu);  
  17.         }  
  18. //      输出结果:   
  19. //      Student [name=Bob YANG, age=22]  
  20. //      Student [name=XJ WANG, age=32]  
  21. //      Student [name=Hao LUO, age=33]  
  22. //      Student [name=Bruce LEE, age=60]  
  23.     }  
  24. }  


例子2:

 

Student.java

 

[java] view plain copy
 
 技术分享技术分享
  1. package com.lovo.demo;  
  2.   
  3. public class Student {  
  4.     private String name;    // 姓名  
  5.     private int age;        // 年龄  
  6.   
  7.     public Student(String name, int age) {  
  8.         this.name = name;  
  9.         this.age = age;  
  10.     }  
  11.   
  12.     /** 
  13.      * 获取学生姓名 
  14.      */  
  15.     public String getName() {  
  16.         return name;  
  17.     }  
  18.   
  19.     /** 
  20.      * 获取学生年龄 
  21.      */  
  22.     public int getAge() {  
  23.         return age;  
  24.     }  
  25.   
  26.     @Override  
  27.     public String toString() {  
  28.         return "Student [name=" + name + ", age=" + age + "]";  
  29.     }  
  30.   
  31. }  

Test02.java

 

 

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



57、sleep()和wait()有什么区别? 

答:sleep()方法是线程类(Thread)的静态方法,导致此线程暂停执行指定时间,将执行机会给其他线程,但是监控状态依然保持,到时后会自动恢复(线程回到就绪(ready)状态),因为调用sleep 不会释放对象锁。wait()是Object 类的方法,对此对象调用wait()方法导致本线程放弃对象锁(线程暂停执行),进入等待此对象的等待锁定池,只有针对此对象发出notify 方法(或notifyAll)后本线程才进入对象锁定池准备获得对象锁进入就绪状态。

技术分享

补充:这里似乎漏掉了一个作为先决条件的问题,就是什么是进程,什么是线程?为什么需要多线程编程?答案如下所示:

进程是具有一定独立功能的程序关于某个数据集合上的一次运行活动,是操作系统进行资源分配和调度的一个独立单位;线程是进程的一个实体,是CPU调度和分派的基本单位,是比进程更小的能独立运行的基本单位。线程的划分尺度小于进程,这使得多线程程序的并发性高;进程在执行时通常拥有独立的内存单元,而线程之间可以共享内存。使用多线程的编程通常能够带来更好的性能和用户体验,但是多线程的程序对于其他程序是不友好的,因为它占用了更多的CPU资源。

 

58、sleep()和yield()有什么区别?

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

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

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

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

 

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

答:不能。其它线程只能访问该对象的非同步方法,同步方法则不能进入。

 

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

答:

  1. wait():使一个线程处于等待(阻塞)状态,并且释放所持有的对象的锁;
  2. sleep():使一个正在运行的线程处于睡眠状态,是一个静态方法,调用此方法要捕捉InterruptedException 异常;
  3. notify():唤醒一个处于等待状态的线程,当然在调用此方法的时候,并不能确切的唤醒某一个等待状态的线程,而是由JVM确定唤醒哪个线程,而且与优先级无关;
  4. notityAll():唤醒所有处入等待状态的线程,注意并不是给所有唤醒线程一个对象的锁,而是让它们竞争;
  5. JDK 1.5通过Lock接口提供了显式(explicit)的锁机制,增强了灵活性以及对线程的协调。Lock接口中定义了加锁(lock())和解锁(unlock())的方法,同时还提供了newCondition()方法来产生用于线程之间通信的Condition对象;
  6. JDK 1.5还提供了信号量(semaphore)机制,信号量可以用来限制对某个共享资源进行访问的线程的数量。在对资源进行访问之前,线程必须得到信号量的许可(调用Semaphore对象的acquire()方法);在完成对资源的访问后,线程必须向信号量归还许可(调用Semaphore对象的release()方法)。
下面的例子演示了100个线程同时向一个银行账户中存入1元钱,在没有使用同步机制和使用同步机制情况下的执行情况。
银行账户类:
[java] view plain copy
 
  1. package com.lovo;  
  2.   
  3. /** 
  4.  * 银行账户 
  5.  * @author 骆昊 
  6.  * 
  7.  */  
  8. public class Account {  
  9.     private double balance;     // 账户余额  
  10.       
  11.     /** 
  12.      * 存款 
  13.      * @param money 存入金额 
  14.      */  
  15.     public void deposit(double money) {  
  16.         double newBalance = balance + money;  
  17.         try {  
  18.             Thread.sleep(10);   // 模拟此业务需要一段处理时间  
  19.         }  
  20.         catch(InterruptedException ex) {  
  21.             ex.printStackTrace();  
  22.         }  
  23.         balance = newBalance;  
  24.     }  
  25.       
  26.     /** 
  27.      * 获得账户余额 
  28.      */  
  29.     public double getBalance() {  
  30.         return balance;  
  31.     }  
  32. }  
存钱线程类:
[java] view plain copy
 
  1. package com.lovo;  
  2.   
  3. /** 
  4.  * 存钱线程 
  5.  * @author 骆昊 
  6.  * 
  7.  */  
  8. public class AddMoneyThread implements Runnable {  
  9.     private Account account;    // 存入账户  
  10.     private double money;       // 存入金额  
  11.   
  12.     public AddMoneyThread(Account account, double money) {  
  13.         this.account = account;  
  14.         this.money = money;  
  15.     }  
  16.   
  17.     @Override  
  18.     public void run() {  
  19.         account.deposit(money);  
  20.     }  
  21.   
  22. }  
测试类:
[java] view plain copy
 
  1. package com.lovo;  
  2.   
  3. import java.util.concurrent.ExecutorService;  
  4. import java.util.concurrent.Executors;  
  5.   
  6. public class Test01 {  
  7.   
  8.     public static void main(String[] args) {  
  9.         Account account = new Account();  
  10.         ExecutorService service = Executors.newFixedThreadPool(100);  
  11.           
  12.         for(int i = 1; i <= 100; i++) {  
  13.             service.execute(new AddMoneyThread(account, 1));  
  14.         }  
  15.           
  16.         service.shutdown();  
  17.           
  18.         while(!service.isTerminated()) {}  
  19.           
  20.         System.out.println("账户余额: " + account.getBalance());  
  21.     }  
  22. }  
在没有同步的情况下,执行结果通常是显示账户余额在10元以下,出现这种状况的原因是,当一个线程A试图存入1元的时候,另外一个线程B也能够进入存款的方法中,线程B读取到的账户余额仍然是线程A存入1元钱之前的账户余额,因此也是在原来的余额0上面做了加1元的操作,同理线程C也会做类似的事情,所以最后100个线程执行结束时,本来期望账户余额为100元,但实际得到的通常在10元以下。解决这个问题的办法就是同步,当一个线程对银行账户存钱时,需要将此账户锁定,待其操作完成后才允许其他的线程进行操作,代码有如下几种调整方案:
1. 在银行账户的存款(deposit)方法上同步(synchronized)关键字
[java] view plain copy
 
  1. package com.lovo;  
  2.   
  3. /** 
  4.  * 银行账户 
  5.  * @author 骆昊 
  6.  * 
  7.  */  
  8. public class Account {  
  9.     private double balance;     // 账户余额  
  10.       
  11.     /** 
  12.      * 存款 
  13.      * @param money 存入金额 
  14.      */  
  15.     public synchronized void deposit(double money) {  
  16.         double newBalance = balance + money;  
  17.         try {  
  18.             Thread.sleep(10);   // 模拟此业务需要一段处理时间  
  19.         }  
  20.         catch(InterruptedException ex) {  
  21.             ex.printStackTrace();  
  22.         }  
  23.         balance = newBalance;  
  24.     }  
  25.       
  26.     /** 
  27.      * 获得账户余额 
  28.      */  
  29.     public double getBalance() {  
  30.         return balance;  
  31.     }  
  32. }  
2. 在线程调用存款方法时对银行账户进行同步
[java] view plain copy
 
  1. package com.lovo;  
  2.   
  3. /** 
  4.  * 存钱线程 
  5.  * @author 骆昊 
  6.  * 
  7.  */  
  8. public class AddMoneyThread implements Runnable {  
  9.     private Account account;    // 存入账户  
  10.     private double money;       // 存入金额  
  11.   
  12.     public AddMoneyThread(Account account, double money) {  
  13.         this.account = account;  
  14.         this.money = money;  
  15.     }  
  16.   
  17.     @Override  
  18.     public void run() {  
  19.         synchronized (account) {  
  20.             account.deposit(money);   
  21.         }  
  22.     }  
  23.   
  24. }  
3. 通过JDK 1.5显示的锁机制,为每个银行账户创建一个锁对象,在存款操作进行加锁和解锁的操作
[java] view plain copy
 
  1. package com.lovo;  
  2.   
  3. import java.util.concurrent.locks.Lock;  
  4. import java.util.concurrent.locks.ReentrantLock;  
  5.   
  6. /** 
  7.  * 银行账户 
  8.  *  
  9.  * @author 骆昊 
  10.  * 
  11.  */  
  12. public class Account {  
  13.     private Lock accountLock = new ReentrantLock();  
  14.     private double balance; // 账户余额  
  15.   
  16.     /** 
  17.      * 存款 
  18.      *  
  19.      * @param money 
  20.      *            存入金额 
  21.      */  
  22.     public void deposit(double money) {  
  23.         accountLock.lock();  
  24.         try {  
  25.             double newBalance = balance + money;  
  26.             try {  
  27.                 Thread.sleep(10); // 模拟此业务需要一段处理时间  
  28.             }  
  29.             catch (InterruptedException ex) {  
  30.                 ex.printStackTrace();  
  31.             }  
  32.             balance = newBalance;  
  33.         }  
  34.         finally {  
  35.             accountLock.unlock();  
  36.         }  
  37.     }  
  38.   
  39.     /** 
  40.      * 获得账户余额 
  41.      */  
  42.     public double getBalance() {  
  43.         return balance;  
  44.     }  
  45. }  
按照上述三种方式对代码进行修改后,重写执行测试代码Test01,将看到最终的账户余额为100元。
 

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

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

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

 

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

 

 

62、synchronized关键字的用法?

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

 

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

多线程并发时,多个线程同时请求同一个资源,必然导致此资源的数据不安全,A线程修改了B线

程的处理的数据,而B线程又修改了A线程处理的数理。显然这是由于全局资源造成的,有时为了解

决此问题,优先考虑使用局部变量,退而求其次使用同步代码块,出于这样的安全考虑就必须牺牲

系统处理性能,加在多线程并发时资源挣夺最激烈的地方,这就实现了线程的同步机制

同步:A线程要请求某个资源,但是此资源正在被B线程使用中,因为同步机制存在,A线程请求

不到,怎么办,A线程只能等待下去

异步:A线程要请求某个资源,但是此资源正在被B线程使用中,因为没有同步机制存在,A线程

仍然请求的到,A线程无需等待

 

显然,同步最最安全,最保险的。而异步不安全,容易导致死锁,这样一个线程死掉就会导致整个

进程崩溃,但没有同步机制的存在,性能会有所提升

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

 

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

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

 

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

答:在面向对象编程中,创建和销毁对象是很费时间的,因为创建一个对象要获取内存资源或者其它更多资源。在Java中更是如此,虚拟机将试图跟踪每一个对象,以便能够在对象销

以上是关于Java程序员面试题集2的主要内容,如果未能解决你的问题,请参考以下文章

面试的哪些事儿之JAVA程序员面试笔试题

Java面试题集--Spring常见面试问题

面试阿里,字节跳动90%会被问到的Java异常面试题集,史上最全系列!

Java面试题集2021版,内容太过真实

Java类加载器面试题集锦

Java程序员精选高频面试笔试题全家桶,通往BAT必备法宝!《附赠PDF》