他们的区别主要在他们底层的数据结构不同。HashSet使用的HashMap来实现的,而TreeSet使用的TreeMap来实现的。
1、HashSet:
HashSet顾名思义,是与hash值有密切关联的一种集合。特点是元素唯一,无序。HashSet是对HashMap的一层封装。HashSet是如何保证元素唯一的呢?是根据元素的hash值以及调用equals()方法实现的。HashSet在添加元素时,首先会先计算元素的hash值,得到的结果作为一维数组的索引,然后会去调用equals()方法,来比较元素的值是否相同,如果相同,则表示同一个对象,不再添加进去,这样就保证了HashSet的唯一性。(就是重写hashcode和equals方法对key和value的值进行equals比较)
2、TreeSet:
TreeSet是一种树形结构,是一种红黑树,这是一种近似平衡的二叉树。TreeSet除了具有HashSet的唯一性之外,它还具有有序性。同样,TreeSet是对TreeMap的一层封装,所以,在这里介绍一下TreeMap的原理。
TreeMap保证元素唯一的特点与HashMap相似,这里不再赘述。简单介绍一下如何做到有序的。前面说到,TreeSet是对TreeMap的一层封装,所以,TreeMap也是一个红黑树,在添加元素时,有可能会破坏树的平衡,这时会自动的做相应的旋转,来保持树的平衡。就类似于天平那样,要保证这颗树平衡。保证平衡的规则就是:父节点的值要比左子树的值大,比右子树的值小。如果添加进来的一个元素破坏了这种平衡,就会自动左相应的调整,从而保证元素的有序性。对TreeSet的函数调用都会转换成合适的TreeMap方法,所以这里也不再赘述TreeSet了。
注: TreeSet在添加元素时,元素必须有可比较性。由于基本类型的包装类以及String类已经重写了hashCode()和equals()方法,所以可以直接添加,如果添加的是自定义实体类的话,必须要实现Comparable接口,或者自定义一个比较器,实现Comparator接口,才能添加进去。
使用场景:当需要元素唯一时,则使用set。接下来看是否要求有序,有序则用treeset、无序用hashset的效率更高
如果元素具备自然顺序的特性,那么会按照元素自然顺序的特性自动进行排序存储。(包装类和String)
如果添加了没有可比性的元素会报错,因为TreeSet会自动帮我们排序。
TreeSet要注意的事项:
1、往TreeSet添加元素的时候,如果元素本身具备了自然顺序的特性,那么就按照元素自然顺序的特性进行排序存储。
2、往TreeSet添加元素的时候,如果元素本身不具备自然顺序的特性,那么该元素的类必须要实现Comparable接口,把元素的比较规则定义在compareTo(T o)方法上。
元素与元素之间的比较规则:compareTo(T o)方法返回负整数、零或正整数,表示此对象是小于、等于还是大于指定对象。
字符串的比较规则:
情况一:可以找到对应不同的字符,比较的就是对应位置上的字符。
情况二:找不到对应不同的字符,比较的就是字符串的长度。
3、如果比较元素的时候,compareTo(T o)方法返回的是0,那么该元素就被视为重复元素,不允许添加。(注意:TreeSet与HashCode、equals方法是没有任何关系的。)
4、往TreeSet添加元素的时候,如果元素本身没有具备自然顺序的特性,而元素所属的类也没有实现omparable接口,那么必须要在创建TreeSet的时候传入一个比较器。
如何自定义一个比较器:自定义一个类实现Comparator接口即可,把元素与元素之间的规则定义在compare方法内即可。
自定义比较器的格式:
class 类名 implements Comparator{
}
推荐使用:使用比较器(Comparator)
5、往TreeSet添加元素的时候,如果元素本身不具备自然顺序的特性,而元素所属的类已经实现了Comparable接口,再创建TreeSet对象的时候也传入了比较器,那么是以比较器的比较规则优先使用。
两者主要的方法都是继承collection的方法:主要有add()、clear()、clone()、contains()、isEmpty()、iterator()、remove()、size()
------------| Collection 单列集合的根接口
-----------------| List 实现了List接口的结合类,具备的特点:有序,可重复
---------------------- | ArrayList ArrayList底层是维护了一个Object数组来实现的,特点:查询速度快、增删慢。
---------------------- | LinkedList LinkedList底层是使用了链表数据结构实现的。特点:查询速度慢,增删快。
---------------------- | Vector Vector类可以实现可增长的对象数组,底层也是维护了一个Object数组。
-----------------| Set 实现了Set接口的集合类,具备的特点:无序,不可重复
---------------------- | HashSet 底层是使用了哈希表来支持的,特点:存取速度快。
---------------------- | TreeSet 如果元素具备自然顺序的特性,那么会按照元素自然顺序的特性进行排序存储。
————————————————
----------Map(四个主要的实现类)---------------hashmap
---------------hashtable
---------------linkedhashmap
--------------treehashmap
集合的基本分布图:
下面看看hashset和hashmap的区别?
HashSet hashSet = new HashSet();
HashMap hashMap = new HashMap();
hashSet.add("111");
hashMap.put("abc","123");
代码中声明了两个对像,一个hashmap一个hashset。由代码可以知道hashmap添加元素用put且传key-value键值对进行存储、hashset用add方法且只用value进行存储(不存在key的值),这只是表面的区别下面看看分别是怎么实现的?先看下hasshmap的put方法中的putval方法的部分方法:
else { //最后则tab和p不为空,且链不是红黑树,即链为单向链表
for (int binCount = 0; ; ++binCount) {//循环计算该链表的长度
if ((e = p.next) == null) {//如果p.next为null(即循环到p链的最后一个元素了),则直接将传进来的接到最后一个的next去
p.next = newNode(hash, key, value, null);
//链表长度大于8转换为红黑树(调用相应方法)
if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
treeifyBin(tab, hash);
break;
}
//如果节点key存在,则覆盖原来位置的key,同时将原来位置的元素,沿着链表向后移一位(这里是先用e记录该节点然后在下面进行value值的替换)
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
break;
p = e;//将p.next传给下一次循环的p,即向下循环p链
}
}
if (e != null) { // existing mapping for key//这里进行上面记录的相同key的节点的value值的替换
V oldValue = e.value;
if (!onlyIfAbsent || oldValue == null)
e.value = value;
afterNodeAccess(e);
return oldValue;
}
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
可以看出,先是将key算出hash值(可以快速判断两者key的hash值是否相同,不相同则可以直接扔掉(这是基于重新了的hashcode方法)这样就不用去运行花销更大的equals方法了(这就是为什么重写equals方法就要重新hashcode的原因)),此时就去比较两键值对的key的hash值是否相等,如果不相等后面就不用执行了,如果相等则再去两个key的equals是否相等,相等则表明是重复key了。此时就用e记录该节点,后面再用新的value将e的旧的value值进性更新。(这里也就是阐述了两个对象equals相等则hashcode要要求也相等的原则)。(所以说hashmap允许value重复不允许key重复就是每次key一重复就不插入而原则替换掉旧的节点信息,从而一直保持着key的唯一性。)
(注意重写hashcode方法也保证了key的唯一性,我们假设没重写hashcode方法,此时两个equals相等的key(内容相等)但没重写hashcode所以可能导致计算出来的hash值不一样,那么就会将相同内容的key存到不同的桶中导致map中key变的不唯一(相同内容的key,可能因为地址等原因导致没重新的hashcode不同))
下面再看看hashset是怎么实现的:
private static final Object PRESENT = new Object();
public HashSet() {
map = new HashMap<>();
}
public boolean add(E e) {
return map.put(e, PRESENT)==null;
}
从hashset的源码可以看出:hashset底层是new了一个hashmap,然后新new一个对象,然后调用hashmap的put方法,已知add方法只能传一个参数,此时将我们传的参数和新new的对象作为key-value键值对给到hashmap的put方法。这里我们hashset的value值就相当于hashmap中的key,而value就是hashset新new出来的一个空对象。所以hashmap对key的唯一性在hashset中则表现为value的唯一性,所以说hashset是不允许值重复的.。
下面总结下hashmap和hashset的不同:
hashmap插入元素用put方法,且参数是key-value,其中key保持唯一性;而hashset是调用add方法,且参数只是value一个,底层是new一个hashmap然后调用其put方法并以value为key传进去,从而保证了hashset的value的唯一性。