天天看點

Java容器

問題及答案來源自《Java程式員面試筆試寶典》第四章 Java基礎知識 4.9容器

1、Java Collections架構是什麼?

Java Collections架構中包含了大量集合接口以及這些接口的實作類和操作它們的算法(比如排序、查找、反轉、替換等)

具體而言主要提供了List(清單)、Queue(隊列)、Set(集合)、Stack(棧)、Map(映射表)等資料結構

其中List(清單)、Queue(隊列)、Set(集合)、Stack(棧)都繼承自Collection接口

Collection的作用隻是提供維護一組對象的基本接口而已

Collection是整個集合架構的基礎,它裡面存儲一組對象,表示不同類型的Collections

Collection架構類圖如下:

下面主要介紹Set、List和Map這三個接口:

Set:表示數學意義上的集合,主要特點是集合中的元素不能重複,是以存入Set的每個元素都必須定義equals方法來確定對象的

唯一性。該接口有兩個實作類:HashSet和TreeSet,TreeSet中的元素是有序的

List:又稱為有序的Collection,是按按對象進入的順序儲存對象,是以它對清單中每個元素的插入和删除位置進行精确的控制。

同時它可以儲存重複的對象。LinkedList、ArrayList和Vector都實作了List接口

Map:提供一個從鍵映射到值得資料結構,用于儲存鍵值對,值可以重複但鍵不能重複。實作它的類:HashMap、TreeMap、

LinkedHashMap、WeakHashMap、IdentityHashMap。雖然它們實作了相同的接口,但是執行效率不是完全相同。具體而言,

HashMap基于散清單實作,可以快速查詢元素,LinkedHashMap采用清單來維護内部的順序,TreeMap基于紅黑樹的資料結構

實作,内部元素是按需排列的

2、什麼是疊代器?

疊代器是什麼:

疊代器(Iterator)是一個對象,它的工作是周遊并選擇序列中的對象,它提供了一種通路容器對象中的元素,而又不必暴露

該對象内部細節的方法。通過疊代器,開發人員不需要了解容器底部結構就可以實作對容器的周遊

疊代器的使用:

  • 使用iterator()方法傳回一個Iterator,然後通過Iterator的next方法傳回第一個元素
  • 使用Iterator的hasNext方法判斷是否還有元素,如果有可以使用next方法擷取下一個元素

執行個體如下:

1 public class IteratorDemo {
 2     public static void main(String[] args) {
 3         Collection<String> c = new ArrayList<String>();
 4         c.add("1");
 5         c.add("2");
 6         c.add("3");
 7         c.add("4");
 8         c.add("5");
 9         c.add("6");
10         Iterator<String> it = c.iterator();    // 擷取集合的疊代器對象
11         while(it.hasNext()){    // 反複判斷有沒有下一個元素
12             String s = it.next();    // 取出下一個元素
13             System.out.println(s);
14         }
15     }
16 }      

并發修改異常:

疊代的正常用法中我們要盡量避免在疊代過程中為集合添加/删除資料。否則會報錯,原因是Java抛出了并發修改異常

疊代過程中并發修改異常的原因為:

疊代器中”記憶”的集合長度與集合中實際長度不同,而導緻出現索引與實際元素不符甚至無限循環的情況發生

是以在使用Iterator時,避免類似操作,for循環底層為疊代器實作,是以也需要避免類似操作

有些疊代器避免了這樣的問題,如ListIterator,但該類并不通用也不常用,實際開發中很少使用,隻需要簡單了解

1 // java規定: 如果一個集合使用疊代器周遊,那麼在周遊的過程中不允許修改集合的長度(增加或删除)
 2 public class ConcurrentModificationExceptionDemo {
 3     public static void main(String[] args) {
 4         Collection<String> c = new ArrayList<String>();
 5         c.add("1");
 6         c.add("2");
 7         c.add("itcast");
 8         c.add("4");
 9         c.add("5");
10         c.add("6");
11         Iterator<String> it = c.iterator();    // 擷取集合的疊代器對象
12         while(it.hasNext()){    // 反複判斷有沒有下一個元素
13             String s = it.next();    // 取出下一個元素
14             if("itcast".equals(s)){
15                 // 如果相等添加一個大寫ITCAST
16                 c.add("ITCAST");    // 報異常 => ConcurrentModificationException
17             }
18             System.out.println(s);
19         }
20     }
21 }      

并發修改異常解決方法(單線程):

把要删除或添加的元素儲存到一個集合中,周遊結束後調用removeAll方法或addAll方法來進行删除或添加

并發修改異常解決方法(多線程):

  • 使用線程安全的容器:比如ConcurrentHashMap和CopyOnWriteArrayList等
  • 使用疊代器周遊容器時對容器的操作放到synchronized代碼塊中

引申 - Iterator和ListIterator有什麼差別?

Iterator隻能正向周遊,适用于擷取移除元素。而ListIterator繼承自Iterator,專門針對List,可以從兩個方向來周遊List,

同時也支援元素的修改

3、ArrayList、Vector和LinkedList有什麼差別?

三者共同點:

三者均在java.util包中,均可以作為伸縮數組,即可以動态改變長度的數組

ArrayList和Vector的相同點:

都基于存儲元素的Object[] array來實作的,會在記憶體中開辟一塊連續的空間來存儲,由于資料存儲是連續的,是以它們

支援用下标通路元素,通路資料速度比較快、插入資料比較慢(要移動元素)

ArrayList和Vector的不同點: 

  • Vector擴充空間預設擴充為原來的2倍(每次擴充空間的大小是可以設定的)
  • ArrayList擴充空間預設擴充為用來的1.5倍(沒有提供方法來設定空間擴充的方法)
  • Vector是線程安全的(Vector的大部分方法是直接或間接同步的)
  • ArrayList不是線程安全的(沒有一個ArrayList的方法是同步的)

關于LinkedList:

LinkedList是采用雙向清單來實作的,對資料的索引需要從清單頭開始周遊,是以用于随機通路則效率比較低,但是插入元素

時不需要對資料進行移動,故插入效率比較高,同時LinkedList是非線程安全的容器

實際使用選擇:

  • 當對資料的主要操作為索引或隻在集合的末端增加、删除元素時:ArrayList或Vector
  • 當對資料的操作主要為指定位置的插入或删除操作時:LinkedList
  • 當在多線程中使用容器時(即多個線程會通路容器):Vector

4、HashMap、HashTable、TreeMap和WeakHashMap有哪些差別?

Map是什麼:

Java為資料結構中的映射定義了一個接口java.util.Map,它包括三個實作類:HashMap、HashTable和TreeMap

Map是用來存儲鍵值對的資料結構,在數組中通過數組下标來對其内容索引的,而在Map中則是通過對象來進行

索引,用來索引的對象叫做key,其對于的對象叫做value

HashMap:

HashMap是最常用的Map,是根據鍵的HashCode值存儲資料,根據鍵可以直接獲得它的值,具有很快的通路速度

HashMap和HashTable的差別:

  • HashMap是HashTable的輕量級實作(非線程安全的實作),都實作了Map接口
  • HashMap中允許存在null鍵值(最多隻能有一條),而HashTable中不允許存在null鍵值的
  • HashMap把HashTable的contains方法改成了containsValue和containsKey
  • HashTable是線程安全的,而HashMap不支援線程的同步(不是線程安全的)
  • HashMap效率比HashTable高(但多線程通路HashMap時開發人員要提供額外的同步機制)
  • HashTable使用Enumeration,而HashMap使用Iterator

HashMap和TreeMap和LinkedHashMap:

  • HashMap裡存入的鍵值對在取出時是随機的,一般而已在Map中插入、删除和定位元素最好用HashMap
  • TreeMap實作了SortMap接口,能把它儲存的記錄根據鍵排序,是以需要排序的鍵值對可以使用TreeMap
  • LinkedHashMap是HashMap的子類,如果需要輸出的順序和輸入的順序相同,可以使用LinkedHashMap

WeakHashMap和HashMap: 

WeakHashMap裡面的key采用的是弱引用的方式,隻要key不再被外部引用就可以被垃圾回收期回收,而HashMap

中的key采用的是強引用的方式,就算沒有被外部引用,但隻有這個key從HashMap删除後才能被垃圾回收器回收

在HashTable上下文中,同步指什麼?

同步意味着一個時間點隻能有一個線程可以修改hash表,任何線程在執行HashTable的更新操作前都需要擷取

對象鎖,其他線程則等待鎖的釋放

如何實作HashTable的同步?

可以通過Map m = Collections.synchronizedMap(new HashMap())來達到同步的效果。具體而言,該方法傳回一個

同步的Map,該Map封裝了底層的HashMap的所有方法,使得底層的HashMap即使是在多線程的環境中也是安全的

5、用自定義類型作為HashMap或HashTable的key需要注意哪些問題?

HashMap和HashTable是用來存放鍵值對的一種容器,在使用這個容器時不能存儲重複的鍵,也就是說每個鍵隻能

映射一個值,當有重複的鍵時不會建立新的映射關系,而會使用先前的鍵值 

但是當用自定義的類的對象作為HashMap中的key時會給人造成一種假象:key是可以重複的,示例如下:

1 class Person {
 2     String id;
 3     String name;
 4 
 5     public Person(String id, String name) {
 6         this.id = id;
 7         this.name = name;
 8     }
 9 
10     public String toString() {
11         return "id=" + id + ", name=" + name;
12     }
13 
14 }
15 
16 public class HashMapTest {
17     public static void main(String[] args) {
18         System.out.println("Use String as key: ");
19         HashMap<Person, String> hm = new HashMap<Person, String>();
20         Person p1 = new Person("111", "name1");
21         Person p2 = new Person("111", "name1");
22         hm.put(p1, "address1");
23         hm.put(p2, "address2");
24 
25         Iterator iter = hm.entrySet().iterator();
26         while (iter.hasNext()) {
27             Map.Entry entry = (Map.Entry) iter.next();
28             Person key = (Person) entry.getKey();
29             String val = (String) entry.getValue();
30             System.out.println(key + " - " + val);
31             // 輸出結果如下:
32             // id=111, name=name1 - address1
33             // id=111, name=name1 - address2
34         }
35     }
36 }      

上面這種現象的原因是:

雖然這兩個對象有着同樣的内容,但是存在在記憶體中不同的位址裡,向hashMap中添加對象時調用equals方法

的傳回值為false,HashMap會認為它們是兩個不同的對象,就會分别建立不同的映射關系,是以為了實作在向

HashMap中添加鍵值對時可根據對象的内容來判斷兩個對象是否相等,就需要重寫equals方法和hashCode方法:

1 class Person {
 2     String id;
 3     String name;
 4     
 5     public int hashCode(){
 6         return id.hashCode();
 7     }
 8     
 9     public Person(String id, String name) {
10         this.id = id;
11         this.name = name;
12     }
13 
14     public String toString() {
15         return "id=" + id + ", name=" + name;
16     }
17     
18     public boolean equals(Object obj){
19         Person p = (Person)obj;
20         if(p.id.equals(this.id)){
21             return true;
22         } else{
23             return false;
24         }
25     }
26 
27 }
28 
29 public class HashMapTest {
30     public static void main(String[] args) {
31         System.out.println("Use String as key: ");
32         HashMap<Person, String> hm = new HashMap<Person, String>();
33         Person p1 = new Person("111", "name1");
34         Person p2 = new Person("111", "name1");
35         hm.put(p1, "address1");
36         hm.put(p2, "address2");
37 
38         Iterator iter = hm.entrySet().iterator();
39         while (iter.hasNext()) {
40             Map.Entry entry = (Map.Entry) iter.next();
41             Person key = (Person) entry.getKey();
42             String val = (String) entry.getValue();
43             System.out.println(key + " - " + val);
44             // 輸出結果如下:
45             // id=111, name=name1 - address2
46         }
47     }
48 }      

總結 - 開發者在使用自定義類作為HashMap的key時,需要注意以下幾點: 

  • 如果想根據對象的相關屬性來自定義對象是否相等的邏輯就要重寫equals方法和hashCode方法
  • 當自定義類的對象作為HashMap(hashTable)的key時,最好把這個類設定為不可變類
  • 從HashMap的工作原理可以看出如果兩個對象相等,那麼這兩個對象有相同的hashCode值,反之則不成立

6、Collection和Collections有什麼差別?

Collection是一個集合接口,它提供了對集合對象進行基本操作的通用接口方法。實作該接口的類主要有List和Set,

該接口的主要設計目标是為各種具體的集合提供最大化統一的操作方式

Collections是針對集合類的一個包裝類,它提供了一系列的靜态方法以實作對各種集合的搜尋、排序、線程安全化

等操作,其中絕大多數方法都是用來處理線性表,Collections類如同工具類,不能被執行個體化(類似Math類)

使用示例:

1 public class CollectionsDemo {
 2     public static void main(String[] args) {
 3         List<Integer> list = new LinkedList<Integer>();
 4         int array[] = {1, 7, 3, 2};
 5         for(int num: array){
 6             list.add(new Integer(num));
 7         }
 8         
 9         for(int i=0; i< array.length; i++){
10             System.out.print(list.get(i));
11         }
12         System.out.println();
13         // 1732
14         
15         Collections.sort(list);
16         for(int i=0; i< array.length; i++){
17             System.out.print(list.get(i));
18         }
19         System.out.println();
20         // 1237
21     }
22 }      

too young too simple sometimes native!