开发者博客 – IT技术 尽在开发者博客

开发者博客 – 科技是第一生产力


  • 首页

  • 归档

  • 搜索

那些年,我们又爱又恨的HashMap(一)

发表于 2020-03-30

一、HashMap集合简介

特点:

  • HashMap是Map接口的一个重要实现类,基于哈希表,以key-value的形式存储数据,线程不安全;
  • null可以作为键,这样的键只能有一个,可以有一个或多个键对应的值为null;
  • 存取元素无序。

底层数据结构:

  • JDK1.8之前,由数组+链表构成,数组是存储数据的主体,链表是为了解决哈希冲突而存在的;
  • JDK1.8以后,由数组+链表+红黑树构成,当链表长度大于阈值(默认为8),并且数组长度大于64时,链表会转化为红黑树去解决哈希冲突。

注意:
链表转化为红黑树之前会进行判断,如果阈值大于8,但是数组长度小于64,这时链表不会转化为红黑树去存储数据,而是会对数组进行扩容。

这样做的原因:
如果数组比较小,应尽量避免红黑树结构。因为红黑树结构较为复杂,红黑树又称为平衡二叉树,需要进行左旋、右旋、变色这些操作才能保证平衡。在数组容量较小的情况下,操作数组要比操作红黑树更节省时间。综上所述:为了提高性能以及减少搜索时间,在阈值大于8并且数组长度大于64的情况下链表才会转化为红黑树而存在。具体参考treeifyBin方法。

HashMap存储数据结构图:

二、HashMap底层存储数据的过程

1.以下面代码所示进行分析:
1
2
3
4
5
6
7
8
9
10
11
12
13
java复制代码package hashmap_demo;
import java.util.HashMap;
public class HashMapTest {
public static void main(String[] args) {
HashMap<String, Integer> map = new HashMap<>();
map.put("柳岩", 18);
map.put("杨幂", 28);
map.put("刘德华", 40);
map.put("柳岩", 20);
System.out.println(map);
}
}
//输出结果:{杨幂=28, 柳岩=20, 刘德华=40}
2.HashMap存储过程图:

3.存储过程分析:

1.当执行HashMap<String, Integer> map = new HashMap<>();这行代码创建HashMap实例对象时;在JDK1.8之前,会在构造方法中创建一个长度为16 的Entry[] table数组用来存储键值对;JDK1.8之后,创建数组的时机发生了变化,不是在构造方法中创建数组了,而是在第一次调用put()方法时(即第一次向HashMap中添加元素)创建Node[] table数组。

注意:
创建HashMap实例对象在JDK1.8前后发生了变化,主要有两点:创建的时机发生了变化;数组类型发生了变化,由原来的Entry[]类型变为Node[]类型。

2.向哈希表中存储柳岩-18,会根据柳岩调用String类中重写后的hashCode()方法计算出柳岩对应的哈希值,然后结合数组长度采用某种算法计算出柳岩在Node[]数组中的索引值。如果该索引位置上无数据,则直接将柳岩-18插入到该索引位置。比如计算出柳岩对应的索引为3,如上图所示。

面试题:哈希表底层采用那种算法计算出索引值?还有哪些算法计算索引值?

答:采用key的hashCode()方法计算出哈希值,然后结合数组长度进行无符号右移(>>>)、按位异或(^)、按位与(&)计算出索引值;还可以采用平方取中法、取余数、伪随机数法。

取余数:10%8=2 11%8=3;位运算效率最高,其他方式效率较低。

3.向哈希表中存储杨幂-28,计算出该索引位置无数据,直接插入。

4.向哈希表中存储刘德华-40,假设刘德华计算出的索引也是3,那么此时该索引位置不为null,这时底层会比较柳岩和刘德华的哈希值是否一致,如果不一致,则在此索引位置上划出一个节点来存储刘德华-40,这种方式称为拉链法。

补充:索引计算源码p = tab[i = (n - 1) & hash],即索引=哈希值&(数组长度-1),按位与运算等价于取余运算,因为51%16=3,19%16=3,所以会出现同一个数组,索引值相同,但哈希值不同的情况。

5.最后向哈希表中存储柳岩-20,柳岩对应的索引值为3。因为该索引位置已有数据,所以此时会比较柳岩与该索引位置上的其他数据的哈希值是否相等,如果相等,则发生哈希碰撞。此时底层会调用柳岩所属String字符串类中的equals()方法比较两个对象的内容是否相同:

相同:则后添加数据的value值会覆盖之前的value值,即柳岩-20覆盖掉柳岩-18。

不相同:继续和该索引位置的其他对象进行比较,如果都不相同,则向下划出一个节点存储(拉链法)。

注意点:如果一个索引位置向下拉链,即链表长度大于阈值8且数组长度大于64,则会将此链表转化为红黑树。因为链表的时间复杂度为O(N),红黑树的时间复杂度为O(logN),链表长度多大时O(N)>O(logN)。

三、HashMap的扩容机制

1.HashMap什么时候进行扩容?

首先看添加元素的put()方法流程:

说明:

  • 上图中的size表示HashMap中K-V的实时数量,不等于数组的长度;
  • threshold(临界值)=capacity(数组容量)*loadFactory(加载因子),临界值表示当前已占用数组的最大值。size如果超过这个临界值时调用resize()方法进行扩容,扩容后的容量是原来的两倍;
  • 默认情况下,16*0.75=12,即HashMap中存储的元素超过12就会进行扩容。
2.HashMap扩容后的大小是多少?
1
复制代码是原来容量的2倍,即HashMap是以2n进行扩容的。
3.HashMap的默认初始容量是多少?

HashMap的无参构造,默认初始值为16,源码如下:

1
2
3
4
5
6
7
java复制代码/**
* Constructs an empty <tt>HashMap</tt> with the default initial capacity
* (16) and the default load factor (0.75).
*/
public HashMap() {
this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted
}

默认初始值源码:

1
2
3
4
5
java复制代码/**
* The default initial capacity - MUST be a power of two.
* 默认初始容量必须是2的幂
*/
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4;

由源码可以看到,HashMap的默认初始容量为1左移4位,即1*2的4次方为16。如果使用HashMap的无参构造进行初始化,第一次put元素时,会触发resize()方法(扩容方法),扩容后的容量为16。这一点和ArrayList初始化过程很相似(使用ArrayList的无参构造初始化时,创建的是一个空数组,当第一次向空数组添加元素时会触发grow()扩容方法,扩容后的容量为10)。

4.指定初始容量为什么必须是2的幂?

HashMap的有参构造,即可以指定初始化容量大小,源码如下:

1
2
3
4
5
6
7
8
9
10
java复制代码/**
* Constructs an empty <tt>HashMap</tt> with the specified initial
* capacity and the default load factor (0.75).
*
* @param initialCapacity the initial capacity.
* @throws IllegalArgumentException if the initial capacity is negative.
*/
public HashMap(int initialCapacity) {
this(initialCapacity, DEFAULT_LOAD_FACTOR);
}

即构造一个指定容量和默认加载因子(0.75)的空HashMap。

由上面的内容我们知道,当向HashMap中添加元素时,首先会根据key的哈希值结合数组长度计算出索引位置。HashMap为了存取高效需要减少哈希碰撞,使数据分配均匀,采用按位与hash&(length-1)计算索引值。

HashMap采用取余的算法计算索引,即hash%length,但是取余运算不如位运算效率高,所以底层采用按位与hash&(length-1)进行运算。两种算法等价的前提就是length是2的n次幂。

5.为什么这样就能均匀分布?

我们需要知道两个结论:

  • 2的n次方就是1后面n个0;如2的4次方为16,二进制表示为10000;
  • 2的n次方-1就是n个1;比如2的4次方-1为15,二进制表示为1111。

举例说明为什么数组长度是2的n次幂可以均匀分布:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
java复制代码按位与运算:相同二进制位上都是1,结果为1,否则为0。
假设数组长度为2的3次幂8,哈希值为3,即3&(8-1)=3,索引为3;
假设数组长度为2的3次幂8,哈希值为2,即2&(8-1)=2,索引为2;
运算过程如下:
3&(8-1)
0000 0011 -->3
0000 0111 -->7
----------------
0000 0011 -->3

2&(8-1)
0000 0010 -->2
0000 0111 -->7
----------------
0000 0010 -->2

结论:索引值不同,不同索引位置都有数据分布,分布均匀。

假设数组长度不是2的n次幂,比如长度为9,运算过程如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
java复制代码假设数组长度为9,哈希值为3,即3&(9-1)=3,索引为0;
假设数组长度为9,哈希值为2,即2&(9-1)=2,索引为0;
运算过程如下:
3&(9-1)
0000 0011 -->3
0000 1000 -->8
----------------
0000 0000 -->0

2&(9-1)
0000 0010 -->2
0000 1000 -->8
----------------
0000 0000 -->0

结论:索引值都为0,导致同一索引位置上有很多数据,而其他索引位置没有数据,致使链表或红黑树过长,效率降低。

注意:
hash%length等价于hash&(length-1)的前提条件是数组长度为2的n次幂。由于底层采用按位与运算计算索引值,所以需要保证数组长度必须为2的n次幂。

6.如果指定的初始容量不是2的n次幂会怎样?
1
复制代码这时HashMap会通过位运算和或运算得到一个2的幂次方数,并且这个数是离指定容量最小的2的幂次数。比如初始容量为10,经过运算最后会得到16。

该过程涉及到的源码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
java复制代码//创建HashMap集合对象,并指定容量为10,不是2的幂
HashMap<String, Integer> map = new HashMap<>(10);
//调用有参构造
public HashMap(int initialCapacity) {
this(initialCapacity, DEFAULT_LOAD_FACTOR);
}
//this关键字继续调用
public HashMap(int initialCapacity, float loadFactor) {//initialCapacity=10
if (initialCapacity < 0)
throw new IllegalArgumentException("Illegal initial capacity: " +
initialCapacity);
if (initialCapacity > MAXIMUM_CAPACITY)
initialCapacity = MAXIMUM_CAPACITY;
if (loadFactor <= 0 || Float.isNaN(loadFactor))
throw new IllegalArgumentException("Illegal load factor: " +
loadFactor);
this.loadFactor = loadFactor;
this.threshold = tableSizeFor(initialCapacity);//initialCapacity=10
}
//调用tableSizeFor()方法
/**
* Returns a power of two size for the given target capacity.
* 返回指定目标容量的2的幂。
*/
static final int tableSizeFor(int cap) {
int n = cap - 1;
n |= n >>> 1;
n |= n >>> 2;
n |= n >>> 4;
n |= n >>> 8;
n |= n >>> 16;
return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1;
}

下面分析tableSizeFor()方法:

  • int n = cap - 1;为什么要减1操作呢?
1
go复制代码这是为了防止`cpa`已经是2的幂了。如果`cpa`已经是2的幂,又没有执行减1的操作,则执行完下面的无符号右移后,返回的将为`cap`的2倍。
  • n等与0时,返回1,这里不讨论你等于0的情况。
  • |表示按位或运算:运算规则为相同二进制位上都是0,结果为0,否则为1。

第1次运算:

1
2
3
4
5
6
java复制代码int n = cap - 1;//cap=10,n=9
n |= n >>> 1;//无符号右移1位,然后再与n进行或运算
00000000 00000000 00000000 00001001 //n=9
00000000 00000000 00000000 00000100 //9无符号右移1位变为4
-----------------------------------------------
00000000 00000000 00000000 00001101 //按位或运算结果为13,即此时n=13

第2次运算:

1
2
3
4
5
6
java复制代码int n = 13
n |= n >>> 2;
00000000 00000000 00000000 00001101 //n=13
00000000 00000000 00000000 00000011 //13无符号右移2位变为3
------------------------------------------------
00000000 00000000 00000000 00001111 //按位或运算结果为15,即此时n=15

第3次运算:

1
2
3
4
5
6
java复制代码int n = 15
n |= n >>> 4;
00000000 00000000 00000000 00001111 //n=15
00000000 00000000 00000000 00000000 //15无符号右移4位变为0
------------------------------------------------
00000000 00000000 00000000 00001111 //按位或运算结果为15,即此时n=15

接下来的运算结果都是n=15,由于最后有一个n + 1操作,最后结果为16。

总结:
由以上运算过程可以看出,如果指定的初始容量不是2的n次幂,经过运算后会得到离初始容量最小的2幂。

四、HashMap源码分析

1.成员变量
1
java复制代码private static final long serialVersionUID = 362498820763181265L; //序列化版本号
1
java复制代码static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; //初始化容量,必须是2的n次幂
1
java复制代码static final int MAXIMUM_CAPACITY = 1 << 30; //集合最大容量:2的30次幂
1
2
3
4
5
6
7
java复制代码static final float DEFAULT_LOAD_FACTOR = 0.75f; //默认的加载因子
/**1.加载因子是用来衡量HashMap的疏密程度,计算HashMap的实时加载因子的方法为:size/capacity;
*2.加载因子太大导致查找元素效率低,太小导致数组的利用率低,默认值为0.75f是官方给出的一个较好的临界值;
*3.当HashMap里面容纳的元素已经达到HashMap数组长度的75%时,表示HashMap太挤了,需要扩容,而扩容这个过程涉及到rehash、复制数据等操作,非常消耗性能,所以开发中尽量减少扩容的次数,可以通过创建HashMap集合对象时指定初始容量来尽量避免扩容;
*4.同时在HashMap的构造方法中可以指定加载因子大小。
*/
HashMap(int initialCapacity, float loadFactor) //构造一个带指定初始容量和加载因子的空HashMap
1
java复制代码static final int TREEIFY_THRESHOLD = 8; //链表转红黑树的第一个条件,链表长度大于阈值8
1
java复制代码static final int UNTREEIFY_THRESHOLD = 6; //删除红黑树节点时,当红黑树节点小于6,转化为链表
1
java复制代码static final int MIN_TREEIFY_CAPACITY = 64; //链表转红黑树的第二个条件,数组长度大于64

五、常见面试题

1.发生哈希碰撞的条件是什么?
1
复制代码两个对象的索引相同,并且hashCode(即哈希值)相等时,会发生哈希碰撞。
2.如何解决哈希冲突?
1
复制代码JDK1.8之前,采用链表解决;JDK1.8之后,采用链表+红黑树解决。
3.如果两个key的hashCode相同,如何存储?
1
2
3
4
5
csharp复制代码使用equals比较内容是否相同:

相同:后添加的value值会覆盖之前的value值;

不相同:划出一个节点存储(拉链法)。
4.HashMap的底层数据结构?

JDK1.8:数组+链表+红黑树。其中数组是主体,链表和红黑树是为解决哈希冲突而存在的,具体如下图所示:

5.JDK1.8为什么引入了红黑树?红黑树结构不是更复杂吗?

JDK1.8以前HashMap的底层数据是数组+链表,我们知道,即使哈希函数做得再好,哈希表中的元素也很难达到百分之百均匀分布。当HashMap中有大量的元素都存在同一个桶(同一个索引位置),这个桶下就会产生一个很长的链表,这时HashMap就相当于是一个单链表的结构了,假如单链表上有n个元素,则遍历的时间复杂度就是O(n),遍历效率很低。针对这种情况,JDK1.8引入了红黑树,遍历红黑树的时间复杂度为O(logn),由于O(n)>O(logn);所以这一问题得到了优化。

6.为什么链表长度大于8才转化为红黑树?

我们知道8是从链表转成红黑树的阈值,在源码中有这样一段注释内容:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
JAVA复制代码/** Because TreeNodes are about twice the size of regular nodes, we use them only when     * bins contain enough nodes to warrant use (see TREEIFY_THRESHOLD). And when they         * become too small (due to removal or resizing) they are converted back to plain bins.   * In usages with well-distributed user hashCodes, tree bins are rarely used.  Ideally,   * under random hashCodes, the frequency of nodes in bins follows a Poisson distribution
* (http://en.wikipedia.org/wiki/Poisson_distribution) with a parameter of about 0.5 on * average for the default resizing threshold of 0.75, although with a large variance * because of resizing granularity. Ignoring variance, the expected occurrences of list * size k are (exp(-0.5) * pow(0.5, k) / factorial(k)). The first values are:
*
* 0: 0.60653066
* 1: 0.30326533
* 2: 0.07581633
* 3: 0.01263606
* 4: 0.00157952
* 5: 0.00015795
* 6: 0.00001316
* 7: 0.00000094
* 8: 0.00000006
* more: less than 1 in ten million
*/

翻译过来的意思就是说:

红黑树节点所占空间是普通链表节点的两倍,并且链表中存储数据的频率符合泊松分布,我们可以看到,在链表为8的节点上存储数据的概率是0.00000006,这也就表明超过8以后的节点存储数据的概率就非常小了。

由上述分析可以得出:

  • 如果小于阈值8就是用红黑树,会使得结构一开始就很复杂;
  • 如果大于阈值8还使用链表,会导致链表节点不能被充分利用;
  • 所以,阈值8是科学合理的一个值,是空间和时间的权衡值。
7.为什么加载因子设置为0.75?边界值是12?
  • 如果加载因子是0.4,那么16*0.4=6,致使数组中满6个空间就扩容,造成数组利用率太低了;
  • 如果加载因子是0.9,那么16*0.9=14,这样就会使数组太满,很大几率造成某一个索引节点下的链表过长,进而导致查找元素效率低;
  • 所以兼顾数组利用率又考虑链表不要太长,经过大量测试0.75是最佳值。

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

SpringBoot源码初学者(二):SpringBoot事

发表于 2020-03-30

ps:真正适合阅读源码的新手来看的SpringBoot源码讲解,如果你真的想读懂SpringBoot源码,可以按照以下推荐的方式来阅读文章

  1. 打开ide,打开SpringBoot源码,跟着文章一起写注释,写自己的注释
  2. 不要过于纠结没讲到的地方,毕竟SpringBoot源码那么多,想全讲完是不可能的,只要跟着文章认真阅读,SpringBoot是如何运行的一定可以有一个较为深刻的理解
  3. 文章适合通篇阅读,不适合跳读,跳跃性的阅读很容易错过重要的东西
  4. 同样的如果之前的文章没有读过,还是最好先去看之前的文章
  5. 阅读源码必然少不了大段大段的源码,一定要耐心,不要翻翻了事,往往是那些最长的方法中才是真正需要学习的
  6. 如果断更了请用点赞、收藏、评论的方式激励我

系列文章链接:
《SpringBoot源码初学者(一):SpringBoot功能扩展接口的使用与源码分析》

一、监听器模式

  在学习的路上遵循一些原则,可以更高效的学习,其中就有这么一条“循循渐进”,在深入SpringBoot之前先要了解清楚什么是监听器,监听器是如何实现的,这些都是对付大魔王的神兵利器,和RPG游戏一样打boss之前先要打小怪提升等级,爆出“屠龙宝刀”。
  伊泽瑞尔作为瓦罗拉大陆上组名的探险家在探险的路上,却总是受到天气的影响无法冒险,所以他拜托我帮他写一个软件,辅助他关注天气。

在这里插入图片描述

1、监听器模式小demo!天气监听器

步骤1:创建抽象类WeatherEvent(天气状态)

1
2
3
4
复制代码public abstract class weatherEvent{
//获取天气状态
public abstract String getWeather();
}

步骤2:实现下雪和下雨事件
下雪事件

1
2
3
4
5
6
复制代码public class SnowEvent extends WeatherEvent{
@Overide
public String getWeather(){
return "下雪了";
}
}

下雨事件

1
2
3
4
5
6
复制代码public class RainEvent extends WeatherEvent{
@Overide
public String getWeather(){
return "下雨了";
}
}

步骤3:创建天气监听器接口

1
2
3
复制代码public interface WeatherListener{
void onWeatherEvent(WeatherEvent event);
}

步骤4:实现监听器,分别处理下雪和下雨的天气
下雪的时候需要穿上大棉袄,带上手套御寒

1
2
3
4
5
6
7
8
9
复制代码public class SnowListener implements WeatherListener{
@Override
public void onWeatherEvent(WeatherEvent event){
if(event instanceof SnowEvent){
event.getWeather();
System.out.println("今天下雪!请增加衣物,做好御寒保护!");
}
}
}

下雨的时候需要带雨伞,穿雨鞋

1
2
3
4
5
6
7
8
9
复制代码public class RainListener implements WeatherListener{
@Override
public void onWeatherEvent(WeatherEvent event){
if(event instanceof RainEvent){
event.getWeather();
System.out.println("今天下雨!出门请带好雨伞");
}
}
}

步骤5:创建广播器接口

1
2
3
4
5
6
7
8
复制代码public interface EventMulticaster{
//广播事件
void multicastEvent(WeatherEvent event);
//添加监听器
void addListener(WeatherListener weaterListener);
//删除监听器
void removeListener(WeatherListener weaterListener);
}

步骤6:抽象类实现广播接口

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
复制代码public abstract class AbstractEventMulticaster implements EventMulticaster{
//存放监听器的集合,所有需要监听的事件都存在这里
private List<WeaterListener> listenerList = new ArrayList<>();

@Override
public void multicastEvent(WeatherEvent event){
//采用模板方法,子类可以实现的doStart和doEnd,在调用监听器之前和之后分别作出扩展
//SpringBoot中有着大量相似的操作
//SpringBoot中的前置处理器和后置处理器,就是这样实现的
doStart();
//循环所有调用所有监听器的onWeatherEvent方法
listenerList.forEach(i -> i.onWeatherEvent(evnet));
doEnd();
}

@Override
public void addListener(WeatherListener weaterListener){
listenerList.add(weaterListener);
}

@Override
public void removeListener(WeatherListener weaterListener){
listenerList.remove(weaterListener);
}

abstract void doStart();
abstract void doEnd();
}

步骤7:实现天气事件的广播

1
2
3
4
5
6
7
8
9
10
11
复制代码public class WeatherEventMulticaster extends AbstractEventMulticaster{
@Override
void doStart(){
System.out.println("开始广播天气预报!");
}

@Override
void doEnd(){
System.out.println("广播结束!Over!");
}
}

步骤8:测试并触发广播

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
复制代码public class Test{
public static void main(String[] args){
//创建广播器
WeatherEventMulticaster eventMulticaster = new WeatherEventMulticaster();
//创建监听器
RainListener rainListener = new RainListener();
SnowListener snowListener = new SnowListener();
//添加监听器
eventMulticaster.addListener(rainListener);
eventMulticaster.addListener(snowListener);

//触发下雨事件
eventMulticaster.multicastEvent(new RainEvent());
//除非下雪事件
eventMulticaster.multicastEvent(new SnowEvent());
}
}

2、黑默丁格大讲堂,监听器模式机制讲解

  伊泽瑞尔的探险活动终于不再受到天气的骚扰了,可是他并不明白小小的玩意为什么如此神奇,多次询问过我,可是无赖我语言贫乏,无法将如此复杂的思想表达清楚,只要求助老友黑默丁格,帮忙说明。

ps:工作中不仅要能实现功能,还要注重表达能力,在面试的时候能把思想表达的清楚可以拿到更高的薪资,在和测试交流的时候可以帮助测试理解实现原理,测试出隐藏在深处的bug,当然作为天才程序员的大伙是没有bug的,肯定是环境问题或者操作不当导致的。

黑默丁格拿到代码,简单看了两眼就分析出了各个模块的作用:

  • 事件:步骤1和步骤2,通过对天气进行抽象,并实现下雨和下雪的天气状态
  • 监听器:步骤3和步骤4,规范对天气监听的模式,并且规范对应天气下,需要如何处理
  • 广播器:步骤5、步骤6和步骤7,当有事件发生的时候,广播器发出信号,告知所有的监听器,监听器根据事件作出相应的处理。触发下雨事件的时候,下雨监听器收到消息,它抬头一看乌云密布电闪雷鸣,微微一愣,大喊一句:“打雷下雨收衣服啊!!”,广播器继续通知下一个监听器下雪监听器,下雪监听器看看天空,摆摆手,说:“这事与我无关去找别人”
  • 触发机制:步骤8,demo中采用的硬编码的形式触发的,在实际运用中,可能是湿度仪检测到湿度暴涨开始下雨了,触发广播。

  在23种设计模式中是没有监听器模式的,监听器模式是观察者模式的一种实现,这两个名字都容易让人产生一些误导,在“监听”、“观察”很容易让人觉得是监听器发现了事件,然后行动。实际上是广播器把事件推送给所有的监听器,每个监听器都对事件做出判断和处理。

二、SpringBoot事件监听器的实现

1、ApplicationListener接口

  ApplicationListener是Spring事件机制的一部分,与抽象类ApplicationEvent类配合来完成ApplicationContext的事件机制,实现ApplicationListener接口的类,会在SpringBoot加入到广播器中,当ApplicationContext触发了一个事件,就用广播器通知所有实现ApplicationListener接口的类。

1
2
3
4
5
6
7
8
9
10
11
12
13
复制代码//这个注解表示,当前类只有一个方法
@FunctionalInterface
//传入的泛型,说明这个监听器,需要监听的事件类型
//继承的EventListener类,是个空类,主要是声明继承它的类是个事件监听器,面向对象编程的思想体现
public interface ApplicationListener<E extends ApplicationEvent> extends EventListener {

/**
* Handle an application event.
* @param event the event to respond to
*/
void onApplicationEvent(E event);

}

  不难发现ApplicationListener的接口与我们实现的天气监听器的步骤3几乎一样,如果理解了小demo这个类的作用肯定已经了解的明明白白。

2、ApplicationEventMulticaster接口

  ApplicationEventMulticaster是Spring事件机制的广播器接口,所有的广播器都需要实现此接口,主要作用是管理所有的监听器,以及推送事件给监听器。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
复制代码public interface ApplicationEventMulticaster {

//添加一个监听器
void addApplicationListener(ApplicationListener<?> listener);

//根据beanName添加一个监听器
void addApplicationListenerBean(String listenerBeanName);

//移除一个监听器
void removeApplicationListener(ApplicationListener<?> listener);

//根据beanName移除一个监听器
void removeApplicationListenerBean(String listenerBeanName);

//移除所有监听器
void removeAllListeners();

//广播事件的方法
void multicastEvent(ApplicationEvent event);

void multicastEvent(ApplicationEvent event, @Nullable ResolvableType eventType);
}

3、SpringBoot的7大事件

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-cu6pwkNo-1585491460589)(en-resource://database/2523:1)]

  • EventObject:事件顶级对象,所有事件对象的根对象
  • ApplicationEvent:应用事件
  • SpringApplicationEvent:Spring自己的事件,Spring框架自身的事件都会实现这个接口
  • ApplicationStartingEvent:启动事件,框架刚刚启动就会发出这个事件
  • ApplicationEnvironmentPreparedEvent:环境在变完成,系统属性和用户指定已经加载完成
  • ApplicationContextInitializedEvent:已经创建好了上下文,并且还没有加载任何bean之前发出这个事件
  • ApplicationPreparedEvent:在Bean定义开始加载之后,尚未完全加载之前,刷新上下文之前触发
  • ApplicationStartedEvent:bean已经创建完成,上下文已经刷新完成,但是ApplicationRunner和CommandLineRunne两个扩展接口并未执行
  • ApplicationReadyEvent:ApplicationRunner和CommandLineRunne两个扩展接口执行完成之后触发
  • ApplicationFailedEvent:在启动发生异常时触发

(1)事件发生顺序

启动 —》ApplicationStartingEvent —》ApplicationEnvironmentPreparedEvent —》ApplicationContextInitializedEvent —》 ApplicationPreparedEvent —》ApplicationStartedEvent —》 ApplicationReadyEvent —》启动完毕

中间发生异常 —》ApplicationFailedEvent —》启动失败

4、事件监听器的源码分析

(1)监听器注册流程

如果看过之前的文章
《 SpringBoot源码初学者(一):SpringBoot功能扩展接口的使用与源码分析》:https://juejin.cn/post/6844904106843193357
这里就很容易理解,不想完整的阅读可以只看一下工厂加载机制源码解析的部分
与ApplicationContextInitializer接口完全一样的流程进行注册的,只是把ApplicationContextInitializer接口换成了ApplicationListener接口

我们还是从最开始的main方法一步步看。
步骤1:查看SpringBoot启动类

1
2
3
4
5
6
7
复制代码@SpringBootApplication
public class Application {
public static void main(String[] args) {
//进入run方法的源码
SpringApplication.run(Application.class, args);
}
}

步骤2:这里可以看到一层简单的调用

1
2
3
4
5
复制代码public static ConfigurableApplicationContext run(Class<?> primarySource,
String... args) {
//进入这个同名方法,继续戳run方法
return run(new Class<?>[] { primarySource }, args);
}

步骤3:这里就比较有意思了,注意一下注释

1
2
3
4
5
复制代码public static ConfigurableApplicationContext run(Class<?>[] primarySources,
String[] args) {
//点这个SpringApplication构造方法
return new SpringApplication(primarySources).run(args);
}

步骤4:没有什么用的封装,对构成函数复用

1
2
3
4
复制代码public SpringApplication(Class<?>... primarySources) {
//点this,查看构造函数
this(null, primarySources);
}

步骤5:这里我们可以看到两个熟悉的名字getSpringFactoriesInstances方法和ApplicationContextInitializer接口

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
复制代码public SpringApplication(ResourceLoader resourceLoader, Class<?>... primarySources) {
this.resourceLoader = resourceLoader;
Assert.notNull(primarySources, "PrimarySources must not be null");
this.primarySources = new LinkedHashSet<>(Arrays.asList(primarySources));
this.webApplicationType = WebApplicationType.deduceFromClasspath();
//这里就是上一篇文章说的ApplicationContextInitializer接口注册
setInitializers((Collection) getSpringFactoriesInstances(
ApplicationContextInitializer.class));
//这里就是ApplicationListener注册的位置,可以看出主要区别就是查询的接口类不同
//setListeners是找到的对象存到容器中,存到一个list属性中,方便以后使用
//这个存放对象的list,对应的是小demo的AbstractEventMulticaster类中list,作用是一样一样的
//getSpringFactoriesInstances方法详解参考文章《SpringBoot功能扩展接口的使用与源码分析》
setListeners((Collection) getSpringFactoriesInstances(ApplicationListener.class));
this.mainApplicationClass = deduceMainApplicationClass();
}

(2)监听器触发流程

步骤1:查看SpringBoot启动类

1
2
3
4
5
6
复制代码@SpringBootApplication
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
}

步骤2:ConfigurableApplicationContext类

1
2
3
4
复制代码public static ConfigurableApplicationContext run(Class<?> primarySource,
String... args) {
return run(new Class<?>[] { primarySource }, args);
}

步骤3:这次进入run方法

1
2
3
4
5
复制代码public static ConfigurableApplicationContext run(Class<?>[] primarySources,
String[] args) {
//点击run方法
return new SpringApplication(primarySources).run(args);
}

步骤4:每次看到这个方法,都感觉它罪孽深重,多少人从它开始看起,踏上阅读源码的不归路
代码较长,这次就不写所有的注释了,具体注释看这里https://juejin.cn/post/6844904106843193357

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
复制代码	public ConfigurableApplicationContext run(String... args) {
StopWatch stopWatch = new StopWatch();
stopWatch.start();
ConfigurableApplicationContext context = null;
Collection<SpringBootExceptionReporter> exceptionReporters = new ArrayList<>();
configureHeadlessProperty();
//获取事件运行器
//SpringApplicationRunListeners内部包含一个SpringApplicationRunListener(这里s没有了)的集合
//SpringApplicationRunListener有7大事件的执行方法,在对应的地点会被调用,SpringBoot通过这个实现事件的触发
//SpringBoot自带一个实现,这个实现分别会执行定义好的7大事件
//使用者可以通过实现SpringApplicationRunListener的接口,定义在对应事件所需执行的命令
//总体流程还是很简单的,留给大家自己阅读
SpringApplicationRunListeners listeners = getRunListeners(args);
//监听器的故事从这里开始,我们这次的故事也从这里起航
//进入starting方法
listeners.starting();
try {
ApplicationArguments applicationArguments = new DefaultApplicationArguments(args);
ConfigurableEnvironment environment = prepareEnvironment(listeners, applicationArguments);
configureIgnoreBeanInfo(environment);
Banner printedBanner = printBanner(environment);
context = createApplicationContext();
exceptionReporters = getSpringFactoriesInstances(SpringBootExceptionReporter.class,
new Class[] { ConfigurableApplicationContext.class }, context);
prepareContext(context, environment, listeners, applicationArguments, printedBanner);
refreshContext(context);
afterRefresh(context, applicationArguments);
stopWatch.stop();
if (this.logStartupInfo) {
new StartupInfoLogger(this.mainApplicationClass).logStarted(getApplicationLog(), stopWatch);
}
listeners.started(context);
callRunners(context, applicationArguments);
}
catch (Throwable ex) {
handleRunFailure(context, ex, exceptionReporters, listeners);
throw new IllegalStateException(ex);
}

try {
listeners.running(context);
}
catch (Throwable ex) {
handleRunFailure(context, ex, exceptionReporters, null);
throw new IllegalStateException(ex);
}
return context;
}

步骤5:没有千层套路

1
2
3
4
5
6
7
8
复制代码public void starting() {
//listeners里面存放了所有的SpringApplicationRunListener(事件触发器)
for (SpringApplicationRunListener listener : this.listeners) {
//循环执行事件触发器的starting方法
//点击进入看看SpringBoot自带的事件触发器是如何运行的
listener.starting();
}
}

步骤6:广播器发送事件

1
2
3
4
5
复制代码@Override
public void starting() {
//initialMulticaster是广播器
this.initialMulticaster.multicastEvent(new ApplicationStartingEvent(this.application, this.args));
}

步骤7:广播器发送事件

1
2
3
4
5
6
复制代码@Override
public void starting() {
//initialMulticaster是广播器
//进入multicastEvent方法
this.initialMulticaster.multicastEvent(new ApplicationStartingEvent(this.application, this.args));
}

步骤8:广播事件的时候要判断这个事件的类型,判断需不需要在这个时间点执行

1
2
3
4
5
6
7
复制代码@Override
public void multicastEvent(ApplicationEvent event) {
//resolveDefaultEventType方法,解析事件的默认类型
//进入resolveDefaultEventType方法,步骤9
//进入multicastEvent方法,步骤11
multicastEvent(event, resolveDefaultEventType(event));
}

步骤9:获取事件类型

1
2
3
4
5
复制代码private ResolvableType resolveDefaultEventType(ApplicationEvent event) {
//获取事件类型
//进入forInstance方法,步骤10
return ResolvableType.forInstance(event);
}

步骤10:通过接口判断时间类型

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
复制代码public static ResolvableType forInstance(Object instance) {
//断路判断,如果instance是个空,就停止SpringBoot的启动,并报错
Assert.notNull(instance, "Instance must not be null");
//判断有没有实现ResolvableTypeProvider这个接口
//ResolvableTypeProvider接口,表明这个类的事件类型可以被解析
if (instance instanceof ResolvableTypeProvider) {
//强转成ResolvableTypeProvider类型,然后获取事件类型
ResolvableType type = ((ResolvableTypeProvider) instance).getResolvableType();
if (type != null) {
//事件类型不为空,就直接返回
return type;
}
}
//返回一个默认类型,传进来的instance是什么类型,就把这个类型包装成ResolvableType,然后返回
//返回步骤8
return ResolvableType.forClass(instance.getClass());
}

步骤11:开始广播
两个参数:event:需要执行的事件    eventType:事件的类型

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
复制代码@Override
public void multicastEvent(final ApplicationEvent event, @Nullable ResolvableType eventType) {
//如果事件类型为空,执行resolveDefaultEventType方法(步骤9和步骤10)
ResolvableType type = (eventType != null ? eventType : resolveDefaultEventType(event));
//获取任务的执行的线程池
//如果没有特别指定,返回为null,SpringBoot这里就是空的
Executor executor = getTaskExecutor();
//getApplicationListeners方法,获取对这个事件感兴趣的监听器
//点击进入getApplicationListeners方法,进入步骤12
for (ApplicationListener<?> listener : getApplicationListeners(event, type)) {
if (executor != null) {
//在指定线程上执行触发
executor.execute(() -> invokeListener(listener, event));
}
else {
//默认方式执行触发
invokeListener(listener, event);
}
}
}

步骤12:获取对这个事件感兴趣的监听器(缓存获取逻辑)
参数说明:
event:当前发生的事件,这个方法就是找到对这个事件感兴趣的监听器
eventType:事件类型

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
复制代码protected Collection<ApplicationListener<?>> getApplicationListeners(
ApplicationEvent event, ResolvableType eventType) {
//获取事件发生的源头类,这里就是SpringApplication
Object source = event.getSource();
//获取原头类的类型
Class<?> sourceType = (source != null ? source.getClass() : null);
//获取缓存的key
ListenerCacheKey cacheKey = new ListenerCacheKey(eventType, sourceType);

//快速执行,从缓存中获取监听器,如果这个方法已经执行了过了,就不要在获取一次了,直接拿到缓存
ListenerRetriever retriever = this.retrieverCache.get(cacheKey);
if (retriever != null) {
//返回对当前事件感兴趣的监听器
return retriever.getApplicationListeners();
}

if (this.beanClassLoader == null ||
(ClassUtils.isCacheSafe(event.getClass(), this.beanClassLoader) &&
(sourceType == null || ClassUtils.isCacheSafe(sourceType, this.beanClassLoader)))) {
//通过key上锁,这是上锁的一个很有效的方式,定义一个属性作为锁的key
synchronized (this.retrievalMutex) {
//上锁之后再次检查,有没有其他地方触发了当前事件,把监听器的列表放入了缓存中
//写过双层验证的单例模式对这里不会陌生,主要原理是一样的
retriever = this.retrieverCache.get(cacheKey);
if (retriever != null) {
//返回对当前事件感兴趣的监听器
return retriever.getApplicationListeners();
}
retriever = new ListenerRetriever(true);
//真正的查找逻辑被封装在这里
//SpringBoot这种千层套路,是有规律可循的,这一次是缓存的封装,下一次是实际的调用
//我们编程的时候可以学习一下,比如封装缓存的查询,再去数据库,降低耦合度
//点retrieveApplicationListeners方法进入 步骤13
Collection<ApplicationListener<?>> listeners =
retrieveApplicationListeners(eventType, sourceType, retriever);
//存入缓存中
this.retrieverCache.put(cacheKey, retriever);
return listeners;
}
}
else {
//不需要加锁的,并且不需要缓存的查询方式
//这个方法中有两处调用了retrieveApplicationListeners方法,在方法的内部对有无缓存,做了不同的处理
//个人观点:应该把内部的缓存逻辑移到这层中,否则耦合度依旧很高
return retrieveApplicationListeners(eventType, sourceType, null);
}
}

步骤13:真正获取监听器的逻辑

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
复制代码private Collection<ApplicationListener<?>> retrieveApplicationListeners(
ResolvableType eventType, @Nullable Class<?> sourceType, @Nullable ListenerRetriever retriever) {

List<ApplicationListener<?>> allListeners = new ArrayList<>();
Set<ApplicationListener<?>> listeners;
Set<String> listenerBeans;
synchronized (this.retrievalMutex) {
//获取所有的监听器实例
listeners = new LinkedHashSet<>(this.defaultRetriever.applicationListeners);
//获取所有监听器的beanName
listenerBeans = new LinkedHashSet<>(this.defaultRetriever.applicationListenerBeans);
}
//对所有的监听器进行逐一的循环
for (ApplicationListener<?> listener : listeners) {
//判断监听器是否对这个事件感兴趣
//点击supportsEvent方法进入 步骤14
if (supportsEvent(listener, eventType, sourceType)) {
if (retriever != null) {
//如果监听器功能开启了缓存,就存到缓存中
retriever.applicationListeners.add(listener);
}
//不管有没有缓存都会存到这里
allListeners.add(listener);
}
}
//通过工厂方式,获取监听器,一般情况不会走这里
if (!listenerBeans.isEmpty()) {
//获取bean工厂
BeanFactory beanFactory = getBeanFactory();
//循环监听器beanName
for (String listenerBeanName : listenerBeans) {
try {
//更具beanName,获取监听器的类型
Class<?> listenerType = beanFactory.getType(listenerBeanName);
// 判断监听器是否对这个事件感兴趣
if (listenerType == null || supportsEvent(listenerType, eventType)) {
//获取bean实例,这个方法写作getBean,读作createBean
//这是ioc中非常重要的一块逻辑,当获取不到bean的时候,就会创建一个bean对象
//具体的我们在后续ioc源码分析的时候讲解
ApplicationListener<?> listener =
beanFactory.getBean(listenerBeanName, ApplicationListener.class);
if (!allListeners.contains(listener) && supportsEvent(listener, eventType, sourceType)) {
//也是判断是否有缓存的逻辑
if (retriever != null) {
//多一个判断是否单例的逻辑
if (beanFactory.isSingleton(listenerBeanName)) {
retriever.applicationListeners.add(listener);
}
else {
//原形bean这里,想起来以前有个组员说这个叫“多例”,最好还是叫“原型”
retriever.applicationListenerBeans.add(listenerBeanName);
}
}
allListeners.add(listener);
}
}
}
catch (NoSuchBeanDefinitionException ex) {

}
}
}
//进行排序,SpringBoot的常规操作了,根据Order接口或者注解进行排序
AnnotationAwareOrderComparator.sort(allListeners);
//对缓存进行一次刷新,把以前的结果清空,将这次运行的结果缓存
if (retriever != null && retriever.applicationListenerBeans.isEmpty()) {
retriever.applicationListeners.clear();
retriever.applicationListeners.addAll(allListeners);
}
//返回获取到的监听器
//返回 步骤12
return allListeners;
}

步骤14:判断监听器是否对当前事件感兴趣

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
复制代码protected boolean supportsEvent(
ApplicationListener<?> listener, ResolvableType eventType, @Nullable Class<?> sourceType) {
//判断监听器,是否是GenericApplicationListener的子类
//starting的事件并不是其子类
//GenericApplicationListener使用了装饰器模式
//著名的装饰器模式是java中io流(inputStream这些)
//GenericApplicationListener中可以解析ApplicationListener接口中的泛型参数,接口如下:
//“ApplicationListener<E extends ApplicationEvent>”要是还想不起来,回头看一下上面小Demo中的使用,和对这个接口的介绍
GenericApplicationListener smartListener = (listener instanceof GenericApplicationListener ?
(GenericApplicationListener) listener : new GenericApplicationListenerAdapter(listener));
//下面就变得简单了,虽然内部的判断很繁杂,总体只做了两件事情
//supportsEventType:判断监听器是否支持当前事件
//supportsSourceType:监听器是否对这个事件的发起来类感兴趣
//返回一个总的bool值,返回 步骤13
return (smartListener.supportsEventType(eventType) && smartListener.supportsSourceType(sourceType));
}

5、自定义SpringBoot监听器

(1)通过spring.factories注入

步骤1:创建监听器,并实现ApplicationListener接口

1
2
3
4
5
6
7
8
复制代码//我们让这个监听器对ApplicationStartedEvent事件感兴趣
@Order(1)
public class TestListener implements ApplicationListener<ApplicationStartedEvent>{
@Ovrride
public void onApplicationEvent(ApplicationStartedEvent event){
System.out.println("hello, Application start is over");
}
}

步骤2:在spring.factories中添加实现类的指引
这里涉及上一讲的内容,还不会的小伙伴们猛戳这里,赶紧补习一下:
https://juejin.cn/post/6844904106843193357

1
2
复制代码#com.gyx.test.Listener是刚刚写的监听器的全路径名
org.springframework.context.ApplicationListener=com.gyx.test.TestListener

然后运行程序,就可以发现打印的语句出现了

(2)SpringApplication手动注入

步骤1:创建监听器,并实现ApplicationListener接口,和上面的完全一样
步骤2:修改SpringBoot启动类

1
2
3
4
5
6
7
8
9
复制代码@SpringBootApplication
public class Application {
public static void main(String[] args) {
SpringApplication springApplication = new SpringApplication(Application.class);
//添加到初始化配置项中
springApplication.addListeners(new TestListener());
springApplication.run(args);
}
}

(3)SpringBoot的配置文件中注册

步骤1:创建监听器,并实现ApplicationListener接口,和上面的完全一样
步骤2:修改配置文件

1
复制代码context.listener.classes=com.gyx.test.TestListener

看过上一课的小伙伴们,是不是发现了,和之前ApplicationContextInitializer的注册方式完全一样!!!是不是有点感觉了,趁热打铁赶紧吧上一讲再去回顾一下吧

(4)多事件监听,实现SmartApplicationListener接口

这种方法只是实现的接口不一样,注入的方式是一样的,上面的三种注入方式都可以使用
步骤1:创建监听器,并实现SmartApplicationListener接口

1
2
3
4
5
6
7
8
9
10
11
12
13
14
复制代码@Order(1)
public class TestSmartListener implements SmartApplicationListener{
@Ovrride
public boolean supportsEventType(Class<? extends ApplicationEvent> eventType){
//这里是类型判断,判断监听器感兴趣的事件
//可以对多个事件感兴趣,这里就配置了两个事件
return ApplicationStartedEvent.class.isAssignableFrom(eventType)
|| ApplicationPreparedEvent.class.isAssignableFrom(eventType);
}
@Ovrride
public void onApplicationEvent(ApplicationStartedEvent event){
System.out.println("hello, This is smartApplicationListener");
}
}

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

争取能让大家都能看懂的 DFA 算法

发表于 2020-03-30

为什么要学习这个算法

我们公司一直都有的一个敏感词检测服务,前一段时间遇到了瓶颈,因为词库太多了导致会有一些速度过慢,而且一个正则表达式已经放不下了,需要进行拆分正则才可以。

正好我以前看过有关 dfa 的介绍,但是并没有深入的进行研究,所以就趁着周末好好的了解一下这个东西。跟 php 的正则进行一下对比,看看速度如何,如果表现较好,说不定还能用得上。

什么是 dfa

通过百度可以知道 dfa 是 确定有穷自动机 的缩写。
应该还会见到类似下面图的说明

原谅我实在一些,我这人数学不好不说,貌似看图能力也不行,这个图恕我直言我没看懂。所以关于精准的解释,请大家去百度或者 google 自行查阅了。
我的理解


说明之前,我们先看看做检测需要准备的东西

  • 一个组织好的关键词树
  • 待检测的字符串

什么是组织好的关键词树

我们一批需要检测词库,比如下面这些

日本人,日本鬼子,日本人傻,破解*版

先做个解释,前三个大家都能看懂,那么 * 是什么,这个是我定义的通配符,代表着 * 可以是 0 - n 个占位符用来替代在关键词中间插入混淆字符。至于可以替换几个我们可以在代码中进行定义,需要注意 n 越大,速度就会越慢。

说明完了,来看看构造好的树是什么一样的,应该是跟下图差不多的。

为什么要手动画一个,因为需要对比,我的理解跟程序是否一致,如果不一致,就要找出程序是不是写的不对了。那么我们来看看程序生成的是啥样的。

程序生成的跟图片一致,到这里还都是正确的。

待检测的字符串

这个就很容易理解了,就是我们需要检测的字符串。

为什么要组织好那样的一棵树(算法思路)

这块需要先说一个概念

它是是通过event和当前的state得到下一个state,即event+state=nextstate

这句话,或者类似的话你会在绝大多数的解释文章里面看到。而我的理解就是,一个字符一个字符的检测,如果检测的字符在我们的树种,就进入命中的树,看下一个字在不在树里面,如果持续的命中就持续进入,最后完全命中了,也就是那个字的子树只有一个元素,并且元素的键是 end (这里是在我们的这个例子中,看图就明白了)。就是完全命中了关键词,就可以记录命中,或者准备替换了。

这里说一个可以优化的点,看我们的例子有两个词 日本人,日本鬼子 这两个,如果为了快,完全可以去掉第二个词,质保流一个就行了,这样当检测到 end 就可以直接屏蔽或者记录了,而在我们的例子中,还需要判断元素数量,不是 1 的情况下还得继续深入,看看是不是命中了长尾。

这样的长尾检测会引发一个问题,那就是 回滚,当我们命中了前置的词,后续的没有命中的时候就得记录并且回滚,这个回滚的长度是是多少呢?其实不仅仅是没有命中长尾的回滚,还有一个 回滚 操作,就是检测率几个字之后就没命中率额,就得回顾,这个回滚的长度是,已检测字符长度 - 1 的长度 。那么没有命中长尾的长度我们就知道了,已检测字符长度 - 上次命中的长度 就可以了。

下面我们来看看代码实现。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
复制代码// 通配符的数量
$maskMin = 0;
$maskMax = 3;
// 关键词词典字符串,这个部分的处理自己可以替换
$dict = "傻瓜";
$checkDfaTree = [];
$dictArr = explode(',', $dict);
// 重组一下带有 * 通配符的数组
$fullDictArr = [];
foreach ($dictArr as $word) {
if (mb_strpos($word, '*') !== false) {
// 带有通配符就把通配符去掉
for ($maskIndex = $maskMin; $maskIndex <= $maskMax; $maskIndex++) {
$maskString = str_pad('', $maskIndex, '*');
$inputWord = str_replace('*', $maskString, $word);
$fullDictArr[] = $inputWord;
}
} else {
$fullDictArr[] = $word;
}
}

foreach ($fullDictArr as $word) {
// 每次开始新词都要回到树的根部
$treeStart = &$checkDfaTree;
$wordLen = mb_strlen($word);
for ($i = 0; $i < $wordLen; $i++) {
$char = mb_substr($word, $i, 1);
$treeStart[$char] = isset($treeStart[$char]) ? $treeStart[$char] : [];
if ($i + 1 == $wordLen) {
// 如果已经是次的结尾了就设置null
$treeStart[$char]['end'] = true;
}
// 移动指针到下一个
$treeStart = &$treeStart[$char];
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
复制代码// 遍历str
$start = microtime(true);
$checkMessageLen = mb_strlen($checkMessage);
$wordArr = [];
$checkTreeStart = &$checkDfaTree;
$hasPrefixLength = 0;
$targetWord = '';

for ($i = 0; $i < $checkMessageLen; $i++) {
// 获取一个字符
$char = mb_substr($checkMessage, $i, 1);

if (isset($checkTreeStart[$char])) {
// 如果有这个字就进入子树里面
if (isset($checkTreeStart[$char]['end']) && $checkTreeStart[$char]['end'] === true) {
// 如果包含这个标识,就记录标识
$hasPrefixLength = mb_strlen($targetWord);
}
$checkTreeStart = &$checkTreeStart[$char];
$targetWord .= $char;
} else if (isset($checkTreeStart['*'])) {
// 如果有通配符就进入子树
$checkTreeStart = &$checkTreeStart['*'];
$targetWord .= $char;
} else {
if ($hasPrefixLength) {
$wordArr[] = mb_substr($targetWord, 0, $hasPrefixLength + 1);
// 回滚
$i -= mb_strlen($targetWord) - $hasPrefixLength;
} else {
// 回滚
$i -= mb_strlen($targetWord);
}
// 回到头部
$checkTreeStart = &$checkDfaTree;
$targetWord = '';
$hasPrefixLength = 0;
}

if (count($checkTreeStart) == 1 && isset($checkTreeStart['end']) && $checkTreeStart['end'] === true) {
// 子树只有一个并且是end 就说明是命中了
// 赋值
$wordArr[] = $targetWord;
// 清空
$targetWord = '';
// 回到头部
$checkTreeStart = &$checkDfaTree;
$hasPrefixLength = 0;
}
}
var_dump($wordArr);
echo "<br /> useTime:" . (microtime(true) - $start) * 1000;

下面这个就是匹配加测试了,目前我能想到的都测试通过了,如果有问题,可以回复我。

结论

目前来看,效率是比正则要好一些,命中的情况下速度差不多,没命中的情况下表现要优于正则。

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

【译】kotlin 协程官方文档(9)-选择表达式(实验阶段

发表于 2020-03-30

公众号:字节数组

希望对你有所帮助 🤣🤣

最近一直在了解关于kotlin协程的知识,那最好的学习资料自然是官方提供的学习文档了,看了看后我就萌生了翻译官方文档的想法。前后花了要接近一个月时间,一共九篇文章,在这里也分享出来,希望对读者有所帮助。个人知识所限,有些翻译得不是太顺畅,也希望读者能提出意见

协程官方文档:coroutines-guide

协程官方文档中文翻译:coroutines-cn-guide

select 表达式可以同时等待多个挂起函数,并选择第一个可用的函数来执行

选择表达式是 kotlinx.coroutines 的一个实验性的特性,这些 API 预计将在 kotlinx.coroutines 库的即将到来的更新中衍化,并可能会有突破性的变化

一、Selecting from channels

我们现在有两个字符串生产者:fizz 和 buzz 。其中 fizz 每 300 毫秒生成一个字符串“Fizz”:

1
2
3
4
5
6
kotlin复制代码fun CoroutineScope.fizz() = produce<String> {
while (true) { // sends "Fizz" every 300 ms
delay(300)
send("Fizz")
}
}

接着 buzz 每 500 毫秒生成一个字符串“Buzz!”:

1
2
3
4
5
6
kotlin复制代码fun CoroutineScope.buzz() = produce<String> {
while (true) { // sends "Buzz!" every 500 ms
delay(500)
send("Buzz!")
}
}

使用挂起函数 receive,我们可以从两个通道接收其中一个的数据。但是 select 表达式允许我们使用其 onReceive 子句同时从两者接收:

1
2
3
4
5
6
7
8
9
10
kotlin复制代码suspend fun selectFizzBuzz(fizz: ReceiveChannel<String>, buzz: ReceiveChannel<String>) {
select<Unit> { // <Unit> means that this select expression does not produce any result
fizz.onReceive { value -> // this is the first select clause
println("fizz -> '$value'")
}
buzz.onReceive { value -> // this is the second select clause
println("buzz -> '$value'")
}
}
}

让我们运行代码 7 次:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
kotlin复制代码import kotlinx.coroutines.*
import kotlinx.coroutines.channels.*
import kotlinx.coroutines.selects.*

fun CoroutineScope.fizz() = produce<String> {
while (true) { // sends "Fizz" every 300 ms
delay(300)
send("Fizz")
}
}

fun CoroutineScope.buzz() = produce<String> {
while (true) { // sends "Buzz!" every 500 ms
delay(500)
send("Buzz!")
}
}

suspend fun selectFizzBuzz(fizz: ReceiveChannel<String>, buzz: ReceiveChannel<String>) {
select<Unit> { // <Unit> means that this select expression does not produce any result
fizz.onReceive { value -> // this is the first select clause
println("fizz -> '$value'")
}
buzz.onReceive { value -> // this is the second select clause
println("buzz -> '$value'")
}
}
}

fun main() = runBlocking<Unit> {
//sampleStart
val fizz = fizz()
val buzz = buzz()
repeat(7) {
selectFizzBuzz(fizz, buzz)
}
coroutineContext.cancelChildren() // cancel fizz & buzz coroutines
//sampleEnd
}

运行结果:

1
2
3
4
5
6
7
kotlin复制代码fizz -> 'Fizz'
buzz -> 'Buzz!'
fizz -> 'Fizz'
fizz -> 'Fizz'
buzz -> 'Buzz!'
fizz -> 'Fizz'
buzz -> 'Buzz!'

二、Selecting on close

当通道关闭时,select 中的 onReceive 子句会失败并导致相应的 select 引发异常。我们可以使用 onReceiveOrNull 子句在通道关闭时执行特定操作。下面的示例还显示了 select 是一个返回其查询方法结果的表达式:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
kotlin复制代码suspend fun selectAorB(a: ReceiveChannel<String>, b: ReceiveChannel<String>): String =
select<String> {
a.onReceiveOrNull { value ->
if (value == null)
"Channel 'a' is closed"
else
"a -> '$value'"
}
b.onReceiveOrNull { value ->
if (value == null)
"Channel 'b' is closed"
else
"b -> '$value'"
}
}

注意,onReceiveOrNull 是一个扩展函数,仅可用于具有不可为空元素的通道,这样就不会意外混淆通道是已关闭还是返回了空值这两种情况

让我们将其与生成四次“Hello”字符串的通道 a 和生成四次“World”字符串的通道 b 一起使用:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
kotlin复制代码import kotlinx.coroutines.*
import kotlinx.coroutines.channels.*
import kotlinx.coroutines.selects.*

suspend fun selectAorB(a: ReceiveChannel<String>, b: ReceiveChannel<String>): String =
select<String> {
a.onReceiveOrNull { value ->
if (value == null)
"Channel 'a' is closed"
else
"a -> '$value'"
}
b.onReceiveOrNull { value ->
if (value == null)
"Channel 'b' is closed"
else
"b -> '$value'"
}
}

fun main() = runBlocking<Unit> {
//sampleStart
val a = produce<String> {
repeat(4) { send("Hello $it") }
}
val b = produce<String> {
repeat(4) { send("World $it") }
}
repeat(8) { // print first eight results
println(selectAorB(a, b))
}
coroutineContext.cancelChildren()
//sampleEnd
}

这段代码的结果非常有趣,所以我们将在细节中分析它:

1
2
3
4
5
6
7
8
kotlin复制代码a -> 'Hello 0'
a -> 'Hello 1'
b -> 'World 0'
a -> 'Hello 2'
a -> 'Hello 3'
b -> 'World 1'
Channel 'a' is closed
Channel 'a' is closed

从中可以观察到几点

首先,select 偏向于第一个子句。当同时可以选择多个子句时,将选择其中的第一个子句。在这里,两个通道都在不断地产生字符串,因此作为 select 中的第一个子句的通道获胜。但是,因为我们使用的是无缓冲通道,所以 a 在其发送调用时会不时地被挂起,从而给了 b 发送的机会

第二个观察结果是,当通道已经关闭时,onReceiveOrNull 将立即被选中

三、Selecting to send

select 表达式有 onSend 子句,可以与 selection 的偏向性质结合使用。
让我们写一个整数生产者的例子,当主通道上的消费者跟不上时,它会将其值发送到 side 通道:

1
2
3
4
5
6
7
8
9
kotlin复制代码fun CoroutineScope.produceNumbers(side: SendChannel<Int>) = produce<Int> {
for (num in 1..10) { // produce 10 numbers from 1 to 10
delay(100) // every 100 ms
select<Unit> {
onSend(num) {} // Send to the primary channel
side.onSend(num) {} // or to the side channel
}
}
}

消费者将会非常缓慢,每个数值处理需要 250 毫秒:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
kotlin复制代码import kotlinx.coroutines.*
import kotlinx.coroutines.channels.*
import kotlinx.coroutines.selects.*

fun CoroutineScope.produceNumbers(side: SendChannel<Int>) = produce<Int> {
for (num in 1..10) { // produce 10 numbers from 1 to 10
delay(100) // every 100 ms
select<Unit> {
onSend(num) {} // Send to the primary channel
side.onSend(num) {} // or to the side channel
}
}
}

fun main() = runBlocking<Unit> {
//sampleStart
val side = Channel<Int>() // allocate side channel
launch { // this is a very fast consumer for the side channel
side.consumeEach { println("Side channel has $it") }
}
produceNumbers(side).consumeEach {
println("Consuming $it")
delay(250) // let us digest the consumed number properly, do not hurry
}
println("Done consuming")
coroutineContext.cancelChildren()
//sampleEnd
}

让我们看看会发生什么:

1
2
3
4
5
6
7
8
9
10
11
kotlin复制代码Consuming 1
Side channel has 2
Side channel has 3
Consuming 4
Side channel has 5
Side channel has 6
Consuming 7
Side channel has 8
Side channel has 9
Consuming 10
Done consuming

四、Selecting deferred values

延迟值可以使用 onAwait 子句来查询。让我们启动一个异步函数,它在随机的延迟后会延迟返回字符串:

1
2
3
4
kotlin复制代码fun CoroutineScope.asyncString(time: Int) = async {
delay(time.toLong())
"Waited for $time ms"
}

让我们随机启动十余个异步函数,每个都延迟随机的时间

1
2
3
4
kotlin复制代码fun CoroutineScope.asyncStringsList(): List<Deferred<String>> {
val random = Random(3)
return List(12) { asyncString(random.nextInt(1000)) }
}

现在,main 函数等待它们中的第一个完成,并统计仍处于活动状态的延迟值的数量。注意,我们在这里使用 select 表达式事实上是一种 Kotlin DSL,因此我们可以使用任意代码为它提供子句。在本例中,我们遍历一个延迟值列表,为每个延迟值提供 onAwait 子句。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
kotlin复制代码import kotlinx.coroutines.*
import kotlinx.coroutines.selects.*
import java.util.*

fun CoroutineScope.asyncString(time: Int) = async {
delay(time.toLong())
"Waited for $time ms"
}

fun CoroutineScope.asyncStringsList(): List<Deferred<String>> {
val random = Random(3)
return List(12) { asyncString(random.nextInt(1000)) }
}

fun main() = runBlocking<Unit> {
//sampleStart
val list = asyncStringsList()
val result = select<String> {
list.withIndex().forEach { (index, deferred) ->
deferred.onAwait { answer ->
"Deferred $index produced answer '$answer'"
}
}
}
println(result)
val countActive = list.count { it.isActive }
println("$countActive coroutines are still active")
//sampleEnd
}

输出结果:

1
2
kotlin复制代码Deferred 4 produced answer 'Waited for 128 ms'
11 coroutines are still active

五、Switch over a channel of deferred values

现在我们来编写一个通道生产者函数,它消费一个产生延迟字符串的通道,并等待每个接收的延迟值,但它只在下一个延迟值到达或者通道关闭之前处于运行状态。此示例将 onReceiveOrNull 和 onAwait 子句放在同一个 select 中:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
kotlin复制代码fun CoroutineScope.switchMapDeferreds(input: ReceiveChannel<Deferred<String>>) = produce<String> {
var current = input.receive() // start with first received deferred value
while (isActive) { // loop while not cancelled/closed
val next = select<Deferred<String>?> { // return next deferred value from this select or null
input.onReceiveOrNull { update ->
update // replaces next value to wait
}
current.onAwait { value ->
send(value) // send value that current deferred has produced
input.receiveOrNull() // and use the next deferred from the input channel
}
}
if (next == null) {
println("Channel was closed")
break // out of loop
} else {
current = next
}
}
}

为了测试它,我们将用一个简单的异步函数,它在特定的延迟后返回特定的字符串:

1
2
3
4
kotlin复制代码fun CoroutineScope.asyncString(str: String, time: Long) = async {
delay(time)
str
}

main 函数只是启动一个协程来打印 switchMapDeferreds 的结果并向它发送一些测试数据:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
kotlin复制代码import kotlinx.coroutines.*
import kotlinx.coroutines.channels.*
import kotlinx.coroutines.selects.*

fun CoroutineScope.switchMapDeferreds(input: ReceiveChannel<Deferred<String>>) = produce<String> {
var current = input.receive() // start with first received deferred value
while (isActive) { // loop while not cancelled/closed
val next = select<Deferred<String>?> { // return next deferred value from this select or null
input.onReceiveOrNull { update ->
update // replaces next value to wait
}
current.onAwait { value ->
send(value) // send value that current deferred has produced
input.receiveOrNull() // and use the next deferred from the input channel
}
}
if (next == null) {
println("Channel was closed")
break // out of loop
} else {
current = next
}
}
}

fun CoroutineScope.asyncString(str: String, time: Long) = async {
delay(time)
str
}

fun main() = runBlocking<Unit> {
//sampleStart
val chan = Channel<Deferred<String>>() // the channel for test
launch { // launch printing coroutine
for (s in switchMapDeferreds(chan))
println(s) // print each received string
}
chan.send(asyncString("BEGIN", 100))
delay(200) // enough time for "BEGIN" to be produced
chan.send(asyncString("Slow", 500))
delay(100) // not enough time to produce slow
chan.send(asyncString("Replace", 100))
delay(500) // give it time before the last one
chan.send(asyncString("END", 500))
delay(1000) // give it time to process
chan.close() // close the channel ...
delay(500) // and wait some time to let it finish
//sampleEnd
}

代码的执行结果:

1
2
3
4
kotlin复制代码BEGIN
Replace
END
Channel was closed

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

面试官多次问我TCP粘包,而我为何屡屡受挫?

发表于 2020-03-29

无论走到哪里,都应该记住,过去都是假的,回忆是一条没有尽头的路,一切以往的春天都不复存在,就连那最坚韧而又狂乱的爱情归根结底也不过是一种转瞬即逝的现实。 ——马尔克斯

本文已经收录至我的GitHub,欢迎大家踊跃star 和 issues。

github.com/midou-tech/…

点关注,不迷路 ❤️❤️❤️

逛论坛看到一个帖子,标题说自己在学习网络模型,经常有人提到TCP粘包问题,他笑了。这个帖子讨论人数还挺多的。既然看到,顺便解释下这个问题。 TCP问题也算是计算机网络中比较重要的一个知识点,面试当然是必不可少的、工作中也经常遇到与之相关的问题。龙叔不光讲网络网面的知识点,其他后端知识点也是会经常给大家唠叨一番的。关注我,精彩内容不错过💕 微信搜索 龙跃十二 即可无忧订阅。

网络模型

网络模型

计算机网络分层模型主要有OSI七层模型,TCP/IP五层模型,也有一种四层模型,四层模型会把网卡层和物理层统称为网络接口层。

OSI七层模型存在于教科书了,TCP/IP五层模型是日常运用最为广泛的一种网络架构模型。在学习网络知识时也要把握住重点去学,七层模型了解即可。

由上面的分层可以看出,TCP是存在于运输层的概念。但是TCP有两种含义的,一种指的是TCP协议,一种是TCP协议族的统称。具体来说,IP或ICMP、TCP或UDP、TELNET或FTP、以及HTTP等都属于TCP/IP的协议。

TCP层是干嘛的?

TCP是属于传输层的协议,我说的TCP层指的是传输层,这点要统一。

**传输层最主要的功能就是能够让应用程序之间实现通信。**一句话就说清楚了TCP层是干嘛的。

TCP层的数据是如何存在的?

TCP的定义,TCP是面向连接的、可靠的流式传输协议。概念往往是高度浓缩的经典货,就比如这句,涵盖了TCP传输建立方式,传输方式,特点。面向连接指的是两个应用程序的传输是需要提前建立一个链接,这个链接就是我们的VIP通道,保证两个应用程序之间的通行是点对点的传输。建立链接的过程就是面试常考的三次握手过程。

三次握手和四次挥手都会在后续文章重点剖析出来,想了解的记住关注我,微信搜索 龙跃十二 即可无忧订阅。

流式传输说的数据传输方式,TCP层数据交互是流式的,什么是流式?流你可以理解为水流,水流是没有边界的。

可靠指的是TCP传输数据的特点,可靠的意思就是你发送的数据一定最大程度保证让对方应用程序接收到。TCP为提供可靠性传输,实行“顺序控制”或“重发控制”机制。此外还具备“流控制(流量控制)”、“拥塞控制”、提高网络利用率等众多功能。具体如何通过这些机制保证消息的可靠性。后续也会出相应的文章,不用提醒 赶紧关注我。

从定义我们很清楚的知道TCP的数据是字节流的方式存在的。TCP发送数据单位准确叫法是数据段。应用程序和TCP的交互是一次一个数据段(大小不等),TCP把应用程序交下来的数据仅仅看成是一连串的无结构的字节流。TCP并不知道所传送的字节流的含义。

TCP不保证接收方应用程序所收到的数据段和发送方应用程序所发出的数据段具有对应大小的关系(例如,发送方应用程序交给发送方的TCP共10个数据段,但接收方的TCP可能只用了4个数据段就把收到的字节流交付上层的应用程序)。接收方应用程序收到的字节流必须和发送方应用程序发出的字节流完全一样。

包的概念是在那一层谈到的?

  • 数据帧(Frame):是一种信息单位,它的起始点和目的点都是数据链路层。
  • 数据包(Packet):也是一种信息单位,它的起始和目的地是网络层。
  • 数据报(Datagram):通常是指起始点和目的地都使用无连接网络服务的的网络层的信息单元。
  • 段(Segment):通常是指起始点和目的地都是传输层的信息单元。
  • 消息(message):是指起始点和目的地都在网络层以上(经常在应用层)的信息单元。
  • 元素(cell)是一种固定长度的信息,它的起始点和目的地都是数据链路层。

元素通常用于异步传输模式(ATM)和交换多兆位数据服务(SMDS)网络等交换环境。

数据单元(data unit)指许多信息单元。常用的数据单元有服务数据单元(SDU)、协议数据单元(PDU)。

SDU是在同一机器上的两层之间传送信息。PDU是发送机器上每层的信息发送到接收机器上的相应层(同等层间交流用的)。

Packet(数据包):封装的基本单元,它穿越网络层和数据链路层的分解面。通常一个Packet映射成一个Frame,但也有例外:即当数据链路层执行拆分或将几个Packet合成一个Frame的时候。

数据链路层的PDU叫做Frame(帧),
网络层的PDU叫做Packet(数据包),
TCP的叫做Segment(数据段), UDP的叫做Datagram。

一个Datagram可能被封装成一个或几个Packets,在数据链路层中传输帧和数据包都是数据的传输形式。帧,工作在二层,数据链路层传输的是数据帧,包含数据包,并且增加相应MAC地址与二层信息;数据包,工作在三层,网络层传输的是数据包,包含数据报文,并且增加传输使用的IP地址等三层信息。

为什么面试官和大家还是会谈论TCP粘包问题呢?

从上面很容易的出,第一、TCP层传输是流式传输,不会发送数据包。第二、数据包是存在于网络层的概念。那为啥还说TCP粘包问题呢?

自顶而下学习网络的同学都知道应用程序首先要将自己的数据通过套接字发送。应用层交付给TCP的是结构化的数据,结构化的数据到了TCP层做流式传输。

流,最大的问题是没有边界,没有边界就会造成数据粘在一起,这种粘在一起就叫做粘包。当然有同学就要问了,那咋不叫粘段呢?这个。。。

具体描述下什么叫粘包。

TCP粘包是指发送方发送的若干包数据到接收方接收时粘成一包,从接收缓冲区看,后一包数据的头紧接着前一包数据的尾。

粘包发生在那些情况下?

TCP是端到端传输的,同时TCP连接是可复用的。什么叫复用呢?复用就是一条连接可以供一台主机上的多个进程使用。

**1.**由TCP连接复用造成的粘包问题。

如果没有复用一个连接只提供给端到端的两个进程使用,这是数据的传输方和发送方都是约定好了数据的格式的,但是多个进程使用一个TCP连接,此时多种不同结构的数据进到TCP的流式传输,边界分割肯定会出这样或者那样的问题。

如果利用tcp每次发送数据,就与对方建立连接,然后双方发送完一段数据后,就关闭连接,这样就不会出现粘包问题

连接复用的问题,之后会出一系列TCP文章讲解。所以赶紧关注我😆,防止走丢喔,微信搜索 龙跃十二,即可无忧订阅。

**2.**因为TCP默认会使用Nagle算法,此算法会导致粘包问题。

而Nagle算法主要做两件事,1)只有上一个分组得到确认,才会发送下一个分组;2)收集多个小分组,在一个确认到来时一起发送。

多个分组拼装为一个数据段发送出去,如果没有好的边界处理,在解包的时候会发生粘包问题。

**3.**数据包过大造成的粘包问题。

比如应用进程缓冲区的一条消息的字节的大小超过了发送缓冲区的大小,就有可能产生粘包问题。因为消息已经被分割了,有可能一部分已经被发送出去了,对方已经接受了,但是另外一部分可能刚放入套接口发送缓冲区里准备进一步发送,就直接导致接受的后一部分,直接导致了粘包问题的出现。

**4.**流量控制,拥塞控制也可能导致粘包。

**5.**接收方不及时接收缓冲区的包,造成多个包接收。

大多数人都是知道Nagle算法、接收方不及时处理两种情况造成的粘包问题,但是龙叔必须提醒你,其他几种情况也是非常常见的,面试官也是超爱问,如果你能把其他三种也答出来,面试通过概率大很多。

粘包问题如何处理?

1.Nagle算法问题导致的,需要结合应用场景适当关闭该算法。

2.其他几种情况的处理方法主要分两种:

  • 尾部标记序列。通过特殊标识符表示数据包的边界,例如\n\r,\t,或者一些隐藏字符。
  • 头部标记分步接收。在TCP报文的头部加上表示数据长度。
  • 应用层发送数据时定长发送。

本文涉及到很多计算机网络的重点知识并没有说清楚,但本文意在让大家明白TCP粘包问题,其他问题龙叔后期会陆续更新。关注我,精彩内容不错过。

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

0xA04 Android 10 源码分析:Apk加载流程之

发表于 2020-03-28

引言

  • 这是 Android 10 源码分析系列的第 4 篇
  • 分支:android-10.0.0_r14
  • 全文阅读大概 10 分钟

通过这篇文章你将学习到以下内容,将在文末会给出相应的答案

  • View中的INVISIBLE、VISIBLE、GONE都有什么作用?
  • 为什么ViewStub是大小为0的视图
  • ViewStub有什么作用?
  • ViewStub是如何创建的?
  • 为什么ViewStub能做到延迟加载?
  • ViewStub指定的Layout布局文件是什么时候被加载的?
  • LayoutInflater是一个抽象类它如何被创建的?
  • 系统服务存储在哪里?如何获取和添加系统服务?

在上一篇文章 0xA02 Android 10 源码分析:Apk加载流程之资源加载 中通过LayoutInflater.inflate方法解析xml文件,了解到了系统如何对merge、include标签是如何处理的,本文主要围绕以下两方面内容

  • 系统对ViewStub如何处理?
  • LayoutInflater是如何被创建的?

系统对merge、include是如何处理的

  • 使用merge标签必须有父布局,且依赖于父布局加载
  • merge并不是一个ViewGroup,也不是一个View,它相当于声明了一些视图,等待被添加,解析过程中遇到merge标签会将merge标签下面的所有子view添加到根布局中
  • merge标签在 XML 中必须是根元素
  • 相反的include不能作为根元素,需要放在一个ViewGroup中
  • 使用 include 标签必须指定有效的 layout 属性
  • 使用 include 标签不写宽高是没有关系的,会去解析被 include 的 layout

merge标签为什么可以起到优化布局的效果?

解析过程中遇到merge标签,会调用rInflate方法,部分代码如下

1
2
3
4
5
6
7
8
ini复制代码// 根据元素名解析,生成对应的view
final View view = createViewFromTag(parent, name, context, attrs);
final ViewGroup viewGroup = (ViewGroup) parent;
final ViewGroup.LayoutParams params = viewGroup.generateLayoutParams(attrs);
// rInflateChildren方法内部调用的rInflate方法,深度优先遍历解析所有的子view
rInflateChildren(parser, view, attrs, true);
// 添加解析的view
viewGroup.addView(view, params);

解析merge标签下面的所有子view,然后添加到根布局中

更多信息查看0xA02 Android 10 源码分析:Apk加载流程之资源加载,接下来看一下系统对ViewStub如何处理

  1. ViewStub是什么

关于ViewStub的介绍,可以点击下方官网链接查看
官网链接https://developer.android.google.cn/reference/android…ViewStub

ViewStub的继承结构

简单来说主要以下几点:

  • ViewStub控件是一个不可见,
  • 大小为0的视图
  • 当ViewStub控件设置可见,或者调用inflate()方法,ViewStub所指定的layout资源就会被加载
  • ViewStub也会从其父控件中移除,ViewStub会被新加载的layout文件代替

为什么ViewStub是大小为0的视图
frameworks/base/core/java/android/view/ViewStub.java

1
2
3
4
arduino复制代码protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
// 设置视图大小为0
setMeasuredDimension(0, 0);
}

ViewStub的作用

主要用来延迟布局的加载,例如:在Android中非常常见的布局,用ListView来展示列表信息,当没有数据或者网络加载失败时, 加载空的 ListView 会占用一些资源,如果用ViewStub包裹ListView,当有数据时,才会调用inflate()方法显示ListView,起到延迟加载了布局效果

1.1 ViewStub 是如何被创建的

在上篇文章0xA02 Android 10 源码分析:Apk加载流程之资源加载中,介绍了view的创建是通过调用了LayoutInflater.createView方法根据完整的类的路径名利用反射机制构建View对象,因为ViewStub是继承View,所以ViewStub的创建和View的创建是相同的,来看一下LayoutInflater.createView方法
frameworks/base/core/java/android/view/LayoutInflater.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
ini复制代码...

try {
// 利用构造函数,创建View
final View view = constructor.newInstance(args);
if (view instanceof ViewStub) {
// 如果是ViewStub,则设置LayoutInflater
final ViewStub viewStub = (ViewStub) view;
viewStub.setLayoutInflater(cloneInContext((Context) args[0]));
}
return view;
} finally {
mConstructorArgs[0] = lastContext;
}

...

根据完整的类的路径名利用反射机制构建View对象,如果遇到ViewStub将当前LayoutInflater设置给ViewStub,当ViewStub控件设置可见,或者调用inflate(),会调用LayoutInflater的inflate方法完成布局加载,接下来分析ViewStub的构造方法

1.2 ViewStub的构造方法

在上面提到了根据完整的类的路径名利用反射机制构建View对象,当view对象被创建的时候,会调用它的构造函数,来看一下ViewStub的构造方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
scss复制代码public ViewStub(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
super(context);

final TypedArray a = context.obtainStyledAttributes(attrs,
R.styleable.ViewStub, defStyleAttr, defStyleRes);
saveAttributeDataForStyleable(context, R.styleable.ViewStub, attrs, a, defStyleAttr,
defStyleRes);
// 解析xml中设置的 android:inflatedId 的属性
mInflatedId = a.getResourceId(R.styleable.ViewStub_inflatedId, NO_ID);
// 解析xml中设置的 android:layout 属性
mLayoutResource = a.getResourceId(R.styleable.ViewStub_layout, 0);
// 解析xml中设置的 android:id 属性
mID = a.getResourceId(R.styleable.ViewStub_id, NO_ID);
a.recycle();
// view不可见
setVisibility(GONE);
// 不会调用 onDraw 方法绘制内容
setWillNotDraw(true);
}
  1. 获取android:inflatedId、android:layout、android:id的值
  2. 调用setVisibility方法,设置View不可见
  3. 调用setWillNotDraw方法,不会调用 onDraw 方法绘制内容

在上面提到了如果想要加载ViewStub所指定的layout资源,需要设置ViewStub控件设置可见,或者调用inflate()方法,来看一下ViewStub的setVisibility方法

1.3 ViewStub的setVisibility方法

setVisibility(int visibility)方法,参数visibility对应三个值分别是INVISIBLE、VISIBLE、GONE

  • VISIBLE:视图可见
  • INVISIBLE:视图不可见的,它仍然占用布局的空间
  • GONE:视图不可见,它不占用布局的空间

接下里查看一下ViewStub的setVisibility方法
frameworks/base/core/java/android/view/ViewStub.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
java复制代码@Override
public void setVisibility(int visibility) {
if (mInflatedViewRef != null) {
// mInflatedViewRef 是 WeakReference的实例,调用inflate方法时候初始化
View view = mInflatedViewRef.get();
if (view != null) {
// 设置View可见
view.setVisibility(visibility);
} else {
throw new IllegalStateException("setVisibility called on un-referenced view");
}
} else {
super.setVisibility(visibility);
// 当View为空且设置视图可见(VISIBLE、INVISIBLE),调用inflate方法
if (visibility == VISIBLE || visibility == INVISIBLE) {
inflate();
}
}
}
  1. mInflatedViewRef 是 WeakReference的实例,调用inflate方法时候初始化
  2. 从 mInflatedViewRef 缓存中获取View,并且设置View可见
  3. 当View为空且设置视图可见(VISIBLE、INVISIBLE),会调用inflate方法

1.4 ViewStub.inflate方法

调用了ViewStub的setVisibility方法,最后都会调用ViewStub.inflate方法,来查看一下
frameworks/base/core/java/android/view/ViewStub.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
java复制代码public View inflate() {
final ViewParent viewParent = getParent();

if (viewParent != null && viewParent instanceof ViewGroup) {
if (mLayoutResource != 0) {
final ViewGroup parent = (ViewGroup) viewParent;
// 解析布局视图
// 返回的view是android:layout指定的布局文件最顶层的view
final View view = inflateViewNoAdd(parent);

// 移除ViewStub
// 添加view到被移除的ViewStub的位置
replaceSelfWithView(view, parent);

// 添加view到 mInflatedViewRef 中
mInflatedViewRef = new WeakReference<>(view);
if (mInflateListener != null) {
// 加载完成之后,回调onInflate 方法
mInflateListener.onInflate(this, view);
}

return view;
} else {
// 需要在xml中设置android:layout,不是layout,否则会抛出异常
throw new IllegalArgumentException("ViewStub must have a valid layoutResource");
}
} else {
// ViewStub不能作为根布局,它需要放在ViewGroup中, 否则会抛出异常
throw new IllegalStateException("ViewStub must have a non-null ViewGroup viewParent");
}
}
  1. 调用inflateViewNoAdd方法返回android:layout指定的布局文件最顶层的view
  2. 调用replaceSelfWithView方法, 移除ViewStub, 添加view到被移除的ViewStub的位置
  3. 添加view到 mInflatedViewRef 中
  4. 加载完成之后,回调onInflate 方法

需要注意以下两点:

  • 使用ViewStub需要在xml中设置android:layout,不是layout,否则会抛出异常
  • ViewStub不能作为根布局,它需要放在ViewGroup中, 否则会抛出异常

来查看一下inflateViewNoAdd方法和replaceSelfWithView方法

1.5 ViewStub.inflateViewNoAdd方法

调用inflateViewNoAdd方法返回android:layout指定的布局文件最顶层的view
frameworks/base/core/java/android/view/ViewStub.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
dart复制代码private View inflateViewNoAdd(ViewGroup parent) {
final LayoutInflater factory;

// mInflater 是View被创建的时候,如果是ViewStub, 将LayoutInflater赋值给mInflater
if (mInflater != null) {
factory = mInflater;
} else {
// 如果mInflater为空,则创建LayoutInflater
factory = LayoutInflater.from(mContext);
}

// 从指定的 mLayoutResource 资源中解析布局视图
// mLayoutResource 是在xml设置的 Android:layout 指定的布局文件
final View view = factory.inflate(mLayoutResource, parent, false);

// mInflatedId 是在xml设置的 inflateId
if (mInflatedId != NO_ID) {
// 将id复制给view
view.setId(mInflatedId);
//注意:如果指定了mInflatedId , 被inflate的layoutView的id就是mInflatedId
}
return view;
}
  • mInflater是View被创建的时候,如果是ViewStub, 将LayoutInflater赋值给mInflater
  • 如果mInflater为空则通过LayoutInflater.from(mContext)构建LayoutInflater
  • 调用LayoutInflater的inflate方法解析布局视图
  • 将mInflatedId设置View

1.6 ViewStub.replaceSelfWithView方法

调用replaceSelfWithView方法, 移除ViewStub, 添加view到被移除的ViewStub的位置
frameworks/base/core/java/android/view/ViewStub.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
java复制代码private void replaceSelfWithView(View view, ViewGroup parent) {
// 获取ViewStub在视图中的位置
final int index = parent.indexOfChild(this);
// 移除ViewStub
// 注意:调用removeViewInLayout方法之后,调用findViewById()是找不到该ViewStub对象
parent.removeViewInLayout(this);

final ViewGroup.LayoutParams layoutParams = getLayoutParams();
// 将xml中指定的 android:layout 布局文件中最顶层的View,添加到被移除的 ViewStub的位置
if (layoutParams != null) {
parent.addView(view, index, layoutParams);
} else {
parent.addView(view, index);
}
}
  1. 获取ViewStub在视图中的位置,然后移除ViewStub
  2. 添加android:layout 布局文件中最顶层的View到被移除的ViewStub的位置

1.7 ViewStub的注意事项

  • 使用ViewStub需要在xml中设置android:layout,不是layout,否则会抛出异常
1
arduino复制代码throw new IllegalArgumentException("ViewStub must have a valid layoutResource");
  • ViewStub不能作为根布局,它需要放在ViewGroup中, 否则会抛出异常
1
arduino复制代码throw new IllegalStateException("ViewStub must have a non-null ViewGroup viewParent");
  • 一旦调用setVisibility(View.VISIBLE)或者inflate()方法之后,该ViewStub将会从试图中被移除(此时调用findViewById()是找不到该ViewStub对象).
1
2
3
4
5
kotlin复制代码// 获取ViewStub在视图中的位置
final int index = parent.indexOfChild(this);
// 移除ViewStub
// 注意:调用removeViewInLayout方法之后,调用findViewById()是找不到该ViewStub对象
parent.removeViewInLayout(this);
  • 如果指定了mInflatedId , 被inflate的layoutView的id就是mInflatedId
1
2
3
4
5
6
scss复制代码// mInflatedId 是在xml设置的 inflateId
if (mInflatedId != NO_ID) {
// 将id复制给view
view.setId(mInflatedId);
//注意:如果指定了mInflatedId , 被inflate的layoutView的id就是mInflatedId
}
  • 被inflate的layoutView的layoutParams与ViewStub的layoutParams相同.
1
2
3
4
5
6
7
8
scss复制代码final ViewGroup.LayoutParams layoutParams = getLayoutParams();
// 将xml中指定的 android:layout 布局文件中最顶层的View 也就是根view,
// 添加到被移除的 ViewStub的位置
if (layoutParams != null) {
parent.addView(view, index, layoutParams);
} else {
parent.addView(view, index);
}

到这里关于ViewStub的构建、布局的加载以及注意事项分析完了,接下来分析一下 LayoutInflater是如何被创建的

2 关于LayoutInflater

在0xA02 Android 10 源码分析:Apk加载流程之资源加载文章中,介绍了Activity启动的时候通过调用LayoutInflater的inflater的方法加载layout文件,那么LayoutInflater是如何被创建的呢,先来看一段代码,相信下面的代码都不会很陌生

1
2
3
4
5
less复制代码public RecyclerView.ViewHolder onCreateViewHolder(@NonNull ViewGroup viewGroup, int i) {
return new AllVh(LayoutInflater
.from(viewGroup.getContext())
.inflate(R.layout.list_item, viewGroup, false));
}

LayoutInflater的inflate方法的三个参数都代表什么意思?

  • resource:要解析的xml布局文件Id
  • root:表示根布局
  • attachToRoot:是否要添加到父布局root中

resource其实很好理解就是资源Id,而root 和 attachToRoot 分别代表什么意思:

  • 当attachToRoot == true且root != null时,新解析出来的View会被add到root中去,然后将root作为结果返回
  • 当attachToRoot == false且root != null时,新解析的View会直接作为结果返回,而且root会为新解析的View生成LayoutParams并设置到该View中去
  • 当attachToRoot == false且root == null时,新解析的View会直接作为结果返回

2.1 LayoutInflater是如何被创建的

LayoutInflater是一个抽象类,通过调用了from()的静态函数,经由系统服务LAYOUT_INFLATER_SERVICE,最终创建了一个LayoutInflater的子类对象PhoneLayoutInflater,继承结构如下:

LayoutInflater.from(ctx) 就是根据传递过来的Context对象,调用getSystemService()来获取对应的系统服务, 来看一下这个方法
frameworks/base/core/java/android/view/LayoutInflater.java

1
2
3
4
5
6
7
8
9
10
java复制代码public static LayoutInflater from(Context context) {
// 获取系统服务 LAYOUT_INFLATER_SERVICE ,并赋值给 LayoutInflater
// Context 是一个抽象类,真正的实现类是ContextImpl
LayoutInflater LayoutInflater =
(LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
if (LayoutInflater == null) {
throw new AssertionError("LayoutInflater not found.");
}
return LayoutInflater;
}

而Context本身是一个抽象类,它真正的实例化对象是ContextImpl
frameworks/base/core/java/android/app/ContextImpl.java

1
2
3
4
5
typescript复制代码public Object getSystemService(String name) {
// SystemServiceRegistry 是管理系统服务的
// 调用getSystemService方法,通过服务名字查找对应的服务
return SystemServiceRegistry.getSystemService(this, name);
}

2.2 SystemServiceRegistry

SystemServiceRegistry管理所有的系统服务,调用getSystemService方法,通过服务名字查找对应的服务
frameworks/base/core/java/android/app/SystemServiceRegistry.java

1
2
3
4
5
6
typescript复制代码public static Object getSystemService(ContextImpl ctx, String name) {
// SYSTEM_SERVICE_FETCHERS 是一个map集合
// 从 map 集合中取出系统服务
ServiceFetcher<?> fetcher = SYSTEM_SERVICE_FETCHERS.get(name);
return fetcher != null ? fetcher.getService(ctx) : null;
}
  • ServiceFetcher为SystemServiceRegistry类的静态内部接口,定义了getService方法
  • ServiceFetcher的实现类CachedServiceFetcher实现了getService方法
  • 所有的系统服务都存储在一个map集合SYSTEM_SERVICE_FETCHERS当中,调用get方法来获取对应的服务

如果有getSystemService方法来获取服务,那么相应的也会有添加服务的方法
frameworks/base/core/java/android/app/SystemServiceRegistry.java

1
2
3
4
5
typescript复制代码private static <T> void registerService(String serviceName, Class<T> serviceClass,
ServiceFetcher<T> serviceFetcher) {
SYSTEM_SERVICE_NAMES.put(serviceClass, serviceName);
SYSTEM_SERVICE_FETCHERS.put(serviceName, serviceFetcher);
}

通过调用SYSTEM_SERVICE_NAMES的put方法,往map集合中添加数据,那么registerService是什么时候调用的,在SystemServiceRegistry类中搜索registerService方法,知道了在类加载的时候通过静态代码块中添加的,来看一下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
typescript复制代码static {
// 初始化加载所有的系统服务

...
// 省略了很多系统服务

registerService(Context.LAYOUT_INFLATER_SERVICE, LayoutInflater.class,
new CachedServiceFetcher<LayoutInflater>() {
@Override
public LayoutInflater createService(ContextImpl ctx) {
return new PhoneLayoutInflater(ctx.getOuterContext());
}});

...
// 省略了很多系统服务
}

最终是创建了一个PhoneLayoutInflater并返回的,到这里LayoutInflater的创建流程就分析完了

总结

View中的INVISIBLE、VISIBLE、GONE都有什么作用?

如果想隐藏或者显示View,可以通过调用setVisibility(int visibility)方法来实现,参数visibility对应三个值分别是INVISIBLE、VISIBLE、GONE

  • VISIBLE:视图可见
  • INVISIBLE:视图不可见的,它仍然占用布局的空间
  • GONE:视图不可见,它不占用布局的空间

为什么ViewStub是大小为0的视图?
frameworks/base/core/java/android/view/ViewStub.java

1
2
3
4
arduino复制代码protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
// 设置视图大小为0
setMeasuredDimension(0, 0);
}

ViewStub有什么作用?

ViewStub的作用主要用来延迟布局的加载

ViewStub是如何创建的?

因为ViewStub是继承View, 所以ViewStub的创建和View的创建是相同的,通过调用了LayoutInflater.createView方法根据完整的类的路径名利用反射机制构建View对象
frameworks/base/core/java/android/view/LayoutInflater.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
ini复制代码...

try {
// 利用构造函数,创建View
final View view = constructor.newInstance(args);
if (view instanceof ViewStub) {
// 如果是ViewStub,则设置LayoutInflater
final ViewStub viewStub = (ViewStub) view;
viewStub.setLayoutInflater(cloneInContext((Context) args[0]));
}
return view;
} finally {
mConstructorArgs[0] = lastContext;
}

...

为什么ViewStub能做到延迟加载?

因为在解析layout文件过程中遇到ViewStub,只是构建ViewStub的对象和初始化ViewStub的属性,没有真正开始解析view,所以可以做到延迟初始化

ViewStub指定的Layout布局文件是什么时候被加载的?

当ViewStub控件设置可见,或者调用inflate()方法,ViewStub所指定的layout资源就会被加载

LayoutInflater是一个抽象类它如何被创建的?

LayoutInflater是一个抽象类,通过调用了from()的静态函数,经由系统服务LAYOUT_INFLATER_SERVICE,最终创建了一个LayoutInflater的子类对象PhoneLayoutInflater,继承结构如下:

LayoutInflater.from(ctx) 就是根据传递过来的Context对象,调用getSystemService()来获取对应的系统服务

系统服务存储在哪里?如何获取和添加系统服务?

SystemServiceRegistry管理所有的系统服务,所有的系统服务都存储在一个map集合SYSTEM_SERVICE_FETCHERS当中,调用getSystemService方法获取系统服务,调用registerService方法添加系统服务

结语

致力于分享一系列 Android 系统源码、逆向分析、算法、翻译、Jetpack 源码相关的文章,正在努力写出更好的文章,如果这篇文章对你有帮助给个 star,文章中有什么没有写明白的地方,或者有什么更好的建议欢迎留言,欢迎一起来学习,在技术的道路上一起前进。

计划建立一个最全、最新的 AndroidX Jetpack 相关组件的实战项目 以及 相关组件原理分析文章,正在逐渐增加 Jetpack 新成员,仓库持续更新,可以前去查看:AndroidX-Jetpack-Practice, 如果这个仓库对你有帮助,请帮我点个赞,我会陆续完成更多 Jetpack 新成员的项目实践。

算法

由于 LeetCode 的题库庞大,每个分类都能筛选出数百道题,由于每个人的精力有限,不可能刷完所有题目,因此我按照经典类型题目去分类、和题目的难易程度去排序。

  • 数据结构: 数组、栈、队列、字符串、链表、树……
  • 算法: 查找算法、搜索算法、位运算、排序、数学、……

每道题目都会用 Java 和 kotlin 去实现,并且每道题目都有解题思路、时间复杂度和空间复杂度,如果你同我一样喜欢算法、LeetCode,可以关注我 GitHub 上的 LeetCode 题解:Leetcode-Solutions-with-Java-And-Kotlin,一起来学习,期待与你一起成长。

Android 10 源码系列

正在写一系列的 Android 10 源码分析的文章,了解系统源码,不仅有助于分析问题,在面试过程中,对我们也是非常有帮助的,如果你同我一样喜欢研究 Android 源码,可以关注我 GitHub 上的 Android10-Source-Analysis,文章都会同步到这个仓库。

  • 0xA01 Android 10 源码分析:APK 是如何生成的
  • 0xA02 Android 10 源码分析:APK 的安装流程
  • 0xA03 Android 10 源码分析:APK 加载流程之资源加载
  • 0xA04 Android 10 源码分析:APK 加载流程之资源加载(二)
  • 0xA05 Android 10 源码分析:Dialog 加载绘制流程以及在 Kotlin、DataBinding 中的使用
  • 更多……

工具系列

  • 为数不多的人知道的 AndroidStudio 快捷键(一)
  • 为数不多的人知道的 AndroidStudio 快捷键(二)
  • 关于 adb 命令你所需要知道的
  • 如何高效获取视频截图
  • 10分钟入门 Shell 脚本编程
  • 如何在项目中封装 Kotlin + Android Databinding

逆向系列

  • 基于 Smali 文件 Android Studio 动态调试 APP
  • 解决在 Android Studio 3.2 找不到 Android Device Monitor 工具

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

java中FutureTask的使用

发表于 2020-03-28

java中FutureTask的使用

FutureTask简介

FutureTask是java 5引入的一个类,从名字可以看出来FutureTask既是一个Future,又是一个Task。

我们看下FutureTask的定义:

1
2
3
复制代码public class FutureTask<V> implements RunnableFuture<V> {
...
}
1
2
3
4
5
6
7
复制代码public interface RunnableFuture<V> extends Runnable, Future<V> {
/**
* Sets this Future to the result of its computation
* unless it has been cancelled.
*/
void run();
}

FutureTask实现了RunnableFuture接口,RunnableFuture接口是Runnable和Future的综合体。

作为一个Future,FutureTask可以执行异步计算,可以查看异步程序是否执行完毕,并且可以开始和取消程序,并取得程序最终的执行结果。

除此之外,FutureTask还提供了一个runAndReset()的方法, 该方法可以运行task并且重置Future的状态。

Callable和Runnable的转换

我们知道Callable是有返回值的,而Runnable是没有返回值的。
Executors提供了很多有用的方法,将Runnable转换为Callable:

1
2
3
4
5
复制代码    public static <T> Callable<T> callable(Runnable task, T result) {
if (task == null)
throw new NullPointerException();
return new RunnableAdapter<T>(task, result);
}

FutureTask内部包含一个Callable,并且可以接受Callable和Runnable作为构造函数:

1
2
3
4
5
6
复制代码    public FutureTask(Callable<V> callable) {
if (callable == null)
throw new NullPointerException();
this.callable = callable;
this.state = NEW; // ensure visibility of callable
}
1
2
3
4
复制代码    public FutureTask(Runnable runnable, V result) {
this.callable = Executors.callable(runnable, result);
this.state = NEW; // ensure visibility of callable
}

它的内部就是调用了Executors.callable(runnable, result);方法进行转换的。

以Runnable运行

既然是一个Runnable,那么FutureTask就可以以线程的方式执行,我们来看一个例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
复制代码@Test
public void convertRunnableToCallable() throws ExecutionException, InterruptedException {
FutureTask<Integer> futureTask = new FutureTask<>(new Callable<Integer>() {
@Override
public Integer call() throws Exception {
log.info("inside callable future task ...");
return 0;
}
});

Thread thread= new Thread(futureTask);
thread.start();
log.info(futureTask.get().toString());
}

上面例子是以单个线程来执行的,同样我们也可以将FutureTask提交给线程池来执行:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
复制代码    @Test
public void workWithExecutorService() throws ExecutionException, InterruptedException {
FutureTask<Integer> futureTask = new FutureTask<>(new Callable<Integer>() {
@Override
public Integer call() throws Exception {
log.info("inside futureTask");
return 1;
}
});
ExecutorService executor = Executors.newCachedThreadPool();
executor.submit(futureTask);
executor.shutdown();
log.info(futureTask.get().toString());
}

本文的例子可参考github.com/ddean2009/l…

更多教程请参考 flydean的博客

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

头条面试官问到的KMP算法

发表于 2020-03-27

本文只是一个学习后的总结,可能会有错误,欢迎各位指出。任意转载。

题目:给定一个字符串 str1 和一个字符串 str2,在字符串 str1 中找出字符串 str2 出现的第一个位置 (从0开始)。如果不存在,则返回 -1。

str1 = aaaaabcabc

str2 = abcabcaa

前段时间偶然接触到左神的算法讲解视频,大概三天的时间,反反复复把 KMP 算法看了三遍。终于有了一些自己的理解与体会。用传统的 KMP 算法去做字符串匹配,其实是用 next 数组对暴力算法的一个优化。另外一种理解是将 KMP 算法理解为动态规划,这里不详细叙述。

这里我分为三部分来讲。

  1. 暴力解法
  2. KMP 算法
  3. 如何求 next 数组

暴力解法

暴力算法看起来非常简单,实际编码还是需要处理一些细节的,建议写一写。这里的给 str1 一个 i 指针,给 str2 一个 j 指针。i 的第一个初始位置是 0,最后一个初始位置是 str1.length - 1。

  1. str1[ i ] 和 str2[ j ]相等: i 和 j 都往后移动一位。
  2. str1[ i ] 和 str2[ j ]不等,j 归 0, i 从下一个初始位置开始比较。

如果 j 能够到 length 这个位置上,说明从第 0 位到第 str2.length - 1 位都已经相等了,此时返回 i - j ,就是 str2 在 str1 中出现的第一个位置的 index 。

如果 i 到达了最后一个初始位置,也就是 str1.length - 1 ,此时还没有匹配成功,那么说明永远都没办法匹配到 str2 。 这个时候返回 -1 。

代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
复制代码public int strStr(String str1, String str2) {
int length1 = str1.length();
int length2 = str2.length();
if(length2 == 0) return 0;
if(length1 < length2) return -1;
int i = 0;
while(i < length1){
int j = 0;
while(i < length1 && j < length2
&& str1.charAt(i) == str2.charAt(j)){
i++;
j++;
}
if(j == length2){
return i-j;
}
i = i - j + 1;
}
return -1;
}

还是建议动手写一下。

KMP算法

这里暂时先不讨论 next 是如何来的。你需要知道它存放的是 str2 的一些信息。他的值等于 str2 前面的所有字符形成的字串的前缀等于后缀的最大值。这里非常绕,举个例子来说明:

index 等于 6 的时候, 字串是 a b c a b c。

前后缀取 1 的时候,前缀为 a, 后缀为 c,此时不等。 next 不能取 1 。

前后缀取 2 的时候,前缀为 ab, 后缀为 bc, next 不能取 2 。

前后缀取 3 的时候,前缀为 abc, 后缀为 abc, 此时相等, next 可以取 。

前后缀取 4 的时候,前缀为 abca, 后缀为 cabc, next 不可以取 4 。

前后缀取 5 的时候,前缀为 abcab, 后缀为 bcabc, next 不可以取 5 。

前后缀不可以取 6 。因为前后缀不可以为字符串本身。

index:0 1 2 3 4 5 6 7 8 9

str1 = a a a a a b c a b c

str2 = a b c a b c a a

next:-1 0 0 0 1 2 3 1

接下来是 KMP 算法的流程。按照暴力的解法,我们还是有两根指针 i 和 j。

  1. 两个元素相等时: i 和 j 往后移动一位。
  2. 两个元素不等时: j = next [ j ],如果此时 next[ j ] 等于 -1,说明 j 指针已经移动到了最前面。

我们来仔细理解这个不相等的两种情况,这里是难点。

next[j] != -1,这种情况下,j 指针直接跳到 str2[next[j]]去。为什么这样做可以?举例子。

index 0 1 2 3 4 5 6 7

str1 = a b c f a b c x

str2 = a b c f a b c y

next=-1 0 0 0 0 1 2 3

在 index 为 6的时候,i = j = 7,这个时候两个元素不相等,我们会把 j 跳到 str2[next[j]],也就是 j = 3。这个时候 str1 前面的子串 和 str2 前面的子串是相等的,他们拥有共同的 next 数组。j 跳到 3,这个 3 代表: y/x 前面这个子串他的前三位和后三位相等。那么,我们的 y 的子串前三位 和 x 子串的后三位这个时候是不是就不需要比较了,因为这个 3 默认了他们相等。那么前三位(index为 0 1 2)就不需要比较了,直接比较第四(index 为 3 )位。这里就是 next 数组的核心。在左神的视频里面讲得更直观。

str1 = a b c f a b c x

str2 = * * * * a b c f a b c y

比较 x 与 f 是否相等。

next[j] == -1,这种情况下,j 已经来到最前面了,没办法继续前移,那么只能 i 向后移。

代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
复制代码public static int getIndexOf(char str1[], char str2[]) {
if(str1.length == 0 || str1.length < str2.length) {
return -1;
}
if(str2.length == 0) {
return 0;
}
int i = 0;
int j = 0;
int next[] = getNextArray(str2);
//对应三种情况
while( i < str1.length && j < str2.length) {
if(str1[i] == str2[j]) {
i++; //两个元素相等
j++;
}else if(next[j] == -1) {
i++; //next[j] == -1
}else {
j = next[j];//next[j] != -1
}
}
return (j == str2.length) ? i-j : -1;
}

next数组

str2 = a b c f a b c y

next=-1 0 * * * * * *

第一位默认为 -1。 因为第一位元素没有子串。

第二位默认为 0。因为第二位元素的子串只有一个元素,那他的前后缀最大相等数目只能为0。

接下来是第三位,第三位的子串是a b,这里是难点。如何求出它的 next 值。j = 3

用 j - 1 的 next 的值,cn = next[j-1]的 str2 对应的元素, 和 str2[j-1] 比较。这里的cn = 0, 那比较的就是第 0 号元素和第 1 号元素的值。比较出来一定有两种情况,相等,不相等。而在不相等的时候又要分两种情况。

index 0 1 2 3 4 5 6 7

str2 = a b c f a b c y

next=-1 0 0 0 0 1 * *

为了更直观看见,我换个例子。j = 6.

cn = next[j-1] = 1, str2[cn] = b

str2[j-1] = b

这个时候是相等的,因此 next[6] = ++cn = 2。为什么?

这个 cn 代表的是什么?cn 代表的是j-1 位的 next 值,这个值代表j-1 位的前后缀最大值。这个最大值是 1,说明他第一位和最后一位相等。那么比较他的第二位(str2[cn])和最后一位的下一位(str2[j-1])是否相等。相等的话,next[6] = ++cn = 2。不相等怎么办?分为两种情况。

  1. cn > 0,cn = next[cn]
  2. cn<= 0,next[j] = 0

这里又是为什么,就是在子串的情况下继续分,去找到和str[j-1]相等的 cn,如果一直找不到呢?怎么办,那next[j] = 0。

代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
复制代码public static int[] getNextArray(char []str) {
if(str.length == 1) {
return new int [] {-1};
}
int next[] = new int [str.length];
next[0] = -1;
next[1] = 0;
int i = 2;
int cn = 0;
while( i < str.length) {
if(str[i-1] == str[cn]) {
next[i++] = ++cn;
}else if(cn > 0) {
cn = next[cn];
}else {
next[i++] = 0;
}
}
return next;
}

小结一下

  1. 暴力解法,多去写,多写两遍就熟练。
  2. KMP 具体实现,有三种情况。元素相等、元素不等且 next 不等于-1、元素不等且 next 等于 -1。
  3. next 的求解方法,也是三种情况。cn 和 j-1 对应的元素相等、 对应的元素不等且cn>0、 对应的元素不等且cn<=0。

公众号: stul

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

【译】kotlin 协程官方文档(8)-共享可变状态和并发性

发表于 2020-03-26

公众号:字节数组

希望对你有所帮助 🤣🤣

最近一直在了解关于kotlin协程的知识,那最好的学习资料自然是官方提供的学习文档了,看了看后我就萌生了翻译官方文档的想法。前后花了要接近一个月时间,一共九篇文章,在这里也分享出来,希望对读者有所帮助。个人知识所限,有些翻译得不是太顺畅,也希望读者能提出意见

协程官方文档:coroutines-guide

协程官方文档中文翻译:coroutines-cn-guide

可以使用多线程调度器(如 Dispatchers.Default)并发执行协程,它呈现了所有常见的并发问题。主要问题是对共享可变状态的同步访问。在协程作用域中解决这个问题的一些方法类似于多线程世界中的方法,但有一些其它方法是独有的

一、一个问题

让我们启动一百个协程,都做同样的操作一千次。我们还将计算它们的完成时间,以便进一步比较:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
kotlin复制代码suspend fun massiveRun(action: suspend () -> Unit) {
val n = 100 // number of coroutines to launch
val k = 1000 // times an action is repeated by each coroutine
val time = measureTimeMillis {
coroutineScope { // scope for coroutines
repeat(n) {
launch {
repeat(k) { action() }
}
}
}
}
println("Completed ${n * k} actions in $time ms")
}

我们从一个非常简单的操作开始,该操作使用多线程调度器 Dispatchers.Default,并增加一个共享的可变变量

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
kotlin复制代码import kotlinx.coroutines.*
import kotlin.system.*

suspend fun massiveRun(action: suspend () -> Unit) {
val n = 100 // number of coroutines to launch
val k = 1000 // times an action is repeated by each coroutine
val time = measureTimeMillis {
coroutineScope { // scope for coroutines
repeat(n) {
launch {
repeat(k) { action() }
}
}
}
}
println("Completed ${n * k} actions in $time ms")
}

//sampleStart
var counter = 0

fun main() = runBlocking {
withContext(Dispatchers.Default) {
massiveRun {
counter++
}
}
println("Counter = $counter")
}
//sampleEnd

最后会打印出什么呢?不太可能打印出 “Counter=100000”,因为100个协程从多个线程并发地递增 counter 而不进行任何同步。

二、Volatiles 是没有作用的

有一种常见的误解是:将变量标记为 volatile 可以解决并发问题。让我们试试:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
kotlin复制代码import kotlinx.coroutines.*
import kotlin.system.*

suspend fun massiveRun(action: suspend () -> Unit) {
val n = 100 // number of coroutines to launch
val k = 1000 // times an action is repeated by each coroutine
val time = measureTimeMillis {
coroutineScope { // scope for coroutines
repeat(n) {
launch {
repeat(k) { action() }
}
}
}
}
println("Completed ${n * k} actions in $time ms")
}

//sampleStart
@Volatile // in Kotlin `volatile` is an annotation
var counter = 0

fun main() = runBlocking {
withContext(Dispatchers.Default) {
massiveRun {
counter++
}
}
println("Counter = $counter")
}
//sampleEnd

这段代码运行得比较慢,但是我们在最后仍然没有得到“Counter=100000”,因为 volatile 变量保证了可线性化(这是“atomic”的一个技术术语)对相应变量的读写,但不提供更大行为的原子性(在我们的例子中指递增操作)

三、线程安全的数据结构

对线程和协程都有效的一个解决方案是使用线程安全的(也称为同步、可线性化或原子)数据结构,该结构为需要在共享状态上执行的相应操作提供所有必要的同步保障。对于一个简单的计数器,我们可以使用 AtomicInteger 类,该类具有保证原子性的 incrementAndGet 方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
kotlin复制代码import kotlinx.coroutines.*
import java.util.concurrent.atomic.*
import kotlin.system.*

suspend fun massiveRun(action: suspend () -> Unit) {
val n = 100 // number of coroutines to launch
val k = 1000 // times an action is repeated by each coroutine
val time = measureTimeMillis {
coroutineScope { // scope for coroutines
repeat(n) {
launch {
repeat(k) { action() }
}
}
}
}
println("Completed ${n * k} actions in $time ms")
}

//sampleStart
var counter = AtomicInteger()

fun main() = runBlocking {
withContext(Dispatchers.Default) {
massiveRun {
counter.incrementAndGet()
}
}
println("Counter = $counter")
}
//sampleEnd

这是解决这个特殊问题的最快方法。它适用于普通计数器、集合、队列和其他标准数据结构及其基本操作。但是,它不容易扩展到复杂的状态或没有实现好了的线程安全的复杂操作

四、以细粒度限制线程

线程限制是解决共享可变状态问题的一种方法,其中对特定共享状态的所有访问都限制在一个线程内。它通常用于 UI 应用程序,其中所有的 UI 状态都限制在“单个事件分派”或“应用程序线程”中。通过使用单线程上下文,可以很容易地使用协程来实现上述的计数器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
kotlin复制代码import kotlinx.coroutines.*
import kotlin.system.*

suspend fun massiveRun(action: suspend () -> Unit) {
val n = 100 // number of coroutines to launch
val k = 1000 // times an action is repeated by each coroutine
val time = measureTimeMillis {
coroutineScope { // scope for coroutines
repeat(n) {
launch {
repeat(k) { action() }
}
}
}
}
println("Completed ${n * k} actions in $time ms")
}

//sampleStart
val counterContext = newSingleThreadContext("CounterContext")
var counter = 0

fun main() = runBlocking {
withContext(Dispatchers.Default) {
massiveRun {
// confine each increment to a single-threaded context
withContext(counterContext) {
counter++
}
}
}
println("Counter = $counter")
}
//sampleEnd

这段代码运行得非常缓慢,因为它执行细粒度的线程限制。每个单独的增值操作都使用 withContext(counterContext) 从多线程 Dispatchers.Default 上下文切换到单线程上下文

五、以粗粒度限制线程

在实践中,线程限制是在比较大的范围内执行的,例如,更新状态的逻辑的范围被限制在单个线程中。下面的示例就是这样做的,首先在单线程上下文中运行每个协程

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
kotlin复制代码import kotlinx.coroutines.*
import kotlin.system.*

suspend fun massiveRun(action: suspend () -> Unit) {
val n = 100 // number of coroutines to launch
val k = 1000 // times an action is repeated by each coroutine
val time = measureTimeMillis {
coroutineScope { // scope for coroutines
repeat(n) {
launch {
repeat(k) { action() }
}
}
}
}
println("Completed ${n * k} actions in $time ms")
}

//sampleStart
val counterContext = newSingleThreadContext("CounterContext")
var counter = 0

fun main() = runBlocking {
// confine everything to a single-threaded context
withContext(counterContext) {
massiveRun {
counter++
}
}
println("Counter = $counter")
}
//sampleEnd

现在这段代码的运行速度会快得多,并产生了正确的结果

六、互斥

互斥问题的解决方案是保护共享状态的所有修改操作,其中的关键代码永远不会同时执行。在一个阻塞的世界中,通常会使用 synchronized 或 ReentrantLock。协程的替换方案称为互斥(Mutex)。它具有 lock 和 unlock 函数以划定一个关键位置。关键的区别在于 Mutex.lock() 是一个挂起函数。它不会阻塞线程

还有一个扩展函数 withLock 可以方便地来实现 mutex.lock(); try {...} finally { mutex.unlock() }

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
kotlin复制代码import kotlinx.coroutines.*
import kotlinx.coroutines.sync.*
import kotlin.system.*

suspend fun massiveRun(action: suspend () -> Unit) {
val n = 100 // number of coroutines to launch
val k = 1000 // times an action is repeated by each coroutine
val time = measureTimeMillis {
coroutineScope { // scope for coroutines
repeat(n) {
launch {
repeat(k) { action() }
}
}
}
}
println("Completed ${n * k} actions in $time ms")
}

//sampleStart
val mutex = Mutex()
var counter = 0

fun main() = runBlocking {
withContext(Dispatchers.Default) {
massiveRun {
// protect each increment with lock
mutex.withLock {
counter++
}
}
}
println("Counter = $counter")
}
//sampleEnd

本例中的锁是细粒度的,因此它也付出了某些代价(消耗)。但是,在某些情况下这是一个很好的选择,比如你必须定期修改某些共享状态,但不具备修改共享状态所需的原生线程

七、Actors

actor 是一个实体,由一个协程、被限制并封装到这个协程中的状态以及一个与其它协程通信的通道组成。简单的 actor 可以写成函数,但具有复杂状态的 actor 更适合类

有一个 actor 协程构造器,它可以方便地将 actor 的 mailbox channel 合并到其接收的消息的作用域中,并将 send channel 合并到生成的 job 对象中,以便可以将对 actor 的单个引用作为其句柄引有

使用 actor 的第一步是定义一类 actor 将要处理的消息。kotlin 的密封类非常适合这个目的。在 CounterMsg 密封类中,我们用 IncCounter 消息来定义递增计数器,用 GetCounter 消息来获取其值,后者需要返回值。为此,这里使用 CompletableDeferred communication primitive,它表示将来已知(通信)的单个值

1
2
3
4
kotlin复制代码// Message types for counterActor
sealed class CounterMsg
object IncCounter : CounterMsg() // one-way message to increment counter
class GetCounter(val response: CompletableDeferred<Int>) : CounterMsg() // a request with reply

然后,我们定义一个函数,该函数使用 actor 协程构造器来启动 actor:

1
2
3
4
5
6
7
8
9
10
kotlin复制代码// This function launches a new counter actor
fun CoroutineScope.counterActor() = actor<CounterMsg> {
var counter = 0 // actor state
for (msg in channel) { // iterate over incoming messages
when (msg) {
is IncCounter -> counter++
is GetCounter -> msg.response.complete(counter)
}
}
}

代码很简单:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
kotlin复制代码import kotlinx.coroutines.*
import kotlinx.coroutines.channels.*
import kotlin.system.*

suspend fun massiveRun(action: suspend () -> Unit) {
val n = 100 // number of coroutines to launch
val k = 1000 // times an action is repeated by each coroutine
val time = measureTimeMillis {
coroutineScope { // scope for coroutines
repeat(n) {
launch {
repeat(k) { action() }
}
}
}
}
println("Completed ${n * k} actions in $time ms")
}

// Message types for counterActor
sealed class CounterMsg
object IncCounter : CounterMsg() // one-way message to increment counter
class GetCounter(val response: CompletableDeferred<Int>) : CounterMsg() // a request with reply

// This function launches a new counter actor
fun CoroutineScope.counterActor() = actor<CounterMsg> {
var counter = 0 // actor state
for (msg in channel) { // iterate over incoming messages
when (msg) {
is IncCounter -> counter++
is GetCounter -> msg.response.complete(counter)
}
}
}

//sampleStart
fun main() = runBlocking<Unit> {
val counter = counterActor() // create the actor
withContext(Dispatchers.Default) {
massiveRun {
counter.send(IncCounter)
}
}
// send a message to get a counter value from an actor
val response = CompletableDeferred<Int>()
counter.send(GetCounter(response))
println("Counter = ${response.await()}")
counter.close() // shutdown the actor
}
//sampleEnd

在什么上下文中执行 actor 本身并不重要(为了正确)。actor 是一个协程,并且协程是按顺序执行的,因此将状态限制到特定的协程可以解决共享可变状态的问题。实际上,actors 可以修改自己的私有状态,但只能通过消息相互影响(避免需要任何锁)

actor 比使用锁更为有效,因为在这种情况下,它总是有工作要做,根本不需要切换到不同的上下文

注意,actor 协程构造器是一个双重的 product 协程构造器 。actor 与它接收消息的通道相关联,而 producer 与向其发送元素的通道相关联

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

Java中的屠龙之术——如何修改语法树

发表于 2020-03-26

在Lombok经常用,但是你知道它的原理是什么吗?,和Lombok经常用,但是你知道它的原理是什么吗?(二)两篇文章中介绍了关于Lombok的底层原理,其实总结为一句话来说就是在编译期通过改变抽象语法树而实现的。上面两篇文章已经讲了抽象语法树的相关知识点,如果有不清楚的可以看一下。

本篇涉及到的所有代码都在github上面有

本篇涉及到的所有代码都在github上面有

本篇涉及到的所有代码都在github上面有

在网上关于如何修改Java的抽象语法树的相关API文档并不多,于是本篇记录一下相关的知识点,以便随后查阅。

JCTree的介绍

JCTree是语法树元素的基类,包含一个重要的字段pos,该字段用于指明当前语法树节点(JCTree)在语法树中的位置,因此我们不能直接用new关键字来创建语法树节点,即使创建了也没有意义。此外,结合访问者模式,将数据结构与数据的处理进行解耦,部分源码如下:

1
2
3
4
5
6
7
8
9
10
复制代码 1public abstract class JCTree implements Tree, Cloneable, DiagnosticPosition {  
2
3    public int pos = -1;
4
5    ...
6
7    public abstract void accept(JCTree.Visitor visitor);
8
9    ...
10}

我们可以看到JCTree是一个抽象类,这里重点介绍几个JCTree的子类

  1. JCStatement:声明语法树节点,常见的子类如下
    • JCBlock:语句块语法树节点
    • JCReturn:return语句语法树节点
    • JCClassDecl:类定义语法树节点
    • JCVariableDecl:字段/变量定义语法树节点
  2. JCMethodDecl:方法定义语法树节点
  3. JCModifiers:访问标志语法树节点
  4. JCExpression:表达式语法树节点,常见的子类如下
    • JCAssign:赋值语句语法树节点
    • JCIdent:标识符语法树节点,可以是变量,类型,关键字等等

TreeMaker介绍

TreeMaker用于创建一系列的语法树节点,我们上面说了创建JCTree不能直接使用new关键字来创建,所以Java为我们提供了一个工具,就是TreeMaker,它会在创建时为我们创建的JCTree对象设置pos字段,所以必须使用上下文相关的TreeMaker对象来创建语法树节点。

具体的API介绍可以参照,TreeMakerAPI,接下来着重介绍一下常用的几个方法。

TreeMaker.Modifiers

TreeMaker.Modifiers方法用于创建访问标志语法树节点(JCModifiers),源码如下

1
2
3
4
5
6
7
8
9
10
11
复制代码 1public JCModifiers Modifiers(long flags) {  
2    return Modifiers(flags, List.< JCAnnotation >nil());
3}
4
5public JCModifiers Modifiers(long flags,
6    List<JCAnnotation> annotations) {
7        JCModifiers tree = new JCModifiers(flags, annotations);
8        boolean noFlags = (flags & (Flags.ModifierFlags | Flags.ANNOTATION)) == 0;
9        tree.pos = (noFlags && annotations.isEmpty()) ? Position.NOPOS : pos;
10        return tree;
11}
  1. flags:访问标志
  2. annotations:注解列表

其中flags可以使用枚举类com.sun.tools.javac.code.Flags来表示,例如我们可以这样用,就生成了下面的访问标志了。

1
2
3
复制代码1treeMaker.Modifiers(Flags.PUBLIC + Flags.STATIC + Flags.FINAL);  
2
3public static final

TreeMaker.ClassDef

TreeMaker.ClassDef用于创建类定义语法树节点(JCClassDecl),源码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
复制代码 1public JCClassDecl ClassDef(JCModifiers mods,  
2    Name name,
3    List<JCTypeParameter> typarams,
4    JCExpression extending,
5    List<JCExpression> implementing,
6    List<JCTree> defs) {
7        JCClassDecl tree = new JCClassDecl(mods,
8                                     name,
9                                     typarams,
10                                     extending,
11                                     implementing,
12                                     defs,
13                                     null);
14        tree.pos = pos;
15        return tree;
16}
  1. mods:访问标志,可以通过TreeMaker.Modifiers来创建
  2. name:类名
  3. typarams:泛型参数列表
  4. extending:父类
  5. implementing:实现的接口
  6. defs:类定义的详细语句,包括字段、方法的定义等等

TreeMaker.MethodDef

TreeMaker.MethodDef用于创建方法定义语法树节点(JCMethodDecl),源码如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
复制代码 1public JCMethodDecl MethodDef(JCModifiers mods,  
2    Name name,
3    JCExpression restype,
4    List<JCTypeParameter> typarams,
5    List<JCVariableDecl> params,
6    List<JCExpression> thrown,
7    JCBlock body,
8    JCExpression defaultValue) {
9        JCMethodDecl tree = new JCMethodDecl(mods,
10                                       name,
11                                       restype,
12                                       typarams,
13                                       params,
14                                       thrown,
15                                       body,
16                                       defaultValue,
17                                       null);
18        tree.pos = pos;
19        return tree;
20}
21
22public JCMethodDecl MethodDef(MethodSymbol m,
23    Type mtype,
24    JCBlock body) {
25        return (JCMethodDecl)
26            new JCMethodDecl(
27                Modifiers(m.flags(), Annotations(m.getAnnotationMirrors())),
28                m.name,
29                Type(mtype.getReturnType()),
30                TypeParams(mtype.getTypeArguments()),
31                Params(mtype.getParameterTypes(), m),
32                Types(mtype.getThrownTypes()),
33                body,
34                null,
35                m).setPos(pos).setType(mtype);
36}
  1. mods:访问标志
  2. name:方法名
  3. restype:返回类型
  4. typarams:泛型参数列表
  5. params:参数列表
  6. thrown:异常声明列表
  7. body:方法体
  8. defaultValue:默认方法(可能是interface中的哪个default)
  9. m:方法符号
  10. mtype:方法类型。包含多种类型,泛型参数类型、方法参数类型、异常参数类型、返回参数类型。

返回类型restype填写null或者treeMaker.TypeIdent(TypeTag.VOID)都代表返回void类型

TreeMaker.VarDef

TreeMaker.VarDef用于创建字段/变量定义语法树节点(JCVariableDecl),源码如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
复制代码 1public JCVariableDecl VarDef(JCModifiers mods,  
2    Name name,
3    JCExpression vartype,
4    JCExpression init) {
5        JCVariableDecl tree = new JCVariableDecl(mods, name, vartype, init, null);
6        tree.pos = pos;
7        return tree;
8}
9
10public JCVariableDecl VarDef(VarSymbol v,
11    JCExpression init) {
12        return (JCVariableDecl)
13            new JCVariableDecl(
14                Modifiers(v.flags(), Annotations(v.getAnnotationMirrors())),
15                v.name,
16                Type(v.type),
17                init,
18                v).setPos(pos).setType(v.type);
19}
  1. mods:访问标志
  2. name:参数名称
  3. vartype:类型
  4. init:初始化语句
  5. v:变量符号

TreeMaker.Ident

TreeMaker.Ident用于创建标识符语法树节点(JCIdent),源码如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
复制代码 1public JCIdent Ident(Name name) {  
2        JCIdent tree = new JCIdent(name, null);
3        tree.pos = pos;
4        return tree;
5}
6
7public JCIdent Ident(Symbol sym) {
8        return (JCIdent)new JCIdent((sym.name != names.empty)
9                                ? sym.name
10                                : sym.flatName(), sym)
11            .setPos(pos)
12            .setType(sym.type);
13}
14
15public JCExpression Ident(JCVariableDecl param) {
16        return Ident(param.sym);
17}

TreeMaker.Return

TreeMaker.Return用于创建return语句(JCReturn),源码如下

1
2
3
4
5
复制代码1public JCReturn Return(JCExpression expr) {  
2        JCReturn tree = new JCReturn(expr);
3        tree.pos = pos;
4        return tree;
5}

TreeMaker.Select

TreeMaker.Select用于创建域访问/方法访问(这里的方法访问只是取到名字,方法的调用需要用TreeMaker.Apply)语法树节点(JCFieldAccess),源码如下

1
2
3
4
5
6
7
8
9
10
11
12
复制代码 1public JCFieldAccess Select(JCExpression selected,  
2    Name selector) 
3{
4        JCFieldAccess tree = new JCFieldAccess(selected, selector, null);
5        tree.pos = pos;
6        return tree;
7}
8
9public JCExpression Select(JCExpression base,
10    Symbol sym) {
11        return new JCFieldAccess(base, sym.name, sym).setPos(pos).setType(sym.type);
12}
  1. selected:.运算符左边的表达式
  2. selector:.运算符右边的表达式

下面给出一个例子,一语句生成的Java语句就是二语句

1
2
3
复制代码1一. TreeMaker.Select(treeMaker.Ident(names.fromString("this")), names.fromString("name"));  
2
3二. this.name

TreeMaker.NewClass

TreeMaker.NewClass用于创建new语句语法树节点(JCNewClass),源码如下:

1
2
3
4
5
6
7
8
9
复制代码1public JCNewClass NewClass(JCExpression encl,  
2    List<JCExpression> typeargs,
3    JCExpression clazz,
4    List<JCExpression> args,
5    JCClassDecl def) {
6        JCNewClass tree = new JCNewClass(encl, typeargs, clazz, args, def);
7        tree.pos = pos;
8        return tree;
9}
  1. encl:不太明白此参数的含义,我看很多例子中此参数都设置为null
  2. typeargs:参数类型列表
  3. clazz:待创建对象的类型
  4. args:参数列表
  5. def:类定义

TreeMaker.Apply

TreeMaker.Apply用于创建方法调用语法树节点(JCMethodInvocation),源码如下:

1
2
3
4
5
6
7
复制代码1public JCMethodInvocation Apply(List<JCExpression> typeargs,  
2    JCExpression fn,
3    List<JCExpression> args) {
4        JCMethodInvocation tree = new JCMethodInvocation(typeargs, fn, args);
5        tree.pos = pos;
6        return tree;
7}
  1. typeargs:参数类型列表
  2. fn:调用语句
  3. args:参数列表

TreeMaker.Assign

TreeMaker.Assign用户创建赋值语句语法树节点(JCAssign),源码如下:

1
2
3
4
5
6
复制代码1ublic JCAssign Assign(JCExpression lhs,  
2    JCExpression rhs) {
3        JCAssign tree = new JCAssign(lhs, rhs);
4        tree.pos = pos;
5        return tree;
6}
  1. lhs:赋值语句左边表达式
  2. rhs:赋值语句右边表达式

TreeMaker.Exec

TreeMaker.Exec用于创建可执行语句语法树节点(JCExpressionStatement),源码如下:

1
2
3
4
5
复制代码1public JCExpressionStatement Exec(JCExpression expr) {  
2        JCExpressionStatement tree = new JCExpressionStatement(expr);
3        tree.pos = pos;
4        return tree;
5}

TreeMaker.Apply以及TreeMaker.Assign就需要外面包一层TreeMaker.Exec来获得一个JCExpressionStatement

TreeMaker.Block

TreeMaker.Block用于创建组合语句的语法树节点(JCBlock),源码如下:

1
2
3
4
5
6
复制代码1public JCBlock Block(long flags,  
2    List<JCStatement> stats) {
3        JCBlock tree = new JCBlock(flags, stats);
4        tree.pos = pos;
5        return tree;
6}
  1. flags:访问标志
  2. stats:语句列表

com.sun.tools.javac.util.List介绍

在我们操作抽象语法树的时候,有时会涉及到关于List的操作,但是这个List不是我们经常使用的java.util.List而是com.sun.tools.javac.util.List,这个List比较奇怪,是一个链式的结构,有头结点和尾节点,但是只有尾节点是一个List,这里作为了解就行了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
复制代码 1public class List<A> extends AbstractCollection<A> implements java.util.List<A> {  
2    public A head;
3    public List<A> tail;
4    private static final List<?> EMPTY_LIST = new List<Object>((Object)null, (List)null) {
5        public List<Object> setTail(List<Object> var1) {
6            throw new UnsupportedOperationException();
7        }
8
9        public boolean isEmpty() {
10            return true;
11        }
12    };
13
14    List(A head, List<A> tail) {
15        this.tail = tail;
16        this.head = head;
17    }
18
19    public static <A> List<A> nil() {
20        return EMPTY_LIST;
21    }
22
23    public List<A> prepend(A var1) {
24        return new List(var1, this);
25    }
26
27    public List<A> append(A var1) {
28        return of(var1).prependList(this);
29    }
30
31    public static <A> List<A> of(A var0) {
32        return new List(var0, nil());
33    }
34
35    public static <A> List<A> of(A var0, A var1) {
36        return new List(var0, of(var1));
37    }
38
39    public static <A> List<A> of(A var0, A var1, A var2) {
40        return new List(var0, of(var1, var2));
41    }
42
43    public static <A> List<A> of(A var0, A var1, A var2, A... var3) {
44        return new List(var0, new List(var1, new List(var2, from(var3))));
45    }
46
47    ...
48}

com.sun.tools.javac.util.ListBuffer

由于com.sun.tools.javac.util.List使用起来不方便,所以又在其上面封装了一层,这个封装类是ListBuffer,此类的操作和我们平时经常使用的java.util.List用法非常类似。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
复制代码 1public class ListBuffer<A> extends AbstractQueue<A> {  
2
3    public static <T> ListBuffer<T> of(T x) {
4        ListBuffer<T> lb = new ListBuffer<T>();
5        lb.add(x);
6        return lb;
7    }
8
9    /** The list of elements of this buffer.
10     */
11    private List<A> elems;
12
13    /** A pointer pointing to the last element of 'elems' containing data,
14     *  or null if the list is empty.
15     */
16    private List<A> last;
17
18    /** The number of element in this buffer.
19     */
20    private int count;
21
22    /** Has a list been created from this buffer yet?
23     */
24    private boolean shared;
25
26    /** Create a new initially empty list buffer.
27     */
28    public ListBuffer() {
29        clear();
30    }
31
32    /** Append an element to buffer.
33     */
34    public ListBuffer<A> append(A x) {
35        x.getClass(); // null check
36        if (shared) copy();
37        List<A> newLast = List.<A>of(x);
38        if (last != null) {
39            last.tail = newLast;
40            last = newLast;
41        } else {
42            elems = last = newLast;
43        }
44        count++;
45        return this;
46    }
47    ........
48}

com.sun.tools.javac.util.Names介绍

这个是为我们创建名称的一个工具类,无论是类、方法、参数的名称都需要通过此类来创建。它里面经常被使用到的一个方法就是fromString(),一般使用方法如下所示。

1
2
复制代码1Names names  = new Names()  
2names. fromString("setName");

实战演练

上面我们大概了解了如何操作抽象语法树,接下来我们就来写几个真实的案例加深理解。

变量相关

在类中我们经常操作的参数就是变量,那么如何使用抽象语法树的特性为我们操作变量呢?接下来我们就将一些对于变量的一些操作。

生成变量

例如生成private String age;这样一个变量,借用我们上面讲的VarDef方法

1
2
复制代码1// 生成参数 例如:private String age;  
2treeMaker.VarDef(treeMaker.Modifiers(Flags.PRIVATE), names.fromString("age"), treeMaker.Ident(names.fromString("String")), null);

对变量赋值

例如我们想生成private String name = "BuXueWuShu",还是利用VarDef方法

1
2
复制代码1// private String name = "BuXueWuShu"  
2treeMaker.VarDef(treeMaker.Modifiers(Flags.PRIVATE),names.fromString("name"),treeMaker.Ident(names.fromString("String")),treeMaker.Literal("BuXueWuShu"))

两个字面量相加

例如我们生成String add = "a" + "b";,借用我们上面讲的Exec方法和Assign方法

1
2
复制代码1// add = "a"+"b"  
2treeMaker.Exec(treeMaker.Assign(treeMaker.Ident(names.fromString("add")),treeMaker.Binary(JCTree.Tag.PLUS,treeMaker.Literal("a"),treeMaker.Literal("b"))))

+=语法

例如我们想生成add += "test",则和上面字面量差不多。

1
2
复制代码1// add+="test"  
2treeMaker.Exec(treeMaker.Assignop(JCTree.Tag.PLUS_ASG, treeMaker.Ident(names.fromString("add")), treeMaker.Literal("test")))

++语法

例如想生成++i

1
复制代码1treeMaker.Exec(treeMaker.Unary(JCTree.Tag.PREINC,treeMaker.Ident(names.fromString("i"))))

方法相关

我们对于变量进行了操作,那么基本上都是要生成方法的,那么如何对方法进行生成和操作呢?我们接下来演示一下关于方法相关的操作方法。

无参无返回值

我们可以利用上面讲到的MethodDef方法进行生成

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
复制代码 1/*  
2    无参无返回值的方法生成
3    public void test(){
4
5    }
6 */
7// 定义方法体
8ListBuffer<JCTree.JCStatement> testStatement = new ListBuffer<>();
9JCTree.JCBlock testBody = treeMaker.Block(0, testStatement.toList());
10
11JCTree.JCMethodDecl test = treeMaker.MethodDef(
12        treeMaker.Modifiers(Flags.PUBLIC), // 方法限定值
13        names.fromString("test"), // 方法名
14        treeMaker.Type(new Type.JCVoidType()), // 返回类型
15        com.sun.tools.javac.util.List.nil(),
16        com.sun.tools.javac.util.List.nil(),
17        com.sun.tools.javac.util.List.nil(),
18        testBody,    // 方法体
19        null
20);

有参无返回值

我们可以利用上面讲到的MethodDef方法进行生成

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
复制代码 1/*  
2    无参无返回值的方法生成
3    public void test2(String name){
4        name = "xxxx";
5    }
6 */
7ListBuffer<JCTree.JCStatement> testStatement2 = new ListBuffer<>();
8testStatement2.append(treeMaker.Exec(treeMaker.Assign(treeMaker.Ident(names.fromString("name")),treeMaker.Literal("xxxx"))));
9JCTree.JCBlock testBody2 = treeMaker.Block(0, testStatement2.toList());
10
11// 生成入参
12JCTree.JCVariableDecl param = treeMaker.VarDef(treeMaker.Modifiers(Flags.PARAMETER), names.fromString("name"),treeMaker.Ident(names.fromString("String")), null);
13com.sun.tools.javac.util.List<JCTree.JCVariableDecl> parameters = com.sun.tools.javac.util.List.of(param);
14
15JCTree.JCMethodDecl test2 = treeMaker.MethodDef(
16        treeMaker.Modifiers(Flags.PUBLIC), // 方法限定值
17        names.fromString("test2"), // 方法名
18        treeMaker.Type(new Type.JCVoidType()), // 返回类型
19        com.sun.tools.javac.util.List.nil(),
20        parameters, // 入参
21        com.sun.tools.javac.util.List.nil(),
22        testBody2,
23        null
24);

有参有返回值

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
复制代码 1 /*  
2    有参有返回值
3    public String test3(String name){
4       return name;
5    }
6 */
7
8ListBuffer<JCTree.JCStatement> testStatement3 = new ListBuffer<>();
9testStatement3.append(treeMaker.Return(treeMaker.Ident(names.fromString("name"))));
10JCTree.JCBlock testBody3 = treeMaker.Block(0, testStatement3.toList());
11
12// 生成入参
13JCTree.JCVariableDecl param3 = treeMaker.VarDef(treeMaker.Modifiers(Flags.PARAMETER), names.fromString("name"),treeMaker.Ident(names.fromString("String")), null);
14com.sun.tools.javac.util.List<JCTree.JCVariableDecl> parameters3 = com.sun.tools.javac.util.List.of(param3);
15
16JCTree.JCMethodDecl test3 = treeMaker.MethodDef(
17        treeMaker.Modifiers(Flags.PUBLIC), // 方法限定值
18        names.fromString("test4"), // 方法名
19        treeMaker.Ident(names.fromString("String")), // 返回类型
20        com.sun.tools.javac.util.List.nil(),
21        parameters3, // 入参
22        com.sun.tools.javac.util.List.nil(),
23        testBody3,
24        null
25);

特殊的

我们学完了如何进行定义参数,如何进行定义方法,其实还有好多语句需要学习,例如如何生成new语句,如何生成方法调用的语句,如何生成if语句。j接下来我们就学习一些比较特殊的语法。

new一个对象

1
2
3
4
5
6
7
8
9
10
11
12
13
14
复制代码 1// 创建一个new语句 CombatJCTreeMain combatJCTreeMain = new CombatJCTreeMain();  
2JCTree.JCNewClass combatJCTreeMain = treeMaker.NewClass(
3        null,
4        com.sun.tools.javac.util.List.nil(),
5        treeMaker.Ident(names.fromString("CombatJCTreeMain")),
6        com.sun.tools.javac.util.List.nil(),
7        null
8);
9JCTree.JCVariableDecl jcVariableDecl1 = treeMaker.VarDef(
10        treeMaker.Modifiers(Flags.PARAMETER),
11        names.fromString("combatJCTreeMain"),
12        treeMaker.Ident(names.fromString("CombatJCTreeMain")),
13        combatJCTreeMain
14);

方法调用(无参)

1
2
3
4
5
6
7
8
9
10
复制代码 1JCTree.JCExpressionStatement exec = treeMaker.Exec(  
2        treeMaker.Apply(
3                com.sun.tools.javac.util.List.nil(),
4                treeMaker.Select(
5                        treeMaker.Ident(names.fromString("combatJCTreeMain")), // . 左边的内容
6                        names.fromString("test") // . 右边的内容
7                ),
8                com.sun.tools.javac.util.List.nil()
9        )
10);

方法调用(有参)

1
2
3
4
5
6
7
8
9
10
11
复制代码 1// 创建一个方法调用 combatJCTreeMain.test2("hello world!");  
2JCTree.JCExpressionStatement exec2 = treeMaker.Exec(
3        treeMaker.Apply(
4                com.sun.tools.javac.util.List.nil(),
5                treeMaker.Select(
6                        treeMaker.Ident(names.fromString("combatJCTreeMain")), // . 左边的内容
7                        names.fromString("test2") // . 右边的内容
8                ),
9                com.sun.tools.javac.util.List.of(treeMaker.Literal("hello world!")) // 方法中的内容
10        )
11);

if语句

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
复制代码 1/*  
2    创建一个if语句
3    if("BuXueWuShu".equals(name)){
4        add = "a" + "b";
5    }else{
6        add += "test";
7    }
8 */
9// "BuXueWuShu".equals(name)
10JCTree.JCMethodInvocation apply = treeMaker.Apply(
11        com.sun.tools.javac.util.List.nil(),
12        treeMaker.Select(
13                treeMaker.Literal("BuXueWuShu"), // . 左边的内容
14                names.fromString("equals") // . 右边的内容
15        ),
16        com.sun.tools.javac.util.List.of(treeMaker.Ident(names.fromString("name")))
17);
18//  add = "a" + "b"
19JCTree.JCExpressionStatement exec3 = treeMaker.Exec(treeMaker.Assign(treeMaker.Ident(names.fromString("add")), treeMaker.Binary(JCTree.Tag.PLUS, treeMaker.Literal("a"), treeMaker.Literal("b"))));
20//  add += "test"
21JCTree.JCExpressionStatement exec1 = treeMaker.Exec(treeMaker.Assignop(JCTree.Tag.PLUS_ASG, treeMaker.Ident(names.fromString("add")), treeMaker.Literal("test")));
22
23JCTree.JCIf anIf = treeMaker.If(
24        apply, // if语句里面的判断语句
25        exec3, // 条件成立的语句
26        exec1  // 条件不成立的语句
27);

源码地址

总结

纸上得来终觉浅,绝知此事要躬行。希望大家看完此篇文章能够自己在本机上自己试验一下。自己设置几个参数,自己学的Lombok学着生成一下get、set方法,虽然本篇知识在日常开发中基本上不会用到,但是万一用到了这些知识那么别人不会而你会,差距其实就慢慢的给拉开了。本篇涉及到的所有代码都在github上面有,拉下来以后全局搜CombatJCTreeProcessor类就可以看到了。

有感兴趣的可以关注一下我新建的公众号,搜索[程序猿的百宝袋]。或者直接扫下面的码也行。

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

1…824825826…956

开发者博客

9558 日志
1953 标签
RSS
© 2025 开发者博客
本站总访问量次
由 Hexo 强力驱动
|
主题 — NexT.Muse v5.1.4
0%