jdk 源码怎么看(源码说-码农老吴解说藏在JDK源码里面的策略模式)
大家好,欢迎关注极客架构师,极客架构师,专注架构师成长,我是码农老吴。
今天我要给大家分享的是,JDK源码里面的策略模式,讲的内容,对于了解策略模式的朋友,可能会颠覆你的认知,甚至有可能会造成某些人信仰崩塌(开个玩笑哦),大家要有个心理准备,我,码农老吴已经崩溃了。
一提到JDK源码里面的策略模式,几乎大部分设计模式相关的书籍,或者是网络文章,都可能会提到JDK里面List集合的排序,里面实现了策略模式,是怎么实现的呢?常常是这样说的,Comparable接口,Comparator接口,这两个接口,都是策略模式里面的抽象策略,我们可以对它们进行扩展,Arrays类是策略模式的Context类,也就是上下文,事情真是这样吗,还是一种人云亦云,以讹传讹,我们拭目以待。
结论先行
由于这篇文章情节的特殊性,里面有些反转,如果提前说出结论,故事就会逊色不少,所以请容许我先保持沉默,我们一步一步揭开真相。
基本思路- 策略模式简要回顾
- JDK List集合排序的三种方式
- List集合排序的策略模式疑云
- JDK8 Stream.sorted()流排序的策略模式
- 一个让码农老吴也心有余悸的结论
已经学习过码农老吴前面关于策略模式分享的朋友,可以略过这个环节,直接跳到下个环节。
策略模式通用类图策略模式通用序列图
基于REIS分析模型,策略模式,包含三种角色,分别是策略服务方角色,策略客户方角色,策略代理方角色,策略模式的宗旨是通过策略代理方,将策略客户方和策略服务方解耦合。策略代理方是整个模式的核心,它向上接收策略客户方的业务请求和指派的具体策略服务方对象,向下转发策略客户方的业务请求,执行具体的策略服务对象的方法。
策略服务方角色(strategy server role):策略服务方,在定义里面就是算法,也就是真正干事情的类,实现方式通常是一个策略接口,多个策略实现类,这些策略实现类因为执行了同样的接口,所以实现了可相互替换。它的职责只有一个,就是执行具体的策略,或者叫实现具体的算法。
策略客户方角色(strategy client role):策略客户方,在定义里面就是算法的客户,也就是真正需要使用算法执行结果的角色。在策略模式中,非常重要的一点,就是策略客户方,虽然依赖策略服务方,但是不会直接调用策略服务方对象的方法,而是通过策略代理方进行调用。它的职责如下:
- 封装业务数据,发送给策略代理方
- 选择具体的策略服务方,并告知策略代理方
策略代理方角色(strategy delegate role):策略代理方,在定义里面并没有直接体现,但它是这个模式的灵魂,在这个模式中,它具有非常重要的地位,属于这个模式的核心类。
它的职责如下所示:
- 接收策略客户方的业务请求
- 接收策略客户指定的具体策略服务方对象
- 将客户方的请求转发给策略服务方对象
- 在转发请求的时候,还可以扩展请求数据,提供策略服务自身所需要的其他数据。
下面的案例,实现了对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();
}
}
运行结果
上面的代码已经过时了
很遗憾,上面List集合的排序方式,调用Collections.sort()方法,在java8里面已经过时了。我们可以跳过Collections类,直接调用List接口的sort方法。代码如下:
为什么会这样,我们钻到Collections类的sort方法里面,一探究竟。
搜得死内,可以看出,Collections类的sort方法,啥事没干,直接调用了List接口的sort()方法。
我们再看看List接口的sort方法,它里面有啥内幕。
大家注意,这里面确实别有洞天,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() {
}
}
运行结果
上面的代码也已经过时了
很遗憾,上面的代码也已经过时了,大家注意,我们上面使用了Comparator接口的匿名类,这个代码以前很流行,在JDK1.8中,现在也已经过时了,大家看下面我的截图,new Comparator()在idea编辑器里面显示的灰色,表示这里的代码需要优化。它建议采用lambda表达式。
根据编辑器的提示,可以优化为lambda表达式语法,当你升级完成了后,编辑器又提升你优化代码,最后优化为以下代码。
Collections.sort(SKU2List, Comparator.comparingInt(SPU::getQuantity));
这些都是java8里面的新语法,大家可以根据情况,酌情使用。
java8的流及lambda是个坑,很大,很大,很大的坑,要掌握和精通里面的语法,有一定的门槛,跳不跳进这个坑,需要大家慎重考虑,但是,现实常常是无奈的,我们很可能没有选择余地,一个团队,只要有一个人用新的语法,其他人,也都或多或少要了解这些,否则项目无法维护。况且新的语法,有时候确实显得简洁,专业,除了可能有些晦涩难懂,可读性差些。大家可以一边用,一边骂,骂着骂着,就精通了。
方式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)));
}
}
};
}
}
运行结果
三种排序方式的汇总
方式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方法,所以这个方法,被时代淘汰了。
List.sort():
流程:
1,将要排序的List对象,转变为对象数组(所以,List集合排序,本质上是数组排序)
2,调用Arrays.sort(),对数组进行排序
3,将排序好的数组,又转化为List集合
Arrays.sort()
这个方法很忙,也是整个排序功能的中枢神经,将来在策略模式中,起到了关键作用。
1,它的分支语句里面,不仅使用了这个方法的入参,还使用了JVM参数。
2,它里面本质上调用了三种排序算法,有两种是独立的类(ComparableTimSort,TimSort),另外一种仅仅是Arrays类里面的方法(LegacyMergeSort),这些我们后面都会讲到。
TimSort.sort()->Comparator.compare()
当我们使用方式2,Comparator接口进行排序,则走这条调用链路。
看看里面,使用了Comparator接口的compare()方法,进行对象的大小比较。
这里面真有策略模式吗?
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()
ReferencePipeline. sorted()
SortedOps.end()
可以看到,java8 Stream里面的排序,底层也是Arrays对象的排序。所以策略模式与上面的相同。这里就不再赘述了。
一个让码农老吴也心有余悸的结论惊不惊喜,意不意外。
根据上面的分析,得出了一个让我也心有余悸的结论。
JDK中的集合排序中没有广为流传的策略模式,但是确实存在着另外一个真正意义的策略模式。
首先,Arrays类和Comparable 和 Comparator 这两个接口,他们并没有构成策略模式,因为它里面缺少策略代理方角色,如果非要说和策略模式沾点边,可以说它们之间的关系,是策略模式的前身,还没有发展成策略模式。
而另外一个真正意义的策略模式,是通过Arrays和ComparableTimSort,TimSort,legacyMergeSort()方法构成的。
Arrays是策略代理方
ComparableTimSort,TimSort,legacyMergeSort(),这两个类外加一个方法,都属于策略服务方。
这才是真正意义上的策略模式,虽然ComparableTimSort,TimSort,legacyMergeSort() 并没有执行相同的接口,但是其实它们结构一致,都实现了数组的排序算法。
本期我们就分享到这里,极客架构师,专注架构师成长,我将持续分享架构师的相关文章和视频,下期见。
,免责声明:本文仅代表文章作者的个人观点,与本站无关。其原创性、真实性以及文中陈述文字和内容未经本站证实,对本文以及其中全部或者部分内容文字的真实性、完整性和原创性本站不作任何保证或承诺,请读者仅作参考,并自行核实相关内容。文章投诉邮箱:anhduc.ph@yahoo.com