Set之hashset、treeset、hashmap

 

他们的区别主要在他们底层的数据结构不同。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的唯一性。

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值