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接口的源碼會發現:
...<br> * @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>. <br> ...
這里也就知道為什么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.example.demo.test; import java.util.ArrayList; import java.util.Iterator; import java.util.List; /** * Created by Luxd on 2019/6/28. */ 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.example.demo.test; import java.util.LinkedList; import java.util.List; /** * Created by Luxd on 2019/6/28. */ 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的源碼會發現下面的內容
...<br> * @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 <br> ...
在這里也會看到set接口時Collection接口的子接口吧~哈
此外需要說明一點,在set接口中的不重復是由特殊要求的。
舉一個例子:對象A和對象B,本來是不同的兩個對象,正常情況下它們是能夠放入到Set里面的,但是
如果對象A和B的都重寫了hashcode和equals方法,並且重寫后的hashcode和equals方法是相同的話。那么A和B是不能同時放入到
Set集合中去的,也就是Set集合中的去重和hashcode與equals方法直接相關。
為了更好的理解,請看下面的例子:
package com.example.demo.test; import java.util.HashSet; import java.util.Set; /** * Created by Luxd on 2019/6/28. */ 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.example.demo.test; import java.util.HashSet; import java.util.Iterator; import java.util.Set; /** * Created by Luxd on 2019/6/28. */ 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的部分源碼如下:
...<br> * @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 <br> ...
這里就可以發現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.example.demo.test; import java.util.Iterator; import java.util.TreeSet; /** * Created by Luxd on 2019/6/28. */ 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> {<br> final K key;<br> V value;<br> Entry<K,V> next;<br> final int hash;<br> ......
以上代碼其實告訴我們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); }
這個方法在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];
也就是說當我們傳入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在后續內容里進行總結。