zoukankan      html  css  js  c++  java
  • HashSet的实现原理

    HashSet定义

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

    HashSet 是一个没有重复元素的集合
    它是由HashMap实现的,不保证元素的顺序,而且HashSet允许使用 null 元素
    HashSet是非同步的。如果多个线程同时访问一个哈希 set,而其中至少一个线程修改了该 set,那么它必须 保持外部同步。这通常是通过对自然封装该 set 的对象执行同步操作来完成的。如果不存在这样的对象,则应该使用 Collections.synchronizedSet 方法来“包装” set。最好在创建时完成这一操作,以防止对该 set 进行意外的不同步访问:

    Set s = Collections.synchronizedSet(new HashSet(...));

    HashSet通过iterator()返回的迭代器是fail-fast的。

    HashSet属性

    // 底层使用HashMap来保存HashSet的元素
        private transient HashMap<E,Object> map;
        // Dummy value to associate with an Object in the backing Map
        // 由于Set只使用到了HashMap的key,所以此处定义一个静态的常量Object类,来充当HashMap的value
        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 {....},好爽。

    HashSet构造函数

    /**
         * 使用HashMap的默认容量大小16和默认加载因子0.75初始化map,构造一个HashSet
         */
        public HashSet() {
            map = new HashMap<E,Object>();
        }
    
        /**
         * 构造一个指定Collection参数的HashSet,这里不仅仅是Set,只要实现Collection接口的容器都可以
         */
        public HashSet(Collection<? extends E> c) {
            map = new HashMap<E,Object>(Math. max((int) (c.size()/.75f) + 1, 16));
           // 使用Collection实现的Iterator迭代器,将集合c的元素一个个加入HashSet中
           addAll(c);
        }
    
        /**
         * 使用指定的初始容量大小和加载因子初始化map,构造一个HashSet
         */
        public HashSet( int initialCapacity, float loadFactor) {
            map = new HashMap<E,Object>(initialCapacity, loadFactor);
        }
    
        /**
         * 使用指定的初始容量大小和默认的加载因子0.75初始化map,构造一个HashSet
         */
        public HashSet( int initialCapacity) {
            map = new HashMap<E,Object>(initialCapacity);
        }
    
        /**
         * 不对外公开的一个构造方法(默认default修饰),底层构造的是LinkedHashMap,dummy只是一个标示参数,无具体意义
         */
        HashSet( int initialCapacity, float loadFactor, boolean dummy) {
            map = new LinkedHashMap<E,Object>(initialCapacity, loadFactor);
    }

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

    API方法摘要


     

    HashSet源码解析(基于JDK1.6.0_45)

    增加和删除

    /**
         * 利用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;
    }

    是否包含

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

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

    容量检查

    /**
         * Returns the number of elements in this set (its cardinality).
         *
         * @return the number of elements in this set (its cardinality)
         */
        public int size() {
            return map .size();
        }
    
        /**
         * Returns <tt>true</tt> if this set contains no elements.
         *
         * @return <tt> true</tt> if this set contains no elements
         */
        public boolean isEmpty() {
            return map .isEmpty();
        }

    HashSet遍历方式

    通过Iterator遍历HashSet

    第一步:根据iterator()获取HashSet的迭代器。
    第二步:遍历迭代器获取各个元素

    // 假设set是HashSet对象
    for(Iterator iterator = set.iterator();
           iterator.hasNext(); ) { 
        iterator.next();
    }

    通过for-each遍历HashSet

    第一步:根据toArray()获取HashSet的元素集合对应的数组。
    第二步:遍历数组,获取各个元素。

    // 假设set是HashSet对象,并且set中元素是String类型
    String[] arr = (String[])set.toArray(new String[0]);
    for (String str:arr)
        System.out.printf("for each : %s
    ", str);

    HashSet示例

    下面我们通过实例学习如何使用HashSet

    import java.util.Iterator;
    import java.util.HashSet;
    
    /*
     * @desc HashSet常用API的使用。
     *
     * @author skywang
     */
    public class HashSetTest {
        public static void main(String[] args) {
            // HashSet常用API
            testHashSetAPIs() ;
        }
        /*
         * HashSet除了iterator()和add()之外的其它常用API
         */
        private static void testHashSetAPIs() {
            // 新建HashSet
            HashSet set = new HashSet();
            // 将元素添加到Set中
            set.add("a");
            set.add("b");
            set.add("c");
            set.add("d");
            set.add("e");
            // 打印HashSet的实际大小
            System.out.printf("size : %d
    ", set.size());
            // 判断HashSet是否包含某个值
            System.out.printf("HashSet contains a :%s
    ", set.contains("a"));
            System.out.printf("HashSet contains g :%s
    ", set.contains("g"));
            // 删除HashSet中的“e”
            set.remove("e");
            // 将Set转换为数组
            String[] arr = (String[])set.toArray(new String[0]);
            for (String str:arr)
                System.out.printf("for each : %s
    ", str);
            // 新建一个包含b、c、f的HashSet
            HashSet otherset = new HashSet();
            otherset.add("b");
            otherset.add("c");
            otherset.add("f");
            // 克隆一个removeset,内容和set一模一样
            HashSet removeset = (HashSet)set.clone();
            // 删除“removeset中,属于otherSet的元素”
            removeset.removeAll(otherset);
            // 打印removeset
            System.out.printf("removeset : %s
    ", removeset);
            // 克隆一个retainset,内容和set一模一样
            HashSet retainset = (HashSet)set.clone();
            // 保留“retainset中,属于otherSet的元素”
            retainset.retainAll(otherset);
            // 打印retainset
            System.out.printf("retainset : %s
    ", retainset);
            // 遍历HashSet
            for(Iterator iterator = set.iterator();iterator.hasNext();) 
                System.out.printf("iterator : %s
    ", iterator.next());
            // 清空HashSet
            set.clear();
            // 输出HashSet是否为空
            System.out.printf("%s
    ", set.isEmpty()?"set is empty":"set is not empty");
        }
    
    }

    运行结果:

    size : 5
    HashSet contains a :true
    HashSet contains g :false
    for each : d
    for each : b
    for each : c
    for each : a
    removeset : [d, a]
    retainset : [b, c]
    iterator : d
    iterator : b
    iterator : c
    iterator : a
    set is empty

    总结

    HashSet和HashMap、Hashtable的区别


     

    参考

    该文为本人学习的笔记,方便以后自己跳槽前复习。参考网上各大帖子,取其精华整合自己的理解而成。集合框架源码面试经常会问,所以解读源码十分必要,希望对你有用。



    文/嘟嘟MD(简书作者)
    原文链接:http://www.jianshu.com/p/c5f85e9c0098
    著作权归作者所有,转载请联系作者获得授权,并标注“简书作者”。
     
  • 相关阅读:
    JavaScript学习笔记(六)----内置对象Global 和Math
    JavaScript学习笔记(五)--- String类型
    线性筛素数
    线性筛欧拉函数
    POJ 3090 Visble Lattice Points
    1091 合唱队形(最长上升子序列模板)
    P1018 乘积最大
    P1020 导弹拦截(弱数据)
    P2757 导弹的召唤(数据加强)
    P1313 计算系数
  • 原文地址:https://www.cnblogs.com/duanxz/p/4393832.html
Copyright © 2011-2022 走看看