Android知識架構 · Java的編程思想下 [復制鏈接]

2018-11-9 10:08
James1991 閱讀:595 評論:0 贊:1
Tag:  

51、類ExampleA繼承Exception,類ExampleB繼承ExampleA。

有如下代碼片斷:

  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. }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7

請問執行此段代碼的輸出是什么?

答:輸出:ExampleA。(根據里氏代換原則[能使用父類型的地方一定能使用子類型],抓取ExampleA類型異常的catch塊能夠抓住try塊中拋出的ExampleB類型的異常)

面試題 - 說出下面代碼的運行結果。(此題的出處是《Java編程思想》一書)

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

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:

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

例子2:

  1. public class Student {
  2. private String name; // 姓名
  3. private int age; // 年齡
  4. public Student(String name, int age) {
  5. this.name = name;
  6. this.age = age;
  7. }
  8. /**
  9. * 獲取學生姓名
  10. */
  11. public String getName() {
  12. return name;
  13. }
  14. /**
  15. * 獲取學生年齡
  16. */
  17. public int getAge() {
  18. return age;
  19. }
  20. @Override
  21. public String toString() {
  22. return "Student [name=" + name + ", age=" + age + "]";
  23. }
  24. }
  • 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
  1. import java.util.ArrayList;
  2. import java.util.Collections;
  3. import java.util.Comparator;
  4. import java.util.List;
  5. class Test02 {
  6. public static void main(String[] args) {
  7. List<Student> list = new ArrayList<>(); // Java 7的鉆石語法(構造器后面的尖括號中不需要寫類型)
  8. list.add(new Student("Hao LUO", 33));
  9. list.add(new Student("XJ WANG", 32));
  10. list.add(new Student("Bruce LEE", 60));
  11. list.add(new Student("Bob YANG", 22));
  12. // 通過sort方法的第二個參數傳入一個Comparator接口對象
  13. // 相當于是傳入一個比較對象大小的算法到sort方法中
  14. // 由于Java中沒有函數指針、仿函數、委托這樣的概念
  15. // 因此要將一個算法傳入一個方法中唯一的選擇就是通過接口回調
  16. Collections.sort(list, new Comparator<Student> () {
  17. @Override
  18. public int compare(Student o1, Student o2) {
  19. return o1.getName().compareTo(o2.getName()); // 比較學生姓名
  20. }
  21. });
  22. for(Student stu : list) {
  23. System.out.println(stu);
  24. }
  25. // 輸出結果:
  26. // Student [name=Bob YANG, age=22]
  27. // Student [name=Bruce LEE, age=60]
  28. // Student [name=Hao LUO, age=33]
  29. // Student [name=XJ WANG, age=32]
  30. }
  31. }
  • 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
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36

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. */
  6. public class Account {
  7. private double balance; // 賬戶余額
  8. /**
  9. * 存款
  10. * @param money 存入金額
  11. */
  12. public void deposit(double money) {
  13. double newBalance = balance + money;
  14. try {
  15. Thread.sleep(10); // 模擬此業務需要一段處理時間
  16. }
  17. catch(InterruptedException ex) {
  18. ex.printStackTrace();
  19. }
  20. balance = newBalance;
  21. }
  22. /**
  23. * 獲得賬戶余額
  24. */
  25. public double getBalance() {
  26. return balance;
  27. }
  28. }
  • 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
  • 存錢線程類:
  1. /**
  2. * 存錢線程
  3. * @author 駱昊
  4. *
  5. */
  6. public class AddMoneyThread implements Runnable {
  7. private Account account; // 存入賬戶
  8. private double money; // 存入金額
  9. public AddMoneyThread(Account account, double money) {
  10. this.account = account;
  11. this.money = money;
  12. }
  13. @Override
  14. public void run() {
  15. account.deposit(money);
  16. }
  17. }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 測試類:
  1. import java.util.concurrent.ExecutorService;
  2. import java.util.concurrent.Executors;
  3. public class Test01 {
  4. public static void main(String[] args) {
  5. Account account = new Account();
  6. ExecutorService service = Executors.newFixedThreadPool(100);
  7. for(int i = 1; i <= 100; i++) {
  8. service.execute(new AddMoneyThread(account, 1));
  9. }
  10. service.shutdown();
  11. while(!service.isTerminated()) {}
  12. System.out.println("賬戶余額: " + account.getBalance());
  13. }
  14. }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20

在沒有同步的情況下,執行結果通常是顯示賬戶余額在10元以下,出現這種狀況的原因是,當一個線程A試圖存入1元的時候,另外一個線程B也能夠進入存款的方法中,線程B讀取到的賬戶余額仍然是線程A存入1元錢之前的賬戶余額,因此也是在原來的余額0上面做了加1元的操作,同理線程C也會做類似的事情,所以最后100個線程執行結束時,本來期望賬戶余額為100元,但實際得到的通常在10元以下(很可能是1元哦)。解決這個問題的辦法就是同步,當一個線程對銀行賬戶存錢時,需要將此賬戶鎖定,待其操作完成后才允許其他的線程進行操作,代碼有如下幾種調整方案:

  • 在銀行賬戶的存款(deposit)方法上同步(synchronized)關鍵字
  1. /**
  2. * 銀行賬戶
  3. * @author 駱昊
  4. *
  5. */
  6. public class Account {
  7. private double balance; // 賬戶余額
  8. /**
  9. * 存款
  10. * @param money 存入金額
  11. */
  12. public synchronized void deposit(double money) {
  13. double newBalance = balance + money;
  14. try {
  15. Thread.sleep(10); // 模擬此業務需要一段處理時間
  16. }
  17. catch(InterruptedException ex) {
  18. ex.printStackTrace();
  19. }
  20. balance = newBalance;
  21. }
  22. /**
  23. * 獲得賬戶余額
  24. */
  25. public double getBalance() {
  26. return balance;
  27. }
  28. }
  • 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
  • 在線程調用存款方法時對銀行賬戶進行同步
  1. /**
  2. * 存錢線程
  3. * @author 駱昊
  4. *
  5. */
  6. public class AddMoneyThread implements Runnable {
  7. private Account account; // 存入賬戶
  8. private double money; // 存入金額
  9. public AddMoneyThread(Account account, double money) {
  10. this.account = account;
  11. this.money = money;
  12. }
  13. @Override
  14. public void run() {
  15. synchronized (account) {
  16. account.deposit(money);
  17. }
  18. }
  19. }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 通過Java 5顯示的鎖機制,為每個銀行賬戶創建一個鎖對象,在存款操作進行加鎖和解鎖的操作
  1. import java.util.concurrent.locks.Lock;
  2. import java.util.concurrent.locks.ReentrantLock;
  3. /**
  4. * 銀行賬戶
  5. *
  6. * @author 駱昊
  7. *
  8. */
  9. public class Account {
  10. private Lock accountLock = new ReentrantLock();
  11. private double balance; // 賬戶余額
  12. /**
  13. * 存款
  14. *
  15. * @param money
  16. * 存入金額
  17. */
  18. public void deposit(double money) {
  19. accountLock.lock();
  20. try {
  21. double newBalance = balance + money;
  22. try {
  23. Thread.sleep(10); // 模擬此業務需要一段處理時間
  24. }
  25. catch (InterruptedException ex) {
  26. ex.printStackTrace();
  27. }
  28. balance = newBalance;
  29. }
  30. finally {
  31. accountLock.unlock();
  32. }
  33. }
  34. /**
  35. * 獲得賬戶余額
  36. */
  37. public double getBalance() {
  38. return balance;
  39. }
  40. }
  • 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
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43

按照上述三種方式對代碼進行修改后,重寫執行測試代碼Test01,將看到最終的賬戶余額為100元。當然也可以使用Semaphore或CountdownLatch來實現同步。

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

答:Java 5以前實現多線程有兩種實現方法:一種是繼承Thread類;另一種是實現Runnable接口。兩種方式都要通過重寫run()方法來定義線程的行為,推薦使用后者,因為Java中的繼承是單繼承,一個類有一個父類,如果繼承了Thread類就無法再繼承其他類了,顯然使用Runnable接口更為靈活。

補充:Java 5以后創建線程還有第三種方式:實現Callable接口,該接口中的call方法可以在線程執行結束時產生一個返回值,代碼如下所示:

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

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。在java.io 包中還有許多其他的流,主要是為了提高性能和使用方便。關于Java的I/O需要注意的有兩點:一是兩種對稱性(輸入和輸出的對稱性,字節和字符的對稱性);二是兩種設計模式(適配器模式和裝潢模式)。另外Java中的流不同于C#的是它只有一個維度一個方向。

面試題 - 編程實現文件拷貝。(這個題目在筆試的時候經常出現,下面的代碼給出了兩種實現方案)

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

注意:上面用到Java 7的TWR,使用TWR后可以不用在finally中釋放外部資源 ,從而讓代碼更加優雅。

70、寫一個方法,輸入一個文件名和一個字符串,統計這個字符串在這個文件中出現的次數。

答:代碼如下:

  1. import java.io.BufferedReader;
  2. import java.io.FileReader;
  3. public final class MyUtil {
  4. // 工具類中的方法都是靜態方式訪問的因此將構造器私有不允許創建對象(絕對好習慣)
  5. private MyUtil() {
  6. throw new AssertionError();
  7. }
  8. /**
  9. * 統計給定文件中給定字符串的出現次數
  10. *
  11. * @param filename 文件名
  12. * @param word 字符串
  13. * @return 字符串在文件中出現的次數
  14. */
  15. public static int countWordInFile(String filename, String word) {
  16. int counter = 0;
  17. try (FileReader fr = new FileReader(filename)) {
  18. try (BufferedReader br = new BufferedReader(fr)) {
  19. String line = null;
  20. while ((line = br.readLine()) != null) {
  21. int index = -1;
  22. while (line.length() >= word.length() && (index = line.indexOf(word)) >= 0) {
  23. counter++;
  24. line = line.substring(index + word.length());
  25. }
  26. }
  27. }
  28. } catch (Exception ex) {
  29. ex.printStackTrace();
  30. }
  31. return counter;
  32. }
  33. }
  • 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
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37

71、如何用Java代碼列出一個目錄下所有的文件?

答: 
如果只要求列出當前文件夾下的文件,代碼如下所示:

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

如果需要對文件夾繼續展開,代碼如下所示:

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

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

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

72、用Java的套接字編程實現一個多線程的回顯(echo)服務器。

答:

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

注意:上面的代碼使用了Java 7的TWR語法,由于很多外部資源類都間接的實現了AutoCloseable接口(單方法回調接口),因此可以利用TWR語法在try結束的時候通過回調的方式自動調用外部資源類的close()方法,避免書寫冗長的finally代碼塊。此外,上面的代碼用一個靜態內部類實現線程的功能,使用多線程可以避免一個用戶I/O操作所產生的中斷影響其他用戶對服務器的訪問,簡單的說就是一個用戶的輸入操作不會造成其他用戶的阻塞。當然,上面的代碼使用線程池可以獲得更好的性能,因為頻繁的創建和銷毀線程所造成的開銷也是不可忽視的。

下面是一段回顯客戶端測試代碼:

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

如果希望用NIO的多路復用套接字實現服務器,代碼如下所示。NIO的操作雖然帶來了更好的性能,但是有些操作是比較底層的,對于初學者來說還是有些難于理解。

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

73、XML文檔定義有幾種形式?它們之間有何本質區別?解析XML文檔有哪幾種方式?

答:XML文檔定義分為DTD和Schema兩種形式,二者都是對XML語法的約束,其本質區別在于Schema本身也是一個XML文件,可以被XML解析器解析,而且可以為XML承載的數據定義類型,約束能力較之DTD更強大。對XML的解析主要有DOM(文檔對象模型,Document Object Model)、SAX(Simple API for XML)和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的另一項功能也將逐漸被業界拋棄。

75、闡述JDBC操作數據庫的步驟。

答:下面的代碼以連接本機的Oracle數據庫為例,演示JDBC操作數據庫的步驟。

  • 加載驅動
Class.forName("oracle.jdbc.driver.OracleDriver");
  • 1
  • 創建連接
 Connection con = DriverManager.getConnection("jdbc:oracle:thin:@localhost:1521:orcl", "scott", "tiger");
  • 1
  • 創建語句
  1. PreparedStatement ps = con.prepareStatement("select * from emp where sal between ? and ?");
  2. ps.setInt(1, 1000);
  3. ps.setInt(2, 3000);
  • 1
  • 2
  • 3
  • 執行語句
  ResultSet rs = ps.executeQuery();
  • 1
  • 處理結果
  1. while(rs.next()) {
  2. System.out.println(rs.getInt("empno") + " - " + rs.getString("ename"));
  3. }
  • 1
  • 2
  • 3
  • 關閉資源
  1. finally {
  2. if(con != null) {
  3. try {
  4. con.close();
  5. } catch (SQLException e) {
  6. e.printStackTrace();
  7. }
  8. }
  9. }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9

提示:關閉外部資源的順序應該和打開的順序相反,也就是說先關閉ResultSet、再關閉Statement、在關閉Connection。上面的代碼只關閉了Connection(連接),雖然通常情況下在關閉連接時,連接上創建的語句和打開的游標也會關閉,但不能保證總是如此,因此應該按照剛才說的順序分別關閉。此外,第一步加載驅動在JDBC 4.0中是可以省略的(自動從類路徑中加載驅動),但是我們建議保留。

76、Statement和PreparedStatement有什么區別?哪個性能更好?

答:與Statement相比,①PreparedStatement接口代表預編譯的語句,它主要的優勢在于可以減少SQL的編譯錯誤并增加SQL的安全性(減少SQL注射攻擊的可能性);②PreparedStatement中的SQL語句是可以帶參數的,避免了用字符串連接拼接SQL語句的麻煩和不安全;③當批量處理SQL或頻繁執行相同的查詢時,PreparedStatement有明顯的性能上的優勢,由于數據庫可以將編譯優化后的SQL語句緩存起來,下次執行相同結構的語句時就會很快(不用再次編譯和生成執行計劃)。

補充:為了提供對存儲過程的調用,JDBC API中還提供了CallableStatement接口。存儲過程(Stored Procedure)是數據庫中一組為了完成特定功能的SQL語句的集合,經編譯后存儲在數據庫中,用戶通過指定存儲過程的名字并給出參數(如果該存儲過程帶有參數)來執行它。雖然調用存儲過程會在網絡開銷、安全性、性能上獲得很多好處,但是存在如果底層數據庫發生遷移時就會有很多麻煩,因為每種數據庫的存儲過程在書寫上存在不少的差別。

77、使用JDBC操作數據庫時,如何提升讀取數據的性能?如何提升更新數據的性能?

答:要提升讀取數據的性能,可以指定通過結果集(ResultSet)對象的setFetchSize()方法指定每次抓取的記錄數(典型的空間換時間策略);要提升更新數據的性能可以使用PreparedStatement語句構建批處理,將若干SQL語句置于一個批處理中執行。

78、在進行數據庫編程時,連接池有什么作用?

答:由于創建連接和釋放連接都有很大的開銷(尤其是數據庫服務器不在本地時,每次建立連接都需要進行TCP的三次握手,釋放連接需要進行TCP四次握手,造成的開銷是不可忽視的),為了提升系統訪問數據庫的性能,可以事先創建若干連接置于連接池中,需要時直接從連接池獲取,使用結束時歸還連接池而不必關閉連接,從而避免頻繁創建和釋放連接所造成的開銷,這是典型的用空間換取時間的策略(浪費了空間存儲連接,但節省了創建和釋放連接的時間)。池化技術在Java開發中是很常見的,在使用線程時創建線程池的道理與此相同。基于Java的開源數據庫連接池主要有:C3P0、Proxool、DBCP、BoneCP、Druid等。

補充:在計算機系統中時間和空間是不可調和的矛盾,理解這一點對設計滿足性能要求的算法是至關重要的。大型網站性能優化的一個關鍵就是使用緩存,而緩存跟上面講的連接池道理非常類似,也是使用空間換時間的策略。可以將熱點數據置于緩存中,當用戶查詢這些數據時可以直接從緩存中得到,這無論如何也快過去數據庫中查詢。當然,緩存的置換策略等也會對系統性能產生重要影響,對于這個問題的討論已經超出了這里要闡述的范圍。

79、什么是DAO模式?

答:DAO(Data Access Object)顧名思義是一個為數據庫或其他持久化機制提供了抽象接口的對象,在不暴露底層持久化方案實現細節的前提下提供了各種數據訪問操作。在實際的開發中,應該將所有對數據源的訪問操作進行抽象化后封裝在一個公共API中。用程序設計語言來說,就是建立一個接口,接口中定義了此應用程序中將會用到的所有事務方法。在這個應用程序中,當需要和數據源進行交互的時候則使用這個接口,并且編寫一個單獨的類來實現這個接口,在邏輯上該類對應一個特定的數據存儲。DAO模式實際上包含了兩個模式,一是Data Accessor(數據訪問器),二是Data Object(數據對象),前者要解決如何訪問數據的問題,而后者要解決的是如何用對象封裝數據。

80、事務的ACID是指什么? 
答:

  • 原子性(Atomic):事務中各項操作,要么全做要么全不做,任何一項操作的失敗都會導致整個事務的失敗;
  • 一致性(Consistent):事務結束后系統狀態是一致的;
  • 隔離性(Isolated):并發執行的事務彼此無法看到對方的中間狀態;
  • 持久性(Durable):事務完成后所做的改動都會被持久化,即使發生災難性的失敗。通過日志和同步備份可以在故障發生后重建數據。

    補充:關于事務,在面試中被問到的概率是很高的,可以問的問題也是很多的。首先需要知道的是,只有存在并發數據訪問時才需要事務。當多個事務訪問同一數據時,可能會存在5類問題,包括3類數據讀取問題(臟讀、不可重復讀和幻讀)和2類數據更新問題(第1類丟失更新和第2類丟失更新)。 
    臟讀(Dirty Read):A事務讀取B事務尚未提交的數據并在此基礎上操作,而B事務執行回滾,那么A讀取到的數據就是臟數據。

時間轉賬事物A取款事務B
T1 開始事務
T2開始事務 
T3 查詢賬戶余額為1000元
T4 取出500元余額修改為500元
T5查詢賬戶余額為500元(臟讀) 
T6 撤銷事務余額恢復為1000元
T7匯入100元把余額修改為600元 
T8提交事務 

不可重復讀(Unrepeatable Read):事務A重新讀取前面讀取過的數據,發現該數據已經被另一個已提交的事務B修改過了。

時間轉賬事物A取款事務B
T1 開始事務
T2開始事務 
T3 查詢賬戶余額為1000元
T4查詢賬戶余額為1000元 
T5 取出100元修改余額為900元
T6 提交事務
T7查詢賬戶余額為900元(不可重復讀) 

幻讀(Phantom Read):事務A重新執行一個查詢,返回一系列符合查詢條件的行,發現其中插入了被事務B提交的行。

時間統計金額事務A取款事務B
T1 開始事務
T2開始事務 
T3統計總存款為10000元 
T4 新增一個存款賬戶存入100元
T5 提交事務
T6再次統計總存款為10100元(幻讀) 

第1類丟失更新:事務A撤銷時,把已經提交的事務B的更新數據覆蓋了。

時間轉賬事物A取款事務B
T1開始事務 
T2 開始事務
T3查詢賬戶余額為1000元 
T4 查詢賬戶余額為1000元
T5 匯入100元修改余額為1100元
T6 提交事務
T7取出100元將余額修改為900元 
T8撤銷事務 
T9余額恢復為1000元(丟失更新) 

第2類丟失更新:事務A覆蓋事務B已經提交的數據,造成事務B所做的操作丟失。