【Java必修课】一图说尽排序,一文细说Sorting(Array、List、Stream的排序)

简说排序

排序是极其常见的使用场景,因为在生活中就有很多这样的实例。国家GDP排名、奥运奖牌排名、明星粉丝排名等,各大排行榜,给人的既是动力,也是压力。

而讲到排序,就会有各种排序算法和相关实现,本文不讲任何排序算法,而只专注于讲使用。通过实例给大家展示,我们可以了解怎样使用既有的工具进行排序。Linux之父说:

Talk is cheap. show me the code!

本文JDK版本为Java 8,但并不代表所介绍到的所有方法只能在JDK1.8上跑,部分方法在之前的版本就已经给出。

如下本次整理的图,记住图中的方法,就能轻松应对大多数场景,赶紧收藏起来吧,哈哈


Java排序

两个接口

Comparable

先上代码:

package java.lang;

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

可以看出这个接口只有一个方法,这个方法只有一个参数,实现了这个接口的类就可以和同类进行比较了。这个方法所实现的,就是比较法则,也是说,它表示如何对两个对象进行比较。
它返回的是一个整数int:

  • 返回正数,代表当前对象大于被比较的对象;
  • 返回0,代表当前对象等于于被比较的对象;
  • 返回负数,代表当前对象小于被比较的对象。

实现了该接口后,我们就可以使用Arrays.sort()和Collections.sort()来进行排序了。
不然对象没有比较法则,程序肯定是不知道如何进行比较排序的。
像我们常用的类String、Integer、Double、Date等,JDK都帮我们实现了Comparable接口,我们可以直接对这类对象进行比较排序。
举个例子,Date Comparable的实现:

public int compareTo(Date anotherDate) {
    long thisTime = getMillisOf(this);
    long anotherTime = getMillisOf(anotherDate);
    return (thisTime<anotherTime ? -1 : (thisTime==anotherTime ? 0 : 1));
}

需要注意的是,当我们自己去实现Comparable接口时,一定要注意与equals()方法保持一致。当两个对象是equals的,compare的结果应该是相等的。

Comparator

还是先看代码,看看接口定义吧:

package java.util;

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

它是一个函数式接口,它的compare方法有两个参数,代表进行比较的两个对象。这个接口代表了可以作为某种对象比较的一种法则,或叫一种策略。
它的返回值正负代表意义与Comparable接口的方法一样。
它的使用通常会有三种方式:

  1. 实现类
  2. 匿名类
  3. Lambda表达式

在Java8之后,我们一般用Lambda比较多,也比较灵活优雅。

两个接口的比较

两个接口功能都是用于比较排序,但其实有很大的区别。

  1. 两者方法参数不同,Comparable只有一个参数,表示被比较的对象,因为它的方法是位于需要比较的类里的,所以只要一个参数就可以了;而Comparator的比较方法则有两个参数,分别表示比较对象和被比较对象。
  2. Comparable与对象绑定,位于对象内,我们可以称之为内比较器;而Comparator是独立于需要比较的类的,我们可以称为外比较器
  3. 当类实现了Comparable方法后,比较法则就确定了,我们称之为自然比较方法,我们无法给它实现多种比较方法;而因为Comparator独立于外,我们可以为同一个类提供多种Comparator的实现,这样来提供多种比较方法/策略,如升序倒序,因此我们也可以将Comparator看成是一种策略模式

相对于Comparable,Comparator有一定的灵活性,假如一个类并没有实现Comparable接口,并且这个类是无法修改的,我们就要通过提供Comparator来进行比较排序。
Comparator这种模式实现了数据与算法的解耦合,对于维护也是很方便的。


工具类

十分友好的是,JDK为我们提供了工具类,它们的静态方法可以帮助我们直接对数组和List进行排序。

数组排序Arrays

Arrays的sort方法可以对已经实现了Comparable接口的进行排序,同时还可指定排序的范围。

//Arrays.sort对String进行排序
String[] strings = {"de", "dc", "aA", "As", "k", "b"};
Arrays.sort(strings);
assertTrue(Arrays.equals(strings,
        new String[]{"As", "aA", "b", "dc", "de", "k"}));

指定范围排序,需要注意的是,index是从0开始算的,包含fromIndex,不包含toIndex:

//Arrays.sort指定范围排序
strings = new String[]{"z", "a", "d", "b"};
Arrays.sort(strings, 0, 3);
assertTrue(Arrays.equals(strings,
        new String[]{"a", "d", "z", "b"}));

对于基本类型,一样可以进行排序,并不需要使用封装类:

//Arrays.sort对基本类型排序
int[] nums = {3, 1, 20, 2, 38, 2, 94};
Arrays.sort(nums);
assertTrue(Arrays.equals(nums,
        new int[]{1, 2, 2, 3, 20, 38, 94}));

还能多线程进行排序,其实是拆分成多个子数组再进行排序,最终再合并结果。

//Arrays.parallelSort多线程排序
nums = new int[]{3, 1, 20, 2, 38, 2, 94};
Arrays.parallelSort(nums);
assertTrue(Arrays.equals(nums,
        new int[]{1, 2, 2, 3, 20, 38, 94}));

对于没有实现Comparable的类也可以使用,但需要提供Comparator来指定比较策略。
本文的没有实现Comparable接口的类Person如下:

import lombok.AllArgsConstructor;
import lombok.Data;

@Data
@AllArgsConstructor
public class Person {
    private String name;
    private int age;
}

排序:

//Arrays.sort提供Comparator进行排序
Person[] persons = new Person[]{
        new Person("Larry", 18),
        new Person("David", 30),
        new Person("James", 20),
        new Person("Harry", 18)};
Arrays.sort(persons, Comparator.comparingInt(Person::getAge));
assertTrue(Arrays.equals(persons, new Person[]{
        new Person("Larry", 18),
        new Person("Harry", 18),
        new Person("James", 20),
        new Person("David", 30)}));

List排序Collections

JDK的Collections工具类提供了排序方法,可以方便使用。
对于实现Comparable的类进行排序:

//Collections.sort对于实现Comparable的类进行排序
List<String> names = asList("Larry", "Harry", "James", "David");
Collections.sort(names);
assertEquals(names, asList("David", "Harry", "James", "Larry"));

提供Comparator进行排序:

//Collections.sort提供Comparator进行排序
List<Person> persons2 = asList(
        new Person("Larry", 18),
        new Person("David", 30),
        new Person("James", 20),
        new Person("Harry", 18));
Collections.sort(persons2, Comparator.comparingInt(Person::getAge));
assertEquals(persons2, asList(
        new Person("Larry", 18),
        new Person("Harry", 18),
        new Person("James", 20),
        new Person("David", 30)));

反序:只是把List反过来,并不代表一定是按照大小顺序的:

//Collections.reverse反序
names = asList("Larry", "Harry", "James", "David");
Collections.reverse(names);
assertEquals(names, asList("David", "James", "Harry", "Larry"));

成员方法

List排序

List接口有sort(Comparator<? super E> c)方法,可以实现对自身的排序,会影响自身的顺序

//List.sort排序
names = asList("Larry", "Harry", "James", "David");
names.sort(Comparator.naturalOrder());
assertEquals(names, asList("David", "Harry", "James", "Larry"));

Stream排序

Stream提供了sorted()和sorted(Comparator<? super T> comparator)进行排序,会返回一个新的Stream。

//Stream.sorted排序
names = asList("Larry", "Harry", "James", "David");
List<String> result = names.stream()
        .sorted()
        .collect(Collectors.toList());
assertEquals(result, asList("David", "Harry", "James", "Larry"));

//Stream.sorted提供Comparator排序
names = asList("Larry", "Harry", "James", "David");
result = names.stream()
        .sorted(Comparator.naturalOrder())
        .collect(Collectors.toList());
assertEquals(result, asList("David", "Harry", "James", "Larry"));

方便对象排序的Comparator

单字段排序

对类的单字段进行排序很简单,只要提供形如:

  • Comparator.comparing(类名::属性getter)

的Comparator就行了。如果需要倒序,就需要:

  • Comparator.comparing(类名::属性getter).reversed()
  • Comparator.comparing(类名::属性getter, Comparator.reverseOrder())

具体代码使用(为了不破坏List的原有顺序,我们都使用Stream来操作):

//单字段排序-升序
List<Person> personList = asList(
        new Person("Larry", 18),
        new Person("David", 30),
        new Person("David", 3),
        new Person("James", 20),
        new Person("Harry", 18));
List<Person> personResult = personList.stream()
        .sorted(Comparator.comparing(Person::getName))
        .collect(Collectors.toList());
assertEquals(personResult, asList(
        new Person("David", 30),
        new Person("David", 3),
        new Person("Harry", 18),
        new Person("James", 20),
        new Person("Larry", 18)));

//单字段排序-倒序1
personResult = personList.stream()
        .sorted(Comparator.comparing(Person::getName).reversed())
        .collect(Collectors.toList());
assertEquals(personResult, asList(
        new Person("Larry", 18),
        new Person("James", 20),
        new Person("Harry", 18),
        new Person("David", 30),
        new Person("David", 3)));
//单字段排序-倒序2
personResult = personList.stream()
        .sorted(Comparator.comparing(Person::getName, Comparator.reverseOrder()))
        .collect(Collectors.toList());
assertEquals(personResult, asList(
        new Person("Larry", 18),
        new Person("James", 20),
        new Person("Harry", 18),
        new Person("David", 30),
        new Person("David", 3)));

多字段排序

多字段其实也很方便,只需要用thenComparing进行连接就可以:
Comparator.comparing(类名::属性一getter).thenComparing(类名::属性二getter)
具体代码使用例子如下:

//多字段排序-1升2升
personResult = personList.stream()
        .sorted(Comparator.comparing(Person::getName)
                .thenComparing(Person::getAge))
        .collect(Collectors.toList());
assertEquals(personResult, asList(
        new Person("David", 3),
        new Person("David", 30),
        new Person("Harry", 18),
        new Person("James", 20),
        new Person("Larry", 18)));

//多字段排序-1升2倒
personResult = personList.stream()
        .sorted(Comparator.comparing(Person::getName)
                .thenComparing(Person::getAge, Comparator.reverseOrder()))
        .collect(Collectors.toList());
assertEquals(personResult, asList(
        new Person("David", 30),
        new Person("David", 3),
        new Person("Harry", 18),
        new Person("James", 20),
        new Person("Larry", 18)));

//多字段排序-1倒2升
personResult = personList.stream()
        .sorted(Comparator.comparing(Person::getName, Comparator.reverseOrder())
                .thenComparing(Person::getAge))
        .collect(Collectors.toList());
assertEquals(personResult, asList(
        new Person("Larry", 18),
        new Person("James", 20),
        new Person("Harry", 18),
        new Person("David", 3),
        new Person("David", 30)));

//多字段排序-1倒2倒
personResult = personList.stream()
        .sorted(Comparator.comparing(Person::getName, Comparator.reverseOrder())
                .thenComparing(Person::getAge, Comparator.reverseOrder()))
        .collect(Collectors.toList());
assertEquals(personResult, asList(
        new Person("Larry", 18),
        new Person("James", 20),
        new Person("Harry", 18),
        new Person("David", 30),
        new Person("David", 3)));

总结

本文从比较排序相关的两个接口(Comparable和Comparator)讲起,并以代码实例的形式,讲解了Array、List、Stream排序的方法,这应该可以覆盖大部分Java排序的使用场景。
对于其它集合类如Set和Map,一样可以进行排序处理,可以将它们转化为Stream然后再进行排序。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 213,254评论 6 492
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,875评论 3 387
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 158,682评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,896评论 1 285
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,015评论 6 385
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,152评论 1 291
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,208评论 3 412
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,962评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,388评论 1 304
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,700评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,867评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,551评论 4 335
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,186评论 3 317
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,901评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,142评论 1 267
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,689评论 2 362
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,757评论 2 351

推荐阅读更多精彩内容

  • 今日任务1、TreeSet介绍(掌握TreeSet集合的应用)2、Comparable 接口介绍(掌握)3、Com...
    Villain丶Cc阅读 1,820评论 0 1
  • 项目中经常会遇到列表搜索查询,大部分的查询是可以通过sql语句来实现的,有些特殊的搜索排序sql则实现不了,例如中...
    信徒_allen阅读 2,580评论 0 1
  • 面向对象主要针对面向过程。 面向过程的基本单元是函数。 什么是对象:EVERYTHING IS OBJECT(万物...
    sinpi阅读 1,048评论 0 4
  • 1.import static是Java 5增加的功能,就是将Import类中的静态方法,可以作为本类的静态方法来...
    XLsn0w阅读 1,219评论 0 2
  • 一、基本数据类型 注释 单行注释:// 区域注释:/* */ 文档注释:/** */ 数值 对于byte类型而言...
    龙猫小爷阅读 4,257评论 0 16