【JavaSE-07】:集合(1):Collection接口(含源码分析)

【JavaSE-07】:集合

  • 一、概述
  • 二、Collection接口和常用方法
    • 1. Collection常用方法
    • 2. Collection接口遍历元素的方式1——使用Iterator(迭代器)
    • 3. Collection接口遍历元素的方式2——增强型循环for
  • 三、 Collection子接口之一:List接口
    • 1. List接口方法
    • 2. List实现类之一:ArrayList
    • 3. List实现类之二:Vector
      • 1、Vector的基本介绍
      • 2、Vector和ArrayLIst的比较
      • 3、Vector源码概览
    • 4. List实现类之三:LinkedList
    • 5.List集合的选择
  • 四、 Collection子接口之二:Set接口
    • 一、Set实现类之一:HashSet
      • 1.HashSet的基本说明
      • 2.HashSet的底层机制分析
        • 1.HashSet添加元素的底层实现:
        • 2.HashSet的扩容和转成红黑树机制
      • 3.HashSet 重写 equals() 和 HashCode() 方法
    • 二、Set实现类之二:LinkedHashSet
      • 1.LinkedHashSet 添加元素的底层实现

一、概述

  • 数组在内存存储方面的特点:
  1. 数组一旦初始化以后,长度就确定了。
  2. 数组声明的类型,就决定了进行元素初始化时的类型,保存和操作的必须为同一类型的元素。
  3. 数组中提供的方法非常有限,对于添加、删除、插入数据等操作,非常不便,同时效率不高。
  4. 获取数组中实际元素的个数的需求,数组没有现成的属性或方法可用。
  5. 数组存储数据的特点:有序、可重复。对于无序、不可重复的需求,不能满足。
  • 集合:
  1. 可以动态保存任意多个对象,使用比较方便;
  2. 提供了一系列方便的操作对象的方法:如add、remove、set、get等;
  3. 使用集合添加、删除元素的代码更简洁。

Java 集合可分为 Collection 和 Map 两种体系
1.单列集合:
【JavaSE-07】:集合(1):Collection接口(含源码分析)_第1张图片
2.双列集合:
【JavaSE-07】:集合(1):Collection接口(含源码分析)_第2张图片

二、Collection接口和常用方法

1. Collection常用方法

常用方法 方法名
1、添加 add(Object obj)
addAll(Collection coll)
2、获取有效元素的个数 int size()
3、清空集合 void clear()
4、是否是空集合 boolean isEmpty()
5、是否包含某个元素 boolean contains(Object obj):是通过元素的equals方法来判断是否是同一个对象;
boolean containsAll(Collection c):也是调用元素的equals方法来比较的。拿两个集合的元素挨个比较。
6、删除 boolean remove(Object obj) :通过元素的equals方法判断是否是要删除的那个元素。只会删除找到的第一个元素
boolean removeAll(Collection coll):取当前集合的差集
7、取两个集合的交集 boolean retainAll(Collection c):把交集的结果存在当前集合中,不 影响c
8、集合是否相等 boolean equals(Object obj)
9、转成对象数组 Object[ ] toArray()
10、获取集合对象的哈希值 hashCode()
11、遍历 iterator():返回迭代器对象,用于集合遍历

2. Collection接口遍历元素的方式1——使用Iterator(迭代器)

   Collection接口继承了java.lang.Iterable接口,该接口有一个iterator()方法,所有实现了Collection接口的集合类都有一个iterator()方法,用以返回一个实现了 Iterator 接口的对象。
   Iterator 仅用于遍历集合,Iterator 本身并不提供承装对象的能力。如果需要创建 Iterator 对象,则必须有一个被迭代的集合。

Iterator iterator = coll.iterator();
        while (iterator.hasNext()) {
            Object obj =  iterator.next();
            System.out.println(obj);
        }

【JavaSE-07】:集合(1):Collection接口(含源码分析)_第3张图片
注意:在调用 iterator.next() 方法之前必须要调用 iterator.hasNext()进行检测。若不调用,且下一条记录无效,直接调用 iterator.next()会抛出NoSuchElementException异常。
● 在调用循环结束后,iterator迭代器指向最后一个元素,再次使用 iterator.next( ) 方法会报错。

Iterator iterator = coll.iterator();
        while (iterator.hasNext()) {
            Object obj =  iterator.next();
            System.out.println(obj);
        }
        iterator.next();//报异常:java.util.NoSuchElementException

如果希望再次遍历,Collection集合需要重新调用 iterator() 获取一个新的迭代器对象,重置迭代器,集合对象每次调用 iterator() 方法都得到一个全新的迭代器对象,默认游标都在集合的第一个元素之前。

Iterator iterator = coll.iterator();
        while (iterator.hasNext()) {
            Object obj =  iterator.next();
            if(obj.equals("Tom")){ 
                  iterator.remove(); 
                  }
        }

注意:

  1. Iterator可以删除集合的元素,但是是遍历过程中通过迭代器对象的 remove 方法,不是集合对象的 remove 方法。
  2. 如果还未调用 next() 或在上一次调用 next 方法之后已经调用了 remove 方法, 再调用 remove 都会报异常 IllegalStateException。

3. Collection接口遍历元素的方式2——增强型循环for

  • 遍历操作不需获取Collection或数组的长度,无需使用索引访问元素。
  • 遍历集合的底层调用Iterator完成操作。可以理解为简化版的 iterator 迭代器遍历。
  • 它也可以用来遍历数组。
    【JavaSE-07】:集合(1):Collection接口(含源码分析)_第4张图片

三、 Collection子接口之一:List接口

由于数组的一些局限性,常采用List替代数组。
● List集合类中元素有序、且可重复,集合中的每个元素都有其对应的顺序索引。
● List容器中的元素都对应一个整数型的序号记载其在容器中的位置,可以根据序号存取容器中的元素。
● JDK API中List接口的实现类常用的有:ArrayList、LinkedList 和 Vector。

1. List接口方法

List除了从Collection集合继承的方法外,List 集合里添加了一些根据索引来操作集合元素的方法。

序号 方法
1 void add(int index, Object ele):在index位置插入ele元素
2 boolean addAll(int index, Collection eles):从index位置开始将eles中的所有元素添加进来
3 Object get(int index):获取指定index位置的元素
4 int indexOf(Object obj):返回obj在集合中首次出现的位置
5 int lastIndexOf(Object obj):返回obj在当前集合中末次出现的位置
6 Object remove(int index):移除指定index位置的元素,并返回此元素
7 Object set(int index, Object ele):设置指定index位置的元素为ele
8 List subList(int fromIndex, int toIndex):返回从fromIndex到toIndex 位置的子集合

2. List实现类之一:ArrayList

● ArrayList 是 List 接口的典型实现类、主要实现类
● 本质上,ArrayList是对象引用的一个"变长"数组

ArrayList的底层操作机制源码分析:重点
扩容机制:

  1. ArrayList中维护了一个Object类型的数组elementData
transient Object[] elementData;// transient  表示瞬间,短暂的,表示该属性不会被序列化
  1. 当创建ArrayList对象时,如果使用的是无参构造器,则初始 elementData 容量为0,在第1次添加元素时,则扩容 elementData 为10,如此后需要再次扩容,则扩容 elementData 为1.5倍。
  2. 如果使用的是指定大小的有参构造器,则初始elementData的容量为指定大小,如果需要扩容,则直接扩容为elementData的1.5倍。
private int size;
private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = new Object[0];
//ArrayList的无参构造器
public ArrayList() {
        this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA; //创建了一个空的elementData数组
    }

//ArrayList的有参构造器
public ArrayList(Collection<? extends E> c) {
        this.elementData = c.toArray();
        if ((this.size = this.elementData.length) != 0) {
            if (this.elementData.getClass() != Object[].class) {
                this.elementData = Arrays.copyOf(this.elementData, this.size, Object[].class);
            }
        } else {
            this.elementData = EMPTY_ELEMENTDATA;
        }

    }

第1次添加元素时:

    public boolean add(E e) {
        ++this.modCount;
        this.add(e, this.elementData, this.size);// 初始默认值 this.size:0
        return true;
    }
    
    private void add(E e, Object[] elementData, int s) {
        if (s == elementData.length) {
            elementData = this.grow();  
        }

        elementData[s] = e; //先确定是否需要扩容,再赋值
        this.size = s + 1;
    }

● 初始状态时,elementData长度默认为0的空数组;
● 第一次赋值时,先使用modCount 来记录集合被修改的次数;
然后调用add方法:
1)先判断是否需要扩容(元素数量等于数组长度);需要扩容则调用 grow() 方法扩容,第一次扩容后新数组长度为 10,第二次及之后扩容为为旧数组的1.5倍;
判断规则:
若第一次赋值的元素数量少于10,( minCapacity=元素个数+1<=10),利用扩容机制,设置新数组长度为10;
若第一次赋值的元素数量大于10,( minCapacity=元素个数+1>10),利用扩容机制,设置新数组长度为当前添加元素的数量长度;
2)再利用copeof()方法拷贝原来的数据,再赋值
● 第二次及之后赋值时,先记录修改次数,然后调用add方法:
先判断是否需要扩容;
1)不需要扩容时,直接赋值;
2)需要扩容则调用grow()方法扩容,扩容为为旧数组的1.5倍;再拷贝原来的数组元素,再赋值。
(此处有检查最大容量的判断)
扩容机制实现源码:

    private Object[] grow() {
        return this.grow(this.size + 1);
    }
    private Object[] grow(int minCapacity) {
        return this.elementData = Arrays.copyOf(this.elementData, this.newCapacity(minCapacity)); //拷贝原数组元素
    } 
      
    private int newCapacity(int minCapacity) {
        int oldCapacity = this.elementData.length;  //第一次扩容前数组长度:0
        int newCapacity = oldCapacity + (oldCapacity >> 1); //第一次扩容后新数组长度为:10,第二次及之后需要扩容时为旧数组的1.5倍长度
        if (newCapacity - minCapacity <= 0) {  //判断是否需要扩容
            if (this.elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
                return Math.max(10, minCapacity);   //第一次添加元素的数量小于10,数组初始长度设定为10;大于10,则为元素个数的指定长度
            } else if (minCapacity < 0) {     //数组长度小于0,则报异常
                throw new OutOfMemoryError();
            } else {
                return minCapacity;
            }
        } else {
            return newCapacity - 2147483639 <= 0 ? newCapacity : hugeCapacity(minCapacity); //判断是否超出最大长度
        }
    }

3)如果使用的是指定大小的有参构造器,则初始elementData的容量为指定大小,如果需要扩容,则直接扩容为elementData的1.5倍。

此处注意一个小细节:
Idea编译器在默认情况下,Debug时显示的数据是简化之后的,要看到完整的数据需要做一下设置:
【JavaSE-07】:集合(1):Collection接口(含源码分析)_第5张图片

3. List实现类之二:Vector

1、Vector的基本介绍

1)Vector类的定义说明

public class Vector<E> extends AbstractList<E> implements List<E>, RandomAccess, Cloneable, Serializable 

2)Vector底层也是一个对象数组: protected Object[ ] elementData;
3)Vector是线程同步的,即线程安全,其操作方法带有synchronized,例如:

    public synchronized int size() {
        return this.elementCount;
    }

4)开发工作中,需要线程同步安全时,考虑使用Vector

2、Vector和ArrayLIst的比较

集合类型 底层结构 版本 线程安全(同步效率) 扩容倍数
ArrayList 可变数组 jdk1.2 不安全,效率高 如果有参构造 :1.5倍
如果是无参构造:
1.第一次 10
2.第二次开始按 1.5倍扩
Vector 可变数组 jdk1.0 安全,效率不高 如果是无参,默认为10,满后就按 2倍扩容
如果指定大小,则每次直接按2倍扩容

3、Vector源码概览

  1. 调用无参构造器时:
    底层依然是采用可变数组。
protected Object[] elementData;

无参构造器默认容量是10,或者有容量大小参数,直接为指定大小

    public Vector(int initialCapacity, int capacityIncrement) {
        if (initialCapacity < 0) {
            throw new IllegalArgumentException("Illegal Capacity: " + initialCapacity);
        } else {
            this.elementData = new Object[initialCapacity];
            this.capacityIncrement = capacityIncrement;//初始值为0
        }
    }

    public Vector(int initialCapacity) {  //有容量大小参数,直接为指定大小
        this(initialCapacity, 0);
    }

    public Vector() {
        this(10); //无参构造器默认容量是10
    }
  1. 调用有参构造器:(如果参数为int类型,则指定容量大小)
    如上述源码所表明,可以调用有参构造器,指定初始容量大小,同时可以指定每次扩容时的增量大小 initialCapacity,指定后按该增量进行扩容。
    public Vector(Collection<? extends E> c) {
        this.elementData = c.toArray();
        this.elementCount = this.elementData.length;
        if (this.elementData.getClass() != Object[].class) {
            this.elementData = Arrays.copyOf(this.elementData, this.elementCount, Object[].class);
        }
    }

扩容机制:

//调用add
    private void add(E e, Object[] elementData, int s) {
        if (s == elementData.length) { //判断是否需要扩容
            elementData = this.grow();// 当有效元素个数+1,即elementData 达到了数组长度时,需要调用grow()扩容
        }
       //不需要扩容,直接赋值
        elementData[s] = e; 
        this.elementCount = s + 1;
    }

    public synchronized boolean add(E e) {
        ++this.modCount;
        this.add(e, this.elementData, this.elementCount);
        return true;
    }

其中,elementCount 为数组当前最后一个元素的下标+1。(如数组容量为10,有效元素个数为5个,最后一个元素为elementData[4],elementCount 此时为5)
扩容时:

   private Object[] grow(int minCapacity) {
        return this.elementData = Arrays.copyOf(this.elementData, this.newCapacity(minCapacity));
    }

    private Object[] grow() {
        return this.grow(this.elementCount + 1);
    }

    private int newCapacity(int minCapacity) { //minCapacity为数组长度+1
        int oldCapacity = this.elementData.length;
        int newCapacity = oldCapacity + (this.capacityIncrement > 0 ? this.capacityIncrement : oldCapacity);//capacityIncrement为增量大小,未指定时初始值为0,新数组长度为旧数组长度的2倍
        if (newCapacity - minCapacity <= 0) { 
            if (minCapacity < 0) {
                throw new OutOfMemoryError();
            } else {
                return minCapacity;//无需扩容
            }
        } else {
            return newCapacity - 2147483639 <= 0 ? newCapacity : hugeCapacity(minCapacity);//最大容量检查
        }
    }

此处,Arrays.copyOf(array, to_index);返回的是新数组,等于数组array的前to_index个数;
newCapacity()返回新数组的长度,需要扩容时是原来的2倍。

4. List实现类之三:LinkedList

  1. LinkedList底层实现了双向链表和双端队列特点;
    底层采用 Node 类型存储数据,定义了一个 前节点 、元素、后节点的属性。
    private static class Node<E> {
        E item;
        LinkedList.Node<E> next;
        LinkedList.Node<E> prev;

        Node(LinkedList.Node<E> prev, E element, LinkedList.Node<E> next) {
            this.item = element;
            this.next = next;
            this.prev = prev;
        }
    }
  1. 可以添加任意元素(元素可以重复),包括null;
  2. 线程不安全,没有实现同步。

源码分析:

  1. 构造器
    LinkedList list = new LinkedList();
    此时 属性 LinkedList.Node< E> first 为null 、 LinkedList.Node< E> last 也为null
public LinkedList() {
        this.size = 0;
    }
  1. 执行添加元素 put 方法
   public boolean add(E e) {
        this.linkLast(e);
        return true;
    }

原来最后一个元素的尾结点next指向新的元素e,使新的元素添加到双向链表的最后: this.last = newNode; ,它的前节点pre 指向 原来的最后一个元素,尾结点为空

 void linkLast(E e) {
        LinkedList.Node<E> l = this.last;
        LinkedList.Node<E> newNode = new LinkedList.Node(l, e, (LinkedList.Node)null);
        this.last = newNode;
        if (l == null) {
            this.first = newNode;
        } else {
            l.next = newNode;
        }

        ++this.size;
        ++this.modCount;
    }

LinkedList 的增删改方法原理类似,通过修改节点的指向来实现。

5.List集合的选择

比较:
【JavaSE-07】:集合(1):Collection接口(含源码分析)_第6张图片
1)如果改查的操作多,选择ArrayList;
2)如果增删的操作多,选择LinkedList;
3)一般来说,在程序中,80%-90%都是查询,因此大部分情况选择ArrayList;
4)在开发项目中,根据业务灵活选择,可以一个模块选用ArrayList,另一个模块选择LinkedList。

四、 Collection子接口之二:Set接口

● Set接口是Collection的子接口,set接口没有提供额外的方法
● Set 集合不允许包含相同的元素,如果试把两个相同的元素加入同一个 Set 集合中,则添加操作失败。可以添加一个null。(只能一个)
● Set 判断两个对象是否相同不是使用 == 运算符,而是根据 equals() 方法
● Set存放数据是无序的,即添加的顺序和取出的顺序不一致(注意:虽然取出顺序与添加时不一致,但是是固定的)
Set中的元素不能通过索引来获取。

一、Set实现类之一:HashSet

1.HashSet的基本说明

● HashSet 是 Set 接口的典型实现,大多数时候使用 Set 集合时都使用这个实现类。
● HashSet 按 Hash 算法来存储集合中的元素,因此具有很好的存取、查找、删除 性能。
● HashSet 不是线程安全的
说明:
1)HashSte实现类Set接口;
2)HashSet实际上是HashMap (HashMap的底层是数组+链表+红黑树),源码如下;

 public HashSet() {
        this.map = new HashMap();
    }

3)可以存放null值,但只能有一个null;
4)HashSet不能保证元素的排列顺序 ,取决于Hash后,再确定索引的结果(添加的顺序和取出的顺序不一致)
5)不能有重复元素/对象。

2.HashSet的底层机制分析

1.HashSet添加元素的底层实现:

1).HashSet底层是HashMap
2)添加一个元素时,先得到hash值,然后它会转成索引值
3)找到存储数据表table,查看这个索引位置是否已经存放了元素
4)如果没有,则直接加入
5)如果有元素了,先调用equals方法比较,相同时放弃添加,不相同时,添加到最后
6)在java8中,如果一条链表的元素个数超过TREEIFY_THRESHOLD(默认值是8),并且table的大小>=MIN_TREEIFY_CAPACITY(默认值64),会进行树化(红黑树)。

案例分析:

public class HashSetSource {
    public static void main(String[] args) {

        HashSet hashSet = new HashSet();
        hashSet.add("java");
        hashSet.add("php");
        hashSet.add("c++");
        System.out.println("hashset="+hashSet);
    }
}

源码分析:
(1)执行无参构造器

    public HashSet() {
        this.map = new HashMap();
    }

(2)执行 add() 方法,其中 PRESENT 是一个静态对象,共享(参考单例模式理解)

    public boolean add(E e) {
        return this.map.put(e, PRESENT) == null;
    }

(3)执行 put() 方法,该方法会执行 hash(key) 返回key对应的hash值,通过算法 key.hashCode()) ^ h >>> 16得到;

    public V put(K key, V value) {
        return this.putVal(hash(key), key, value, false, true);  //此时key=“java”  
    }
    
        static final int hash(Object key) {
        int h;
        return key == null ? 0 : (h = key.hashCode()) ^ h >>> 16;
    }

*(4)执行 putVal 方法(见内部分析)

 final V putVal(int hash, K key, V value, boolean onlyIfAbsent, boolean evict) {
        HashMap.Node[] tab;  //定义了一些辅助变量,tab是HashMap 的一个数组,类型是Node
        int n;
        // if 语句判断当前tab 是null或者大小为空的情况,通过调用 resize() 方法,返回一个容量为16的数组,缓存极限为12
        if ((tab = this.table) == null || (n = tab.length) == 0) {
            n = (tab = this.resize()).length;
        }

        Object p;
        int i;
        /** (1)根据 key ,上一步(3)中调用 hash(key) 返回key对应的hash值 得到key应该存放到tab表的索引位置(即算法 i = n - 1 & hash ),
                将这个位置的对象,赋值给 p
         (2)此时判断 p 是否为 null 
              2.1 若为null,说明当前位置没有元素,则调用下面的方法,创建一个Node对象  Node(key=“java”,value=PRESENT,next=null)
                   HashMap.Node newNode(int hash, K key, V value, HashMap.Node next) {
                         //此处传入hash 是为了判断下一次添加时是否重复
                  		 return new HashMap.Node(hash, key, value, next);  
                    }
                    然后将该对象放到这个位置;
              2.2 若不为空,见下
        */
      
        if ((p = tab[i = n - 1 & hash]) == null) {
            tab[i] = this.newNode(hash, key, value, (HashMap.Node)null);
        } else {
            Object e;
            Object k;
            /** 在 p 不为空的情况下,
                一、如果当前索引位置对应的链表中第一个元素和准备添加的元素hash值相同 ((HashMap.Node)p).hash == hash
                      并且满足一下条件之一时,都不能加入
                      1)准备添加的key 和 p 指向的Node节点的 key是同一个对象
                      2) p 指向的Node节点的 key使用equals 方法和准备加入的 key比较后相同
           */
            if (((HashMap.Node)p).hash == hash && ((k = ((HashMap.Node)p).key) == key || key != null && key.equals(k))) {
                e = p;
            // 二、然后判断 p 是否是一棵红黑树,如果是一棵红黑树,则调用 putTreeVal 进行添加(细节较复杂,此处不详述)
            } else if (p instanceof HashMap.TreeNode) {
                e = ((HashMap.TreeNode)p).putTreeVal(this, tab, hash, key, value);
            } else {
                int binCount = 0;
               
               /**三、以上情况都不满足时,需要使用循环依次和当前索引位置对应的链表中的元素逐一比较
                         (1)循环结束后,如果不和其中的元素相同,则添加到链表的最后
                             注意: 添加新的元素后,立即判断 此链表是否已经达到8个节点,
                              是就调用 treeifyBin 方法对当前链表树化,在转化为红黑树时,会进行判断:
                              tab != null && (n = tab.length) >= 64,满足时,先进行table扩容,
                              只有不满足才进行树化
                         (2)循环过程中,发现有相同的元素时,就break跳出
                */
                while(true) {
                    if ((e = ((HashMap.Node)p).next) == null) {
                        ((HashMap.Node)p).next = this.newNode(hash, key, value, (HashMap.Node)null);
                        if (binCount >= 7) {
                            this.treeifyBin(tab, hash);
                        }
                        break;
                    }

                    if (((HashMap.Node)e).hash == hash && ((k = ((HashMap.Node)e).key) == key || key != null && key.equals(k))) {
                        break;
                    }

                    p = e;
                    ++binCount;
                }
            }

            if (e != null) {
                V oldValue = ((HashMap.Node)e).value;
                if (!onlyIfAbsent || oldValue == null) {
                    ((HashMap.Node)e).value = value;
                }

                this.afterNodeAccess((HashMap.Node)e);  
                return oldValue;
            }
        }

        ++this.modCount;
        if (++this.size > this.threshold) {  //此处判断是否要扩容
            this.resize();
        }

        this.afterNodeInsertion(evict);//此方法在HashMap中是一个空方法,它主要是用来给HashMap的子类去实现这个方法,做一些操作
        return null;
    }

2.HashSet的扩容和转成红黑树机制

1).HashSet底层是HashMap,第一次添加时,table数组扩容到16,临界值为12(16加载因子0.75);
2).如果table数组使用到临界值,即12时,会扩容到16
2=32 ,新的临界值位 32*0.75=24,依次类推;
3).在Java8中,如果一条链表的元素个数到达TREEIFY_THRESHOLD(默认值为8),并且table 的大小 >=MIN_TREEIFY_CAPACITY(默认值64),就会进行树化,否则仍然采用数组扩容机制。

代码简单演示:

public class HashSetIncrement {
    public static void main(String[] args) {

        HashSet hashSet = new HashSet();
//        for (int i = 0; i <= 100; i++) {
//            hashSet.add(i);
//        }
        for (int i = 1; i <= 12; i++) {
            hashSet.add(new Person(i));
        }
    }
}

class Person {
    private int age;
    public Person(int age) {
        this.age = age;
    }
    @Override
    public int hashCode() { //保证哈希值一样
        return 100;
    }
}

i=8时,不会树化:
当i=8时,准备添加第8个元素,前7个节点均有值,不为null,binCount 自增,当binCount =6时,判断table表的链表数组索引为7的元素是否为null,此时索引值为6的位置存放元素person(7) ,索引值为7的位置为null ,直接赋值,但此时不满足>=7,不跳转树方法;
【JavaSE-07】:集合(1):Collection接口(含源码分析)_第7张图片
当i=9时,准备添加第9个元素,前8个节点均有值,不为null,binCount 自增,当binCount =7时,判断table表的链表数组索引为8的元素是否为null,此时索引值为7的位置存放元素person(8) ,索引值为8的位置为null ,赋值后进入树方法。
进入树方法后,由于table容量小于64,跳过树化逻辑代码,直接跳转扩容机制方法,table扩容为原来的2倍,16*2=32,此时阈值为24。

当i=10时,准备添加第10个元素,直到binCount =8,判断发现table表中链表数组索引为9的元素才为null ,person(10) 赋值给索引值为9的位置后,发现满足条件 (binCount >= 7) 进入树方法,由于table容量小于64,跳过树化逻辑代码,直接跳转扩容机制方法,table扩容为32*2=64,此时阈值为48。

else {
                int binCount = 0;

                while(true) {
                    if ((e = ((HashMap.Node)p).next) == null) {
                        ((HashMap.Node)p).next = this.newNode(hash, key, value, (HashMap.Node)null);
                        if (binCount >= 7) {
                            this.treeifyBin(tab, hash);
                        }
                        break;
                    }

                    if (((HashMap.Node)e).hash == hash && ((k = ((HashMap.Node)e).key) == key || key != null && key.equals(k))) {
                        break;
                    }

                    p = e;
                    ++binCount;
                }

【JavaSE-07】:集合(1):Collection接口(含源码分析)_第8张图片
当i=11时,准备添加第11个元素,直到binCount =9,判断发现table表中链表数组索引为10的元素才为null ,person(11) 赋值给索引值为10的位置后,发现满足条件 (binCount:9 >= 7) 进入树方法,由于table容量此时等于64,执行树化逻辑代码,完成树化,此处执行后,该链表转成红黑树。
【JavaSE-07】:集合(1):Collection接口(含源码分析)_第9张图片

关于扩容机制的补充说明
源码:

 if (++this.size > this.threshold) {
    this.resize();
}

其中的 size 只要往HashSet中添加一个元素,底层的table的size 就自增1,即使在 size达到12个元素时,可能这些元素只使用到了部分链表,依然会触发扩容机制。
例如测试代码:

public class HashSetIncrement_2 {
    public static void main(String[] args) {
        HashSet hashSet = new HashSet();
        
        for (int i = 0; i <= 7; i++) {
            hashSet.add(new PersonA(1));
        }
        for (int i = 0; i <= 7; i++) {
            hashSet.add(new PersonB(1));
        }
    }
}

class PersonA {
    private int age;
    public PersonA(int age) {
        this.age = age;
    }
    @Override
    public int hashCode() {
        return 100;
    }
}

class PersonB {
    private int age;
    public PersonB(int age) {
        this.age = age;
    }
    @Override
    public int hashCode() {
        return 50;
    }
}

【JavaSE-07】:集合(1):Collection接口(含源码分析)_第10张图片
即使此时这12个元素只占用了table下的2个链表,依然会触发扩容机制。
【JavaSE-07】:集合(1):Collection接口(含源码分析)_第11张图片

3.HashSet 重写 equals() 和 HashCode() 方法

HashSet 集合判断两个元素相等的标准:两个对象通过 hashCode() 方法比较相等,并且两个对象的 equals() 方法返回值也相等。
● 对于存放在Set容器中的对象,对应的类一定要重写equals()和hashCode(Object obj)方法,以实现对象相等规则。即:“相等的对象必须具有相等的散列码”。
重写原则:
1、 在程序运行时,同一个对象多次调用 hashCode() 方法应该返回相同的值。
2、当两个对象的 equals() 方法比较返回 true 时,这两个对象的 hashCode() 方法的返回值也应相等。
3、对象中用作 equals() 方法比较的 Field(属性),都应该用来计算 hashCode 值。
*4、复写equals方法的时候一般都需要同时复写hashCode方法。通 常参与计算hashCode的对象的属性也应该参与到equals()中进行计算。

二、Set实现类之二:LinkedHashSet

● LinkedHashSet 是 HashSet 的子类;
● LinkedHashSet 底层是一个LinkedHashMap(HashMap的子类),底层维护了一个数组和双向链表;
● LinkedHashSet 根据元素的 hashCode 值来决定元素的存储位置, 但它同时使用双向链表维护元素的次序(图),这使得元素看起来是以插入 顺序保存的;
● LinkedHashSet插入性能略低于 HashSet,但在迭代访问 Set 里的全部元素时有很好的性能;
● LinkedHashSet 不允许集合元素重复。

1.LinkedHashSet 添加元素的底层实现

1)在LinkedHashSet 中维护了一个Hash表和双向链表;(LinkedHashSet 有head 和 tail)
2)每一个节点有 before 和 after 属性,形成双向链表;
3)添加一个元素时,先计算其 hash值,再求索引,确定该元素在table 中的位置,然后将添加的元素加入到双向链表,如果已经存在不添加(原则和HashSet一样)
例如:
tail.next = newElement;
newElement.pre = tail;
tail = newElement;
4)在遍历LinkedHashSet 时,也能确保插入的顺序和遍历时一致。

public class LinkedHashSetSource {
    public static void main(String[] args) {

        LinkedHashSet set = new LinkedHashSet();
        set.add(new String("Hello!"));
        set.add(123);
        set.add(123);
        set.add(new Student("古力娜扎",25));
        set.add(456);
        set.add("AABB");
        System.out.println("set="+set);
    }
}

class Student{
    private String name;
    private int age;

    public Student(String name, int age) {
        this.name = name;
        this.age = age;
    }

    @Override
    public String toString() {
        return "Student{" +
                "name='" + name + '\'' +
                ", age=" + age +
                '}';
    }
}

分析:

  1. LinkedHashSet 加入元素和取出元素的顺序一样;
  2. LinkedHashSet 底层维护的是一个 LinkedHashMap (HashMap的子类)
  3. LinkedHashSet 底层数据结构:table数组 + 双向链表
  4. 第一次添加元素时,直接将 table数组扩容为16 ,存放的节点类型是 LinkedHashSet E n t r y ∗ 此 处 数 组 H a s h M a p Entry * 此处 数组 HashMap EntryHashMapNode[] 存放的元素/数据类型是 LinkedHashSet$Entry 继承关系
    * static class Node implements Entry {}
    【JavaSE-07】:集合(1):Collection接口(含源码分析)_第12张图片

添加元素时,底层依然是调用 HashMap 的 putVal() 方法,同上。遇到相同的元素时,判断机制也一样同上。
添加完成后,这些元素之间有链表连接的关系。
【JavaSE-07】:集合(1):Collection接口(含源码分析)_第13张图片
结构关系如下:
【JavaSE-07】:集合(1):Collection接口(含源码分析)_第14张图片

你可能感兴趣的:(JavaSE,java)