一、引言

数组的缺点:

  1. 数组长度初始化时必须指定,而且一旦指定,不能修改;
  2. 数组中保存的元素必须为同一类型的元素;
  3. 使用数组进行增删时复杂度较高

List和Set继承关系图:
image.png
Map的继承关系图:
image.png
Collection接口有两个重要的子接口:List和Set,他两的实现子类都是单列集合。
Map接口的实现子类都是双列集合。

二、API介绍

2.1 Collection接口

  1. public interface Collection<E> extends Iterable<E>

2.1.1 本接口的实现类的特点

image.png

2.1.2 本接口常用方法

image.png
由于接口不能被实例化,只有实现了接口的类才能被实例化。
List中存储的是对象,而不是基本数据类型。

  1. List list = new ArrayList();
  2. list.add("tom");
  3. list.add(10); // 这句相当于list.add(new Integer(10)),有个自动装箱的过程
  4. list.add(true);

2.1.3 本接口遍历方式

1、迭代器遍历
image.png
迭代器执行原理:
image.png
迭代器实例:
image.png
IDEA中显示所有快捷键可以按Ctrl + J,即可跳出所有的快捷键。
注意:当退出while循环后,这时iterator迭代器指向最后的元素。如果强行再执行iterator.next(),会报NoSuchElementException异常。如果希望再次遍历,需要重置迭代器,即iterator = col.iterator()
2、增强for循环
image.png
注意:增强for底层仍然是迭代器,查看源码就知道了。


2.2 List接口

2.2.1 List接口介绍

image.png
List接口的父接口和实现类:
image.png
注意:List集合中所有元素的添加顺序和取出顺序一致,且元素可重复。索引从0开始。

2.2.2 List接口常用API

image.png

2.2.3 List遍历方式

三种遍历方式都可以,即List
image.png

2.3 ArrayList

image.png
ArrayList是线程不安全的,源码中方法之前并没有加关键字synchronized关键字,此关键字专门做线程互斥的。

2.3.1 ArrayList扩容机制

重点,难点
image.png
1、

2.3.2 ArrayList底层分析

在进行Debug之前,在IDEA中设置以下debug的格式,不然进不去源码。
image.png
然后来到Data Views下的Java,去掉图中的√,可以看到好多信息,值为null的也能看到。
image.png

①、无参构造建对象

1、创建一个测试方法,进行Debug调试

  1. // 使用ArrayList的无参构造器创建对象
  2. ArrayList<Integer> list = new ArrayList<>();
  3. for(int i = 0; i < 12; i++){
  4. list.add(i);
  5. }

Ⅰ、第一次扩容

程序跳转到ArrayList.java中的164行,即ArrayList的无参构造方法,DEFAULTCAPACITY_EMPTY_ELEMENTDATA其实是本文件中的一个Object类型的空数组,elementData也是一个Object类型的空数组,即初始化elementData的容量为0,

  1. public ArrayList() {
  2. this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
  3. }

👇

  1. private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};
  2. transient Object[] elementData;

image.png
然后程序会对int基本数据类型进行装箱,程序会跳转到Integer.java文件的valueOf方法,valueOf方法执行完后,会再次回来for里面,然后再进去到add方法。
image.png
进入到add方法后,第一行有一个确定扩容的方法,进去看看。
image.png
来到ensureCapacityInternal方法,进入if中,常量DEFAULT_CAPACITY为10(下下图所示),if后得到的minCapacity为10,这是第一次扩容。
image.png
image.png
程序来到ensureExplicitCapacity方法,modCount是用来记录集合被修改的次数,为了防止有多个线程同时去修改容量,初始值为0。if中表示如果最小容量减去当前数组实际的大小>0,则真正执行grow方法去扩容。
image.png
image.png
扩容:程序来到grow()方法,oldCapacity为原始容量(初始为0),newCapacity为扩容后的容量,就是对原始容量1.5倍扩容,其实第一次传进来的oldCapacity为0,那1.5倍扩后,newCapacity还是为0,第一次有点特殊。

  • 如果得到的新容量newCapacity比minCapacity(值为10),那新容量就干脆使用minCapacity,所以第一次进来并不是按照1.5倍扩容的,而是把minCapacity赋值给newCapacity,那么从第二次开始就用1.5倍扩容机制。
  • 如果当前新容量比最大值MAX_ARRAY_SIZE还要大,就去hugeCapacity()方法中去处理。
  • 最后一行代码,执行扩容,使用的是数组复制,这种方式可以保留原来数组中的数据,比如10个数据扩容到15,那先复制原数组中前10个数据到新数组,再在后面增加5个空间,值为null。

image.pngimage.png
最终,得到的数组elementData里面包含10个数据,全为null值,第一次扩容成功。
image.png
grow方法执行完后,向上返回,来到add方法中第二行,此时elementData数组容量为10,size为0,此行代码表示将e这个元素放到elementData数组的0下标位置,size再自增。add方法的返回值为true,表示添加成功。
image.png

第一次的minCapacity由1变成10,来自下面这一步 minCapacity = Math._max_(**_DEFAULT_CAPACITY_**, minCapacity); 而 扩容的前提是下面这个条件满足,即当前至少需要的容量比此时数组elementData的实际容量还要大,就得去grow()方法扩容elementData了。 **if **(minCapacity - **elementData**.**length **> 0)

Ⅱ、第二次扩容

当第一次扩的容量10个全部用完后,如果再次往里面添加,那么又会进入到grow()方法里面去扩容。
image.png
👇
image.png

Ⅲ、测试代码
  1. @Test
  2. public void test01() {
  3. ArrayList<Integer> list = new ArrayList<>();
  4. // 第一次扩容,容量为10
  5. for(int i = 1; i <= 10; i++){
  6. list.add(i);
  7. }
  8. // 第二次扩容,容量为10*1.5=15
  9. for(int i = 11; i <= 15; i++){
  10. list.add(i);
  11. }
  12. // 第三次扩容,容量为15*1.5=22
  13. list.add(16);
  14. list.add(17);
  15. }

②、指定大小构造器

如果使用的是指定大小的构造器来建对象,则初始的elementData数组容量为指定的大小,如果需要扩容,则直接扩容elementData为1.5倍。

  1. ArrayList<Integer> list = new ArrayList<>(8);

Debug,程序跳转到ArrayList的有参构造器,传入的参数为8,则只会执行if中的语句,创建了一个容量为8的Object类型的数组,并赋值给elementData。那么此时得到的list的size还是为0,只不过它的容量为8而已。
image.png
image.png
同样,对其执行添加操作,过程如下:
image.png
image.png
image.png
然后,程序向上返回,添加,返回true。
当list的size刚好达到初始设置容量8了,如果再往里面添加,则会采取1.5倍扩容了。
image.png
image.png
image.png
image.png
向上返回,执行添加操作,返回true。


2.4 Vector

Vector是List接口的实现子类,

image.png
如果某个集合在操作过程中只有一个线程来操作,那么此时用ArrayList是最合适的,因为效率比较高。如果某个集合在操作时有好多个线程来操作,那么建议使用Vector,因为它是线程安全的。

2.4.1 Vector 扩容机制

image.png

2.4.2 Vector底层分析

①、无参构造

对以下代码进行Debug,分析Vector的扩容机制。

  1. @Test
  2. public void test01(){
  3. Vector<Object> vector = new Vector<>();
  4. for (int i = 0; i < 10; i++) {
  5. vector.add(i);
  6. }
  7. }

1、程序首先跳转到Vector类的无参构造,无参构造内this(10)会调用Vector的单参构造,单参构造内又会去调用双参构造,说明Vector初始时如果是无参构造创建对象,那么默认容量就是10。
image.png
👇
image.png
2、接着程序向上返回,回到for循环里面添加元素。然后,vector也会来到valueOf()方法,对基本数据类型自动装箱,转成引用数据类型返回,回到for循环,下一次就跳转到add操作了。
image.png
image.png
3、来到add()方法,modCount表示对集合操作的次数,程序跳转到ensureCapacityHelper方法。
image.png
4、ensureCapacityHelper内部也有一个grow方法,执行真正的扩容操作,由于刚开始elementData数组就有初始值10,当vector容量没有达到10时,不会执行grow方法。
image.png
5、程序返回到add方法中,执行添加操作,并返回true。
image.png


6、当vector中刚好有10个元素了,如果再往里面添加,则会执行扩容机制了。此时至少需要11个空间来存放,单目前只有10个空间,故要进入grow方法中进行扩容。
image.png
image.png
image.png
7、grow方法如下,每次量满后,再添加的话都是两倍扩容。
image.png


②、有参构造一

  1. Vector<Object> vector = new Vector<>(6);
  2. //上面这行代码走的是如下构造器

跟上面无参构造类似,也是达到容量(无参初始容量是10,有参初始容量为括号中的数)后,再执行添加操作,会调用grow()方法进行扩容(容量够时,是不会调用grow方法的)。
image.png
👇
image.png
👇
image.png
👇
image.png
👇
image.png
👇
image.png
以上流程就是当初始容量设为6,且vector中容量已满,再往内部添加第7个时,就会调用grow()方法进行两倍扩容,容量扩到12。

③、有参构造二

  1. // 创建对象时直接指定初始容量为6,每次扩容大小为5
  2. Vector<Object> vector = new Vector<>(6, 5);

Debug过程如下所示:
1、调用如下双参构造器
image.png
2、程序来到for里面,执行add操作,首先将基本数据类型自动装箱。
image.png
image.png
image.png
3、进入add方法,初始容量为6,未填满之前,不用扩容,即grow方法不执行。
image.png
image.png
4、添加元素到elementData数组,并返回true。
image.png
5、添加6个元素后,再来看看如何扩容。
image.png
image.png
image.png
6、添加第7个元素时,至少需要7个容量,但elementData数组长度为6,就得调用grow方法扩容。由于刚开始使用的是双参构造器,指定了每次扩容的大小,故扩容后的大小不是原容量的两倍了,而是oldCapacity+capacityIncrement的值(具体看三目运算符)
image.png
image.png


2.5 LinkedList

2.5.1 简介

image.png
image.png
手写链表,定义一个类,表示Node对象

  1. class Node{
  2. public Object item;
  3. public Node next;
  4. public Node pre;
  5. public Node(Object name) {
  6. this.item = name;
  7. }
  8. }

编写测试类

  1. @Test
  2. public void test(){
  3. Node jack = new Node("jack");
  4. Node tom = new Node("tom");
  5. Node mary = new Node("mary");
  6. jack.next = tom;
  7. tom.next = mary;
  8. mary.pre = tom;
  9. tom.pre = jack;
  10. Node first = jack;
  11. Node last = mary;
  12. while(true){
  13. if(first == null) break;
  14. sout(first);
  15. first = first.next;
  16. }
  17. while(true){
  18. if(last == null) break;
  19. sout(last);
  20. last = last.pre;
  21. }
  22. }

2.5.2 LinkedList底层

①、添加操作

  1. @Test
  2. public void test01(){
  3. LinkedList<Object> list = new LinkedList<>();
  4. list.add(10);
  5. list.add(100);
  6. }

分析以上代码执行过程,了解链表。
1、首先调用LinkedList的无参构造器,得到的list对象封装了四个属性,参考下图红色方框。
image.png
2、接着,对基本类型进行自动装箱并返回,再调用add方法
image.png
3、来到add方法,add方法内调用linkLast方法,linkLast中有一个创建结点newNode的过程(一个结点含有三个属性,item、next和prev),此时newNode的值item为e,next和prev都是null。那么插入第一个结点的时候,first和last都指向这个newNode,size表示此时链表长度,modCount表示修改链表的次数(防止多个线程一起操作链表)。
image.png
image.png
4、回到add方法,返回true,第一个结点添加操作结束。
image.png
5、继续添加第二个结点,自动装箱,调用add方法。
image.png
6、add内调用linkLast方法执行插入操作。
image.png
image.png
7、那么此时first指向第一个元素,last指向第二个(最后一个)元素,第一个元素的prev为空,最后一个元素为next为空。
image.png

②、删除操作

  1. @Test
  2. public void test01(){
  3. LinkedList<Object> list = new LinkedList<>();
  4. list.add(10);
  5. list.add(100);
  6. list.add(50);
  7. list.add(10);
  8. // 默认remove()方法删除链表第一个
  9. list.remove();
  10. list.remove(2);
  11. }

1、在remove()方法处断点,remove方法会去调用removeFirst方法,此方法又会去调用unlinkFirst(),传入参数为首元节点(是个Node类型)
image.png
2、unlinkFirst(Node f)方法如下,返回值为所删节点的item值,不断往上返回。
image.png
image.png
3、按照下标删除,首先判断下标index是否满足条件。

  1. list.remove(2);

image.png
4、检查完index后,得调用unlink()方法,但括号里面还有一个方法,node(int index):表示找到下标为index的结点。查找过程如下面大图所示:

  • 如果下标index 小于 表长一半,则令x指向first,不断右移x,直到移到index位置;
  • 如果下标index 大于等于 表长一半,则令x指向last,不断左移x,直到移到index位置;

image.png
image.png
image.png
5、执行unlink方法,删除x结点,返回值为删除节点的值。
image.png
image.png

2.5.3 ArrayList vs LinkedList

image.png


2.6 Set接口

2.6.1 基本介绍

image.png
image.png
image.png
image.png

2.6.2 基本使用

  1. @Test
  2. public void test(){
  3. Set<Object> set = new HashSet<>();
  4. set.add("zhangsan");
  5. set.add("lisi");
  6. set.add("wangwu");
  7. set.add("zhaoliu");
  8. set.add(zhangsan);
  9. set.add(null);
  10. set.add(null);
  11. System.out.println(set);
  12. }
  13. // 运行结果:[null, lisi, zhaoliu, zhangsan, wangwu]

注意:

  • set接口的实现类对象,不能存放重复的元素,可以添加null值,但只能添加一个;
  • se接口的实现类对象,存放数据是无序的【添加的顺序和取出的顺序不同】;
  • 虽然取出的顺序不是添加的顺序,但是它是固定的,即每次遍历set得到的结果都相同;
  • set中没有索引index,不能索引遍历(传统for循环),set中没有get(index)这个方法。
  1. // set的遍历方式:迭代器 + 增强for
  2. Iterator iterator = set.iterator();
  3. while(iterator.hasNext()) {
  4. Object obj = iterator.next();
  5. sout(obj);
  6. }
  7. for(Object obj : set){
  8. sout(obj);
  9. }

2.7 HashSet

image.png
在执行add方法后,会返回一个boolean值,如果添加成功,返回true,否则,返回false。

  1. Set<Object> set = new HashSet<>();
  2. System.out.println(set.add("张三"));
  3. System.out.println(set.add("李四"));
  4. System.out.println(set.add("王五"));
  5. System.out.println(set.add("马六"));
  6. System.out.println(set.add("Tom"));
  7. System.out.println(set.add("Cat"));
  8. System.out.println(set.add("张三")); // false ,其余全是true
  9. System.out.println(set); // [李四, 张三, 马六, Tom, 王五, Cat]

由于HashSet底层是HashMap,HashMap底层是(数组+链表+红黑树),下面先来模拟一下HashSet的底层:
javaSE之集合 - 图96

  1. // 结点,存储数据
  2. class Node{
  3. Object item;
  4. Node next;
  5. public Node(Object item, Node next){
  6. this.item = item;
  7. this.next = next;
  8. }
  9. }
  10. public class HashSetStructure{
  11. @Test
  12. public void test(){
  13. // 创建一个数组,数组类型是Node[]
  14. Node[] table = new Node[16];
  15. // 创建结点
  16. Node tom = New Node("tom", null);
  17. table[1] = tom;
  18. Node jack = New Node("jack", null);
  19. tom.next = jack;
  20. Node rose = New Node("rose", null);
  21. jack.next = rose;
  22. Node lucy = New Node("lucy", null);
  23. rose.next = lucy;
  24. }
  25. }

javaSE之集合 - 图97

2.7.1 底层分析(重难点)

image.png
对以下代码进行Debug分析。

  1. @Test
  2. public void test(){
  3. Set<Object> set = new HashSet<>();
  4. System.out.println(set.add("张三"));
  5. System.out.println(set.add("李四"));
  6. System.out.println(set.add("王五"));
  7. System.out.println(set.add("张三"));
  8. System.out.println(set.add("Tom"));
  9. System.out.println(set.add("Cat"));
  10. System.out.println(set.add("张三"));
  11. System.out.println(set);
  12. }

①、添加第一个

1、首先HashSet初始化,其实底层是HashMap。
image.png
2、返回到主方法,开始添加值。来到add方法,首先调用put方法,key为”张三”,value为定义的Object类型对象PRESENT。put方法中调用putVal方法,这里有一个hash(key)

  1. static final int hash(Object key) {
  2. int h;
  3. // 这么写的目的是为了让不同的key尽量得到不同的哈希值,避免碰撞做了这个算法
  4. // 得到的哈希值并不是key的hashCode,还对hashCode进行^ (h >>> 16)这一处的
  5. return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
  6. }
  • 如果传进来的key不为null,则求出key的hashcode并返回;
  • 如果传进来的key为null,则返回0;

map.put()方法返回的值如果是空,则代表插入成功,则add方法往上返回true;否则,如果put方法返回的不是null,则代表插入失败,那么add方法往上返回false。
image.png
image.png
image.png
3、来到V putVal()方法,这个方法是精髓。

  1. /**
  2. hash:hash(key)返回的哈希值;key:添加的值,如"张三";value:Object类型变量
  3. key:插入的值,如“张三”
  4. value:Object类型
  5. */
  6. final V putVal(int hash, K key, V value, boolean onlyIfAbsent, boolean evict) {
  7. // 定义辅助变量,
  8. Node<K,V>[] tab;
  9. Node<K,V> p;
  10. int n, i;
  11. //table是HashMap定义的属性,是放Node结点的数组,初始时为null
  12. // 如果当前table为null或大小为0,则执行resize进行第一次扩容,扩容后大小为16
  13. if ((tab = table) == null || (n = tab.length) == 0)
  14. // 第一次resize()返回的值为16,tab是一个容量为16的Node型数组,n为16
  15. n = (tab = resize()).length;
  16. // 根据key得到的hash去计算该key应该存放在table表的哪个索引位置i
  17. // 并把此索引位置i的对象赋给辅助变量p
  18. if ((p = tab[i = (n - 1) & hash]) == null)
  19. // 如果p为null,表示此索引还未存放元素,就创建一个Node结点,把结点放在tab[i]位置
  20. // 该结点hash为key的哈希值,key为“张三”,value为Object类的值,next指向null
  21. tab[i] = newNode(hash, key, value, null);
  22. else {
  23. ....添加第一个元素时这里不会执行,我把它省了。
  24. }
  25. if (e != null) { // existing mapping for key
  26. V oldValue = e.value;
  27. if (!onlyIfAbsent || oldValue == null)
  28. e.value = value;
  29. afterNodeAccess(e);
  30. return oldValue;
  31. }
  32. }
  33. // 修改次数加一
  34. ++modCount;
  35. // size加1,如果此时size达到threshold(第一次为12)时,执行resize进行扩容
  36. if (++size > threshold)
  37. resize();
  38. // 这是个空方法,在这里主要是为了让HashMap的子类去实现这个方法,
  39. afterNodeInsertion(evict);
  40. // 返回null代表插入成功
  41. return null;
  42. }

上面代码调用的方法如下:

  1. final Node<K,V>[] resize() {
  2. // 将table赋给一个Node类型数组oldTab,初始table为null
  3. Node<K,V>[] oldTab = table;
  4. // 第一次oldCap为0
  5. int oldCap = (oldTab == null) ? 0 : oldTab.length;
  6. // threshold是一个全局变量,初始为0
  7. int oldThr = threshold;
  8. int newCap, newThr = 0;
  9. // 以下if结构,第一次会进入到else中
  10. if (oldCap > 0) {
  11. if (oldCap >= MAXIMUM_CAPACITY) {
  12. threshold = Integer.MAX_VALUE;
  13. return oldTab;
  14. }
  15. else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
  16. oldCap >= DEFAULT_INITIAL_CAPACITY)
  17. newThr = oldThr << 1; // double threshold
  18. }
  19. else if (oldThr > 0) // initial capacity was placed in threshold
  20. newCap = oldThr;
  21. else { // zero initial threshold signifies using defaults
  22. // 将等式右边的常量(默认初始容量)16赋给newCap
  23. newCap = DEFAULT_INITIAL_CAPACITY;
  24. // 0.75 * 16 = 12,得到的newThr是12,即总共16个空间中,如果使用了12个的话,就要开始
  25. // 扩容了,防止多个线程同时执行添加操作,就会导致阻塞,所以就设置了个加载因子0.75.
  26. newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
  27. }
  28. if (newThr == 0) {
  29. float ft = (float)newCap * loadFactor;
  30. newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
  31. (int)ft : Integer.MAX_VALUE);
  32. }
  33. // 将12赋值给threshold,临界值
  34. threshold = newThr;
  35. @SuppressWarnings({"rawtypes","unchecked"})
  36. // newTab数组现在容量为16了,因为newCap的值为16
  37. Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
  38. // 把newTab赋值给table,此时table的容量也为16,不过全为null
  39. table = newTab;
  40. // oldTab为null,这里if不会执行,直接返回newTab
  41. if (oldTab != null) {
  42. for (int j = 0; j < oldCap; ++j) {
  43. Node<K,V> e;
  44. if ((e = oldTab[j]) != null) {
  45. oldTab[j] = null;
  46. if (e.next == null)
  47. newTab[e.hash & (newCap - 1)] = e;
  48. else if (e instanceof TreeNode)
  49. ((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
  50. else { // preserve order
  51. Node<K,V> loHead = null, loTail = null;
  52. Node<K,V> hiHead = null, hiTail = null;
  53. Node<K,V> next;
  54. do {
  55. next = e.next;
  56. if ((e.hash & oldCap) == 0) {
  57. if (loTail == null)
  58. loHead = e;
  59. else
  60. loTail.next = e;
  61. loTail = e;
  62. }
  63. else {
  64. if (hiTail == null)
  65. hiHead = e;
  66. else
  67. hiTail.next = e;
  68. hiTail = e;
  69. }
  70. } while ((e = next) != null);
  71. if (loTail != null) {
  72. loTail.next = null;
  73. newTab[j] = loHead;
  74. }
  75. if (hiTail != null) {
  76. hiTail.next = null;
  77. newTab[j + oldCap] = hiHead;
  78. }
  79. }
  80. }
  81. }
  82. }
  83. // 返回newTab,容量为16,值为null
  84. return newTab;
  85. }

newNode构造方法:

  1. // 创建一个新节点
  2. Node<K,V> newNode(int hash, K key, V value, Node<K,V> next) {
  3. return new Node<>(hash, key, value, next);
  4. }
  5. Node(int hash, K key, V value, Node<K,V> next) {
  6. this.hash = hash;
  7. this.key = key;
  8. this.value = value;
  9. this.next = next;
  10. }

②、添加第二个

以上是添加第一个元素时执行的流程,下面分析添加第二个元素时的操作:
1、来到HashSet的add方法,跳转到HashMap的put()方法,因为HashSet底层就是HashMap。put方法会去调用putVal方法,括号里面有个hash(key)表示将传入的值key转化成哈希值。
image.png
image.png
image.png
2、来到了putVal方法,流程如下,插入成功则返回true【以下隐藏了else】
image.png


③、添加重复元素

以上是添加第二个元素时执行的流程,下面分析添加重复元素时的操作:
1、继续插入”张三”元素,来到putVal方法:
image.png

  1. // p所指的那个tab[i]为空,执行if,否则,有元素了,就执行else
  2. if ((p = tab[i = (n - 1) & hash]) == null)
  3. tab[i] = newNode(hash, key, value, null);
  4. else {
  5. // 初始化变量
  6. Node<K,V> e; K k;
  7. // 如果p指向的tab[i]的hash值跟待插的值的hash值相等,且p指向的tab[i]的key值跟待插入的
  8. // key相等,或待插的key非空且equals那个p指向的tab[i]的key值,则把p的值赋给e
  9. if (p.hash == hash && ((k = p.key) == key || (key != null && key.equals(k))))
  10. e = p;
  11. else if (p instanceof TreeNode)
  12. e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
  13. //
  14. else {
  15. for (int binCount = 0; ; ++binCount) {
  16. if ((e = p.next) == null) {
  17. p.next = newNode(hash, key, value, null);
  18. if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
  19. treeifyBin(tab, hash);
  20. break;
  21. }
  22. if (e.hash == hash &&
  23. ((k = e.key) == key || (key != null && key.equals(k))))
  24. break;
  25. p = e;
  26. }
  27. }
  28. // 执行了这个if,说明map中已存在该元素,返回此tab[i]的value,即oldValue
  29. if (e != null) { // existing mapping for key
  30. V oldValue = e.value;
  31. if (!onlyIfAbsent || oldValue == null)
  32. e.value = value;
  33. afterNodeAccess(e);
  34. return oldValue;
  35. }
  36. }

2、由于putVal方法返回的是oldValue,那么下面的add方法就返回false了,插入失败。
image.png

小结一下: 在插入重复元素时,比较的规则如下【全在HashMap.java中的putVal方法中】:

  1. 先获取待插元素的哈希值hash
  2. 对哈希值进行运算,得出一个索引值i即为要存放在哈希表tab中的下标;
  3. 如果该位置tab[i]没有其他元素,则直接存放;如果tab[i]中已有其他元素,则需要进行hash值和equals判断,如果相等,则不再添加,否则不相等,则以链表的方式添加。

注意:equals方法可以由程序员重写来确定的,不能简单理解为就是比较他两的内容。因为每个类都可以有自己的equals方法,equals方法具体怎么比较,有自己来决定。举个例子,比如在添加一个Person对象时,你可以认为person的名字和年龄相同,就是同一个人,或者person的名字和薪水相同,就是同一个人。不能简单地理解为是字符串比较,因为String类重写了equals方法,如果两字符串内容相同,就是同一个String,但并不是所有的类都是按照这个标准来的。

HashSet的底层是HashMap。另外,第一次添加时,table数组会扩容到16,加载因子(loadFactor)为0.75,那么临界值threshold=16loadFactor=12,即如果table数组+链表中使用了临界值12个容量,就会扩容两倍,新的容量为162=32,那么新的临界值为320.75=24,依此类推。
在jdk 1.8中,如果一条链表(以数组为头结点)的元素个数到达TREEIFY_THRESHOLD(默认是8),并且table数组的大小 >=_*MIN_TREEIFY_CAPACITY
_(默认64),就会进行树化(红黑树),否则仍然采用数组的达到threshold后二倍扩容机制。


④、链表机制

现在来分析链表的连接机制。
想办法让添加的数据全部都连在同一个链表上,那么最关键的是算出来的hash(key)要一样的,然后得到同样的table索引i,只要保证hash值相同,添加数据的内容不同就可以连成一个链表。

  1. class Dog {
  2. private String name;
  3. private int age;
  4. public Dog(String name, int age){
  5. this.name = name;
  6. this.age = age;
  7. }
  8. @Override
  9. public int hashCode() {
  10. // 这里改成hashCode返回值是一样的,那每次添加Dog对象就可连在同一个链表
  11. return 10;
  12. }
  13. @Override
  14. public String toString() {
  15. return "Dog{" +
  16. "name='" + name + '\'' +
  17. ", age=" + age +
  18. '}';
  19. }
  20. }

看下putVal中的一段代码:

  1. // 待插结点通过hash值得到的下标索引如果此时已经有元素了(类似冲突),则执行else
  2. else {
  3. Node<K,V> e; K k;
  4. // 这个if判断的是p所指向的数组中下标为i位置(链表首元节点)和待插入的数据是否一模一样
  5. // ,如果一模一样,则执行这个if
  6. if (p.hash == hash &&
  7. ((k = p.key) == key || (key != null && key.equals(k))))
  8. e = p;
  9. // 如果此时p所指向的数组中下标为i位置的结点已经树化为红黑树了,执行else if
  10. else if (p instanceof TreeNode)
  11. e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
  12. // 把待插结点插入到链表结尾(以p所指向的数组中下标为i位置的结点为头结点)
  13. else {
  14. // binCount用于记录以tab[i]为头的链表中结点的下标,就是p的下标(从0开始)
  15. for (int binCount = 0; ; ++binCount) {
  16. // 如果p(p指向首元结点,就是数组中索引)的next为null,这一步后e指向链表第二
  17. // 个结点(如果存在),如果第二个节点不存在执行if,不断循环,p和e后移,
  18. // 直到e指向null了(这是p指向链表最后一个节点),才执行此if
  19. if ((e = p.next) == null) {
  20. // 将待插结点挂载到p的后面
  21. p.next = newNode(hash, key, value, null);
  22. // 如果此时链表长度大于等于8,即binCount为7时,就要进入if了
  23. if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
  24. treeifyBin(tab, hash);
  25. break;
  26. }
  27. // 如果e不为空,则执行这个if
  28. // e从链表第二个结点开始后移,判断待插结点是否跟链表中元素有重复。
  29. //如果重复了,就跳出循环,否则,p指向e(相当于p后移)
  30. if (e.hash == hash &&
  31. ((k = e.key) == key || (key != null && key.equals(k))))
  32. break;
  33. p = e;
  34. }
  35. }

image.png
image.png
image.png

javaSE之集合 - 图112

注意++modCount; if (++size > threshold)
resize();
这段代码是HashMap.java中的putVal()方法的,size表示【数组+链表】的元素个数,不是单指插在table表的节点个数,只要插入【数组+链表】的结点达到threshold,就会去调用resize()方法,进行二倍扩容。请看下面实例:

image.png
已经往【数组+链表】结构插入了12个值,threshold也为12,如果再插入一个值,那么table表会两倍扩容了。
image.png


2.7.2 HashSet练习

题目1
image.png
必须知道的知识:
image.png
解答:

  1. // Employee类中已重写hashCode和equals方法,在此认为两对象的name和age相等就是同一个对象
  2. class Employee {
  3. private String name;
  4. private int age;
  5. public Employee(String name, int age) {
  6. this.name = name;
  7. this.age = age;
  8. }
  9. @Override
  10. public boolean equals(Object o) {
  11. if (this == o) return true;
  12. if (o == null || getClass() != o.getClass()) return false;
  13. Employee employee = (Employee) o;
  14. return age == employee.age && Objects.equals(name, employee.name);
  15. }
  16. @Override
  17. public int hashCode() {
  18. return Objects.hash(name, age);
  19. }
  20. @Override
  21. public String toString() {
  22. return "Employee{" +
  23. "name='" + name + '\'' +
  24. ", age=" + age +
  25. '}';
  26. }
  27. }
  28. @Test
  29. public void test(){
  30. Set<Object> set = new HashSet<>();
  31. Employee tom = new Employee("tom", 15);
  32. Employee jack = new Employee("jack", 18);
  33. Employee tom1 = new Employee("tom", 15);
  34. set.add(tom);
  35. set.add(jack);
  36. set.add(tom1);
  37. System.out.println(set);
  38. }
  39. 输出:
  40. [Employee{name='jack', age=18}, Employee{name='tom', age=15}]
  41. 可以看到tom1对象没有添加进去,Employee类中重写了hashCodeequals方法后,则认为nameage一样的
  42. 两对象是同一个,故认为重复,第二个重复的加不进去。如果不重写hashCodeequals方法,则能加进去,如:
  43. [Employee{name='tom', age=15}, Employee{name='jack', age=18}, Employee{name='tom', age=15}]

题目2
image.png
解答:

  1. class Employee_ {
  2. private String name;
  3. private int sal;
  4. private MyDate birthday;
  5. public Employee_(String name, int sal, MyDate birthday) {
  6. this.name = name;
  7. this.sal = sal;
  8. this.birthday = birthday;
  9. }
  10. // 重写hashCode和equals方法,当name和birthday相同时,认为是相同员工,不能添加到HashSet集合
  11. @Override
  12. public boolean equals(Object o) {
  13. if (this == o) return true;
  14. if (o == null || getClass() != o.getClass()) return false;
  15. Employee_ employee_ = (Employee_) o;
  16. return Objects.equals(name, employee_.name) && Objects.equals(birthday, employee_.birthday);
  17. }
  18. @Override
  19. public int hashCode() {
  20. return Objects.hash(name, birthday);
  21. }
  22. @Override
  23. public String toString() {
  24. return "Employee_{" +
  25. "name='" + name + '\'' +
  26. ", sal=" + sal +
  27. ", birthday=" + birthday +
  28. '}';
  29. }
  30. }
  31. class MyDate {
  32. private int year;
  33. private int month;
  34. private int day;
  35. public MyDate(int year, int month, int day) {
  36. this.year = year;
  37. this.month = month;
  38. this.day = day;
  39. }
  40. @Override
  41. public boolean equals(Object o) {
  42. if (this == o) return true;
  43. if (o == null || getClass() != o.getClass()) return false;
  44. MyDate myDate = (MyDate) o;
  45. return year == myDate.year && month == myDate.month && day == myDate.day;
  46. }
  47. // 重写hashCode和equals方法,当年月日一样,则认为是同一个人,为Employee_类做准备
  48. @Override
  49. public int hashCode() {
  50. return Objects.hash(year, month, day);
  51. }
  52. @Override
  53. public String toString() {
  54. return "MyDate{" +
  55. "year=" + year +
  56. ", month=" + month +
  57. ", day=" + day +
  58. '}';
  59. }
  60. }
  61. @Test
  62. public void test() {
  63. Employee_ e1 = new Employee_("curry", 1000, new MyDate(2000, 10, 1));
  64. Employee_ e2 = new Employee_("curry", 1000, new MyDate(2000, 10, 1));
  65. Employee_ e3 = new Employee_("curry", 5000, new MyDate(2000, 10, 1));
  66. HashSet<Object> set = new HashSet<>();
  67. set.add(e1);
  68. set.add(e2);
  69. set.add(e3);
  70. System.out.println(set);
  71. }
  72. // 输出结果:
  73. [Employee_{name='curry', sal=1000, birthday=MyDate{year=2000, month=10, day=1}}]
  74. 只有第一个添加成功

2.8 LinkedHashSet

image.png
LinkedHashSet示意图:
image.png

2.8.1 底层分析

首先进行初始化得到set对象,LinkedHashSet底层还是调用了LinkedHashMap。
image.png
调用add()方法,再继续调用put方法,put再调用核心方法putVal,这个流程跟HashSet一模一样
image.png
上面putVal会调用afterNodeInsertion(boolean evict)方法,

添加第二个元素,值跟第一个添加的一样,那么在putVal方法中执行过程如下。
image.png
添加完5个元素后,双向链表基本建成,head指向双向链表的首元节点(第一个插入的结点),tail指向双向链表的尾结点(最后一次插入的结点),
image.png
image.png


2.9 Map

image.png
回顾一下HashSet,HashSet底层使用的是HashMap,即HashSet在保存数据时也是基于Key-Value键值对存储的,只是在HashSet中,所有添加的Key的Value都是一个常量,即源码中的PRESENT
private static final Object _PRESENT _= new Object();

2.9.1 Map接口特点及API

Map存放数据的key-value,一对key-value是放在一个HashMap$Node中的,又因为Node实现了Entry接口,有些书上也说一对key-value就是一个Entry。
Map<String, Integer> map = new HashMap<>();对这句话Debug,过程如下:
先调用HashMap的无参构造,加载因子为0.75,
image.png
执行 map.put("张三", 28);这句代码,先把int类型28自动装箱成Integer类型,再进入HashMap.java中的put方法,然后再调用putVal方法,这跟HashSet添加一个样。
image.png
image.png
image.png
创建结点,调用newNode()方法newNode(int hash, K key, V value, Node<K,V> next),newNode再去初始化,调用构造器进行实例化。Node类是一个静态内部类,即每次添加键值对时,key-value都是保存在一个Node里面的(Node内部有四个属性),
image.png
image.png
javaSE之集合 - 图132

注意: 为了方便程序员遍历,即获取map中所有的key和所有的values,Set集合里面存放了所有的key,Collection接口的一个实现子类里面存放了所有的values。但真正的key和value如上图所示,是放在HashMap$Node里面的,而Set集合和Collection集合只是指向了对应Node而已。

Map体系继承图
image.png
Map接口常用方法
image.png

2.9.2 Map接口遍历方式

image.png

回顾一下: JDK设计者为了使程序员遍历Map比较方便,可以使用一个Set把Map中的key取出来,但Set其实指向的是HashMap$Node结点中的key,value也可以使用一个Collection取出来,同样,Collection也指向的是hashMap&Node结点中的value。

image.png

  1. Map<String, Integer> map = new HashMap<>();
  2. map.put("张三", 28);
  3. map.put("lisi", 36);
  4. map.put("奥巴马", 55);
  5. map.put(null, 18);
  6. map.put("curry", 33);
  7. map.put("god", null);

遍历方式一:for (Object key : keySet){}

  1. Set<String> keySet = map.keySet();
  2. // 增强for遍历keySet
  3. for (Object key : keySet) {
  4. System.out.println(key + ": " + map.get(key));
  5. }

遍历方式二:keySet.iterator();

  1. // 迭代器遍历keySet
  2. Iterator<String> iterator = keySet.iterator();
  3. while(iterator.hasNext()){
  4. String key = iterator.next();
  5. System.out.println(key + ": " + map.get(key));
  6. }

遍历方式三:map.entrySet();

  1. Set<Map.Entry<String, Integer>> entrySet = map.entrySet();
  2. for (Map.Entry<String, Integer> entry : entrySet) {
  3. System.out.println(entry.getKey() + ": " + entry.getValue());
  4. }

遍历方式四:entrySet.iterator();

  1. Iterator<Map.Entry<String, Integer>> iterator = entrySet.iterator();
  2. while(iterator.hasNext()){
  3. Map.Entry<String, Integer> entry = iterator.next();
  4. System.out.println(entry.getKey() + ": " + entry.getValue());
  5. }

2.9.3 HashMap小结

image.png
image.png
image.png


2.10 HashTable