HashSet:实不相瞒,我就是个套壳 HashMap程序员
来自专辑
我有点儿基础
古时的风筝第 82 篇原创文章 面试
做者 | 风筝
公众号:古时的风筝(ID:gushidefengzheng)
转载请联系受权,扫码文末二维码加微信算法
据说公众号又改版了,以前推送不是按时间线来了,也就是你在订阅号消息中看到的推送是按照某种推荐规则来的,而不是按发文顺序,这致使若是你们不往下多翻翻,可能都看不到某些号的推文了,好比我这个小号。(除非加星标)缓存
这两天的改变是公众号底部除了「在看」外,增长了「赞」和「分享」这两个功能,这个意思就是告诉各位同窗,你好不容易能看到个人文章一回,不三连一下分享、点赞、在看,实在于心不忍吧。着急的话,赶忙滑到底部 Trible Kill 一下,不着急的话,能够看完文章再说。不差这几分钟的。安全
正文开始微信
以前的 7000 字说清楚 HashMap 已经详细介绍了 HashMap 的原理和实现,本次再来讲说他的同胞兄弟 HashSet,这两兄弟常常被拿出来一块儿说,面试的时候,也常常是二者结合着考察。难道它们两个的实现方式很相似吗,否则为何老是放在一块儿比较。数据结构
实际上并非由于它俩类似,从根本上来讲,它俩原本就是同一个东西。再说的清楚明白一点, HashSet 就是个套了壳儿的 HashMap。所谓君子善假于物,HashSet 就假了 HashMap。既然你 HashMap 都摆在那儿了,那我 HashSet 何须重复造轮子,借你同样,何不美哉!多线程
HashSet并发
下面是 HashSet的继承关系图,仍是老样子,咱们看一个数据结构的时候先看它的继承关系图。与 HashSet并列的还有 TreeSet,另外 HashSet 还有个子类型 LinkedHashSet,这个咱们后面再说。ide
HashSet 继承关系
套壳 HashMap
为啥这么说呢,在我第一次看 HashSet源码的时候,已经准备好了笔记本,拿好了圆珠笔,准备好好探究一下 HashSet的神奇所在。可当我按着Ctrl+鼠标左键进入源码的构造函数的时候,我觉得我走错了地方,这构造函数有点简单,甚至还有点神奇。new 了一个 HashMap而且赋给了 map 属性。
private transient HashMap<E,Object> map; public HashSet() { map = new HashMap<>(); }
再三确认没看错的状况下,我明白了,HashSet就是在HashMap的基础上套了个壳儿,咱们用的是个HashSet,实际上它的内部存储逻辑都是 HashMap的那套逻辑。
除了上面的那个无参类型的构造方法,还有其余的有参构造方法,一看便知,其实就是 HashMap包装了一层而已。
public HashSet(Collection<? extends E> c) { map = new HashMap<>(Math.max((int) (c.size()/.75f) + 1, 16)); addAll(c); } public HashSet(int initialCapacity, float loadFactor) { map = new HashMap<>(initialCapacity, loadFactor); } public HashSet(int initialCapacity) { map = new HashMap<>(initialCapacity); }
用法
HashSet应该算是众多数据结构中最简单的一个了,满打满算也就那么几个方法。
public Iterator<E> iterator() { return map.keySet().iterator(); } public int size() { return map.size(); } public boolean isEmpty() { return map.isEmpty(); } public boolean contains(Object o) { return map.containsKey(o); } public boolean add(E e) { return map.put(e, PRESENT)==null; } public boolean remove(Object o) { return map.remove(o)==PRESENT; } public void clear() { map.clear(); }
很简单对不对,就这么几个方法,并且你看每一个方法其实都是对应的操做 map,也就是内部的 HashMap,也就是说只要你懂了 HashMap天然也就懂了 HashSet。
Set接口要求不能有重复项,只要继承了 Set就要遵照这个规定。咱们大多数状况下使用 HashSet也是由于它有去重的功能。
那它是如何办到的呢,这就要从它的 add方法提及了。
// Dummy value to associate with an Object in the backing Map private static final Object PRESENT = new Object(); public boolean add(E e) { return map.put(e, PRESENT)==null; }
HashSet的 add方法其实就是调用了HashMap的put方法,可是咱们都知道 put进去的是一个键值对,可是 HashSet存的不是键值对啊,是一个泛型啊,那它是怎么办到的呢?
它把你要存的值当作 HashMap的 key,而 value 值是一个 final的Object对象,只起一个占位做用。而HashMap自己就不容许重复键,正好被HashSet拿来即用。
如何保证不重复呢
HashMap中不容许存在相同的 key 的,那怎么保证 key 的惟一性呢,判断的代码以下。
if (p.hash == hash && ((k = p.key) == key || (key != null && key.equals(k))))
首先经过 hash 算法算出的值必须相等,算出的结果是 int,因此能够用 == 符号判断。只是这个条件可不行,要知道哈希碰撞是什么意思,有可能两个不同的 key 最后产生的 hash 值是相同的。
而且待插入的 key == 当前索引已存在的 key,或者 待插入的 key.equals(当前索引已存在的key),注意== 和 equals 是或的关系。== 符号意味着这是同一个对象, equals 用来肯定两个对象内容相同。
若是 key 是基本数据类型,好比 int,那相同的值确定是相等的,而且产生的 hashCode 也是一致的。
String 类型算是最经常使用的 key 类型了,咱们都知道相同的字符串产生的 hashCode 也是同样的,而且字符串能够用 equals 判断相等。
可是若是用引用类型当作 key 呢,好比我定义了一个 MoonKey 做为 key 值类型
public class MoonKey { private String keyTile; public String getKeyTile() { return keyTile; } public void setKeyTile(String keyTile) { this.keyTile = keyTile; } @Override public boolean equals(Object o) { if (this == o) return true; if (o == null || getClass() != o.getClass()) return false; MoonKey moonKey = (MoonKey) o; return Objects.equals(keyTile, moonKey.keyTile); } }
而后用下面的代码进行两次添加,你说 size 的长度是 1 仍是 2 呢?
Map<MoonKey, String> m = new HashMap<>(); MoonKey moonKey = new MoonKey(); moonKey.setKeyTile("1"); MoonKey moonKey1 = new MoonKey(); moonKey1.setKeyTile("1"); m.put(moonKey, "1"); m.put(moonKey1, "2"); System.out.println(hash(moonKey)); System.out.println(hash(moonKey1)); System.out.println(m.size());
答案是 2 ,为何呢,由于 MoonKey 没有重写 hashCode 方法,致使 moonkey 和 moonKey1 的 hash 值不可能同样,当不重写 hashCode 方法时,默认继承自 Object的 hashCode 方法,而每一个 Object对象的 hash 值都是独一无二的。
划重点,正确的作法应该是加上 hashCode的重写。
@Override public int hashCode() { return Objects.hash(keyTile); }
这也是为何要求重写 equals 方法的同时,也必须重写 hashCode方法的缘由之一。若是两个对象经过调用equals方法是相等的,那么这两个对象调用hashCode方法必须返回相同的整数。有了这个基础才能保证 HashMap或者HashSet的 key 惟一。
因为HashMap不是线程安全的,天然,HashSet也不是线程安全啦。在多线程、高并发环境中慎用,若是要用的话怎么办呢,不像 HashMap那样有多线程版本的ConcurrentHashMap,不存在 `ConcurrentHashSet
这种数据结构,若是想用的话要用下面这种方式。
Set<String> set = Collections.synchronizedSet(new HashSet<String>());
或者使用 ConcurrentHashMap.KeySetView也能够,可是,这其实就不是 HashSet了,而是 ConcurrentHashMap的一个实现了 Set接口的静态内部类,多线程状况下使用起来彻底没问题。
ConcurrentHashMap.KeySetView<String,Boolean> keySetView = ConcurrentHashMap.newKeySet(); keySetView.add("a"); keySetView.add("b"); keySetView.add("c"); keySetView.add("a"); keySetView.forEach(System.out::println);
若是说 HashSet是套壳儿HashMap,那么LinkedHashSet就是套壳儿LinkedHashMap。对比 HashSet,它的一个特色就是保证数据有序,插入的时候什么顺序,遍历的时候就是什么顺序。
看一下它其中的无参构造函数。
public LinkedHashSet() { super(16, .75f, true); }
LinkedHashSet继承自 HashSet,因此 super(16, .75f, true);是调用了HashSet三个参数的构造函数。
HashSet(int initialCapacity, float loadFactor, boolean dummy) { map = new LinkedHashMap<>(initialCapacity, loadFactor); }
此次不是 new HashMap了,而是 new 了一个 LinkedHashMap,这就是它能保证有序性的关键。LinkedHashMap用双向链表的方式在 HashMap的基础上额外保存了键值对的插入顺序。
HashMap中定义了下面这三个方法,这三个方法是在插入和删除键值对的时候调用的方法,用来维护双向链表,在LinkedHashMap中有具体的实现。
// Callbacks to allow LinkedHashMap post-actions void afterNodeAccess(Node<K,V> p) { } void afterNodeInsertion(boolean evict) { } void afterNodeRemoval(Node<K,V> p) { }
因为LinkedHashMap能够保证键值对顺序,因此,用来实现简单的 LRU 缓存。
因此,若是你有场景既要保证元素无重复,又要保证元素有序,可使用 LinkedHashSet。
其实你掌握了 HashMap就掌握了 HashSet,它没有什么新东西,就是巧妙的利用了 HashMap而已,新不新没关系,好用才是最重要的。
还能够读:
别说你还不懂 HashMap
有趣的图说 HashMap,普通人也能看懂
跟我极速尝鲜 Spring Boot 2.3
公众号:古时的风筝
一个兼具深度与广度的程序员鼓励师,一个本打算写诗却写起了代码的田园码农!你可选择如今就关注我,或者看看历史文章再关注也不迟。
技术交流还能够加群或者直接加我微信。