程序師世界是廣大編程愛好者互助、分享、學習的平台,程序師世界有你更精彩!
首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫知識
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
 程式師世界 >> 編程語言 >> JAVA編程 >> 關於JAVA >> 深入理解Collections API

深入理解Collections API

編輯:關於JAVA

一個 List l 可能被做如下排序:

Collections.sort(l);

如果這個 list 由 String 元素所組成, 那麼它將按詞典排序法(按字母順序)進行排序; 如果它是由 Date 元素所組成, 那麼它將按年代順序來排序。Java 怎麼會知道該怎麼做呢? 這一定是個魔術! 其實不然。實際上, String 和 Date 均實現了Comparable接口。Comparable 接口為一個類提供一個自然排序( natural ordering), 它允許那個類的對象被自動排序。下表列出了實現了Comparable 的JDK類:

類 自然排序

Byte 帶符號的數字排序

Character 不帶符號的數字排序

Long 帶符號的數字排序

Integer 帶符號的數字排序

Short 帶符號的數字排序

Double 帶符號的數字排序

Float 帶符號的數字排序

BigInteger 帶符號的數字排序

BigDecimal 帶符號的數字排序

File 依賴系統的按路徑名字母順序排序

String 按字母順序排序

Date 按年代順序排序

CollationKey 特定字符集按字母順序排序

如果你要為一個其元素沒有實現 Comparable的列表排序,Collections.sort(list) 將扔出一個 ClassCastException。類似的,如果你要為一個其元素沒有作相互比較的列表進行排序, Collections.sort 將扔出一個 ClassCastException. 能夠被相互比較的元素被稱作 mutually comparable(可相互比較的)。雖然不同類型的元素有可能被相互比較,但以上列出的任何JDK類型都不允許在類之間的比較 (inter-class comparison)。

如果你只是要為可比較的元素的列表進行排序,或為它們創建排序的對象集, 則這就是你實際需要了解的全部有關 Comparable 接口的內容。如果你要實現你自己的 Comparable 類型,則下一節將會引起你的興趣。

編寫你自己的 Comparable 類型

Comparable 接口由一個單一的方法構成:

public interface Comparable {

public int compareTo(Object o);

}

compareTo 方法將接收對象與特定對象進行比較,並在接收對象小於、等於或大於特定對象時分別返回負整數、空或一個正整數。如果特定對象不能與接收對象相比較,該方法扔出一個ClassCastException. 這是一個表示某人姓名的類(a class representing a person′s name), 它實現了 Comparable:

import java.util.*;
public class Name implements Comparable {
private String firstName, lastName;
public Name(String firstName, String lastName) {
if (firstName==null || lastName==null)
throw new NullPointerException();
this.firstName = firstName;
this.lastName = lastName;
}
public String firstName() {return firstName;}
public String lastName() {return lastName;}
public boolean equals(Object o) {
if (!(o instanceof Name))
return false;
Name n = (Name)o;
return n.firstName.equals(firstName) &&
n.lastName.equals(lastName);
}
public int hashCode() {
return 31*firstName.hashCode() + lastName.hashCode();
}
public String toString() {return firstName + " " + lastName;}
public int compareTo(Object o) {
Name n = (Name)o;
int lastCmp = lastName.compareTo(n.lastName);
return (lastCmp!=0 ? lastCmp :
firstName.compareTo(n.firstName));
}
}

為了使這個例子短一些,該類受到了一點限制:它不支持中間名,它要求必須同時具有first name 和 last name, 而這不是在全世界都通用的。盡管如此,這個例子仍有幾個重要之處:

Name 對象是不變的( immutable)。作為相等、不變類型的所有其它事情就是如何做的問題,特別是對那些將被用來作為 Sets 中的元素或 Maps 中的鍵的對象來說,更是如此。如果你對這些 對象集 中的元素或鍵做了更改,這些 對象集 將中斷。

構造函數可檢查它的參數是否為 null。這可以保證所有的Name 對象都能很好地形成。因而沒有其它方法會扔出NullPointerException.

hashCode 方法被重新定義。對重新定義 equals 方法的任意類來說,這是必需的(essential)。一般約定(general contract)需要 Object.equals. (Equal 對象必須具有相等的哈希代碼) 。

如果特定對象為 null,或一個不適當的類型, equals 方法則返回 false。在這種情況下, compareTo 方法扔出一個運行時異常。這兩個行為都是各自方法的一般約定所必需的。

toString 方法已被重新定義,從而可以以人們能夠讀懂的形式打印 Name 。這總是一個好主意,特別是對要被放入對象集 中的對象來說,更有益處。各種 對象集 類型的 toString 方法依賴它們的元素、鍵和值的 toString 方法。

由於這一節介紹的是有關元素排序的問題,因而讓我們稍微多談一點 Name 的 compareTo 方法。它實現標准的姓名-排序算法,在該算法中,last name 優先於 first name。這恰恰是你在一個natural ordering(自然排序)中所想要的。如果自然排序不自然,那才容易引起混亂呢!

請看 compareTo 是如何被實現的,因為它是相當典型的。首先,你將 Object 參數轉換為適當類型; 如果參數類型是不適當的,則會扔出一個適當的異常(ClassCastException);那麼你應該比較對象的最重要部分(在此案例中為 last name)。通常,你可以使用該部分的類型的自然排序。在次案例中,該部分是一個 String, 並且自然的(按詞典順序的)排序正是所要求的。如果比較的結果是空(它表示等同性)之外的其它東西,你就做完了:你可以返回結果。如果最重要的部分是相等的,你就應該繼續比較次重要部分。在此案例中,只有兩個部分 (first name and last name)。如果有更多的部分,你就應該以顯而易見的方式繼續進行,直到發現兩個不相等的部分(否則你就應該比較最不重要的部分),這時,你就可以返回比較結果了。這是 一個建立 Name 對象列表並對它們進行排序的小程序:

import java.util.*;
class NameSort {
public static void main(String args[]) {
Name n[] = {
new Name("John", "Lennon"),
new Name("Karl", "Marx"),
new Name("Groucho", "Marx"),
new Name("Oscar", "Grouch")
};
List l = Arrays.asList(n);
Collections.sort(l);
System.out.println(l);
}
}

如果你運行這個程序,以下是它所打印的結果:

[Oscar Grouch, John Lennon, Groucho Marx, Karl Marx]對 compareTo 方法的行為有四個限制,我們現在不作一一討論,因為它們的技術性太強,並且十分枯燥,我們最好將其留在API文本中。但是,所有實現 Comparable 的類都必須接受這些限制的約束,這一點是確實重要的。因此,如果你要編寫一個實現Comparable 的類,請讀那些有關 Comparable 的文本吧。要試圖為違反了那些限制的對象的列表進行排序可能引發不可預知的行為。從技術上講,這些限制保證了自然排序是實現它的類的對象的部分順序(partial order)。保證排序被很好地定義是十分必要的。

比較器(Comparators)

好,到目前為止,你已經了解了自然排序。那麼,如果要對某些對象不按自然順序進行排序,又會怎麼樣呢?或者,如果你要為某些不實現 Comparable 的對象進行排序呢?為做這些事情,你需要提供一個Comparator。Comparator 實際就是一個封裝了排序的對象。與 Comparable 接口類似,Comparator 接口由一個的方法構成:

public interface Comparator { int compare(Object o1, Object o2);}

compare 方法比較它的兩個參數,當第一個參數小於、等於或大於第二個參數時,分別返回一個負整數、空或正整數。如果其中一個參數具有對 Comparator 不適合的類型,compare 方法則扔出一個 ClassCastException。

在上一節中的有關 Comparable 的許多內容也適用Comparator。編寫一個 compare 方法幾乎等同於編寫一個compareTo 方法,除前者是把兩個參數都當作參數之外。compare 方法必須象Comparable 的 compareTo 方法一樣,服從同樣的四個"技術限制"。出於同樣的原因, Comparator 必須對它所比較的對象誘發一個 partial order(部分順序)。

假設你有一個稱作 EmployeeRecord 的類:

public class EmployeeRecord implements Comparable { public Name name(); public int employeeNumber(); public Date hireDate(); ...}

我們假設 EmployeeRecord 對象的自然排序是對雇員姓名的排序 (就象上一個例子中所定義的)。不幸的是,老板要求我們提出一個按雇員資歷排序的列表。這就意味著我們必須做些額外的工作,但是不多。以下是一個將生成所需列表的程序:

import java.util.*;class EmpSort { static final Comparator SENIORITY_ORDER = new Comparator() { public int compare(Object o1, Object o2) { EmployeeRecord r1 = (EmployeeRecord) o1; EmployeeRecord r2 = (EmployeeRecord) o2; return r2.hireDate().compareTo(r1.hireDate()); } }; static final Collection employees = ... ; // Employee Database public static void main(String args[]) { List emp = new ArrayList(employees); Collections.sort(emp, SENIORITY_ORDER); System.out.println(emp); }}

以上程序中的 Comparator 相當簡單。它將它的參數轉換為EmployeeRecord, 並依賴適用於 hireDate()方法的 Date 的自然排序。請注意:Comparator 將它的第二個參數的雇用-日期傳遞給第一個參數,而不是按反方向傳遞。這是因為,最新雇用的雇員資歷最淺:按照雇用-日期排序將使列表成為反向資歷-順序。另一個獲得相同結果的方法是:保持參數順序,但對比較結果求反。

return -r1.hireDate().compareTo(r2.hireDate());

兩種方法同樣可取。使用哪一種,全由你自己。

以上程序中的 Comparator ,在對 List 進行排序時,效果很好。但有一個小的缺陷:它不能被用來對一個排序的 對象集 (如TreeSetM) 進行排序,因為它生成一個嚴格的部分(strictly partial) 排序。這意味著這個comparator 使不相等的對象相等。特別的,它會使任意兩個雇用日期相同的雇員成為相等。當你為一個 List 排序時,這沒關系,但當你使用 Comparator 為一個sort排序的對象集 排序時, 這就是致命的了。如果你將多個雇用日期相同的雇員用Comparator插入到一個TreeSet之中,那麼只有第一個將被添加到 set,第二個將被作為一個復制元素而忽略。

為解決這個問題,你必須做的一切就是修整 Comparator 使之生成一個 total ordering(完整排序)。換句話說,修整 Comparator 是為了使在使用compare 時被認為相等的唯一元素即是那些在使用equals 時被認為相等的元素。實現這個目的的途徑是做一個兩部分(two-part)比較 (就象我們為 Name 做的那樣),這裡的第一部分是我們真正感興趣的(此案例中為雇用-日期),而第二部分是可唯一識別的對象屬性。在此案例中,雇員號是作為第二部分使用的明顯的屬性。請看下面的 Comparator :

static final Comparator SENIORITY_ORDER = new Comparator() { public int compare(Object o1, Object o2) { EmployeeRecord r1 = (EmployeeRecord) o1; EmployeeRecord r2 = (EmployeeRecord) o2; int dateCmp = r2.hireDate().compareTo(r1.hireDate()); if (dateCmp != 0) return dateCmp; return (r1.employeeNumber() < r2.employeeNumber() ? -1 : (r1.employeeNumber() == r2.employeeNumber() ? 0 : 1)); }};

最後注意一點,你可能被引誘用更簡單的程序來替代 Comparator 中最後的 return 語句:

return r1.employeeNumber() - r2.employeeNumber();

不要這樣做,除非你能絕對保證不出現一個負的雇員數!這個技巧不可普遍使用,因為一個帶正負號的整數類型,即使再大,也不足以表示兩個任意的帶正負號的整數的差值。如果 i 是一個大的正整數,j 是一個大的負整數,i-j 將溢出並返回一個負整數。Comparator 的結果違反了我們一直在講的四個技術限制之中的一個限制(傳遞性),並導致可怕而玄妙的故障。這並不是一個純技術問題;搞不好,它會傷著你。

  1. 上一頁:
  2. 下一頁:
Copyright © 程式師世界 All Rights Reserved