Java复习之集合框架总结

时间:2022-08-26 17:43:36

俗话说:温故而知新。想想学过的知识,就算是以前学得很不错,久不用了,就会忘记,所以温习一下以前学习的知识我认为是非常有必要的。而本篇文件温习的是 java基础中的集合框架

为什么会有集合框架?

平时我们用数组存储一些基本的数据类型,或者是引用数据类型,但是数组的长度是固定的,当添加的元素超过了数组的长度时,需要对数组进行重新的定义,这样就会显得写程序太麻烦,所以java内部为了我们方便,就提供了集合类,能存储任意对象,长度是可以改变的,随着元素的增加而增加,随着元素的减少而减少。

数组可以存储基本数据类型,也可以存储引用数据类型,基本数据类型存储的是值,引用数据类型存储的是地址,而集合只能存储引用数据类型(也就是对象),其实集合中也可以存储基本数据类型,但是在存储的时候会自动装箱变成对象。

有了集合不意味着我们要抛弃数组,如果需要存储的元素个数是固定的,我们可以使用数组,而当存储的元素不固定,我们使用集合。

集合的种类

集合分为单列集合和双列集合。单列集合的根接口为collection,双列结合的根接口为map,两种集合都有基于哈希算法的集合类(hashmap和hashset),现在我们可能会有疑问,到底是双列集合基于单列集合还是单列集合基于双列集合呢,下面我们来看往集合hashmap和hashset添加元素的源码:

?
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
/*
*hashmap的put 方法
*/
 public v put(k key, v value) {
    return putval(hash(key), key, value, false, true);
  }
 
final v putval(int hash, k key, v value, boolean onlyifabsent,
          boolean evict) {
    node<k,v>[] tab; node<k,v> p; int n, i;
    if ((tab = table) == null || (n = tab.length) == 0)
      n = (tab = resize()).length;
    if ((p = tab[i = (n - 1) & hash]) == null)
      tab[i] = newnode(hash, key, value, null);
    else {
      node<k,v> e; k k;
      if (p.hash == hash &&
        ((k = p.key) == key || (key != null && key.equals(k))))
        e = p;
      else if (p instanceof treenode)
        e = ((treenode<k,v>)p).puttreeval(this, tab, hash, key, value);
      else {
        for (int bincount = 0; ; ++bincount) {
          if ((e = p.next) == null) {
            p.next = newnode(hash, key, value, null);
            if (bincount >= treeify_threshold - 1) // -1 for 1st
              treeifybin(tab, hash);
            break;
          }
          if (e.hash == hash &&
            ((k = e.key) == key || (key != null && key.equals(k))))
            break;
          p = e;
        }
      }
      if (e != null) { // existing mapping for key
        v oldvalue = e.value;
        if (!onlyifabsent || oldvalue == null)
          e.value = value;
        afternodeaccess(e);
        return oldvalue;
      }
    }
    ++modcount;
    if (++size > threshold)
      resize();
    afternodeinsertion(evict);
    return null;
  }
 
/*
* hashset 的add 方法
*/
public boolean add(e e) {
    return map.put(e, present)==null;
  }
// present是一个object 对象
 private static final object present = new object();

由上源码,我们可以看出,双列集合的put方法源码中并没有出现add方法,而单列集合的add源码只能怪出现了put;我们可以知道单列集合是基于双列集合实现的。其实道理很简单,单列集合每次添加元素,只要添加key就可以,而key也是双列集合元素的唯一标识,而其值value则由一个object对象代替并且隐藏,每次加入,输出元素都是隐藏单列结合的这个值, 底层基于双列集合,隐藏一列是很好实现的,而如果是单列集合要变成双列集合估计就会有很大的难度,就好比魔术师变魔术,魔术师变东西前肯定事先准备好要变的东西,只是将其隐藏,但是如果魔术师变魔术是真的从无到有,那我估计他就是神仙了,想要什么就变出来什么便是。

单列集合

首先我们看单列结合的继承图,单列集合的根接口是collection,而list的实现类为arraylist、linkedlist、vector,set接口的实现类是hashset和treeset。

Java复习之集合框架总结

其次我们来看看各个集合的功能

list集合的特有功能概述

    * void add(int index,e element) //集合中添加元素
    * e remove(int index) //删除集合中index位置上的元素
    * e get(int index)  //获取集合中index位置上的元素
    * e set(int index,e element) 将index位置上的元素替换成 element

vector类特有功能

    * public void addelement(e obj)  添加元素
    * public e elementat(int index)  //获取元素
    * public enumeration elements()  //获取元素的枚举(迭代遍历的时候用到)

linkedlist类特有功能

    * public void addfirst(e e)及addlast(e e)  //集合头添加元素或者集合尾添加元素
    * public e getfirst()及getlast() //获取头元素 获取尾元素
    * public e removefirst()及public e removelast() //删除头元素删除尾元素
    * public e get(int index);//获取index元素

根据上述linkedlist的功能,我们可以模拟栈获取队列的数据结构,栈是先进后出,队列为先进先出。

?
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
/**
 * 模拟的栈对象
 * @author 毛麒添
 * 底层还是使用linkedlist实现
 * 如果实现队列只需要将出栈变为 removefirst
 */
public class stack {
 
  private linkedlist linklist=new linkedlist();
  
  /**
   * 进栈的方法
   * @param obj
   */
  public void in(object obj){
    linklist.addlast(obj);
  }
  
  /**
   * 出栈
   */
  public object out(){
    return linklist.removelast();
  }
  /**
   * 栈是否为空
   * @return
   */
  public boolean isempty(){
    return linklist.isempty();
  }
}
 
 
//集合的三种迭代(遍历集合)删除
arraylist<string> list=new arraylist();
    list.add("a");
    list.add("b");
    list.add("c");
    list.add("world");
//1,普通for循环删除元素
for(int i=0;i<list,size();i++){//删除元素b
   if("b".equals(list.get(i))){
    list.remove(i--);// i-- 当集合中有重复元素的时候保证要删除的重复元素被删除
 }
}
//2.使用迭代器遍历集合
iterator<string> it=list.iterator();
while(it.hasnext){
  if("b".equals(it.next())){
   it.remove();//这里必须使用迭代器的删除方法,而不能使用集合的删除方法,否则会出现并发修改异常(concurrentmodificationexception)
 }
}
 
//使用增强for循环来进行删除
for (string str:list){
   if("b".equals(str)){
    list.remove("b");//增强for循环底层依赖的是迭代器,而这里删除使用的依旧是集合的删除方法,同理肯定是会出现并发修改异常(concurrentmodificationexception),所以增强for循环一直能用来遍历集合,不能对集合的元素进行删除。
 }
}

接下里我们看set集合,我们知道set 集合存储无序,无索引,不可以存储重复的对象;我们使用set集合都是需要去掉重复元素的, 如果在存储的时候逐个equals()比较, 效率较低,哈希算法提高了去重复的效率, 降低了使用equals()方法的次数,其中hashset底层基于哈希算法,当hashset调用add方法存储对象,先调用对象的hashcode()方法得到一个哈希值, 然后在集合中查找是否有哈希值相同的对象,如果没有哈希值相同的对象就直接存入集合,如果有哈希值相同的对象, 就和哈希值相同的对象逐个进行equals()比较,比较结果为false就存入, true则不存。下面给出hashset存储自定义对象的一个例子,自定义对象需要重写hashcode()和equals()方法。

?
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
74
75
76
/**
 * 自定义对象
 * @author 毛麒添
 * hashset 使用的bean 重写了equals和hashcode方法
 */
public class person1 {
  private string name;
  private int age;
  public person1() {
    super();
    // todo auto-generated constructor stub
  }
  public person1(string name, int age) {
    super();
    this.name = name;
    this.age = age;
  }
  public string getname() {
    return name;
  }
  public void setname(string name) {
    this.name = name;
  }
  public int getage() {
    return age;
  }
  public void setage(int age) {
    this.age = age;
  }
  @override
  public string tostring() {
    return "person1 [name=" + name + ", age=" + age + "]";
  }
  
  //使hashset存储元素唯一
  @override
  public int hashcode() {
    final int prime = 31;
    int result = 1;
    result = prime * result + age;
    result = prime * result + ((name == null) ? 0 : name.hashcode());
    return result;
  }
  @override
  public boolean equals(object obj) {
    system.out.println("equals调用了");
    if (this == obj)
      return true;
    if (obj == null)
      return false;
    if (getclass() != obj.getclass())
      return false;
    person1 other = (person1) obj;
    if (age != other.age)
      return false;
    if (name == null) {
      if (other.name != null)
        return false;
    } else if (!name.equals(other.name))
      return false;
    return true;
  
}
 
public class demo1_hashset {
 
  public static void main(string[] args) {
    hashset<person1> hs=new hashset<person1>();
    hs.add(new person1("张三", 23));
    hs.add(new person1("张三", 23));
    hs.add(new person1("李四", 24));
    hs.add(new person1("李四", 24));
    hs.add(new person1("李四", 24));
    hs.add(new person1("李四", 24));
    system.out.println(hs);
  }

运行结果如图,达到了不存储重复自定义对象的目的。其实hashset的下面还有一个linkedhashset,底层是链表实现的,是set中唯一一个能保证怎么存就怎么取的集合对象,是hashset的子类,保证元素唯一,与hashset原理一样,这里就不多说了。

Java复习之集合框架总结

最后是treeset集合,该集合是用来进行排序的,同样也可以保证元素的唯一,可以指定一个顺序, 对象存入之后会按照指定的顺序排列。

指定排序有两种实现:

comparable:集合加入自定义对象的时候,自定义对象需要实现comparable接口,

  • 实现接口的抽象方法中返回0,则集合中只有一个元素
  • 返回正数,则集合中怎么存则怎么取,
  • 返回负数,集合倒序存储

comparator(比较器):

  • 创建treeset的时候可以制定 一个comparator
  • 如果传入了comparator的子类对象, 那么treeset就会按照比较器中的顺序排序
  • add()方法内部会自动调用comparator接口中compare()方法排序
  • 调用的对象是compare方法的第一个参数,集合中的对象是compare方法的第二个参数

原理:

  • treeset底层二叉排序树
  • 返回小的数字存储在树的左边(负数),大的存储在右边(正数),相等则不存(等于0)
  • 在treeset集合中如何存取元素取决于compareto()方法的返回值

下面来看例子:

?
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
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
/**
 * 自定义对象 用于treeset
 * @author 毛麒添
 *
 */
public class person implements comparable<person>{
 
  private string name;
  private int age;
  
  public person(){
    super();
  }
  public person(string name, int age) {
    super();
    this.name = name;
    this.age = age;
  }
  public string getname() {
    return name;
  }
  public void setname(string name) {
    this.name = name;
  }
  public int getage() {
    return age;
  }
  public void setage(int age) {
    this.age = age;
  }
  @override
  public string tostring() {
    return "person [name=" + name + ", age=" + age + "]";
  }
  
  @override
  public boolean equals(object obj) {
    person p=(person) obj;
    return this.name.equals(p.name)&&this.age==p.age;
  }
  
  @override
  public int hashcode() {
    // todo auto-generated method stub
    return 1;
  }
  
  /*//按照年龄进行排序(treeset)
  @override
  public int compareto(person o) {
    int number=this.age-o.age;//年龄是比较的主要条件
    //当年龄一样的时候比较名字来确定排序
    return number==0?this.name.compareto(o.name):number;
  }*/
  
  //按照姓名进行排序(treeset)
    @override
    public int compareto(person o) {
      int number=this.name.compareto(o.name);//姓名是比较的主要条件
      //当姓名一样的时候比年龄来确定排序
      return number==0?this.age- o.age:number;
    }
    
    //按照姓名长度进行排序(treeset)
        /*@override
        public int compareto(person o) {
          int length=this.name.length()-o.name.length();//姓名长度比较的次要条件
          int number=length==0?this.name.compareto(o.name):length;//姓名是比较的次要条件
          //比年龄也是次要条件
          return number==0?this.age- o.age:number;
        }*/
}
 
/**
 *
 * @author 毛麒添
 * treeset集合
 * 集合加入自定义对象的时候,自定义对象需要实现comparable接口,
 * 实现接口的抽象方法中返回0,则集合中只有一个元素
 * 返回正数,则集合中怎么存则怎么取,
 * 返回负数,集合倒序存储
 *
 * 将字符按照长度来进行排序在treeset中,需要使用有比较的构造方法进行比较。
 */
public class demo_treeset {
 
  public static void main(string[] args) {
    // todo auto-generated method stub
    demo1();//整数存取排序
    demo2();//自定义对象排序
    
    //将字符按照长度来进行排序在treeset中
    treeset<string> strlenset=new treeset<string>(new comparebylen());
    strlenset.add("aaaaa");
    strlenset.add("lol");
    strlenset.add("wrc");
    strlenset.add("wc");
    strlenset.add("b");
    strlenset.add("wnnnnnnnnnnn");
    
    system.out.println(strlenset);
  }
 
  private static void demo2() {
    treeset<person> ptreeset=new treeset<person>();
    
    ptreeset.add(new person("李四",12));
    ptreeset.add(new person("李四",16));
    ptreeset.add(new person("李青",16));
    ptreeset.add(new person("王五",19));
    ptreeset.add(new person("赵六",22));
    
    system.out.println(ptreeset);
  }
 
  private static void demo1() {
    treeset< integer> treeset=new treeset<integer>();
    treeset.add(1);
    treeset.add(1);
    treeset.add(1);
    treeset.add(3);
    treeset.add(3);
    treeset.add(3);
    treeset.add(2);
    treeset.add(2);
    treeset.add(2);
    
    system.out.println(treeset);
  }
 
}
 
//treeset 构造器,实现compare对需要存储的字符串进行比较
class comparebylen implements comparator<string>{
 
  //按照字符串的长度进行比较,该方法的返回值和继承comparable的compareto方法返回值同理。
  @override
  public int compare(string o1, string o2) {
    int num=o1.length()-o2.length();//以长度为主要条件
    return num==0?o1.compareto(o2):num;//内容为次要条件
  }
  
}

下面是运行截图,其中compareto的实现方法对几种条件排序进行了实现,具体可以查看person自定义类中的实现。

Java复习之集合框架总结

单列集合复习就到这里吧。

双列集合

同样的,在复习双列结合之前我们先看看双列集合的继承图。

Java复习之集合框架总结

map集合的功能梳理:

添加功能

  • v put(k key,v value):添加元素。
  • 如果键是第一次存储,就直接存储元素,返回null
  • 如果键不是第一次存在,就用值把以前的值替换掉,返回以前的值

删除功能

  • void clear():移除所有的键值对元素
  • v remove(object key):根据键删除键值对元素,并把值返回

判断功能

  • boolean containskey(object key):判断集合是否包含指定的键
  • boolean containsvalue(object value):判断集合是否包含指定的值
  • boolean isempty():判断集合是否为空

获取功能

  • set<map.entry<k,v>> entryset():
  • v get(object key):根据键获取值
  • set<k> keyset():获取集合中所有键的集合
  • collection<v> values():获取集合中所有值的集合

长度功能

  • int size():返回集合中的键值对的个数

map类集合也有三种遍历方式:

  • 使用迭代器进行遍历
  • 使用增强for循环来进行遍历
  • 使用map.entry来遍历集合中的元素

下面我们来看看如何实现上面三种遍历方式

?
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
/**
 *
 * @author 毛麒添
 * map 集合的遍历
 */
public class demo {
 
  public static void main(string[] args) {
    map<string ,integer> map=new hashmap<string, integer>();
    map.put("张三", 18);
    map.put("李四", 19);
    map.put("王五", 20);
    map.put("赵六", 21);
    
    //使用迭代器进行遍历
    /*set<string> keyset = map.keyset();//获取所有key的集合
    iterator<string> iterator = keyset.iterator();
    while(iterator.hasnext()){
      string key = iterator.next();
      integer i=map.get(key);
      system.out.println(i);
    }*/
    //使用增强for循环来进行遍历
    for (string key :map.keyset()) {
      integer integer = map.get(key);
      system.out.println(integer);
    }
/*---------------------------使用map.entry来遍历集合中的元素--------------------------*/
    set<map.entry<string,integer>> en=map.entryset();////获取所有的键值对象的集合
    /*//使用迭代器来遍历
    iterator<entry<string, integer>> iterator = en.iterator();
    while(iterator.hasnext()){
      entry<string, integer> e=iterator.next();//获取键值对对象
      string key = e.getkey();//根据键值对对象获取键
      integer value = e.getvalue();//根据键值对对象获取值
      system.out.print(key);
      system.out.println(value);
    }*/
    //使用增强for循环来遍历
    for (entry<string, integer> entry : en) {
      string key = entry.getkey();
      integer value = entry.getvalue();
      system.out.print(key);
      system.out.println(value);
    }
/*---------------------------使用map.entry来遍历集合中的元素--------------------------*/
  }
}

linkhashmap与linkhashset一样,怎么存怎么取,保证元素唯一(key 是唯一判定值),由于保证元素唯一,其性能肯定会低一些,这里就不细说了。

treemap是双列集合,其实他和treeset是很像的,但是双列集合的键是唯一标识,所以treemap排序的是每个元素的键。对于存储自定义对象排序,它也有comparable和comparator,下面我们来看例子

?
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
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
/**
 *
 * @author 毛麒添
 * treemap
 * 通treeset 原理,存取自定义对象也需要继承comparable结构,
 * 或者实现比较器comparator
 */
public class demo6_treemap {
 
  public static void main(string[] args) {
    treemap<student, string> tm=new treemap<student, string>(new comparator<student>() {
 
      @override
      public int compare(student s1, student s2) {
        int num=s1.getname().compareto(s2.getname());//以姓名作为主要比较条件
        return num==0?s1.getage()-s2.getage():num;
      }
    });
    tm.put(new student("张三",13),"杭州");
    tm.put(new student("张三",14), "贺州");
    tm.put(new student("王五",15), "广州");
    tm.put(new student("赵六",16), "深圳");
    
    system.out.println(tm);
  }
 
}
 
/**
 * 自定义对象
 * @author 毛麒添
 * hashmap 存储对象 与 hashset 同理 需要重写 hashcode 和equals 方法
 * treemap 实现 comparable接口
 */
public class student implements comparable<student>{
 
  private int age;
  private string name;
  
  public student(){
    super();
  }
  public student(string name,int age){
    this.name=name;
    this.age=age;
  }
  public int getage() {
    return age;
  }
  public void setage(int age) {
    this.age = age;
  }
  public string getname() {
    return name;
  }
  public void setname(string name) {
    this.name = name;
  }
  
  
  @override
  public string tostring() {
    return "student [age=" + age + ", name=" + name + "]";
  }
  @override
  public int hashcode() {
    final int prime = 31;
    int result = 1;
    result = prime * result + age;
    result = prime * result + ((name == null) ? 0 : name.hashcode());
    return result;
  }
  @override
  public boolean equals(object obj) {
    if (this == obj)
      return true;
    if (obj == null)
      return false;
    if (getclass() != obj.getclass())
      return false;
    student other = (student) obj;
    if (age != other.age)
      return false;
    if (name == null) {
      if (other.name != null)
        return false;
    } else if (!name.equals(other.name))
      return false;
    return true;
  }
  @override
  public int compareto(student o) {
    int num =this.age-o.age;//以年龄为主要条件
    return num==0?this.name.compareto(o.name):num;//姓名作为次要条件
  
}

到这里,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
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
74
75
76
77
78
79
80
81
82
83
84
85
/**
 *
 * @author 毛麒添
 * 模拟斗地主洗牌和发牌,牌排序
 * 买一副扑克
 * 洗牌
 * 发牌
 * 看牌
 */
public class doudizhu_progress {
 
  public static void main(string[] args) {
    // todo auto-generated method stub
    //构造一副扑克牌
    string[] number={"3","4","5","6","7","8","9","10","j","q","k","a","2"};
    string[]color={"黑桃","红桃","梅花","方块"};
    hashmap<integer, string> pokermap=new hashmap<integer, string>();//存放牌的map
    arraylist<integer> list=new arraylist<integer>();//存放牌的索引
    int index=0; //索引
    for (string s1 : number) {
      for (string s2 : color) {
        pokermap.put(index,s2.concat(s1));
        list.add(index);
        index++;
      
      }
    }
    //加入大小王
    pokermap.put(index,"小王");
    list.add(index);
    index++;
    pokermap.put(index,"大王");
    list.add(index);
    
    //洗牌
    collections.shuffle(list);
    //system.out.println(list);
    
    //发牌,3个人玩 加上底牌3张 使用treeset 来存放索引,并自动对索引排好序
    treeset<integer> mao=new treeset<integer>();
    treeset<integer> li=new treeset<integer>();
    treeset<integer> huang=new treeset<integer>();
    treeset<integer> dipai=new treeset<integer>();
    
    for(int i=0;i<list.size();i++){
      if(i>=list.size()-3){//最后三张牌,作为底牌
        dipai.add(list.get(i));
      }else if(i%3==0){
        mao.add(list.get(i));
      }else if(i%3==1){
        li.add(list.get(i));
      }else {
        huang.add(list.get(i));
      }
    }
    
    //看牌
    lookpoker(pokermap,mao,"mao");
    lookpoker(pokermap,li,"li");
    lookpoker(pokermap,huang,"huang");
    lookpoker(pokermap,dipai,"底牌");
  }
 
  /**
   * 看牌的方法
   * @param pokermap 存放牌的map
   * @param mao 该玩家的牌的索引集合
   * @param name 玩家名字
   */
  private static void lookpoker(hashmap<integer, string> pokermap,
      treeset<integer> mao, string name) {
    if(name.equals("底牌")){
      system.out.print("地主"+name+"的牌是:");
    }else{
      system.out.print("玩家"+name+"的牌是:");
    }
    for (integer integer : mao) {
      system.out.print(pokermap.get(integer)+" ");
    }
    
    system.out.println();
    
  }
 
}

运行截图:

Java复习之集合框架总结

写在最后:

如果你看到这里,估计你也温故知新了吧,那就这样吧,这篇又臭又长的文章就到这里啦。希望对大家的学习有所帮助,也希望大家多多支持服务器之家。

原文链接:http://www.jianshu.com/p/972dfad0c95b