jdk 源码怎么看(源码说-码农老吴解说藏在JDK源码里面的策略模式)

大家好,欢迎关注极客架构师,极客架构师,专注架构师成长,我是码农老吴。

今天我要给大家分享的是,JDK源码里面的策略模式,讲的内容,对于了解策略模式的朋友,可能会颠覆你的认知,甚至有可能会造成某些人信仰崩塌(开个玩笑哦),大家要有个心理准备,我,码农老吴已经崩溃了。

一提到JDK源码里面的策略模式,几乎大部分设计模式相关的书籍,或者是网络文章,都可能会提到JDK里面List集合的排序,里面实现了策略模式,是怎么实现的呢?常常是这样说的,Comparable接口,Comparator接口,这两个接口,都是策略模式里面的抽象策略,我们可以对它们进行扩展,Arrays类是策略模式的Context类,也就是上下文,事情真是这样吗,还是一种人云亦云,以讹传讹,我们拭目以待。

jdk 源码怎么看(源码说-码农老吴解说藏在JDK源码里面的策略模式)(1)

结论先行

由于这篇文章情节的特殊性,里面有些反转,如果提前说出结论,故事就会逊色不少,所以请容许我先保持沉默,我们一步一步揭开真相。

基本思路
  1. 策略模式简要回顾
  2. JDK List集合排序的三种方式
  3. List集合排序的策略模式疑云
  4. JDK8 Stream.sorted()流排序的策略模式
  5. 一个让码农老吴也心有余悸的结论
策略模式简要回顾

已经学习过码农老吴前面关于策略模式分享的朋友,可以略过这个环节,直接跳到下个环节。

策略模式通用类图

jdk 源码怎么看(源码说-码农老吴解说藏在JDK源码里面的策略模式)(2)

策略模式通用序列图

jdk 源码怎么看(源码说-码农老吴解说藏在JDK源码里面的策略模式)(3)

基于REIS分析模型,策略模式,包含三种角色,分别是策略服务方角色,策略客户方角色,策略代理方角色,策略模式的宗旨是通过策略代理方,将策略客户方和策略服务方解耦合。策略代理方是整个模式的核心,它向上接收策略客户方的业务请求和指派的具体策略服务方对象,向下转发策略客户方的业务请求,执行具体的策略服务对象的方法。

策略服务方角色(strategy server role):策略服务方,在定义里面就是算法,也就是真正干事情的类,实现方式通常是一个策略接口,多个策略实现类,这些策略实现类因为执行了同样的接口,所以实现了可相互替换。它的职责只有一个,就是执行具体的策略,或者叫实现具体的算法。

策略客户方角色(strategy client role):策略客户方,在定义里面就是算法的客户,也就是真正需要使用算法执行结果的角色。在策略模式中,非常重要的一点,就是策略客户方,虽然依赖策略服务方,但是不会直接调用策略服务方对象的方法,而是通过策略代理方进行调用。它的职责如下:

  1. 封装业务数据,发送给策略代理方
  2. 选择具体的策略服务方,并告知策略代理方

策略代理方角色(strategy delegate role):策略代理方,在定义里面并没有直接体现,但它是这个模式的灵魂,在这个模式中,它具有非常重要的地位,属于这个模式的核心类。

它的职责如下所示:

  1. 接收策略客户方的业务请求
  2. 接收策略客户指定的具体策略服务方对象
  3. 将客户方的请求转发给策略服务方对象
  4. 在转发请求的时候,还可以扩展请求数据,提供策略服务自身所需要的其他数据。
JDK List集合排序的三种方式方式1 传统方式:Comparable 接口方式

下面的案例,实现了对SKU的排序,不知道SKU的,可以看我以前的分享,或者上网百度一下。

这种方式注意两点:

1,SKU实体类执行了Comparable接口

2,调用Collections.sort()方法进行排序

代码

package com.geekarchitect.patterns.demo0106; import lombok.Data; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.util.ArrayList; import java.util.Collections; import java.util.List; import java.util.Random; /** * @author 极客架构师@吴念 * @createTime 2022/5/23 */ public class TestProductSortDemo1 { private static final Logger LOG = LoggerFactory.getLogger(TestProductSortDemo1.class); public static void main(String[] args) { TestProductSortDemo1 testProductSortDemo1 = new TestProductSortDemo1(); testProductSortDemo1.sortSKU(); } public void sortSKU() { List<SKU> skuList = generateSKU(5); LOG.info("List排序方式1:Comparable 接口方式"); LOG.info("排序前"); skuList.forEach(e -> { LOG.info(e.toString()); }); Collections.sort(skuList); LOG.info("排序后"); skuList.forEach(e -> { LOG.info(e.toString()); }); } /** * 生成测试数据 * * @param max * @return */ public List<SKU> generateSKU(int max) { return new ArrayList<SKU>() { { for (int i = 0; i < max; i ) { add(new SKU("产品" i, new Random().nextInt(1000))); } } }; } } @Data class SKU implements Comparable<SKU> { private String name; private int quantity; public SKU(String name, int quantity) { this.name = name; this.quantity = quantity; } public SKU() { } @Override public int compareTo(SKU sku) { return sku.getQuantity() - this.getQuantity(); } }

运行结果

jdk 源码怎么看(源码说-码农老吴解说藏在JDK源码里面的策略模式)(4)

上面的代码已经过时了

很遗憾,上面List集合的排序方式,调用Collections.sort()方法,在java8里面已经过时了。我们可以跳过Collections类,直接调用List接口的sort方法。代码如下:

jdk 源码怎么看(源码说-码农老吴解说藏在JDK源码里面的策略模式)(5)

为什么会这样,我们钻到Collections类的sort方法里面,一探究竟。

搜得死内,可以看出,Collections类的sort方法,啥事没干,直接调用了List接口的sort()方法。

jdk 源码怎么看(源码说-码农老吴解说藏在JDK源码里面的策略模式)(6)

我们再看看List接口的sort方法,它里面有啥内幕。

jdk 源码怎么看(源码说-码农老吴解说藏在JDK源码里面的策略模式)(7)

大家注意,这里面确实别有洞天,List本身是个接口,但是它里面的sort方法,却是一个真实的方法,大家要注意default这个关键字。不知道的可以上网百度一下,了解一下相关知识,还有就是这个sort方式,是从1.8开始才出现的,也就是只有java8里面的List接口,有这个sort()方法。

所以说,在java8里面,List集合排序,不用再调用Collections类的sort()方法了,而是直接调用List集合自己的sort()方法。

方式2 传统方式:Comparator接口方式

下面的案例,实现了对SPU的排序,一个SPU可以对应多个SKU,关于它们之间的关系,网上有篇电商类的文章,有精彩的讲解。这里为什么要换成SPU排序,仅仅是为了解决名称重复问题。

这种方式注意三点:

1,SPU实体类没有执行任何接口

2,调用Collections.sort()方法执行排序

3,调用上面的sort()方法时,使用了根据Comparator接口建立的匿名类

代码

package com.geekarchitect.patterns.demo0106; import lombok.Data; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.util.*; /** * @author 极客架构师@吴念 * @createTime 2022/5/23 */ public class TestProductSortDemo2 { private static final Logger LOG = LoggerFactory.getLogger(TestProductSortDemo1.class); public static void main(String[] args) { TestProductSortDemo2 testProductSortDemo2 = new TestProductSortDemo2(); testProductSortDemo2.sortSPU(); } public void sortSPU() { List<SPU> spuList = generateSPU(5); LOG.info("List排序方式2:Comparator接口方式"); LOG.info("排序前"); spuList.forEach(e -> { LOG.info(e.toString()); }); Collections.sort(spuList, new Comparator<SPU>() { @Override public int compare(SPU spu1, SPU spu2) { return spu1.getQuantity() - spu2.getQuantity(); } }); //Collections.sort(SKU2List, (spu1, spu2) -> spu1.getQuantity()-spu2.getQuantity()); //Collections.sort(SKU2List, Comparator.comparingInt(SPU::getQuantity)); LOG.info("排序后"); spuList.forEach(e -> { LOG.info(e.toString()); }); } /** * 生成测试数据 * * @param max * @return */ public List<SPU> generateSPU(int max) { return new ArrayList<SPU>() { { for (int i = 0; i < max; i ) { add(new SPU("产品" i, new Random().nextInt(1000))); } } }; } } @Data class SPU { private String name; private int quantity; public SPU(String name, int quantity) { this.name = name; this.quantity = quantity; } public SPU() { } }

运行结果

jdk 源码怎么看(源码说-码农老吴解说藏在JDK源码里面的策略模式)(8)

上面的代码也已经过时了

很遗憾,上面的代码也已经过时了,大家注意,我们上面使用了Comparator接口的匿名类,这个代码以前很流行,在JDK1.8中,现在也已经过时了,大家看下面我的截图,new Comparator()在idea编辑器里面显示的灰色,表示这里的代码需要优化。它建议采用lambda表达式。

jdk 源码怎么看(源码说-码农老吴解说藏在JDK源码里面的策略模式)(9)

根据编辑器的提示,可以优化为lambda表达式语法,当你升级完成了后,编辑器又提升你优化代码,最后优化为以下代码。

Collections.sort(SKU2List, Comparator.comparingInt(SPU::getQuantity));

这些都是java8里面的新语法,大家可以根据情况,酌情使用。

java8的流及lambda是个坑,很大,很大,很大的坑,要掌握和精通里面的语法,有一定的门槛,跳不跳进这个坑,需要大家慎重考虑,但是,现实常常是无奈的,我们很可能没有选择余地,一个团队,只要有一个人用新的语法,其他人,也都或多或少要了解这些,否则项目无法维护。况且新的语法,有时候确实显得简洁,专业,除了可能有些晦涩难懂,可读性差些。大家可以一边用,一边骂,骂着骂着,就精通了。

jdk 源码怎么看(源码说-码农老吴解说藏在JDK源码里面的策略模式)(10)

方式3 java8新语法:流方式排序

下面的案例,还是对SPU进行排序,只不过使用了java8里面,流排序的新语法(我每次写这种代码,心里面都会骂,啥玩意,太不直观了,有辱Java语言的门风)。

这种方式注意三点:

1,调用Collection接口的stream().sorted()方法进行排序

2,方法参数Comparator.comparing(SPU::getQuantity),表示根据SPU的quantity属性进行排序

3,“SPU::getQuantity” 这段代码里面有两个冒号(什么玩意,不伦不类,这是我骂的最厉害的地方,Java语言要堕落啦)

代码

package com.geekarchitect.patterns.demo0106; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.util.ArrayList; import java.util.Comparator; import java.util.List; import java.util.Random; import java.util.stream.Collectors; /** * @author 极客架构师@吴念 * @createTime 2022/5/23 */ public class TestProductSortDemo3 { private static final Logger LOG = LoggerFactory.getLogger(TestProductSortDemo1.class); public static void main(String[] args) { TestProductSortDemo3 testProductSortDemo3 = new TestProductSortDemo3(); testProductSortDemo3.sortSPU(); } public void sortSPU() { List<SPU> spuList = generateSPU(5); LOG.info("List排序方式3:流方式"); LOG.info("排序前"); spuList.forEach(e -> { LOG.info(e.toString()); }); spuList = spuList.stream().sorted(Comparator.comparing(SPU::getQuantity)).collect(Collectors.toList()); LOG.info("排序后"); spuList.forEach(e -> { LOG.info(e.toString()); }); } /** * 生成测试数据 * * @param max * @return */ public List<SPU> generateSPU(int max) { return new ArrayList<SPU>() { { for (int i = 0; i < max; i ) { add(new SPU("产品" i, new Random().nextInt(1000))); } } }; } }

运行结果

jdk 源码怎么看(源码说-码农老吴解说藏在JDK源码里面的策略模式)(11)

三种排序方式的汇总

方式1 传统方式:Comparable 接口方式

调用Collections.sort()方法,被排序的对象,需要执行Comparable接口

方式2 传统方式:Comparator接口方式

调用Collections.sort()方法,被排序的对象无需执行接口,但是排序方法需要传入一个Comparator接口的对象,可以使用匿名类。

方式3 java8新语法:流方式排序

调用Collection接口的stream().sorted()方法进行排序,传入一段丑陋的参数。

前面的方式1和方式2,共同点都是从Collections对象的sort方法开始的,我们的策略模式探险之旅,就从这里开始。

List集合底层的策略模式疑云调用链路

Comparable方式调用链路:

Collections.sort()->List.sort()->Arrays.sort()->ComparableTimSort.sort()->Comparable.compareTo()

Comparator方式调用链路:

Collections.sort->List.sort()->Arrays.sort()->TimSort.sort()->Comparator.compare()

我们的两条调用链路,我们重点看第二种,弄懂一个,另外一个也就搞明白了。

Collections.sort()

里面啥也没干,就调用了List接口的sort方法,所以这个方法,被时代淘汰了。

jdk 源码怎么看(源码说-码农老吴解说藏在JDK源码里面的策略模式)(12)

List.sort():

流程:

1,将要排序的List对象,转变为对象数组(所以,List集合排序,本质上是数组排序)

2,调用Arrays.sort(),对数组进行排序

3,将排序好的数组,又转化为List集合

jdk 源码怎么看(源码说-码农老吴解说藏在JDK源码里面的策略模式)(13)

Arrays.sort()

这个方法很忙,也是整个排序功能的中枢神经,将来在策略模式中,起到了关键作用。

1,它的分支语句里面,不仅使用了这个方法的入参,还使用了JVM参数。

2,它里面本质上调用了三种排序算法,有两种是独立的类(ComparableTimSort,TimSort),另外一种仅仅是Arrays类里面的方法(LegacyMergeSort),这些我们后面都会讲到。

jdk 源码怎么看(源码说-码农老吴解说藏在JDK源码里面的策略模式)(14)

TimSort.sort()->Comparator.compare()

当我们使用方式2,Comparator接口进行排序,则走这条调用链路。

看看里面,使用了Comparator接口的compare()方法,进行对象的大小比较。

jdk 源码怎么看(源码说-码农老吴解说藏在JDK源码里面的策略模式)(15)

jdk 源码怎么看(源码说-码农老吴解说藏在JDK源码里面的策略模式)(16)

这里面真有策略模式吗?

Comparator方式调用链路:

Collections.sort->List.sort()->Arrays.sort()->TimSort.sort()->Comparator.compare()

在上面的调用链路中,到底有没有策略模式呢?

既然大部分设计模式的书籍和网上海量的文章,告诉我们这里有策略模式,我们就先相信它有。

前面我们已经回顾过,策略模式的三种角色,分别是策略服务方角色,策略客户方角色,策略代理方角色。

那么上面谁是策略服务方角色?

这个角色往往比较明显的,能轻而易举识别出来, Comparator 接口很像策略服务方里面的接口,按照传统的说法,叫抽象策略(之所以说很像,是因为后面有反转)

我们根据Comparator接口建立的匿名内部类,好像是具体策略。我们暂且认为它们都是策略服务方角色。

那么策略代理方是谁呢?

策略代理方,也就是传统说法中的Context类,在上面的我们分析的调用链路中,TimSort 类直接调用了策略服务方角色( Comparator 接口)里面的相关方法(compare())。

仅仅从这一点上看,TimSort类应该属于策略代理方,因为策略客户方,是不会自己直接调用策略服务方角色里面的方法的,如果客户自己调了,那还要代理方干啥呢。

但是,但是,但是,根据我的判断,TimSort 类,并不是策略代理方,而更像策略客户方,为什么呢?

因为TimSort类在调用 Comparator 接口对应的compare()方法后,自己直接使用了返回值,实现了自己的排序算法,并没有把这个返回值传递到上游,所以说它们并不是代理,而是实实在在的客户。

一个策略模式,可以没有策略客户方,但是不能没有策略代理类,因为策略代理类是策略模式存在的关键,是这个设计模式的核心。

因此,我的结论是这里其实并没有策略模式。

如果硬要和策略模式沾边,那也是一个即将发展为策略模式的结构,或者说是策略模式的前身。

对于策略模式,如果不存在策略代理方(策略代理方是整个策略模式结构的核心),就不能称之为策略模式。否则的话,策略模式也太廉价了,所有软件里面,到处都充斥着一个类调用了一个接口,难道它们都是策略模式吗。这也太小看策略模式了。

以上分析,和现在大部分设计模式书籍以及网上能够查询的资料,得出的结论都不一样。也出乎我的意料,但是没办法,我们做技术研究,就应该实事求是。我们可以犯逻辑错误,但是不能忽略事实,欺骗自己。

但愿是我错了,大家怎么看,可以评论区留言,欢迎吐槽,虚心接受任何反驳。

后面我还会再分析一下其他开源软件中的策略模式,再给大家带来多角度的观察。

Arrays类里面真有策略模式

开始反转了,好戏要登场了。

舞台的幕布在缓缓打开,聚光灯又回到了站在舞台中央的Arrays类。

Arrays类里面到底有没有策略模式,有,就在附近,百步之内,必有芳草。

在Arrays类的sort()方法中,其中确实存在着策略模式,只是这个策略模式不够完美,容易被人忽视。

Arrays类和ComparableTimSort类,TimSort类,以及legacyMergeSort()方法(这里没写错,它就是一个方法),这几个对象(注意,方法也可以看成是对象),其实已经构成了策略模式。

谁是策略服务方呢?

ComparableTimSort类,TimSort类,Arrays类里面的legacyMergeSort()方法,这两个类外加一个方法,都是策略服务方。

ComparableTimSort,TimSort都实现的排序算法,legacyMergeSort()虽然只是个方法,但它也实现了一种排序算法(要把它提取为一个类,是分分钟的事情)。这两个类(ComparableTimSort类,TimSort类)加上一个方法(legacyMergeSort()),都属于策略服务方。

虽然它们没有执行相同的接口,但其实它们的结构是相似的,如果非要执行同一个接口,也是很容易的。

接口本质是一种合同,是一种契约,契约精神的最高境界,就是不需要合同。

谁是策略代理方呢?

Arrays类,在这里面起到了代理方的职责,它会根据上游客户方传递的参数,判断需要使用哪种排序策略。然后把排序结果返回给上游。

至于策略客户方,List接口以及后面要说的java8里面的 SortedOps,都属于策略客户方,都需要排序结果。

反转结束,Arrays类里面确实有策略模式,虽然这个策略模式不完美,甚至连一个策略接口都没有定义,但它符合策略模式的精神,是名副其实的策略模式。

stream.sorted()底层的策略模式

调用链路:

(Stream.sorted)ReferencePipeline. sorted()->SortedOps.end()->Arrays.sort()

jdk 源码怎么看(源码说-码农老吴解说藏在JDK源码里面的策略模式)(17)

ReferencePipeline. sorted()

jdk 源码怎么看(源码说-码农老吴解说藏在JDK源码里面的策略模式)(18)

SortedOps.end()

jdk 源码怎么看(源码说-码农老吴解说藏在JDK源码里面的策略模式)(19)

可以看到,java8 Stream里面的排序,底层也是Arrays对象的排序。所以策略模式与上面的相同。这里就不再赘述了。

一个让码农老吴也心有余悸的结论

惊不惊喜,意不意外。

根据上面的分析,得出了一个让我也心有余悸的结论。

JDK中的集合排序中没有广为流传的策略模式,但是确实存在着另外一个真正意义的策略模式。

首先,Arrays类和Comparable 和 Comparator 这两个接口,他们并没有构成策略模式,因为它里面缺少策略代理方角色,如果非要说和策略模式沾点边,可以说它们之间的关系,是策略模式的前身,还没有发展成策略模式。

而另外一个真正意义的策略模式,是通过Arrays和ComparableTimSort,TimSort,legacyMergeSort()方法构成的。

Arrays是策略代理方

ComparableTimSort,TimSort,legacyMergeSort(),这两个类外加一个方法,都属于策略服务方。

这才是真正意义上的策略模式,虽然ComparableTimSort,TimSort,legacyMergeSort() 并没有执行相同的接口,但是其实它们结构一致,都实现了数组的排序算法。

本期我们就分享到这里,极客架构师,专注架构师成长,我将持续分享架构师的相关文章和视频,下期见。

,

免责声明:本文仅代表文章作者的个人观点,与本站无关。其原创性、真实性以及文中陈述文字和内容未经本站证实,对本文以及其中全部或者部分内容文字的真实性、完整性和原创性本站不作任何保证或承诺,请读者仅作参考,并自行核实相关内容。文章投诉邮箱:anhduc.ph@yahoo.com

    分享
    投诉
    首页