48吨悍、運行時異常與受檢異常有何異同扫茅?
答: 異常表示程序運行過程中可能出現(xiàn)的非正常狀態(tài),運行時異常表示虛擬機的通常 操作中可能遇到的異常育瓜,是一種常見運行錯誤葫隙,只要程序設(shè)計得沒有問題通常就 不會發(fā)生。受檢異常跟程序運行的上下文環(huán)境有關(guān)躏仇,即使程序設(shè)計無誤恋脚,仍然可 能因使用的問題而引發(fā)。Java 編譯器要求方法必須聲明拋出可能發(fā)生的受檢異常焰手, 但是并不要求必須聲明拋出未被捕獲的運行時異常糟描。異常和繼承一樣,是面向?qū)?象程序設(shè)計中經(jīng)常被濫用的東西书妻,在 Effective Java 中對異常的使用給出了以下指 導(dǎo)原則:
- 不要將異常處理用于正常的控制流(設(shè)計良好的 API 不應(yīng)該強迫它的調(diào) 用者為了正常的控制流而使用異常)
- 對可以恢復(fù)的情況使用受檢異常船响,對編程錯誤使用運行時異常
- 避免不必要的使用受檢異常(可以通過一些狀態(tài)檢測手段來避免異常的發(fā)生)
- 優(yōu)先使用標(biāo)準(zhǔn)的異常
- 每個方法拋出的異常都要有文檔
- 保持異常的原子性
- 不要在 catch 中忽略掉捕獲到的異常
49、列出一些你常見的運行時異常?
答:
- ArithmeticException(算術(shù)異常)
- ClassCastException (類轉(zhuǎn)換異常)
- IllegalArgumentException (非法參數(shù)異常)
- IndexOutOfBoundsException (下標(biāo)越界異常)
- NullPointerException (空指針異常)
- SecurityException (安全異常)
50见间、闡述 final聊闯、finally、finalize 的區(qū)別米诉。
答:
- final:修飾符(關(guān)鍵字)有三種用法:如果一個類被聲明為 final菱蔬,意味 著它不能再派生出新的子類,即不能被繼承史侣,因此它和 abstract 是反義詞拴泌。將 變量聲明為 final,可以保證它們在使用中不被改變惊橱,被聲明為 final 的變量必須 在聲明時給定初值蚪腐,而在以后的引用中只能讀取不可修改。被聲明為 final 的方 法也同樣只能使用税朴,不能在子類中被重寫削茁。
- finally:通常放在 try…catch…的后面構(gòu)造總是執(zhí)行代碼塊,這就意味著 程序無論正常執(zhí)行還是發(fā)生異常掉房,這里的代碼只要 JVM 不關(guān)閉都能執(zhí)行,可以 將釋放外部資源的代碼寫在 finally 塊中慰丛。
- finalize:Object 類中定義的方法卓囚,Java 中允許使用 finalize()方法在垃 圾收集器將對象從內(nèi)存中清除出去之前做必要的清理工作。這個方法是由垃圾收 集器在銷毀對象時調(diào)用的诅病,通過重寫 finalize()方法可以整理系統(tǒng)資源或者執(zhí)行 其他清理工作哪亿。
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}
**請問執(zhí)行此段代碼的輸出是什么?
答: 輸出:ExampleA芥永。(根據(jù)里氏代換原則[能使用父類型的地方一定能使用子類型]篡殷, 抓取 ExampleA 類型異常的 catch 塊能夠抓住 try 塊中拋出的 ExampleB 類型的 異常)
面試題 - 說出下面代碼的運行結(jié)果。(此題的出處是《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 有明顯的區(qū)別, 而 Set 存儲的零散的元素且不允許有重復(fù)元素(數(shù)學(xué)中的集合也是如此),List 是線性結(jié)構(gòu)的容器画畅,適用于按數(shù)值索引訪問元素的情形砸琅。
53、闡述 ArrayList夜赵、Vector明棍、LinkedList 的存儲性能和特性。
答: ArrayList 和 Vector 都是使用數(shù)組方式存儲數(shù)據(jù)寇僧,此數(shù)組元素數(shù)大于實際存儲的 數(shù)據(jù)以便增加和插入元素摊腋,它們都允許直接按序號索引元素,但是插入元素要涉 及數(shù)組元素移動等內(nèi)存操作嘁傀,所以索引數(shù)據(jù)快而插入數(shù)據(jù)慢兴蒸,Vector 中的方法由 于添加了 synchronized 修飾,因此 Vector 是線程安全的容器细办,但性能上ArrayList 差橙凳,因此已經(jīng)是 Java 中的遺留容器。LinkedList 使用雙向鏈表實現(xiàn)存 儲(將內(nèi)存中零散的內(nèi)存單元通過附加的引用關(guān)聯(lián)起來笑撞,形成一個可以按序號索 引的線性結(jié)構(gòu)岛啸,這種鏈?zhǔn)酱鎯Ψ绞脚c數(shù)組的連續(xù)存儲方式相比,內(nèi)存的利用率更 高)茴肥,按序號索引數(shù)據(jù)需要進行前向或后向遍歷坚踩,但是插入數(shù)據(jù)時只需要記錄項的前后項即可,所以插入速度較快瓤狐。Vector 屬于遺留容器(Java 早期的版本中 提供的容器瞬铸,除此之外,Hashtable础锐、Dictionary嗓节、BitSet、Stack皆警、Properties 都是遺留容器)拦宣,已經(jīng)不推薦使用,但是由于 ArrayList 和 LinkedListed 都是非 線程安全的信姓,如果遇到多個線程操作同一個容器的場景恢着,則可以通過工具類 Collections 中的 synchronizedList 方法將其轉(zhuǎn)換成線程安全的容器后再使用(這 是對裝潢模式的應(yīng)用,將已有對象傳入另一個類的構(gòu)造器中創(chuàng)建新的對象來增強 實現(xiàn))财破。
補充:遺留容器中的 Properties 類和 Stack 類在設(shè)計上有嚴(yán)重的問題掰派,Properties 是一個鍵和值都是字符串的特殊的鍵值對映射,在設(shè)計上應(yīng)該是關(guān)聯(lián)一個 Hashtable 并將其兩個泛型參數(shù)設(shè)置為 String 類型左痢,但是 Java API 中的 Properties 直接繼承了 Hashtable靡羡,這很明顯是對繼承的濫用系洛。這里復(fù)用代碼的方式應(yīng)該是 Has-A 關(guān)系而不是 Is-A 關(guān)系,另一方面容器都屬于工具類略步,繼承工具 類本身就是一個錯誤的做法描扯,使用工具類最好的方式是 Has-A 關(guān)系(關(guān)聯(lián))或 Use-A 關(guān)系(依賴)。同理趟薄,Stack 類繼承 Vector 也是不正確的绽诚。Sun 公司的工 程師們也會犯這種低級錯誤,讓人唏噓不已杭煎。
54恩够、Collection 和 Collections 的區(qū)別?
答: Collection 是一個接口羡铲,它是 Set蜂桶、List 等容器的父接口;Collections 是個一個 工具類也切,提供了一系列的靜態(tài)方法來輔助容器操作扑媚,這些方法包括對容器的搜索、 排序雷恃、線程安全化等等疆股。
55、List倒槐、Map押桃、Set 三個接口存取元素時,各有什么特點导犹?
答: List 以特定索引來存取元素,可以有重復(fù)元素羡忘。Set 不能存放重復(fù)元素(用對象的 equals()方法來區(qū)分元素是否重復(fù))谎痢。Map 保存鍵值對(key-value pair)映射, 映射關(guān)系可以是一對一或多對一卷雕。Set 和 Map 容器都有基于哈希存儲和排序樹的 兩種實現(xiàn)版本节猿,基于哈希存儲的版本理論存取時間復(fù)雜度為 O(1),而基于排序樹 版本的實現(xiàn)在插入或刪除元素時會按照元素或元素的鍵(key)構(gòu)成排序樹從而達 到排序和去重的效果漫雕。
56滨嘱、TreeMap 和 TreeSet 在排序時如何比較元素? Collections 工具類中的 sort()方法如何比較元素浸间?
答: TreeSet 要求存放的對象所屬的類必須實現(xiàn) Comparable 接口太雨,該接口提供了比 較元素的 compareTo()方法,當(dāng)插入元素時會回調(diào)該方法比較元素的大小魁蒜。 TreeMap 要求存放的鍵值對映射的鍵必須實現(xiàn) Comparable 接口從而根據(jù)鍵對元 素進行排序囊扳。Collections 工具類的 sort 方法有兩種重載的形式吩翻,第一種要求傳入 的待排序容器中存放的對象比較實現(xiàn) Comparable 接口以實現(xiàn)元素的比較;第二 種不強制性的要求容器中的元素必須可比較锥咸,但是要求傳入第二個參數(shù)狭瞎,參數(shù)是 Comparator 接口的子類型(需要重寫 compare 方法實現(xiàn)元素的比較),相當(dāng)于 一個臨時定義的排序規(guī)則搏予,其實就是通過接口注入比較元素大小的算法熊锭,也是對 回調(diào)模式的應(yīng)用(Java 中對函數(shù)式編程的支持)。
例子 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(構(gòu)造器后面的尖括號中不需要寫類型)
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// 輸出結(jié)果:
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* 獲取學(xué)生姓名
10*/
11public String getName() {
12return name;
13}
14/**
15* 獲取學(xué)生年齡
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(構(gòu)造器后面的尖括號中不需要寫類型)
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 方法的第二個參數(shù)傳入一個 Comparator 接口對象
14// 相當(dāng)于是傳入一個比較對象大小的算法到 sort 方法中
15// 由于 Java 中沒有函數(shù)指針雪侥、仿函數(shù)碗殷、委托這樣的概念
16// 因此要將一個算法傳入一個方法中唯一的選擇就是通過接口回調(diào)
17Collections.sort(list, new Comparator<Student> () {
18@Override
19public int compare(Student o1, Student o2) {
20return o1.getName().compareTo(o2.getName()); //
21比較學(xué)生姓名
22}
23});
24for(Student stu : list) {
25System.out.println(stu);
26}
27// 輸出結(jié)果:
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()方法都可以讓線 程暫停執(zhí)行校镐,它們有什么區(qū)別?
答: sleep()方法(休眠)是線程類(Thread)的靜態(tài)方法亿扁,調(diào)用此方法會讓當(dāng)前線程 暫停執(zhí)行指定的時間,將執(zhí)行機會(CPU)讓給其他線程鸟廓,但是對象的鎖依然保 持从祝,因此休眠時間結(jié)束后會自動恢復(fù)(線程回到就緒狀態(tài),請參考第 66 題中的線 程狀態(tài)轉(zhuǎn)換圖)引谜。wait()是 Object 類的方法牍陌,調(diào)用對象的 wait()方法導(dǎo)致當(dāng)前線 程放棄對象的鎖(線程暫停執(zhí)行),進入對象的等待池(wait pool)员咽,只有調(diào)用 對象的 notify()方法(或 notifyAll()方法)時才能喚醒等待池中的線程進入等鎖池 (lock pool)毒涧,如果線程重新獲得對象的鎖就可以進入就緒狀態(tài)。
補充:可能不少人對什么是進程贝室,什么是線程還比較模糊契讲,對于為什么需要多線 程編程也不是特別理解。簡單的說:進程是具有一定獨立功能的程序關(guān)于某個數(shù) 據(jù)集合上的一次運行活動滑频,是操作系統(tǒng)進行資源分配和調(diào)度的一個獨立單位捡偏;線 程是進程的一個實體,是 CPU 調(diào)度和分派的基本單位峡迷,是比進程更小的能獨立運 行的基本單位志秃。線程的劃分尺度小于進程蓝纲,這使得多線程程序的并發(fā)性高暗赶;進程 在執(zhí)行時通常擁有獨立的內(nèi)存單元扰魂,而線程之間可以共享內(nèi)存。使用多線程的編 程通常能夠帶來更好的性能和用戶體驗夯辖,但是多線程的程序?qū)τ谄渌绦蚴遣挥?好的琉预,因為它可能占用了更多的 CPU 資源。當(dāng)然蒿褂,也不是線程越多模孩,程序的性能 就越好尖阔,因為線程之間的調(diào)度和切換也會浪費 CPU 時間。時下很時髦的 Node.js 就采用了單線程異步 I/O 的工作模式榨咐。
58介却、線程的 sleep()方法和 yield()方法有什么區(qū)別?
答:
① sleep()方法給其他線程運行機會時不考慮線程的優(yōu)先級块茁,因此會給低優(yōu)先級的 線程以運行的機會齿坷;yield()方法只會給相同優(yōu)先級或更高優(yōu)先級的線程以運行的 機會;
② 線程執(zhí)行 sleep()方法后轉(zhuǎn)入阻塞(blocked)狀態(tài)数焊,而執(zhí)行 yield()方法后轉(zhuǎn) 入就緒 (ready)狀態(tài)永淌;
③ sleep()方法聲明拋出 InterruptedException,而 yield()方法沒有聲明任何異 常佩耳;
④ sleep()方法比 yield()方法(跟操作系統(tǒng) CPU 調(diào)度相關(guān))具有更好的可移植性遂蛀。
59、當(dāng)一個線程進入一個對象的 synchronized 方法 A 之后干厚, 其它線程是否可進入此對象的 synchronized 方法 B李滴?
答: 不能。其它線程只能訪問該對象的非同步方法蛮瞄,同步方法則不能進入所坯。因為非靜 態(tài)方法上的 synchronized 修飾符要求執(zhí)行方法時要獲得對象的鎖,如果已經(jīng)進入 A 方法說明對象鎖已經(jīng)被取走挂捅,那么試圖進入 B 方法的線程就只能在等鎖池(注 意不是等待池哦)中等待對象的鎖芹助。
60、請說出與線程同步以及線程調(diào)度相關(guān)的方法闲先。
答:
- wait():使一個線程處于等待(阻塞)狀態(tài)状土,并且釋放所持有的對象的鎖;
- sleep():使一個正在運行的線程處于睡眠狀態(tài)伺糠,是一個靜態(tài)方法蒙谓,調(diào)用 此方法要處理 InterruptedException 異常;
- notify():喚醒一個處于等待狀態(tài)的線程退盯,當(dāng)然在調(diào)用此方法的時候,并 不能確切的喚醒某一個等待狀態(tài)的線程泻肯,而是由 JVM 確定喚醒哪個線程渊迁,而且 與優(yōu)先級無關(guān);
- notityAll():喚醒所有處于等待狀態(tài)的線程灶挟,該方法并不是將對象的鎖給 所有線程琉朽,而是讓它們競爭,只有獲得鎖的線程才能進入就緒狀態(tài)稚铣; \
提示:關(guān)于 Java 多線程和并發(fā)編程的問題箱叁,建議大家看我的另一篇文章《關(guān)于 Java 并發(fā)編程的總結(jié)和思考》墅垮。 補充:Java 5 通過 Lock 接口提供了顯式的鎖機制(explicit lock),增強了靈活 性以及對線程的協(xié)調(diào)耕漱。Lock 接口中定義了加鎖(lock())和解鎖(unlock())的方 法算色,同時還提供了 newCondition()方法來產(chǎn)生用于線程之間通信的 Condition 對 象;此外螟够,Java 5 還提供了信號量機制(semaphore)灾梦,信號量可以用來限制對 某個共享資源進行訪問的線程的數(shù)量。在對資源進行訪問之前妓笙,線程必須得到信 號量的許可(調(diào)用 Semaphore 對象的 acquire()方法)若河;在完成對資源的訪問后, 線程必須向信號量歸還許可(調(diào)用 Semaphore 對象的 release()方法)寞宫。
下面的例子演示了 100 個線程同時向一個銀行賬戶中存入 1 元錢萧福,在沒有使用同 步機制和使用同步機制情況下的執(zhí)行情況。
- 銀行賬戶類:
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); // 模擬此業(yè)務(wù)需要一段處理時間
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}
在沒有同步的情況下辈赋,執(zhí)行結(jié)果通常是顯示賬戶余額在 10 元以下鲫忍,出現(xiàn)這種狀況 的原因是,當(dāng)一個線程 A 試圖存入 1 元的時候炭庙,另外一個線程 B 也能夠進入存款 的方法中饲窿,線程 B 讀取到的賬戶余額仍然是線程 A 存入 1 元錢之前的賬戶余額, 因此也是在原來的余額 0 上面做了加 1 元的操作焕蹄,同理線程 C 也會做類似的事情逾雄, 所以最后 100 個線程執(zhí)行結(jié)束時,本來期望賬戶余額為 100 元腻脏,但實際得到的通 常在 10 元以下(很可能是 1 元哦)鸦泳。解決這個問題的辦法就是同步,當(dāng)一個線程 對銀行賬戶存錢時永品,需要將此賬戶鎖定做鹰,待其操作完成后才允許其他的線程進行 操作,代碼有如下幾種調(diào)整方案:
- 在銀行賬戶的存款(deposit)方法上同步(synchronized)關(guān)鍵字
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); // 模擬此業(yè)務(wù)需要一段處理時間
16}
17catch(InterruptedException ex) {
18ex.printStackTrace();
19}
20balance = newBalance;
21}
22/**
23* 獲得賬戶余額
24*/
25public double getBalance() {
26return balance;
27}
28}
- 在線程調(diào)用存款方法時對銀行賬戶進行同步
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 顯示的鎖機制鼎姐,為每個銀行賬戶創(chuàng)建一個鎖對象钾麸,在存款操 作進行加鎖和解鎖的操作
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); // 模擬此業(yè)務(wù)需要一段處理時間
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}
按照上述三種方式對代碼進行修改后,重寫執(zhí)行測試代碼 Test01炕桨,將看到最終的 賬戶余額為 100 元饭尝。當(dāng)然也可以使用 Semaphore 或 CountdownLatch 來實現(xiàn)同步。
61献宫、編寫多線程程序有幾種實現(xiàn)方式钥平?
答: Java 5 以前實現(xiàn)多線程有兩種實現(xiàn)方法:一種是繼承 Thread 類;另一種是實現(xiàn) Runnable 接口姊途。兩種方式都要通過重寫 run()方法來定義線程的行為涉瘾,推薦使用 后者知态,因為 Java 中的繼承是單繼承,一個類有一個父類立叛,如果繼承了 Thread 類 就無法再繼承其他類了,顯然使用 Runnable 接口更為靈活囚巴。
補充:Java 5 以后創(chuàng)建線程還有第三種方式:實現(xiàn) Callable 接口庶柿,該接口中的 call 方法可以在線程執(zhí)行結(jié)束時產(chǎn)生一個返回值,代碼如下所示:
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}
62秽浇、synchronized 關(guān)鍵字的用法浮庐?
答: synchronized 關(guān)鍵字可以將對象或者方法標(biāo)記為同步,以實現(xiàn)對對象和方法的互 斥訪問柬焕,可以用 synchronized(對象) { … }定義同步代碼塊审残,或者在聲明方法時 將 synchronized 作為方法的修飾符。在第 60 題的例子中已經(jīng)展示了 synchronized 關(guān)鍵字的用法斑举。
63搅轿、舉例說明同步和異步。
答: 如果系統(tǒng)中存在臨界資源(資源數(shù)量少于競爭資源的線程數(shù)量的資源)富玷,例如正 在寫的數(shù)據(jù)以后可能被另一個線程讀到璧坟,或者正在讀的數(shù)據(jù)可能已經(jīng)被另一個線 程寫過了,那么這些數(shù)據(jù)就必須進行同步存仁昱场(數(shù)據(jù)庫操作中的排他鎖就是最好 的例子)雀鹃。當(dāng)應(yīng)用程序在對象上調(diào)用了一個需要花費很長時間來執(zhí)行的方法,并 且不希望讓程序等待方法的返回時励两,就應(yīng)該使用異步編程黎茎,在很多情況下采用異 步途徑往往更有效率。事實上当悔,所謂的同步就是指阻塞式操作傅瞻,而異步就是非阻 塞式操作。
64盲憎、啟動一個線程是調(diào)用 run()還是 start()方法嗅骄?
答: 啟動一個線程是調(diào)用 start()方法,使線程所代表的虛擬處理機處于可運行狀態(tài)焙畔, 這意味著它可以由 JVM 調(diào)度并執(zhí)行掸读,這并不意味著線程就會立即運行串远。run()方 法是線程啟動后要進行回調(diào)(callback)的方法宏多。
65儿惫、什么是線程池(thread pool)?
答: 在面向?qū)ο缶幊讨猩斓瑒?chuàng)建和銷毀對象是很費時間的肾请,因為創(chuàng)建一個對象要獲取內(nèi) 存資源或者其它更多資源。在 Java 中更是如此更胖,虛擬機將試圖跟蹤每一個對象铛铁, 以便能夠在對象銷毀后進行垃圾回收。所以提高服務(wù)程序效率的一個手段就是盡 可能減少創(chuàng)建和銷毀對象的次數(shù)却妨,特別是一些很耗資源的對象創(chuàng)建和銷毀饵逐,這就 是”池化資源”技術(shù)產(chǎn)生的原因。線程池顧名思義就是事先創(chuàng)建若干個可執(zhí)行的 線程放入一個池(容器)中彪标,需要的時候從池中獲取線程不用自行創(chuàng)建倍权,使用完 畢不需要銷毀線程而是放回池中,從而減少創(chuàng)建和銷毀線程對象的開銷捞烟。 Java 5+中的 Executor 接口定義一個執(zhí)行線程的工具薄声。它的子類型即線程池接口 是 ExecutorService。要配置一個線程池是比較復(fù)雜的题画,尤其是對于線程池的原理 不是很清楚的情況下默辨,因此在工具類 Executors 面提供了一些靜態(tài)工廠方法,生 成一些常用的線程池苍息,如下所示:
newSingleThreadExecutor:創(chuàng)建一個單線程的線程池缩幸。這個線程池只 有一個線程在工作,也就是相當(dāng)于單線程串行執(zhí)行所有任務(wù)档叔。如果這個唯一的程因為異常結(jié)束桌粉,那么會有一個新的線程來替代它。此線程池保證所有任務(wù)的執(zhí) 行順序按照任務(wù)的提交順序執(zhí)行衙四。
newFixedThreadPool:創(chuàng)建固定大小的線程池铃肯。每次提交一個任務(wù)就創(chuàng) 建一個線程,直到線程達到線程池的最大大小传蹈。線程池的大小一旦達到最大值就 會保持不變押逼,如果某個線程因為執(zhí)行異常而結(jié)束,那么線程池會補充一個新線程惦界。
newCachedThreadPool:創(chuàng)建一個可緩存的線程池挑格。如果線程池的大小 超過了處理任務(wù)所需要的線程,那么就會回收部分空閑(60 秒不執(zhí)行任務(wù))的 線程沾歪,當(dāng)任務(wù)數(shù)增加時漂彤,此線程池又可以智能的添加新線程來處理任務(wù)。此線程 池不會對線程池大小做限制,線程池大小完全依賴于操作系統(tǒng)(或者說 JVM) 能夠創(chuàng)建的最大線程大小挫望。
newScheduledThreadPool:創(chuàng)建一個大小無限的線程池立润。此線程池支 持定時以及周期性執(zhí)行任務(wù)的需求。
newSingleThreadExecutor:創(chuàng)建一個單線程的線程池媳板。此線程池支持 定時以及周期性執(zhí)行任務(wù)的需求桑腮。
第 60 題的例子中演示了通過 Executors 工具類創(chuàng)建線程池并使用線程池執(zhí)行線程 的代碼。如果希望在服務(wù)器上使用線程池蛉幸,強烈建議使用 newFixedThreadPool 方法來創(chuàng)建線程池破讨,這樣能獲得更好的性能。
66奕纫、線程的基本狀態(tài)以及狀態(tài)之間的關(guān)系提陶?
答:
說明:其中 Running 表示運行狀態(tài),Runnable 表示就緒狀態(tài)(萬事俱備匹层,只欠 CPU)搁骑,Blocked 表示阻塞狀態(tài),阻塞狀態(tài)又有多種情況又固,可能是因為調(diào)用 wait() 方法進入等待池仲器,也可能是執(zhí)行同步方法或同步代碼塊進入等鎖池,或者是調(diào)用 了 sleep()方法或 join()方法等待休眠或其他線程結(jié)束仰冠,或是因為發(fā)生了 I/O 中斷
67乏冀、簡述 synchronized 和 java.util.concurrent.locks.Lock 的異同?
答: Lock 是 Java 5 以后引入的新的 API洋只,和關(guān)鍵字 synchronized 相比主要相同點: Lock 能完成 synchronized 所實現(xiàn)的所有功能辆沦;主要不同點:Lock 有比 synchronized 更精確的線程語義和更好的性能,而且不強制性的要求一定要獲得 鎖识虚。synchronized 會自動釋放鎖肢扯,而 Lock 一定要求程序員手工釋放,并且最在 finally 塊中釋放(這是釋放外部資源的最好的地方)担锤。
68蔚晨、Java 中如何實現(xiàn)序列化,有什么意義肛循?
答: 序列化就是一種用來處理對象流的機制铭腕,所謂對象流也就是將對象的內(nèi)容進行流 化《嗫罚可以對流化后的對象進行讀寫操作累舷,也可將流化后的對象傳輸于網(wǎng)絡(luò)之間。 序列化是為了解決對象流讀寫操作時可能引發(fā)的問題(如果不進行序列化可能會 存在數(shù)據(jù)亂序的問題)夹孔。 要實現(xiàn)序列化被盈,需要讓一個類實現(xiàn) Serializable 接口析孽,該接口是一個標(biāo)識性接口, 標(biāo)注該類對象是可被序列化的只怎,然后使用一個輸出流來構(gòu)造一個對象輸出流并通 過 writeObject(Object)方法就可以將實現(xiàn)對象寫出(即保存其狀態(tài))绿淋;如果需要 反序列化則可以用一個輸入流建立對象輸入流,然后通過 readObject 方法從流中 讀取對象尝盼。序列化除了能夠?qū)崿F(xiàn)對象的持久化之外,還能夠用于對象的深度克隆 (可以參考第 29 題)佑菩。
69盾沫、Java 中有幾種類型的流?
答: 字節(jié)流和字符流殿漠。字節(jié)流繼承于 InputStream赴精、OutputStream,字符流繼承Reader绞幌、Writer蕾哟。在 http://java.io 包中還有許多其他的流,主要是為了提高性能和使 用方便莲蜘。關(guān)于 Java 的 I/O 需要注意的有兩點:一是兩種對稱性(輸入和輸出的對 稱性谭确,字節(jié)和字符的對稱性);二是兩種設(shè)計模式(適配器模式和裝潢模式)票渠。 另外 Java 中的流不同于 C#的是它只有一個維度一個方向逐哈。
面試題 - 編程實現(xiàn)文件拷貝。(這個題目在筆試的時候經(jīng)常出現(xiàn)问顷,下面的代碼給 出了兩種實現(xiàn)方案)
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 中釋放外部資源 , 從而讓代碼更加優(yōu)雅杜窄。
70肠骆、寫一個方法,輸入一個文件名和一個字符串塞耕,統(tǒng)計這個字 符串在這個文件中出現(xiàn)的次數(shù)蚀腿。
答: 代碼如下:
1import java.io.BufferedReader;
2import java.io.FileReader;
3public final class MyUtil {
4// 工具類中的方法都是靜態(tài)方式訪問的因此將構(gòu)造器私有不允許創(chuàng)建對象
5(絕對好習(xí)慣)
6private MyUtil() {
7throw new AssertionError();
8}
9/**
10* 統(tǒng)計給定文件中給定字符串的出現(xiàn)次數(shù)
11*
12* @param filename 文件名
13* @param word 字符串
14* @return 字符串在文件中出現(xiàn)的次數(shù)
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 代碼列出一個目錄下所有的文件扫外?
答:
如果只要求列出當(dāng)前文件夾下的文件唯咬,代碼如下所示:
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}
如果需要對文件夾繼續(xù)展開,代碼如下所示:
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}
72胆胰、用 Java 的套接字編程實現(xiàn)一個多線程的回顯(echo)服 務(wù)器。
答:
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("服務(wù)器已經(jīng)啟動...");
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發(fā)送的: " + 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 語法刻获,由于很多外部資源類都間接的實現(xiàn)了 AutoCloseable 接口(單方法回調(diào)接口)蜀涨,因此可以利用 TWR 語法在 try結(jié)束的時候通過回調(diào)的方式自動調(diào)用外部資源類的 close()方法瞎嬉,避免書寫冗長的finally 代碼塊。此外厚柳,上面的代碼用一個靜態(tài)內(nèi)部類實現(xiàn)線程的功能氧枣,使用多線程可以避免一個用戶 I/O 操作所產(chǎn)生的中斷影響其他用戶對服務(wù)器的訪問,簡單的說就是一個用戶的輸入操作不會造成其他用戶的阻塞别垮。當(dāng)然便监,上面的代碼使用線程池可以獲得更好的性能,因為頻繁的創(chuàng)建和銷毀線程所造成的開銷也是不可忽視的碳想。
下面是一段回顯客戶端測試代碼:
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("請輸入內(nèi)容: ");
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 的多路復(fù)用套接字實現(xiàn)服務(wù)器烧董,代碼如下所示。NIO 的操作雖然 帶來了更好的性能胧奔,但是有些操作是比較底層的逊移,對于初學(xué)者來說還是有些難于理解。
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; // 多路復(fù)用選擇器
16private static ByteBuffer buffer = null; // 緩沖區(qū)
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}
73龙填、XML 文檔定義有幾種形式胳泉?它們之間有何本質(zhì)區(qū)別?解析 XML 文檔有哪幾種方式岩遗?
答: XML 文檔定義分為 DTD 和 Schema 兩種形式扇商,二者都是對 XML 語法的約束,其 本質(zhì)區(qū)別在于 Schema 本身也是一個 XML 文件宿礁,可以被 XML 解析器解析钳吟,而且 可以為 XML 承載的數(shù)據(jù)定義類型,約束能力較之 DTD 更強大窘拯。對 XML 的解析主 要有 DOM(文檔對象模型红且,Document Object Model)、SAX(Simple API foXML)和 StAX(Java 6 中引入的新的解析 XML 的方式涤姊,Streaming API for XML)暇番,其中 DOM 處理大型文件時其性能下降的非常厲害,這個問題是由 DOM 樹結(jié)構(gòu)用的內(nèi)存較多造成的思喊,而且 DOM 解析方式必須在解析文件之前把整個文檔裝入內(nèi) 存壁酬,適合對 XML 的隨機訪問(典型的用空間換取時間的策略);SAX 是事件驅(qū)動 型的 XML 解析方式恨课,它順序讀取 XML 文件舆乔,不需要一次全部裝載整個文件。當(dāng) 遇到像文件開頭剂公,文檔結(jié)束希俩,或者標(biāo)簽開頭與標(biāo)簽結(jié)束時,它會觸發(fā)一個事件纲辽, 用戶通過事件回調(diào)代碼來處理 XML 文件颜武,適合對 XML 的順序訪問璃搜;顧名思義, StAX 把重點放在流上鳞上,實際上 StAX 與其他解析方式的本質(zhì)區(qū)別就在于應(yīng)用程能夠把 XML 作為一個事件流來處理这吻。將 XML 作為一組事件來處理的想法并不新 穎(SAX 就是這樣做的),但不同之處在于 StAX 允許應(yīng)用程序代碼把這些事件逐 個拉出來篙议,而不用提供在解析器方便時從解析器中接收事件的處理程序唾糯。
74、你在項目中哪些地方用到了 XML鬼贱?
答: XML 的主要作用有兩個方面:數(shù)據(jù)交換和信息配置移怯。在做數(shù)據(jù)交換時,XML 將數(shù) 據(jù)用標(biāo)簽組裝成起來吩愧,然后壓縮打包加密后通過網(wǎng)絡(luò)傳送給接收者,接收解密與 解壓縮后再從 XML 文件中還原相關(guān)信息進行處理增显,XML 曾經(jīng)是異構(gòu)系統(tǒng)間交換數(shù) 據(jù)的事實標(biāo)準(zhǔn)雁佳,但此項功能幾乎已經(jīng)被 JSON(JavaScript Object Notation)取 而代之。當(dāng)然同云,目前很多軟件仍然使用 XML 來存儲配置信息糖权,我們在很多項目中 通常也會將作為配置信息的硬代碼寫在 XML 文件中,Java 的很多框架也是這么做 的炸站,而且這些框架都選擇了 dom4j 作為處理 XML 的工具星澳,因為 Sun 公司的官方 API 實在不怎么好用。
補充:現(xiàn)在有很多時髦的軟件(如 Sublime)已經(jīng)開始將配置文件書寫成 JSON 格式旱易,我們已經(jīng)強烈的感受到 XML 的另一項功能也將逐漸被業(yè)界拋棄禁偎。