List Map Set源码总结

近期总结

近期还好吧,主要是了解List集合、Map集合、Set集合,对他们源码进行一个了解,对大牛的思想进行一个解析、对其代码进行一个学习,从而进一步提高自己的水平。

List

首先来说List吧。List是咱们最常用的一个容器,他的实现类有很多种,首先就是ArrayList

ArrayList

ArrayList是一个动态数组,在咱们的印象中,数组是声明空间大小后无法进行更改的,但是为什么是动态数组呢?这与ArrayList的扩容操作有关。

在ArrayList的源码中,咱们可以看到ArrayList有一个方法是:grow()也正是这个方法使其进行了一个扩容操作。

咱们查看他的源码:

private void grow(int minCapacity) {
    // overflow-conscious code
    int oldCapacity = elementData.length;
    int newCapacity = oldCapacity + (oldCapacity >> 1);
    if (newCapacity - minCapacity < 0)
        newCapacity = minCapacity;
    if (newCapacity - MAX_ARRAY_SIZE > 0)
        newCapacity = hugeCapacity(minCapacity);
    // minCapacity is usually close to size, so this is a win:
    elementData = Arrays.copyOf(elementData, newCapacity);
}

在这里我们可以看到首先将容量扩容到原来的1.5倍,如果其大小容量仍然不满足所需的的最小容量,那么直接讲所需的最小容量设置为当前的数组的容量,继而进一步的进行比较,如果超过了所限制的最大容量,那么设置其容量为int类型的最大容量。随后进行一个Arrays.copyOf()操作进行一个数组的扩容。

我们观看其源码的过程中会发现内部有很多Arrays.copyOfSystem.arrayCopy()这两个方法,我们可以知道其本质上是一个数组拷贝的过程中,继续深究可以发现,System.arrayCopy()是一个本地方法,而Arrays.copyOf的底层还是System.arrayCopy()方法。这两个方法都可以用于数组的复制,但是System.arrayCopy()也可以用于数组的扩容、移动等操作。

LinkedList

LinkedList是一个双向链表,由于是一个双向链表,那么他无需担心扩容问题,但是在某些方面他的效率比较低,例如查找、修改,在这个过程中需要进行一个遍历,那么遍历的过程往往是其最消耗时间的地方部分,而修改也是需要一个便利的过程。

LinkedList与ArrayList的异同点
  1. ArrayList底层是一个Object类型的数组,而LinkedList底层是一个Object类型的双向链表
  2. ArrayList在理论上空间利用率是100%,但是实际上小于100%,因为它在创建的过程中往往需要声明更大的空间以防溢出,而LinkedList是一个数组,他除了记录数据,还需要进行记录他的关系(上一节点的地址、下一节点的地址等),所以二者空间的利用率都是小于100%的。
  3. ArrayList与LinkedList都是一个线程不安全的List集合,但是其速度比较快。
Set

Set集合是一个不能够不能重复的集合,也就是说在添加进去之后如果重复不会进行添加。

但是Set集合在根本上脱离不了Map集合,也就是说Set集合是Map集合的一种特殊形式。

那么为什么说Set集合是Map集合的一种特殊形式呢?

我们查看源码,查看其无参构造:

public HashSet() {
    map = new HashMap<>();
}

再查看一个Set集合

public TreeSet() {
    this(new TreeMap<E,Object>());
}

我们可以看到其本质上都是调用Map集合,所以我们可以理解为这是一个特殊的Map集合,那么Map是一个K-V的形式,而Set是一个K的形式,二者再插入的时候如何实现,实际上Set内部有一个Object对象PRESENT,这是一个静态的不可修改的对象,这个对象被用于作为一个公共的Value,也就是说Set集合的Key是你输入的数据,而Value是PRESENT这个对象。

那么为什么Set集合不能够重复呢?我们知道Set集合是Map集合的一种特殊形式,Set不能重复其实是因为Map的一个特征:Key不能重复 ,这个特征所导致的,那么Key为什么不能重复?如果重复又会怎么样?咱们还得去Map集合一探究竟。

Map集合

Map集合是咱们最常用的K-V的集合,那么Map集合的底层是什么呢?

HashMap集合的底层:

我们查看Map集合的底层,会发现其本质上是Entry这个对象,在这个对象中进行记录数组的Key Value Hash next,到后来可以看到其本质上是 数组+链表+红黑树。

数组的类型是一个Entry类型,其实是一个Node类型,这个Node类型是Entry的实现类。

那么为什么要使用红黑树?为什么要使用链表?

使用红黑树、链表的原因:

在底层我们可以看到其有一个函数hash() 被我们称之为扰动函数,如下:

static final int hash(Object key) {
    int h;
    return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}

这个函数使其高位与地位进行一个运算,使其进行一个扰动,能够散列开来,尽可能地避免这个哈希冲突,也就是说其排列方式是一个哈希排列的方法,这也是其无序的原因,那么位置是如何进行确定的呢?

这时候我们就需要借助路由函数:

(p = tab[i = (n - 1) & hash])

正式这个路由函数让他确定了自己所在的位置。

hashMap底层采用的是哈希排列的方法进行一个散列、排列,那么哈希排列必然会有冲突,这个冲突的解决方法有很多种例如:

  1. 开放定址法

所谓的开放定址法就是一旦发生了冲突,就去寻找下一个空的散列地址,只要散列表足够大,空的散列地址总能找到,并将记录存入

  1. 再哈希法

    再哈希法又叫双哈希法,有多个不同的Hash函数,当发生冲突时,使用第二个,第三个,….,等哈希函数,计算地址,直到无冲突。虽然不易发生聚集,但是增加了计算时间。

  2. 拉链法

    发生冲突就将其放在这个位置的链表后面

  3. 二次侦测法:

再次进行侦测,一次进行1 ,-1 , 4,-4,9,-9。。。。。。这种形式

这里采用的是拉链法,那么为什么采用红黑树呢?

为什么使用红黑树

我们都知道链表查找的时间效率为O(n)而树查找的时间效率为O(log2n)很显然树的查找效率更高,那么为什么非得使用红黑树呢?

树的查找效率一般情况下确实为log2n,但是还有一些极端情况:所有的节点都位于某一侧,最后形成的结果和链表一样,这时候我们就需要使用到平衡二叉树了,平衡二叉树的左右两侧高度差的绝对值小于等于1,这就解决了二叉树的极端情况,那么为什么使用红黑树?因为红黑树的效率更高。

HashMap注意点:
  1. HashMap的结构是一个数组+链表+红黑树

  2. HashMap是延迟声明空间。

    什么意思,也就是说,map集合在世new完之后数组还没有被创建,只有你添加第一个元素的时候才会进行空间的声明。

  3. HashMap存在这阈值、负载因子,这两个东西是HashMap的灵魂之一,

    为什么说是灵魂呢?因为这两个东西对Map十分重要这里进行一个细说:

    阈值大家都理解是一个临界值,如果某个操作使其超过了阈值,那么就会引发一系列的反应,这里的阈值是如何计算的呢?

    阈值的计算公式为: 当前的容量*负载因子,

    那么负载因子怎么来:默认情况下是0.75f,这是人家进行过计算的,咱也不知道为啥是默认0.75f,一般不要进行更改。

    如果达到阈值会引发扩容操作,扩容操作的代码:

    final Node<K,V>[] resize() {
        Node<K,V>[] oldTab = table;
        int oldCap = (oldTab == null) ? 0 : oldTab.length;
        int oldThr = threshold;
        int newCap, newThr = 0;
        if (oldCap > 0) {
            if (oldCap >= MAXIMUM_CAPACITY) {
                threshold = Integer.MAX_VALUE;
                return oldTab;
            }
            else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
                     oldCap >= DEFAULT_INITIAL_CAPACITY)
                newThr = oldThr << 1; // double threshold
        }
        else if (oldThr > 0) // initial capacity was placed in threshold
            newCap = oldThr;
        else {               // zero initial threshold signifies using defaults
            newCap = DEFAULT_INITIAL_CAPACITY;
            newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
        }
        if (newThr == 0) {
            float ft = (float)newCap * loadFactor;
            newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
                      (int)ft : Integer.MAX_VALUE);
        }
        threshold = newThr;
        @SuppressWarnings({"rawtypes","unchecked"})
        Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
        table = newTab;
        if (oldTab != null) {
            for (int j = 0; j < oldCap; ++j) {
                Node<K,V> e;
                if ((e = oldTab[j]) != null) {
                    oldTab[j] = null;
                    if (e.next == null)
                        newTab[e.hash & (newCap - 1)] = e;
                    else if (e instanceof TreeNode)
                        ((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
                    else { // preserve order
                        Node<K,V> loHead = null, loTail = null;
                        Node<K,V> hiHead = null, hiTail = null;
                        Node<K,V> next;
                        do {
                            next = e.next;
                            if ((e.hash & oldCap) == 0) {
                                if (loTail == null)
                                    loHead = e;
                                else
                                    loTail.next = e;
                                loTail = e;
                            }
                            else {
                                if (hiTail == null)
                                    hiHead = e;
                                else
                                    hiTail.next = e;
                                hiTail = e;
                            }
                        } while ((e = next) != null);
                        if (loTail != null) {
                            loTail.next = null;
                            newTab[j] = loHead;
                        }
                        if (hiTail != null) {
                            hiTail.next = null;
                            newTab[j + oldCap] = hiHead;
                        }
                    }
                }
            }
        }
        return newTab;
    }
    
  4. HashMap的key不能重复的,如果重复就会进行一个修改操作:

    我们查看HashMap的底层源码:

    public V put(K key, V value) {
        return putVal(hash(key), key, value, false, true);
    }
    

    在这里调用了putVal方法:

    final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
                   boolean evict) {
        Node<K,V>[] tab; Node<K,V> p; int n, i;
        if ((tab = table) == null || (n = tab.length) == 0)
            n = (tab = resize()).length;
        if ((p = tab[i = (n - 1) & hash]) == null)
            tab[i] = newNode(hash, key, value, null);
        else {
            Node<K,V> e; K k;
            if (p.hash == hash &&
                ((k = p.key) == key || (key != null && key.equals(k))))
                e = p;
            else if (p instanceof TreeNode)
                e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
            else {
                for (int binCount = 0; ; ++binCount) {
                    if ((e = p.next) == null) {
                        p.next = newNode(hash, key, value, null);
                        if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
                            treeifyBin(tab, hash);
                        break;
                    }
                    if (e.hash == hash &&
                        ((k = e.key) == key || (key != null && key.equals(k))))
                        break;
                    p = e;
                }
            }
            if (e != null) { // existing mapping for key
                V oldValue = e.value;
                if (!onlyIfAbsent || oldValue == null)
                    e.value = value;
                afterNodeAccess(e);
                return oldValue;
            }
        }
        ++modCount;
        if (++size > threshold)
            resize();
        afterNodeInsertion(evict);
        return null;
    }
    

    在这里我们可以看到其分为三个类型:

    1. 如果没有发生冲突,确定的位置没有元素,那么直接放入
    2. 如果当前位置的元素于自己的元素的key相同或者key的内容相同,那么将p付给e,也就是说将咱们这个元素的值赋给e,
    3. 如果当前位置的值存在元素,且不符合条件2,那么判断是否为一个TreeNode节点,如果是,那么说名此处是一个红黑树的结构,需要在红黑树中进行下一步操作
    4. 如果2、3条件都不满足,那么只能对链表进行一个遍历,如果找到相同的,那么就会p付给e,也就是说将咱们这个元素的值赋给e,如果没有找到,就直接放在链表尾部。

    那么如果找到相同的话会怎么办?:

    我们查看源码,:

    if (e != null) { // existing mapping for key
        V oldValue = e.value;
        if (!onlyIfAbsent || oldValue == null)
            e.value = value;
        afterNodeAccess(e);
        return oldValue;
    }
    

    也就是说如果找到相同的,那么e就不为null,那么就会进行一个赋值操作,将传进来的元素的值赋给e,使得value被修改,并且返回原来的值。

    如果不相同的话,那么返回一个null。

    这个返回值十分的重要,为什么呢?因为这个返回值是HashSet集合添加成功的判断标志:

    如果添加成功,那么HashMap的put返回值是一个null,而HashSet就对其进行判断如果是Null,那么就返回true,表示添加成功,否则添加失败。

    代码如下:

    public boolean add(E e) {
        return map.put(e, PRESENT)==null;
    }
    
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值