给jdk写注释系列之jdk1.6容器(6)-HashSet源码解析&Map迭代器

  今天的主角是HashSet,Set是什么东东,当然也是一种java容器了。

现在再看到Hash心底里有没有会心一笑呢,这里不再赘述hash的概念原理等一大堆东西了(不懂得需要先回去看下HashMap了),需要在啰嗦一句的是hash表是基于快速存取的角度设计的,也是一种典型的空间换时间的做法(这个在分析HashMap中都有讲过)。那么今天的HashSet它又是怎么一回事的,他的存在又是为了解决什么问题呢?

先来看下Set的特点:Set元素无顺序,且元素不可以重复。 。想到了什么?无顺序,由于散列的缘故;不可重复,HashMap的key就是不能重复的。是的,你有想对了。HashSet就是基于HashMap的key来实现的,整个HashSet中基本所有方法都是调用的HashMap的方法。利用HashMap可以实现两个卖点:1.不可重复,2.快速查找(contains)

一起来看下吧:

1.定义

1 public class HashSet<E>
2     extends AbstractSet<E>
3     implements Set<E>, Cloneable, java.io.Serializable

  我们看到HashSet继承了AbstractSet抽象类,并实现了Set、Cloneable、Serializable接口。AbstractSet是一个抽象类,对一些基础的set操作进行封装。继续来看下Set接口的定义:

 1 public interface Set<E> extends Collection<E> {
 2     // Query Operations
 3     int size();
 4     boolean isEmpty();
 5     boolean contains(Object o);
 6     Iterator<E> iterator();
 7     Object[] toArray();
 8     <T> T[] toArray(T[] a);
 9     // Modification Operations
10     boolean add(E e);
11     boolean remove(Object o);
12     // Bulk Operations
13     boolean containsAll(Collection<?> c);
14     boolean addAll(Collection<? extends E> c);
15     boolean retainAll(Collection<?> c);
16     boolean removeAll(Collection<?> c);
17     void clear();
18     // Comparison and hashing
19     boolean equals(Object o);
20     int hashCode();
21 }

  发现了什么,Set接口和java.util.List接口一样也实现了Collection接口,但是Set和List所不同的是,Set没有get等跟下标先关的一些操作方法,那怎么取值呢?Iterator还记得吗,使用迭代器对不对。(不明白的回去看Iterator讲解

2.底层存储

1     // 底层使用HashMap来保存HashSet的元素
2     private transient HashMap<E,Object> map;
3
4     // Dummy value to associate with an Object in the backing Map
5     // 由于Set只使用到了HashMap的key,所以此处定义一个静态的常量Object类,来充当HashMap的value
6     private static final Object PRESENT = new Object();

  看到这里就明白了,和我们前面说的一样,HashSet是用HashMap来保存数据,而主要使用到的就是HashMap的key。

  看到private static final Object PRESENT = new Object();不知道你有没有一点疑问呢。这里使用一个静态的常量Object类来充当HashMap的value,既然这里map的value是没有意义的,为什么不直接使用null值来充当value呢?比如写成这样子private final Object PRESENT = null;我们都知道的是,Java首先将变量PRESENT分配在栈空间,而将new出来的Object分配到堆空间,这里的new Object()是占用堆内存的(一个空的Object对象占用8byte),而null值我们知道,是不会在堆空间分配内存的。那么想一想这里为什么不使用null值。想到什么吗,看一个异常类java.lang.NullPointerException, 噢买尬,这绝对是Java程序员的一个噩梦,这是所有Java程序猿都会遇到的一个异常,你看到这个异常你以为很好解决,但是有些时候也不是那么容易解决,Java号称没有指针,但是处处碰到NullPointerException。所以啊,为了从根源上避免NullPointerException的出现,浪费8个byte又怎么样,在下面的代码中我再也不会写这样的代码啦if (xxx == null) { ... } else {....},好爽。

3.构造方法

 1  /**
 2      * 使用HashMap的默认容量大小16和默认加载因子0.75初始化map,构造一个HashSet
 3      */
 4     public HashSet() {
 5         map = new HashMap<E,Object>();
 6     }
 7
 8     /**
 9      * 构造一个指定Collection参数的HashSet,这里不仅仅是Set,只要实现Collection接口的容器都可以
10      */
11     public HashSet(Collection<? extends E> c) {
12         map = new HashMap<E,Object>(Math. max((int) (c.size()/.75f) + 1, 16));
13        // 使用Collection实现的Iterator迭代器,将集合c的元素一个个加入HashSet中
14        addAll(c);
15     }
16
17     /**
18      * 使用指定的初始容量大小和加载因子初始化map,构造一个HashSet
19      */
20     public HashSet( int initialCapacity, float loadFactor) {
21         map = new HashMap<E,Object>(initialCapacity, loadFactor);
22     }
23
24     /**
25      * 使用指定的初始容量大小和默认的加载因子0.75初始化map,构造一个HashSet
26      */
27     public HashSet( int initialCapacity) {
28         map = new HashMap<E,Object>(initialCapacity);
29     }
30
31     /**
32      * 不对外公开的一个构造方法(默认default修饰),底层构造的是LinkedHashMap,dummy只是一个标示参数,无具体意义
33      */
34     HashSet( int initialCapacity, float loadFactor, boolean dummy) {
35         map = new LinkedHashMap<E,Object>(initialCapacity, loadFactor);
36 }

  从构造方法可以很轻松的看出,HashSet的底层是一个HashMap,理解了HashMap后,这里没什么可说的。只有最后一个构造方法有写区别,这里构造的是LinkedHashMap,该方法不对外公开,实际上是提供给LinkedHashSet使用的,而第三个参数dummy是无意义的,只是为了区分其他构造方法。

4.增加和删除

    /**
     * 利用HashMap的put方法实现add方法
     */
    public boolean add(E e) {
        return map .put(e, PRESENT)== null;
    }

    /**
     * 利用HashMap的remove方法实现remove方法
     */
    public boolean remove(Object o) {
        return map .remove(o)==PRESENT;
    }

    /**
     * 添加一个集合到HashSet中,该方法在AbstractCollection中
     */
    public boolean addAll(Collection<? extends E> c) {
        boolean modified = false;
       // 取得集合c迭代器Iterator
       Iterator<? extends E> e = c.iterator();
       // 遍历迭代器
        while (e.hasNext()) {
           // 将集合c的每个元素加入到HashSet中
           if (add(e.next()))
              modified = true;
       }
        return modified;
    }

    /**
     * 删除指定集合c中的所有元素,该方法在AbstractSet中
     */
    public boolean removeAll(Collection<?> c) {
        boolean modified = false;

        // 判断当前HashSet元素个数和指定集合c的元素个数,目的是减少遍历次数
        if (size() > c.size()) {
            // 如果当前HashSet元素多,则遍历集合c,将集合c中的元素一个个删除
            for (Iterator<?> i = c.iterator(); i.hasNext(); )
                modified |= remove(i.next());
        } else {
            // 如果集合c元素多,则遍历当前HashSet,将集合c中包含的元素一个个删除
            for (Iterator<?> i = iterator(); i.hasNext(); ) {
                if (c.contains(i.next())) {
                    i.remove();
                    modified = true;
                }
            }
        }
        return modified;
}

5.是否包含

 1     /**
 2      * 利用HashMap的containsKey方法实现contains方法
 3      */
 4     public boolean contains(Object o) {
 5         return map .containsKey(o);
 6     }
 7
 8     /**
 9      * 检查是否包含指定集合中所有元素,该方法在AbstractCollection中
10      */
11     public boolean containsAll(Collection<?> c) {
12        // 取得集合c的迭代器Iterator
13        Iterator<?> e = c.iterator();
14        // 遍历迭代器,只要集合c中有一个元素不属于当前HashSet,则返回false
15         while (e.hasNext())
16            if (!contains(e.next()))
17                return false;
18         return true;
19 }

  

  由于HashMap基于hash表实现,hash表实现的容器最重要的一点就是可以快速存取,那么HashSet对于contains方法,利用HashMap的containsKey方法,效率是非常之快的。在我看来,这个方法也是HashSet最核心的卖点方法之一。

6.容量检查

 1 /**
 2      * Returns the number of elements in this set (its cardinality).
 3      *
 4      * @return the number of elements in this set (its cardinality)
 5      */
 6     public int size() {
 7         return map .size();
 8     }
 9
10     /**
11      * Returns <tt>true</tt> if this set contains no elements.
12      *
13      * @return <tt> true</tt> if this set contains no elements
14      */
15     public boolean isEmpty() {
16         return map .isEmpty();
17     }

  以上代码都很简单,因为基本都是基于HashMap实现,只要理解了HashMap,HashSet理解起来真的是小菜一碟了。

那么HashSet就结束了。。。等等,不对还有一个东西,那就是迭代器,在HashMap和LinkedHashMap中都说过,这两个的迭代器实现都要依赖Set接口,下面就让我们先看下HashSet的迭代器吧。

7.迭代器

 

     7.1 HashMap的迭代器

 

     在《Iterator设计模式》中,我们分析了,实现Iterator迭代器的几个角色,并且自己简单实现了一个。而且我们看到Collection实现了Iterable接口,并且要求其子类实现一个返回Iterator接口的iterator()方法。那么既然HashSet是Collection的孙子类,那么HashSet也应该实现了一个返回Iterator接口的iterator()方法,对不对,我们去看看。

 1     /**
 2      * Returns an iterator over the elements in this set.  The elements
 3      * are returned in no particular order.
 4      *
 5      * @return an Iterator over the elements in this set
 6      * @see ConcurrentModificationException
 7      */
 8     public Iterator<E> iterator() {
 9         return map .keySet().iterator();
10     }

  我cha,咋回事,HashSet的iterator()方法竟然也是利用HashMap实现的,我们去看看HashMap的keySet()方法是什么鬼。

1 public Set<K> keySet() {
2         Set<K> ks = keySet;
3         return (ks != null ? ks : (keySet = new KeySet()));
4 }

  HashMap的keySet()方法的返回值竟然是一个Set,具体实现是一个叫KeySet的东东,KeySet又是什么鬼。

 1 private final class KeySet extends AbstractSet<K> {
 2         public Iterator<K> iterator() {
 3             return newKeyIterator();
 4         }
 5         public int size() {
 6             return size ;
 7         }
 8         public boolean contains(Object o) {
 9             return containsKey(o);
10         }
11         public boolean remove(Object o) {
12             return HashMap.this.removeEntryForKey(o) != null;
13         }
14         public void clear() {
15             HashMap. this.clear();
16         }
17 }

  哦,KeySet是一个实现了AbstractSet的HashMap的内部类。而KeySet的iterator()方法返回的是一个newKeyIterator()方法,好绕好绕,头晕了。

1 Iterator<K> newKeyIterator()   {
2         return new KeyIterator();
3 }

  newKeyIterator()方法返回的又是一个KeyIterator()方法,what are you 弄啥嘞?

1 private final class KeyIterator extends HashIterator<K> {
2         public K next() {
3             return nextEntry().getKey();
4         }
5 }

  好吧,不想说什么了,继续往下看吧。

 1 private abstract class HashIterator<E> implements Iterator<E> {
 2         // 下一个需要返回的节点
 3         Entry<K,V> next;   // next entry to return
 4         int expectedModCount ;     // For fast-fail
 5         int index ;          // current slot
 6         // 当前需要返回的节点
 7         Entry<K,V> current;// current entry
 8
 9         HashIterator() {
10             expectedModCount = modCount ;
11             if (size > 0) { // advance to first entry
12                 Entry[] t = table;
13                // 初始化next参数,将next赋值为HashMap底层的第一个不为null节点
14                 while (index < t.length && ( next = t[index ++]) == null)
15                     ;
16             }
17         }
18
19         public final boolean hasNext() {
20             return next != null;
21         }
22
23         final Entry<K,V> nextEntry() {
24             if (modCount != expectedModCount)
25                 throw new ConcurrentModificationException();
26             // 取得HashMap底层数组中链表的一个节点
27             Entry<K,V> e = next;
28             if (e == null)
29                 throw new NoSuchElementException();
30
31             // 将next指向下一个节点,并判断是否为null
32             if ((next = e.next) == null) {
33                 Entry[] t = table;
34                 // 如果为null,则遍历真个数组,知道取得一个不为null的节点
35                 while (index < t.length && ( next = t[index ++]) == null)
36                     ;
37             }
38            current = e;
39            // 返回当前节点
40             return e;
41         }
42
43         public void remove() {
44             if (current == null)
45                 throw new IllegalStateException();
46             if (modCount != expectedModCount)
47                 throw new ConcurrentModificationException();
48             Object k = current.key ;
49             current = null;
50             HashMap. this.removeEntryForKey(k);
51             expectedModCount = modCount ;
52         }
53
54 }

  最终找到了HashIterator这个类(也是HashMap的内部类),好累。。。主要看下nextEntry()这个方法,该方法主要思路是,首选拿去HashMap低层数组中第一个不为null的节点,每次调用迭代器的next()方法,就用该节点next一下,当当前节点next到最后为null,就拿数组中下一个不为null的节点继续遍历。什么意思呢,就是循环从数组第一个索引开始,遍历整个Hash表。

至于你问我Iterator实现起来本来挺容易的一件事,为什么HashMap搞得这么复杂,我只想说不要问我,我也不知道。。。

当然map是一个k-v键值对的容器,除了有对key的迭代keySet(),当然还有对value的迭代values(为什么value的迭代不是返回Set,因为value是可以重复的嘛),还有对整个键值对k-v的迭代entrySet(),和上面的代码都是一个原理,这里就不多讲了。

     7.2 LinkedHashMap的迭代器

看完HashMap的Iterator实现,再来看下LinkedHashMap是怎么实现的吧(不从头开始找了,直接看最核心代码吧)。

 1 private abstract class LinkedHashIterator<T> implements Iterator<T> {
 2        // header.after为LinkedHashMap双向链表的第一个节点,因为LinkedHashMap的header节点不保存数据
 3        Entry<K,V> nextEntry    = header .after;
 4        // 最后一次返回的节点
 5        Entry<K,V> lastReturned = null;
 6
 7         /**
 8         * The modCount value that the iterator believes that the backing
 9         * List should have.  If this expectation is violated, the iterator
10         * has detected concurrent modification.
11         */
12         int expectedModCount = modCount;
13
14         public boolean hasNext() {
15             return nextEntry != header;
16        }
17
18         public void remove() {
19            if (lastReturned == null)
20                throw new IllegalStateException();
21            if (modCount != expectedModCount)
22                throw new ConcurrentModificationException();
23
24             LinkedHashMap. this.remove(lastReturned .key);
25             lastReturned = null;
26             expectedModCount = modCount ;
27        }
28
29        Entry<K,V> nextEntry() {
30            if (modCount != expectedModCount)
31                throw new ConcurrentModificationException();
32             if (nextEntry == header)
33                 throw new NoSuchElementException();
34
35             // 将要返回的节点nextEntry赋值给lastReturned
36             // 将nextEntry赋值给临时变量e(因为接下来nextEntry要指向下一个节点)
37             Entry<K,V> e = lastReturned = nextEntry ;
38             // 将nextEntry指向下一个节点
39             nextEntry = e.after ;
40             // 放回当前需返回的节点
41             return e;
42        }
43 }

  可以看出LinkedHashMap的迭代器,不在遍历真个Hash表,而只是遍历其自身维护的双向循环链表,这样就不在需要对数组中是否为空节点进行的判断。所以说LinkedHashMap在迭代器上的效率面通常是高与HashMap的,既然这里是通常,那么什么时候不通常呢,那就是HashMap中元素较少,分布均匀,没有空节点的时候。

Map的迭代器源码读起来比较不太容易懂(主要是各种调用,各种内部类,核心代码不好找),但是找到核心代码后,逻辑原理也就很容易看懂了,当然前提是建立在了解了HashMap和LinkedHashMap的底层存储结构。

额,这一篇确实是讲HashSet的,不是讲Map,这算不算走题了。。。

HashSet 完!

参见:

给jdk写注释系列之jdk1.6容器(4)-HashMap源码解析

给jdk写注释系列之jdk1.6容器(3)-Iterator设计模式

时间: 2024-10-02 10:11:13

给jdk写注释系列之jdk1.6容器(6)-HashSet源码解析&Map迭代器的相关文章

给jdk写注释系列之jdk1.6容器(12)-PriorityQueue源码解析

PriorityQueue是一种什么样的容器呢?看过前面的几个jdk容器分析的话,看到Queue这个单词你一定会,哦~这是一种队列.是的,PriorityQueue是一种队列,但是它又是一种什么样的队列呢?它具有着什么样的特点呢?它的底层实现方式又是怎么样的呢?我们一起来看一下. PriorityQueue其实是一个优先队列,什么是优先队列呢?这和我们前面讲的先进先出(First In First Out )的队列的区别在于,优先队列每次出队的元素都是优先级最高的元素.那么怎么确定哪一个元素的优

给jdk写注释系列之jdk1.6容器(2)-LinkedList源码解析

LinkedList是基于链表结构的一种List,在分析LinkedList源码前有必要对链表结构进行说明. 1.链表的概念 链表是由一系列非连续的节点组成的存储结构,简单分下类的话,链表又分为单向链表和双向链表,而单向/双向链表又可以分为循环链表和非循环链表,下面简单就这四种链表进行图解说明.           1.1.单向链表 单向链表就是通过每个结点的指针指向下一个结点从而链接起来的结构,最后一个节点的next指向null.      1. 2.单向循环链表           单向循环

给jdk写注释系列之jdk1.6容器(5)-LinkedHashMap源码解析

前面分析了HashMap的实现,我们知道其底层数据存储是一个hash表(数组+单向链表).接下来我们看一下另一个LinkedHashMap,它是HashMap的一个子类,他在HashMap的基础上维持了一个双向链表(hash表+双向链表),在遍历的时候可以使用插入顺序(先进先出,类似于FIFO),或者是最近最少使用(LRU)的顺序. 来具体看下LinkedHashMap的实现. 1.定义  1 public class LinkedHashMap<K,V> 2 extends HashMap&

给jdk写注释系列之jdk1.6容器(13)-总结篇之Java集合与数据结构

是的,这篇blogs是一个总结篇,最开始的时候我提到过,对于java容器或集合的学习也可以看做是对数据结构的学习与应用.在前面我们分析了很多的java容器,也接触了好多种常用的数据结构,今天我们就来总结下这些内容. 下面我们以数据结构的维度来总结下,在Java集合的实现过程中,底层到底使用了哪些常用的数据结构中,他们分别又有什么特点.      1. 数组(Array) 结构说明:在程序设计中,为了处理方便, 把具有相同类型的若干变量按有序的形式组织起来.这些按序排列的同类数据元素的集合称为数组

给jdk写注释系列之jdk1.6容器(1):ArrayList源码解析

原文出自吞噬天地,链接整理自ImportNew 给jdk写注释系列之jdk1.6容器(2):LinkedList源码解析 给jdk写注释系列之jdk1.6容器(3):Iterator设计模式 给jdk写注释系列之jdk1.6容器(4)-HashMap源码解析 给jdk写注释系列之jdk1.6容器(5)-LinkedHashMap源码解析 给jdk写注释系列之jdk1.6容器(6)-HashSet源码解析&Map迭代器 给jdk写注释系列之jdk1.6容器(1):ArrayList源码解析 工作中

给jdk写注释系列之jdk1.6容器(10)-Stack&amp;Vector源码解析

前面我们已经接触过几种数据结构了,有数组.链表.Hash表.红黑树(二叉查询树),今天再来看另外一种数据结构:栈. 什么是栈呢,我就不找它具体的定义了,直接举个例子,栈就相当于一个很窄的木桶,我们往木桶里放东西,往外拿东西时会发现,我们最开始放的东西在最底部,最先拿出来的是刚刚放进去的.所以,栈就是这么一种先进后出( First In Last Out,或者叫后进先出) 的容器,它只有一个口,在这个口放入元素,也在这个口取出元素. 栈最主要了两个动作就是入栈和出栈操作,其实还是很容易的明白的对不

给jdk写注释系列之jdk1.6容器(1)-ArrayList

工作中经常听到别人讲“容器”,各种各样的容器,话说到底什么是容器,通俗的讲“容器就是用来装东西的器皿,比如:水桶就是用来盛水的,水桶就是一个容器.” ok,在我们写程序的时候常常要对大量的对象进行管理,比如查询,遍历,修改等.jdk为我们提供的容器位于java.util包,也是我们平时用的最多的包之一. 但是为什么不用数组(其实也不是不用,只是不直接用)呢,因为数组的长度需要提前确定,而且不能改变大小,用起来手脚受限嘛. 下面步入正题,首先我们想,一个对象管理容器需要哪些功能?增加,删除,修改,

Feign 系列(05)Spring Cloud OpenFeign 源码解析

Feign 系列(05)Spring Cloud OpenFeign 源码解析 [TOC] Spring Cloud 系列目录(https://www.cnblogs.com/binarylei/p/11563952.html#feign) 在 上一篇 文章中我们分析 Feign 参数解析的整个流程,Feign 原生已经支持 Feign.JAX-RS 1/2 声明式规范,本文着重关注 Spring Cloud 是如果整合 OpenFeign 的,使之支持 Spring MVC? 1. Sprin

vue系列---响应式原理实现及Observer源码解析(一)

_ 阅读目录 一. 什么是响应式? 二:如何侦测数据的变化? 2.1 Object.defineProperty() 侦测对象属性值变化 2.2 如何侦测数组的索引值的变化 2.3 如何监听数组内容的增加或减少? 2.4 使用Proxy来实现数据监听 三. Observer源码解析 回到顶部 一. 什么是响应式? 我们可以这样理解,当一个数据状态发生改变的时候,那么与这个数据状态相关的事务也会发生改变.用我们的前端专业术语来讲,当我们JS中的对象数据发生改变的时候,与JS中对象数据相关联的DOM