互聯網 Java 工程師面試題(Java 面試題二)

4八、運行時異常與受檢異常有何異同?

答: 異常表示程序運行過程當中可能出現的非正常狀態,運行時異常表示虛擬機的一般 操做中可能遇到的異常,是一種常見運行錯誤,只要程序設計得沒有問題一般就 不會發生。受檢異常跟程序運行的上下文環境有關,即便程序設計無誤,仍然可 能因使用的問題而引起。Java 編譯器要求方法必須聲明拋出可能發生的受檢異常, 可是並不要求必須聲明拋出未被捕獲的運行時異常。異常和繼承同樣,是面向對 象程序設計中常常被濫用的東西,在 Effective Java 中對異常的使用給出瞭如下指 導原則:java

  • 不要將異常處理用於正常的控制流(設計良好的 API 不該該強迫它的調 用者爲了正常的控制流而使用異常)
  • 對能夠恢復的狀況使用受檢異常,對編程錯誤使用運行時異常
  • 避免沒必要要的使用受檢異常(能夠經過一些狀態檢測手段來避免異常的發生)
  • 優先使用標準的異常
  • 每一個方法拋出的異常都要有文檔
  • 保持異常的原子性
  • 不要在 catch 中忽略掉捕獲到的異常

4九、列出一些你常見的運行時異常?

答:程序員

  • 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()方法能夠整理系統資源或者執行 其餘清理工做。

5一、類 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}

5二、List、Set、Map 是否繼承自 Collection 接口?

答: List、Set 是,Map 不是。Map 是鍵值對映射容器,與 List 和 Set 有明顯的區別, 而 Set 存儲的零散的元素且不容許有重複元素(數學中的集合也是如此),List 是線性結構的容器,適用於按數值索引訪問元素的情形。數組

5三、闡述 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 公司的工 程師們也會犯這種低級錯誤,讓人唏噓不已。安全

5四、Collection 和 Collections 的區別?

答: Collection 是一個接口,它是 Set、List 等容器的父接口;Collections 是個一個 工具類,提供了一系列的靜態方法來輔助容器操做,這些方法包括對容器的搜索、 排序、線程安全化等等。

5五、List、Map、Set 三個接口存取元素時,各有什麼特色?

答: List 以特定索引來存取元素,能夠有重複元素。Set 不能存放重複元素(用對象的 equals()方法來區分元素是否重複)。Map 保存鍵值對(key-value pair)映射, 映射關係能夠是一對一或多對一。Set 和 Map 容器都有基於哈希存儲和排序樹的 兩種實現版本,基於哈希存儲的版本理論存取時間複雜度爲 O(1),而基於排序樹 版本的實如今插入或刪除元素時會按照元素或元素的鍵(key)構成排序樹從而達 到排序和去重的效果。

5六、TreeMap 和 TreeSet 在排序時如何比較元素? Collections 工具類中的 sort()方法如何比較元素?

答: TreeSet 要求存放的對象所屬的類必須實現 Comparable 接口,該接口提供了比 較元素的 compareTo()方法,當插入元素時會回調該方法比較元素的大小。 TreeMap 要求存放的鍵值對映射的鍵必須實現 Comparable 接口從而根據鍵對元 素進行排序。Collections 工具類的 sort 方法有兩種重載的形式,第一種要求傳入 的待排序容器中存放的對象比較實現 Comparable 接口以實現元素的比較;第二 種不強制性的要求容器中的元素必須可比較,可是要求傳入第二個參數,參數是 Comparator 接口的子類型(須要重寫 compare 方法實現元素的比較),至關於 一個臨時定義的排序規則,其實就是經過接口注入比較元素大小的算法,也是對 回調模式的應用(Java 中對函數式編程的支持)。

例子 1:

1public class Student implements Comparable<Student> {
 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<Student> 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<Student> 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<Student> () {
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}

5七、Thread 類的 sleep()方法和對象的 wait()方法均可以讓線 程暫停執行,它們有什麼區別?

答: sleep()方法(休眠)是線程類(Thread)的靜態方法,調用此方法會讓當前線程 暫停執行指定的時間,將執行機會(CPU)讓給其餘線程,可是對象的鎖依然保 持,所以休眠時間結束後會自動恢復(線程回到就緒狀態,請參考第 66 題中的線 程狀態轉換圖)。wait()是 Object 類的方法,調用對象的 wait()方法致使當前線 程放棄對象的鎖(線程暫停執行),進入對象的等待池(wait pool),只有調用 對象的 notify()方法(或 notifyAll()方法)時才能喚醒等待池中的線程進入等鎖池 (lock pool),若是線程從新得到對象的鎖就能夠進入就緒狀態。

補充:可能很多人對什麼是進程,什麼是線程還比較模糊,對於爲何須要多線 程編程也不是特別理解。簡單的說:進程是具備必定獨立功能的程序關於某個數 據集合上的一次運行活動,是操做系統進行資源分配和調度的一個獨立單位;線 程是進程的一個實體,是 CPU 調度和分派的基本單位,是比進程更小的能獨立運 行的基本單位。線程的劃分尺度小於進程,這使得多線程程序的併發性高;進程 在執行時一般擁有獨立的內存單元,而線程之間能夠共享內存。使用多線程的編 程一般可以帶來更好的性能和用戶體驗,可是多線程的程序對於其餘程序是不友 好的,由於它可能佔用了更多的 CPU 資源。固然,也不是線程越多,程序的性能 就越好,由於線程之間的調度和切換也會浪費 CPU 時間。時下很時髦的 Node.js 就採用了單線程異步 I/O 的工做模式。

5八、線程的 sleep()方法和 yield()方法有什麼區別?

答:

① sleep()方法給其餘線程運行機會時不考慮線程的優先級,所以會給低優先級的 線程以運行的機會;yield()方法只會給相同優先級或更高優先級的線程以運行的 機會;

② 線程執行 sleep()方法後轉入阻塞(blocked)狀態,而執行 yield()方法後轉 入就緒 (ready)狀態;

③ sleep()方法聲明拋出 InterruptedException,而 yield()方法沒有聲明任何異 常;

④ sleep()方法比 yield()方法(跟操做系統 CPU 調度相關)具備更好的可移植性。

5九、當一個線程進入一個對象的 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 來實現同步。

6一、編寫多線程程序有幾種實現方式?

答: 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<Integer> {
 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<Future<Integer>> 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<Integer> future : list) {
31// while(!future.isDone()) ;
32sum += future.get();
33}
34System.out.println(sum);
35}
36}

6二、synchronized 關鍵字的用法?

答: synchronized 關鍵字能夠將對象或者方法標記爲同步,以實現對對象和方法的互 斥訪問,能夠用 synchronized(對象) { … }定義同步代碼塊,或者在聲明方法時 將 synchronized 做爲方法的修飾符。在第 60 題的例子中已經展現了 synchronized 關鍵字的用法。

6三、舉例說明同步和異步。

答: 若是系統中存在臨界資源(資源數量少於競爭資源的線程數量的資源),例如正 在寫的數據之後可能被另外一個線程讀到,或者正在讀的數據可能已經被另外一個線 程寫過了,那麼這些數據就必須進行同步存取(數據庫操做中的排他鎖就是最好 的例子)。當應用程序在對象上調用了一個須要花費很長時間來執行的方法,並 且不但願讓程序等待方法的返回時,就應該使用異步編程,在不少狀況下采用異 步途徑每每更有效率。事實上,所謂的同步就是指阻塞式操做,而異步就是非阻 塞式操做。

6四、啓動一個線程是調用 run()仍是 start()方法?

答: 啓動一個線程是調用 start()方法,使線程所表明的虛擬處理機處於可運行狀態, 這意味着它能夠由 JVM 調度並執行,這並不意味着線程就會當即運行。run()方 法是線程啓動後要進行回調(callback)的方法。

6五、什麼是線程池(thread pool)?

答: 在面向對象編程中,建立和銷燬對象是很費時間的,由於建立一個對象要獲取內 存資源或者其它更多資源。在 Java 中更是如此,虛擬機將試圖跟蹤每個對象, 以便可以在對象銷燬後進行垃圾回收。因此提升服務程序效率的一個手段就是盡 可能減小建立和銷燬對象的次數,特別是一些很耗資源的對象建立和銷燬,這就 是」池化資源」技術產生的緣由。線程池顧名思義就是事先建立若干個可執行的 線程放入一個池(容器)中,須要的時候從池中獲取線程不用自行建立,使用完 畢不須要銷燬線程而是放回池中,從而減小建立和銷燬線程對象的開銷。 Java 5+中的 Executor 接口定義一個執行線程的工具。它的子類型即線程池接口 是 ExecutorService。要配置一個線程池是比較複雜的,尤爲是對於線程池的原理 不是很清楚的狀況下,所以在工具類 Executors 面提供了一些靜態工廠方法,生 成一些經常使用的線程池,以下所示:

newSingleThreadExecutor:建立一個單線程的線程池。這個線程池只 有一個線程在工做,也就是至關於單線程串行執行全部任務。若是這個惟一的程由於異常結束,那麼會有一個新的線程來替代它。此線程池保證全部任務的執 行順序按照任務的提交順序執行。
newFixedThreadPool:建立固定大小的線程池。每次提交一個任務就創 建一個線程,直到線程達到線程池的最大大小。線程池的大小一旦達到最大值就 會保持不變,若是某個線程由於執行異常而結束,那麼線程池會補充一個新線程。
newCachedThreadPool:建立一個可緩存的線程池。若是線程池的大小 超過了處理任務所須要的線程,那麼就會回收部分空閒(60 秒不執行任務)的 線程,當任務數增長時,此線程池又能夠智能的添加新線程來處理任務。此線程 池不會對線程池大小作限制,線程池大小徹底依賴於操做系統(或者說 JVM) 可以建立的最大線程大小。
newScheduledThreadPool:建立一個大小無限的線程池。此線程池支 持定時以及週期性執行任務的需求。
newSingleThreadExecutor:建立一個單線程的線程池。此線程池支持 定時以及週期性執行任務的需求。
第 60 題的例子中演示了經過 Executors 工具類建立線程池並使用線程池執行線程 的代碼。若是但願在服務器上使用線程池,強烈建議使用 newFixedThreadPool 方法來建立線程池,這樣能得到更好的性能。

6六、線程的基本狀態以及狀態之間的關係?

答:

說明:其中 Running 表示運行狀態,Runnable 表示就緒狀態(萬事俱備,只欠 CPU),Blocked 表示阻塞狀態,阻塞狀態又有多種狀況,多是由於調用 wait() 方法進入等待池,也多是執行同步方法或同步代碼塊進入等鎖池,或者是調用 了 sleep()方法或 join()方法等待休眠或其餘線程結束,或是由於發生了 I/O 中斷

6七、簡述 synchronized 和 java.util.concurrent.locks.Lock 的異同?

答: Lock 是 Java 5 之後引入的新的 API,和關鍵字 synchronized 相比主要相同點: Lock 能完成 synchronized 所實現的全部功能;主要不一樣點:Lock 有比 synchronized 更精確的線程語義和更好的性能,並且不強制性的要求必定要得到 鎖。synchronized 會自動釋放鎖,而 Lock 必定要求程序員手工釋放,而且最在 finally 塊中釋放(這是釋放外部資源的最好的地方)。

6八、Java 中如何實現序列化,有什麼意義?

答: 序列化就是一種用來處理對象流的機制,所謂對象流也就是將對象的內容進行流 化。能夠對流化後的對象進行讀寫操做,也可將流化後的對象傳輸於網絡之間。 序列化是爲了解決對象流讀寫操做時可能引起的問題(若是不進行序列化可能會 存在數據亂序的問題)。 要實現序列化,須要讓一個類實現 Serializable 接口,該接口是一個標識性接口, 標註該類對象是可被序列化的,而後使用一個輸出流來構造一個對象輸出流並通 過 writeObject(Object)方法就能夠將實現對象寫出(即保存其狀態);若是須要 反序列化則能夠用一個輸入流創建對象輸入流,而後經過 readObject 方法從流中 讀取對象。序列化除了可以實現對象的持久化以外,還可以用於對象的深度克隆 (能夠參考第 29 題)。

6九、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}

7一、如何用 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<Path>() {
 5@Override
 6public FileVisitResult visitFile(Path file, BasicFileAttributes
 7attrs)
 8throws IOException {
 9System.out.println(file.getFileName().toString());
10return FileVisitResult.CONTINUE;
11}
12});
13}
14}

7二、用 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<SelectionKey> 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}

7三、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 容許應用程序代碼把這些事件逐 個拉出來,而不用提供在解析器方便時從解析器中接收事件的處理程序。

7四、你在項目中哪些地方用到了 XML?

答: XML 的主要做用有兩個方面:數據交換和信息配置。在作數據交換時,XML 將數 據用標籤組裝成起來,而後壓縮打包加密後經過網絡傳送給接收者,接收解密與 解壓縮後再從 XML 文件中還原相關信息進行處理,XML 曾經是異構系統間交換數 據的事實標準,但此項功能幾乎已經被 JSON(JavaScript Object Notation)取 而代之。固然,目前不少軟件仍然使用 XML 來存儲配置信息,咱們在不少項目中 一般也會將做爲配置信息的硬代碼寫在 XML 文件中,Java 的不少框架也是這麼作 的,並且這些框架都選擇了 dom4j 做爲處理 XML 的工具,由於 Sun 公司的官方 API 實在不怎麼好用。

補充:如今有不少時髦的軟件(如 Sublime)已經開始將配置文件書寫成 JSON 格式,咱們已經強烈的感覺到 XML 的另外一項功能也將逐漸被業界拋棄。

相關文章
相關標籤/搜索