Java Collections API和泛型
數據結構和算法
學會一門編程語言,你可以寫出一些可以工作的代碼用計算機來解決一些問題,然而想要優雅而高效的解決問題,就要學習數據結構和算法了。當然對數據結構和算法的理解在開發優秀的軟件時是非常重要的,與其同等重要的是在我們的開發工作中應用軟件工程中的一些良好准則。鄒欣老師(博客,微博,豆瓣)的《現代軟件工程講義》提到三個公式:
- 程序 = 數據結構+算法
- 軟件 = 程序 + 軟件工程
- 軟件企業 = 軟件 + 商業模式
我們學習編寫程序是要來解決實際問題的,編程的一般過程:
- 如何用數據形式描述問題?—即由問題抽象出一個適當的數學模型;
- 問題所涉及的數據量大小及數據之間的關系;
- 如何在計算機中存儲數據及體現數據之間的關系?
- 處理問題時需要對數據作何種運算?
- 所編寫的程序的性能是否良好?
上面所列舉的問題基本上要由數據結構和算法來解決了。
鄒老師說:
程序是基本功,但是除了程序之外,軟件工程決定了軟件的命運。
程序基本功除了編程語言外,還有數據結構和算法。
《數據結構與算法》是計算機科學中的一門綜合性專業基礎課。是介於數學、計算機硬件、計算機軟件三者之間的一門核心課程,不僅是一般程序設計的基礎,而且是設計和實現編譯程序、操作系統、數據庫系統及其他系統程序和大型應用程序的重要基礎。
數據結構和算法的研究是計算機科學的重要基石。這是一個富集優雅技術和復雜數學分析結果的領域。一個好的算法或數據結構可能使某個原來需要用成年累月才能完成的問題在分秒之中得到解決。在某些特殊領域,例如密碼學,圖形學、數據庫、語法分析、數值分析和模擬等等,解決問題的能力幾乎完全依賴於最新的算法和數據結構
數據結構
計算機是一門研究用計算機進行信息表示和處理的科學。這里面涉及到兩個問題:
- 信息的表示
- 信息的處理
信息的表示和組織又直接關系到處理信息的程序的效率。隨着應用問題的不斷復雜,導致信息量劇增與信息范圍的拓寬,使許多系統程序和應用程序的規模很大,結構又相當復雜。
日常生活中,人們習慣以松散的方式處理信息:比如板子上的釘子,筆記本中的便條,或者文件夾中的圖片。然而,用計算機處理信息需要將數據規則地組織起來。
數據有各種形式和大小,但通常它們可以以相同的方式來組織。
數據結構(Data Structure):是指相互之間具有(存在)一定聯系(關系)的數據元素的集合。
使用數據結構的三個原因是:效率、抽象和重用性。
- 效率
數據結構組織數據的方式使得算法變得更加高效。例如,考慮一下我們該如何組織數據以對其進行檢索。一種簡單的方式是將數據存放到數組中,然后遍歷其中每一個元素直到找到需要的元素為止。然而這種方式確是低效的,因為在很多情況下需要遍歷數組中的每一個元素才行。通過使用另一種數據結構,比如哈希表或者二叉樹我們可以顯著地提高檢索的速度。
- 抽象
數據結構使我們以一種更加容易理解的方式去看待數據。也就是說它們為解決問題提供了一層抽象概念。比如,要把數據存入一個棧,可以把精力集中在可以對棧做什么操作上,例如壓棧和出棧,而不是實現每種操作的具體細節上。換句話說,數據結構使我們以不那么“程序化”的方式看待程序。
- 重用性
數據結構是可重用的,因為它們應該是模塊化且上下文無關的。它們是模塊化的,因為每種數據結構都有各自指定的接口,通過它訪問數據結構中存儲的數據是受限的。也就是說,只能通過定義接口的操作來訪問數據。數據結構是上下文無關的,因為它們能在任意環境或上下文中應用於任意一種類型的數據之上。
數據結構的三個組成部分:
- 邏輯結構: 數據元素之間邏輯關系的描述
- 存儲結構: 數據元素在計算機中的存儲及其邏輯關系的表現稱為數據的存儲結構或物理結構。
- 數據操作: 對數據要進行的運算
元素之間的相互聯系(關系)稱為邏輯結構。數據元素之間的邏輯結構有四種基本類型:
- 集合:結構中的數據元素除了“同屬於一個集合”外,沒有其它關系。
- 線性結構(1:1):結構中的數據元素之間存在一對一的關系。
- 樹型結構(1:M):結構中的數據元素之間存在一對多的關系。
- 圖狀結構或網狀結構(M:N):結構中的數據元素之間存在多對多的關系。
數據結構在計算機內存中的存儲包括數據元素的存儲和元素之間的關系的表示。
元素之間的關系在計算機中有兩種不同的表示方法:順序表示和非順序表示。由此得出兩種不同的存儲結構:順序存儲結構和鏈式存儲結構。
- 順序存儲結構:用數據元素在存儲器中的相對位置來表示數據元素之間的邏輯結構(關系)。
- 鏈式存儲結構:在每一個數據元素中增加一個存放另一個元素地址的指針(pointer )或引用(reference),用該指針或引用來表示數據元素之間的邏輯結構(關系)。
在Java中,用一維數組表示順序存儲結構;用類來表示鏈式存儲結構。
數據類型(Data Type)指的是一個值的集合和定義在該值集上的一組操作的總稱。
數據類型是和數據結構密切相關的一個概念。在Java中數據類型有:基本類型和類類型(引用類型)。
數據結構不同於數據類型,也不同於數據對象,它不僅要描述數據類型的數據對象,而且要描述數據對象各元素之間的相互關系。
當面對數據結構時,我們通常會想到特定的行為或者操作,我們通常也希望對它們執行這些操作。例如,給定一個鏈表,我們會自然地想到插入、移除、遍歷和計算元素個數等操作。數據結構加上這些基本操作就稱為抽象數據類型(ADT)。一個抽象數據類型的操作就組成它的公共接口。抽象數據類型的公共接口精確地定義了我們可以對它做什么。建立並遵守抽象數據類型的接口是絕對必要的,因為這會使我們能更好地管理程序的數據,使得程序變得更容易理解也更容易維護。
在計算機科學領域中,一些最常用來組織數據的方式有:鏈表、棧、隊列、集合、哈希表、樹、堆、優先級隊列和圖。
算法
算法是定義良好的用來解決問題的步驟。在計算機科學領域中,算法是必不可少的,因為它們正是計算機完成系統操作所需要的具體步驟。好的算法就如同好的工具一樣,以合理的付出完成相應的工作。使用不當的或定義不清的算法就像用台鋸去切割一張紙,或者用剪刀去切夾板一樣:盡管工作也許能完成,但你不得不考慮完成工作的效率。
算法(Algorithm):是對特定問題求解方法(步驟)的一種描述,是指令的有限序列,其中每一條指令表示一個或多個操作。
算法具有以下五個特性
-
有窮性: 一個算法必須總是在執行有窮步之后結束,且每一步都在有窮時間內完成。
-
確定性:算法中每一條指令必須有確切的含義。不存在二義性。且算法只有一個入口和一個出口。
-
可行性: 一個算法是能行的。即算法描述的操作都可以通過已經實現的基本運算執行有限次來實現。
-
輸入: 一個算法有零個或多個輸入,這些輸入取自於某個特定的對象集合。
-
輸出: 一個算法有一個或多個輸出,這些輸出是同輸入有着某些特定關系的量。
一個算法可以用多種方法描述,主要有:使用自然語言描述;使用形式語言描述;使用計算機程序設計語言描述。
算法和程序是兩個不同的概念。一個計算機程序是對一個算法使用某種程序設計語言的具體實現。算法必須可終止意味着不是所有的計算機程序都是算法。
評價一個好的算法有以下幾個標准
- 正確性(Correctness ):算法應滿足具體問題的需求。
- 可讀性(Readability):算法應容易供人閱讀和交流。可讀性好的算法有助於對算法的理解和修改。
- 健壯性(Robustness): 算法應具有容錯處理。當輸入非法或錯誤數據時,算法應能適當地作出反應或進行處理,而不會產生莫名其妙的輸出結果。
- 通用性(Generality): 算法應具有一般性 ,即算法的處理結果對於一般的數據集合都成立。
- 效率與存儲量需求: 效率指的是算法執行的時間;存儲量需求指算法執行過程中所需要的最大存儲空間。一般地,這兩者與問題的規模有關。
和數據結構一樣,使用算法也有3個原因:效率、抽象和重用性。
-
效率
由於特定類型的問題經常在計算機領域出現,隨着時間的推移人們已經找到了高效的方法來解決這類問題。比如,試想一下要對一本書中的索引號排序。因為排序是一項常見的任務,所以對於有許多高效的算法可以完成排序。 -
抽象
在解決問題時,算法能夠提供一定程度的抽象,因為很多看似復雜的問題都可以用已存在的著名算法來簡化。一旦我們能用簡化的眼光去看待復雜的問題,更為復雜的問題就可以看做一個更為簡單問題的抽象。例如,試想一下如何找到互聯網上兩個網關之間數據包的最短路由。一旦我們意識到這個問題不過就是更具一般性的單對最短路徑問題的變種時,我們就能夠以這種泛化的方式來解決問題。 -
重用性
算法在很多不同場景下能夠得到重用。因為很多著名的算法解決的問題都是由復雜的問題抽象而來的,這也是因為很多復雜的問題都能夠簡化為簡單的問題。一些能夠有效解決這類特定問題的方法使我們有可能解決更多其他的問題。
算法設計的一般方法
從廣義上講,很多算法解決問題的思路是相同的。因此,為了方便,通常按照算法采用的方法和思路來給它們分類。這樣給算法分類的一個原因是:如果我們理解了它采用的一般思路我們常常就能夠對該算法獲得一些深入的了解。在解決一些沒有現成算法求解,但與現有問題類似的問題時,我們從中可以得到一些啟發和靈感。當然,有些算法有悖於分類原則,而另一些則是多種方法相結合的產物。這一節將介紹一些常用的方法。
- 隨機法
隨機法依賴於隨機數的統計特性。一個應用隨機法的例子是快速排序。
- 分治法
分治法包含3個步驟:分解、求解與合並。在分解階段,將數據分解為更小、更容易管理的部分。在求解階段,對每個分解出的部分進行處理。在合並階段,將每部分處理的結果進行合並。一個分治法的例子是歸並排序。
- 動態規划
動態規划同分治法類似,都是將較大的問題分解為子問題最后再將結果合並。然而,它們處理問題的方式與子問題之間的關系有關。在分治法中,每一個子問題都是獨立的。為此,我們以遞歸的方式解決每一個子問題,然后將結果與其他子問題的結果合並。在動態規划中,子問題之間並不是獨立的。換句話說,子問題之間可能有關聯。這類問題采用動態規划法比分治法更合適。因為若用分治法來解決這類問題會多做很多不必要的工作,有些子問題會重復計算多次。
- 貪心法
貪心法在求解問題時總能夠做出在當前的最佳選擇。換句話說,不是從整體最優上考慮,而僅僅是在某種意義上的局部最優解。遺憾的是,當前的最優解長遠來看卻未必是最優的。因此,貪心法並不會一直產生最優結果。然而,在某些方面來說,貪心法確是最佳選擇。一個采用貪心法的例子是霍夫曼編碼,這是一個數據壓縮算法。
- 近似法
近似法並不計算出最優解,相反,它只計算出“足夠好”的解。通常利用近似法解決那些計算成本很高又因為其本身十分有價值而不願放棄的問題。推銷員問題是一個通常會用近似法去解決的問題。
算法效率度量
算法執行時間需通過依據該算法編制的程序在計算機上運行所消耗的時間來度量。其方法通常有兩種:
- 事后統計:
計算機內部進行執行時間和實際占用空間的統計。
問題:必須先運行依據算法編制的程序;依賴軟硬件環境,容易掩蓋算法本身的優劣;沒有實際價值。
事前分析:求出該算法的一個時間界限函數。
- 事前分析:
求出該算法的一個時間界限函數。
與此相關的因素有:依據算法選用何種策略;問題的規模;程序設計的語言;編譯程序所產生的機器代碼的質量;機器執行指令的速度;
撇開軟硬件等有關因素,可以認為一個特定算法“運行工作量”的大小,只依賴於問題的規模(通常用n表示),或者說,它是問題規模的函數。
算法中基本操作重復執行的次數是問題規模n的某個函數,其時間量度記作 T(n)=O(f(n)),稱作算法的漸近時間復雜度(Asymptotic Time complexity),簡稱時間復雜度。
一般地,常用最深層循環內的語句中的原操作的執行頻度(重復執行的次數)來表示。
以下六種計算算法時間的多項式是最常用的。其關系為:
O(1)<O(㏒n)<O(n)<O(n㏒n)<O(n的平方)<O(n的立方)
指數時間的關系為:O(2的n次方)<O(n!)<O(n的n次方)
當n取得很大時,指數時間算法和多項式時間算法在所需時間上非常懸殊。因此,只要有人能將現有指數時間算法中的任何一個算法化簡為多項式時間算法,那就取得了一個偉大的成就。
我們還應該區分算法的最壞情況的行為和期望行為。要定義好“期望”的意義非常困難,
因為它依賴於對可能出現的輸入有什么假定。另一方面,我們通常能夠比較精確
地了解最壞情況,雖然有時它會造成誤解。
常見的一些時間復雜度:
Java Collections APIs
如果你正要進入一個新領域去開發程序,那么首先需要弄清楚在這里已經有了些什么,以免無謂地把時間浪費在別人早已做好的東西上。
每個程序都要依靠算法與數據結構,但很少有程序依賴於必須發明一批全新的東西。即使是很復雜的程序,比如在編譯器或者網絡瀏覽器里,主要的數據結構也是數組、表、樹和散列表等等。如果在一個程序里要求某些更精巧的東西,它多半也是基於這些簡單東西構造起來的。因此,對大部分程序員而言,所需要的是知道有哪些合適的、可用的算法和數據結構,知道如何在各種可以互相替代的東西之中做出選擇。
候捷老師在《 深入淺出MFC 2e(電子版)》中引用林語堂先生的一句話:
只用一樣東西,不明白它的道理,實在不高明
只知道How,不知道Why,出了一點小問題時就無能為力了。我們課上鼓勵大家在Linux下學習編程,盡量在命令行中編輯/編譯/調試程序,Git的使用,數據庫的管理都先會命令方式下使用,這樣在IDE中,在GUI界面中出了問題,我們有更好的方法查找。
現在我們遇到另外一個極端,不會用一樣東西,卻想要明白它的道理,實在太難。比如有的同學連Linux都沒用過,卻想弄明白Linux內核。
大多數學生學到的第一門語言是語言,用C語言進行《數據結構》的教學好像是順理成章的事情,
然而由於C語言非常簡單,C庫也只有100多個函數,C語言沒有C++的STL庫,Java的Collection API這樣的東西,用C語言進行《數據結構》教學有"不會用一樣東西,卻想要明白它的道理,實在太難"的問題。學完數據結構后,遇到數組進行排序的問題,大多同學的反應是自己寫個冒泡排序程序,不知道C語言中有qsort函數,更不用說會用了。
使用Java進行《數據結構》教學有個好處是:我們可以先使用數據結構,就是Java的Collection API,然后現深入實現細節。
在編寫面向對象程序的時候,常常需要操作一組對象。我們了解了數組是用來組織相同類型的對象的。遺憾的是,數組缺乏快速開發應用程序所需要的靈活性。例如,數組不能修改其大小。好在Java帶有一組接口和類,使得操作成組的對象更為容易。
集合( collection)是將其他對象組織到一起的一個對象。集合也叫作容器(container),它提供了一種方法來存儲、訪問和操作其元素。集合幫助 Java 程序員很容易地管理對象。Java程序員應該熟悉集合框架中一些重要的類型,它們在java.util包中。
集合框架中的主要類型,當然是 Collection 接口。 List、 Set 和 Queue 是 Collection 的 3 個主要的子接口。此外,還有一個 Map 接口,它可以用於存儲鍵/值對。 Map 的一個子接口 SortedMap,保證了鍵按照升序排列。 Map 的其他實現還有 AbstractMap 及其具體的實現 HashMap。其他的接口包括 Iterator 和Comparator。后者使得對象成為可排序和可比較的。
集合框架的大多數接口都帶有實現類。有時候,一個實現有兩個版本,同步的版本和非同步的版本。例如,java.util.Vector 類和 ArrayList 類實現了 List 接口。Vector 和 ArrayList 都提供了類似的功能,但是 Vector是同步的,而 ArrayLis 是非同步的。
Collection 接口
數據結構的主要運算包括:
- 建立(Create)一個數據結構;
- 消除(Destroy)一個數據結構;
- 從一個數據結構中刪除(Delete)一個數據元素;
- 把一個數據元素插入(Insert)到一個數據結構中;
- 對一個數據結構進行訪問(Access);
- 對一個數據結構(中的數據元素)進行修改(Modify);
- 對一個數據結構進行排序(Sort);
- 對一個數據結構進行查找(Search);
- ...
我們看看java的Collection接口:
Collection 接口將對象組織到一起。數組不能調整大小,並且只能組織相同類型的對象,而Collections允許添加任何類型的對象,並且不強迫你指定初始大小。
Collection 帶有一些很容易使用的方法:
- 要添加一個元素,使用 add 方法
- 要添加另一個 Collection 的成員,使用 addAll
- 要刪除所有的元素,使用 clear 方法
- 要查詢 Collection 中的元素的數目,調用其 size 方法
- 要測試一個 Collection 是否包含一個元素,使用 isEmpty 方法
- 要把集合元素放入到一個數組中,使用方法 toArray。
需要注意的重要的一點是, Collection 擴展了 Iterable 接口, Collection 從那里繼承了 iterator 方法。該方法返回一個 Iterator對象,可以用來遍歷集合的元素。我們還會學習如何使用 for 循環來遍歷一個 Collection 的元素。
List 和 ArrayList
List 是 Collection 最為常用的接口,而 ArrayList 是最為常用的 List 的實現。List 又叫作序列( sequence),它是一個有序的集合。你可以使用索引來訪問其元素,而且可以在確切的位置插入一個元素。一個 List 的索引0引用其第1個元素,索引1引用第 2個元素,依次類推。
繼承自 Collection 的 add 方法,將指定的元素添加到列表的末尾。該方法的簽名如下:
public boolean add(java.lang.Object element)
如果添加成功,該方法返回 true。否則,它返回 false。 List 的一些實現(如 ArrayList)允許添加空的元素,有些實現則不允許。
List 使用如下的簽名添加另一個 add 方法:
public void add(int index, java.lang.Object element)
可以用這個 add 方法在任何位置插入一個元素。
此外,可以分別使用 set 和 remove 方法來替換和刪除一個元素。
public java.lang.Object set(int index, java.lang.Object element)
public java.lang.Object remove(int index)
set 方法用 element 來替換 index 所指定的位置的元素,並且返回插入的元素的索引。 remove 方法刪除指定的位置的元素,並且返回對刪除的元素的一個引用。
要創建一個 List,通常將一個 ArrayList 對象賦值給 List 引用變量。
List myList = new ArrayList();
ArrayList 的無參數構造方法創建了一個ArrayList對象,具有10個元素的初始容量。如果你添加的元素超出其容量,這個大小將會自動增加。如果你知道ArrayList中的元素數目將會大於其容量,可以使用其第 2 個構造方法:
public ArrayList(int initialCapacity)
此方法將會產生一個略微快一些的 ArrayList,因為這個實例不必去增加容量。
List 允許你存儲重復的元素,這意味着,可以存儲兩個或多個指向相同元素的引用。下面代碼展
示了 List 及其一些方法的應用:
import java.util.ArrayList;
import java.util.List;
public class ListDemo1 {
public static void main(String[] args) {
List myList = new ArrayList();
String s1 = "Hello";
String s2 = "Hello";
myList.add(100);
myList.add(s1);
myList.add(s2);
myList.add(s1);
myList.add(1);
myList.add(2, "World");
myList.set(3, "Yes");
myList.add(null);
System.out.println("Size: " + myList.size());
for (Object object : myList) {
System.out.println(object);
}
}
}
運行程序,這段代碼在控制台顯示如下的結果。
Size: 7
100
Hello
World
Yes
Hello
1
null
java.util.Arrays 類提供了一個asList方法,它允許你一次向一個List添加數組或任意多個元素。例如,如下的代碼段一次添加了多個 String 類型的數據:
List members = Arrays.asList("Chuck", "Harry", "Larry", "Wang");
然而, Arrays.asList 返回具有固定大小的 List, 這意味着,你不能向其添加成員。
List 還增加了方法來搜索集合,即 indexOf 和 lastIndexOf:
public int indexOf(java.lang.Object obj)
public int lastIndexOf(java.lang.Object obj)
indexOf 方法從第一個元素開始使用 equals 方法,以比較 obj 參數及其元素,並且返回第 1 次匹配的索引。 lastIndexOf 做同樣的事情,但是,其比較是從最后一個元素到第 1 個元素。如果沒有找到匹配, indexOf和 lastIndexOf 方法都返回-1。
java.util. Collections 類是一個輔助類或工具類,它提供了靜態方法來操作 List 和其他的 Collection。例如,你可以使用 List 的 sort 方法很容易地對一個 List進行排序,如下面代碼所示。
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
public class ListDemo2 {
public static void main(String[] args) {
List numbers = Arrays.asList(9, 4, -9, 100);
Collections.sort(numbers);
for (Object i : numbers) {
System.out.println(i);
}
}
}
如果運行這個 ListDemo2 類,將會在控制台看到如下內容。
-9
4
9
100
使用 Iterator 和 for 遍歷一個Collection
在操作集合的時候,遍歷一個Collection是最常見的任務之一。有兩種方法可能做到這一點,使用 Iterator或使用 for。
Collection 擴展了 Iterable,后者有一個方法iterator。這個方法返回一個java.util.Iterator,可以用來遍歷 Collection。 Iterator 接口有如下的方法:
- hasNext。 Iterator 使用了一個內部指針,其最初指向第1個元素之前的一個位置。如果在指針的后面還有更多的元素,hasNext 返回 true。調用 next 會將這個指針移動到下一個元素。第 1 次在 Iterator上調用 next,會導致其指針指向第 1 個元素。
- next。將內部指針移動到下一個元素並返回該元素。在最后一個元素返回之后調用 next,將會拋出一個 java.util.NoSuchElementException。因此,在調用 next 之前,先調用 hasNext 方法測試是否還有下一個元素,這樣做是比較安全的。
- remove。刪除內部指針所指向的元素。
一個 Iterator 遍歷 Collection 的常用方式是使用 while 或 for。假設 myList是你想要遍歷的 ArrayList 。如下的代碼段使用 while 語句來遍歷一個集合,並且打印出集合中的每一個元素。
Iterator iterator = myList.iterator();
while (iterator.hasNext()) {
String element = (String) iterator.next();
System.out.println(element);
}
相同的代碼是:
for (Iterator iterator = myList.iterator(); iterator.hasNext(); ) {
String element = (String) iterator.next();
System.out.println(element);
}
for 語句可以遍歷一個 Collection 而不需要調用 iterator 方法。其語法如下:
for (Type identifier : expression) {
statement(s)
}
這里的 expression 必須是一個 Iterable。由於 Collection擴展了Iterable,你可以使用增強的 for 來遍歷任何 Collection。例如,如下的代碼展示了如何使用 for。
for (Object object : myList) {
System.out.println(object);
}
使用 for 遍歷一個集合是使用Iterator的快捷方式。實際上,以上使用for的代碼,會被編譯器翻譯為如下的代碼。
for (Iterator iterator = myList.iterator(); iterator.hasNext(); ) {
String element = (String) iterator.next();
System.out.println(element);
}
Set 和 HashSet
Set 表示一個數學的集。和 List 不同, Set 不允許重復的內容。假設兩個元素, e1 和 e2,如果 e1.equals(e2)的話,它們是不能在 Set中同時存在的。如果試圖添加一個重復的元素, Set 的 add 方法會返回 false。例如,如下的代碼會打印出“ addition failed”。
Set set = new HashSet();
set.add("Hello");
if (set.add("Hello")) {
System.out.println("addition successful");
} else {
System.out.println("addition failed");
}
第 1 次調用 add 的時候,添加了字符串“Hello”。第2次調用的時候,將會失敗,因為要添加另一個“ Hello”將會導致 Set 中有重復的元素。
Set 允許最多有一個空元素。有些實現不允許有空元素。例如,Set最流行的實現HashSet,允許最多有一個空元素。當使用 HashSet 的時候,需要注意,並不能保證元素的順序保持不變。 HashSet 應該是 Set的首選,因為它比 Set 的其他實現( TreeSet 和 LinkedHashSet)要快.
Queue 和 LinkedList
Queue 通過添加支持按照先進先出(first-in-first-out,FIFO)的方式排序元素的方法,擴展了 Collection。FIFO 意味着,當獲取元素的時候,最先添加的元素將會是第一個元素。這和 List 不同,在 List 中,是通過傳給其 get 方法一個索引來選擇要訪問的元素。
Queue 添加了如下的方法。
-
offer。這個方法就像 add方法一樣用來添加一個元素。但是,如果添加一個元素有可能會失敗的話,應該使用 offer。如果添加一個元素失敗的話,offer會返回false,並且不會拋出一個異常。如果使用 add 添加失敗,會拋出一個異常。
-
remove。該方法刪除並返回Queue頭部的元素。如果Queue為空,該方法拋出一個java.util.NoSuchElementException。
-
poll。該方法就像是 remove 方法一樣。但是,如果 Queue為空,它將會返回空而不會拋出一個
異常。 -
element。該方法返回 Queue 頭部的元素但不會刪除它。 但是,如果Queue為空,該方法拋出一
個 java.util.NoSuchElementException。 -
peek。返回 Queue 頭部的元素但不會刪除它。 但是,如果 Queue 為空,該方法將會返回空。
當在一個 Queue 上調用 add 或 offer 方法的時候,該元素總是添加在 Queue的末尾。要訪問這個元素,可以使用 remove 或 poll 方法。 remove 和 poll 方法總是刪除並返回 Queue 頭部的元素。
例如,如下的代碼創建了一個 LinkedList(這是 Queue 的一個實現)來展示 Queue 的 FIFO 特性。
Queue queue = new LinkedList();
queue.add("one");
queue.add("two");
queue.add("three");
System.out.println(queue.remove());
System.out.println(queue.remove());
System.out.println(queue.remove());
這段代碼產生的結果如下:
one
two
three
上面代碼展示了 remove 總是刪除 Queue 頭部的元素。換句話說,你不能在刪除“ one”和“ two”之前刪除“ three”( Queue 的第 3 個元素)。
與Queue類似,java.util.Stack類是一個Collection,其行為是后進先出(last-in-first-out,LIFO)的方式。
集合轉換
Collection 實現通常有一個構造函數,它接受一個Collection對象。這使得你可以將一個Collection 轉換為另一個不同類型的 Collection。如下是這個構造方法的一些實現:
public ArrayList(Collection c)
public HashSet(Collection c)
public LinkedList(Collection c)
作為示例,如下的代碼把一個 Queue 轉換為一個 List。
Queue queue = new LinkedList();
queue.add("Hello");
queue.add("World");
List list = new ArrayList(queue);
如下的代碼把一個 List 轉換為一個 Set。
List myList = new ArrayList();
myList.add("Hello");
myList.add("World");
myList.add("World");
Set set = new HashSet(myList);
myList 有 3 個元素,其中的兩個是重復的。因為Set並不允許重復的元素,因此,只有一個重復的元素會被接受。因此,上面的 Set 只有兩個元素。
Map 和 HashMap
Map 保存了鍵到值的映射。Map中不能有重復的元素,並且每個鍵最多映射一個值。要給Map添加鍵/值對,需要用 put 方法。其簽名如下所示:
public void put(java.lang.Object key, java.lang.Object value)
注意,鍵和值都不能是基本類型。但是,在如下的代碼中,為鍵和值都傳遞基本類型是合法的,因為在調用 put 方法之前會先執行裝箱操作。
map.put(1, 3000);
此外,也可以使用 putAll 方法並傳入一個 Map 參數。
public void putAll(Map map)
可以通過給 remove 方法傳遞鍵來刪除一個映射。
public void remove(java.lang.Object key)
要刪除所有的映射,可以使用 clear方法。要得到映射的數目,可以使用size方法。此外,如果有 0個映射的話, isEmpty 方法會返回 true。
要獲取一個值,給 get 方法傳入一個鍵:
public java.lang.Object get(java.lang.Object key)
除了目前為止所討論的方法,還有 3 個無參數方法,能夠提供查看 Map 的功能。
- keySet。返回包含 Map 中的所有鍵的一個 Set。
- values。返回包含 Map 中的所有值的一個 Collection。
- entrySet。返回包含了Map.Entry對象的一個Set,其中每個Map.Entry對象表示一個鍵/值對。Map.Entry接口提供了 getKey 方法,它能夠返回鍵的部分;還有 getValue 方法,能夠返回值的部分。
java.util 包中有 Map 的幾個實現,最常使用的是HashMap和Hashtable。HashMap是非同步的,Hashtable是同步的。因此, HashMap 是二者中更快的一個。
如下的代碼展示了 Map 和 HashMap 的用法。
Map map = new HashMap();
map.put("1", "one");
map.put("2", "two");
System.out.println(map.size()); //prints 2
System.out.println(map.get("1")); //prints "one"
Set keys = map.keySet();
// print the keys
for (Object object : keys) {
System.out.println(object);
}
使得對象可比較和可排序
在現實世界中,當我們說“我的汽車和你的汽車一樣”的時候,我的意思是說,我的車和你的車是同
樣的型號,一樣的新,具有相同的顏色等。
在 Java 中,我們使用引用對象的變量來操作對象。引用變量並不包含對象,而是包含了對象在內存中的地址,因此,當你比較兩個引用變量 a 和 b 的時候,可以使用下面的代碼。
if (a == b)
實際上,你是在詢問 a 和 b 是否引用同一個變量,而不是說 a 和 b 引用的對象是否是相同的。
考慮如下的示例。
Object a = new Object();
Object b = new Object();
a 引用的對象的類型和 b 引用的對象的類型相同。
但是, a 和 b 引用了兩個不同的實例,而且 a 和 b 包含了不同的內存地址。因此,( a == b)返回 false。
這種比較對象引用的方式很難有用,因為大多數時候,我們更關心對象,而不是對象的地址。如果你想要比較對象,需要找到該類所提供的專門比較對象的方法。例如,要比較兩個 String 對象,可以調用其equals方法。能否比較兩個對象,取決於該對象的類是否支持比較。一個類可以通過實現它從 java.lang.Object繼承而來的 equals 和 hashCode 方法來支持對象的比較。
此外,可以通過實現 java.lang.Comparable和java.util.Comparator接口讓對象成為可比較的。
使用 java.lang.Comparable
java.util.Arrays 類提供了靜態的方法 sort,它可以排序對象的一個數組。該方法的簽名如下。
public static void sort(java.lang.Object[]a)
由於所有的 Java 類都派生自 java.lang.Object,所有的 Java 對象都是 java.lang.Object 類型的。這意味着,可以將任何對象的數組傳遞給 sort 方法。
和數組類似, java.util.Collections 類也有一個 sort 方法用來排序 List。
sort 方法怎么知道如何去排序任意的對象呢?排序數字或字符串很容易,但是,如何排序 Elephant 對象的一個數組呢?
首先,看一下如下代碼中的 Elephant 類。
public class Elephant {
public float weight;
public int age;
public float tuskLength; // in centimeters
}
作為 Elephant 類的編寫者,由你來決定想要讓Elephant對象如何排序。假設你想要根據其體重和年齡來排序。現在,如何告訴 Arrays.sort 或 Collections.sort 你的決定呢?
這兩個 sort 方法都定義了它們自身和需要排序的對象之間的一個協議。這個協議的形式是java.lang.Comparable 接口。
package java.lang;
public interface Comparable {
public int compareTo(Object obj);
}
需要通過 Arrays.sort 或 Collections.sort 來支持排序的任何類,都必須實現 Comparable 接口。在上面代碼中, compareTo方法中的參數obj引用了需要和該對象進行比較的對象。如果該對象比參數對象大,在實現類中,實現該方法的代碼必須返回一個正值;如果兩個對象相等,這個方法返回 0;如果該對象比參數對象小,該方法返回一個負值。
下面代碼給出了一個修改了的 Elephant 類,它實現了 Comparable。
public class Elephant implements Comparable {
public float weight;
public int age;
public float tuskLength;
public int compareTo(Object obj) {
Elephant anotherElephant = (Elephant) obj;
if (this.weight > anotherElephant.weight) {
return 1;
} else if (this.weight < anotherElephant.weight) {
return -1;
} else {
// both elephants have the same weight, now
// compare their age
return (this.age - anotherElephant.age);
}
}
}
既然 Elephant 實現了 Comparable 接口,可以使用 Arrays.sort 或 Collections.sort 來排序 Elephant 對象的一個數組或 list。 sort 方法會將Elephant對象當作一個Comparable對象(因為 Elephant 實現了 Comparable,一個Elephant對象可以被認為是Comparable類型的),並且在該對象上調用 compareTo 方法。sort方法重復這一過程,直到數組中的Elephant對象都已經按照其體重和年齡正確地組織好了。
下面代碼提供了一個類來測試 Elephant 對象上的 sort 方法。
import java.util.Arrays;
public class ElephantTest {
public static void main(String[] args) {
Elephant elephant1 = new Elephant();
elephant1.weight = 100.12F;
elephant1.age = 20;
Elephant elephant2 = new Elephant();
elephant2.weight = 120.12F;
elephant2.age = 20;
Elephant elephant3 = new Elephant();
elephant3.weight = 100.12F;
elephant3.age = 25;
Elephant[] elephants = new Elephant[3];
elephants[0] = elephant1;
elephants[1] = elephant2;
elephants[2] = elephant3;
System.out.println("Before sorting");
for (Elephant elephant : elephants) {
System.out.println(elephant.weight + ":" +
elephant.age);
}
Arrays.sort(elephants);
System.out.println("After sorting");
for (Elephant elephant : elephants) {
System.out.println(elephant.weight + ":" +
elephant.age);
}
}
}
如果運行 ElephantTest 類,將會在控制台看到如下內容。
Before sorting
100.12:20
120.12:20
100.12:25
After sorting
100.12:20
100.12:25
120.12:20
像 java.lang.String、 java.util.Date 這樣的類,以及基本類型的包裝器類,都實現了 java.lang.Comparable。
這就說明了能夠排序它們的原因。
使用 Comparator
實現 java.lang.Comparable接口使得你可以定義一種方式來比較類的實例。但是,對象有時候需要以更多的方式進行比較。例如,兩個Person對象可能需要按照年齡、姓氏和名字進行比較。對於類似這樣的情況,你需要創建一個Comparator實例,它定義了應該如何比較兩個對象。要讓對象可以按照兩種方式比較,就需要兩個Comparator 實例。有了Comparator,我們就可以比較兩個對象,即便它們的類沒有實現 Comparable 接口。
要創建一個 Comparator 對象,編寫一個實現了 Comparator 接口的類。然后,提供其 compare 方法的實現。該方法簽名如下:
public int compare(java.lang.Object o1, java.lang.Object o2)
如果 o1 和 o2 相等,compare返回0;如果o1小於o2,它返回一個負整數;如果o1大於o2,返回一個正整數。
下面示例實現了 Comparable接口,展示了Person對象的兩個Comparator(按照姓氏比較和按照名字比較)。
實現了 Comparable 的 Person 類
public class Person implements Comparable {
private String firstName;
private String lastName;
private int age;
public String getFirstName() {
return firstName;
}
public void setFirstName(String firstName) {
this.firstName = firstName;
}
public String getLastName()
return lastName;
}
public void setLastName(String lastName) {
this.lastName = lastName;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
public int compareTo(Object anotherPerson throws ClassCastException {
if (!(anotherPerson instanceof Person)) {
throw new ClassCastException("A Person object expected.");
}
int anotherPersonAge = ((Person) anotherPerson).getAge();
return this.age - anotherPersonAge;
}
}
LastNameComparator 類
import java.util.Comparator;
public class LastNameComparator implements Comparator {
public int compare(Object person, Object anotherPerson) {
String lastName1 = ((Person)
person).getLastName().toUpperCase();
String firstName1 =
((Person) person).getFirstName().toUpperCase();
String lastName2 = ((Person)
anotherPerson).getLastName().toUpperCase();
String firstName2 = ((Person) anotherPerson).getFirstName()
.toUpperCase();
if (lastName1.equals(lastName2)) {
return firstName1.compareTo(firstName2);
} else {
return lastName1.compareTo(lastName2);
}
}
}
FirstNameComparator 類
import java.util.Comparator;
public class FirstNameComparator implements Comparator {
public int compare(Object person, Object anotherPerson) {
String lastName1 = ((Person)
person).getLastName().toUpperCase();
String firstName1 = ((Person)
person).getFirstName().toUpperCase();
String lastName2 = ((Person)
anotherPerson).getLastName().toUpperCase();
String firstName2 = ((Person) anotherPerson).getFirstName()
.toUpperCase();
if (firstName1.equals(firstName2)) {
return lastName1.compareTo(lastName2);
} else {
return firstName1.compareTo(firstName2);
}
}
}
PersonTest 類
import java.util.Arrays;
public class PersonTest {
public static void main(String[] args) {
Person[] persons = new Person[4];
persons[0] = new Person();
persons[0].setFirstName("Elvis");
persons[0].setLastName("Goodyear");
persons[0].setAge(56);
persons[1] = new Person();
persons[1].setFirstName("Stanley");
persons[1].setLastName("Clark");
persons[1].setAge(8);
persons[2] = new Person();
persons[2].setFirstName("Jane");
persons[2].setLastName("Graff");
persons[2].setAge(16);
persons[3] = new Person();
persons[3].setFirstName("Nancy");
persons[3].setLastName("Goodyear");
persons[3].setAge(69);
System.out.println("Natural Order");
for (int i = 0; i < 4; i++) {
Person person = persons[i];
String lastName = person.getLastName();
String firstName = person.getFirstName();
int age = person.getAge();
System.out.println(lastName + ", " + firstName +
". Age:" + age);
}
Arrays.sort(persons, new LastNameComparator());
System.out.println();
System.out.println("Sorted by last name");
for (int i = 0; i < 4; i++) {
Person person = persons[i];
String lastName = person.getLastName();
String firstName = person.getFirstName();
int age = person.getAge();
System.out.println(lastName + ", " + firstName +
". Age:" + age);
}
Arrays.sort(persons, new FirstNameComparator());
System.out.println();
System.out.println("Sorted by first name");
for (int i = 0; i < 4; i++) {
Person person = persons[i];
String lastName = person.getLastName();
String firstName = person.getFirstName();
int age = person.getAge();
System.out.println(lastName + ", " + firstN
". Age:" + age);
}
Arrays.sort(persons);
System.out.println();
System.out.println("Sorted by age");
for (int i = 0; i < 4; i++) {
Person person = persons[i];
String lastName = person.getLastName();
String firstName = person.getFirstName();
int age = person.getAge();
System.out.println(lastName + ", " + firstN
". Age:" + age);
}
}
}
如果運行 PersonTest 類,將會得到如下的結果。
Natural Order
Goodyear, Elvis. Age:56
Clark, Stanley. Age:8
Graff, Jane. Age:16
Goodyear, Nancy. Age:69
Sorted by last name
Clark, Stanley. Age:8
Goodyear, Elvis. Age:56
Goodyear, Nancy. Age:69
Graff, Jane. Age:16
Sorted by first name
Goodyear, Elvis. Age:56
Graff, Jane. Age:16
Goodyear, Nancy. Age:69
Clark, Stanley. Age:8
Sorted by age
Clark, Stanley. Age:8
Graff, Jane. Age:16
Goodyear, Elvis. Age:56
Goodyear, Nancy. Age:69
Java 泛型
參考資料
歡迎關注“rocedu”微信公眾號(手機上長按二維碼)
做中教,做中學,實踐中共同進步!
-
原文地址:http://www.cnblogs.com/rocedu/p/java-collections-generic.html
-
版權聲明:自由轉載-非商用-非衍生-保持署名| Creative Commons BY-NC-ND 3.0