[Java]Java中的排序原来可以这么玩

1. Java中的排序算法

讲Java中的集合排序前,我们来一起思考一下: 什么样的排序算法适合做为标准类库的算法?

  • 对比各种常见排序算法的优劣性
算法 稳定性 时间复杂度 空间复杂度
选择排序 不稳定 1
希尔排序 不稳定 说法很多,N^(1.25-2) 1
堆排序 不稳定 NlogN 1
快速排序 不稳定 NlogN lgN
插入排序 稳定 N-N² 1
归并排序 稳定 NlogN N

其中,快速排序算法对于多数的情况都是效率较高的,其内循环中的指令很少并且总是顺序地访问数据,可以利用缓存(预加载),但是稳定性无法保证(如果待排序的元素序列本身即为有序的状态).
如果对稳定性有要求,那么归并排序可能是更为适合的,它的弊端是需要更多的空间(通常编写归并排序,我们需要创建临时数组).
关于局部性原理知识,可以通往下面链接看知乎大神如何讨论:
如何理解计算机操作系统中的局部性原理?
关于快速排序算法的讨论:
快速排序到底有多快
排序算法(三): Quick Sort 快速排序(包含三向切分)

  • 原始类型数据与引用类型数据

这里说一下笔者的一些简单理解
原始类型的数据,大小比较都是可以确定的,比如:
比较int大小,可以直接用 a > b这种方式进行比较.
但是Java中还提供了Comparable接口,它通过实现该接口的方法-compareTo来决定排列顺序.
这时,无法通过直接比较数值的大小,而是通过引用来执行方法得到结果,需要额外的空间.

为此,Java中对 原始类型数组(类型为 int、long、short、char、byte、boolean、float 或 double 的数组) 采用优化的快速排序算法-DualPivotQuicksort.
对于引用类型数据的集合采用基于归并排序的TimSort算法.

2. 使用集合排序需要注意的问题

禁止对不可变列表(unmodifiableList)进行排序,传入的列表可以为必须是可修改的, 但不必是可以改变大小的.

  • 错误示范
Student xiaoMing = Student.builder().age(20).name("20岁的小明").build();
Student xiaoHong = Student.builder().age(22).name("22岁的小红").build();
Student xiaoHua = Student.builder().age(25).name("25岁的小华").build();
List<Student> studentList = Collections.unmodifiableList(Lists.newArrayList(xiaoHong, xiaoMing, xiaoHua));
// 使用不可变集合,会抛出UnsupportedOperationException异常
Collections.sort(studentList);
studentList.forEach(e -> System.out.print(JacksonUtils.toJsonString(e) + "\n"));

3. 实战Java排序

3.1 数据源准备

为了更好地复用数据,我们先声明两个数据列表:
一个为数值型的数据列表
一个为业务对象的数据列表

  • 业务对象-Student
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;

import java.io.Serializable;
import java.math.BigDecimal;
import java.util.Date;
import java.util.List;

/**
 * @author jaymin
 */
@Builder
@NoArgsConstructor
@AllArgsConstructor
@Data
public class Student implements Serializable, Comparable {
    /**
     * 年龄
     */
    private Integer age;
    /**
     * 名字
     */
    private String name;

    /**
     * the value 0 if x == y; a value less than 0 if x < y; and a value greater than 0 if x > y
     * @param o 与之比较的其他学生
     * @return
     */
    @Override
    public int compareTo(Object o) {
        Student otherStudent = (Student) o;
        return Integer.compare(this.age, otherStudent.age);
    }
}
  • 声明成员变量
import com.google.common.collect.Lists;
import com.tea.modules.model.Student;
import com.tea.modules.util.JacksonUtils;

import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;
import java.util.stream.Collectors;
/**
 * @author jaymin.<br>
 * 在一次使用lambda的时候对Java排序产生了兴趣。因此本demo用来研究集合排序.<br>
 * 2021/2/18 20:16
 */
@SuppressWarnings("unchecked")
public class CollectionSort {
    /**
     * 数字列表
     */
    protected final static List<Integer> numberList = Lists.newArrayList(2, 3, 4, 1, 5, 2, 5, 6, 7, 99);
    /**
     * 存储不同年龄段的学生列表
     */
    protected static List<Student> studentList;

    /**
     * 构建出学生列表参数
     */
    private static void createStudentList() {
        Student xiaoMing = Student.builder().age(20).name("20岁的小明").build();
        Student xiaoHong = Student.builder().age(22).name("22岁的小红").build();
        Student xiaoHua = Student.builder().age(25).name("25岁的小华").build();
        studentList = Lists.newArrayList(xiaoHong, xiaoMing, xiaoHua);
    }
}

3.2 对原始类型列表进行排序

使用Collections.sort可以直接对Integer列表进行排序

    /**
     * 使用Collections.sort对集合进行升序排序.<br>
     * 要求集合中的对象实现Comparable接口.<br>
     */
    public static void sortByCollections() {
        Collections.sort(numberList);
        numberList.forEach(e -> System.out.print(e + "-"));
    }
  • Result
1-2-2-3-4-5-5-6-7-99-

3.3 对实现了Comparable接口的对象进行排序

Collections.sort中的对象需要重写compareTo方法

    /**
     * 这里我们使用了集合存储学生的信息.<br>
     * 如果此时希望按照年龄来进行排序,可以在Student类中实现Comparable接口。<br>
     * 然后再使用Collections.sort进行排序.<br>
     */
    public static void sortByCollectionsComparable() {
        createStudentList();
        Collections.sort(studentList);
        studentList.forEach(e -> System.out.print(JacksonUtils.toJsonString(e) + "\n"));
    }
  • Result
{
  "age" : 20,
  "name" : "20岁的小明"
}
{
  "age" : 22,
  "name" : "22岁的小红"
}
{
  "age" : 25,
  "name" : "25岁的小华"
}

3.4 使用比较器-Comparator来进行排序

通过匿名内部类来创建Comparator

    /**
     * 不实现接口,使用比较器-Comparator来进行排序.<br>
     * 比较器接收两个参数,可以理解成,给定两个学生对象,你实现compare方法来规定按什么顺序进行排序.<br>
     * 比较规则:the value 0 if x == y; a value less than 0 if x < y; and a value greater than 0 if x > y.<br>
     * 首先我们通过匿名内部类来完成这项工作.<br>
     *
     * @see Comparator
     */
    public static void sortByCollectionsComparator() {
        createStudentList();
        Collections.sort(studentList, new Comparator<Student>() {
            @Override
            public int compare(Student studentA, Student studentB) {
                return Integer.compare(studentA.getAge(), studentB.getAge());
            }
        });
        studentList.forEach(e -> System.out.print(JacksonUtils.toJsonString(e) + "\n"));
    }

输出结果同上,这里就不贴了

3.5 Java8中使用lambda来简化匿名内部类.

    /**
     * 在Java8中,你可以使用lambda来简化匿名内部类.<br>
     */
    public static void sortByCollectionsComparatorInJava8() {
        createStudentList();
        // 常规的lambda
        Collections.sort(studentList, (studentA, studentB) -> Integer.compare(studentA.getAge(), studentB.getAge()));
        // 同时,你可以将这种方式改写成方法引用
        Collections.sort(studentList, Comparator.comparingInt(Student::getAge));
        studentList.forEach(e -> System.out.print(JacksonUtils.toJsonString(e) + "\n"));
    }

输出结果同上,这里就不贴了

3.6 使用Java8的stream进行排序

    /**
     * 使用Java8的stream流,让你的集合操作更多元化.<br>
     */
    public static void sortByStream() {
        createStudentList();
        // 升序
        List<Student> result = studentList.stream()
                .sorted(Comparator.comparing(Student::getAge))
                .collect(Collectors.toList());
        result.forEach(e -> System.out.print(JacksonUtils.toJsonString(e) + "\n"));
        System.out.println("------------reversedOrder---------------");
        // 倒序
        List<Student> reversedOrderResult = studentList.stream()
                .sorted(Comparator.comparingInt(Student::getAge).reversed())
                .collect(Collectors.toList());
        reversedOrderResult.forEach(e -> System.out.print(JacksonUtils.toJsonString(e) + "\n"));
    }
  • Result
{
  "age" : 20,
  "name" : "20岁的小明"
}
{
  "age" : 22,
  "name" : "22岁的小红"
}
{
  "age" : 25,
  "name" : "25岁的小华"
}
------------reversedOrder---------------
{
  "age" : 25,
  "name" : "25岁的小华"
}
{
  "age" : 22,
  "name" : "22岁的小红"
}
{
  "age" : 20,
  "name" : "20岁的小明"
}
  • 空指针保护的Comparator.comparing

在上面的实验中,我们使用了Comparator.comparing()来进行比较,但是,如果student对象中的age为空时,我们会遇到空指针异常.

  • 触发空指针异常
    /**
     * 使用null值保护比较器来避免空指针异常<br>
     */
    public static void sortByStreamWithNullFirst() {
        createStudentList();
        studentList.get(0).setAge(null);
        // 升序
        List<Student> result = studentList.stream()
                .sorted(Comparator.comparing(Student::getAge))
                .collect(Collectors.toList());
        result.forEach(e -> System.out.print(JacksonUtils.toJsonString(e) + "\n"));
    }
  • 使用nullFirst和nullLast来避免空指针异常

nullFirst表示遇到null时,将包含null属性的对象排列在第一位.
nullsLast表示遇到null时,将包含null属性的对象排列在最后一位.
两者都需要声明排序顺序:
升序为:Comparator.naturalOrder()
降序为:Comparator.reverseOrder()

    /**
     * 使用null值保护比较器来避免空指针异常<br>
     */
    public static void sortByStreamWithNullFirst() {
        createStudentList();
        studentList.get(0).setAge(null);
        // 升序,空值排在第一位
        List<Student> result = studentList.stream()
                .sorted(Comparator.comparing(Student::getAge,Comparator.nullsFirst(Comparator.naturalOrder())))
                .collect(Collectors.toList());
        result.forEach(e -> System.out.print(JacksonUtils.toJsonString(e) + "\n"));
        System.out.println("******************** Null First reverseOrderResult*****************************");
        // 降序,空值排在第一位
        List<Student> reverseOrderResult = studentList.stream()
                .sorted(Comparator.comparing(Student::getAge,Comparator.nullsFirst(Comparator.reverseOrder())))
                .collect(Collectors.toList());
        reverseOrderResult.forEach(e -> System.out.print(JacksonUtils.toJsonString(e) + "\n"));
        System.out.println("******************** Null Last reverseOrderResult*****************************");
        // 降序,空值排在第一位
        List<Student> nullLastResult = studentList.stream()
                .sorted(Comparator.comparing(Student::getAge,Comparator.nullsLast(Comparator.reverseOrder())))
                .collect(Collectors.toList());
        nullLastResult.forEach(e -> System.out.print(JacksonUtils.toJsonString(e) + "\n"));
    }
  • Result
{
  "name" : "22岁的小红"
}
{
  "age" : 20,
  "name" : "20岁的小明"
}
{
  "age" : 25,
  "name" : "25岁的小华"
}
******************** Null First reverseOrderResult*****************************
{
  "name" : "22岁的小红"
}
{
  "age" : 25,
  "name" : "25岁的小华"
}
{
  "age" : 20,
  "name" : "20岁的小明"
}
******************** Null Last reverseOrderResult*****************************
{
  "age" : 25,
  "name" : "25岁的小华"
}
{
  "age" : 20,
  "name" : "20岁的小明"
}
{
  "name" : "22岁的小红"
}

源码地址

所有源码已经上传到我的个人仓库,有兴趣的可以pull下来进行demo.
仓库地址
代码引用地址:com.tea.modules.java8.collection.sort.CollectionSort

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