4一、日期和時間:
- 如何取得年月日、小時分鐘秒?
- 如何取得從1970年1月1日0時0分0秒到如今的毫秒數?
- 如何取得某月的最後一天?
- 如何格式化日期?
答:
問題1:建立java.util.Calendar 實例,調用其get()方法傳入不一樣的參數便可得到參數所對應的值。Java 8中能夠使用java.time.LocalDateTimel來獲取,代碼以下所示。html
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
|
public
class
DateTimeTest {
public
static
void
main(String[] args) {
Calendar cal = Calendar.getInstance();
System.out.println(cal.get(Calendar.YEAR));
System.out.println(cal.get(Calendar.MONTH));
// 0 - 11
System.out.println(cal.get(Calendar.DATE));
System.out.println(cal.get(Calendar.HOUR_OF_DAY));
System.out.println(cal.get(Calendar.MINUTE));
System.out.println(cal.get(Calendar.SECOND));
// Java 8
LocalDateTime dt = LocalDateTime.now();
System.out.println(dt.getYear());
System.out.println(dt.getMonthValue());
// 1 - 12
System.out.println(dt.getDayOfMonth());
System.out.println(dt.getHour());
System.out.println(dt.getMinute());
System.out.println(dt.getSecond());
}
}
|
問題2:如下方法都可得到該毫秒數。前端
1
2
3
|
Calendar.getInstance().getTimeInMillis();
System.currentTimeMillis();
Clock.systemDefaultZone().millis();
// Java 8
|
問題3:代碼以下所示。java
1
2
|
Calendar time = Calendar.getInstance();
time.getActualMaximum(Calendar.DAY_OF_MONTH);
|
問題4:利用java.text.DataFormat 的子類(如SimpleDateFormat類)中的format(Date)方法可將日期格式化。Java 8中能夠用java.time.format.DateTimeFormatter來格式化時間日期,代碼以下所示。mysql
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
|
import
java.text.SimpleDateFormat;
import
java.time.LocalDate;
import
java.time.format.DateTimeFormatter;
import
java.util.Date;
class
DateFormatTest {
public
static
void
main(String[] args) {
SimpleDateFormat oldFormatter =
new
SimpleDateFormat(
"yyyy/MM/dd"
);
Date date1 =
new
Date();
System.out.println(oldFormatter.format(date1));
// Java 8
DateTimeFormatter newFormatter = DateTimeFormatter.ofPattern(
"yyyy/MM/dd"
);
LocalDate date2 = LocalDate.now();
System.out.println(date2.format(newFormatter));
}
}
|
補充:Java的時間日期API一直以來都是被詬病的東西,爲了解決這一問題,Java 8中引入了新的時間日期API,其中包括LocalDate、LocalTime、LocalDateTime、Clock、Instant等類,這些的類的設計都使用了不變模式,所以是線程安全的設計。若是不理解這些內容,能夠參考個人另外一篇文章《關於Java併發編程的總結和思考》。程序員
4二、打印昨天的當前時刻。
答:面試
1
2
3
4
5
6
7
8
9
|
import
java.util.Calendar;
class
YesterdayCurrent {
public
static
void
main(String[] args){
Calendar cal = Calendar.getInstance();
cal.add(Calendar.DATE, -
1
);
System.out.println(cal.getTime());
}
}
|
在Java 8中,能夠用下面的代碼實現相同的功能。正則表達式
1
2
3
4
5
6
7
8
9
10
11
|
import
java.time.LocalDateTime;
class
YesterdayCurrent {
public
static
void
main(String[] args) {
LocalDateTime today = LocalDateTime.now();
LocalDateTime yesterday = today.minusDays(
1
);
System.out.println(yesterday);
}
}
|
4三、比較一下Java和JavaSciprt。
答:JavaScript 與Java是兩個公司開發的不一樣的兩個產品。Java 是原Sun Microsystems公司推出的面向對象的程序設計語言,特別適合於互聯網應用程序開發;而JavaScript是Netscape公司的產品,爲了擴展Netscape瀏覽器的功能而開發的一種能夠嵌入Web頁面中運行的基於對象和事件驅動的解釋性語言。JavaScript的前身是LiveScript;而Java的前身是Oak語言。
下面對兩種語言間的異同做以下比較:
- 基於對象和麪向對象:Java是一種真正的面向對象的語言,即便是開發簡單的程序,必須設計對象;JavaScript是種腳本語言,它能夠用來製做與網絡無關的,與用戶交互做用的複雜軟件。它是一種基於對象(Object-Based)和事件驅動(Event-Driven)的編程語言,於是它自己提供了很是豐富的內部對象供設計人員使用。
- 解釋和編譯:Java的源代碼在執行以前,必須通過編譯。JavaScript是一種解釋性編程語言,其源代碼不需通過編譯,由瀏覽器解釋執行。(目前的瀏覽器幾乎都使用了JIT(即時編譯)技術來提高JavaScript的運行效率)
- 強類型變量和類型弱變量:Java採用強類型變量檢查,即全部變量在編譯以前必須做聲明;JavaScript中變量是弱類型的,甚至在使用變量前能夠不做聲明,JavaScript的解釋器在運行時檢查推斷其數據類型。
- 代碼格式不同。算法
補充:上面列出的四點是網上流傳的所謂的標準答案。其實Java和JavaScript最重要的區別是一個是靜態語言,一個是動態語言。目前的編程語言的發展趨勢是函數式語言和動態語言。在Java中類(class)是一等公民,而JavaScript中函數(function)是一等公民,所以JavaScript支持函數式編程,能夠使用Lambda函數和閉包(closure),固然Java 8也開始支持函數式編程,提供了對Lambda表達式以及函數式接口的支持。對於這類問題,在面試的時候最好仍是用本身的語言回答會更加靠譜,不要背網上所謂的標準答案。sql
4四、何時用斷言(assert)?
答:斷言在軟件開發中是一種經常使用的調試方式,不少開發語言中都支持這種機制。通常來講,斷言用於保證程序最基本、關鍵的正確性。斷言檢查一般在開發和測試時開啓。爲了保證程序的執行效率,在軟件發佈後斷言檢查一般是關閉的。斷言是一個包含布爾表達式的語句,在執行這個語句時假定該表達式爲true;若是表達式的值爲false,那麼系統會報告一個AssertionError。斷言的使用以下面的代碼所示:數據庫
1
|
assert
(a >
0
);
// throws an AssertionError if a <= 0
|
斷言能夠有兩種形式:
assert Expression1;
assert Expression1 : Expression2 ;
Expression1 應該老是產生一個布爾值。
Expression2 能夠是得出一個值的任意表達式;這個值用於生成顯示更多調試信息的字符串消息。
要在運行時啓用斷言,能夠在啓動JVM時使用-enableassertions或者-ea標記。要在運行時選擇禁用斷言,能夠在啓動JVM時使用-da或者-disableassertions標記。要在系統類中啓用或禁用斷言,可以使用-esa或-dsa標記。還能夠在包的基礎上啓用或者禁用斷言。
注意:斷言不該該以任何方式改變程序的狀態。簡單的說,若是但願在不知足某些條件時阻止代碼的執行,就能夠考慮用斷言來阻止它。
4五、Error和Exception有什麼區別?
答:Error表示系統級的錯誤和程序沒必要處理的異常,是恢復不是不可能但很困難的狀況下的一種嚴重問題;好比內存溢出,不可能期望程序能處理這樣的狀況;Exception表示須要捕捉或者須要程序進行處理的異常,是一種設計或實現問題;也就是說,它表示若是程序運行正常,從不會發生的狀況。
面試題:2005年摩托羅拉的面試中曾經問過這麼一個問題「If a process reports a stack overflow run-time error, what’s the most possible cause?」,給了四個選項a. lack of memory; b. write on an invalid memory space; c. recursive function calling; d. array index out of boundary. Java程序在運行時也可能會遭遇StackOverflowError,這是一個沒法恢復的錯誤,只能從新修改代碼了,這個面試題的答案是c。若是寫了不能迅速收斂的遞歸,則頗有可能引起棧溢出的錯誤,以下所示:
1
2
3
4
5
6
|
class
StackOverflowErrorTest {
public
static
void
main(String[] args) {
main(
null
);
}
}
|
提示:用遞歸編寫程序時必定要牢記兩點:1. 遞歸公式;2. 收斂條件(何時就再也不繼續遞歸)。
4六、try{}裏有一個return語句,那麼緊跟在這個try後的finally{}裏的代碼會不會被執行,何時被執行,在return前仍是後?
答:會執行,在方法返回調用者前執行。
注意:在finally中改變返回值的作法是很差的,由於若是存在finally代碼塊,try中的return語句不會立馬返回調用者,而是記錄下返回值待finally代碼塊執行完畢以後再向調用者返回其值,而後若是在finally中修改了返回值,就會返回修改後的值。顯然,在finally中返回或者修改返回值會對程序形成很大的困擾,C#中直接用編譯錯誤的方式來阻止程序員幹這種齷齪的事情,Java中也能夠經過提高編譯器的語法檢查級別來產生警告或錯誤,Eclipse中能夠在如圖所示的地方進行設置,強烈建議將此項設置爲編譯錯誤。
4七、Java語言如何進行異常處理,關鍵字:throws、throw、try、catch、finally分別如何使用?
答:Java經過面向對象的方法進行異常處理,把各類不一樣的異常進行分類,並提供了良好的接口。在Java中,每一個異常都是一個對象,它是Throwable類或其子類的實例。當一個方法出現異常後便拋出一個異常對象,該對象中包含有異常信息,調用這個對象的方法能夠捕獲到這個異常並能夠對其進行處理。Java的異常處理是經過5個關鍵詞來實現的:try、catch、throw、throws和finally。通常狀況下是用try來執行一段程序,若是系統會拋出(throw)一個異常對象,能夠經過它的類型來捕獲(catch)它,或經過老是執行代碼塊(finally)來處理;try用來指定一塊預防全部異常的程序;catch子句緊跟在try塊後面,用來指定你想要捕獲的異常的類型;throw語句用來明確地拋出一個異常;throws用來聲明一個方法可能拋出的各類異常(固然聲明異常時容許無病呻吟);finally爲確保一段代碼無論發生什麼異常情況都要被執行;try語句能夠嵌套,每當遇到一個try語句,異常的結構就會被放入異常棧中,直到全部的try語句都完成。若是下一級的try語句沒有對某種異常進行處理,異常棧就會執行出棧操做,直到遇到有處理這種異常的try語句或者最終將異常拋給JVM。
4八、運行時異常與受檢異常有何異同?
答:異常表示程序運行過程當中可能出現的非正常狀態,運行時異常表示虛擬機的一般操做中可能遇到的異常,是一種常見運行錯誤,只要程序設計得沒有問題一般就不會發生。受檢異常跟程序運行的上下文環境有關,即便程序設計無誤,仍然可能因使用的問題而引起。Java編譯器要求方法必須聲明拋出可能發生的受檢異常,可是並不要求必須聲明拋出未被捕獲的運行時異常。異常和繼承同樣,是面向對象程序設計中常常被濫用的東西,在Effective 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。
有以下代碼片段:
1
2
3
4
5
6
7
|
try
{
throw
new
ExampleB(
"b"
)
}
catch
(ExampleA e){
System.out.println(
"ExampleA"
);
}
catch
(Exception e){
System.out.println(
"Exception"
);
}
|
請問執行此段代碼的輸出是什麼?
答:輸出:ExampleA。(根據里氏代換原則[能使用父類型的地方必定能使用子類型],抓取ExampleA類型異常的catch塊可以抓住try塊中拋出的ExampleB類型的異常)
面試題 - 說出下面代碼的運行結果。(此題的出處是《Java編程思想》一書)
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
|
class
Annoyance
extends
Exception {}
class
Sneeze
extends
Annoyance {}
class
Human {
public
static
void
main(String[] args)
throws
Exception {
try
{
try
{
throw
new
Sneeze();
}
catch
( Annoyance a ) {
System.out.println(
"Caught Annoyance"
);
throw
a;
}
}
catch
( Sneeze s ) {
System.out.println(
"Caught Sneeze"
);
return
;
}
finally
{
System.out.println(
"Hello World!"
);
}
}
}
|
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:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
|
public
class
Student
implements
Comparable<Student> {
private
String name;
// 姓名
private
int
age;
// 年齡
public
Student(String name,
int
age) {
this
.name = name;
this
.age = age;
}
@Override
public
String toString() {
return
"Student [name="
+ name +
", age="
+ age +
"]"
;
}
@Override
public
int
compareTo(Student o) {
return
this
.age - o.age;
// 比較年齡(年齡的升序)
}
}
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
|
import
java.util.Set;
import
java.util.TreeSet;
class
Test01 {
public
static
void
main(String[] args) {
Set<Student> set =
new
TreeSet<>();
// Java 7的鑽石語法(構造器後面的尖括號中不須要寫類型)
set.add(
new
Student(
"Hao LUO"
,
33
));
set.add(
new
Student(
"XJ WANG"
,
32
));
set.add(
new
Student(
"Bruce LEE"
,
60
));
set.add(
new
Student(
"Bob YANG"
,
22
));
for
(Student stu : set) {
System.out.println(stu);
}
// 輸出結果:
// Student [name=Bob YANG, age=22]
// Student [name=XJ WANG, age=32]
// Student [name=Hao LUO, age=33]
// Student [name=Bruce LEE, age=60]
}
}
|
例子2:
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
|
public
class
Student {
private
String name;
// 姓名
private
int
age;
// 年齡
public
Student(String name,
int
age) {
this
.name = name;
this
.age = age;
}
/**
* 獲取學生姓名
*/
public
String getName() {
return
name;
}
/**
* 獲取學生年齡
*/
public
int
getAge() {
return
age;
}
@Override
public
String toString() {
return
"Student [name="
+ name +
", age="
+ age +
"]"
;
}
}
|
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
|
import
java.util.ArrayList;
import
java.util.Collections;
import
java.util.Comparator;
import
java.util.List;
class
Test02 {
public
static
void
main(String[] args) {
List<Student> list =
new
ArrayList<>();
// Java 7的鑽石語法(構造器後面的尖括號中不須要寫類型)
list.add(
new
Student(
"Hao LUO"
,
33
));
list.add(
new
Student(
"XJ WANG"
,
32
));
list.add(
new
Student(
"Bruce LEE"
,
60
));
list.add(
new
Student(
"Bob YANG"
,
22
));
// 經過sort方法的第二個參數傳入一個Comparator接口對象
// 至關因而傳入一個比較對象大小的算法到sort方法中
// 因爲Java中沒有函數指針、仿函數、委託這樣的概念
// 所以要將一個算法傳入一個方法中惟一的選擇就是經過接口回調
Collections.sort(list,
new
Comparator<Student> () {
@Override
public
int
compare(Student o1, Student o2) {
return
o1.getName().compareTo(o2.getName());
// 比較學生姓名
}
});
for
(Student stu : list) {
System.out.println(stu);
}
// 輸出結果:
// Student [name=Bob YANG, age=22]
// Student [name=Bruce LEE, age=60]
// Student [name=Hao LUO, age=33]
// Student [name=XJ WANG, age=32]
}
}
|
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
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
|
/**
* 銀行帳戶
* @author 駱昊
*
*/
public
class
Account {
private
double
balance;
// 帳戶餘額
/**
* 存款
* @param money 存入金額
*/
public
void
deposit(
double
money) {
double
newBalance = balance + money;
try
{
Thread.sleep(
10
);
// 模擬此業務須要一段處理時間
}
catch
(InterruptedException ex) {
ex.printStackTrace();
}
balance = newBalance;
}
/**
* 得到帳戶餘額
*/
public
double
getBalance() {
return
balance;
}
}
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
|
/**
* 存錢線程
* @author 駱昊
*
*/
public
class
AddMoneyThread
implements
Runnable {
private
Account account;
// 存入帳戶
private
double
money;
// 存入金額
public
AddMoneyThread(Account account,
double
money) {
this
.account = account;
this
.money = money;
}
@Override
public
void
run() {
account.deposit(money);
}
}
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
|
import
java.util.concurrent.ExecutorService;
import
java.util.concurrent.Executors;
public
class
Test01 {
public
static
void
main(String[] args) {
Account account =
new
Account();
ExecutorService service = Executors.newFixedThreadPool(
100
);
for
(
int
i =
1
; i <=
100
; i++) {
service.execute(
new
AddMoneyThread(account,
1
));
}
service.shutdown();
while
(!service.isTerminated()) {}
System.out.println(
"帳戶餘額: "
+ account.getBalance());
}
}
|
在沒有同步的狀況下,執行結果一般是顯示帳戶餘額在10元如下,出現這種情況的緣由是,當一個線程A試圖存入1元的時候,另一個線程B也可以進入存款的方法中,線程B讀取到的帳戶餘額仍然是線程A存入1元錢以前的帳戶餘額,所以也是在原來的餘額0上面作了加1元的操做,同理線程C也會作相似的事情,因此最後100個線程執行結束時,原本指望帳戶餘額爲100元,但實際獲得的一般在10元如下(極可能是1元哦)。解決這個問題的辦法就是同步,當一個線程對銀行帳戶存錢時,須要將此帳戶鎖定,待其操做完成後才容許其餘的線程進行操做,代碼有以下幾種調整方案:
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
|
/**
* 銀行帳戶
* @author 駱昊
*
*/
public
class
Account {
private
double
balance;
// 帳戶餘額
/**
* 存款
* @param money 存入金額
*/
public
synchronized
void
deposit(
double
money) {
double
newBalance = balance + money;
try
{
Thread.sleep(
10
);
// 模擬此業務須要一段處理時間
}
catch
(InterruptedException ex) {
ex.printStackTrace();
}
balance = newBalance;
}
/**
* 得到帳戶餘額
*/
public
double
getBalance() {
return
balance;
}
}
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
|
/**
* 存錢線程
* @author 駱昊
*
*/
public
class
AddMoneyThread
implements
Runnable {
private
Account account;
// 存入帳戶
private
double
money;
// 存入金額
public
AddMoneyThread(Account account,
double
money) {
this
.account = account;
this
.money = money;
}
@Override
public
void
run() {
synchronized
(account) {
account.deposit(money);
}
}
}
|
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
|
import
java.util.concurrent.locks.Lock;
import
java.util.concurrent.locks.ReentrantLock;
/**
* 銀行帳戶
*
* @author 駱昊
*
*/
public
class
Account {
private
Lock accountLock =
new
ReentrantLock();
private
double
balance;
// 帳戶餘額
/**
* 存款
*
* @param money
* 存入金額
*/
public
void
deposit(
double
money) {
accountLock.lock();
try
{
double
newBalance = balance + money;
try
{
Thread.sleep(
10
);
// 模擬此業務須要一段處理時間
}
catch
(InterruptedException ex) {
ex.printStackTrace();
}
balance = newBalance;
}
finally
{
accountLock.unlock();
}
}
/**
* 得到帳戶餘額
*/
public
double
getBalance() {
return
balance;
}
}
|
按照上述三種方式對代碼進行修改後,重寫執行測試代碼Test01,將看到最終的帳戶餘額爲100元。固然也能夠使用Semaphore或CountdownLatch來實現同步。
6一、編寫多線程程序有幾種實現方式?
答:Java 5之前實現多線程有兩種實現方法:一種是繼承Thread類;另外一種是實現Runnable接口。兩種方式都要經過重寫run()方法來定義線程的行爲,推薦使用後者,由於Java中的繼承是單繼承,一個類有一個父類,若是繼承了Thread類就沒法再繼承其餘類了,顯然使用Runnable接口更爲靈活。
補充:Java 5之後建立線程還有第三種方式:實現Callable接口,該接口中的call方法能夠在線程執行結束時產生一個返回值,代碼以下所示:
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
|
import
java.util.ArrayList;
import
java.util.List;
import
java.util.concurrent.Callable;
import
java.util.concurrent.ExecutorService;
import
java.util.concurrent.Executors;
import
java.util.concurrent.Future;
class
MyTask
implements
Callable<Integer> {
private
int
upperBounds;
public
MyTask(
int
upperBounds) {
this
.upperBounds = upperBounds;
}
@Override
public
Integer call()
throws
Exception {
int
sum =
0
;
for
(
int
i =
1
; i <= upperBounds; i++) {
sum += i;
}
return
sum;
}
}
class
Test {
public
static
void
main(String[] args)
throws
Exception {
List<Future<Integer>> list =
new
ArrayList<>();
ExecutorService service = Executors.newFixedThreadPool(
10
);
for
(
int
i =
0
; i <
10
; i++) {
list.add(service.submit(
new
MyTask((
int
) (Math.random() *
100
))));
}
int
sum =
0
;
for
(Future<Integer> future : list) {
// while(!future.isDone()) ;
sum += future.get();
}
System.out.println(sum);
}
}
|
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。在java.io 包中還有許多其餘的流,主要是爲了提升性能和使用方便。關於Java的I/O須要注意的有兩點:一是兩種對稱性(輸入和輸出的對稱性,字節和字符的對稱性);二是兩種設計模式(適配器模式和裝潢模式)。另外Java中的流不一樣於C#的是它只有一個維度一個方向。
面試題 - 編程實現文件拷貝。(這個題目在筆試的時候常常出現,下面的代碼給出了兩種實現方案)
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
|
import
java.io.FileInputStream;
import
java.io.FileOutputStream;
import
java.io.IOException;
import
java.io.InputStream;
import
java.io.OutputStream;
import
java.nio.ByteBuffer;
import
java.nio.channels.FileChannel;
public
final
class
MyUtil {
private
MyUtil() {
throw
new
AssertionError();
}
public
static
void
fileCopy(String source, String target)
throws
IOException {
try
(InputStream in =
new
FileInputStream(source)) {
try
(OutputStream out =
new
FileOutputStream(target)) {
byte
[] buffer =
new
byte
[
4096
];
int
bytesToRead;
while
((bytesToRead = in.read(buffer)) != -
1
) {
out.write(buffer,
0
, bytesToRead);
}
}
}
}
public
static
void
fileCopyNIO(String source, String target)
throws
IOException {
try
(FileInputStream in =
new
FileInputStream(source)) {
try
(FileOutputStream out =
new
FileOutputStream(target)) {
FileChannel inChannel = in.getChannel();
FileChannel outChannel = out.getChannel();
ByteBuffer buffer = ByteBuffer.allocate(
4096
);
while
(inChannel.read(buffer) != -
1
) {
buffer.flip();
outChannel.write(buffer);
buffer.clear();
}
}
}
}
}
|
注意:上面用到Java 7的TWR,使用TWR後能夠不用在finally中釋放外部資源 ,從而讓代碼更加優雅。
70、寫一個方法,輸入一個文件名和一個字符串,統計這個字符串在這個文件中出現的次數。
答:代碼以下:
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
|
import
java.io.BufferedReader;
import
java.io.FileReader;
public
final
class
MyUtil {
// 工具類中的方法都是靜態方式訪問的所以將構造器私有不容許建立對象(絕對好習慣)
private
MyUtil() {
throw
new
AssertionError();
}
/**
* 統計給定文件中給定字符串的出現次數
*
* @param filename 文件名
* @param word 字符串
* @return 字符串在文件中出現的次數
*/
public
static
int
countWordInFile(String filename, String word) {
int
counter =
0
;
try
(FileReader fr =
new
FileReader(filename)) {
try
(BufferedReader br =
new
BufferedReader(fr)) {
String line =
null
;
while
((line = br.readLine()) !=
null
) {
int
index = -
1
;
while
(line.length() >= word.length() && (index = line.indexOf(word)) >=
0
) {
counter++;
line = line.substring(index + word.length());
}
}
}
}
catch
(Exception ex) {
ex.printStackTrace();
}
return
counter;
}
}
|
7一、如何用Java代碼列出一個目錄下全部的文件?
答:
若是隻要求列出當前文件夾下的文件,代碼以下所示:
1
2
3
4
5
6
7
8
9
10
11
12
13
|
import
java.io.File;
class
Test12 {
public
static
void
main(String[] args) {
File f =
new
File(
"/Users/Hao/Downloads"
);
for
(File temp : f.listFiles()) {
if
(temp.isFile()) {
System.out.println(temp.getName());
}
}
}
}
|
若是須要對文件夾繼續展開,代碼以下所示:
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
|
import
java.io.File;
class
Test12 {
public
static
void
main(String[] args) {
showDirectory(
new
File(
"/Users/Hao/Downloads"
));
}
public
static
void
showDirectory(File f) {
_walkDirectory(f,
0
);
}
private
static
void
_walkDirectory(File f,
int
level) {
if
(f.isDirectory()) {
for
(File temp : f.listFiles()) {
_walkDirectory(temp, level +
1
);
}
}
else
{
for
(
int
i =
0
; i < level -
1
; i++) {
System.out.print(
"\t"
);
}
System.out.println(f.getName());
}
}
}
|
在Java 7中能夠使用NIO.2的API來作一樣的事情,代碼以下所示:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
|
class
ShowFileTest {
public
static
void
main(String[] args)
throws
IOException {
Path initPath = Paths.get(
"/Users/Hao/Downloads"
);
Files.walkFileTree(initPath,
new
SimpleFileVisitor<Path>() {
@Override
public
FileVisitResult visitFile(Path file, BasicFileAttributes attrs)
throws
IOException {
System.out.println(file.getFileName().toString());
return
FileVisitResult.CONTINUE;
}
});
}
}
|
7二、用Java的套接字編程實現一個多線程的回顯(echo)服務器。
答:
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
|
import
java.io.BufferedReader;
import
java.io.IOException;
import
java.io.InputStreamReader;
import
java.io.PrintWriter;
import
java.net.ServerSocket;
import
java.net.Socket;
public
class
EchoServer {
private
static
final
int
ECHO_SERVER_PORT =
6789
;
public
static
void
main(String[] args) {
try
(ServerSocket server =
new
ServerSocket(ECHO_SERVER_PORT)) {
System.out.println(
"服務器已經啓動..."
);
while
(
true
) {
Socket client = server.accept();
new
Thread(
new
ClientHandler(client)).start();
}
}
catch
(IOException e) {
e.printStackTrace();
}
}
private
static
class
ClientHandler
implements
Runnable {
private
Socket client;
public
ClientHandler(Socket client) {
this
.client = client;
}
@Override
public
void
run() {
try
(BufferedReader br =
new
BufferedReader(
new
InputStreamReader(client.getInputStream()));
PrintWriter pw =
new
PrintWriter(client.getOutputStream())) {
String msg = br.readLine();
System.out.println(
"收到"
+ client.getInetAddress() +
"發送的: "
+ msg);
pw.println(msg);
pw.flush();
}
catch
(Exception ex) {
ex.printStackTrace();
}
finally
{
try
{
client.close();
}
catch
(IOException e) {
e.printStackTrace();
}
}
}
}
}
|
注意:上面的代碼使用了Java 7的TWR語法,因爲不少外部資源類都間接的實現了AutoCloseable接口(單方法回調接口),所以能夠利用TWR語法在try結束的時候經過回調的方式自動調用外部資源類的close()方法,避免書寫冗長的finally代碼塊。此外,上面的代碼用一個靜態內部類實現線程的功能,使用多線程能夠避免一個用戶I/O操做所產生的中斷影響其餘用戶對服務器的訪問,簡單的說就是一個用戶的輸入操做不會形成其餘用戶的阻塞。固然,上面的代碼使用線程池能夠得到更好的性能,由於頻繁的建立和銷燬線程所形成的開銷也是不可忽視的。
下面是一段回顯客戶端測試代碼:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
|
import
java.io.BufferedReader;
import
java.io.InputStreamReader;
import
java.io.PrintWriter;
import
java.net.Socket;
import
java.util.Scanner;
public
class
EchoClient {
public
static
void
main(String[] args)
throws
Exception {
Socket client =
new
Socket(
"localhost"
,
6789
);
Scanner sc =
new
Scanner(System.in);
System.out.print(
"請輸入內容: "
);
String msg = sc.nextLine();
sc.close();
PrintWriter pw =
new
PrintWriter(client.getOutputStream());
pw.println(msg);
pw.flush();
BufferedReader br =
new
BufferedReader(
new
InputStreamReader(client.getInputStream()));
System.out.println(br.readLine());
client.close();
}
}
|
若是但願用NIO的多路複用套接字實現服務器,代碼以下所示。NIO的操做雖然帶來了更好的性能,可是有些操做是比較底層的,對於初學者來講仍是有些難於理解。
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
|
import
java.io.IOException;
import
java.net.InetSocketAddress;
import
java.nio.ByteBuffer;
import
java.nio.CharBuffer;
import
java.nio.channels.SelectionKey;
import
java.nio.channels.Selector;
import
java.nio.channels.ServerSocketChannel;
import
java.nio.channels.SocketChannel;
import
java.util.Iterator;
public
class
EchoServerNIO {
private
static
final
int
ECHO_SERVER_PORT =
6789
;
private
static
final
int
ECHO_SERVER_TIMEOUT =
5000
;
private
static
final
int
BUFFER_SIZE =
1024
;
private
static
ServerSocketChannel serverChannel =
null
;
private
static
Selector selector =
null
;
// 多路複用選擇器
private
static
ByteBuffer buffer =
null
;
// 緩衝區
public
static
void
main(String[] args) {
init();
listen();
}
private
static
void
init() {
try
{
serverChannel = ServerSocketChannel.open();
buffer = ByteBuffer.allocate(BUFFER_SIZE);
serverChannel.socket().bind(
new
InetSocketAddress(ECHO_SERVER_PORT));
serverChannel.configureBlocking(
false
);
selector = Selector.open();
serverChannel.register(selector, SelectionKey.OP_ACCEPT);
}
catch
(Exception e) {
throw
new
RuntimeException(e);
}
}
private
static
void
listen() {
while
(
true
) {
try
{
if
(selector.select(ECHO_SERVER_TIMEOUT) !=
0
) {
Iterator<SelectionKey> it = selector.selectedKeys().iterator();
while
(it.hasNext()) {
SelectionKey key = it.next();
it.remove();
handleKey(key);
}
}
}
catch
(Exception e) {
e.printStackTrace();
}
}
}
private
static
void
handleKey(SelectionKey key)
throws
IOException {
SocketChannel channel =
null
;
try
{
if
(key.isAcceptable()) {
ServerSocketChannel serverChannel = (ServerSocketChannel) key.channel();
channel = serverChannel.accept();
channel.configureBlocking(
false
);
channel.register(selector, SelectionKey.OP_READ);
}
else
if
(key.isReadable()) {
channel = (SocketChannel) key.channel();
buffer.clear();
if
(channel.read(buffer) >
0
) {
buffer.flip();
CharBuffer charBuffer = CharsetHelper.decode(buffer);
String msg = charBuffer.toString();
System.out.println(
"收到"
+ channel.getRemoteAddress() +
"的消息:"
+ msg);
channel.write(CharsetHelper.encode(CharBuffer.wrap(msg)));
}
else
{
channel.close();
}
}
}
catch
(Exception e) {
e.printStackTrace();
if
(channel !=
null
) {
channel.close();
}
}
}
}
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
|
import
java.nio.ByteBuffer;
import
java.nio.CharBuffer;
import
java.nio.charset.CharacterCodingException;
import
java.nio.charset.Charset;
import
java.nio.charset.CharsetDecoder;
import
java.nio.charset.CharsetEncoder;
public
final
class
CharsetHelper {
private
static
final
String UTF_8 =
"UTF-8"
;
private
static
CharsetEncoder encoder = Charset.forName(UTF_8).newEncoder();
private
static
CharsetDecoder decoder = Charset.forName(UTF_8).newDecoder();
private
CharsetHelper() {
}
public
static
ByteBuffer encode(CharBuffer in)
throws
CharacterCodingException{
return
encoder.encode(in);
}
public
static
CharBuffer decode(ByteBuffer in)
throws
CharacterCodingException{
return
decoder.decode(in);
}
}
|
7三、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容許應用程序代碼把這些事件逐個拉出來,而不用提供在解析器方便時從解析器中接收事件的處理程序。
7四、你在項目中哪些地方用到了XML?
答:XML的主要做用有兩個方面:數據交換和信息配置。在作數據交換時,XML將數據用標籤組裝成起來,而後壓縮打包加密後經過網絡傳送給接收者,接收解密與解壓縮後再從XML文件中還原相關信息進行處理,XML曾經是異構系統間交換數據的事實標準,但此項功能幾乎已經被JSON(JavaScript Object Notation)取而代之。固然,目前不少軟件仍然使用XML來存儲配置信息,咱們在不少項目中一般也會將做爲配置信息的硬代碼寫在XML文件中,Java的不少框架也是這麼作的,並且這些框架都選擇了dom4j做爲處理XML的工具,由於Sun公司的官方API實在不怎麼好用。
補充:如今有不少時髦的軟件(如Sublime)已經開始將配置文件書寫成JSON格式,咱們已經強烈的感覺到XML的另外一項功能也將逐漸被業界拋棄。
7五、闡述JDBC操做數據庫的步驟。
答:下面的代碼以鏈接本機的Oracle數據庫爲例,演示JDBC操做數據庫的步驟。
1
|
Class.forName(
"oracle.jdbc.driver.OracleDriver"
);
|
1
|
Connection con = DriverManager.getConnection(
"jdbc:oracle:thin:@localhost:1521:orcl"
,
"scott"
,
"tiger"
);
|
1
2
3
|
PreparedStatement ps = con.prepareStatement(
"select * from emp where sal between ? and ?"
);
ps.setInt(
1
,
1000
);
ps.setInt(
2
,
3000
);
|
1
|
ResultSet rs = ps.executeQuery();
|
1
2
3
|
while
(rs.next()) {
System.out.println(rs.getInt(
"empno"
) +
" - "
+ rs.getString(
"ename"
));
}
|
1
2
3
4
5
6
7
8
9
|
finally
{
if
(con !=
null
) {
try
{
con.close();
}
catch
(SQLException e) {
e.printStackTrace();
}
}
}
|
提示:關閉外部資源的順序應該和打開的順序相反,也就是說先關閉ResultSet、再關閉Statement、在關閉Connection。上面的代碼只關閉了Connection(鏈接),雖然一般狀況下在關閉鏈接時,鏈接上建立的語句和打開的遊標也會關閉,但不能保證老是如此,所以應該按照剛纔說的順序分別關閉。此外,第一步加載驅動在JDBC 4.0中是能夠省略的(自動從類路徑中加載驅動),可是咱們建議保留。
7六、Statement和PreparedStatement有什麼區別?哪一個性能更好?
答:與Statement相比,①PreparedStatement接口表明預編譯的語句,它主要的優點在於能夠減小SQL的編譯錯誤並增長SQL的安全性(減小SQL注射攻擊的可能性);②PreparedStatement中的SQL語句是能夠帶參數的,避免了用字符串鏈接拼接SQL語句的麻煩和不安全;③當批量處理SQL或頻繁執行相同的查詢時,PreparedStatement有明顯的性能上的優點,因爲數據庫能夠將編譯優化後的SQL語句緩存起來,下次執行相同結構的語句時就會很快(不用再次編譯和生成執行計劃)。
補充:爲了提供對存儲過程的調用,JDBC API中還提供了CallableStatement接口。存儲過程(Stored Procedure)是數據庫中一組爲了完成特定功能的SQL語句的集合,經編譯後存儲在數據庫中,用戶經過指定存儲過程的名字並給出參數(若是該存儲過程帶有參數)來執行它。雖然調用存儲過程會在網絡開銷、安全性、性能上得到不少好處,可是存在若是底層數據庫發生遷移時就會有不少麻煩,由於每種數據庫的存儲過程在書寫上存在很多的差異。
7七、使用JDBC操做數據庫時,如何提高讀取數據的性能?如何提高更新數據的性能?
答:要提高讀取數據的性能,能夠指定經過結果集(ResultSet)對象的setFetchSize()方法指定每次抓取的記錄數(典型的空間換時間策略);要提高更新數據的性能能夠使用PreparedStatement語句構建批處理,將若干SQL語句置於一個批處理中執行。
7八、在進行數據庫編程時,鏈接池有什麼做用?
答:因爲建立鏈接和釋放鏈接都有很大的開銷(尤爲是數據庫服務器不在本地時,每次創建鏈接都須要進行TCP的三次握手,釋放鏈接須要進行TCP四次握手,形成的開銷是不可忽視的),爲了提高系統訪問數據庫的性能,能夠事先建立若干鏈接置於鏈接池中,須要時直接從鏈接池獲取,使用結束時歸還鏈接池而沒必要關閉鏈接,從而避免頻繁建立和釋放鏈接所形成的開銷,這是典型的用空間換取時間的策略(浪費了空間存儲鏈接,但節省了建立和釋放鏈接的時間)。池化技術在Java開發中是很常見的,在使用線程時建立線程池的道理與此相同。基於Java的開源數據庫鏈接池主要有:C3P0、Proxool、DBCP、BoneCP、Druid等。
補充:在計算機系統中時間和空間是不可調和的矛盾,理解這一點對設計知足性能要求的算法是相當重要的。大型網站性能優化的一個關鍵就是使用緩存,而緩存跟上面講的鏈接池道理很是相似,也是使用空間換時間的策略。能夠將熱點數據置於緩存中,當用戶查詢這些數據時能夠直接從緩存中獲得,這不管如何也快過去數據庫中查詢。固然,緩存的置換策略等也會對系統性能產生重要影響,對於這個問題的討論已經超出了這裏要闡述的範圍。
7九、什麼是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所作的操做丟失。
時間 | 轉帳事務A | 取款事務B |
---|---|---|
T1 | 開始事務 | |
T2 | 開始事務 | |
T3 | 查詢帳戶餘額爲1000元 | |
T4 | 查詢帳戶餘額爲1000元 | |
T5 | 取出100元將餘額修改成900元 | |
T6 | 提交事務 | |
T7 | 匯入100元將餘額修改成1100元 | |
T8 | 提交事務 | |
T9 | 查詢帳戶餘額爲1100元(丟失更新) |
數據併發訪問所產生的問題,在有些場景下多是容許的,可是有些場景下可能就是致命的,數據庫一般會經過鎖機制來解決數據併發訪問問題,按鎖定對象不一樣能夠分爲表級鎖和行級鎖;按併發事務鎖定關係能夠分爲共享鎖和獨佔鎖,具體的內容你們能夠自行查閱資料進行了解。
直接使用鎖是很是麻煩的,爲此數據庫爲用戶提供了自動鎖機制,只要用戶指定會話的事務隔離級別,數據庫就會經過分析SQL語句而後爲事務訪問的資源加上合適的鎖,此外,數據庫還會維護這些鎖經過各類手段提升系統的性能,這些對用戶來講都是透明的(就是說你不用理解,事實上我確實也不知道)。ANSI/ISO SQL 92標準定義了4個等級的事務隔離級別,以下表所示:
隔離級別 | 髒讀 | 不可重複讀 | 幻讀 | 第一類丟失更新 | 第二類丟失更新 |
---|---|---|---|---|---|
READ UNCOMMITED | 容許 | 容許 | 容許 | 不容許 | 容許 |
READ COMMITTED | 不容許 | 容許 | 容許 | 不容許 | 容許 |
REPEATABLE READ | 不容許 | 不容許 | 容許 | 不容許 | 不容許 |
SERIALIZABLE | 不容許 | 不容許 | 不容許 | 不容許 | 不容許 |
須要說明的是,事務隔離級別和數據訪問的併發性是對立的,事務隔離級別越高併發性就越差。因此要根據具體的應用來肯定合適的事務隔離級別,這個地方沒有萬能的原則。
8一、JDBC中如何進行事務處理?
答:Connection提供了事務處理的方法,經過調用setAutoCommit(false)能夠設置手動提交事務;當事務完成後用commit()顯式提交事務;若是在事務處理過程當中發生異常則經過rollback()進行事務回滾。除此以外,從JDBC 3.0中還引入了Savepoint(保存點)的概念,容許經過代碼設置保存點並讓事務回滾到指定的保存點。
8二、JDBC可否處理Blob和Clob?
答: Blob是指二進制大對象(Binary Large Object),而Clob是指大字符對象(Character Large Objec),所以其中Blob是爲存儲大的二進制數據而設計的,而Clob是爲存儲大的文本數據而設計的。JDBC的PreparedStatement和ResultSet都提供了相應的方法來支持Blob和Clob操做。下面的代碼展現瞭如何使用JDBC操做LOB:
下面以MySQL數據庫爲例,建立一個張有三個字段的用戶表,包括編號(id)、姓名(name)和照片(photo),建表語句以下:
1
2
3
4
5
6
|
create table tb_user
(
id
int
primary key auto_increment,
name varchar(
20
) unique not
null
,
photo longblob
);
|
下面的Java代碼向數據庫中插入一條記錄:
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
|
import
java.io.FileInputStream;
import
java.io.IOException;
import
java.io.InputStream;
import
java.sql.Connection;
import
java.sql.DriverManager;
import
java.sql.PreparedStatement;
import
java.sql.SQLException;
class
JdbcLobTest {
public
static
void
main(String[] args) {
Connection con =
null
;
try
{
// 1. 加載驅動(Java6以上版本能夠省略)
Class.forName(
"com.mysql.jdbc.Driver"
);
// 2. 創建鏈接
con = DriverManager.getConnection(
"jdbc:mysql://localhost:3306/test"
,
"root"
,
"123456"
);
// 3. 建立語句對象
PreparedStatement ps = con.prepareStatement(
"insert into tb_user values (default, ?, ?)"
);
ps.setString(
1
,
"駱昊"
);
// 將SQL語句中第一個佔位符換成字符串
try
(InputStream in =
new
FileInputStream(
"test.jpg"
)) {
// Java 7的TWR
ps.setBinaryStream(
2
, in);
// 將SQL語句中第二個佔位符換成二進制流
// 4. 發出SQL語句得到受影響行數
System.out.println(ps.executeUpdate() ==
1
?
"插入成功"
:
"插入失敗"
);
}
catch
(IOException e) {
System.out.println(
"讀取照片失敗!"
);
}
}
catch
(ClassNotFoundException | SQLException e) {
// Java 7的多異常捕獲
e.printStackTrace();
}
finally
{
// 釋放外部資源的代碼都應當放在finally中保證其可以獲得執行
try
{
if
(con !=
null
&& !con.isClosed()) {
con.close();
// 5. 釋放數據庫鏈接
con =
null
;
// 指示垃圾回收器能夠回收該對象
}
}
catch
(SQLException e) {
e.printStackTrace();
}
}
}
}
|
8三、簡述正則表達式及其用途。
答:在編寫處理字符串的程序時,常常會有查找符合某些複雜規則的字符串的須要。正則表達式就是用於描述這些規則的工具。換句話說,正則表達式就是記錄文本規則的代碼。
說明:計算機誕生初期處理的信息幾乎都是數值,可是時過境遷,今天咱們使用計算機處理的信息更多的時候不是數值而是字符串,正則表達式就是在進行字符串匹配和處理的時候最爲強大的工具,絕大多數語言都提供了對正則表達式的支持。
8四、Java中是如何支持正則表達式操做的?
答:Java中的String類提供了支持正則表達式操做的方法,包括:matches()、replaceAll()、replaceFirst()、split()。此外,Java中能夠用Pattern類表示正則表達式對象,它提供了豐富的API進行各類正則表達式操做,請參考下面面試題的代碼。
面試題: - 若是要從字符串中截取第一個英文左括號以前的字符串,例如:北京市(朝陽區)(西城區)(海淀區),截取結果爲:北京市,那麼正則表達式怎麼寫?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
import
java.util.regex.Matcher;
import
java.util.regex.Pattern;
class
RegExpTest {
public
static
void
main(String[] args) {
String str =
"北京市(朝陽區)(西城區)(海淀區)"
;
Pattern p = Pattern.compile(
".*?(?=\\()"
);
Matcher m = p.matcher(str);
if
(m.find()) {
System.out.println(m.group());
}
}
}
|
說明:上面的正則表達式中使用了懶惰匹配和前瞻,若是不清楚這些內容,推薦讀一下網上頗有名的《正則表達式30分鐘入門教程》。
8五、得到一個類的類對象有哪些方式?
答:
- 方法1:類型.class,例如:String.class
- 方法2:對象.getClass(),例如:」hello」.getClass()
- 方法3:Class.forName(),例如:Class.forName(「java.lang.String」)
8六、如何經過反射建立對象?
答:
- 方法1:經過類對象調用newInstance()方法,例如:String.class.newInstance()
- 方法2:經過類對象的getConstructor()或getDeclaredConstructor()方法得到構造器(Constructor)對象並調用其newInstance()方法建立對象,例如:String.class.getConstructor(String.class).newInstance(「Hello」);
8七、如何經過反射獲取和設置對象私有字段的值?
答:能夠經過類對象的getDeclaredField()方法字段(Field)對象,而後再經過字段對象的setAccessible(true)將其設置爲能夠訪問,接下來就能夠經過get/set方法來獲取/設置字段的值了。下面的代碼實現了一個反射的工具類,其中的兩個靜態方法分別用於獲取和設置私有字段的值,字段能夠是基本類型也能夠是對象類型且支持多級對象操做,例如ReflectionUtil.get(dog, 「owner.car.engine.id」);能夠得到dog對象的主人的汽車的引擎的ID號。
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
|
import
java.lang.reflect.Constructor;
import
java.lang.reflect.Field;
import
java.lang.reflect.Modifier;
import
java.util.ArrayList;
import
java.util.List;
/**
* 反射工具類
* @author 駱昊
*
*/
public
class
ReflectionUtil {
private
ReflectionUtil() {
throw
new
AssertionError();
}
/**
* 經過反射取對象指定字段(屬性)的值
* @param target 目標對象
* @param fieldName 字段的名字
* @throws 若是取不到對象指定字段的值則拋出異常
* @return 字段的值
*/
public
static
Object getValue(Object target, String fieldName) {
Class<?> clazz = target.getClass();
String[] fs = fieldName.split(
"\\."
);
try
{
for
(
int
i =
0
; i < fs.length -
1
; i++) {
Field f = clazz.getDeclaredField(fs[i]);
f.setAccessible(
true
);
target = f.get(target);
clazz = target.getClass();
}
Field f = clazz.getDeclaredField(fs[fs.length -
1
]);
f.setAccessible(
true
);
return
f.get(target);
}
catch
(Exception e) {
throw
new
RuntimeException(e);
}
}
/**
* 經過反射給對象的指定字段賦值
* @param target 目標對象
* @param fieldName 字段的名稱
* @param value 值
*/
public
static
void
setValue(Object target, String fieldName, Object value) {
Class<?> clazz = target.getClass();
String[] fs = fieldName.split(
"\\."
);
try
{
for
(
int
i =
0
; i < fs.length -
1
; i++) {
Field f = clazz.getDeclaredField(fs[i]);
f.setAccessible(
true
);
Object val = f.get(target);
if
(val ==
null
) {
Constructor<?> c = f.getType().getDeclaredConstructor();
c.setAccessible(
true
);
val = c.newInstance();
f.set(target, val);
}
target = val;
clazz = target.getClass();
}
Field f = clazz.getDeclaredField(fs[fs.length -
1
]);
f.setAccessible(
true
);
f.set(target, value);
}
catch
(Exception e) {
throw
new
RuntimeException(e);
}
}
}
|
8八、如何經過反射調用對象的方法?
答:請看下面的代碼:
1
2
3
4
5
6
7
8
9
10
|
import
java.lang.reflect.Method;
class
MethodInvokeTest {
public
static
void
main(String[] args)
throws
Exception {
String str =
"hello"
;
Method m = str.getClass().getMethod(
"toUpperCase"
);
System.out.println(m.invoke(str));
// HELLO
}
}
|
8九、簡述一下面向對象的」六原則一法則」。
答:
- 單一職責原則:一個類只作它該作的事情。(單一職責原則想表達的就是」高內聚」,寫代碼最終極的原則只有六個字」高內聚、低耦合」,就如同葵花寶典或辟邪劍譜的中心思想就八個字」欲練此功必先自宮」,所謂的高內聚就是一個代碼模塊只完成一項功能,在面向對象中,若是隻讓一個類完成它該作的事,而不涉及與它無關的領域就是踐行了高內聚的原則,這個類就只有單一職責。咱們都知道一句話叫」由於專一,因此專業」,一個對象若是承擔太多的職責,那麼註定它什麼都作很差。這個世界上任何好的東西都有兩個特徵,一個是功能單一,好的相機絕對不是電視購物裏面賣的那種一個機器有一百多種功能的,它基本上只能照相;另外一個是模塊化,好的自行車是組裝車,從減震叉、剎車到變速器,全部的部件都是能夠拆卸和從新組裝的,好的乒乓球拍也不是成品拍,必定是底板和膠皮能夠拆分和自行組裝的,一個好的軟件系統,它裏面的每一個功能模塊也應該是能夠輕易的拿到其餘系統中使用的,這樣才能實現軟件複用的目標。)
- 開閉原則:軟件實體應當對擴展開放,對修改關閉。(在理想的狀態下,當咱們須要爲一個軟件系統增長新功能時,只須要從原來的系統派生出一些新類就能夠,不須要修改原來的任何一行代碼。要作到開閉有兩個要點:①抽象是關鍵,一個系統中若是沒有抽象類或接口系統就沒有擴展點;②封裝可變性,將系統中的各類可變因素封裝到一個繼承結構中,若是多個可變因素混雜在一塊兒,系統將變得複雜而換亂,若是不清楚如何封裝可變性,能夠參考《設計模式精解》一書中對橋樑模式的講解的章節。)
- 依賴倒轉原則:面向接口編程。(該原則說得直白和具體一些就是聲明方法的參數類型、方法的返回類型、變量的引用類型時,儘量使用抽象類型而不用具體類型,由於抽象類型能夠被它的任何一個子類型所替代,請參考下面的里氏替換原則。)
里氏替換原則:任什麼時候候均可以用子類型替換掉父類型。(關於里氏替換原則的描述,Barbara Liskov女士的描述比這個要複雜得多,但簡單的說就是能用父類型的地方就必定能使用子類型。里氏替換原則能夠檢查繼承關係是否合理,若是一個繼承關係違背了里氏替換原則,那麼這個繼承關係必定是錯誤的,須要對代碼進行重構。例如讓貓繼承狗,或者狗繼承貓,又或者讓正方形繼承長方形都是錯誤的繼承關係,由於你很容易找到違反里氏替換原則的場景。須要注意的是:子類必定是增長父類的能力而不是減小父類的能力,由於子類比父類的能力更多,把能力多的對象當成能力少的對象來用固然沒有任何問題。)
- 接口隔離原則:接口要小而專,毫不能大而全。(臃腫的接口是對接口的污染,既然接口表示能力,那麼一個接口只應該描述一種能力,接口也應該是高度內聚的。例如,琴棋書畫就應該分別設計爲四個接口,而不該設計成一個接口中的四個方法,由於若是設計成一個接口中的四個方法,那麼這個接口很難用,畢竟琴棋書畫四樣都精通的人仍是少數,而若是設計成四個接口,會幾項就實現幾個接口,這樣的話每一個接口被複用的可能性是很高的。Java中的接口表明能力、表明約定、表明角色,可否正確的使用接口必定是編程水平高低的重要標識。)
- 合成聚合複用原則:優先使用聚合或合成關係複用代碼。(經過繼承來複用代碼是面向對象程序設計中被濫用得最多的東西,由於全部的教科書都無一例外的對繼承進行了鼓吹從而誤導了初學者,類與類之間簡單的說有三種關係,Is-A關係、Has-A關係、Use-A關係,分別表明繼承、關聯和依賴。其中,關聯關係根據其關聯的強度又能夠進一步劃分爲關聯、聚合和合成,但說白了都是Has-A關係,合成聚合複用原則想表達的是優先考慮Has-A關係而不是Is-A關係複用代碼,緣由嘛能夠本身從百度上找到一萬個理由,須要說明的是,即便在Java的API中也有很多濫用繼承的例子,例如Properties類繼承了Hashtable類,Stack類繼承了Vector類,這些繼承明顯就是錯誤的,更好的作法是在Properties類中放置一個Hashtable類型的成員而且將其鍵和值都設置爲字符串來存儲數據,而Stack類的設計也應該是在Stack類中放一個Vector對象來存儲數據。記住:任什麼時候候都不要繼承工具類,工具是能夠擁有並能夠使用的,而不是拿來繼承的。)
- 迪米特法則:迪米特法則又叫最少知識原則,一個對象應當對其餘對象有儘量少的瞭解。(迪米特法則簡單的說就是如何作到」低耦合」,門面模式和調停者模式就是對迪米特法則的踐行。對於門面模式能夠舉一個簡單的例子,你去一家公司洽談業務,你不須要了解這個公司內部是如何運做的,你甚至能夠對這個公司一無所知,去的時候只須要找到公司入口處的前臺美女,告訴她們你要作什麼,她們會找到合適的人跟你接洽,前臺的美女就是公司這個系統的門面。再複雜的系統均可覺得用戶提供一個簡單的門面,Java Web開發中做爲前端控制器的Servlet或Filter不就是一個門面嗎,瀏覽器對服務器的運做方式一無所知,可是經過前端控制器就可以根據你的請求獲得相應的服務。調停者模式也能夠舉一個簡單的例子來講明,例如一臺計算機,CPU、內存、硬盤、顯卡、聲卡各類設備須要相互配合才能很好的工做,可是若是這些東西都直接鏈接到一塊兒,計算機的佈線將異常複雜,在這種狀況下,主板做爲一個調停者的身份出現,它將各個設備鏈接在一塊兒而不須要每一個設備之間直接交換數據,這樣就減少了系統的耦合度和複雜度,以下圖所示。迪米特法則用通俗的話來將就是不要和陌生人打交道,若是真的須要,找一個本身的朋友,讓他替你和陌生人打交道。)
90、簡述一下你瞭解的設計模式。
答:所謂設計模式,就是一套被反覆使用的代碼設計經驗的總結(情境中一個問題通過證明的一個解決方案)。使用設計模式是爲了可重用代碼、讓代碼更容易被他人理解、保證代碼可靠性。設計模式令人們能夠更加簡單方便的複用成功的設計和體系結構。將已證明的技術表述成設計模式也會使新系統開發者更加容易理解其設計思路。
在GoF的《Design Patterns: Elements of Reusable Object-Oriented Software》中給出了三類(建立型[對類的實例化過程的抽象化]、結構型[描述如何將類或對象結合在一塊兒造成更大的結構]、行爲型[對在不一樣的對象之間劃分責任和算法的抽象化])共23種設計模式,包括:Abstract Factory(抽象工廠模式),Builder(建造者模式),Factory Method(工廠方法模式),Prototype(原始模型模式),Singleton(單例模式);Facade(門面模式),Adapter(適配器模式),Bridge(橋樑模式),Composite(合成模式),Decorator(裝飾模式),Flyweight(享元模式),Proxy(代理模式);Command(命令模式),Interpreter(解釋器模式),Visitor(訪問者模式),Iterator(迭代子模式),Mediator(調停者模式),Memento(備忘錄模式),Observer(觀察者模式),State(狀態模式),Strategy(策略模式),Template Method(模板方法模式), Chain Of Responsibility(責任鏈模式)。
面試被問到關於設計模式的知識時,能夠揀最經常使用的做答,例如:
- 工廠模式:工廠類能夠根據條件生成不一樣的子類實例,這些子類有一個公共的抽象父類而且實現了相同的方法,可是這些方法針對不一樣的數據進行了不一樣的操做(多態方法)。當獲得子類的實例後,開發人員能夠調用基類中的方法而沒必要考慮到底返回的是哪個子類的實例。
- 代理模式:給一個對象提供一個代理對象,並由代理對象控制原對象的引用。實際開發中,按照使用目的的不一樣,代理能夠分爲:遠程代理、虛擬代理、保護代理、Cache代理、防火牆代理、同步化代理、智能引用代理。
- 適配器模式:把一個類的接口變換成客戶端所期待的另外一種接口,從而使本來因接口不匹配而沒法在一塊兒使用的類可以一塊兒工做。
- 模板方法模式:提供一個抽象類,將部分邏輯以具體方法或構造器的形式實現,而後聲明一些抽象方法來迫使子類實現剩餘的邏輯。不一樣的子類能夠以不一樣的方式實現這些抽象方法(多態實現),從而實現不一樣的業務邏輯。
除此以外,還能夠講講上面提到的門面模式、橋樑模式、單例模式、裝潢模式(Collections工具類和I/O系統中都使用裝潢模式)等,反正基本原則就是揀本身最熟悉的、用得最多的做答,以避免言多必失。
9一、用Java寫一個單例類。
答:
- 餓漢式單例
1
2
3
4
5
6
7
|
public
class
Singleton {
private
Singleton(){}
private
static
Singleton instance =
new
Singleton();
public
static
Singleton getInstance(){
return
instance;
}
}
|
1
2
3
4
5
6
7
8
|
public
class
Singleton {
private
static
Singleton instance =
null
;
private
Singleton() {}
public
static
synchronized
Singleton getInstance(){
if
(instance ==
null
) instance =
new
Singleton();
return
instance;
}
}
|
注意:實現一個單例有兩點注意事項,①將構造器私有,不容許外界經過構造器建立對象;②經過公開的靜態方法向外界返回類的惟一實例。這裏有一個問題能夠思考:Spring的IoC容器能夠爲普通的類建立單例,它是怎麼作到的呢?
9二、什麼是UML?
答:UML是統一建模語言(Unified Modeling Language)的縮寫,它發表於1997年,綜合了當時已經存在的面向對象的建模語言、方法和過程,是一個支持模型化和軟件系統開發的圖形化語言,爲軟件開發的全部階段提供模型化和可視化支持。使用UML能夠幫助溝通與交流,輔助應用設計和文檔的生成,還可以闡釋系統的結構和行爲。
9三、UML中有哪些經常使用的圖?
答:UML定義了多種圖形化的符號來描述軟件系統部分或所有的靜態結構和動態結構,包括:用例圖(use case diagram)、類圖(class diagram)、時序圖(sequence diagram)、協做圖(collaboration diagram)、狀態圖(statechart diagram)、活動圖(activity diagram)、構件圖(component diagram)、部署圖(deployment diagram)等。在這些圖形化符號中,有三種圖最爲重要,分別是:用例圖(用來捕獲需求,描述系統的功能,經過該圖能夠迅速的瞭解系統的功能模塊及其關係)、類圖(描述類以及類與類之間的關係,經過該圖能夠快速瞭解系統)、時序圖(描述執行特定任務時對象之間的交互關係以及執行順序,經過該圖能夠了解對象能接收的消息也就是說對象可以向外界提供的服務)。
用例圖:
類圖:
時序圖:
9四、用Java寫一個冒泡排序。
答:冒泡排序幾乎是個程序員都寫得出來,可是面試的時候如何寫一個逼格高的冒泡排序卻不是每一個人都能作到,下面提供一個參考代碼:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
|
import
java.util.Comparator;
/**
* 排序器接口(策略模式: 將算法封裝到具備共同接口的獨立的類中使得它們能夠相互替換)
* @author駱昊
*
*/
public
interface
Sorter {
/**
* 排序
* @param list 待排序的數組
*/
public
<T
extends
Comparable<T>>
void
sort(T[] list);
/**
* 排序
* @param list 待排序的數組
* @param comp 比較兩個對象的比較器
*/
public
<T>
void
sort(T[] list, Comparator<T> comp);
}
|
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
|
import
java.util.Comparator;
/**
* 冒泡排序
*
* @author駱昊
*
*/
public
class
BubbleSorter
implements
Sorter {
@Override
public
<T
extends
Comparable<T>>
void
sort(T[] list) {
boolean
swapped =
true
;
for
(
int
i =
1
, len = list.length; i < len && swapped; ++i) {
swapped =
false
;
for
(
int
j =
0
; j < len - i; ++j) {
if
(list[j].compareTo(list[j +
1
]) >
0
) {
T temp = list[j];
list[j] = list[j +
1
];
list[j +
1
] = temp;
swapped =
true
;
}
}
}
}
@Override
public
<T>
void
sort(T[] list, Comparator<T> comp) {
boolean
swapped =
true
;
for
(
int
i =
1
, len = list.length; i < len && swapped; ++i) {
swapped =
false
;
for
(
int
j =
0
; j < len - i; ++j) {
if
(comp.compare(list[j], list[j +
1
]) >
0
) {
T temp = list[j];
list[j] = list[j +
1
];
list[j +
1
] = temp;
swapped =
true
;
}
}
}
}
}
|
9五、用Java寫一個折半查找。
答:折半查找,也稱二分查找、二分搜索,是一種在有序數組中查找某一特定元素的搜索算法。搜素過程從數組的中間元素開始,若是中間元素正好是要查找的元素,則搜素過程結束;若是某一特定元素大於或者小於中間元素,則在數組大於或小於中間元素的那一半中查找,並且跟開始同樣從中間元素開始比較。若是在某一步驟數組已經爲空,則表示找不到指定的元素。這種搜索算法每一次比較都使搜索範圍縮小一半,其時間複雜度是O(logN)。
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
|
import
java.util.Comparator;
public
class
MyUtil {
public
static
<T
extends
Comparable<T>>
int
binarySearch(T[] x, T key) {
return
binarySearch(x,
0
, x.length-
1
, key);
}
// 使用循環實現的二分查找
public
static
<T>
int
binarySearch(T[] x, T key, Comparator<T> comp) {
int
low =
0
;
int
high = x.length -
1
;
while
(low <= high) {
int
mid = (low + high) >>>
1
;
int
cmp = comp.compare(x[mid], key);
if
(cmp <
0
) {
low= mid +
1
;
}
else
if
(cmp >
0
) {
high= mid -
1
;
}
else
{
return
mid;
}
}
return
-
1
;
}
// 使用遞歸實現的二分查找
private
static
<T
extends
Comparable<T>>
int
binarySearch(T[] x,
int
low,
int
high, T key) {
if
(low <= high) {
int
mid = low + ((high -low) >>
1
);
if
(key.compareTo(x[mid])==
0
) {
return
mid;
}
else
if
(key.compareTo(x[mid])<
0
) {
return
binarySearch(x,low, mid -
1
, key);
}
else
{
return
binarySearch(x,mid +
1
, high, key);
}
}
return
-
1
;
}
}
|
說明:上面的代碼中給出了折半查找的兩個版本,一個用遞歸實現,一個用循環實現。須要注意的是計算中間位置時不該該使用(high+ low) / 2的方式,由於加法運算可能致使整數越界,這裏應該使用如下三種方式之一:low + (high – low) / 2或low + (high – low) >> 1或(low + high) >>> 1(>>>是邏輯右移,是不帶符號位的右移)