java进阶|HashSet的源码分析

    技术2022-07-12  73

    任何一项集合容器的使用都有着特定的场景,记得我们刚开始学习HashSet这样的集合时,总会记得那句话"HashSet集合的元素是不重复的",然而,过了很久也是这样去记得,但是为什么是不能重复的?是如何保证不能重复的?却在自己的心里打上一个问号?也就有了这篇文章最初想写的想法了。

    一,首先按照以往的风格看下HashSet集合的类继承结构。

    public class HashSet<E> extends AbstractSet<E> implements Set<E>, Cloneable, java.io.Serializable {}

    继承了AbstractSet是为了复用已经提供好的方法,然后实现Set接口实现其增加,删除,判断元素是否存在的方法,实现序列化接口主要在于实现序列化的内容。

    二,集合的特点主要是为了装填元素的,在内存中进行数据的装载,便于操作,这是自己的理解,我们先看下HashSet的构造函数,然后再进一步去分析它的方法的实现。

    public HashSet() { map = new HashMap<>(); }

    new一个HashSet的构造函数,相当于new了一个HashMap键值对集合容器,这也是可以保证HashSet集合是如何保证元素不重复的,关于HashMap的内容,这里先不做内容介绍,因为这里面涉及到的内容太多了,现在分析还有点太早。

    三,下面介绍一下HashSet的add()添加元素的方法。

    public boolean add(E e) { return map.put(e, PRESENT)==null; }   步骤一:    public V put(K key, V value) { return putVal(hash(key), key, value, false, true); }   步骤二:   再往下面分析就涉及到HashMap了,这里就不分析了,记住一点HashMap的put方法的过程,   若已经存在了则直接覆盖,不存在才添加元素进去。

    其实HashSet集合添加元素是将元素E当做hashMap的键,于此同时,HashMap毕竟是一个键值对集合,所以对应的value就整成了一个常量进去,由于HashMap的key是不能够重复的,所以这也是为什么HashSet集合是如何保证集合元素不重复的原因了。

    private static final Object PRESENT = new Object();

    四,集合的添加方法介绍完了,我们继续看下集合的size()方法,这也就是我们常用集合最常用的方法了。

    public int size() { return map.size(); }

    调用的依然是map的size()方法,接下来我们看下HashMap的统计元素个数的方法。

    public int size() { return size; }

    看下下面这段话就明白了size变量的作用

    /** * The number of key-value mappings contained in this map. */ transient int size;

    用来统计键值对集合元素个数的大小的

    五,HashSet的size()方法介绍完了之后,继续看下判断某个元素是否包含在集合中的contains()方法。

    public boolean contains(Object o) { return map.containsKey(o); }

    调用的是HashMap的containsKey方法,主要是判断元素o能否在HashMap这样的键值对集合中能否找到对应的key,因为最初添加元素的时候我们是将元素E当做HashMap的键进行设置的,所以就根据元素o和HashMap的键集合进行比对了,若能找到则返回true,否则返回false。

    final Node<K,V> getNode(int hash, Object key) { Node<K,V>[] tab; Node<K,V> first, e; int n; K k; if ((tab = table) != null && (n = tab.length) > 0 && (first = tab[(n - 1) & hash]) != null) { if (first.hash == hash && // always check first node ((k = first.key) == key || (key != null && key.equals(k)))) return first; if ((e = first.next) != null) { if (first instanceof TreeNode) return ((TreeNode<K,V>)first).getTreeNode(hash, key); do { if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k)))) return e; } while ((e = e.next) != null); } } return null; }

    上面的查找过程,这里就先简单说下吧,首先先判断hash值是否一样

    public boolean containsKey(Object key) { return getNode(hash(key), key) != null; }

    上面的代码是将传入的元素key先进行计算一下它的hash值,然后再调用getNode()方法的实现。然后再根据是否是链表还是树的特点进行循环遍历,若能找到则直接返回对应的元素,若不能找到则直接返回null,然后根据返回的元素与null进行比对,判断是true或者是false。

    六,一般我们对集合进行操作之前首先都会判断是否为空的判断,然后再继续下面的元素操作,集合若为空,再继续操作也没什么意思了,这也算作是前置校验的一种吧,接下来继续看下isEmpty()方法是如何实现的。

    public boolean isEmpty() { return map.isEmpty();     } 步骤一: public boolean isEmpty() { return size == 0; }

    首先调用map的isEmpty()方法,因为整个实现的过程都是基于map进程操作的,所以自然会复用对应的方法实现,然后判断map的size大小是否为0,若为0则说明键值对集合map的容器是空的,里面没有装载任何元素。

    七,由于自己介绍集合的时候主要是偏工作中实用的方法,所以没有用到的或者说不常用的,这里就不过多介绍了,所以这里介绍如何清空对应的元素clear()方法。

    public void clear() { map.clear(); } 步骤一: public void clear() { map.clear(); } 步骤二: public void clear() { Node<K,V>[] tab; modCount++; if ((tab = table) != null && size > 0) { size = 0; for (int i = 0; i < tab.length; ++i) tab[i] = null; } }

    上面的操作还是基于map的操作,首先判断map的size()方法是否大于0,然后循环将map集合的每个元素置为null,然后这样垃圾收集器就会去清理元素为null,这也清空了集合的元素,我们的操作都是基于内存操作的。

    八,总结,整个HashSet的源码分析到这里就结束了,这是自己在分析源码时用到的一些测试数据。

    import java.util.HashSet; import java.util.Set; public class TestHashSet { public static void main(String[] args) { Set<Integer> set=new HashSet<>(); set.add(1); set.add(2); set.add(3); set.add(4); set.add(1); System.out.println("set.size() = " + set.size()); boolean flag = set.contains(1); System.out.println("flag = " + flag); System.out.println("set = " + set.isEmpty()); } }

    到这里说下吧,集合的源码分析文章大部分都分析完了,想了解数组,链表以及栈的源码的可以看以往的历史文章,其实看一篇文章的内容不在于分享者是如何实现的,而在于我们读完了之后我们是如何做的,只有自己觉得,哦,这个是对的,它对我有帮助了,我就是觉得这个是好的,而不是长篇大论介绍一番之后,自己没有看到自己需要的内容,文章简短有简短的好处,看自己喜好,到这里就结束了,觉得有用,顺手帮点个在看。

    Processed: 0.030, SQL: 9