写在前面:Set在《On Java 8》这本书里被称为集合,也是非常基础的集合类型了(嘿,Set叫集合,Collection也叫集合,会不会搞混呢)。但是有关于Set的面试题很少,日常业务中也很少用到,那么想搞清楚set有什么好处呢?于是张三的面试开始了。有关于张三的背景介绍参考:张三背景介绍
面试官:先来个自我介绍吧。
张三:!@#¥%……&*。
面试官:HashSet了解吗?
张三:了解的,HashSet是一种无序不重复的集合。
面试官:那它是怎么保证不可重复的呢?
张三:HashSet的底层是HashMap,通过Map的key不可重复的特性保证Set不不重复的。
面试官:那么既然HashSet的底层是HashMap,那我们用就用HashMap不行吗?为什么还要有HashSet呢?
来自灵魂的发问,张三懵了,他要这么写我怎么知道……
张三:可能用HashSet的效率比HashMap高吧。
面试官:HashSet都是封装了一层了,怎么会比HashMap效率高呢?
张三:啊……那我就不清楚了
有关于Set的面试结束了,其他的面试过程省略
面试官:这次的面试就先到这里,后面会有我们的HR联系你。
张三:好的……
----------------------------------------------------------------------
张三被问懵了很正常,可能很多人都没了解过HashSet的底层实现,又有多少人思考过这个问题呢?那我们就伴随着源码和实验逐步分析这个问题。
首先,HashSet的底层是HashMap这句话对吗?
先来看HashSet的无参构造器:
/**
* Constructs a new, empty set; the backing <tt>HashMap</tt> instance has
* default initial capacity (16) and load factor (0.75).
*/
public HashSet() {
map = new HashMap<>();
}
好家伙,上来就给我们new了一个HashMap,注释还写的明明白白,我就是靠HashMap实现的,默认容量16,负载因子0.75。
那好,我们再来看看HashSet的基本添加操作add()方法:
public boolean add(E e) {
return map.put(e, PRESENT)==null;
}
你这代码也太省事了吧,把传进来的值当成HashMap的key,放入一个叫PRESENT的空Object对象做value,还真就完全使用map来解决问题吗?
那我们再来看看判断Set大小的size()方法:
public int size() {
return map.size();
}
再看看判空方法呢:
public boolean isEmpty() {
return map.isEmpty();
}
看到这,再随意瞟一眼其他源码,全是和定义出的map这个对象有关,那我们还学个啥,就学HashMap完事了呗--果然,面试喜欢问HashMap是有原因的。
有关于HashMap的知识点,本篇不讲,上一篇也只是讲了一部分知识点,具体的大家一定要自己好好掌握。
那么我们基本清楚HashSet的底层就是靠HashMap来实现的了。但是我们不得不思考一个问题,HashSet实现了Set接口,而Set实现了Collection接口。但是Map接口和Collection接口并没有什么关系,而HashSet底层又用到了HashMap,不觉得这关系有一点那么说不清道不明吗?
然而源码就是这样写的,至于是写源码的人偷懒还是有什么特殊的说法我也是还没有找到结论。
那到这里我们只能明白,HashSet的底层是HashMap,把HashMap搞明白了,HashSet甚至不用刻意去了解(所以为什么面试喜欢问Map!!!)。
所以这里我们走不下去了,就来试试效率问题吧。
同样的,我们编写简单的代码进行试验--注意我们的代码参考了书上关于随机数的例子,保证了每次随机的值相同,控制变量。(java中的随机算法其实是假随机,是不是又学到了一招)
测试HashSet代码:
@Test
public void testHashSetTime() {
Random rand = new Random(47);
Set<Integer> intset = new HashSet<>();
DateTime hashSetStartTime = new DateTime();
for (int i = 0; i < 1000000000; i++) {
intset.add(rand.nextInt(30));
}
DateTime hashSetEndTime = new DateTime();
System.out
.println("HashSet用时:" + (hashSetEndTime.getMillis() - hashSetStartTime.getMillis()) + "ms");
System.out.println(JSON.toJSONString(intset));
}
三次耗时的时间:
测试HashMap代码--既然HashSet的源码里面用的是空对象做value,那我们也这么做:
@Test
public void testHashMapTime() {
Random rand = new Random(47);
Map<Integer, Object> map = new HashMap<>();
DateTime hashMaptStartTime = new DateTime();
for (int i = 0; i < 1000000000; i++) {
map.put(rand.nextInt(30), PRESENT);
}
DateTime hashMapEndTime = new DateTime();
System.out.println(
"HashMap用时:" + (hashMapEndTime.getMillis() - hashMaptStartTime.getMillis()) + "ms");
System.out.println(JSON.toJSONString(map));
}
三次代码时间:
虽然在这里HashMap要时间长那么一点点呢,但是经过我的多次验证,各种改参数,最后得出的结论还是两者的时间是差不多的。可以验证HashSet的底层是HashMap的结论正确。
那么既然HashSet底层是HashMap,到底为什么有了HashMap还要有HashSet呢?HashSet存在的意义是什么呢?
作者查了半天资料也没查出个结论来,思考了一下,set的插入会返回一个boolean值来判断有没有插入重复的值,而map没有返回boolean值,根据源码来看,光使用map也是可以实现的。所以我认为,也许HashSet存在的意义仅仅只是为了遇到需要这种数据结构下的时候操作方便吧,重点还是应该了解map。
那么HashMap说完了,我们来看看TreeSet的构造函数源码:
public TreeSet() {
this(new TreeMap<E,Object>());
}
再来看看LinkedHashSet的呢:
public LinkedHashSet() {
super(16, .75f, true);
}
HashSet(int initialCapacity, float loadFactor, boolean dummy) {
map = new LinkedHashMap<>(initialCapacity, loadFactor);
}
哈,这个LinkedHashMap又来了,不清楚LinkedHashMap的同学建议花时间补一下缺少的知识。
好吧,整一个Set家族都是这样的,我们了解了Set的底层就是通过Map来实现的,但是还是不能确定这样设计是出于什么考虑。非要说用Set会比用Map好在哪的话,我觉得只能说在需要存储这种无序不可重复的集合的时候,用Set写的代码会比较少且清晰吧,毕竟虽然我们完全能够用Map实现Set的功能,但无论是插入还是打印都写起来还是有点麻烦的。
虽然感觉张三被面试官摆了一道,但是通过对底层源码的剖析以及自己的编码尝试,对Set的底层有了更深刻的认识,以后任何操作在脑海里都很清晰了。并且对HashMap的重要程度有了新的看法,果然Map在面试中是绕不开的啊。
有关于本系列博客的全部代码在github上同步更新
张三又学到了许多,我们下周再见。