集合 HashSet 的使用及源码分析(jdk1.8)

引出问题

  1. HashSetSet集合(Collection)三者有什么联系与区别?
  2. HashSet 怎么保证元素不重复?
  3. HashSet 能否添加null元素数据?
  4. HashSet 添加元素是有顺序的吗?
  5. HashSet 是线程安全的吗(即是同步的吗)?
  6. fail-fast(快速失败)是什么?
  7. 应用场景是什么?

简介

JAVA集合是指 存在 java.unil 包中的 Collection相关的类,包括 CollectionMap 相关的所有类。

集合类.png

本文的 HashSet 是 Set 的一种实现方式,底层主要使用 HashMap 来实现。

HashSet、Set、集合(Collection)三者有什么联系与区别?

先看下 HashSet 的类继承图。

HashSet集合继承图.png

由此可以看出:

  • HashSet 是 Set 的一种实现方式,Set 又是 Collection 的一种实现。
  • HashSet 继承了 AbstractSet 类,AbstractSet 是 Set 的默认实现, 接口同时继承了 AbstractCollection 类。

源码分析

先来一张 HashSet 的方法图:

HashSet方法图.png

这里我们主要分析 add 、remove 、contains 、clear、contains、size、isEmpty 几个方法。

  • 属性

通过看源码发现有一个 私有的 map 属性和一个 Object 的静态常量:

    /** 内部使用的 HashMap ,用来存放数据 */
    private transient HashMap map;

    /** 虚拟对象,用来作为 map 的 value 元素 */
    private static final Object PRESENT = new Object();

这里的 map属性 是做为 hashSet 的内部元素存储容器;PRESENT 用来 做为 map 元素的value 值。

  • 构造函数

共有四个构造函数:

  1. 无参数
public HashSet() {
        /*** 内部初始化 map 容器  */
        map = new HashMap<>();
}

2.参数为 一个 Collection 的 集合

  public HashSet(Collection c) {
       /*** 内部初始化 map 容器  */
        map = new HashMap<>(Math.max((int) (c.size()/.75f) + 1, 16));
       /** 把传进来的 集合 添加 到 HashSet 集合中  */
        addAll(c);
    }
  1. 参数为 initialCapacity(初始化大小),loadFactor (填充因子 )
public HashSet(int initialCapacity, float loadFactor) {
         /*** initialCapacity 初始化大小 , loadFactor  填充因子   */
        map = new HashMap<>(initialCapacity, loadFactor);
}
  1. 参数为 initialCapacity(初始化大小)
  public HashSet(int initialCapacity) {
        /*** initialCapacity 初始化大小  */
        map = new HashMap<>(initialCapacity);
   }
  1. 还有个 非 public 的构造函。
 HashSet(int initialCapacity, float loadFactor, boolean dummy) {
        map = new LinkedHashMap<>(initialCapacity, loadFactor);
 }

这个 方法有点特殊,不是 public 的,意味它只能被同一个包或者子类调用。在这里主要是给LinkHashSet 使用的。

构造 方法都是调用 HashMap 对应的构造方法 , 来初始化 map属性。

  • add (添加元素)
 public boolean add(E e) {
        return map.put(e, PRESENT)==null;
 }

内部直接调用 HaskMap 的 put 方法,把元素的本身作为 key , 用 PRESENT 作为 value值,换句话说 就是在 map中的所有value 都是一样了。

  • remove (删除元素)
/*** 返回值为 boolean  值 */
 public boolean remove(Object o) {
        return map.remove(o)==PRESENT;
 }

举例 1:错误的删除,会抛出 ConcurrentModificationException 异常 , 具体 分析 请看下文 fail-fast(快速失败) 板块

    static final String DELETE_KEY = "aa";
    public static void main(String[] args) {
        final Set all = new HashSet<>();
        
        all.add("bb");
        all.add("cc");\
        all.add("aa");
        all.add("111");

        for (String s : all) {
            if(DELETE_KEY.equals(s)){
                // 这里会报错
                all.remove(DELETE_KEY);
            }
        }
}

举例 2:正确的删除

        final Set all = new HashSet<>();
        /** 添加元素 */
        all.add("aa");
        all.add("bb");
        all.add("cc");
        all.add("111");
        all.add("222");
        all.add("333");
        all.add("test");
       /** 遍历删除元素,利用 iterator 删除  */
        Iterator  iter=  all.iterator();
        while (iter.hasNext()){
            if("aa".equals(iter.next())){
                iter.remove();
            }
        }

内部直接调用 HaskMap 的 remove 方法,将返回值与 PRESENT做相等运算。这里返回的是 boolean 值。注意 HaskMap 的 remove 返回的是删除元素的value值.

  • contains (查询元素)
public boolean contains(Object o) {
        return map.containsKey(o);
}

内部直接调用 HaskMap 的 containsKey方法,判断是否存在 与之相等 key 值.

这里要特别提示一下 , Set 集合中 是没有 get() 方法的哦!因为感觉 get 好像没有多大的意义,不能 向 List 集合 那样 用 下标(index)来访问元素。

  • clear(删除所有元素)
 public void clear() {
        map.clear();
 }

内部直接调用 HaskMap 的 clear方法。

  • size (获取元素个数)
 public int size() {
        return map.size();
 }

内部直接调用 HaskMap 的 size方法。

  • isEmpty (判断是否包含任何元素)
    public boolean isEmpty() {
        return map.isEmpty();
    }

内部直接调用 HaskMap 的 isEmpty方法。

  • spliterator (遍历元素)
    public Spliterator spliterator() {
        return new HashMap.KeySpliterator(map, 0, -1, 0, 0);
    }

可分隔的迭代器,主要用于多线程下迭代处理元素使用。

fail-fast(快速失败)

  1. 概念:
    是JAVA集合中的一种错误机制。当我们使用 迭代器 迭代元素时,发现集合中有修改元素的情况,则快速做出效应,立即抛出 ConcurrentModificationException 异常。这种修改操作可能是其他线程在修改,也有可能是当前的线程自已修改导致的(例如:在 迭代过程中 调用 remove 方法等)。

直接附上代码:

  static final String DELETE_KEY = "aa";
    
    public static void main(String[] args) {

        final Set all = new HashSet<>();

        all.add("aa");
        all.add("bb");
        all.add("cc");
        all.add("111");
        all.add("222");
        all.add("333");

        for (String s : all) {
            if(DELETE_KEY.equals(s)){
                // 这里会报错
                all.remove(DELETE_KEY);
            }
        }
}

结果:


fail_fast.png
  1. 注意:
    并不是所有的 JAVA 集合容器都有 fail-fast 这个机制,如: ConcurrentHashMap 等 就是没得。

  2. 实现原理

查看 HashMap 源码 (因为内部使用 HashMap 来存储数据)可以得出,有一个 属性

/**
    *   被修改的次数
    *
    * The number of times this HashMap has been structurally modified
    * Structural modifications are those that change the number of mappings in
    * the HashMap or otherwise modify its internal structure (e.g.,
    * rehash).  This field is used to make iterators on Collection-views of
    * the HashMap fail-fast.  (See ConcurrentModificationException).
    */
   transient int modCount;

每次对 集合容器中的元素先进性修改时,这个 值 都会加 1,在遍历前记录这个属性到 expectedModCount 属性中,遍历中检查检查 expectedModCount 与 modCount 是否一致,如果不一致就说明 有修改线程修改的元素,则抛出 ConcurrentModificationException 异常。

回答问题

通过 查看源码知道 , 在使用 HashSet 集合作为存储容器的时候,操作 HashSet 容器 在内部 都是 间接的操作 HashMap 。

  1. HashSet 怎么保证元素不重复?

回答:HashSet 内部使用 HashMap 来存储数据,key 为元素本身对象,value 都是一样值;HashMap 的 key 不能重复。

  1. HashSet 能否添加null元素数据?

回答:HashSet 只能允许有一个元素为 null 元素,因为 HashMap 的 key 允许为空。

  1. HashSet 添加元素是有顺序的吗?

回答:HashSet 是无顺序的,因为 HashMap 的 key 是无顺序。

  1. HashSet 是线程安全的吗(即是同步的吗)?

回答:HashSet 不是线程安全的,因为 HashMap 不是线程安全的。

  1. 应用场景是什么?

回答:HashSet 内部使用 HashMap 来存储数据,由于HashMap 的特性,可以用于以下环境中。
不要求线程安全性、元素之间不要求顺序、元素之间不重复、并且不能 通过 get 获取到的场景。

你可能感兴趣的:(集合 HashSet 的使用及源码分析(jdk1.8))