JDK中的排序:Arrays.sort的源码实现

简介: JDK中的排序:Arrays.sort的源码实现

JDK中的排序:Arrays.sort的源码实现

文章目录

  • JDK中的排序:Arrays.sort的源码实现
  • Java中的排序并没有那么简单
  • 整体看看Arrays.sort的所有重载方法
  • 对基本数据类型数组的排序
  • 对Object数组和泛型数组的排序
  • 在使用Arrays.sort时需要注意的点

Java中的排序并没有那么简单

  JDK中的排序是如何实现的?用了什么算法?或许有人说用了快排,但事实上JDK中排序的实现并没有那么简单,我们进入Arrays.sort的源码来一探究竟

整体看看Arrays.sort的所有重载方法

  Arrays.sort作为重载方法,具有很多不同的参数实现:

1.png

  从排序是对数组整体排序还是部分排序来看,可分为两类,部分排序即需要传入排序部分的起终点,根据我们的经验,整体排序可能是调用的部分排序的sort方法,不过传入的起点为0,终点为length - 1,看看源码:

2.png

3.png

  我们发现整体排序并不是调用的部分排序的方法,Arrays.sort(int[] a)Arrays.sort(int[] a, int fromIndex, int toIndex)只是个入口,它们都会去调用DualPivotQuicksort.sort方法,都会传入排序部分的起终点,不过整体排序传入的起终点为0和length - 1。

  从数组元素的类型来看,可以将Arrays.sort分为对基本数据类型的排序和对泛型及Object数组的排序。进入源码我们可以发现:对于基本数据类型数组的排序,Arrays.sort都将调用DualPivotQuicksort.sort方法,而泛型及Object数组的排序实现则与之不同。

对基本数据类型数组的排序

  对于基本数据类型数组的排序,Arrays.sort都将调用DualPivotQuicksort.sort方法,我们来看看这个方法的部分源码:

4.png

5.png

6.png

  在该方法的最开始,会进行判断,若数组长度<286,将调用sort(a, left, right, true),我们来看看该方法:

7.png

8.png

  也就是说,若数组长度较小,长度<47,将使用插入排序。若数组长度>=47,将使用快速排序。这是由排序算法的特性决定的,因为在数组长度很小时,在大量测试的平均结果下,插入排序将快于快排。

  那么当数组长度>=286时呢?我们重新回到DualPivotQuicksort.sort方法,发现它会对数组的结构性进行判断,若数组基本有序,则将使用归并排序,若数组的元素排列较为混乱,则调用sort(a, left, right, true)方法,由于数组长度>=286,也>=47,因此会进行快速排序。为什么这样设计也是由排序算法的特性决定的,虽然快排和归并排序的(平均)时间复杂度是一样的,但对于基本有序的数组,归并排序的速度会比快速排序快,而对于近乎无序的数组,归并排序速度会比快速排序慢。

  总结一下,对于基本数据类型数组的排序,排序算法的选择和数组长度的关系如下:

数组长度 所使用的排序算法
length < 47 插入排序
47 <= length < 286 快速排序
length >= 286 且数组基本有序 归并排序
length >= 286 且数组基本无序 快速排序

对Object数组和泛型数组的排序

  对于泛型数组的排序,我们可以传入实现了Comparator接口的类的对象,也可以不传,实际上传和不传都是调用的同一个方法,只不过不传入时,对应的参数为null。我们来看看Arrays.sort对Object数组和泛型数组的排序源码:

9.png

10.png

  我们发现有个if else条件判断最终要调用哪个方法,而JDK8会默认选择TimSort作为排序算法。TimSort算法是一种起源于归并排序和插入排序的混合排序算法,原则上TimSort是归并排序,但小片段的合并中用了插入排序。对于泛型数组的排序,若不传入实现了Comparator接口的类的对象,将调用sort(Object[] a)方法

  对于上述两个Arrays.sort的重载方法,它们都只是入口,而它们最终调用的排序方法不同,一个是ComparableTimSort.sort,一个是TimSort.sort。我们进入源码来看看两者的区别:

  首先来看看对Object数组排序将调用的ComparableTimSort.sort方法的部分源码:

11.png

  ComparableTimSort.sort将会调用countRunAndMakeAscending方法和binarySort方法,而这两个方法都有将数组元素强转为Comparable接口类型的操作,因为它需要调用Comparable接口中的compareTo方法进行元素间的比较,Comparable接口中只定义了一个方法,那就是compareTo。

12.png

13.png

14.png

  因此,若调用Arrays.sort(Object[] o)对Object数组进行排序,但数组元素类型表示的类并没有实现Comparable接口,那么Java将认为该类的对象是无法比较的,一定会抛出ClassCastException异常,比如:

15.png

  我们再来看看对泛型数组排序将调用的TimSort.sort方法:

16.png

  我们发现它也会去调用countRunAndMakeAscending方法和binarySort方法,但这两个方法是泛型版的,这是JDK的老套路了,当年为了支持泛型,复刻了很多原有方法的泛型版本,我们来看看源码:

17.png

  我们发现在泛型版本中,它会使用我们传入的参数c,即实现了Comparator接口的类的对象的compare方法进行元素之间的比较,Comparator接口定义了包括compare方法在内的很多方法。

  因此,若对泛型数组进行排序,要么数组元素表示的类实现了Comparable接口(调用Arrays.sort(T[] a)相当于调用Arrays.sort(Object[] a)),要么调用Arrays.sort时传入实现了Comparator接口的类的对象,否则一定会抛异常。

  有人可能会问,那对于包装类的数组呢,比如Integer类型的数组,我都是直接去排序的,为什么没抛异常?那是因为包装类都实现了Comparable接口,都是可比较的

在使用Arrays.sort时需要注意的点

  JDK中的Arrays.sort实际上采用的是设计模式中的模板模式,它将排序算法的步骤封装了起来,而将如何比较两个数组元素交给了程序员来实现。当我们排序自定义类时,我们可以让这个类实现Comparable接口,并重写其compareTo方法。也可以创建一个实现了Comparator接口的类,重写其compare方法。具体如何比较两个数组元素的逻辑就写在了需要重写的这两个方法中。

  比较两个数组元素o1与o2的大小无非三种结果:o1>o2,o1=o2,o1<o2。因此compareTo方法和compare方法的返回值有三种情况,这是针对默认升序设计的,当o1>o2,返回一个正整数,若o1=o2,返回0,若o1<o2,返回一个负整数。对于实现了Comparable接口的类,o1即为this,表示当前类对象。若我们在重写方法的逻辑中按上述对应关系去返回对应值,则调用Arrays.sort将会得到升序结果,若把对应关系写反,则会得到降序结果。

  需要注意的是,compareTo方法和compare方法的返回值一定要具有一个负整数和一个正整数,否则TimSort将失效,我们还是不能偷懒,得规规矩矩把正负零三种返回值给写上。

目录
相关文章
|
4月前
|
安全 前端开发 Java
JDK源码级别彻底剖析JVM类加载机制
JDK源码级别彻底剖析JVM类加载机制
|
4月前
|
缓存 Dubbo Java
趁同事上厕所的时间,看完了 Dubbo SPI 的源码,瞬间觉得 JDK SPI 不香了
趁同事上厕所的时间,看完了 Dubbo SPI 的源码,瞬间觉得 JDK SPI 不香了
|
1月前
|
算法 安全 Java
深入JDK源码:揭开ConcurrentHashMap底层结构的神秘面纱
【8月更文挑战第24天】`ConcurrentHashMap`是Java并发编程中不可或缺的线程安全哈希表实现。它通过精巧的锁机制和无锁算法显著提升了并发性能。本文首先介绍了早期版本中使用的“段”结构,每个段是一个带有独立锁的小型哈希表,能够减少线程间竞争并支持动态扩容以应对高并发场景。随后探讨了JDK 8的重大改进:取消段的概念,采用更细粒度的锁控制,并引入`Node`等内部类以及CAS操作,有效解决了哈希冲突并实现了高性能的并发访问。这些设计使得`ConcurrentHashMap`成为构建高效多线程应用的强大工具。
37 2
|
设计模式 Java 程序员
太爆了!阿里最新出品2023版JDK源码学习指南,Github三天已万赞
最近后台收到很多粉丝私信,说的是程序员究竟要不要去读源码?当下行情,面试什么样的薪资/岗位才会被问到源码? 对此,我的回答是:一定要去读,并且要提到日程上来! 据不完全统计,现在市面上不管是初级,中级,还是高级岗,面试的时候都有可能会问到源码中的问题,它已经成为程序员常规必备的一个技术点。如果你当下想通过一个面试,或者想把中级薪资要到相对于比较高的话,源码这块就必须要会。
136 0
|
3月前
|
Java Spring
深入解析Spring源码,揭示JDK动态代理的工作原理。
深入解析Spring源码,揭示JDK动态代理的工作原理。
45 0
|
4月前
|
设计模式 Java
根据JDK源码Calendar来看工厂模式和建造者模式
根据JDK源码Calendar来看工厂模式和建造者模式
|
4月前
|
算法 Java 索引
【数据结构与算法】4、双向链表(学习 jdk 的 LinkedList 部分源码)
【数据结构与算法】4、双向链表(学习 jdk 的 LinkedList 部分源码)
61 0
|
4月前
|
Java Linux iOS开发
Spring5源码(27)-静态代理模式和JDK、CGLIB动态代理
Spring5源码(27)-静态代理模式和JDK、CGLIB动态代理
40 0
|
4月前
|
消息中间件 Oracle Dubbo
Netty 源码共读(一)如何阅读JDK下sun包的源码
Netty 源码共读(一)如何阅读JDK下sun包的源码
110 1
|
4月前
|
算法 安全 Java
ConcurrentLinkedQueue的源码解析(基于JDK1.8)
ConcurrentLinkedQueue的源码解析(基于JDK1.8) ConcurrentLinkedQueue是Java集合框架中的一种线程安全的队列,它是通过CAS(Compare and Swap)算法实现的并发队列。在并发场景下,ConcurrentLinkedQueue能够保证队列的线程安全性,同时性能也很不错。