java HashSet和LinkedHashSet 底层实现和源码分析

国际惯例

上来先了解一下HashSet这东西是个什么来头

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

继承自AbstractSet抽象类,实现了Set接口。该集合内无重复元素且遍历是无序的。基本操作就是add跟remove那些方法,基本不变。

成员变量跟构造方法

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();

 说实话,当我点开的时候我心里有点平静,甚至还差点笑出声,你确定就是这样?

 我一再劝说自己应该是看少了些什么,但是命运就是如此,当我点开构造方法的时候心里也就释然了

public HashSet() {
        map = new HashMap<>();
    }
public HashSet(int initialCapacity) {
        map = new HashMap<>(initialCapacity);
    }
public HashSet(int initialCapacity, float loadFactor) {
        map = new HashMap<>(initialCapacity, loadFactor);
    }
public HashSet(Collection<? extends E> c) {
        map = new HashMap<>(Math.max((int) (c.size()/.75f) + 1, 16));
        addAll(c);
    }
HashSet(int initialCapacity, float loadFactor, boolean dummy) {
        map = new LinkedHashMap<>(initialCapacity, loadFactor);
    }

HashSet的底层由一个HashMap来实现默认大小为0,而参数为集合的时候,默认大小会在集合数量与0.75的倍数加1,同16之间取最大值;且其增删查改都是基于内部维护的HashMap来做对应的操作。因为其底层基于HashMap,可以确定HashSet的遍历也是无序的。但是这样子我们就由疑问了,HashMap允许空键(当然只允许存在一个),也允许不同键内多个空值,但是HashSet只允许非重复元素呀。如果是这样的话,衍生了4个问题:那如果说HashSet的底层是HashMap的话,究竟我们add方法执行的时候,存的元素是在key-value的哪一个?怎样存?怎样避免重复值保存?还有那HashSet允许空值么?我们带着这个问题来看add方法

怎么实现的无重复元素

我们点开add方法一脸的思密达,于是我们再看了看remove。

public boolean add(E e) {
        return map.put(e, PRESENT)==null;
    }
public boolean remove(Object o) {
        return map.remove(o)==PRESENT;
    }

此时我颤抖的嘴角强行微笑,因为,他真的是用map来操作,足见第一二个问题的答案,HashSet调用add方法其实就是将元素作为key,将HashSet内部维护的一个标示对象存入map中,但是这里的比较是否等于null是几个意思?就这样来实现去重判断?

这里我们需要回顾一下HashMap的put方法,因为HashSet本身就是对HashMap的又一层封装,如果不懂的需要回头去看一下我在《java HashMap 底层实现和源码分析》里面分享的内容。这里贴出HashMap的put方法。

public V put(K key, V value) {
        if (table == EMPTY_TABLE) {
            inflateTable(threshold);
        }
        if (key == null)
            return putForNullKey(value);
        int hash = sun.misc.Hashing.singleWordWangJenkinsHash(key);
        int i = indexFor(hash, table.length);
        for (HashMapEntry<K,V> e = table[i]; e != null; e = e.next) {
            Object k;
            if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {
                V oldValue = e.value;
                e.value = value;
                e.recordAccess(this);
                return oldValue;
            }
        }

        modCount++;
        addEntry(hash, key, value, i);
        return null;
    }

private V putForNullKey(V value) {
        for (HashMapEntry<K,V> e = table[0]; e != null; e = e.next) {
            if (e.key == null) {
                V oldValue = e.value;
                e.value = value;
                e.recordAccess(this);
                return oldValue;
            }
        }
        modCount++;
        addEntry(0, null, value, 0);
        return null;
    }

void addEntry(int hash, K key, V value, int bucketIndex) {
        if ((size >= threshold) && (null != table[bucketIndex])) {
            resize(2 * table.length);
            hash = (null != key) ? sun.misc.Hashing.singleWordWangJenkinsHash(key) : 0;
            bucketIndex = indexFor(hash, table.length);
        }

        createEntry(hash, key, value, bucketIndex);
    }

void createEntry(int hash, K key, V value, int bucketIndex) {
        HashMapEntry<K,V> e = table[bucketIndex];
        table[bucketIndex] = new HashMapEntry<>(hash, key, value, e);
        size++;
    }

到这里,我们可以看到,HashMap在做put操作的时候,其实是已经做对应结果返回了。

如果是未存在的key,那么就创建Entry单向链表保存value值(只是在HashSet的环境下我们只关注key就可以,value的值是什么无意义),创建完成返回null;如果是已经存在的key,那么就修改该key对应的hash值在哈希table中的下标Entry,最后返回旧值。

第三个问题的答案便是:HashSet只需要根据HashMap返回的结果,就可以知道现在add所传的对象是否已经存在,并将结果返回。所以,即使HashSet调用add方法返回了false,其实内部交由HashMap去执行时候也是执行过一次put操作了,只是插入的值没有变而已

最后一个问题我想大家也都猜到了,HashSet允许用add插入null值

感觉HashSet寥寥数言,这里顺便介绍下LinkedHashSet吧

LinkedHashSet

不错,唯一成员就是序列化的id

private static final long serialVersionUID = -2851667679971038690L;

 剩下的就是构造函数

public LinkedHashSet(int initialCapacity, float loadFactor) {
        super(initialCapacity, loadFactor, true);
    }
public LinkedHashSet(int initialCapacity) {
        super(initialCapacity, .75f, true);
    }
public LinkedHashSet() {
        super(16, .75f, true);
    }
public LinkedHashSet(Collection<? extends E> c) {
        super(Math.max(2*c.size(), 11), .75f, true);
        addAll(c);
    }

可以确定的是,对内部维护的HashMap的加载因子是使用默认的0.75,且默认的Entry数组大小根据不同的情况确定,无参则是16,有参则是在集合数量的2倍,同11之间取最大值

  • 3
    点赞
  • 12
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值