LinkedList它實現的基礎是雙向鏈表,所以在插入刪除方面具備性能優點,它也能夠用來實現stack和queue。順便說一句,Java容器框架中有一個遺留的類Stack,它是基於Vector實現的,被大師們評價爲「幼稚的設計」,咱們不要用。
LinkedList主要有三個屬性:java
int size
Node<E> first
Node<E> last
也就是經過一個鏈表把size個Node從頭串到尾。而Node就是一個類的節點包裝類,有item,有prev和next。圖示以下:算法
LinkedList也是List,所以一樣具備List的那些操做,這一點跟ArrayList同樣,所以下面咱們只介紹它不同的部分。
LinkedList同時也實現了Deque,所以它具備Deque的方法,以下面的12個:數組
First Element (Head) Last Element (Tail)
Insert addFirst(e)
offerFirst(e)
addLast(e)
offerLast(e)
Remove removeFirst()
pollFirst()
removeLast()
pollLast()
Examine getFirst()
peekFirst()
getLast()
peekLast()
若是咱們把它當作FIFO先進先出的,就成了一個Queue了,Deque擴展了Queue,有幾個方法是徹底等同的:安全
Queue Method Equivalent Deque Method
add(e)
addLast(e)
offer(e)
offerLast(e)
remove()
removeFirst()
poll()
pollFirst()
element()
getFirst()
peek()
peekFirst()
咱們若是把它當作FILO先進後出的,那就成了一個stack,事實上有幾個方法也是同樣的功能:數據結構
Stack Method Equivalent Deque Method
push(e)
addFirst(e)
pop()
removeFirst()
peek()
peekFirst()
下面咱們仍是用一個例子把上面的12個方法簡單演示一下,使用的方法有多線程
addFirst(e) offerFirst(e) addLast(e) offerLast(e)
removeFirst() pollFirst() removeLast() pollLast()
getFirst() peekFirst() getLast() peekLast()
代碼以下(LinkedListTest1.java):
public class LinkedListTest1 {
public static void main(String[] args) {
LinkedList<String> list1 = new LinkedList<>();
list1.addFirst("北京");
list1.offerFirst("上海");
list1.addLast("廣州");
list1.offerLast("深圳");
list1.offer("杭州");
list1.add("蘇州");
list1.push("廈門");
System.out.println(list1);
System.out.println(list1.get(2));
System.out.println(list1.getLast());
System.out.println(list1.getFirst());
System.out.println(list1.peek());
System.out.println(list1.peekFirst());
System.out.println(list1.peekLast());
System.out.println(list1);
list1.remove();
list1.removeLast();
list1.removeFirst();
list1.remove("深圳");
list1.poll();
list1.pollLast();
list1.pop();
System.out.println(list1);
}
}
你們本身運行一下,很簡單。app
咱們提到過,List是有次序的,次序就是放進去的次序。若是要另外排序呢?能夠的。咱們看一個簡單的例子,代碼以下(ListSort.java):框架
public class ListSort {
public static void main(String[] args) {
List<Student> list = new ArrayList<>();
list.add(new Student(2,"b","very good"));
list.add(new Student(1,"a","good"));
list.add(new Student(3,"c","basic"));
System.out.println(list);
list.sort((s1,s2)->s1.name.compareTo(s2.name));
System.out.println(list);
}
}
運行結果:dom
[b-very good, a-good, c-basic]
[a-good, b-very good, c-basic]
從運行結果能夠看出,list從新按照咱們給的規則(名字排序)排序了,實現一個Comparator就能夠了。
咱們這邊自定義的是值的比較規則,而排序算法是沒有地方選擇的,不一樣的JDK版本內部的排序算法是不同的,JDK6和以前的版本,都是用的merge sort(歸併排序算法),JDK7及以後用的Tim排序算法。Tim排序算法是結合了歸併排序和插入排序的新算法,對各類數據排列都比較好,而merge排序算法要對基本排好的數據再排序會很好,而有的數據效果比較差,性能接近o(n2)。ide
public class ListSort {
public static void main(String[] args) {
long start;
long end;
int bound = 10;
List<Integer> list1 = new ArrayList<>();
for (int i=0; i<bound; i++){
list1.add(i);
}
start=System.currentTimeMillis();
System.out.println(list1);
list1.sort((i1,i2)->i1-i2);
end=System.currentTimeMillis();
System.out.println(list1);
System.out.println(end-start);
Random r = new Random();
List<Integer> list2 = new ArrayList<>();
for (int i=0; i<bound; i++){
list2.add(r.nextInt(bound));
}
start=System.currentTimeMillis();
System.out.println(list2);
list2.sort((i1,i2)->i1-i2);
end=System.currentTimeMillis();
System.out.println(list2);
System.out.println(end-start);
List<Integer> list3 = new ArrayList<>();
for (int i=bound-1; i>=0; i--){
list3.add(i);
}
start=System.currentTimeMillis();
System.out.println(list3);
list3.sort((i1,i2)->i1-i2);
end=System.currentTimeMillis();
System.out.println(list3);
System.out.println(end-start);
}
}
結果爲:
[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
442
[7, 8, 1, 7, 3, 0, 8, 0, 6, 8]
[0, 0, 1, 3, 6, 7, 7, 8, 8, 8]
2
[9, 8, 7, 6, 5, 4, 3, 2, 1, 0]
[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
2
不用管具體的數值,只是感受一下原始數據不一樣排列狀況下,排序算法性能差別很大。
咱們嘴巴上總是說LinkedList和ArrayList之間性能的差別,如今我用一個例子演示一下,這個例子不是我寫的,我直接從《Thinking in Java》裏抄過來的,因此版權屬於Bruce Eckel。代碼以下(ListPerformance.java):
public class ListPerformance {
private static final int REPS = 100;
private abstract static class Tester {
String name;
int size;
Tester(String name, int size) {
this.name = name;
this.size = size;
}
abstract void test(List a);
}
private static Tester[] tests = {new Tester("get", 300) {
void test(List a) {
for (int i = 0; i < REPS; i++) {
for (int j = 0; j < a.size(); j++) {
a.get(j);
}
}
}
}, new Tester("iteration", 300) {
void test(List a) {
for (int i = 0; i < REPS; i++) {
Iterator it = a.iterator();
while (it.hasNext()) it.next();
}
}
}, new Tester("insert", 1000) {
void test(List a) {
int half = a.size() / 2;
String s = "test";
ListIterator it = a.listIterator(half);
for (int i = 0; i < size * 10; i++) {
it.add(s);
}
}
}, new Tester("remove", 5000) {
void test(List a) {
ListIterator it = a.listIterator(3);
while (it.hasNext()) {
it.next();
it.remove();
}
}
},
};
public static void test(List a) {
System.out.println("Testing " + a.getClass().getName());
for (int i = 0; i < tests.length; i++) {
fill(a, tests[i].size);
System.out.print(tests[i].name);
long t1 = System.currentTimeMillis();
tests[i].test(a);
long t2 = System.currentTimeMillis();
System.out.print(":" + (t2 - t1)+" ms ");
}
}
public static Collection fill(Collection c, int size) {
for (int i = 0; i < size; i++) {
c.add(Integer.toString(i));
}
return c;
}
public static void main(String[] args) {
test(new ArrayList());
System.out.println();
test(new LinkedList());
}
}
運行以後的結果是:
Testing java.util.ArrayList
get:4 ms iteration:6 ms insert:6 ms remove:28 ms
Testing java.util.LinkedList
get:13 ms iteration:5 ms insert:2 ms remove:4 ms
結果印證了咱們的說法,ArrayList確實get比較塊,LinkedList確實刪除增長比較快,而iterator二者差很少的。
Bruce Eckel還提供了一個更加專業的測試,結果以下:
--- Array as List ---
size get set
10 130 183
100 130 164
1000 129 165
10000 129 165
--------------------- ArrayList ---------------------
size add get set iteradd insert remove
10 121 139 191 435 3952 446
100 72 141 191 247 3934 296
1000 98 141 194 839 2202 923
10000 122 144 190 6880 14042 7333
--------------------- LinkedList ---------------------
size add get set iteradd insert remove
10 182 164 198 658 366 262
100 106 202 230 457 108 201
1000 133 1289 1353 430 136 239
10000 172 13648 13187 435 255 239
----------------------- Vector -----------------------
size add get set iteradd insert remove
10 129 145 187 290 3635 253
100 72 144 190 263 3691 292
1000 99 145 193 846 2162 927
10000 108 145 186 6871 14730 7135
-------------------- Queue tests --------------------
size addFirst addLast rmFirst rmLast
10 199 163 251 253
100 98 92 180 179
1000 99 93 216 212
10000 111 109 262 384
按照Bruce Eckel的建議,首選ArrayList,當確認要對數據進行頻繁的增長刪除的時候,就用LinkedList。
好,咱們講過了List,咱們接着講講Map。
Java容器框架中,Map是獨立的一類。咱們講講用得最多的HashMap。接口是Map,還有個抽象類AbstractMap,具體的實現類是HashMap。
HashMap 是Java的鍵值對數據類型容器。它根據鍵的哈希值(hashCode)來存儲數據,訪問速度高,性能是常數,沒有順序。HashMap 容許鍵值爲空和記錄爲空,非線程安全。
先看一個簡單的例子,代碼以下(HashMapTest.java):
public class HashMapTest {
public static void main(String[] args) {
Map<String, String> map = new HashMap<>();
map.put("BJC", "北京首都機場");
map.put("PDX", "上海浦東機場");
map.put("GZB", "廣州白雲機場");
map.put("SZX", "深圳寶安機場");
String usage = map.get("SZX");
System.out.println("Map: " + map);
System.out.println("Map Size: " + map.size());
System.out.println("Map is empty: " + map.isEmpty());
System.out.println("Map contains PDX key: " + map.containsKey("PDX"));
System.out.println("Usage: " + usage);
System.out.println("removed: " + map.remove("SZX"));
}
}
程序很簡單,把一個個key-value放入HashMap中,而後執行get(),size(),isEmpty,containsKey(),remove()等操做。
結果以下:
Map: {SZX=深圳寶安機場, PDX=上海浦東機場, BJC=北京首都機場, GZB=廣州白雲機場}
Map Size: 4
Map is empty: false
Map contains PDX key: true
Usage: 深圳寶安機場
removed: 深圳寶安機場
咱們翻一下JDK,看看HashMap的介紹。
public class HashMap
extends AbstractMap
implements Map, Cloneable, Serializable
Hash table 實現了Map 接口,容許null values and the null key,不是同步的。這個類不保證數據的次序,特別地,也不保證數據次序的恆定,也就是說,第一次查找的時候是這個次序,下一次可能就變了。
HashMap有兩個參數影響性能: initial capacity and load factor. The capacity是bucket桶的數量,默認值是16,load factor是hash表多滿後自動擴容,0.75是默認值。每次擴容是增長一倍容量,擴容能夠很耗時間。對capacity和load factor,要有一個平衡,合理兼顧空間佔用和時間消耗。
HashMap不是同步的,若是要同步須要在外面本身實現,或者用Map m = Collections.synchronizedMap(new HashMap(…));轉換成同步的。
跟Collection同樣,多線程的狀況下,若是一個 iterator遍歷中間HashMap有結構性變化,就會fail-fast,拋出 ConcurrentModificationException。,v>,v>,v>
看看HashMap的構造函數:
HashMap()
Constructs an empty HashMap with the default initial capacity (16) and the default load factor (0.75).
HashMap(int initialCapacity)
Constructs an empty HashMap with the specified initial capacity and the default load factor (0.75).
HashMap(int initialCapacity, float loadFactor)
Constructs an empty HashMap with the specified initial capacity and load factor.
HashMap(Map<? extends K,? extends V> m)
Constructs a new HashMap with the same mappings as the specified Map.
對HashMap裏面的方法不一一舉例了,咱們看一個小例子,代碼以下(HashMapTest2.java):
public class HashMapTest2 {
public static void main(String[] args) {
HashMap<String, String> map = new HashMap<>();
map.put("BJC", "北京首都機場");
map.put("PDX", "上海虹橋機場");
map.put("GZB", "廣州白雲機場");
map.put("SZX", "深圳寶安機場");
Set<String> keys = map.keySet();
keys.forEach(System.out::println);
for (String key : map.keySet()) {
System.out.println("value=" +map.get(key));
}
Set<Map.Entry<String, String>> entries = map.entrySet();
entries.forEach((Map.Entry<String, String> entry) -> {
String key = entry.getKey();
String value = entry.getValue();
System.out.println("key=" + key + ", value=" + value);
});
map.replace("PDX", "上海浦東機場");
Iterator<Map.Entry<String, String>> iterator = map.entrySet().iterator();
while (iterator.hasNext()) {
Map.Entry<String, String> entry = iterator.next();
System.out.println("key=" + entry.getKey() + ", value=" + entry.getValue());
map.merge(entry.getKey(), "有限公司", (oldVal, newVal) -> oldVal + newVal);
map.compute(entry.getKey(), (key, oldVal) -> oldVal + "有限公司");
}
List<String> valuesList = new ArrayList<String>(map.values());
for(String str:valuesList){
System.out.println(str);
}
}
}
程序簡單。你們熟悉一下幾種遍歷方式,還有merge(), compute(),和map.values()。
有了這些基礎,接下來咱們要講更多的東西,幫助你們更好地理解HashMap。咱們先看看數據結構中介紹的一點理論知識。
簡單來說,HashMap底下用的數據結構是數組+鏈表(紅黑樹)。Key值經過一個hash函數映射到數組的下標,重複的下標經過鏈表(紅黑樹)解決衝突。術語中把此處的數組叫作bucket桶。
有一個圖,很形象地說明了HashMap的結構。
table是一個數組,數組每一個位置(就是每個桶)保存一個元素,或者是跟着一個鏈表或者紅黑樹(開頭都是鏈表,數據量>8以後,就自動轉成紅黑樹)。查找數據先定位在數組哪一個位置,再順藤摸瓜找到在鏈表或者紅黑樹上的哪個具體節點。
咱們知道,查找數據來講,其實數組是最快的,由於能夠根據下標直接定位。因此哈希的核心思路是用一個函數將查找的key值轉換成一個整數值,而後以此爲下標,把key值存放在數組中。這樣下次再找的時候,還用這個函數,直接定位了。因此定位數組下標,性能是o(1),若是定位的這個數組後面跟了一個鏈表,要接着找具體的節點,性能是o(l),其中l是鏈表長度。天然,鏈表長度越短越好,意味着須要這個hash函數衝突越少越好。因此,HashMap的性能關鍵在於要找到一個合適的函數。
要寫出一個像樣子的哈希函數,在《Effective Java》這本書中,Joshua Bloch給了一個指導:
1 給int變量result賦予一個非零值常量,如17
2 爲對象內每一個有意義的域f(即每一個能夠作equals()操做的域)計算出一個int散列碼c:
域類型 計算
boolean c=(f?0:1)
byte、char、short或int c=(int)f
long c=(int)(f^(f>>>32))
float c=Float.floatToIntBits(f);
double long l = Double.doubleToLongBits(f);
c=(int)(l^(l>>>32))
Object,其equals()調用這個域的equals() c=f.hashCode()
數組 對每一個元素應用上述規則
3. 合併計算散列碼:result = 37 * result + c;
4. 返回result。
5. 檢查hashCode()最後生成的結果,確保相同的對象有相同的散列碼。
《Thinking in Java》裏面給了一個簡單的例子,我拷貝到這裏,版權屬於Bruce Eckel。代碼以下(CountedString.java):
public class CountedString {
private static List<String> created = new ArrayList<String>();
private String s;
private int id = 0;
public CountedString(String str) {
s = str;
created.add(s);
// id is the total number of instances
// of this string in use by CountedString:
for(String s2 : created)
if(s2.equals(s))
id++;
}
public String toString() {
return "String: " + s + " id: " + id + " hashCode(): " + hashCode();
}
public int hashCode() {
// The very simple approach:
// return s.hashCode() * id;
// Using Joshua Bloch's recipe:
int result = 17;
result = 37 * result + s.hashCode();
result = 37 * result + id;
return result;
}
public boolean equals(Object o) {
return o instanceof CountedString &&
s.equals(((CountedString)o).s) &&
id == ((CountedString)o).id;
}
public static void main(String[] args) {
Map<CountedString,Integer> map = new HashMap<CountedString,Integer>();
CountedString[] cs = new CountedString[5];
for(int i = 0; i < cs.length; i++) {
cs[i] = new CountedString("hi");
map.put(cs[i], i); // Autobox int -> Integer
}
System.out.println(map);
for(CountedString cstring : cs) {
System.out.println("Looking up " + cstring);
System.out.println(map.get(cstring));
}
}
}
運行結果以下:
{String: hi id: 4 hashCode(): 146450=3, String: hi id: 5 hashCode(): 146451=4, String: hi id: 2 hashCode(): 146448=1, String: hi id: 3 hashCode(): 146449=2, String: hi id: 1 hashCode(): 146447=0}
Looking up String: hi id: 1 hashCode(): 146447
0
Looking up String: hi id: 2 hashCode(): 146448
1
Looking up String: hi id: 3 hashCode(): 146449
2
Looking up String: hi id: 4 hashCode(): 146450
3
Looking up String: hi id: 5 hashCode(): 146451
4
你們能夠看出對給定的key值生成的不同的hashcode。
講完了HashMap,我再簡單介紹一下Set。你們或許以爲奇怪,Set不是Collection裏面的一員嗎?沒什麼不放在更前面談?我這麼講是由於Set底層是基於Map實現的,因此講授放在哪一邊都是能夠的。說白了,Set是Map的一層馬甲。Set不能有重複數據。
Set是實現Collection接口的,除了Collection的常規操做,還有一些與集合相關的操做,並,交,補等等。
看一個簡單的例子,代碼以下(HashSetTest.java):
public class HashSetTest {
public static void main(String[] args) {
Set<String> s1 = new HashSet<>();
s1.add("北京首都機場");
s1.add("上海虹橋機場");
s1.add("廣州白雲機場");
s1.add("深圳寶安機場");
s1.add("上海虹橋機場");
Set<String> s2 = new HashSet<>();
s2.add("上海虹橋機場");
s2.add("長沙黃花機場");
s2.add("杭州蕭山機場");
for(String s : s1) {
System.out.print(s+" ");
}
System.out.println("");
Iterator iterator = s2.iterator();
while (iterator.hasNext()) {
System.out.print(iterator.next()+" ");
}
System.out.println("");
doUnion(s1, s2);
doIntersection(s1, s2);
doDifference(s1, s2);
isSubset(s1, s2);
}
public static void doUnion(Set<String> s1, Set<String> s2) {
Set<String> s1Unions2 = new HashSet<>(s1);
s1Unions2.addAll(s2);
System.out.println("s1 union s2: " + s1Unions2);
}
public static void doIntersection(Set<String> s1, Set<String> s2) {
Set<String> s1Intersections2 = new HashSet<>(s1);
s1Intersections2.retainAll(s2);
System.out.println("s1 intersection s2: " + s1Intersections2);
}
public static void doDifference(Set<String> s1, Set<String> s2) {
Set<String> s1Differences2 = new HashSet<>(s1);
s1Differences2.removeAll(s2);
Set<String> s2Differences1 = new HashSet<>(s2);
s2Differences1.removeAll(s1);
System.out.println("s1 difference s2: " + s1Differences2);
System.out.println("s2 difference s1: " + s2Differences1);
}
public static void isSubset(Set<String> s1, Set<String> s2) {
System.out.println("s2 is subset s1: " + s1.containsAll(s2));
System.out.println("s1 is subset s2: " + s2.containsAll(s1));
}
}
簡單,不解釋了。你們只要注意s1.add("上海虹橋機場");執行了兩遍,可是最後Set裏面只有一個。由於判斷這是同一個對象。
這兒要多提一下,世界上沒有兩片徹底同樣的樹葉,兩個字符串,怎麼會認爲是同一個呢?這是由於判斷是否爲同一個採用的方法是調用equals()方法。因此對自定義的類,須要從新寫equals()方法,不然就是直接用的Object自帶的equals()方法,那是比較的引用地址,確定就不一樣了,而咱們須要比較的是對象裏面的內容。
看一個例子。之前講過,再試一遍。
先寫一個自定義類Student:
public class Student {
int id = 0;
String name = "";
String mark = "";
public Student() {
}
public Student(int id, String name, String mark) {
this.id = id;
this.name = name;
this.mark = mark;
}
public void setId(int id) {
this.id = id;
}
public void setName(String name) {
this.name = name;
}
public void setMark(String mark) {
this.mark = mark;
}
public String toString() {
return name + "-" + mark;
}
}
再用一個測試程序看看,代碼以下(HashSetTest2.java):
public class HashSetTest2 {
public static void main(String[] args) {
Set<Student> s1 = new HashSet<>();
s1.add(new Student(1,"a","aaa"));
s1.add(new Student(2,"b","bbb"));
s1.add(new Student(3,"c","ccc"));
s1.add(new Student(1,"a","aaa"));
System.out.print(s1);
}
}
運行結果以下:
[c-ccc, a-aaa, b-bbb, a-aaa]
注意了,aaa添加了兩遍,在Set中也有兩份。這不是重複了嗎?形成這種狀況的緣由就是重複不重複,是看的equals()。所以,咱們必須改寫equals(),Student程序增長一個方法以下:
public boolean equals (Object obj){
if(this==obj){
return true;
}
if(!(obj instanceof Student)){
return false;
}
Student s=(Student) obj;
if(this.id==s.id&&this.name.equals(s.name)&&this.mark.equals(s.mark)) {
return true;
}
return false;
}
咱們重寫equals()覆蓋Object默認的方法,比較對象內部的內容。
再次運行,結果是:
[c-ccc, a-aaa, b-bbb, a-aaa]
沒有變化!這是怎麼回事呢?咱們回顧一下HashMap的查找方法,第一步是比較hashcode,相同的話,就在同一個bucket桶裏找相同的元素,這個時候纔會調用在equals()。咱們的程序,在hashcode這一層就被擋住了,不會調用equals(),因此,咱們對Student類,還須要重寫hashCode(),Student程序修改一下,增長hashCode():
public int hashCode(){
return this.name.hashCode();
}
再運行,就出現了咱們想要的結果。同時也印證了咱們的說法,Set實際上是基於Map的。JDK說明中,對HashSet的第一句話就是:This class implements the Set interface, backed by a hash table (actually a HashMap instance)。
好,到此爲止,咱們就把幾個基本的類介紹過了,ArrayList,LinkedList,HashMap,HashSet。普通應用主要用它們幾個,通常也認爲容器類是一門實用的語言最重要的類。你們要好好掌握這些基本的使用方法。