前言:排序算法是我們初學編程時常用的算法,每個人都嘗試過排序,但只是侷限於基本數據類型的簡單排序。比如:將下列數字進行排序1,3,5,8,3,6,於是我們得出結果1,3,3,5,6,8。將下列字母(字符)進行排序 a,i,e,f,w,s 於是我們得出結果 a,e,f,i,s,w。但是我們工作後遇到的情況就不是如此簡單了。比如給公司裏的商品進行排序,我們很輕易的想到按照商品的名稱排序不就完了,而且簡單明瞭,但現實並如我們相信那般簡單。同一商品名稱可以有不同的進貨時間,可能還會有進貨單價的不同,這顯然只根據商品名稱排序是不合理的。所以在實際應用中,我們往往有需要比較兩個自定義對象大小的地方。而這些自定義對象的比較,就不像簡單的整型數據那麼簡單,它們往往包含有許多的屬性,我們一般都是根據這些屬性對自定義對象進行比較的。所以Java中要比較對象的大小或者要對對象的集合進行排序,需要通過比較這些對象的某些屬性的大小來確定它們之間的大小關係。
這就涉及到我們下面要講的如何給自定義類的對象進行排序,利用排序接口Comparable或者比較器接口Comparator。首先類要實現接口,並且使用泛型規定要進行比較的對象所屬的類,然後類實現了接口後,還需要實現接口定義的比較方法(compareTo方法或者compare方法),在這些方法中傳入需要比較大小的另一個對象,通過選定的成員變量與之比較,如果大於則返回1,小於返回-1,相等返回0。
一、java.lang.Comparable接口簡介
1.1、Comparable接口應用背景
public final class Integer extends Number implements Comparable<Integer> {...}
今天在開發中無意看到Integer包裝類內部實現了Comparable接口,因此探查一下API解釋:
Comparable接口強行對實現它的每個類的對象進行整體排序。此排序被稱爲該類的自然排序 ,類的 compareTo方法被稱爲它的自然比較方法 。實現此接口的對象列表(和數組)可以通過 Collections.sort(和 Arrays.sort )進行自動排序。實現此接口的對象可以用作有序映射表中的鍵或有序集合中的元素,無需指定比較器。
爲什麼需要實現這個接口呢?
首先看一下數據的例子:
String[] strArr = {"A","B","C","E","D"};
Arrays.sort(strArr);
for (String string : strArr) {
System.out.print(string+";");
}
輸出結果:
A;B;C;D;E;
從中我們可以看出sort方法對數據中的String字符串按照一定規則進行了排序,那爲什麼會排序呢?
查看String類源碼我們可以看到:
public final class String
implements Serializable, Comparable, CharSequence{
...
}
它也實現了Comparable接口。裏面實現了compareTo方法,所以按照某種規則能夠進行排序。
如果數組中的對象不是String而是自定義的類型呢?
public class ComparableDemo{
public static void main(String[] args) {
Object[] objArray = {new Person(20,"jack"),new Person(17,"tom"),
new Person(27,"aj")};
for (Object object : objArray) {
System.out.print(object.toString());
}
Arrays.sort(objArray);
for (Object object : objArray) {
System.out.print(object.toString());
}
}
}
public class Person {
private Integer age;
private String name;
public Person(int age,String name) {
this.age = age;
this.name = name;
}
@Override
public String toString() {
return "Person [age=" + age + ", name=" + name + "]";
}
}
結果爲:
Person [age=20, name=jack]Person [age=17, name=tom]Person [age=27, name=aj]
Exception in thread "main" java.lang.ClassCastException: interfacedemo.Person cannot be cast to java.lang.Comparable
at java.util.ComparableTimSort.countRunAndMakeAscending(Unknown Source)
at java.util.ComparableTimSort.sort(Unknown Source)
at java.util.ComparableTimSort.sort(Unknown Source)
at java.util.Arrays.sort(Unknown Source)
at interfacedemo.ComparableDemo.main(ComparableDemo.java:18)
可以看到不進行排序打印是正常的,但是排序時報錯了。因爲系統無法知道使用什麼規則進行排序。
我們存入字符串排序成功是因爲String類已經實現Comparable接口,因此想要實現自定義對象比較同樣需要實現該Comparable接口,其中的比較方法規則由我們自己設定。
1.2、Comparable排序接口總結
該接口定義如下:
package java.lang;
import java.util.*;
public interface Comparable<T>
{
public int compareTo(T o);
}
Comparable是一個泛型接口。Java類庫中:Byte,Short,Integer,Long,Float,Double,Character,BigTnteger,BigDecimal,Calendar,String及Data類都實現了Comparable接口,這些基本類型對象可以直接調用進行排序。
Comparable是排序接口。若一個類實現了Comparable接口,就意味着該類支持排序。實現了Comparable接口的類的對象的列表或數組可以通過Collections.sort或Arrays.sort進行自動排序。此外,實現此接口的對象可以用作有序映射表中的鍵或有序集合中的集合,無需指定比較器。
Comparable接口的意義:在排序時,Java本身提供的一些類,它們已經實現comparable接口,可以直接進行對象比較。但是在List容器裏添加自定義的對象,就沒法直接調用Collections.sort()方法了,編譯期不知道按照什麼來進行排序,會報錯,這個時候就必須用到Comparable接口。
實現了Comparable接口的類,需要實現compareTo()方法。jdk中的默認排序都是通過判斷此接口實現,通過該接口的compareTo方法返回值比較大小排序。如Collections.Sort,及TreeMap中的排序都是默認通過compareTo的返回值進行排序。Comparable常被稱爲內比較器。
int CompareTo(T object)方法
Java提供Comparable接口爲我們解決比較兩個對象的問題。
接口中的compareTo(T object)方法判斷這個象相對於比較對象的順序,小於返回負整數,等於返回0,大於返回正整數。
1.3、Comparable排序接口的用法實例
package coreJavaOne;
import java.util.Arrays;
public class User implements Comparable<User> {
private String id;
private int age;
public User(String id, int age) {
this.id = id;
this.age = age;
}
public int getAge() {
return this.age;
}
public String getId() {
return this.id;
}
public void setId(String id) {
this.id = id;
}
public void setAge(int age) {
this.age = age;
}
public String toString() {
return "[ id = " + this.id + ", age = " + this.age + "]";
}
public int compareTo(User other) {
return this.age - other.getAge();
}
public static void main(String args[]) {
User[] users = {new User("wangzhengyi", 26), new User("bululu", 27)};
Arrays.sort(users);
for (int i = 0; i < users.length; i++) {
System.out.println(users[i]);
}
}
}
輸出結果:
二、 java.util.Comparator簡介
2.1、Comparator比較器接口總結
與上面的Comparable接口不同的是:
Comparator位於包java.util下,而Comparable位於包java.lang下。
Comparable接口將比較代碼嵌入需要進行比較的類的自身代碼中,而Comparator接口在一個獨立的類外實現比較。
如果前期類的設計沒有考慮到類的Compare問題而沒有實現Comparable接口,後期可以通過Comparator接口來實現比較算法進行排序,並且爲了使用不同的排序標準做準備,比如:升序、降序。
Comparable接口強制進行自然排序,而Comparator接口不強制進行自然排序,可以指定排序順序。
Comparator是比較器接口,我們如果需要控制某個自定義類的對象次序,而該類本身不支持排序(即沒有實現Comparable接口),那麼我們就可以建立一個“該類的比較器”來進行排序,這個“比較器”只需要實現Comparator接口即可。也就是說,我們可以通過實現Comparator來新建一個比較器,然後通過這個比較器對類進行排序。Comparator相當於外部比較器。
該接口定義如下:
package java.util;
public interface Comparator<T>
{
int compare(T o1, T o2);
boolean equals(Object obj);
}
注意:1、若一個類要實現Comparator接口:它一定要實現compare(T o1, T o2) 函數,但可以不實現 equals(Object obj) 函數。
2、int compare(T o1, T o2) 是“比較o1和o2的大小”。返回“負數”,意味着“o1比o2小”;返回“零”,意味着“o1等於o2”;返回“正數”,意味着“o1大於o2“
使用場景:
① 類雖然已實現Comparable接口,但是我對他的比較規則不滿意,我想根據我自己的業務情況重新定義比較規則。這時候可以使用匿名內部類的方式來達到我們的目的。
②某個類我們手裏只有字節碼文件(拿不到源代碼),不管這個類沒有實現Comparable接口,我們可以通過Comparator接口達到比較的目的。
2.2、java.util.Comparator 比較器接口的用法實例
示例①:
package coreJavaOne;
import java.util.Arrays;
import java.util.Comparator;
class User implements Comparable<User> {
private String id;
private int age;
public User(String id, int age) {
this.id = id;
this.age = age;
}
public int getAge() {
return this.age;
}
public String getId() {
return this.id;
}
public void setId(String id) {
this.id = id;
}
public void setAge(int age) {
this.age = age;
}
public String toString() {
return "[ id = " + this.id + ", age = " + this.age + "]";
}
public int compareTo(User other) {
return this.age - other.getAge();
}
}
public class SampleComparator implements Comparator<User> {
/**
* 重寫接口方法,按照年齡從大到小排序(注意:Comparable示例是按照年齡從小到大排序)
*/
public int compare(User o1, User o2) {
return o2.getAge() - o1.getAge();
}
public static void main(String args[]) {
User[] users = {new User("wangzhengyi", 26), new User("bululu", 27)};
Arrays.sort(users, new SampleComparator());
for (int i = 0; i < users.length; i ++) {
System.out.println(users[i]);
}
}
}
輸出結果:
示例②:
public class UserInfo {
private int id;
private String name;
private int age;
public UserInfo(int id, String name, int age) {
this.id = id;
this.name = name;
this.age = age;
}
@Override
public String toString() {
return "UserInfo[id=" + id + " name=" + name + " age=" + age + "]";
}
public int getId() {
return id;
}
public void setId(int id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
}
public class InterTest {
public static void main(String[] args) {
List<UserInfo> userInfoList = new ArrayList<>();
userInfoList.add(new UserInfo(0, "大強", 12));
userInfoList.add(new UserInfo(1, "大黃", 18));
userInfoList.add(new UserInfo(2, "大黑", 16));
userInfoList.add(new UserInfo(0, "大白", 52));
userInfoList.add(new UserInfo(0, "紅紅", 8));
userInfoList.add(new UserInfo(0, "翠花", 16));
userInfoList.add(new UserInfo(0, "芹菜", 36));
System.out.println("排序前");
for (UserInfo userInfo : userInfoList) {
System.out.println(userInfo);
}
Collections.sort(userInfoList, new Comparator<UserInfo>() {
@Override
public int compare(UserInfo o1, UserInfo o2) {
return o1.getAge() > o2.getAge() ? 1 : (o1.getAge() == o2.getAge() ? 0 : -1);
}
});
System.out.println("排序後");
for (UserInfo userInfo : userInfoList) {
System.out.println(userInfo);
}
}
}
輸出結果:
排序前
UserInfo[id=0 name=大強 age=12]
UserInfo[id=1 name=大黃 age=18]
UserInfo[id=2 name=大黑 age=16]
UserInfo[id=0 name=大白 age=52]
UserInfo[id=0 name=紅紅 age=8]
UserInfo[id=0 name=翠花 age=16]
UserInfo[id=0 name=芹菜 age=36]
排序後
UserInfo[id=0 name=大紅 age=8]
UserInfo[id=0 name=大強 age=12]
UserInfo[id=2 name=大黑 age=16]
UserInfo[id=0 name=翠花 age=16]
UserInfo[id=1 name=大黃 age=18]
UserInfo[id=0 name=芹菜 age=36]
UserInfo[id=0 name=大白 age=52]
三、Comparable和Comparator區別比較
Java 之所以出現了java.lang.Compareable和java.util.Comparator兩個接口,前者是爲了當你寫一個類時,你要是要使它能夠排序,可以選擇實現;當你沒實現,而後來又想實現排序功能時,兩種解決辦法,一是重寫此類,讓它實現Compareable接口,但一般不這麼用,爲了滿足設計模式開閉原則,所以選擇後者,寫一個此類的比較類,實現Comparator接口。
Comparable是排序接口,若一個類實現了Comparable接口,就意味着“該類支持排序”。而Comparator是比較器接口,我們若需要控制某個類的次序,且該類沒有實現Comparable接口,可以建立一個“該類的比較器”來進行排序。Comparable相當於“內部比較器”,而Comparator相當於“外部比較器”。
兩種方法各有優劣, 用Comparable 簡單, 只要實現Comparable 接口的對象直接就成爲一個可以比較的對象,但是需要修改源代碼。 用Comparator 的好處是不需要修改源代碼, 而是另外實現一個比較器, 當某個自定義的對象需要作比較的時候,把比較器和對象一起傳遞過去就可以比大小了, 並且在Comparator 裏面用戶可以自己實現複雜的可以通用的邏輯,使其可以匹配一些比較簡單的對象,那樣就可以節省很多重複勞動了。
1. 相同點
二者都是接口,而且都可以實現比較,進而實現排序。
簡單的說:
實現Comparable接口可以直接使用sort方法Collections.sort(List list)排序。
實現Comparator接口可以用Collections.sort(List list, Comparator c)排序。
2. 不同點
Comparable接口的耦合性較強一些,必須要在類的源代碼裏面進行修改。
而Comparator接口則靈活性強一些,它可以在我們沒有源代碼的情況下,可以實現我們自定義的排序規則。
一個類實現了Camparable接口則表明這個類的對象之間是可以相互比較的,這個類對象組成的集合就可以直接使用sort方法排序。
Comparator可以看成是一種外部算法的實現,將算法和數據分離,Comparator也可以在下面兩種環境下使用:
- 類的設計師沒有考慮到比較問題而沒有實現Comparable,可以通過Comparator來實現排序從而不必改變對象本身。
- 可以使用多種排序標準,比如升序、降序等。
四、Collections如何調用重寫的compareTo()方法的
集合框架中,Collections工具類支持兩種排序方法:如果待排序的列表中是數字或者字符,可以直接使用Collections.sort(list);當需要排序的集合或數組是自定義類的對象時,需要自己定義排序規則,實現一個Comparator比較器。
Collections.sort(List<T> list);
Collections.sort(List<T> list, Comparator<? super T> c)
Collections.sort(list)方法,方法傳遞一個List集合,這裏要求List泛型裏面裝的元素必須實現Compareable接口。也就是列表中的所有元素都必須是可相互比較的(也就是說,對於列表中的任何 e1 和 e2 元素,e1.compareTo(e2) 不得拋出 ClassCastException)。
3.1 Collections.sort源碼
public static <T extends Comparable<? super T>> void sort(List<T> list) {
Object[] a = list.toArray();
Arrays.sort(a);
ListIterator<T> i = list.listIterator();
for (int j=0; j<a.length; j++) {
i.next();
i.set((T)a[j]);
}
}
由源碼可以看出來,sort內部調用了Arrays.sort的方法,繼續向下看
3.2 Arrays.sort源碼
public static void sort(Object[] a) {
if (LegacyMergeSort.userRequested)
legacyMergeSort(a);
else
ComparableTimSort.sort(a);
}
源碼裏首先判斷是否採用傳統的排序方法,LegacyMergeSort.userRequested屬性默認爲false,也就是說默認選中 ComparableTimSort.sort(a)方法(傳統歸併排序在Java 1.5及之前是默認排序方法,Jav1.5之後默認執行ComparableTimSort.sort()方法。除非程序中強制要求使用傳統歸併排序,語句如下:System.setProperty("java.util.Arrays.useLegacyMergeSort", "true"))
繼續看 ComparableTimSort.sort(a)源碼
3.3 ComparableTimSort.sort(a)源碼
public static void sort(Object[] a) {
sort(a, 0, a.length);
}
public static void sort(Object[] a, int lo, int hi) {
rangeCheck(a.length, lo, hi);
int nRemaining = hi - lo;
if (nRemaining < 2)
return; // Arrays of size 0 and 1 are always sorted
// If array is small, do a "mini-TimSort" with no merges
if (nRemaining < MIN_MERGE) {
int initRunLen = countRunAndMakeAscending(a, lo, hi);
binarySort(a, lo, hi, lo + initRunLen);
return;
}
/**
* March over the array once, left to right, finding natural runs,
* extending short natural runs to minRun elements, and merging runs
* to maintain stack invariant.
*/
ComparableTimSort ts = new ComparableTimSort(a);
int minRun = minRunLength(nRemaining);
do {
// Identify next run
int runLen = countRunAndMakeAscending(a, lo, hi);
// If run is short, extend to min(minRun, nRemaining)
if (runLen < minRun) {
int force = nRemaining <= minRun ? nRemaining : minRun;
binarySort(a, lo, lo + force, lo + runLen);
runLen = force;
}
// Push run onto pending-run stack, and maybe merge
ts.pushRun(lo, runLen);
ts.mergeCollapse();
// Advance to find next run
lo += runLen;
nRemaining -= runLen;
} while (nRemaining != 0);
// Merge all remaining runs to complete sort
assert lo == hi;
ts.mergeForceCollapse();
assert ts.stackSize == 1;
}
nRemaining表示沒有排序的對象個數,方法執行前,如果這個數小於2,就不需要排序了。
如果2<= nRemaining <=32,即MIN_MERGE的初始值,表示需要排序的數組是小數組,可以使用mini-TimSort方法進行排序,否則需要使用歸併排序。
mini-TimSort排序方法:先找出數組中從下標爲0開始的第一個升序序列,或者找出降序序列後轉換爲升序重新放入數組,將這段升序數組作爲初始數組,將之後的每一個元素通過二分法排序插入到初始數組中。注意,這裏就調用到了我們重寫的compareTo()方法了。
private static int countRunAndMakeAscending(Object[] a, int lo, int hi) {
assert lo < hi;
int runHi = lo + 1;
if (runHi == hi)
return 1;
// Find end of run, and reverse range if descending
if (((Comparable) a[runHi++]).compareTo(a[lo]) < 0) { // Descending
while (runHi < hi && ((Comparable) a[runHi]).compareTo(a[runHi - 1]) < 0)
runHi++;
reverseRange(a, lo, runHi);
} else { // Ascending
while (runHi < hi && ((Comparable) a[runHi]).compareTo(a[runHi - 1]) >= 0)
runHi++;
}
return runHi - lo;
}