1. 程式人生 > >Java基礎知識回顧之四 ----- 集合List、Map和Set

Java基礎知識回顧之四 ----- 集合List、Map和Set

linked 訪問速度 因此 比較 foreach循環 代碼示例 的區別 不同的 寫法

前言

在上一篇中回顧了Java的三大特性:封裝、繼承和多態。本篇則來介紹下集合。

集合介紹

我們在進行Java程序開發的時候,除了最常用的基礎數據類型和String對象外,也經常會用到集合相關類。
集合類存放的都是對象的引用,而非對象本身,出於表達上的便利,我們稱集合中的對象就是指集合中對象的引用。
集合類型主要有3種:List、Set、和Map。
它們之間的關系可用下圖來表示:

技術分享圖片

註:Map不是collections的子類,但是它們完全整合在集合中了!

List

List 接口是繼承於 Collection接口並定義 一個允許重復項的有序集合。該接口不但能夠對列表的一部分進行處理,還添加了面向位置的操作。

一般來說,我們在單線程中主要使用的List是ArrayList和LinkedList來實現,多線程則是使用Vector或者使用Collections.sychronizedList來裝飾一個集合。
這三個的解釋如下:

  • ArrayList:內部是通過數組實現的,它允許對元素進行快隨機訪問。當從ArrayList的中間位置插入或者刪除元素時,需要對數組進行復制、移動、代價比較高。因此,它適合隨機查找和遍歷,不適合插入和刪除。
  • LinkedList: 則是鏈表結構存儲數據的,很適合數據的動態插入和刪除,隨機訪問和遍歷速度比較慢。另外,他還提供了List接口中沒有定義的方法,專門用於操作表頭和表尾元素,可以當作堆棧、隊列和雙向隊列使用。
  • Vector: 通過數組實現的,不同的是它支持線程的同步。訪問速度ArrayList慢。

它們的用法如下:

List list1 = new ArrayList();
List list2 = new LinkedList();
List list3 = new Vector();
List list4=Collections.synchronizedList(new ArrayList())

在了解了它們的用法之後,我們來看看為什麽說使用ArrayList比LinkedList查詢快,使用LinkedList比ArrayList新增和刪除快!
這裏也用以下代碼來進行說明,順便也將Vector進行比較。

代碼示例:

    private final static int count=50000;

    private static ArrayList arrayList = new ArrayList<>();  
    private static LinkedList linkedList = new LinkedList<>();  
    private static Vector vector = new Vector<>();  

    public static void main(String[] args) {
        insertList(arrayList);
        insertList(linkedList);
        insertList(vector);

        System.out.println("--------------------");

        readList(arrayList);
        readList(linkedList);
        readList(vector);

        System.out.println("--------------------");

        delList(arrayList);
        delList(linkedList);
        delList(vector);
    }

    private  static void insertList(List list){   
         long start=System.currentTimeMillis();   
         Object o = new Object();   
         for(int i=0;i<count;i++){   
             list.add(0, o);   
         }
        System.out.println(getName(list)+"插入"+count+"條數據,耗時:"+(System.currentTimeMillis()-start)+"ms");
     }   

    private  static void readList(List list){   
         long start=System.currentTimeMillis();   
         Object o = new Object();   
         for(int i = 0 ; i < count ; i++){  
                list.get(i);  
            }
        System.out.println(getName(list)+"查詢"+count+"條數據,耗時:"+(System.currentTimeMillis()-start)+"ms");
     }  

    private  static void delList(List list){   
         long start=System.currentTimeMillis();   
         Object o = new Object();   
         for(int i = 0 ; i < count ; i++){  
             list.remove(0);   
            }
        System.out.println(getName(list)+"刪除"+count+"條數據,耗時:"+(System.currentTimeMillis()-start)+"ms");
     }  

    private static String getName(List list) {  
        String name = "";  
        if(list instanceof ArrayList){  
            name = "ArrayList";  
        }  
        else if(list instanceof LinkedList){  
            name = "LinkedList";  
        }  
        else if(list instanceof Vector){  
            name = "Vector";  
        }  
        return name;  
    }  

輸出結果:

    ArrayList插入50000條數據,耗時:281ms
    LinkedList插入50000條數據,耗時:2ms
    Vector插入50000條數據,耗時:274ms
    --------------------
    ArrayList查詢50000條數據,耗時:1ms
    LinkedList查詢50000條數據,耗時:1060ms
    Vector查詢50000條數據,耗時:2ms
    --------------------
    ArrayList刪除50000條數據,耗時:143ms
    LinkedList刪除50000條數據,耗時:1ms
    Vector刪除50000條數據,耗時:137ms

從上述結果中,可以明顯看出ArrayList和LinkedList在新增、刪除和查詢性能上的區別。

在集合中,我們一般用於存儲數據。不過有時在有多個集合的時候,我們想將這幾個集合做合集、交集、差集和並集的操作。在List中,這些方法已經封裝好了,我們無需在進行編寫相應的代碼,直接拿來使用就行。
代碼示例如下:

/**
     * 合集
     * @param ls1
     * @param ls2
     * @return
     */
    private static List<String> addAll(List<String> ls1,List<String>ls2){
        ls1.addAll(ls2);
        return ls1;
    }

    /**
     * 交集 (retainAll 會刪除 ls1在ls2中沒有的元素)
     * @param ls1
     * @param ls2
     * @return
     */
    private static List<String> retainAll(List<String> ls1,List<String>ls2){
        ls1.retainAll(ls2);
        return ls1;
    }

    /**
     * 差集 (刪除ls2中沒有ls1中的元素)
     * @param ls1
     * @param ls2
     * @return
     */
    private static List<String> removeAll(List<String> ls1,List<String>ls2){
        ls1.removeAll(ls2);
        return ls1;
    }

    /**
     * 無重復的並集 (ls1和ls2中並集,並無重復)
     * @param ls1
     * @param ls2
     * @return
     */
    private static List<String> andAll(List<String> ls1,List<String>ls2){
        //刪除在ls1中出現的元素
        ls2.removeAll(ls1);
        //將剩余的ls2中的元素添加到ls1中
        ls1.addAll(ls2);
        return ls1;
    }

當然,經常用到的還有對List進行遍歷。
List數組遍歷主要有這三種方法,普通的for循環,增強for循環(jdk1.5之後出現),和Iterator(叠代器)。

代碼示例:

     List<String> list=new ArrayList<String>();
     list.add("a");
     list.add("b");
     list.add("c");

     for(int i=0;i<list.size();i++){
         System.out.println(list.get(i));
     }

     for (String str : list) {  
         System.out.println(str);
     }

     Iterator<String> iterator=list.iterator();
     while(iterator.hasNext())
     {
         System.out.println(iterator.next());
     }

說明:普通的for循環和增強for循環區別不大,主要區別在於普通的for循環可以獲取集合的下標,而增強for循環則不可以。但增強for循環寫起來方法,如果不需要獲取具體集合的下標,推薦使用增強for循環。至於Iterator(叠代器)這種也是無法獲取數據下標,但是該方法可以不用擔心在遍歷的過程中會集合的長度發生改變。也就是在遍歷的時候對集合進行增加和刪除。

<阿裏巴巴Java開發手冊>中,對於集合操作也有這種說明。

不要在 foreach 循環裏進行元素的 remove / add 操作。 remove 元素請使用Iterator方式,如果並發操作,需要對 Iterator 對象加鎖。

那麽為什麽不要使用 foreach 循環進行元素的 remove / add 操作呢?
我們這裏可以簡單的做下驗證。

代碼示例:

    List<String> list = new ArrayList<String>();
         list.add("1");
         list.add("2");
         System.out.println("list遍歷之前:"+list);
         for (String item : list) {
           if ("2".equals(item)) {
            list.remove(item);
            //如果這裏不適用break的話,會直接報錯的
            break; 
         }
       } 
        System.out.println("list遍歷之後:"+list);

        List<String> list1 = new ArrayList<String>();
         list1.add("1");
         list1.add("2");
        System.out.println("list1遍歷之前:"+list1);
         Iterator<String> iterator = list1.iterator();
         while (iterator.hasNext()) {
             String item = iterator.next();
             if ("2".equals(item)) {
                 iterator.remove();
             }
         }
         System.out.println("list1遍歷之後:"+list1);

輸出結果:

    list遍歷之前:[1, 2]
    list遍歷之後:[1]
    list1遍歷之前:[1, 2]
    list1遍歷之後:[1]

註意:上述代碼中,在對list進行for循環遍歷的時候,加了break,

上述示例中,都正確的打印我們想要的數據,不過在foreach循環中,我在其中是加上了break。如果不加break,就會直接拋出ConcurrentModificationException異常!

Map

Map 接口並不是 Collection 接口的繼承。Map提供key到value的映射。一個Map中不能包含相同的key,每個key只能映射一個value。Map接口提供3種集合的視圖,Map的內容可以被當作一組key集合,一組value集合,或者一組key-value映射。

Map接口主要由HashMap、TreeMap、LinkedHashMap、Hashtable和ConcurrentHashMap這幾個類實現。
它們的解釋如下:

  • HashMap: HashMap的鍵是根據HashCode來獲取,所以根據鍵可以很快的獲取相應的值。不過它的鍵對象是不可以重復的,它允許鍵為Null,但是最多只能有一條記錄,不過卻是可以允許多條記錄的值為Null。因為HashMap是非線程安全的,所以它的效率很高。
  • TreeMap:可以將保存的記錄根據鍵進行排序,默認是按鍵值的升序排序(自然順序)。也可以指定排序的比較器,當用Iterator遍歷TreeMap時,得到的記錄是排過序的。它也是不允許key值為空,並且不是線程安全的。
  • LinkedHashMap:LinkedHashMap基本和HashMap一致。不過區別在與LinkedHashMap是維護一個雙鏈表,可以將裏面的數據按寫入 的順序讀出。可以認為LinkedHashMap是HashMap+LinkedList。即它既使用HashMap操作數據結構,又使用LinkedList維護插入元素的先後順序。它也不是線程安全的。
  • Hashtable:Hashtable與HashMap類似,可以說是HashMap的線程安全版。不過它是不允許記錄的鍵或者值為null。因為它支持線程的同步,是線程安全的,所以也導致了Hashtale在效率較低。
  • ConcurrentHashMap: ConcurrentHashMap在Java 1.5作為Hashtable的替代選擇新引入的。使用鎖分段技術技術來保證線程安全的,可以看作是Hashtable的升級版。

在工作中,我們使用得最多的Map應該是HashMap。不過有時在使用Map的時候,需要進行自然順序排序。這裏我們就可以使用TreeMap,而不必自己實現這個功能。TreeMap的使用和HashMap差不多。不過需要註意的是TreeMap是不允許key為null。 這裏簡單的介紹下TreeMap的使用。

代碼示例:

    Map<String,Object> hashMap=new HashMap<String,Object>();
        hashMap.put("a", 1);
        hashMap.put("c", 3);
        hashMap.put("b", 2);
        System.out.println("HashMap:"+hashMap);

        Map<String,Object> treeMap=new TreeMap<String,Object>();
        treeMap.put("a", 1);
        treeMap.put("c", 3);
        treeMap.put("b", 2);
        System.out.println("TreeMap:"+treeMap);

輸出結果:

    HashMap:{b=2, c=3, a=1}
    TreeMap:{a=1, b=2, c=3}

上述中可以看出HashMap是無序的,TreeMap是有序的。

在使用Map的時候,也會對Map進行遍歷。一般遍歷Map的key和value有三種方式:
第一種通過Map.keySet遍歷;
第二種通過Map.entrySet使用iterator遍歷;
第三種是通過Map.entrySet進行遍歷。
使用如下:

 Map<String, String> map = new HashMap<String, String>();
 for (String key : map.keySet()) {
       System.out.println("key= "+ key + " and value= " + map.get(key));
      }
  Iterator<Map.Entry<String, String>> it = map.entrySet().iterator();
    while (it.hasNext()) {
       Map.Entry<String, String> entry = it.next();
       System.out.println("key= " + entry.getKey() + " and value= " + entry.getValue());
      }

      for (Map.Entry<String, String> entry : map.entrySet()) {
       System.out.println("key= " + entry.getKey() + " and value= " + entry.getValue());
      }

如果只想獲取Map中value的話,可以使用foreach對Map.values()進行遍歷。

for (String v : map.values()) {
     System.out.println("value= " + v);
 }

在上述遍歷中,我們最多使用的是第一種Map.keySet,因為寫起來比較簡單。不過在容量大的時候,推薦使用第三種,效率會更高!

Set

Set是一種不包含重復的元素的Collection,即任意的兩個元素e1和e2都有e1.equals(e2)=false,Set最多有一個null元素。因為Set是一個抽象的接口,所以是不能直接實例化一個set對象。Set s = new Set() 這種寫法是錯誤的。

Set接口主要是由HashSet、TreeSet和LinkedHashSet來實現。
它們簡單的使用如下:

Set hashSet = new HashSet();
Set treeSet = new TreeSet();
Set linkedSet = new LinkedHashSet();

因為Set是無法擁有重復元素的,所以也經常用它來去重。例如在一個list集合中有兩條相同的數據,想去掉一條,這時便可以使用Set的機制來去重。
代碼示例:

    public static void set(){
        List<String> list = new ArrayList<String>();
        list.add("Java");
        list.add("C");
        list.add("C++");
        list.add("JavaScript");
        list.add("Java");
        Set<String> set = new HashSet<String>();
        for (int i = 0; i < list.size(); i++) {
            String items = list.get(i);
            System.out.println("items:"+items);
            if (!set.add(items)) {
                System.out.println("重復的數據: " + items);
            }
        }
        System.out.println("list:"+list);
    }

輸出結果:

items:Java
items:C
items:C++
items:JavaScript
items:Java
重復的數據: Java
list:[Java, C, C++, JavaScript, Java]

註意:如果是將對象進行去重的話,是需要重寫set中的equals和hashcode方法的。

總結

關於集合中List、Map、Set這三個的總結如下:

  • List:List和數組類似,可以動態增長,根據實際存儲的數據的長度自動增長List的長度。查找元素效率高,插入刪除效率低,因為會引起其他元素位置改變 <實現類有ArrayList,LinkedList,Vector>

    • ArrayList:非線程安全,適合隨機查找和遍歷,不適合插入和刪除。
    • LinkedList : 非線程安全,適合插入和刪除,不適合查找。
    • Vector : 線程安全。不過不推薦。
  • Map:一個key到value的映射的類 。

    • HashMap:非線程安全,鍵和值都允許有null值存在。
    • TreeMap:非線程安全,按自然順序或自定義順序遍歷鍵(key)。
    • LinkedHashMap:非線程安全,維護一個雙鏈表,可以將裏面的數據按寫入的順序讀出。寫入比HashMap強,新增和刪除比HashMap差。
    • Hashtable:線程安全,鍵和值不允許有null值存在。不推薦使用。
    • ConcurrentHashMap:線程安全,Hashtable的升級版。推薦多線程使用。
  • Set:不允許重復的數據 。檢索效率低下,刪除和插入效率高。

    • HashSet: 非線程安全、無序、數據可為空。
    • TreeSet: 非線程安全、有序、數據不可為空。
    • LinkedHashSet:非線程安全、無序、數據可為空。寫入比HashSet強,新增和刪除比HashSet差。

    到此,本文結束,謝謝閱讀。

版權聲明:
作者:虛無境
博客園出處:http://www.cnblogs.com/xuwujing
CSDN出處:http://blog.csdn.net/qazwsxpcm    
個人博客出處:http://www.panchengming.com
原創不易,轉載請標明出處,謝謝!

Java基礎知識回顧之四 ----- 集合List、Map和Set