1. 程式人生 > >物件排序 — java 7 collection詳解(三)

物件排序 — java 7 collection詳解(三)

轉載自:http://peiquan.blog.51cto.com/7518552/1293970

 承接著前兩篇collection講解,這一次繼續談一下在collection裡的物件排序。在談物件排序排序之前,先來了解一下,在java裡是如何判斷兩個物件是相等(同)的。

一、基礎補充

    在根類Object裡提供裡兩個方法hasCode()和equal()方法,如果兩個物件相等,則hasCode的返回值應一致,equal()應該返回true。現在來看一下java API,瞭解一下這兩個方法的定義:

    hasCode():返回該物件的雜湊碼值。支援此方法是為了提高雜湊表的效能。

    hasCode的常規協定是:

  • 在 Java 應用程式執行期間,如果物件進行equals比較時沒有被修改任何資訊,那麼同一物件多次呼叫 hashCode方法,必須一致地返回相同的整數。同一程式的兩次執行,該整數無需保持一致。
  • 如果根據 equals(Object) 方法判斷出兩個物件是相等的,那麼對這兩個物件中的每個物件呼叫 hashCode 方法都必須生成相同的整數結果。
  • 如果根據 equals(java.lang.Object) 方法,兩個物件不相等,那麼對這兩個物件中的任一物件上呼叫 hashCode方法 要求一定生成不同的整數結果。但是,應該意識到,為不相等的物件生成不同整數結果可以提高雜湊表的效能。

    equals(object obj):指示某個物件obj是否與此物件“相等”。

    equals方法需要遵循一下規則:

  • 自反性:對於任何非空引用值 xx.equals(x) 都應返回 true
  • 對稱性:對於任何非空引用值 x 和 y,當且僅當 y.equals(x) 返回 true 時,x.equals(y) 才應返回 true
  • 傳遞性:對於任何非空引用值 xy 和 z,如果 x.equals(y) 返回 true,並且 y.equals(z) 返回 true,那麼x.equals(z) 應返回 true
  • 一致性:對於任何非空引用值 x 和 y,如果物件進行equals比較時沒有被修改任何資訊,多次呼叫 x.equals(y)始終返回 true 或始終返回 false
  • 對於任何非空引用值 xx.equals(null) 都應返回 false

    簡單來說就是,對於任何非空引用值 x 和 y,當且僅當 x 和 y 引用同一個物件時,此方法才返回 true(x == y 具有值 true)。 另外,對於hasCode()和equals()方法,如果過載了其中一個,一般來說都需要過載另外一個,即兩個方法都要過載。

    對於hasCode()和 equals()方法,初看之下,可能覺得有點難懂。其實,這兩個方法的重寫很簡單,舉個例子:重寫Student類的hasCode和equals方法(假設Student類只有兩個成員變數,班別和姓名),

public class Student {

	private String grade;
	private String name;
	
	public Student(String grade, String name) {
		super();
		this.grade = grade;
		this.name = name;
	}

	@Override
	public int hashCode() {
		final int prime = 31;
		int result = 1;
		result = prime * result + ((grade == null) ? 0 : grade.hashCode());
		result = prime * result + ((name == null) ? 0 : name.hashCode());
		return result;
	}

	@Override
	public boolean equals(Object obj) {
		if (this == obj)
			return true;
		if (obj == null)
			return false;
		if (!(obj instanceof Student))
			return false;
		Student other = (Student) obj;
		if (grade == null) {
			if (other.grade != null)
				return false;
		} else if (!grade.equals(other.grade))
			return false;
		if (name == null) {
			if (other.name != null)
				return false;
		} else if (!name.equals(other.name))
			return false;
		return true;
	}
	
	public static void main(String[] args) {
		Student stu1 = new Student("7班", "張三");
		Student stu2 = new Student("7班", "張三");
		Student stu3 = new Student("7班", "李四");
		System.out.println(stu1.equals(stu2));	//true
		System.out.println(stu1.hashCode() == stu2.hashCode());//true
		System.out.println(stu1.equals(stu3));	//false
		System.out.println(stu1 == stu2);		//false
	}
}

  通過Student來看,重寫equals方法時,其實就是需要去判斷兩個物件所攜帶的資訊是否一致。至於hasCode()方法,可能對prime = 31有點迷茫,為什麼要用31呢??用別的數字行嗎??為此我搜索過相關資訊,籠統地說,31是一個魔法數,利於方便地為當前的物件分配記憶體地址(這個數字不大不小) 減少hash衝突 ,提高查詢速度等。

   同樣注意到是:System.out.println(stu1 == stu2);在這裡引入這個判斷輸出,只是想指出:equals()方法判斷的是物件所包含的的資料是否一致,而==判斷是否為同一個物件的應用,詳情可以檢視這裡

  溫馨提示:對於eclipse和netbeans都提供了比較完善的關於equals和hasCode的重寫方法,以eclipse為例,可以按快捷鍵ctrl+shift+S,接著選擇重寫equals()和hasCode()方法。

二、Comparable介面

    Comparable定義了類物件的排序方式,compareTo()方法定義了排序的邏輯,預設是按照類的自然排序邏輯來排序。如下表,總結了一些常用類的預設排序方式:

Class

預設的排序方式

Class

預設的排序方式

Byte

升序(按照數字由小到大)

Float

升序

Character

升序

BigInteger

升序

Long

升序

BigDecimal

升序

Integer

升序

Boolean

Boolean.FALSE < Boolean.TRUE

Short

升序

File

預設使用系統路徑的排序方式

Double

升序

String

字典排序

Date

按照日期先後排序

   上表所有類都實現了Comparable介面,都可以使用Collections.sort()和Arrays.sort()進行排序,如下所示:

   Collections.sort(list) ;Arrays.sort(array);

    注意的是,所有待排序的物件都應該是可比較(大小)的,如果物件之間不能相互比較(大小),會丟擲ClassCastException。同時排序的元素裡不能有null,否則會丟擲NullPointerException。給一個簡答的例子—將string陣列排序輸出:

String[] fruits = new String[]{"orange","Apple","pair","watermelon"};
		Arrays.sort(fruits);
		for (String str : fruits) {
			System.out.print(str + "");
		}


    其輸出如下:Apple orange pair watermelon。

    如果需要實現自定義類的排序,則可以選擇實現Comparable介面,Comparable介面的程式碼如下:

public interface Comparable<T> {
    public int compareTo(T o);
}

     在Comparable接口裡只定義了compareTo(obj)方法,用來獲得此物件與指定物件obj的順序。如果該物件小於、等於或大於指定物件obj,則分別返回負整數、零或正整數。如果兩個物件不能相互比較(大小),此方法會丟擲ClassCastException。

    Demo:Student.java演示如何實現Comparable介面,這裡按照Id的升序輸出,

public class Student implements Comparable<Student> {
	
	private int Id;
	private int score;
	private String name;

	public Student(int id, int score, String name) {
		super();
		if (name == null) {
			throw new NullPointerException();
		}
		Id = id;
		this.score = score;
		this.name = name;
	}

	//按照Id值得大小,升序輸出
	@Override
	public int compareTo(Student stu) {
		return Id > stu.Id ? 1 : (Id < stu.Id ? -1 : 0);
	}

	//為了輸出的方便和排版,重寫toString方法,只是輸出Id的值
	@Override
	public String toString() {
		return String.valueOf(Id);
	}
	
	public static void main(String[] args) {
		Student[] stu = {new Student(102, 60, "張三"),
				new Student(101, 70, "李四"),
				new Student(103, 90, "王五")
		};
		List<Student> stuList = Arrays.asList(stu);
		//未排序前的物件順序
		System.out.println("未排序前的物件順序:" + stuList);
		
		//以自然邏輯排序的物件順序,即Comparable定義的排序邏輯(Id值得大小)
		Collections.sort(stuList);
		System.out.println("自然邏輯排序的物件順序:" + stuList);
	}
}

    其輸出如下:image

    在Student.java裡實現了Student物件按照Id值大小順序輸出物件,類似得可以定義按照成績或者名字的自然邏輯順序輸出物件,如下:

    return score > stu.score ? 1:(score < stu.score ? -1:0);

    既然談到排序,我們再深入一點談實現Comparable介面需要遵守的規則。在java裡,倡導所有實現Comparable介面的類都應該保持與equals()一致的排序順序(也可以不保持)。什麼意思呢??簡單得說,對一個類裡的任意兩個物件obj1和obj2,obj.compareTo(obj2) == 0 與 obj1.equals(obj2)應該具有相同的boolean值。換一種說法就是,在實現Comparable介面的類裡應該重寫equals()方法(伴隨著需要重寫hasCode()方法),如上面的Student.java,我們這樣重寫equals()與hasCode()方法:

@Override
	public int hashCode() {
		final int prime = 31;
		int result = 1;
		result = prime * result + Id;
		result = prime * result + name.hashCode();
		result = prime * result + score;
		return result;
	}

	@Override
	public boolean equals(Object obj) {
		if (!(obj instanceof Student))
			return false;
		Student other = (Student) obj;
		return (Id == other.Id) && (score == other.score) && (name.equals(other.name));
	}

     這樣Student.java的邏輯就算比較完善了,如果你曾經重寫過equals和hasCode方法,會發現這裡的重寫方法裡沒有考慮到null異常(因為在建構函式裡,確保裡Student的物件 不會包含null,所有不需要考慮null異常)。溫馨提示一下,對於eclipse和netbeans都提供了比較完善的關於equals和hasCode的重寫方法,以eclipse為例,可以按快捷鍵ctrl+shift+S,接著選擇重寫equals()和hasCode()方法。

三、Comparator介面

    對於沒有實現Comparable介面類,如果需要排序輸出物件,同樣可以選擇提供Comparator的實現,Comparator介面程式碼如下:

public interface Comparator<T> {
    int compare(T o1, T o2);
}

    在Comparator介面也只是定義裡一個compare方法,用來定義物件的排序邏輯,如果物件o1小於、等於或大於物件o2,則分別返回負整數、零或正整數。如果兩個物件不能相互比較(大小),此方法會丟擲ClassCastException。

   Demo:Student2.java,演示瞭如何使用Comparator定義輸出物件的順序:

public class Student2 {
	private int Id;
	private int score;
	private String name;

	public Student2(int id, int score, String name) {
		super();
		if (name == null) {
			throw new NullPointerException();
		}
		this.Id = id;
		this.score = score;
		this.name = name;
	}

	//為了輸出的方便和排版,重寫toString方法,只是輸出Id的值
	@Override
	public String toString() {
		return String.valueOf(Id);
	}

	public static class OrderById implements Comparator<Student2>{

		@Override
		public int compare(Student2 stu1, Student2 stu2) {
			return stu1.Id > stu2.Id ? 1:(stu1.Id < stu2.Id ? -1:0);
		}
		
	}
	
	public static void main(String[] args) {
		Student2[] stu = {new Student2(102, 60, "張三"),
				new Student2(101, 70, "李四"),
				new Student2(103, 90, "王五")
		};
		List<Student2> stuList = Arrays.asList(stu);
		//未排序前的物件順序
		System.out.println("未排序前的物件順序:" + stuList);
		
		//以Comparator定義的排序輸出物件(Id值得大小)
		Collections.sort(stuList,new Student2.OrderById());
		System.out.println("自然邏輯排序的物件順序:" + stuList);
	}
}

    其輸出如下:image

    相對Comparable的實現,Comparator是在類裡通過一個靜態類來定義輸出物件的排序邏輯(按照Id的大小,升序輸出;類似的可以定義score和name的排序邏輯),還有一點不同的是sort()函式的使用:

    Collections.sort(stuList,new Student2.OrderById());

    其他的實現基本一致,那是否可以認為Comparator與Comparable是等同的呢??如果只是定義一種排序邏輯,選擇Comparator和Comparable都可以,習慣上選擇Comparable。可需要多種排序邏輯輸出物件的時候,就需要Comparable和Comparator的配合使用。為了更為詳細的演示排序,這次同樣實現物件的逆序輸出,而且以內部類的形式實現Comparator:

Demo:Student.java

public class Student implements Comparable<Student> {

	private int Id;
	private int score;
	private String name;

	public Student(int id, int score, String name) {
		super();
		if (name == null) {
			throw new NullPointerException();
		}
		Id = id;
		this.score = score;
		this.name = name;
	}

	// 為了輸出的方便和排版,重寫toString方法,只是輸出Id的值
	@Override
	public String toString() {
		return String.valueOf(Id);
	}

	@Override
	public int hashCode() {
		final int prime = 31;
		int result = 1;
		result = prime * result + Id;
		result = prime * result + name.hashCode();
		result = prime * result + score;
		return result;
	}

	@Override
	public boolean equals(Object obj) {
		if (!(obj instanceof Student))
			return false;
		Student other = (Student) obj;
		return (Id == other.Id) && (score == other.score) && (name.equals(other.name));
	}

	// 按照Id值得大小,升序輸出
	@Override
	public int compareTo(Student stu) {
		return Id > stu.Id ? 1 : (Id < stu.Id ? -1 : 0);
		// return score > stu.score ? 1:(score < stu.score ? -1:0);
	}

	// 按照分數的高低排序
	static final Comparator<Student> ORDERBYSCORE_ORDER = new Comparator<Student>() {

		@Override
		public int compare(Student stu1, Student stu2) {
			return stu1.score > stu2.score ? 1 : (stu1.score < stu2.score ? -1 : 0);
		}
	};

	// 按姓名的字典順序排序
	static final Comparator<Student> ORDERBYNAME_ORDER = new Comparator<Student>() {

		@Override
		public int compare(Student stu1, Student stu2) {
			return stu1.name.compareTo(stu2.name);
		}
	};

	public static void main(String[] args) {
		Student[] stu = { new Student(102, 60, "張三"), 
				new Student(101, 70, "李四"),
				new Student(103, 90, "王五") };
		
		List<Student> stuList = Arrays.asList(stu);
		// 未排序前的物件順序
		System.out.println("未排序前的物件順序:" + stuList);

		// 以自然邏輯排序的物件順序,Comparable定義的邏輯
		Collections.sort(stuList);
		System.out.println("自然邏輯排序的物件順序:" + stuList);

		// 降序排序輸出物件
		Collections.sort(stuList, Collections.reverseOrder());
		System.out.println("降序排序輸出物件:" + stuList);

		// 使用Comparator順序輸出物件,OederByScore
		Collections.sort(stuList, ORDERBYSCORE_ORDER);
		System.out.println("OederByScore順序輸出物件" + stuList);

		// 使用另一個Comparator順序輸出物件,OederByName
		Collections.sort(stuList, ORDERBYNAME_ORDER);
		System.out.println("OederByName順序輸出物件" + stuList);
	}
}


    其輸出如下:

未排序前的物件順序:[102, 101, 103] 
自然邏輯排序的物件順序:[101, 102, 103] 
降序排序輸出物件:[103, 102, 101] 
OederByScore順序輸出物件[102, 101, 103] 
OederByName順序輸出物件[102, 101, 103]

    如上程式碼,可以實現了多種排序邏輯的物件輸出,同時注意到逆序輸出的方式:

    Collections.sort(stuList, Collections.reverseOrder());

    最後提一下的是,在Demo Student裡,都是使用Collections.sort()方法來輸出物件的,同樣地也可以使用Arrays.sort()方法來輸出物件,類似的Arrays.sort()也有兩種過載形式:

    Arrays.sort(array);

    Arrays.sort(array,Comparator);

    其用法和Student提到的方法一致。