java集合詳解(轉自樓道小組)

 

 

 

 

 

 

 

集合 html

 

版本號:1.0 java

 

 

 

 

 

 

 

 

 

做者:huangdos 程序員

 日期: 2006 6 06 算法


    數據庫

 

摘要內容 編程

Java裏面最重要,最經常使用也就是集會一部分了。可以用好集合和理解好集合對於作Java程序的開發擁有無比的好處。本文詳細解釋了關於Java中的集合是如何實現的,以及他們的實現原理。 設計模式

 

關鍵字: 數組

Collection , List ,Set , Map , 集合,框架。安全



集合

1         集合框架

1.1         集合框架概述

1.1.1         容器簡介

到目前爲止,咱們已經學習瞭如何建立多個不一樣的對象,定義了這些對象之後,咱們就能夠利用它們來作一些有意義的事情。

       舉例來講,假設要存儲許多僱員,不一樣的僱員的區別僅在於僱員的身份證號。咱們能夠經過身份證號來順序存儲每一個僱員,可是在內存中實現呢?是否是要準備足夠的內存來存儲1000個僱員,而後再將這些僱員逐一插入?若是已經插入了500條記錄,這時須要插入一個身份證號較低的新僱員,該怎麼辦呢?是在內存中將500條記錄所有下移後,再從開頭插入新的記錄? 仍是建立一個映射來記住每一個對象的位置?當決定如何存儲對象的集合時,必須考慮以下問題。

       對於對象集合,必須執行的操做主要如下三種:

u       添加新的對象

u       刪除對象

u       查找對象

咱們必須肯定如何將新的對象添加到集合中。能夠將對象添加到集合的末尾、開頭或者中間的某個邏輯位置。

從集合中刪除一個對象後,對象集合中現有對象會有什麼影響呢?可能必須將內存移來移去,或者就在現有對象所駐留的內存位置下一個「洞」。

       在內存中創建對象集合後,必須肯定如何定位特定對象。可創建一種機制,利用該機制可根據某些搜索條件(例如身份證號)直接定位到目標對象;不然,便須要遍歷集合中的每一個對象,直到找到要查找的對象爲止。

       前面你們已經學習過了數組。數組的做用是能夠存取一組數據。可是它卻存在一些缺點,使得沒法使用它來比較方便快捷的完成上述應用場景的要求。

1.         首先,在不少數狀況下面,咱們須要可以存儲一組數據的容器,這一點雖然數組能夠實現,可是若是咱們須要存儲的數據的個數多少並不肯定。好比說:咱們須要在容器裏面存儲某個應用系統的當前的全部的在線用戶信息,而當前的在線用戶信息是時刻均可能在變化的。 也就是說,咱們須要一種存儲數據的容器,它可以自動的改變這個容器的所能存放的數據數量的大小。這一點上,若是使用數組來存儲的話,就顯得十分的笨拙。

2.         咱們再假設這樣一種場景:假定一個購物網站,通過一段時間的運行,咱們已經存儲了一系列的購物清單了,購物清單中有商品信息。若是咱們想要知道這段時間裏面有多少種商品被銷售出去了。那麼咱們就須要一個容器可以自動的過濾掉購物清單中的關於商品的重複信息。若是使用數組,這也是很難實現的。

3.         最後再想一想,咱們常常會遇到這種狀況,我知道某我的的賬號名稱,但願可以進一步瞭解這我的的其餘的一些信息。也就是說,咱們在一個地方存放一些用戶信息,咱們但願可以經過用戶的賬號來查找到對應的該用戶的其餘的一些信息。再舉個查字典例子:假設咱們但願使用一個容器來存放單詞以及對於這個單詞的解釋,而當咱們想要查找某個單詞的意思的時候,可以根據提供的單詞在這個容器中找到對應的單詞的解釋。若是使用數組來實現的話,就更加的困難了。

爲解決這些問題,Java裏面就設計了容器集合,不一樣的容器集合以不一樣的格式保存對象。

 

數學背景

在常見用法中,集合(collection和數學上直觀的集(set的概念是相同的。集是一個惟一項組,也就是說組中沒有重複項。實際上,集合框架包含了一個 Set 接口和許多具體的 Set 類。但正式的集概念卻比 Java 技術提早了一個世紀,那時英國數學家 George Boole 按邏輯正式的定義了集的概念。大部分人在小學時經過咱們熟悉的維恩圖引入的集的交集的並學到過一些集的理論。 

集的基本屬性以下:

u       集內只包含每項的一個實例

u       集能夠是有限的,也能夠是無限的

u       能夠定義抽象概念

集不只是邏輯學、數學和計算機科學的基礎,對於商業和系統的平常應用來講,它也很實用。鏈接池這一律念就是數據庫服務器的一個開放鏈接集。Web 服務器必須管理客戶機和鏈接集。文件描述符提供了操做系統中另外一個集的示例。

映射是一種特別的集。它是一種對(pair)集,每一個對錶示一個元素到另外一元素的單向映射。一些映射示例有:

u       IP 地址到域名(DNS)的映射

u       關鍵字到數據庫記錄的映射

u       字典(詞到含義的映射)

u       2 進制到 10 進制轉換的映射

就像集同樣,映射背後的思想比 Java 編程語言早的多,甚至比計算機科學還早。而Java中的Map 就是映射的一種表現形式。

1.1.2        容器的分類

既然您已經具有了一些集的理論,您應該可以更輕鬆的理解集合框架集合框架由一組用來操做對象的接口組成。不一樣接口描述不一樣類型的組。在很大程度上,一旦您理解了接口,您就理解了框架。雖然您總要建立接口特定的實現,但訪問實際集合的方法應該限制在接口方法的使用上;所以,容許您更改基本的數據結構而沒必要改變其它代碼。框架接口層次結構以下圖所示。

 

Java容器類類庫的用途是「保存對象」,並將其劃分爲兩個不一樣的概念:

1)  Collection 一組對立的元素,一般這些元素都服從某種規則。List必須保持元素特定的順序,而Set 不能有重複元素。

2)  Map 一組 成對的「鍵值對」對象。初看起來這彷佛應該是一個Collection ,其元素是成對的對象,可是這樣的設計實現起來太笨拙了,因而咱們將Map明確的提取出來造成一個獨立的概念。另外一方面,若是使用Collection 表示Map的部份內容,會便於查看此部份內容。所以Map同樣容易擴展成多維Map ,無需增長新的概念,只要讓Map中的鍵值對的每一個「值」也是一個Map便可。

CollectionMap的區別在於容器中每一個位置保存的元素個數。Collection 每一個位置只能保存一個元素(對象)。此類容器包括:List ,它以特定的順序保存一組元素;Set 則是元素不能重複。

Map保存的是「鍵值對」,就像一個小型數據庫。咱們能夠經過「鍵」找到該鍵對應的「值」。

u     Collection 對象之間沒有指定的順序,容許重複元素。

u     Set   對象之間沒有指定的順序,不容許重複元素

u     List  對象之間有指定的順序,容許重複元素,並引入位置下標。

u     Map   接口用於保存關鍵字(Key)和數值(Value)的集合,集合中的每一個對象加入時都提供數值和關鍵字。Map 接口既不繼承 Set 也不繼承 Collection

 

ListSetMap共同的實現基礎是Object數組

除了四個歷史集合類外,Java 2 框架還引入了六個集合實現,以下表所示。

 

 

接口

實現

歷史集合類

Set

HashSet

 

 

TreeSet

 

List

ArrayList

Vector

 

LinkedList

Stack

Map

HashMap

Hashtable

 

TreeMap

Properties

 

這裏沒有 Collection 接口的實現,接下來咱們再來看一下下面的這張關於集合框架的大圖:

       這張圖看起來有點嚇人,熟悉以後就會發現其實只有三種容器:MapListSet ,它們各自有兩個三個實現版本。經常使用的容器用黑色粗線框表示。

點線方框表明「接口」,虛線方框表明抽象類,而實線方框表明普通類(即具體類,而非抽象類)。虛線箭頭指出一個特定的類實現了一個接口(在抽象類的狀況下,則是「部分」實現了那個接口)。實線箭頭指出一個類可生成箭頭指向的那個類的對象。例如任何集合( Collection )都能產生一個迭代器( Iterator ),而一個List 除了能生成一個ListIterator 列表迭代器)外,還能生成一個普通迭代器,由於List 正是從集合繼承來的.

 

 

1.2         Collection

1.2.1         經常使用方法

Collection 接口用於表示任何對象或元素組。想要儘量以常規方式處理一組元素時,就使用這一接口。Collection 在前面的大圖也能夠看出,它是ListSet 的父類。而且它自己也是一個接口。它定義了做爲集合所應該擁有的一些方法。以下:

 

注意:

集合必須只有對象,集合中的元素不能是基本數據類型。

Collection接口支持如添加和除去等基本操做。設法除去一個元素時,若是這個元素存在,除去的僅僅是集合中此元素的一個實例。

u     boolean add(Object element)

u     boolean remove(Object element)

Collection 接口還支持查詢操做:

u     int size()

u     boolean isEmpty()

u     boolean contains(Object element)

u     Iterator iterator()

組操做 Collection 接口支持的其它操做,要麼是做用於元素組的任務,要麼是同時做用於整個集合的任務。

u     boolean containsAll(Collection collection)

u     boolean addAll(Collection collection)

u     void clear()

u     void removeAll(Collection collection)

u     void retainAll(Collection collection)

containsAll() 方法容許您查找當前集合是否包含了另外一個集合的全部元素,即另外一個集合是不是當前集合的子集。其他方法是可選的,由於特定的集合可能不支持集合更改。 addAll() 方法確保另外一個集合中的全部元素都被添加到當前的集合中,一般稱爲 clear() 方法從當前集合中除去全部元素。 removeAll() 方法相似於 clear() ,但只除去了元素的一個子集。 retainAll() 方法相似於 removeAll() 方法,不過可能感到它所作的與前面正好相反:它從當前集合中除去不屬於另外一個集合的元素,即

 

咱們看一個簡單的例子,來了解一下集合類的基本方法的使用:

import java.util.*;

public class CollectionToArray {

    public static void main(String[] args) {     

       Collection collection1=new ArrayList();//建立一個集合對象

       collection1.add("000");//添加對象到Collection集合中

       collection1.add("111");

       collection1.add("222");

       System.out.println("集合collection1的大小:"+collection1.size());

       System.out.println("集合collection1的內容:"+collection1);

       collection1.remove("000");//從集合collection1中移除掉 "000" 這個對象

       System.out.println("集合collection1移除 000 後的內容:"+collection1);

       System.out.println("集合collection1中是否包含000 "+collection1.contains("000"));

       System.out.println("集合collection1中是否包含111 "+collection1.contains("111"));

       Collection collection2=new ArrayList();

       collection2.addAll(collection1);//collection1 集合中的元素所有都加到collection2

       System.out.println("集合collection2的內容:"+collection2);

       collection2.clear();//清空集合 collection1 中的元素

       System.out.println("集合collection2是否爲空 "+collection2.isEmpty());

       //將集合collection1轉化爲數組

       Object s[]= collection1.toArray();

       for(int i=0;i<s.length;i++){

           System.out.println(s[i]);

       }

    }

}

運行結果爲:

集合collection1的大小:3

集合collection1的內容:[000, 111, 222]

集合collection1移除 000 後的內容:[111, 222]

集合collection1中是否包含000 false

集合collection1中是否包含111 true

集合collection2的內容:[111, 222]

集合collection2是否爲空 true

111

222

 

這裏須要注意的是,Collection 它僅僅只是一個接口,而咱們真正使用的時候,確是建立該接口的一個實現類。作爲集合的接口,它定義了全部屬於集合的類所都應該具備的一些方法。

ArrayList (列表)類是集合類的一種實現方式。

 

這裏須要一提的是,由於Collection的實現基礎是數組,因此有轉換爲Object數組的方法:

u     Object[] toArray()

u     Object[] toArray(Object[] a)

其中第二個方法Object[] toArray(Object[] a) 的參數 a 應該是集合中全部存放的對象的類的父類。

 

1.2.2         迭代器

任何容器類,都必須有某種方式能夠將東西放進去,而後由某種方式將東西取出來。畢竟,存放事物是容器最基本的工做。對於ArrayListadd()是插入對象的方法,而get()是取出元素的方式之一。ArrayList很靈活,能夠隨時選取任意的元素,或使用不一樣的下標一次選取多個元素。

若是從更高層的角度思考,會發現這裏有一個缺點:要使用容器,必須知道其中元素的確切類型。初看起來這沒有什麼很差的,可是考慮以下狀況:若是本來是ArrayList ,可是後來考慮到容器的特色,你想換用Set ,應該怎麼作?或者你打算寫通用的代碼,它們只是使用容器,不知道或者說不關心容器的類型,那麼如何才能不重寫代碼就能夠應用於不一樣類型的容器?

       因此迭代器(Iterator)的概念,也是出於一種設計模式就是爲達成此目的而造成的。因此Collection不提供get()方法。若是要遍歷Collectin中的元素,就必須用Iterator

       迭代器(Iterator)自己就是一個對象,它的工做就是遍歷並選擇集合序列中的對象,而客戶端的程序員沒必要知道或關心該序列底層的結構。此外,迭代器一般被稱爲「輕量級」對象,建立它的代價小。可是,它也有一些限制,例如,某些迭代器只能單向移動。

Collection 接口的 iterator() 方法返回一個 IteratorIterator 和您可能已經熟悉的 Enumeration 接口相似。使用 Iterator 接口方法,您能夠從頭到尾遍歷集合,並安全的從底層 Collection 中除去元素。

 

 

 

下面,咱們看一個對於迭代器的簡單使用:

 

import java.util.ArrayList;

import java.util.Collection;

import java.util.Iterator;

 

public class IteratorDemo {

    public static void main(String[] args) {

       Collection collection = new ArrayList();

       collection.add("s1");

       collection.add("s2");

       collection.add("s3");

       Iterator iterator = collection.iterator();//獲得一個迭代器

       while (iterator.hasNext()) {//遍歷

           Object element = iterator.next();

           System.out.println("iterator = " + element);

       }

       if(collection.isEmpty())

           System.out.println("collection is Empty!");

       else

           System.out.println("collection is not Empty! size="+collection.size());

       Iterator iterator2 = collection.iterator();

       while (iterator2.hasNext()) {//移除元素

           Object element = iterator2.next();

           System.out.println("remove: "+element);

           iterator2.remove();

       }     

       Iterator iterator3 = collection.iterator();

       if (!iterator3.hasNext()) {//察看是否還有元素

           System.out.println("還有元素");

       }  

       if(collection.isEmpty())

           System.out.println("collection is Empty!");

       //使用collection.isEmpty()方法來判斷

    }

}

程序的運行結果爲:

iterator = s1

iterator = s2

iterator = s3

collection is not Empty! size=3

remove: s1

remove: s2

remove: s3

還有元素

collection is Empty!

 

能夠看到,JavaCollectionIterator 可以用來,:

1)      使用方法 iterator() 要求容器返回一個Iterator .第一次調用Iterator next() 方法時,它返回集合序列的第一個元素。

2)      使用next() 得到集合序列的中的下一個元素。

3)      使用hasNext()檢查序列中是否元素。

4)      使用remove()將迭代器新返回的元素刪除。

須要注意的是:方法刪除由next方法返回的最後一個元素,在每次調用next時,remove方法只能被調用一次

你們看,Java 實現的這個迭代器的使用就是如此的簡單。Iterator(跌代器)雖然功能簡單,但仍然能夠幫助咱們解決許多問題,同時針對List 還有一個更復雜更高級的ListIterator。您能夠在下面的List講解中獲得進一步的介紹。

 

1.3         List

1.3.1        概述

前面咱們講述的Collection接口實際上並無直接的實現類。而List是容器的一種,表示列表的意思。當咱們不知道存儲的數據有多少的狀況,咱們就可使用List 來完成存儲數據的工做。例如前面提到的一種場景。咱們想要在保存一個應用系統當前的在線用戶的信息。咱們就可使用一個List來存儲。由於List的最大的特色就是可以自動的根據插入的數據量來動態改變容器的大小。下面咱們先看看List接口的一些經常使用方法。

1.3.2         經常使用方法

List 就是列表的意思,它是Collection 的一種,繼承了 Collection 接口,以定義一個容許重複項的有序集合。該接口不但可以對列表的一部分進行處理,還添加了面向位置的操做。List 是按對象的進入順序進行保存對象,而不作排序或編輯操做。它除了擁有Collection接口的全部的方法外還擁有一些其餘的方法。

面向位置的操做包括插入某個元素或 Collection 的功能,還包括獲取、除去或更改元素的功能。在 List 中搜索元素能夠從列表的頭部或尾部開始,若是找到元素,還將報告元素所在的位置。

u       void add(int index, Object element) :添加對象element到位置index

u       boolean addAll(int index, Collection collection) :在index位置後添加容器collection中全部的元素

u       Object get(int index) :取出下標爲index的位置的元素

u       int indexOf(Object element) :查找對象element List中第一次出現的位置

u       int lastIndexOf(Object element) :查找對象element List中最後出現的位置

u       Object remove(int index) :刪除index位置上的元素

u       Object set(int index, Object element) :將index位置上的對象替換爲element 並返回老的元素。

 

 

先看一下下面表格:

 

 

簡述

實現

操做特性

成員要求

List

提供基於索引的對成員的隨機訪問

ArrayList

提供快速的基於索引的成員訪問,對尾部成員的增長和刪除支持較好

成員可爲任意Object子類的對象

LinkedList

對列表中任何位置的成員的增長和刪除支持較好,但對基於索引的成員訪問支持性能較差

成員可爲任意Object子類的對象

集合框架中有兩種常規的 List 實現:ArrayList LinkedList。使用兩種 List 實現的哪種取決於您特定的須要。若是要支持隨機訪問,而沒必要在除尾部的任何位置插入或除去元素,那麼,ArrayList 提供了可選的集合。但若是,您要頻繁的從列表的中間位置添加和除去元素,而只要順序的訪問列表元素,那麼,LinkedList 實現更好。

 

咱們以ArrayList 爲例,先看一個簡單的例子:

例子中,咱們把12個月份存放到ArrayList 中,而後用一個循環,並使用get()方法將列表中的對象都取出來。

LinkedList 添加了一些處理列表兩端元素的方法(下圖只顯示了新方法):

使用這些新方法,您就能夠輕鬆的把 LinkedList 看成一個堆棧、隊列或其它面向端點的數據結構。

咱們再來看另一個使用LinkedList 來實現一個簡單的隊列的例子:

import java.util.*;

 

public class ListExample {

  public static void main(String args[]) {

    LinkedList queue = new LinkedList();

    queue.addFirst("Bernadine");

    queue.addFirst(" Elizabeth ");

    queue.addFirst("Gene");

    queue.addFirst(" Elizabeth ");

    queue.addFirst("Clara");

    System.out.println(queue);

    queue.removeLast();

    queue.removeLast();

    System.out.println(queue);

  }

}

運行程序產生了如下輸出。請注意,與 Set 不一樣的是 List 容許重複。

[Clara, Elizabeth, Gene, Elizabeth, Bernadine] 
 
  

  
  
  
  [Clara, Elizabeth, Gene]

該的程序演示了具體 List 類的使用。第一部分,建立一個由 ArrayList 支持的 List。填充完列表之後,特定條目就獲得了。示例的 LinkedList 部分把 LinkedList 看成一個隊列,從隊列頭部添加東西,從尾部除去。

List 接口不但以位置友好的方式遍歷整個列表,還能處理集合的子集:

u       ListIterator listIterator() :返回一個ListIterator 跌代器,默認開始位置爲0

u       ListIterator listIterator(int startIndex) :返回一個ListIterator 跌代器,開始位置爲startIndex

u       List subList(int fromIndex, int toIndex) :返回一個子列表List ,元素存放爲從 fromIndex toIndex以前的一個元素。

處理 subList() 時,位於 fromIndex 的元素在子列表中,而位於 toIndex 的元素則不是,提醒這一點很重要。如下 for-loop 測試案例大體反映了這一點:

for (int i=fromIndex; i<toIndex; i++) { 
 
  
  // process element at position i 
 
  

  
  
  
  }

此外,咱們還應該提醒的是:對子列表的更改(如 add()remove() set() 調用)對底層 List 也有影響。

ListIterator 接口

ListIterator 接口繼承 Iterator 接口以支持添加或更改底層集合中的元素,還支持雙向訪問。

如下源代碼演示了列表中的反向循環。請注意 ListIterator 最初位於列表尾以後(list.size()),由於第一個元素的下標是0

List list = ...; 
 
  
ListIterator iterator = list.listIterator(list.size()); 
 
  
while (iterator.hasPrevious()) { 
 
  
  Object element = iterator.previous(); 
 
  
  // Process element 
 
  

  
  
  
  }

正常狀況下,不用 ListIterator 改變某次遍歷集合元素的方向向前或者向後。雖然在技術上可能實現時,但在 previous() 後馬上調用 next(),返回的是同一個元素。把調用 next() previous() 的順序顛倒一下,結果相同。

咱們看一個List的例子:

 

import java.util.*;

 

public class ListIteratorTest {

 

    public static void main(String[] args) {

       List list = new ArrayList();

       list.add("aaa");

       list.add("bbb");

       list.add("ccc");

       list.add("ddd"); 

       System.out.println("下標0開始:"+list.listIterator(0).next());//next()

       System.out.println("下標1開始:"+list.listIterator(1).next());

       System.out.println("List 1-3:"+list.subList(1,3));//子列表

       ListIterator it = list.listIterator();//默認從下標0開始

       //隱式光標屬性add操做 ,插入到當前的下標的前面

       it.add("sss");

       while(it.hasNext()){

           System.out.println("next Index="+it.nextIndex()+",Object="+it.next());

       }     

       //set屬性

       ListIterator it1 = list.listIterator();

       it1.next();

       it1.set("ooo");

       ListIterator it2 = list.listIterator(list.size());//下標

       while(it2.hasPrevious()){

           System.out.println("previous Index="+it2.previousIndex()+",Object="+it2.previous());

       }

    }

}

程序的執行結果爲:

下標0開始:aaa

下標1開始:bbb

List 1-3:[bbb, ccc]

next Index=1,Object=aaa

next Index=2,Object=bbb

next Index=3,Object=ccc

next Index=4,Object=ddd

previous Index=4,Object=ddd

previous Index=3,Object=ccc

previous Index=2,Object=bbb

previous Index=1,Object=aaa

previous Index=0,Object=ooo

咱們還須要稍微再解釋一下 add() 操做。添加一個元素會致使新元素馬上被添加到隱式光標的前面。所以,添加元素後調用 previous() 會返回新元素,而調用 next() 則不起做用,返回添加操做以前的下一個元素。下標的顯示方式,以下圖所示:

對於List 的基本用法咱們學會了,下面咱們來進一步瞭解一下List的實現原理,以便價升咱們對於集合的理解。

1.3.3         實現原理

前面已經提了一下Collection的實現基礎都是基於數組的。下面咱們就已ArrayList 爲例,簡單分析一下ArrayList 列表的實現方式。首先,先看下它的構造函數。

下列表格是在SUN提供的API中的描述:

 

ArrayList()           Constructs an empty list with an initial capacity of ten.

ArrayList(Collection c)           Constructs a list containing the elements of the specified collection, in the order they are returned by the collection's iterator.

ArrayList(int initialCapacity)           Constructs an empty list with the specified initial capacity.

其中第一個構造函數ArrayList()和第二構造函數ArrayList(Collection c) 是按照Collection 接口文檔所述,所應該提供兩個構造函數,一個無參數,一個接受另外一個 Collection

3個構造函數:

ArrayList(int initialCapacity) ArrayList實現的比較重要的構造函數,雖然,咱們不經常使用它,可是某認的構造函數正是調用的該帶參數:initialCapacity 的構造函數來實現的。 其中參數:initialCapacity 表示咱們構造的這個ArrayList列表的初始化容量是多大。若是調用默認的構造函數,則表示默認調用該參數爲initialCapacity =10 的方式,來進行構建一個ArrayList列表對象。

爲了更好的理解這個initialCapacity 參數的概念,咱們先看看ArrayListSun 提供的源碼中的實現方式。先看一下它的屬性有哪些:

ArrayList 繼承了AbstractList 咱們主要看看ArrayList中的屬性就能夠了。

ArrayList中主要包含2個屬性:

u       private transient Object elementData[];

u       private int size;

其中數組:elementData[] 是列表的實現核心屬性:數組。 咱們使用該數組來進行存放集合中的數據。而咱們的初始化參數就是該數組構建時候的長度,即該數組的length屬性就是initialCapacity 參數。

Keystransient 表示被修飾的屬性不是對象持久狀態的一部分,不會自動的序列化。

2個屬性:size表示列表中真實數據的存放個數。

咱們再來看一下ArrayList的構造函數,加深一下ArrayList是基於數組的理解。

從源碼中能夠看到默認的構造函數調用的就是帶參數的構造函數: 

public ArrayList(int initialCapacity)

不過參數initialCapacity10 。

咱們主要看ArrayList(int initialCapacity) 這個構造函數。能夠看到:

this.elementData = new Object[initialCapacity];

咱們就是使用的initialCapacity 這個參數來建立一個Object數組。而咱們全部的往該集合對象中存放的數據,就是存放到了這個Object數組中去了。

咱們在看看另一個構造函數的源碼:

這裏,咱們先看size() 方法的實現形式。它的做用便是返回size屬性值的大小。而後咱們再看另一個構造函數public ArrayList(Collection c) ,該構造函數的做用是把另一個容器對象中的元素存放到當前的List 對象中。

能夠看到,首先,咱們是經過調用另一個容器對象C 的方法size()來設置當前的List對象的size屬性的長度大小。

    接下來,就是對elementData 數組進行初始化,初始化的大小爲原先容器大小的1.1倍。最後,就是經過使用容器接口中的Object[] toArray(Object[] a) 方法來把當前容器中的對象都存放到新的數組elementData 中。這樣就完成了一個ArrayList 的創建。

    可能你們會存在一個問題,那就是,咱們創建的這個ArrayList 是使用數組來實現的,可是數組的長度一旦被定下來,就不能改變了。而咱們在給ArrayList對象中添加元素的時候,卻沒有長度限制。這個時候,ArrayList 中的elementData 屬性就必須存在一個須要動態的擴充容量的機制。咱們看下面的代碼,它描述了這個擴充機制:

 

這個方法的做用就是用來判斷當前的數組是否須要擴容,應該擴容多少。其中屬性:modCount是繼承自父類,它表示當前的對象對elementData數組進行了多少次擴容,清空,移除等操做。該屬性至關因而一個對於當前List 對象的一個操做記錄日誌號。 咱們主要看下面的代碼實現:

1.      首先獲得當前elementData 屬性的長度oldCapacity

2.      而後經過判斷oldCapacityminCapacity參數誰大來決定是否須要擴容

n         若是minCapacity大於oldCapacity,那麼咱們就對當前的List對象進行擴容。擴容的的策略爲:取(oldCapacity * 3)/2 + 1minCapacity之間更大的那個。而後使用數組拷貝的方法,把之前存放的數據轉移到新的數組對象中

n         若是minCapacity不大於oldCapacity那麼就不進行擴容。

下面咱們看看上的那個ensureCapacity方法的是如何使用的:

上的兩個add方法都是往List 中添加元素。每次在添加元素的時候,咱們就須要判斷一下,是否須要對於當前的數組進行擴容。

咱們主要看看  public boolean add(Object o)方法,能夠發如今添加一個元素到容器中的時候,首先咱們會判斷是否須要擴容。由於只增長一個元素,因此擴容的大小判斷也就爲當前的size+1來進行判斷。而後,就把新添加的元素放到數組elementData中。

第二個方法public boolean addAll(Collection c)也是一樣的原理。將新的元素放到elementData數組以後。同時改變當前List 對象的size屬性。

    相似的List 中的其餘的方法也都是基於數組進行操做的。你們有興趣能夠看看源碼中的更多的實現方式。

    最後咱們再看看如何判斷在集合中是否已經存在某一個對象的:

由源碼中咱們能夠看到,public boolean contains(Object elem)方法是經過調用public int indexOf(Object elem)方法來判斷是否在集合中存在某個對象elem。咱們看看indexOf方法的具體實現。

u       首先咱們判斷一下elem 對象是否爲null ,若是爲null的話,那麼遍歷數組elementData 把第一個出現null的位置返回。

u       若是elem不爲null 的話,咱們也是遍歷數組elementData ,並經過調用elem對象的equals()方法來獲得第一個相等的元素的位置。

這裏咱們能夠發現,ArrayList中用來判斷是否包含一個對象,調用的是各個對象本身實現的equals()方法。在前面的高級特性裏面,咱們能夠知道:若是要判斷一個類的一個實例對象是否等於另一個對象,那麼咱們就須要本身覆寫Object類的public boolean equals(Object obj) 方法。若是不覆寫該方法的話,那麼就會調用Objectequals()方法來進行判斷。這就至關於比較兩個對象的內存應用地址是否相等了。

    在集合框架中,不只僅是List,全部的集合類,若是須要判斷裏面是否存放了的某個對象,都是調用該對象的equals()方法來進行處理的。

1.4         Map

1.4.1        概述

數學中的映射關係在Java中就是經過Map來實現的。它表示,裏面存儲的元素是一個對(pair,咱們經過一個對象,能夠在這個映射關係中找到另一個和這個對象相關的東西。

前面提到的咱們對於根據賬號名獲得對應的人員的信息,就屬於這種狀況的應用。咱們講一我的員的賬戶名和這人員的信息做了一個映射關係,也就是說,咱們把賬戶名和人員信息當成了一個「鍵值對」,「鍵」就是賬戶名,「值」就是人員信息。下面咱們先看看Map 接口的經常使用方法。

1.4.2         經常使用方法

Map 接口不是 Collection 接口的繼承。而是從本身的用於維護鍵-值關聯的接口層次結構入手。按定義,該接口描述了從不重複的鍵到值的映射。

咱們能夠把這個接口方法分紅三組操做:改變、查詢和提供可選視圖。

改變操做容許您從映射中添加和除去鍵-值對。鍵和值均可覺得 null。可是,您不能把 Map 做爲一個鍵或值添加給自身。

u       Object put(Object key,Object value):用來存放一個鍵-值對Map

u       Object remove(Object key):根據key(),移除一個鍵-值對,並將值返回

u       void putAll(Map mapping) :將另一個Map中的元素存入當前的Map

u       void clear() :清空當前Map中的元素

查詢操做容許您檢查映射內容:

u       Object get(Object key) :根據key()取得對應的值

u       boolean containsKey(Object key) :判斷Map中是否存在某鍵(key

u       boolean containsValue(Object value):判斷Map中是否存在某值(value)

u       int size():返回Map -值對的個數

u       boolean isEmpty() :判斷當前Map是否爲空

最後一組方法容許您把鍵或值的組做爲集合來處理。

u       public Set keySet() :返回全部的鍵(key),並使用Set容器存放

u       public Collection values() :返回全部的值(Value),並使用Collection存放

u       public Set entrySet() 返回一個實現 Map.Entry 接口的元素 Set

由於映射中鍵的集合必須是惟一的,就使用 Set 來支持。由於映射中值的集合可能不惟一,就使用 Collection 來支持。最後一個方法返回一個實現 Map.Entry 接口的元素 Set

咱們看看Map的經常使用實現類的比較,以下表:

 

簡述

實現

操做特性

成員要求

Map

保存鍵值對成員,基於鍵找值操做,使用compareTocompare方法對鍵進行排序

HashMap

能知足用戶對Map的通用需求

鍵成員可爲任意Object子類的對象,但若是覆蓋了equals方法,同時注意修改hashCode方法。

TreeMap

支持對鍵有序地遍歷,使用時建議先用HashMap增長和刪除成員,最後從HashMap生成TreeMap 附加實現了SortedMap接口,支持子Map等要求順序的操做

鍵成員要求實現Comparable接口,或者使用Comparator構造TreeMap鍵成員通常爲同一類型。

LinkedHashMap

保留鍵的插入順序,用equals 方法檢查鍵和值的相等性

成員可爲任意Object子類的對象,但若是覆蓋了equals方法,同時注意修改hashCode方法。

下面咱們看一個簡單的例子:

 

import java.util.*;

 

public class MapTest {

public static void main(String[] args) {

        Map map1 = new HashMap();

       Map map2 = new HashMap();

       map1.put("1","aaa1");

       map1.put("2","bbb2");   

       map2.put("10","aaaa10");

       map2.put("11","bbbb11");

//根據鍵 "1" 取得值:"aaa1"

       System.out.println("map1.get(/"1/")="+map1.get("1"));

// 根據鍵 "1" 移除鍵值對"1"-"aaa1"

       System.out.println("map1.remove(/"1/")="+map1.remove("1"));

        System.out.println("map1.get(/"1/")="+map1.get("1"));

       map1.putAll(map2);//map2所有元素放入map1

       map2.clear();//清空map2

       System.out.println("map1 IsEmpty?="+map1.isEmpty());

       System.out.println("map2 IsEmpty?="+map2.isEmpty());

       System.out.println("map1 中的鍵值對的個數size = "+map1.size());

       System.out.println("KeySet="+map1.keySet());//set

       System.out.println("values="+map1.values());//Collection

       System.out.println("entrySet="+map1.entrySet());    

       System.out.println("map1 是否包含鍵:11 = "+map1.containsKey("11"));

       System.out.println("map1 是否包含值:aaa1 = "+map1.containsValue("aaa1"));

    }

}

運行輸出結果爲:

map1.get("1")=aaa1

map1.remove("1")=aaa1

map1.get("1")=null

map1 IsEmpty?=false

map2 IsEmpty?=true

map1 中的鍵值對的個數size = 3

KeySet=[10, 2, 11]

values=[aaaa10, bbb2, bbbb11]

entrySet=[10=aaaa10, 2=bbb2, 11=bbbb11]

map1 是否包含鍵:11 = true

map1 是否包含值:aaa1 = false

 

在該例子中,咱們建立一個HashMap,並使用了一下Map接口中的各個方法。

其中Map中的entrySet()方法先提一下,該方法返回一個實現 Map.Entry 接口的對象集合。集合中每一個對象都是底層 Map 中一個特定的鍵-值對。

Map.Entry 接口是Map 接口中的一個內部接口,該內部接口的實現類存放的是鍵值對。在下面的實現原理中,咱們會對這方面再做介紹,如今咱們先無論這個它的具體實現。

咱們再看看排序的Map是如何使用:

import java.util.*;

 

public class MapSortExample {

  public static void main(String args[]) {

    Map map1 = new HashMap();

    Map map2 = new LinkedHashMap();

    for(int i=0;i<10;i++){

    double s=Math.random()*100;//產生一個隨機數,並將其放入Map

     map1.put(new Integer((int) s)," "+i+" 個放入的元素:"+s+"/n");

     map2.put(new Integer((int) s)," "+i+" 個放入的元素:"+s+"/n");

    }

   

    System.out.println("未排序前HashMap"+map1);

    System.out.println("未排序前LinkedHashMap"+map2);

    //使用TreeMap來對另外的Map進行重構和排序

    Map sortedMap = new TreeMap(map1);

    System.out.println("排序後:"+sortedMap);

    System.out.println("排序後:"+new TreeMap(map2));

  }

}

該程序的一次運行結果爲:

未排序前HashMap{64= 1 個放入的元素:64.05341725531845

, 15= 9 個放入的元素:15.249165766266382

, 2= 4 個放入的元素:2.66794706854534

, 77= 0 個放入的元素:77.28814965781416

, 97= 5 個放入的元素:97.32893518378948

, 99= 2 個放入的元素:99.99412014935982

, 60= 8 個放入的元素:60.91451419025399

, 6= 3 個放入的元素:6.286974058646977

, 1= 7 個放入的元素:1.8261658496439903

, 48= 6 個放入的元素:48.736039522423106

}

未排序前LinkedHashMap{77= 0 個放入的元素:77.28814965781416

, 64= 1 個放入的元素:64.05341725531845

, 99= 2 個放入的元素:99.99412014935982

, 6= 3 個放入的元素:6.286974058646977

, 2= 4 個放入的元素:2.66794706854534

, 97= 5 個放入的元素:97.32893518378948

, 48= 6 個放入的元素:48.736039522423106

, 1= 7 個放入的元素:1.8261658496439903

, 60= 8 個放入的元素:60.91451419025399

, 15= 9 個放入的元素:15.249165766266382

}

排序後:{1= 7 個放入的元素:1.8261658496439903

, 2= 4 個放入的元素:2.66794706854534

, 6= 3 個放入的元素:6.286974058646977

, 15= 9 個放入的元素:15.249165766266382

, 48= 6 個放入的元素:48.736039522423106

, 60= 8 個放入的元素:60.91451419025399

, 64= 1 個放入的元素:64.05341725531845

, 77= 0 個放入的元素:77.28814965781416

, 97= 5 個放入的元素:97.32893518378948

, 99= 2 個放入的元素:99.99412014935982

}

排序後:{1= 7 個放入的元素:1.8261658496439903

, 2= 4 個放入的元素:2.66794706854534

, 6= 3 個放入的元素:6.286974058646977

, 15= 9 個放入的元素:15.249165766266382

, 48= 6 個放入的元素:48.736039522423106

, 60= 8 個放入的元素:60.91451419025399

, 64= 1 個放入的元素:64.05341725531845

, 77= 0 個放入的元素:77.28814965781416

, 97= 5 個放入的元素:97.32893518378948

, 99= 2 個放入的元素:99.99412014935982

}

從運行結果,咱們能夠看出,HashMap的存入順序和輸出順序無關。而LinkedHashMap 則保留了鍵值對的存入順序。TreeMap則是對Map中的元素進行排序。在實際的使用中咱們也常常這樣作:使用HashMap或者LinkedHashMap 來存放元素,當全部的元素都存放完成後,若是使用則是須要一個通過排序的Map的話,咱們再使用TreeMap來重構原來的Map對象。這樣作的好處是:由於HashMapLinkedHashMap 存儲數據的速度比直接使用TreeMap 要快,存取效率要高。當完成了全部的元素的存放後,咱們再對整個的Map中的元素進行排序。這樣能夠提升整個程序的運行的效率,縮短執行時間。

這裏須要注意的是,TreeMap中是根據鍵(Key)進行排序的。而若是咱們要使用TreeMap來進行正常的排序的話,Key 中存放的對象必須實現Comparable 接口。

咱們簡單介紹一下這個接口:

1.4.3        Comparable 接口

java.lang 包中,Comparable 接口適用於一個類有天然順序的時候。假定對象集合是同一類型,該接口容許您把集合排序成天然順序。

它只有一個方法:compareTo() 方法,用來比較當前實例和做爲參數傳入的元素。若是排序過程當中當前實例出如今參數前(當前實例比參數大),就返回某個負值。若是當前實例出如今參數後(當前實例比參數小),則返回正值。不然,返回零。若是這裏不要求零返回值表示元素相等。零返回值能夠只是表示兩個對象在排序的時候排在同一個位置。

上面例子中的整形的包裝類:Integer 就實現了該接口。咱們能夠看一下這個類的源碼:

能夠看到compareTo 方法裏面經過判斷當前的Integer對象的值是否大於傳入的參數的值來獲得返回值的。

Java 2 SDK,版本 1.2 中有十四個類實現 Comparable 接口。下表展現了它們的天然排序。雖然一些類共享同一種天然排序,但只有相互可比的類才能排序。

排序

BigDecimal, BigInteger, Byte, Double, Float, Integer, Long, Short

按數字大小排序

Character

Unicode 值的數字大小排序

CollationKey

按語言環境敏感的字符串排序

Date

按年代排序

File

按系統特定的路徑名的全限定字符的 Unicode 值排序

ObjectStreamField

按名字中字符的 Unicode 值排序

String

按字符串中字符 Unicode 值排序

 

這裏只是簡單的介紹一下排序接口,若是要詳細的瞭解排序部份內容的話,能夠參考文章最後的附錄部分對於排序的更加詳細的描述。

咱們再回到Map中來,Java提升的API中除了上面介紹的幾種Map比較經常使用覺得還有一些Map,你們能夠了解一下:

u   WeakHashMap WeakHashMap Map 的一個特殊實現,它只用於存儲對鍵的弱引用。當映射的某個鍵在 WeakHashMap 的外部再也不被引用時,就容許垃圾收集器收集映射中相應的鍵值對。使用 WeakHashMap 有益於保持相似註冊表的數據結構,其中條目的鍵再也不能被任何線程訪問時,此條目就沒用了。

u   IdentifyHashMap Map的一種特性實現,關鍵屬性的hash碼不是由hashCode()方法計算,而是由System.identityHashCode 方法計算,使用==進行比較而不是equals()方法。

 

經過簡單的對與Map中各個經常使用實現類的使用,爲了更好的理解Map,下面咱們再來了解一下Map的實現原理。

1.4.4         實現原理

有的人可能會認爲 Map 會繼承 Collection。在數學中,映射只是對(pair)的集合。可是,在集合框架中,接口 Map Collection 在層次結構沒有任何親緣關係,它們是大相徑庭的。這種差異的緣由與 Set Map Java 庫中使用的方法有關。Map 的典型應用是訪問按關鍵字存儲的值。它支持一系列集合操做的所有,但操做的是鍵-值對,而不是單個獨立的元素。所以 Map 須要支持 get() put() 的基本操做,而 Set 不須要。此外,還有返回 Map 對象的 Set 視圖的方法:

Set set = aMap.keySet();

       下面咱們以HashMap爲例,對Map的實現機制做一下更加深刻一點的理解。

由於HashMap裏面使用Hash算法,因此在理解HashMap以前,咱們須要先了解一下Hash算法和Hash表。

Hash,通常翻譯作散列,也有直接音譯爲"哈希"的,就是把任意長度的輸入(又叫作 預映射, pre-image),經過散列算法,變換成固定長度的輸出,該輸出就是散列值。這種轉換是一種壓縮映射,也就是,散列值的空間一般遠小於輸入的空間,不一樣的輸入可能 會散列成相同的輸出,而不可能從散列值來惟一的肯定輸入值。

說的通俗一點,Hash算法的意義在於提供了一種快速存取數據的方法,它用一種算法創建鍵值與真實值之間的對應關係,(每個真實值只能有一個鍵值,可是一個鍵值能夠對應多個真實值),這樣能夠快速在數組等裏面存取數據。

看下圖:

咱們創建一個HashTable(哈希表),該表的長度爲N,而後咱們分別在該表中的格子中存放不一樣的元素。每一個格子下面存放的元素又是以鏈表的方式存放元素。

u       當添加一個新的元素Entry 的時候,首先咱們經過一個Hash函數計算出這個Entry元素的Hashhashcode。經過該hashcode值,就能夠直接定位出咱們應該把這個Entry元素存入到Hash表的哪一個格子中,若是該格子中已經存在元素了,那麼只要把新的Entry元存放到這個鏈表中便可。

u       若是要查找一個元素Entry的時候,也一樣的方式,經過Hash函數計算出這個Entry元素的Hashhashcode。而後經過該hashcode值,就能夠直接找到這個Entry是存放到哪一個格子中的。接下來就對該格子存放的鏈表元素進行逐個的比較查找就能夠了。

 舉一個比較簡單的例子來講明這個算法的運算方式:

假定咱們有一個長度爲8Hash表(能夠理解爲一個長度爲8的數組)。在這個Hash表中存放數字:以下表

0

1

2

3

4

5

6

7

假定咱們的Hash函數爲:

Hashcode = X%8    -------- 8 取餘數

其中X就是咱們須要放入Hash表中的數字,而這個函數返回的Hashcode就是Hash碼。

假定咱們有下面10個數字須要依次存入到這個Hash表中:

11 , 23 , 44 , 9 , 6 , 32 , 12 , 45 , 57 , 89

經過上面的Hash函數,咱們能夠獲得分別對應的Hash碼:

11――3 ; 23――7 ;44――4 ;9――16――632――012――445――557――189――1

計算出來的Hash碼分別表明,該數字應該存放到Hash表中的哪一個對應數字的格子中。若是改格子中已經有數字存在了,那麼就以鏈表的方式將數字依次存放在該格子中,以下表:

0

1

2

3

4

5

6

7

32

9

 

11

44

45

6

23

 

57

 

 

12

 

 

 

 

89

 

 

 

 

 

 

Hash表和Hash算法的特色就是它的存取速度比數組差一些,可是比起單純的鏈表,在查找和存儲方面卻要好不少。同時數組也不利於數據的重構而排序等方面的要求。

更具體的說明,讀者能夠參考數據結構相關方面的書籍。

 

簡單的瞭解了一下Hash算法後,咱們就來看看HashMap的屬性有哪些:

裏面最重要的3個屬性:

n         transient Entry[] table: 用來存放鍵值對的對象Entry數組,也就是Hash

n         transient int size當前Map中存放的鍵值對的個數

n         final float loadFactor負載因子,用來決定什麼狀況下應該對Entry進行擴容

咱們Entry 對象是Map接口中的一個內部接口。便是使用它來保存咱們的鍵值對的。

咱們看看這個Entry 內部接口在HashMap中的實現:

 

經過查看源碼,咱們能夠看到Entry類有點相似一個單向鏈表。其中:

        final Object key 和 Object value;存放的就是咱們放入Map中的鍵值對。

屬性Entry next;表示當前鍵值對的下一個鍵值對是哪一個Entry

接下來,咱們看看HashMap的主要的構造函數:

咱們主要看看 public HashMap(int initialCapacity, float loadFactor)

由於,另外兩個構造函數實行也是一樣的方式進行構建一個HashMap 的。

該構造函數:

1.         首先是判斷參數int initialCapacityfloat loadFactor是否合法

2.         而後肯定Hash表的初始化長度。肯定的策略是:經過傳進來的參數initialCapacity 來找出第一個大於它的2的次方的數。好比說咱們傳了18這樣的一個initialCapacity 參數,那麼真實的table數組的長度爲25次方,即32之因此採用這種策略來構建Hash表的長度,是由於2的次方的運算對於現代的處理器來講,能夠經過一些方法獲得更加好的執行效率。

3.         接下來就是獲得重構因子(threshold)了,這個屬性也是HashMap中的一個比較重要的屬性,它表示,當Hash表中的元素被存放了多少個以後,咱們就須要對該Hash表進行重構。

4.         最後就是使用獲得的初始化參數capacity 來構建Hash表:Entry[] table

 

下面咱們看看一個鍵值對是如何添加到HashMap中的。

 

put方法是用來添加一個鍵值對(key-value)到Map中,若是Map中已經存在相同的鍵的鍵值對的話,那麼就把新的值覆蓋老的值,並把老的值返回給方法的調用者。若是不存在同樣的鍵,那麼就返回null 。咱們看看方法的具體實現:

1.         首先咱們判斷若是keynull則使用一個常量來代替該key值,該行爲在方法maskNull()終將key替換爲一個非null 的對象k

2.         計算key值的Hash碼:hash

3.         經過使用Hash碼來定位,咱們應該把當前的鍵值對存放到Hash表中的哪一個格子中。indexFor()方法計算出的結果:i 就是Hash表(table)中的下標。

4.         而後遍歷當前的Hash表中table[i]格中的鏈表。從中判斷已否已經存在同樣的鍵(Key)的鍵值對。若是存在同樣的key的鍵,那麼就用新的value覆寫老的value,並把老的value返回

5.         若是遍歷後發現沒有存在一樣的鍵值對,那麼就增長當前鍵值對到Hash表中的第i個格子中的鏈表中。並返回null

最後咱們看看一個鍵值對是如何添加到各個格子中的鏈表中的:

咱們先看void addEntry(int hash, Object key, Object value, int bucketIndex)方法,該方法的做用就用來添加一個鍵值對到Hash表的第bucketIndex個格子中的鏈表中去。這個方法做的工做就是:

1.         建立一個Entry對象用來存放鍵值對。

2.         添加該鍵值對 ---- Entry對象到鏈表中

3.         最後在size屬性加一,並判斷是否須要對當前的Hash表進行重構。若是須要就在void resize(int newCapacity)方法中進行重構。

之因此須要重構,也是基於性能考慮。你們能夠考慮這樣一種狀況,假定咱們的Hash表只有4個格子,那麼咱們全部的數據都是放到這4個格子中。若是存儲的數據量比較大的話,例如100。這個時候,咱們就會發現,在這個Hash表中的4個格子存放的4個長長的鏈表。而咱們每次查找元素的時候,其實至關於就是遍歷鏈表了。這種狀況下,咱們用這個Hash表來存取數據的性能實際上和使用鏈表差很少了。

可是若是咱們對這個Hash表進行重構,換爲使用Hash表長度爲200的表來存儲這100個數據,那麼平均2個格子裏面纔會存放一個數據。這個時候咱們查找的數據的速度就會很是的快。由於基本上每一個格子中存放的鏈表都不會很長,因此咱們遍歷鏈表的次數也就不多,這樣也就加快了查找速度。可是這個時候又存在了另外的一個問題。咱們使用了至少200個數據的空間來存放100個數據,這樣就形成至少100個數據空間的浪費。 在速度和空間上面,咱們須要找到一個適合本身的中間值。在HashMap中咱們經過負載因子(loadFactor)來決定應該何時應該重構咱們的Hash表,以達到比較好的性能狀態。

咱們再看看重構Hash表的方法:void resize(int newCapacity)是如何實現的:

它的實現方式比較簡單:

1.      首先判斷若是Hash表的長度已經達到最大值,那麼就不進行重構了。由於這個時候Hash表的長度已經達到上限,已經沒有必要重構了。

2.      而後就是構建新的Hash

3.      把老的Hash表中的對象數據所有轉移到新的HashnewTable中,並設置新的重構因子threshold

 

對於HashMap中的實現原理,咱們就分析到這裏。你們可能會發現,HashCode的計算,是用來定位咱們的鍵值對應該放到Hash表中哪一個格子中的關鍵屬性。而這個HashCode的計算方法是調用的各個對象本身的實現的hashCode()方法。而這個方法是在Object對象中定義的,因此咱們本身定義的類若是要在集合中使用的話,就須要正確的覆寫hashCode() 方法。下面就介紹一下應該如何正確覆寫hashCode()方法。

1.4.5        覆寫hashCode()

在明白了HashMap具備哪些功能,以及實現原理後,瞭解如何寫一個hashCode()方法就更有意義了。固然,在HashMap中存取一個鍵值對涉及到的另一個方法爲equals (),由於該方法的覆寫在高級特性已經講解了。這裏就不作過多的描述。

    設計hashCode()時最重要的因素就是:不管什麼時候,對同一個對象調用hashCode()都應該生成一樣的值。若是在將一個對象用put()方法添加進HashMap時產生一個hashCode()值,而用get()取出時卻產生了另一個hashCode()值,那麼就沒法從新取得該對象了。因此,若是你的hashCode()方法依賴於對象中易變的數據,那用戶就要當心了,由於此數據發生變化時,hashCode()就會產生一個不一樣的hash碼,至關於產生了一個不一樣的「鍵」。

    此外,也不該該使hashCode()依賴於具備惟一性的對象信息,尤爲是使用this的值,這隻能產生很糟糕的hashCode()。由於這樣作沒法生成一個新的「鍵」,使之與put()種原始的「鍵值對」中的「鍵」相同。例如,若是咱們不覆寫ObjecthashCode()方法,那麼調用該方法的時候,就會調用ObjecthashCode()方法的默認實現。ObjecthashCode()方法,返回的是當前對象的內存地址。下次若是咱們須要取一個同樣的「鍵」對應的鍵值對的時候,咱們就沒法獲得同樣的hashCode值了。由於咱們後來建立的「鍵」對象已經不是存入HashMap中的那個內存地址的對象了。

咱們看一個簡單的例子,就能更加清楚的理解上面的意思。假定咱們寫了一個類:Person (人),咱們判斷一個對象「人」是否指向同一我的,只要知道這我的的身份證號一直就能夠了。

先看咱們沒有實現hashCode的狀況:

package c08.hashEx;

 

import java.util.*;

//身份證類

class Code{

    final int id;//身份證號碼已經確認,不能改變

    Code(int i){

       id=i;

    }

    //身份號號碼相同,則身份證相同

    public boolean equals(Object anObject) {

        if (anObject instanceof Code){

           Code other=(Code) anObject;

           return this.id==other.id;

       }

       return false;

    }

    public String toString() {

       return "身份證:"+id;

    }  

}

//人員信息類

class Person {

    Code id;// 身份證

    String name;// 姓名

    public Person(String name, Code id) {

       this.id=id;

       this.name=name;

    }

    //若是身份證號相同,就表示兩我的是同一我的

    public boolean equals(Object anObject) {

       if (anObject instanceof Person){

           Person other=(Person) anObject;

           return this.id.equals(other.id);

       }

       return false;

    }

    public String toString() {

       return "姓名:"+name+" 身份證:"+id.id+"/n";

    }

}

 

public class HashCodeEx {

    public static void main(String[] args) {

       HashMap map=new HashMap();

       Person p1=new Person("張三",new Code(123));

       map.put(p1.id,p1);//咱們根據身份證來做爲key值存放到Map

       Person p2=new Person("李四",new Code(456));

       map.put(p2.id,p2);

       Person p3=new Person("王二",new Code(789));

       map.put(p3.id,p3);

       System.out.println("HashMap 中存放的人員信息:/n"+map);

       // 張三 更名爲:張山 可是仍是同一我的。

       Person p4=new Person("張山",new Code(123));

       map.put(p4.id,p4);

       System.out.println("張三更名後 HashMap 中存放的人員信息:/n"+map);

       //查找身份證爲:123 的人員信息

       System.out.println("查找身份證爲:123 的人員信息:"+map.get(new Code(123)));

    }

}

運行結果爲:

HashMap 中存放的人員信息:

{身份證:456=姓名:李四 身份證:456

, 身份證:123=姓名:張三 身份證:123

, 身份證:789=姓名:王二 身份證:789

}

張三更名後 HashMap 中存放的人員信息:

{身份證:456=姓名:李四 身份證:456

, 身份證:123=姓名:張三 身份證:123

, 身份證:123=姓名:張山 身份證:123

, 身份證:789=姓名:王二 身份證:789

}

查找身份證爲:123 的人員信息:null

 

上面的例子的演示的是,咱們在一個HashMap中存放了一些人員的信息。並以這些人員的身份證最爲人員的「鍵」。當有的人員的姓名修改了的狀況下,咱們須要更新這個HashMap。同時假如咱們知道某個身份證號,想了解這個身份證號對應的人員信息如何,咱們也能夠根據這個身份證號在HashMap中獲得對應的信息。

而例子的輸出結果表示,咱們所作的更新和查找操做都失敗了。失敗的緣由就是咱們的身份證類:Code 沒有覆寫hashCode()方法。這個時候,當查找同樣的身份證號碼的鍵值對的時候,使用的是默認的對象的內存地址來進行定位。這樣,後面的全部的身份證號對象new Code(123) 產生的hashCode()值都是不同的。因此致使操做失敗。

下面,咱們給Code類加上hashCode()方法,而後再運行一下程序看看:

//身份證類

class Code{

    final int id;//身份證號碼已經確認,不能改變

    Code(int i){

       id=i;

    }

    //身份號號碼相同,則身份證相同

    public boolean equals(Object anObject) {

       if (anObject instanceof Code){

           Code other=(Code) anObject;

           return this.id==other.id;

       }

       return false;

    }

    public String toString() {

       return "身份證:"+id;

    }  

    //覆寫hashCode方法,並使用身份證號做爲hash

    public int hashCode(){

       return id;

    }

}

再次執行上面的HashCodeEx 的結果就爲:

HashMap 中存放的人員信息:

{身份證:456=姓名:李四 身份證:456

, 身份證:789=姓名:王二 身份證:789

, 身份證:123=姓名:張三 身份證:123

}

張三更名後 HashMap 中存放的人員信息:

{身份證:456=姓名:李四 身份證:456

, 身份證:789=姓名:王二 身份證:789

, 身份證:123=姓名:張山 身份證:123

}

查找身份證爲:123 的人員信息:姓名:張山 身份證:123

 

這個時候,咱們發現。咱們想要作的更新和查找操做都成功了。

對於Map部分的使用和實現,主要就是須要注意存放「鍵值對」中的對象的equals()方法和hashCode()方法的覆寫。若是須要使用到排序的話,那麼還須要實現Comparable 接口中的compareTo() 方法。咱們須要注意Map中的「鍵」是不能重複的,而是否重複的判斷,是經過調用「鍵」對象的equals()方法來決定的。而在HashMap中查找和存取「鍵值對」是同時使用hashCode()方法和equals()方法來決定的。

 

1.5        Set

1.5.1        概述

Java 中的Set和正好和數學上直觀的集(set的概念是相同的。Set最大的特性就是不容許在其中存放的元素是重複的。根據這個特色,咱們就可使用Set 這個接口來實現前面提到的關於商品種類的存儲需求。Set 能夠被用來過濾在其餘集合中存放的元素,從而獲得一個沒有包含重複新的集合。

1.5.2        經常使用方法

按照定義,Set 接口繼承 Collection 接口,並且它不容許集合中存在重複項。全部原始方法都是現成的,沒有引入新方法。具體的 Set 實現類依賴添加的對象的 equals() 方法來檢查等同性。

咱們簡單的描述一下各個方法的做用:

u     public int size() :返回set中元素的數目,若是set包含的元素數大於Integer.MAX_VALUE,返回Integer.MAX_VALUE

u     public boolean isEmpty() :若是set中不含元素,返回true

u     public boolean contains(Object o) :若是set包含指定元素,返回true

u     public Iterator iterator()

l         返回set中元素的迭代器

l         元素返回沒有特定的順序,除非set是提升了該保證的某些類的實例

u     public Object[] toArray() :返回包含set中全部元素的數組

u     public Object[] toArray(Object[] a) :返回包含set中全部元素的數組,返回數組的運行時類型是指定數組的運行時類型

u     public boolean add(Object o) :若是set中不存在指定元素,則向set加入

u     public boolean remove(Object o) :若是set中存在指定元素,則從set中刪除

u     public boolean removeAll(Collection c) :若是set包含指定集合,則從set中刪除指定集合的全部元素

u     public boolean containsAll(Collection c) :若是set包含指定集合的全部元素,返回true。若是指定集合也是一個set,只有是當前set的子集時,方法返回true

u     public boolean addAll(Collection c) :若是set中中不存在指定集合的元素,則向set中加入全部元素

u     public boolean retainAll(Collection c) :只保留set中所含的指定集合的元素(可選操做)。換言之,從set中刪除全部指定集合不包含的元素。 若是指定集合也是一個set,那麼該操做修改set的效果是使它的值爲兩個set的交集

u     public boolean removeAll(Collection c) :若是set包含指定集合,則從set中刪除指定集合的全部元素

u     public void clear() :從set中刪除全部元素

 

集合框架支持 Set 接口兩種普通的實現:HashSet TreeSet以及LinkedHashSet。下表中是Set的經常使用實現類的描述:

 

 

簡述

實現

操做特性

成員要求

Set

成員不能重複

HashSet

外部無序地遍歷成員。

成員可爲任意Object子類的對象,但若是覆蓋了equals方法,同時注意修改hashCode方法。

TreeSet

外部有序地遍歷成員;

附加實現了SortedSet, 支持子集等要求順序的操做

成員要求實現Comparable接口,或者使用Comparator構造TreeSet。成員通常爲同一類型。

LinkedHashSet

外部按成員的插入順序遍歷成員

成員與HashSet成員相似

 

在更多狀況下,您會使用 HashSet 存儲重複自由的集合。同時HashSet中也是採用了Hash算法的方式進行存取對象元素的。因此添加到 HashSet 的對象對應的類也須要採用恰當方式來實現 hashCode() 方法。雖然大多數系統類覆蓋了 Object 中缺省的 hashCode() 實現,但建立您本身的要添加到 HashSet 的類時,別忘了覆蓋 hashCode()

    對於Set的使用,咱們先以一個簡單的例子來講明:

import java.util.*;

 

public class HashSetDemo {

    public static void main(String[] args) {

       Set set1 = new HashSet();

       if (set1.add("a")) {//添加成功

           System.out.println("1 add true");

       }

       if (set1.add("a")) {//添加失敗

           System.out.println("2 add true");

       }     

       set1.add("000");//添加對象到Set集合中

       set1.add("111");

       set1.add("222");

       System.out.println("集合set1的大小:"+set1.size());

       System.out.println("集合set1的內容:"+set1);

       set1.remove("000");//從集合set1中移除掉 "000" 這個對象

       System.out.println("集合set1移除 000 後的內容:"+set1);

       System.out.println("集合set1中是否包含000 "+set1.contains("000"));

       System.out.println("集合set1中是否包含111 "+set1.contains("111"));

       Set set2=new HashSet();

       set2.add("111");

       set2.addAll(set1);//set1 集合中的元素所有都加到set2

       System.out.println("集合set2的內容:"+set2);

       set2.clear();//清空集合 set1 中的元素

       System.out.println("集合set2是否爲空 "+set2.isEmpty());

        Iterator iterator = set1.iterator();//獲得一個迭代器

       while (iterator.hasNext()) {//遍歷

           Object element = iterator.next();

           System.out.println("iterator = " + element);

       }

       //將集合set1轉化爲數組

       Object s[]= set1.toArray();

       for(int i=0;i<s.length;i++){

           System.out.println(s[i]);

       }

    }

}

程序執行的結果爲:

1 add true

集合set1的大小:4

集合set1的內容:[222, a, 000, 111]

集合set1移除 000 後的內容:[222, a, 111]

集合set1中是否包含000 false

集合set1中是否包含111 true

集合set2的內容:[222, a, 111]

集合set2是否爲空 true

iterator = 222

iterator = a

iterator = 111

222

a

111

從上面的這個簡單的例子中,咱們能夠發現,Set中的方法與直接使用Collection中的方法同樣。惟一須要注意的就是Set中存放的元素不能重複。

    咱們再看一個例子,來了解一下其它的Set的實現類的特性:

package c08;

 

import java.util.*;

 

public class SetSortExample {

  public static void main(String args[]) {

    Set set1 = new HashSet();

    Set set2 = new LinkedHashSet();

    for(int i=0;i<5;i++){

    //產生一個隨機數,並將其放入Set

    int s=(int) (Math.random()*100);

     set1.add(new Integer( s));

     set2.add(new Integer( s));

     System.out.println(" "+i+" 次隨機數產生爲:"+s);

    }

    System.out.println("未排序前HashSet"+set1);

    System.out.println("未排序前LinkedHashSet"+set2);

    //使用TreeSet來對另外的Set進行重構和排序

    Set sortedSet = new TreeSet(set1);

    System.out.println("排序後 TreeSet "+sortedSet);

  }

}

該程序的一次執行結果爲:

0 次隨機數產生爲:96

1 次隨機數產生爲:64

2 次隨機數產生爲:14

3 次隨機數產生爲:95

4 次隨機數產生爲:57

未排序前HashSet[64, 96, 95, 57, 14]

未排序前LinkedHashSet[96, 64, 14, 95, 57]

排序後 TreeSet [14, 57, 64, 95, 96]

從這個例子中,咱們能夠知道HashSet的元素存放順序和咱們添加進去時候的順序沒有任何關係,而LinkedHashSet 則保持元素的添加順序。TreeSet則是對咱們的Set中的元素進行排序存放。

通常來講,當您要從集合中以有序的方式抽取元素時,TreeSet 實現就會有用處。爲了能順利進行,添加到 TreeSet 的元素必須是可排序的。 而您一樣須要對添加到TreeSet中的類對象實現 Comparable 接口的支持。對於Comparable接口的實現,在前一小節的Map中已經簡單的介紹了一下。咱們暫且假定一棵樹知道如何保持 java.lang 包裝程序器類元素的有序狀態。通常說來,先把元素添加到 HashSet,再把集合轉換爲 TreeSet 來進行有序遍歷會更快。這點和HashMap的使用很是的相似。

其實Set的實現原理是基於Map上面的。經過下面咱們對Set的進一步分析你們就能更加清楚的瞭解這點了。

1.5.3         實現原理

JavaSet的概念和數學中的集合(set)一致,都表示一個集內能夠存放的元素是不能重複的。

       前面咱們會發現,Set中不少實現類和Map中的一些實現類的使用上很是的類似。並且前面再講解Map的時候,咱們也提到:Map中的「鍵值對」,其中的「鍵」是不能重複的。這個和Set中的元素不能重複一致。咱們以HashSet爲例來分析一下,會發現其實Set利用的就是Map中「鍵」不能重複的特性來實現的。

先看看HashSet中的有哪些屬性:

再結合構造函數來看看:

經過這些方法,咱們能夠發現,其實HashSet的實現,所有的操做都是基於HashMap來進行的。咱們看看是如何經過HashMap來保證咱們的HashSet的元素不重複性的:

看到這個操做咱們能夠發現HashSet的巧妙實現:就是創建一個「鍵值對」,「鍵」就是咱們要存入的對象,「值」則是一個常量。這樣能夠確保,咱們所須要的存儲的信息之是「鍵」。而「鍵」在Map中是不能重複的,這就保證了咱們存入Set中的全部的元素都不重複。而判斷是否添加元素成功,則是經過判斷咱們向Map中存入的「鍵值對」是否已經存在,若是存在的話,那麼返回值確定是常量:PRESENT ,表示添加失敗。若是不存在,返回值就爲null 表示添加成功。

咱們再看看其餘的方法實現:

       瞭解了這些後,咱們就不難理解,爲何HashMap中須要注意的地方,在HashSet中也一樣的須要注意。其餘的Set的實現類也是差很少的原理。

       至此對於Set咱們就應該可以比較好的理解了。

1.6        總結:集合框架中經常使用類比較

集合框架設計軟件時,記住該框架四個基本接口的下列層次結構關係會有用處:

  • Collection 接口是一組容許重複的對象。
  • Set 接口繼承 Collection,但不容許重複。
  • List 接口繼承 Collection,容許重複,並引入位置下標。
  • Map 接口既不繼承 Set 也不繼承 Collection, 存取的是鍵值對

咱們如下面這個圖表來描述一下經常使用的集合的實現類之間的區別:

Collection/Map

接口

成員重複性

元素存放順序(Ordered/Sorted

元素中被調用的方法

基於那中數據結構來實現的

HashSet

Set

Unique elements

No order

equals()

hashCode()

Hash

LinkedHashSet

Set

Unique elements

Insertion order

equals()

hashCode()

Hash 表和雙向鏈表

TreeSet

SortedSet

Unique elements

Sorted

equals()

compareTo()

平衡樹(Balanced tree

ArrayList

List

Allowed

Insertion order

equals()

數組

LinkedList

List

Allowed

Insertion order

equals()

鏈表

Vector

List

Allowed

Insertion order

equals()

數組

HashMap

Map

Unique keys

No order

equals()

hashCode()

Hash

LinkedHashMap

Map

Unique keys

Key insertion order/Access order of entries

equals()

hashCode()

Hash 表和雙向鏈表

Hashtable

Map

Unique keys

No order

equals()

hashCode()

Hash

TreeMap

SortedMap

Unique keys

Sorted in key order

equals()

compareTo()

平衡樹(Balanced tree

2        練習

撰寫一個Person class,表示一我的員的信息。令該類具有多輛Car的信息,表示一我的能夠擁有的車子的數據,以及:

       Certificate code: 身份證對象

name: 姓名

cash: 現金

List car;   擁有的汽車,其中存放的是Car對象

boolean buycar

相關文章
相關標籤/搜索