基于散列的集合简析-HashMap、HashSet、LinkedHashSet、LinkedHashMap

HashMap简析

基于散列的集合中,HashMap应该是用的最多的键值对容器,既然用的这么频繁,我觉得还是很有必要搞清楚原理,而写出来,思路会更清晰。
先看下简单的继承体系:
这里写图片描述
从图上看,继承体系很简单,2个标记性接口,然后就是作为Map的功能,很单纯。
然后再来看看数据结构图:
这里写图片描述
图上一个空格就代表一个节点,而每个key-value对都保存在一个节点中,下面的代码正式HashMap每个节点的数据结构。

static class Entry<K,V> implements Map.Entry<K,V> {
        final K key;
        V value;
        Entry<K,V> next;
        int hash;
    }

从数据结构图判断,HashMap中肯定维护着一个数组,用来存储每个节点,现在就有2个问题,如何在数组中定位自己的位置(行话叫:桶),万一桶被占用了怎么办?
既然叫HashMap,通过名字大概能够猜想得出了,是通过hash值来定位的,通过hash算出自己桶的位置,然而位置是有限的,那么通过函数映射(hash与数组长取模)到其中的位置时,必然的会出现冲突,解决冲突办法,就如同上图所示,链表法。
优化性能的设计
HashMap设计时候需要考虑速度的问题,链表连接的越长性能就越差,极端的例子,退化成单链表,时间复杂度O(N),这样就失去了快速访问的特性。
如果为了速度最快,最好的办法就是没有链表,既然是通过取模的方式进行定位,要达到最少冲突,肯定需要很长的数组,而数组越长,就越容易浪费。
为了达到理想的效果,又不浪费太多的空间,hashmap使用了一个阀值,当对象的数量达到阀值,才会进行扩容,这样就能够保证浪费的空间是在可控的有限范围内。通过调节阀值,可以使得链表的链接的节点尽量的少。
如何定义阀值?hashmap中用了负载因子这么个概念,用数组的容量*负载因子作为阀值,默认负载因子是0.75。可以自行调整,但是一般不建议,优化有时候是个很棘手的问题,不成熟的优化是所有罪恶之源,最好的策略就是置之不顾,直到你发现真正需要优化他。

其他集合的分析

基于散列的集合,除了HashMap外,常用的还有HashSet、LinkedHashSet、LinkedHashMap。而这三个都是在HashMap的基础上进行改造的,只要明白了HashMap的原理,其他的非常容易清楚。
HashSet
我们来简单分析下,在jdk中的源码:

public class HashSet<E>
    extends AbstractSet<E>
    implements Set<E>, Cloneable, java.io.Serializable
{
    static final long serialVersionUID = -5024744406713321676L;

    private transient HashMap<E,Object> map;

    // Dummy value to associate with an Object in the backing Map
    private static final Object PRESENT = new Object();

    /**
     * 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<>();//实例化时,同时实例化HashMap
    }

上面是摘抄HashSet源码的一段,发现没,这是组合,是代理,用hashMap进行代理,下面最后列举下最常用的功能:

    public boolean add(E e) {
        return map.put(e, PRESENT)==null; //PRESENT为常量
    }
    public Iterator<E> iterator() {
        return map.keySet().iterator();
    }

发了没,是委托给HashMap的。
LinkedHashMap
估计 应该都知道,它既能拥有hashmap的速度,又能有序遍历,直接看源码:

public class LinkedHashMap<K,V>
    extends HashMap<K,V>
    implements Map<K,V>

发现没,是继承自HashMap,可以说,复用了大部分的HashMap功能。
思考 :它是如何在hashmap的基础上进行改造,使的元素有序?
我们先看下面这幅图:
这里写图片描述
大家应该都知道了,左边的是hashmap的元素节点,而右边的正是LinkedHashMap的元素节点。发现多了什么了吗?2个指针,指向前驱和指向后继。想下,如果每个元素都能够知道后面是谁,从头开始按照顺序进行元素遍历,这个还难吗?看下面创造节点的代码:

 void createEntry(int hash, K key, V value, int bucketIndex) {
        HashMap.Entry<K,V> old = table[bucketIndex];
        Entry<K,V> e = new Entry<>(hash, key, value, old);
        table[bucketIndex] = e;
        e.addBefore(header);
        size++;
    }
 private void addBefore(Entry<K,V> existingEntry) {
            after  = existingEntry; 
            before = existingEntry.before; //取前一个节点,对于header参数来说就是最后一个节点。
            before.after = this;
            after.before = this;
        }

看见没,createEntry比hashmap多了一个重定位指针的操作。header节点中保存有第二个和最后一个节点的指针(不懂看上图),保证有序,只需要把现有最后一个节点的after 指向本节点,就保证新加入的元素是有序,现有最后一个节点可以通过header.before获取,同时为了下一次新加入节点能正常有序,需要把header的前节点重新地位到新的最后一个节点(也就是说header.before指向新节点)。这样再通过after指针就能顺藤摸瓜,顺序遍历了。
LinkedHashSet
看源码:

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

好像和我们想象的不一样,不应该是LinkedHashMap吗?不急继续看其构造函数

    public LinkedHashSet(int initialCapacity) {
        super(initialCapacity, .75f, true);
    }
        HashSet(int initialCapacity, float loadFactor, boolean dummy) {
        map = new LinkedHashMap<>(initialCapacity, loadFactor);
    }

发现没,HashSet这种构造方法用的是LinkedHashMap,现在还需要写下去吗,如同hashset复用hashMap原理一样。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值