下面要開始java中相關集合框架的學習啦。
Are you ready?Let's go~~
今天要講解的Java中的集合框架。
1) 首先檢視jdk中Collection類的原始碼後會發現如下內容:
...
* @see AbstractCollection * @since 1.2 */ public interface Collection<E> extends Iterable<E> { // Query Operations
通過檢視可以發現Collection是一個介面類,其繼承了java迭代介面Iterable。
眾所周知在我們使用Java中的類的儲存的時候經常會使用一些容器,連結串列的概念,本文將徹底幫您弄清連結串列的各種概念和模型!!!!
注意理解哦~~~ 大致框架如下:
Collection介面有兩個主要的子介面List和Set,注意Map不是Collection的子介面哦這個要牢記。
Collection中可以儲存的元素間無序,可以重複組各 自獨立的元素, 即其內的每個位置僅持有一個元素,同時允許有多個null元素物件。
Collection介面中的方法如下:
1)List介面
List介面對Collection進行了簡單的擴充
檢視List介面的原始碼會發現:
...
* @see AbstractList * @see AbstractSequentialList * @since 1.2 */ public interface List<E> extends Collection<E> { // Query Operations /** * Returns the number of elements in this list. If this list contains * more than <tt>Integer.MAX_VALUE</tt> elements, returns * <tt>Integer.MAX_VALUE</tt>.
...
這裡也就知道為什麼Collection介面時List介面的父介面了吧。
List介面中的元素的特點為:
List中儲存的元素實現類排序,而且可以重複的儲存相關元素。
同時List介面又有兩個常用的實現類ArrayList和LinkedList
1)ArrayList:
ArrayList陣列線性表的特點為:類似陣列的形式進行儲存,因此它的隨機訪問速度極快。
ArrayList陣列線性表的缺點為:不適合於線上性表中間需要頻繁進行插入和刪除操作。因為每次插入和刪除都需要移動陣列中的元素。
可以這樣理解ArrayList就是基於陣列的一個線性表,只不過陣列的長度可以動態改變而已。
對於ArrayList的詳細使用資訊以及建立的過程可以檢視jdk中ArrayList的原始碼,這裡不做過多的講解。
對於使用ArrayList的開發者而言,下面幾點內容一定要注意啦,尤其找工作面試的時候經常會被問到。
注意啦!!!!!!!!
a.如果在初始化ArrayList的時候沒有指定初始化長度的話,預設的長度為10.
/** * Constructs an empty list with an initial capacity of ten. */ public ArrayList() { this(10); }
b.ArrayList在增加新元素的時候如果超過了原始的容量的話,ArrayList擴容ensureCapacity的方案為“原始容量*3/2+1"哦。
/**
* Increases the capacity of this <tt>ArrayList</tt> instance, if
* necessary, to ensure that it can hold at least the number of elements
* specified by the minimum capacity argument.
*
* @param minCapacity the desired minimum capacity
*/
public void ensureCapacity(int minCapacity) {
modCount++;
int oldCapacity = elementData.length;
if (minCapacity > oldCapacity) {
Object oldData[] = elementData;
int newCapacity = (oldCapacity * 3)/2 + 1;
if (newCapacity < minCapacity)
newCapacity = minCapacity;
// minCapacity is usually close to size, so this is a win:
elementData = Arrays.copyOf(elementData, newCapacity);
}
}
c.ArrayList是執行緒不安全的,在多執行緒的情況下不要使用。
如果一定在多執行緒使用List的,您可以使用Vector,因為Vector和ArrayList基本一致,區別在於Vector中的絕大部分方法都
使用了同步關鍵字修飾,這樣在多執行緒的情況下不會出現併發錯誤哦,還有就是它們的擴容方案不同,ArrayList是通過原始
容量*3/2+1,而Vector是允許設定預設的增長長度,Vector的預設擴容方式為原來的2倍。
切記Vector是ArrayList的多執行緒的一個替代品。
d.ArrayList實現遍歷的幾種方法
package com.yonyou.test; import java.util.ArrayList; import java.util.Iterator; import java.util.List; /** * 測試類 * @author 小浩 * @建立日期 2015-3-2 */ public class Test{ public static void main(String[] args) { List<String> list=new ArrayList<String>(); list.add("Hello"); list.add("World"); list.add("HAHAHAHA"); //第一種遍歷方法使用foreach遍歷List for (String str : list) { //也可以改寫for(int i=0;i<list.size();i++)這種形式 System.out.println(str); } //第二種遍歷,把連結串列變為陣列相關的內容進行遍歷 String[] strArray=new String[list.size()]; list.toArray(strArray); for(int i=0;i<strArray.length;i++) //這裡也可以改寫為foreach(String str:strArray)這種形式 { System.out.println(strArray[i]); } //第三種遍歷 使用迭代器進行相關遍歷 Iterator<String> ite=list.iterator(); while(ite.hasNext()) { System.out.println(ite.next()); } } }
尼瑪,以上四點面試經常會被問到的。到時候死翹翹別說哥沒告訴你。
2)LinkedList
LinkedList的鏈式線性表的特點為: 適合於在連結串列中間需要頻繁進行插入和刪除操作。
LinkedList的鏈式線性表的缺點為: 隨機訪問速度較慢。查詢一個元素需要從頭開始一個一個的找。速度你懂的。
可以這樣理解LinkedList就是一種雙向迴圈連結串列的鏈式線性表,只不過儲存的結構使用的是鏈式表而已。
對於LinkedList的詳細使用資訊以及建立的過程可以檢視jdk中LinkedList的原始碼,這裡不做過多的講解。
對於使用LinkedList的開發者而言,下面幾點內容一定要注意啦,尤其找工作面試的過程時候經常會被問到。
注意啦!!!!!!!!
a.LinkedList和ArrayList的區別和聯絡
ArrayList陣列線性表的特點為:類似陣列的形式進行儲存,因此它的隨機訪問速度極快。
ArrayList陣列線性表的缺點為:不適合於線上性表中間需要頻繁進行插入和刪除操作。因為每次插入和刪除都需要移動陣列中的元素。
LinkedList的鏈式線性表的特點為: 適合於在連結串列中間需要頻繁進行插入和刪除操作。
LinkedList的鏈式線性表的缺點為: 隨機訪問速度較慢。查詢一個元素需要從頭開始一個一個的找。速度你懂的。
b.LinkedList的內部實現
對於這個問題,你最好看一下jdk中LinkedList的原始碼。這樣你會醍醐灌頂的。
這裡我大致說一下:
LinkedList的內部是基於雙向迴圈連結串列的結構來實現的。在LinkedList中有一個類似於c語言中結構體的Entry內部類。
在Entry的內部類中包含了前一個元素的地址引用和後一個元素的地址引用類似於c語言中指標。
c.LinkedList不是執行緒安全的
注意LinkedList和ArrayList一樣也不是執行緒安全的,如果在對執行緒下面訪問可以自己重寫LinkedList
然後在需要同步的方法上面加上同步關鍵字synchronized
d.LinkedList的遍歷方法
package com.yonyou.test; import java.util.LinkedList; import java.util.List; /** * 測試類 * @author 小浩 * @建立日期 2015-3-2 */ public class Test{ public static void main(String[] args) { List<String> list=new LinkedList<String>(); list.add("Hello"); list.add("World"); list.add("龍不吟,虎不嘯"); //LinkedList遍歷的第一種方式使用陣列的方式 String[] strArray=new String[list.size()]; list.toArray(strArray); for(String str:strArray) { System.out.println(str); } //LinkedList遍歷的第二種方式 for(String str:list) { System.out.println(str); } //至於還是否有其它遍歷方式,我沒查,感興趣自己研究研究 } }
e.LinkedList可以被當做堆疊來使用
由於LinkedList實現了介面Dueue,所以LinkedList可以被當做堆疊來使用,這個你自己研究吧。
2)Set介面
Set介面也是Collection介面的一個常用子介面。
檢視Set介面的原始碼你會發現:
* @see Collections#EMPTY_SET * @since 1.2 */ public interface Set<E> extends Collection<E> { // Query Operations /** * Returns the number of elements in this set (its cardinality). If this * set contains more than <tt>Integer.MAX_VALUE</tt> elements, returns
這裡就自然而然的知道Set介面是Collection介面的子介面了吧。
Set介面區別於List介面的特點在於:
Set中的元素實現了不重複,有點象集合的概念,無序,不允許有重複的元素,最多允許有一個null元素物件。
需要注意的是:雖然Set中元素沒有順序,但是元素在set中的位置是有由該元素的HashCode決定的,其具體位置其實是固定的。
檢視jdk的原始碼會發現下面的內容
...
* @see Collections#EMPTY_SET * @since 1.2 */ public interface Set<E> extends Collection<E> { // Query Operations /** * Returns the number of elements in this set (its cardinality). If this * set contains more than <tt>Integer.MAX_VALUE</tt> elements, returns
...
在這裡也會看到set介面時Collection介面的子介面吧~哈
此外需要說明一點,在set介面中的不重複是由特殊要求的。
舉一個例子:物件A和物件B,本來是不同的兩個物件,正常情況下它們是能夠放入到Set裡面的,但是
如果物件A和B的都重寫了hashcode和equals方法,並且重寫後的hashcode和equals方法是相同的話。那麼A和B是不能同時放入到
Set集合中去的,也就是Set集合中的去重和hashcode與equals方法直接相關。
為了更好的理解,請看下面的例子:
package com.yonyou.test; import java.util.HashSet; import java.util.Set; /** * 測試類 * @author 小浩 * @建立日期 2015-3-2 */ public class Test{ public static void main(String[] args) { Set<String> set=new HashSet<String>(); set.add("Hello"); set.add("world"); set.add("Hello"); System.out.println("集合的尺寸為:"+set.size()); System.out.println("集合中的元素為:"+set.toString()); } }
由於String類中重寫了hashcode和equals方法,所以這裡的第二個Hello是加不進去的哦。
Set介面的常見實現類有HashSet,LinedHashSet和TreeSet這三個。下面我們將分別講解這三個類。
1)HashSet
HashSet是Set介面的最常見的實現類了。其底層是基於Hash演算法進行儲存相關元素的。
下面是HashSet的部分原始碼:
/** * Constructs a new, empty set; the backing <tt>HashMap</tt> instance has * default initial capacity (16) and load factor (0.75). */ public HashSet() { map = new HashMap<E,Object>(); }
你看到了什麼,沒錯,對於HashSet的底層就是基於HashMap來實現的哦。
我們都知道在HashMap中的key是不允許重複的,你換個角度看看,那不就是說Set集合嗎?
這裡唯一一個需要處理的就是那個Map的value弄成一個固定值即可。
看來一切水到渠成啊~哈哈~這裡的就是Map中的Key。
對於HashMap和Hash演算法的講解會在下面出現,先彆著急,下面繼續講解HashSet。
下面講解一下HashSet使用和理解中容易出現的誤區:
a.HashSet中存放null值
HashSet中時允許出入null值的,但是在HashSet中僅僅能夠存入一個null值哦。
b.HashSet中儲存元素的位置是固定的
HashSet中儲存的元素的是無序的,這個沒什麼好說的,但是由於HashSet底層是基於Hash演算法實現的,使用了hashcode,
所以HashSet中相應的元素的位置是固定的哦。
c.遍歷HashSet的幾種方法
具體的方法不說了,請看下面的程式碼:
package com.yonyou.test; import java.util.HashSet; import java.util.Iterator; import java.util.Set; /** * 測試類 * @author 小浩 * @建立日期 2015-3-2 */ public class Test{ public static void main(String[] args) { Set<String> set=new HashSet<String>(); set.add("Hello"); set.add("world"); set.add("Hello"); //遍歷集合的第一種方法,使用陣列的方法 String[] strArray=new String[set.size()]; strArray=set.toArray(strArray); for(String str:strArray)//此處也可以使用for(int i=0;i<strArray.length;i++) { System.out.println(str); } //遍歷集合的第二中方法,使用set集合直接遍歷 for(String str:set) { System.out.println(str); } //遍歷集合的第三種方法,使用iterator迭代器的方法 Iterator<String> iterator=set.iterator(); while(iterator.hasNext()) { System.out.println(iterator.next()); } } }
2)LinkHashSet
LinkHashSet不僅是Set介面的子介面而且還是上面HashSet介面的子介面。
檢視LinkedHashSet的部分原始碼如下:
...
* @see Hashtable * @since 1.4 */ public class LinkedHashSet<E> extends HashSet<E> implements Set<E>, Cloneable, java.io.Serializable { private static final long serialVersionUID = -2851667679971038690L; /** * Constructs a new, empty linked hash set with the specified initial
...
這裡就可以發現Set介面時HashSet介面的一個子介面了吧~
通過檢視LinkedHashSet的原始碼可以發現,其底層是基於LinkedHashMap來實現的哦。
對於LinkedHashSet而言,它和HashSet主要區別在於LinkedHashSet中儲存的元素是在雜湊演算法的基礎上增加了
鏈式表的結構。
3)TreeSet
TreeSet是一種排序二叉樹。存入Set集合中的值,會按照值的大小進行相關的排序操作。底層演算法是基於紅黑樹來實現的。
TreeSet和HashSet的主要區別在於TreeSet中的元素會按照相關的值進行排序~
TreeSet和HashSet的區別和聯絡
1. HashSet是通過HashMap實現的,TreeSet是通過TreeMap實現的,只不過Set用的只是Map的key
2. Map的key和Set都有一個共同的特性就是集合的唯一性.TreeMap更是多了一個排序的功能.
3. hashCode和equal()是HashMap用的, 因為無需排序所以只需要關注定位和唯一性即可.
a. hashCode是用來計算hash值的,hash值是用來確定hash表索引的.
b. hash表中的一個索引處存放的是一張連結串列, 所以還要通過equal方法迴圈比較鏈上的每一個物件
才可以真正定位到鍵值對應的Entry.
c. put時,如果hash表中沒定位到,就在連結串列前加一個Entry,如果定位到了,則更換Entry中的value,並返回舊value
4. 由於TreeMap需要排序,所以需要一個Comparator為鍵值進行大小比較.當然也是用Comparator定位的.
a. Comparator可以在建立TreeMap時指定
b. 如果建立時沒有確定,那麼就會使用key.compareTo()方法,這就要求key必須實現Comparable介面.
c. TreeMap是使用Tree資料結構實現的,所以使用compare介面就可以完成定位了.
下面是一個使用TreeSet的例項:
package com.yonyou.test; import java.util.Iterator; import java.util.TreeSet; /** * TreeSet測試類 * @author 小浩 * @建立日期 2015-4-3 */ public class Test{ public static void main(String[] args) { //String實體類中實現Comparable介面,所以在初始化TreeSet的時候, //無需傳入比較器 TreeSet<String> treeSet=new TreeSet<String>(); treeSet.add("d"); treeSet.add("c"); treeSet.add("b"); treeSet.add("a"); Iterator<String> iterator=treeSet.iterator(); while(iterator.hasNext()) { System.out.println(iterator.next()); } } }
3)Map介面
說到Map介面的話大家也許在熟悉不過了。Map介面實現的是一組Key-Value的鍵值對的組合。 Map中的每個成員方法由一個關鍵字(key)和一個值(value)構成。Map介面不直接繼承於Collection介面(需要注意啦),因為它包裝的是一組成對的“鍵-值”物件的集合,而且在Map介面的集合中也不能有重複的key出現,因為每個鍵只能與一個成員元素相對應。在我們的日常的開發專案中,我們無時無刻不在使用者Map介面及其實現類。Map有兩種比較常用的實現:HashMap和TreeMap等。HashMap也用到了雜湊碼的演算法,以便快速查詢一個鍵,TreeMap則是對鍵按序存放,因此它便有一些擴充套件的方法,比如firstKey(),lastKey()等,你還可以從TreeMap中指定一個範圍以取得其子Map。鍵和值的關聯很簡單,用pub(Object key,Object value)方法即可將一個鍵與一個值物件相關聯。用get(Object key)可得到與此key物件所對應的值物件。
另外前邊已經說明了,Set介面的底層是基於Map介面實現的。Set中儲存的值,其實就是Map中的key,它們都是不允許重複的。
Map介面的部分原始碼如下:
* @see Collection * @see Set * @since 1.2 */ public interface Map<K,V> { // Query Operations /** * Returns the number of key-value mappings in this map. If the
為了更好的理解上面的內容,這裡我們有必要簡單瞭解一下Hash演算法的內容,由於篇幅限制,這裡就不具體的講解Hash演算法的實現過程了。如果感興趣的可以參考:http://www.cnblogs.com/xiohao/p/4389672.html
接下來我們講解Map介面的常見實現類HashMap、TreeMap、LinkedHashMap、Properties(繼承HashTable)以及老版本的HashTable等。
3)HashMap
HashMap實現了Map、CloneMap、Serializable三個介面,並且繼承自AbstractMap類。
HashMap基於hash陣列實現,若key的hash值相同則使用連結串列方式進行儲存。
新建一個HashMap時,預設的話會初始化一個大小為16,負載因子為0.75的空的HashMap
/** * Constructs an empty <tt>HashMap</tt> with the default initial capacity * (16) and the default load factor (0.75). */ public HashMap() { this.loadFactor = DEFAULT_LOAD_FACTOR; threshold = (int)(DEFAULT_INITIAL_CAPACITY * DEFAULT_LOAD_FACTOR); table = new Entry[DEFAULT_INITIAL_CAPACITY]; init(); }
下面是一個HashMap中還存在一個內部類Entry,用於連結串列的儲存,如
static class Entry<K,V> implements Map.Entry<K,V> {
final K key;
V value;
Entry<K,V> next;
final int hash;
......
面程式碼其實告訴我們Entry是一個結點,它持有下一個元素的引用,這樣就構成了一個連結串列。
那麼,整體上來說HashMap底層就是使用這樣一個資料結構來實現的。
我們提到使用Hash,但是Hash值如何與元素的儲存建立關係呢?(Hash演算法)
在資料結構課中我們學習過Hash的簡單演算法,就是給你一個Hash因子,通過對該元素的hashCode簡單的求餘,來實現對其快速的定位和索引。
在HashMap中有這樣的程式碼:
- /**
- * Returns index for hash code h.
- */
- static int indexFor(int h, int length) {
- return h & (length-1);
- }
/**
* Returns index for hash code h.
*/
static int indexFor(int h, int length) {
return h & (length-1);
}
這個方法在HashMap中非常重要,凡是與查詢、新增、刪除有關的方法中都有呼叫該方法,為什麼這麼短的一個程式碼使用率這麼高?根據程式碼註釋我們知道,這個方法是根據hashCode及當前table的長度(陣列的長度,不是map的size)得到該元素應該存放的位置,或者在table中的索引。
現在我們需要看一下當資料量已經超過初始定義的負載因子時,HashMap如何處理?
在HashMap中當資料量很多時,並且已經達到了負載限度時,會重新做一次雜湊,也就是說會再雜湊。呼叫的方法為resize(),並且java預設傳入的引數為2*table.length。先看一下JDK原始碼:
- /**
- * Rehashes the contents of this map into a new array with a
- * larger capacity. This method is called automatically when the
- * number of keys in this map reaches its threshold.
- *
- * If current capacity is MAXIMUM_CAPACITY, this method does not
- * resize the map, but sets threshold to Integer.MAX_VALUE.
- * This has the effect of preventing future calls.
- *
- * @param newCapacity the new capacity, MUST be a power of two;
- * must be greater than current capacity unless current
- * capacity is MAXIMUM_CAPACITY (in which case value
- * is irrelevant).
- */
- void resize(int newCapacity) {
- Entry[] oldTable = table;
- int oldCapacity = oldTable.length;
- if (oldCapacity == MAXIMUM_CAPACITY) {
- threshold = Integer.MAX_VALUE;
- return;
- }
- Entry[] newTable = new Entry[newCapacity];
- transfer(newTable);
- table = newTable;
- threshold = (int)(newCapacity * loadFactor);
- }
- /**
- * Transfers all entries from current table to newTable.
- */
- void transfer(Entry[] newTable) {
- Entry[] src = table;
- int newCapacity = newTable.length;
- for (int j = 0; j < src.length; j++) {
- Entry<K,V> e = src[j];
- if (e != null) {
- src[j] = null;
- do {
- Entry<K,V> next = e.next;
- int i = indexFor(e.hash, newCapacity);
- e.next = newTable[i];
- newTable[i] = e;
- e = next;
- } while (e != null);
- }
- }
- }
看到這裡我們會發現resize(再雜湊)的工作量是不是很大啊。再雜湊是重新建一個指定容量的陣列,然後將每個元素重新計算它要放的位置,這個工作量確實是很大的。
這裡就產生了一個很重要的問題,那就是怎麼讓雜湊表的分佈比較均勻,也就是說怎麼讓它即不會成為一個單連結串列(極限情況,每個key的hash值都集中到了一起),又不會使hash空間過大(導致記憶體浪費)?
上面兩個問題一個是解決了怎麼計算hash值快速存取,一個是怎麼實現再雜湊,何時需要再雜湊。快速存取的前提是元素分佈均勻,不至於集中到一點,再雜湊是元素過於零散,導致不斷的重新構建表。
那麼在第一個問題中我們看到了這樣一個程式碼return h & (length-1);在第二個問題中我們說過內部呼叫傳入的值為2*table.length;並且預設情況下HashMap的大小為一個16的數字,除了預設構造提供大小為16的空間外,如果我們使用
public HashMap(int initialCapacity, float loadFactor)
上面的構造方法,我們會發現這樣的程式碼:
- // Find a power of 2 >= initialCapacity
- int capacity = 1;
- while (capacity < initialCapacity)
- capacity <<= 1;
- ……
- table = new Entry[capacity];
// Find a power of 2 >= initialCapacity
int capacity = 1;
while (capacity < initialCapacity)
capacity <<= 1;
……
table = new Entry[capacity];
也就是說當我們傳入1000時,它並沒有給我們構造一個容量為1000的雜湊表,而是構建了一個容量為1024大小的雜湊表。
從整體上我們發現一個問題,那就是無論什麼情況HashMap中雜湊表的容量總是2的n次方的一個數。並且有這樣一個公式:
當length=2^n時,hashcode & (length-1) == hashcode % length
也就是這一點驗證了第一個問題,hash索引值的計算方法其實就是對雜湊因子求餘。只有大小為2的n次方時,那樣的計算才成立,所以HashMap為我們維護了一個這樣大小的一個雜湊表。(位運算速度比取模運算快的多)
c) HashMap的使用方法:
我在很多程式碼中都用到了HashMap,原因是首先它符合儲存關聯資料的要求,其次它的存取速度快,這是一個選擇的問題。
比較重要的是HashMap的遍歷方法,在我的部落格中有專門寫到HashMap的遍歷方法:http://blog.csdn.net/tsyj810883979/article/details/6746274
Ø LinkedHashMap的特點、實現機制及使用方法
a) LinkedHashMap的特點:
LinkedHashMap繼承自HashMap並且實現了Map介面。和HashMap一樣,LinkedHashMap允許key和value均為null。
於該資料結構和HashMap一樣使用到hash演算法,因此它不能保證對映的順序,尤其是不能保證順序持久不變(再雜湊)。
如果你想在多執行緒中使用,那麼需要使用Collections.synchronizedMap方法進行外部同步。
LinkedHashMap與HashMap的不同之處在於,LinkedHashMap維護者執行於所有條目的雙重連結列表,此連結列表可以是插入順序或者訪問順序。
b) LinkedHashMap的實現機制:
無法總結下去,在網上看到這樣一篇文章:http://zhangshixi.iteye.com/blog/673789
感覺真的沒辦法總結下去了。
Ø HashMap與Hashtable的區別:
Hashtable實現Map介面,繼承自古老的Dictionary類,實現一個key-value的鍵值對映表。任何非空的(key-value)均可以放入其中。
區別主要有三點:
1. Hashtable是基於陳舊的Dictionary實現的,而HashMap是基於Java1.2引進的Map介面實現的;
2. Hashtable是執行緒安全的,而HashMap是非執行緒安全的,我們可以使用外部同步的方法解決這個問題。
3. HashMap可以允許你在列表中放一個key值為null的元素,並且可以有任意多value為null,而Hashtable不允許鍵或者值為null。
Ø WeakHashMap的特點:
我沒有使用過這個類。網摘:WeakHashMap是一種改進的HashMap,它對key實行“弱引用”,如果一個key不再被外部所引用,那麼該key可以被GC回收。(後續使用後進行總結)
Ø Properties及TreeMap在後續內容裡進行總結。
就先這樣吧,Java集合框架的講解就先到這裡了啊~