Java集合框架(例如基本的數據結構)裏包含了最多見的Java常見面試問題。很好地理解集合框架,能夠幫助你理解和利用Java的一些高級特性。下面是面試Java核心技術的一些很實用的問題。html
Q:最多見的數據結構有哪些,在哪些場景下應用它們?java
A. 大部分人都會遺漏樹和圖這兩種數據結構。樹和圖都是頗有用的數據結構。若是你在回答中說起到它們的話,面試者可能會對你進行進一步進行的考覈。node
Q:你如何本身實現List,Set和Map?面試
A:雖然Java已經提供了這些接口的通過實踐證實和測試過的實現,可是面試者仍是喜歡這樣問,來測試你對數據結構的理解。我寫的《Core Java Career Essentials》一書中經過圖例和代碼詳細地講解了這些內容。算法
常見的數據結構數據庫
數組是最經常使用的數據結構。數組的特色是長度固定,能夠用下標索引,而且全部的元素的類型都是一致的。數組經常使用的場景有把:從數據庫裏讀取僱員的信息存儲爲EmployeeDetail[],把一個字符串轉換並存儲到一個字節數組中便於操做和處理,等等。儘可能把數組封裝在一個類裏,防止數據被錯誤的操做弄亂。另外,這一點也適合其餘的數據結構。編程
列表和數組很類似,只不過它的大小能夠改變。列表通常都是經過一個固定大小的數組來實現的,而且會在須要的時候自動調整大小。列表裏能夠包含重複的元素。經常使用的場景有,添加一行新的項到訂單列表裏,把全部過時的商品移出商品列表,等等。通常會把列表初始化成一個合適的大小,以減小調整大小的次數。數組
集合和列表很類似,不過它不能放重複的元素。當你須要存儲不一樣的元素時,你可使用集合。瀏覽器
堆棧只容許對最後插入的元素進行操做(也就是後進先出,Last In First Out – LIFO)。若是你移除了棧頂的元素,那麼你能夠操做倒數第二個元素,依次類推。這種後進先出的方式是經過僅有的peek(),push()和pop()這幾個方法的強制性限制達到的。這種結構在不少場景下都很是實用,例如解析像(4+2)*3這樣的數學表達式,把源碼中的方法和異常按照他們出現的順序放到堆棧中,檢查你的代碼看看小括號和花括號是否是匹配的,等等。數據結構
這種用堆棧來實現的後進先出(LIFO)的機制在不少地方都很是實用。例如,表達式求值和語法解析,校驗和解析XML,文本編輯器裏的撤銷動做,瀏覽器裏的瀏覽記錄,等等。這裏是一些關於堆棧的一些Java面試題。
隊列和堆棧有些類似,不一樣之處在於在隊列裏第一個插入的元素也是第一個被刪除的元素(便是先進先出)。這種先進先出的結構是經過只提供peek(),offer()和poll()這幾個方法來訪問數據進行限制來達到的。例如,排隊等待公交車,銀行或者超市裏的等待列隊等等,都是能夠用隊列來表示。
這裏是一個用多線程來訪問阻塞隊列的例子。
鏈表是一種由多個節點組成的數據結構,而且每一個節點包含有數據以及指向下一個節點的引用,在雙向鏈表裏,還會有一個指向前一個節點的引用。例如,能夠用單向鏈表和雙向鏈表來實現堆棧和隊列,由於鏈表的兩端都是能夠進行插入和刪除的動做的。固然,也會有在鏈表的中間頻繁插入和刪除節點的場景。Apache的類庫裏提供了一個TreeList的實現,它是鏈表的一個很好的替代,由於它只多佔用了一點內存,可是性能比鏈表好不少。也就是說,從這點來看鏈表其實不是一個很好的選擇。
ArrayList是列表的一個很好的實現。相比較TreeList而言,ArrayList在除了在列表中間插入或者刪除元素的狀況,其餘操做都比TreeList快不少。TreeList的實現是在內部使用了一個樹形的結構來保證全部的插入和刪除動做的複雜度都是O(log n)的。這種實現方式使得TreeList在頻繁插入和刪除元素的時候的性能遠遠高於ArrayList和LinkedList。
1
2
3
4
5
|
class
Link {
private
int
id;
// data
private
String name;
// data
private
Link next;
// reference to next link
}
|
HashMap的訪問時間接近穩定,它是一種鍵值對映射的數據結構。這個數據結構是經過數組來實現的。它經過hash函數來給元素定位,而且用衝突檢測算法來處理被hash到同一位置的值。例如,保存僱員的信息能夠用僱員的id來做爲key,對從properties文件裏讀入的屬性-屬性值能夠用key/value對來保存,等等。HashMap在初始化的時候,給定一個合適的大小能夠減小調整大小的次數。
樹是一種由節點組成的數據結構,每一個節點都包含數據元素,而且有一個或多個子節點,每一個子節點指向一個父節點(譯者注:除了根節點)能夠表示層級關係或者數據元素的順序關係。經常使用的場景有表示一個組織裏的僱員層級關係,XML元素的層級關係,等等。若是樹的每一個子節點最多有兩個葉子節點,那麼這種樹被稱爲二叉樹。二叉樹是一種很是經常使用的樹形結構, 由於它的這種結構使得節點的插入和刪除都很是高效。樹的邊表示從一個節點到另一個節點的快捷路徑。
Java裏面沒有直接提供樹的實現,可是它很容易經過下面的方式來實現。只須要建立一個Node對象,裏面包含一個指向葉子節點的ArrayList。
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
|
package
bigo;
import
java.util.ArrayList;
import
java.util.List;
public
class
Node {
private
String name;
private
List<node> children =
new
ArrayList<node>( );
private
Node parent;
public
Node getParent( ) {
return
parent;
}
public
void
setParent(Node parent) {
this
.parent = parent;
}
public
Node(String name) {
this
.name = name;
}
public
void
addChild(Node child) {
children.add(child);
}
public
void
removeChild(Node child) {
children.remove(child);
}
public
String toString( ) {
return
name;
}
}
|
只要數據元素的關係能夠表示成節點和邊的網狀結構的話,就能夠用圖來表示。樹是一種特殊的圖,它的全部節點都只能有一個父節點。和樹不一樣的是,圖的形狀是由實際問題或者問題的抽象來決定的。例如,圖中節點(或者頂點)能夠表示不一樣的城市,而圖的邊則能夠表示兩個城市之間的航線。
在Java裏構造一個圖,你須要解決數據經過什麼方式保存和訪問的問題。在圖裏面也會用到上面提到的數據結構。Java的API裏沒有提供圖的實現。不過有不少第三方庫裏提供了,例如JUNG,JGraphT,以及JDSL(不過好像不支持泛型)。《Core Java Career Essential》一書包含了用Java實現的可用示例。
Q:你對大O這個符號有什麼瞭解呢,你是否能夠根據不一樣的數據結構舉出一些列子來?
A:大O符號能夠表示一個算法的效率,也能夠用來描述當數據元素增長時,最壞狀況下的算法的性能。大O符號也能夠用來衡量的性能,例如內存消耗量。有時候你可能會爲了減小內存使用量而選擇一個比較慢的算法。大O符號能夠表示在大量數據的狀況下程序的性能。不過,對於程序在大量數據量下的性能的測量,惟一比較實際的方式是行用較大的數據集來進行性能基準測試,這樣能夠把一些在大O複雜度分析裏沒有考慮到的狀況包含進去,例如在虛擬內存使用比較多的時候系統會發生換頁的狀況。雖然基準測試比大O符號表示的結果更加實際,可是它不適用於設計階段,因此在這個這時候大O複雜度分析是最合適的選擇。
各類數據結構在搜索,插入和刪除算法上的性能均可以用下面方式表示:常量複雜度O(1),線性複雜度O(n),對數複雜度O(log n),指數複雜度O(c^n),多項式複雜度O(n^c),平方複雜度O(n^2)以及階乘複雜度O(n!),這裏面的n都指的是數據結構裏的元素的數量。性能和內存佔用是能夠相互權衡的。下面是一些示例。
示例1:在HashMap裏查找一個元素的的時間複雜度是常量的,也便是O(1)。這是由於查找元素使用的是哈希函數,而且計算一個哈希值的時間是不受HashMap裏的元素的個數的影響的。
示例2:線性搜索一個數組,列表以及鏈表都是的複雜度線性的,也便是O(n),這是查找的時候須要遍歷整個列表。也就是說,若是一個列表的長度是原來的兩倍,那麼搜索所花的時間也是原來的兩倍。
示例3:一個須要比較數組裏的全部元素的排序算法的複雜度是多項式的,便是O(n^2)。這是由於一個嵌套的for循環的複雜度是O(n^2)。在搜素算法裏有這樣的例子。
示例4:二分搜索一個數組或者數組列表的複雜度是對數的,便是O(log n)。在鏈表裏查詢一個節點的複雜度通常是O(n)。相比較數組鏈表和數組的O(log n)的性能而言,隨着元素數量的增加,鏈表的O(n)的複雜度的性能就比較差了。對數的時間複雜度就是若是10個元素花費的時間是x單位的話,100個元素最多花費2x單位的時間,而10000個元素最多花費4x個單位的時間。若是你在一個平面座標上畫出圖形的話,你會發現時間的增加沒有n(元素的個數)快。
Q:HashMap和TreeMap在性能上有什麼樣的差異呢?你比較傾向於使用哪個?
A:一個平衡樹的性能是O(logn)。Java裏的TreeMap用一個紅黑樹來保證key/value的排序。紅黑樹是平衡二叉樹。保證二叉樹的平衡性,使得插入,刪除和查找都比較快,時間複雜度都是O(log n)。不過它沒有HashMap快,HashMap的時間複雜度是O(1),可是TreeMap的優勢在於它裏面鍵值是排過序的,這樣就提供了一些其餘的頗有用的功能。
Q:怎麼去選擇該使用哪個呢?
A:使用無序的HashSet和HashMap,仍是使用有序的TreeSet和TreeMap,主要取決於你的實際使用場景,必定程度上還和數據的大小以及運行環境有關。比較實際的一個緣由是,若是插入和更新都比較頻繁的話,那麼保證元素的有序能夠提升快速和頻繁查找的性能。若是對於排序操做(例如產生一個報表合做者運行一個批處理程序)的要求不是很頻繁的話,那麼把數據以無序的方式存儲,而後在須要排序的時候用Collections.sort(…)來進行排序,會比用有序的方式來存儲可能會更加高效。這個只是一種可選的方式,沒人能給你一個確切的答案。即便是複雜度的理論,例如O(n),成立的前提也是在n足夠大的狀況下。只要在n足夠小的狀況下,就算是O(n)的算法也可能會比O(log n)的算法更加高效。另外,一個算法可能在AMD處理器上的速度比在Intel處理器上快。若是你的系統有交換區的話,那麼你還要考慮磁盤的性能。惟一能夠肯定的性能測試途徑是用大小合適的數據來測試和衡量程序的性能和內存使用量。在你所選擇的硬件上來測試這兩種指標,是最合適的方法。
Q:如何權衡是用無序的數組仍是有序的數組呢?
A:有序數組最大的優勢在於n比較大的時候,搜索元素所花的時間O(log n)比無序素組所須要的時間O(n)要少不少。有序數組的缺點在於插入的時間開銷比較大(通常是O(n)),由於全部比插入元素大的值都要日後移動。而無序數組的插入時間開銷是常量時間,也就是說,插入的速度和元素的數量無關。下面的代碼片斷展現了向有序數組和無序數組插入元素。
插入元素到一個無序的數組裏
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
|
package
bigo;
import
java.util.Arrays;
public
class
InsertingElementsToArray {
public
static
void
insertUnsortedArray(String toInsert) {
String[ ] unsortedArray = {
"A"
,
"D"
,
"C"
};
String[ ] newUnsortedArray =
new
String[unsortedArray.length +
1
];
System.arraycopy(unsortedArray,
0
, newUnsortedArray,
0
,
3
);
newUnsortedArray[newUnsortedArray.length -
1
] = toInsert;
System.out.println(Arrays.toString(newUnsortedArray));
}
public
static
void
main(String[ ] args) {
insertUnsortedArray(
"B"
);
}
}
|
插入元素到一個有序數組
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
|
package
bigo;
import
java.util.Arrays;
public
class
InsertingElementsToArray {
public
static
void
insertSortedArray(String toInsert) {
String[ ] sortedArray = {
"A"
,
"C"
,
"D"
};
/*
* Binary search returns the index of the search item
* if found, otherwise returns the minus insertion point. This example
* returns index = -2, which means the elemnt is not found and needs to
* be inserted as a second element.
*/
int
index = Arrays.binarySearch(sortedArray, toInsert);
if
(index <
0
) {
// not found.
// array indices are zero based. -2 index means insertion point of
// -(-2)-1 = 1, so insertIndex = 1
int
insertIndex = -index -
1
;
String[ ] newSortedArray =
new
String[sortedArray.length +
1
];
System.arraycopy(sortedArray,
0
, newSortedArray,
0
, insertIndex);
System.arraycopy(sortedArray, insertIndex,
newSortedArray, insertIndex +
1
, sortedArray.length - insertIndex);
newSortedArray[insertIndex] = toInsert;
System.out.println(Arrays.toString(newSortedArray));
}
}
public
static
void
main(String[ ] args) {
insertSortedArray(
"B"
);
}
}
|
因此,如何去選擇仍是取決於實際的使用狀況。你須要考慮下面幾個問題。你的程序是插入/刪除的操做多,仍是查找的操做多?數組裏最多可能存儲多少元素?排序的頻率是多少?以及你的性能基準測試的結果是怎樣的?
Q:怎麼實現一個不可變集合?
A:這個功能在Collections類裏實現了,它經過裝飾模式實現了對通常集合的封裝。
1
2
3
4
5
6
7
8
9
10
11
|
public
class
ReadOnlyExample {
public
static
void
main(String args[ ]) {
Set<string> set =
new
HashSet<string>( );
set.add(
"Java"
);
set.add(
"JEE"
);
set.add(
"Spring"
);
set.add(
"Hibernate"
);
set = Collections.unmodifiableSet(set);
set.add(
"Ajax"
);
// not allowed.
}
}
|
Q:下面的代碼的功能是什麼呢?其中的LinkedHashSet能用HashSet來取代嗎?
1
2
3
4
5
6
7
8
9
|
import
java.util.ArrayList;
import
java.util.LinkedHashSet;
import
java.util.List;
public
class
CollectionFunction {
public
<e> List<e> function (List <e> list) {
return
new
ArrayList<e>(
new
LinkedHashSet<e>(list));
}
}
|
A:上面的代碼代碼經過把原有列表傳入一個LinkedHashSet來去除重複的元素。在這個狀況裏,LinkedHashSet能夠保持元素原來的順序。若是這個順序是不須要的話,那麼上面的LinkedHashSet能夠用HashSet來替換。
Q:Java集合框架都有哪些最佳實踐呢?
A:根據實際的使用狀況選擇合適的數據結構,例如固定大小的仍是須要增長大小的,有重複元素的仍是沒有的,須要保持有序仍是不須要,遍歷是正向的仍是雙向的,插入是在末尾的仍是任意位置的,更多的插入仍是更多的讀取,是否須要並行訪問,是否容許修改,元素類型是相同的仍是不一樣的,等等。另外,還須要儘早考慮多線程,原子性,內存使用量以及性能等因素。
不要假設你的集合裏元素的數量一直會保持較小,它也有可能隨着時間增加。因此,你的集合最好可以給定一個合適的大小。
針對接口編程優於針對實現編程。例如,可能在某些狀況下,LinkedList是最佳的選擇,可是後來ArrayList可能由於性能的緣由變得更加合適
1
|
ArrayList list =
new
ArrayList(
100
);
|
1
2
3
4
5
6
|
// program to interface so that the implementation can change
List list =
new
ArrayList(
100
);
List list2 =
new
LinkedList(
100
);
List emptyList = Collections.emptyList( );
Set emptySet = Collections.emptySet( );
|
在取得列表的時候,若是返回的結果是空的話,最好返回一個長度爲0的集合或者數組,而不要返回null。由於,返回null的話可能能會致使程序錯誤。調用你的方法的開發人員可能會忘記對返回爲null的狀況進行處理。
封裝好集合:通常來講,集合都是不可變的對象。因此儘可能不要把集合的成員變量暴露給調用者。由於他們的操做通常都不會進行必要的校驗。
歡迎學Java和大數據的朋友們加入java架構交流: 855835163 羣內提供免費的架構資料還有:Java工程化、高性能及分佈式、高性能、深刻淺出。高架構。性能調優、Spring,MyBatis,Netty源碼分析和大數據等多個知識點高級進階乾貨的免費直播講解 能夠進來一塊兒學習交流哦