JDK集合源码剖析

集合类源码剖析

1. ArrayList

1.0 ArrayList的优缺点场景

ArrayList底层是由数组实现的,数组的长度是固定的,java里面的数组都是定长数组,

Transient 关键字,被这个关键字修饰的成员变量是不可以被序列化的
1.0.1 缺点
缺点1:
比如说数组的大小设置为100,此时你不停的往ArrayList里面塞入这个数据,此时元素数量超过了100以后,此时就会发生一个数组的扩容,就会搞一个更大的数组,把以前的数组拷贝到新的数组中去,这个数组扩容和元素拷贝的过程,相对来说会慢一些,所以说,我们使用ArrayList时候,不要频繁的往arrayList里面去塞数组,导致它频繁的数组扩容,避免扩容的时候较差的性能影响了系统的运行
缺点2:
数组来实现,要往里面去塞一个数据,要把数组中的那些新增的元素后面的元素全部往后面挪动一位,所以说,如果向arrayList中间插入一个元素也是非常耗费性能的。
1.0.2 优点
基于数组来实现,非常适合随机读,你可以随机的去读数组中的某个元素,
1.0.3 使用场景
1.不会频繁的插入数据,不会导致数组扩容,元素移动,就是有一些数据,查询出来,写入到arrayList中去,后面就不会频繁的写入数据了,主要就是遍历集合,或者是随机读取某个元素,那么使用arrayList是比较合适的,如果你要频繁插入元素就不合适了。
2.开发系统的时候,大量的场景,需要一个集合,里面可以按照顺序写入一些数据,ArrayList的话,他的最最主要的功能作用,就是说他里面的元素是有顺序的,我们在系统里的一些数据,都是需要按照我插入的顺序来排序的

1.1 ArrayList核心方法原理

1.1.1 默认构造函数
private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};

public ArrayList() {
    this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
}

ArrayList的构造函数,首先实例化了一个数组,有一个默认的初始化的数值的大小,是10,也就是我们初始化了一个数组长度为10的一个数组
1.1.2 带参构造函数
public ArrayList(int initialCapacity) {
    if (initialCapacity > 0) {
        this.elementData = new Object[initialCapacity];
    } else if (initialCapacity == 0) {
        this.elementData = EMPTY_ELEMENTDATA;
    } else {
        throw new IllegalArgumentException("Illegal Capacity: "+
                                           initialCapacity);
    }
}

所以说我们在使用ArrayList的时候,不应该用我们这个默认的构造函数,我们应该是给定一个默认的数组长度的初始值,避免数组的长度太小,我们频繁的向数组中插入数据的时候,导致数组频繁的扩容,数组的拷贝
ArrayList list = new ArrayList(100);
1.1.3 Add()方法
public boolean add(E e) {
    ensureCapacityInternal(size + 1);  // Increments modCount!!
    elementData[size++] = e;
    return true;
}

ensureCapacityInternal(size + 1);
这个方法,你每次往ArrayList中插入数据的时候,人家都会判断一下,当前数组的元素是否满了,如果满了,那么就会将数组扩容,然后将老数组中的元素拷贝的新数组中去,确保是可以放下所有的数据的

ElementData开始是一个空数组,然后是将elementData[0] = e; 然后就将size++,进行完这个操作以后,那么此时数组就变成了
elementData[e], size = 1;
1.1.4 Set()方法
public E set(int index, E element) {
    rangeCheck(index);

    E oldValue = elementData(index);
    elementData[index] = element;
    return oldValue;
}
首先先检查一下数组是否越界,然后将我们的新的值替换到index下的老的数值,然后将老的数值返回。
1.1.5 Add(index,element)方法
public void add(int index, E element) {
    rangeCheckForAdd(index);

    ensureCapacityInternal(size + 1);  // Increments modCount!!
    System.arraycopy(elementData, index, elementData, index + 1,
                     size - index);
    elementData[index] = element;
    size++;
}
首先判断数组是否越界,然后人家都会判断一下,当前数组的元素是否满了,如果满了,那么就会将数组扩容,然后将老数组中的元素拷贝的新数组中去,确保是可以放下所有的数据的,然后进行数组拷贝,然后将index下的数值变成我们的新的数值,给size++
如果开始element[1,2,3,4,5] ,我们的方法是add(1,1),此时我们首先进行的数组拷贝是
Element[1,2,2,3,4,5],然后再对index = 1进行数值1的替换替换完毕是
Element[1,1,2,3,4,5]
1.1.6 Get()方法
public E get(int index) {
    rangeCheck(index);

    return elementData(index);
}
这个方法最简单了,直接elementData[index],基于数组直接定位到这个元素,获取到这个元素,这个ArrayList性能最好的一个
1.1.7 Remove()方法
public E remove(int index) {
    rangeCheck(index);

    modCount++;
    E oldValue = elementData(index);

    int numMoved = size - index - 1;
    if (numMoved > 0)
        System.arraycopy(elementData, index+1, elementData, index,
                         numMoved);
    elementData[--size] = null; // clear to let GC do its work

    return oldValue;
}
首先检查数组是否越界,首先先拿到index下的一个旧的值,然后定位到index+1的一个位置上面去,就是王五的这个位置,然后进行数组拷贝,那么就是elementData[“张三”,”王五”,”麻子”],最后将elementData最后一个元素设置为null
比如elementData[“张三”,”李四”,”王五”,”麻子”] size = 4; index = 1;
进行完数组拷贝后
elementData[“张三”,”王五”,”麻子”,”麻子”]
然后将最后一个元素设置为 null
elementData[“张三”,”王五”,”麻子”,null]
那么就是
elementData[“张三”,”王五”,”麻子”] size = 3
这俩个方法,都会导致数组的拷贝,大量数据的拷贝,其实性能都不是很高
1.1.8 扩容和数据拷贝
ensureCapacityInternal(size + 1);
假设我们现在用的是一个默认的数组大小,也就是10,现在已经往这个数组中添加了10个元素了,此时的数组的size = 10; capacity = 10;
此时,调用add方法,那么就是调用第11个元素了,这时肯定是放不进去的,
ensureCapacityInternal(11);
calculateCapacity(elementData, minCapacity);
此时elementData已经填充了10个元素了,此时minCapacity就是11了
最底层的方法是
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);
}
int newCapacity = oldCapacity + (oldCapacity >> 1);
这里相当于是 oldCapacity + (oldCapacity/2);
此时新数组大小是10 + 5 = 15了,然后将elementData中的数据拷贝到新的数组中,size是15
1.1.9 方法总结
Add(),add(index,element),这俩个方法,都会导致数组需要扩容,数组长度是固定的,默认初始大小是10个元素,如果不停的往数组中塞入数组,可能导致数组不停的扩容,数据的拷贝,导致系统性能下降
Set()get(),这俩个方法,都会定位到我们随机的位置,替换那个元素,或者是获取到那个元素,这个其实还是比较靠谱的,基于数组来实现随机位置的定位,这个性能还是很高的。

2.LinkedList

2.0 LinkedList基本原理及优缺点

2.0.1 基本原理
底层是基双向链表实现的,由基本的Node节点组合而成
2.0.2 优点
往中间插入一些元素,或者往中间不停的插入元素,都没关系,因为人家是链表,中间插入元素不需要跟ArrayList数组那样子,挪动大量的元素的,不需要,人家直接在链表里加一个节点就可以了

所以LinkedList非常适合各种元素频繁的插入到链表中去
2.0.3 缺点
不太适合在随机的位置,获取某个随机的元素,比如说LinkedList.get(10),这种操作,性能就非常的低,因为他需要遍历这个链表,从头开始遍历这个链表,直到找到那个值 index = 10的这个元素为止,
2.0.4 适合场景
适合,频繁的在list中插入和删除某个元素,然后尤其是LinkedList,它其实是可以当作队列来用的,这个东西的话,先进先出,在list尾部插入一个元素,然后从头部拿出来一个元素。如果要在内存里实现一个基本的队列的话,可以用LinkedList

2.1 LinkedList双向链表数据结构

2.1.1 双向链表数据Node节点
transient Node<E> first;
transient Node<E> last;
private static class Node<E> {
    E item;
    Node<E> next;
    Node<E> prev;

    Node(Node<E> prev, E element, Node<E> next) {
        this.item = element;
        this.next = next;
        this.prev = prev;
    }
}

2.2 LinkedList插入元素的原理

2.2.1 在尾部插入元素
public boolean add(E e) {
    linkLast(e);
    return true;
}
void linkLast(E e) {
    final Node<E> l = last;
    final Node<E> newNode = new Node<>(l, e, null);
    last = newNode;
    if (l == null)
        first = newNode;
    else
        l.next = newNode;
    size++;
    modCount++;
}
首先,把last节点拿出来,node拿出来,此时再封装一个新的node,新node的prev指针是指向我们的last的节点的,然后element就是我们的node中的e,我们的next节点是指向nullOffer()==add(),就是在队列尾部入队,将一个元素插入队列的尾部,
Poll() 从队列头部出队
Peek() 获取队列头部的元素,但是头部的元素不出队
2.2.2 在头部插入元素
public void addFirst(E e) {
    linkFirst(e);
}
private void linkFirst(E e) {
    final Node<E> f = first;
    final Node<E> newNode = new Node<>(null, e, f);
    first = newNode;
    if (f == null)
        last = newNode;
    else
        f.prev = newNode;
    size++;
    modCount++;
}
2.2.3 在中间插入元素
public void add(int index, E element) {
    checkPositionIndex(index);

    if (index == size)
        linkLast(element);
    else
        linkBefore(element, node(index));
}
void linkBefore(E e, Node<E> succ) {
    // assert succ != null;
    final Node<E> pred = succ.prev;
    final Node<E> newNode = new Node<>(pred, e, succ);
    succ.prev = newNode;
    if (pred == null)
        first = newNode;
    else
        pred.next = newNode;
    size++;
    modCount++;
}
在拿出来的index的那个node前边插入一个node,首先是拿到index前面的那个node,然后又封装了一个新的Node,这个newNode其实就是prev指针指向了index前面的那个node,next指针指向的是我们index的,这个时候相当于是将这个元素插入到里面去了
Node<E> node(int index) {
    // assert isElementIndex(index);
// index < size / 2 
    if (index < (size >> 1)) {
        Node<E> x = first;
        for (int i = 0; i < index; i++)
            x = x.next;
        return x;
    } else {
        Node<E> x = last;
        for (int i = size - 1; i > index; i--)
            x = x.prev;
        return x;
    }
}
获取index那个位置的node,如果说index是在队列的前半部分,那么就从头部开始遍历整个链表,找到index那个位置的node
如果说index是在队列的后半部分,那么就从尾部开始遍历整个链表,找到index那个位置的node

2.3 LinkedList获取元素的原理

2.3.1 获取头部的元素
public E getFirst() {
    final Node<E> f = first;
    if (f == null)
        throw new NoSuchElementException();
    return f.item;
}
直接返回first指针的item元素
2.3.2 获取尾部的元素
public E getLast() {
    final Node<E> l = last;
    if (l == null)
        throw new NoSuchElementException();
    return l.item;
}
返回last指针的item元素
2.3.3 获取中间的元素
public E get(int index) {
    checkElementIndex(index);
    return node(index).item;
}
Node<E> node(int index) {
    if (index < (size >> 1)) {
        Node<E> x = first;
        for (int i = 0; i < index; i++)
            x = x.next;
        return x;
    } else {
        Node<E> x = last;
        for (int i = size - 1; i > index; i--)
            x = x.prev;
        return x;
    }
}
在方法add的时候,插入一个元素其实就用到了这个node的方法,获取到某个随机位的元素,需要进行链表的一个遍历

2.4 LinkedList删除元素的原理

2.4.1 删除头部元素
public E removeFirst() {
    final Node<E> f = first;
    if (f == null)
        throw new NoSuchElementException();
    return unlinkFirst(f);
}
private E unlinkFirst(Node<E> f) {
    // assert f == first && f != null;
    final E element = f.item;
    final Node<E> next = f.next;
    f.item = null;
    f.next = null; // help GC
    first = next;
    if (next == null)
        last = null;
    else
        next.prev = null;
    size--;
    modCount++;
    return element;
}
2.4.2 删除尾部元素
public E removeLast() {
    final Node<E> l = last;
    if (l == null)
        throw new NoSuchElementException();
    return unlinkLast(l);
}
private E unlinkLast(Node<E> l) {
    // assert l == last && l != null;
    final E element = l.item;
    final Node<E> prev = l.prev;
    l.item = null;
    l.prev = null; // help GC
    last = prev;
    if (prev == null)
        first = null;
    else
        prev.next = null;
    size--;
    modCount++;
    return element;
}
这里的以前的last指针指向的节点都为null,那么下次垃圾回收的时候,是会将这个节点给回收掉的
2.4.3 删除中间元素
public E remove(int index) {
    checkElementIndex(index);
    return unlink(node(index));
}
E unlink(Node<E> x) {
    // assert x != null;
    final E element = x.item;
    final Node<E> next = x.next;
    final Node<E> prev = x.prev;

    if (prev == null) {
        first = next;
    } else {
        prev.next = next;
        x.prev = null;
    }

    if (next == null) {
        last = prev;
    } else {
        next.prev = prev;
        x.next = null;
    }

    x.item = null;
    size--;
    modCount++;
    return element;
}
首先先通过node(index)方法来找到那个节点,然后在调用unLink方法,
然后找到index对应节点的前后节点,然后将前后节点的prev和next指针相连,将index节点的prev和next指针还有item元素设置为null,等待垃圾回收

2.5 总结

双向链表来实现linkedList数据结构,应该看到他底层的一个双向队列的数据结构,插入,获取,删除,都可以从对头,队尾来实现,完全可以当做一个队列来用,offer()往队尾插入元素,poll()从对头删除一个元素

如果向链表中不断的疯狂的插入数据,哪怕是大量的数据,优点就是它是基于链表来实现的,不会出现数组扩容和大量数据的拷贝
在中间插入元素性能没有队头和队尾那么好,他要走一个遍历,遍历找到我们index的那个node,用node()方法

3 HashMap

3.0 HashMap数据结构

3.0.1 HashMap的数据结构是什么?
数组+链表+红黑树
初步的介绍一下JDK1.8开始的hashmap的基本的数据结构和原理
Map.put(1,“张三”)
Map.put(2,”李四”)
这里要对你的key进行一个hashCode()的一个运算,获取你的key的hash值,常规的一个做法就是用这个hash值对数组的长度进行取模(hash模算法,这个算法的意思是hash值对数组长度取模以后,就会保证每一个key,都可以分配到数组里面的一个元素中去),根据取模的结果,将key-value对放在数组的某一个元素上去
Map.get(1),这个东西和插入的时候是同理的,首先对key进行一个hash,然后hash值对数组的长度进行一个取模,找到index,然后定位到我们的key-value对的位置上
如果说,某俩个key的hash值是一样的怎么样呢(hash冲突,hash碰撞)?
Hash值一样会导致他们放到同一个数组的索引的位置上去,此时该如何处理
会在数组的同一个位置挂一个链表,放到这个链表上边去,如果同一个index相同的hash值超过8个,那么会自动将链表转换成红黑树
3.0.2 HashMap中hash冲突的时候怎么解决?
链表,用链表来处理
JDK1.8 开始优化了hashmap的数据结构,链表 --> 红黑树,来解决hash冲突的问题
3.0.3 说一说hashmap的原理?
对key进行hash,找到对应的位置,放在里面,查询的时候,也是对key进行hash,去找到对应的key-value对

3.1核心成员变量的作用分析

3.1.1 默认数组大小
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16
这个是默认的数组的初始大小,应该是16,这个跟ArrayList是不一样的,ArrayList的默认的初始大小是10
数组的大小一般要自己指定一下,就跟你用ArrayList一样,初始的默认大小是10,你预估一下你要用到多大的数组长度,避免频繁的数组扩容
3.1.2 默认的负载因子
static final float DEFAULT_LOAD_FACTOR = 0.75f;
这个时候默认的负载因子,0.75,如果你在数组中的元素的个数,达到了数组大小(16* 负载因子(0.75),默认是达到12个元素就会进行数组的扩容
3.1.3 重要的节点Node
static class Node<K,V> implements Map.Entry<K,V> {
    final int hash;
    final K key;
    V value;
    Node<K,V> next;
}
这是一个很关键的内部类,他其实是代表了一个key-value对,里面包含了key的hash值,key,value,还有就是可以有一个next的指针,指向下一个Node,也就是指向单向链表中的下一个节点,通过next的指针就可以形成一个链表
3.1.4 map的核心数组
transient Node<K,V>[] table;
这个就是所谓的map里的核心的数据结构的数组,数组的元素就是Node类型,天然就可以挂成一个链表
3.1.5 threshold
这个值,其实就是说capacity(就是默认的数组的大小),就是说capacity * loadFactory,就是threshold,如果size达到了threshold那么就会进行数组的扩容
3.1.6 loadFactor
默认就是负载因子,默认的值是0.75f,你也可以自己制定,入彀你指定的越大,一般就越是拖慢扩容的速度,一般不要修改

3.3 优化降低冲突概率的hash算法

3.3.1 hash(key)
public V put(K key, V value) {
    return putVal(hash(key), key, value, false, true);
}
首先是hash(key),对key进行一个hash,对key进行hash获取一个对应的hash值,然后将key-value传入到putVal()方法里面去,将key-value对根据其hash值找到对应的数组的位置

static final int hash(Object key) {
    int h;
    return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
这里首先是对key进行hashCode 然后 将其hash值右位移16,然后做一个异或运算
假设hash值是以下的一串东西
1111 1111 1111 1111 1111 1010 0111 1100
h >>> 16,这个是一个位运算的东西,将32位二进制的数字,所有的bit往右移动了160000 0000 0000 0000 1111 1111 1111 1111
h = key.hashCode()) ^ (h >>> 16)
计算出来以后就是
1111 1111 1111 1111 0000 0101 1000 0011
为什么要做这样的一个操作呢,为什么要右移然后再异或?
他这么做,其实是考虑到,将它的高16位和低16位进行了一个异或运算,这里是因为后面在用这个hash值定位到数组的index的时候,也有一个位运算,但是,后面的那个位运算是用低16位进行运算,提前将hash()函数中,就会将高16位和低16位进行一下异或运算,就可以保证,在hash值的低16位里面,同时可以保留他的高16位和低16位的特征,这个目的是通过这样的方式计算出来的hash值,可以降低hash冲突的概率

3.4 put操作原理以及hash寻址算法

3.4.1 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;
}
if ((tab = table) == null || (n = tab.length) == 0)
        n = (tab = resize()).length;
刚开始数组是空的,那么这里将会给它分配一个默认大小的一个数组,数组的大小是16,负载因子是0.75f,threshold是12
3.4.2 hash寻址算法
if ((p = tab[i = (n - 1) & hash]) == null)
    tab[i] = newNode(hash, key, value, null);
(n-1) & hash操作,定位到数组的位置上,如果这个位置是null,那么就创建一个newNode,放到数组的那个位置上边去
(16-1)&hash
就是以下的表示
0000 0000 0000 0000 0000 0000 0000 1111
&(俩个都是1才是1,要不就是0)
1111 1111 1111 1111 0000 0101 1000 0011
=
0000 0000 0000 0000 0000 0000 0000 0011 就是3,所以它的index = 3
他的hash寻址的算法,并不是说用hash值对数组大小取模,取模就可以将任意一个hash值定位到数组的一个index那去,取模的操作性能不是很高,&操作取模的效果,它优化以后的一个小姑,就是说他的数组刚开始的初始值,以及未来的扩容的值,都是2的n次方,
也就是说他后面的每次扩容,数组的大小就是2的n次方,只要保证数组的大小是2的n次方,就可以保证说,(n-1)&hash,可以保证就是hash % 数组.length取模的一样的效果,也就是说通过(n-1)&hash,就可以将任意的一个hash值定位到数组的某个index里去

3.5 hash冲突时的链表处理

假设某俩个key的hash值是一样的,俩个key不同,hash值是一样的,这个概率很低,如果你重写了hashCode方法,有可能造成hash值一样,也有可能俩个key的hash值不一样,但是通过寻址算法,定位到了数组的同一个key上去,此时就造成了hash冲突,或者是hash碰撞,默认情况下会用单向链表来处理
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;
    }
}
3.5.1 相同keyhash定位冲突
if (p.hash == hash &&
    ((k = p.key) == key || (key != null && key.equals(k))))
    e = p;
if (e != null) { // existing mapping for key
    V oldValue = e.value;
    if (!onlyIfAbsent || oldValue == null)
        e.value = value;
    afterNodeAccess(e);
    return oldValue;
}
如果满足上述条件,说面是相同的key,覆盖旧的value
e.value = value;
将数组那个位置的Node的value设置为了新的值
上边那些代码其实就是相同的key然后进行一个value的一个覆盖
3.5.2 不同keyhash寻址冲突
上边的if不成立,那么说明key不一样,但是定位到了同一个index上边去,进入以下的else
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 (binCount >= TREEIFY_THRESHOLD - 1)
如果当前的链表的长度(binCount),大于等于TREEIFY_THRESHOLD-1的话,如果说链表的长度大于等于8,那么此时就需要将这个链表转换为一个红黑树的数据结构
假设再有一个hash冲突的时候才会走到
if (e.hash == hash &&
                ((k = e.key) == key || (key != null && key.equals(k))))
                break;
            p = e;
首先判断是不是同一个key,如果不是的话,那么将p=e,指针转换,下次再次进入
if ((e = p.next) == null) 
创建新的节点

3.6 JDK1.8引入红黑树解决hash冲突

3.6.1 JDK1.8前直接挂链表的问题
如果说出现大量的hash冲突以后,假设给某个位置挂的链表特别长,那就很恶心了,如果链表长度太长的话,会导致有一些get()操作的时间复杂度就是O(n)了,正常来说,你基于table[i]数组索引定位的方式,其实是O(1)
所以说JDK1.8以后优化了这块东西,会判断,如果链表的长度达到8的时候,那么就会将链表转换为红黑树,如果用红黑树的话,get()操作,即使对一个很大的红黑树进行二叉查找,那么时间的复杂度会变成O(lgn),性能会有大幅度的提升
3.6.2 JDK1.8引进链表转红黑树
链表转红黑树
if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
                    treeifyBin(tab, hash);
                break;
当你遍历到第8个节点,此时binCount是7,同时你挂上了第9个节点,然后就会发现binCount>=7,达到了临界值,也就是说,当你的链表节点的数量超过8的时候,此时就会将链表转换成红黑树
final void treeifyBin(Node<K,V>[] tab, int hash) {
    int n, index; Node<K,V> e;
    if (tab == null || (n = tab.length) < MIN_TREEIFY_CAPACITY)
        resize();
    else if ((e = tab[index = (n - 1) & hash]) != null) {
        TreeNode<K,V> hd = null, tl = null;
        do {
            TreeNode<K,V> p = replacementTreeNode(e, null);
            if (tl == null)
                hd = p;
            else {
                p.prev = tl;
                tl.next = p;
            }
            tl = p;
        } while ((e = e.next) != null);
        if ((tab[index] = hd) != null)
            hd.treeify(tab);
    }
}
这个do while方法主要是将先前的一个单向链表转换成了一个双向链表,都转换完成后跳出do while循环
hd.treeify(tab);
这个方法就是将链表转换成了红黑树
3.6.3总结
当链表的长度超过8的时候,链表就先是变成双向链表,然后是变成红黑树,

3.7 基于数组的扩容原理

hashMap底层是基于数组来实现的核心的数据结构,如果是用数组的话,就天然会有一个问题,和ArrayList一样,数组满了后,就会有扩容的问题,其实非常简单,首先是俩倍扩容,其次是rehash,扩容后,每个key-value对,都会基于key的hash值重新寻址到新的数组的新的位置
本来那个数组的长度是16,扩容后那个新的数组的长度变成了32,
本来所有的key的hash,16取模的话是一个位置,比如说index = 5,但是如果对32取模的话,可能就是index=11,位置可能会变化
基于key的hash值重新在新的数组里找到新的位置,很多key在新数组的位置都不一样了,如果说是之前冲突的key,可能在新的数组中分配到不同的位置
这个原理是JDK1.7的原理

JDK1.8以后,都是数组的大小是2的n次方扩容,用的是&操作符来实现hash寻址的算法,来进行扩容以后,进行rehash的操作

3.8 JDK1.8高性能rehash算法

3.8.1 rehash算法的原理
JDK1.8以后,为了提升rehash的这个过程的性能,不是说简单的用key的hash值对新数组.length取模,取模性能较低,所以说以后对于hash寻址是用的& * length-1这种方式
假设数组长度开始默认是16那么就有以下的运算
n -1    0000 0000 0000 0000 0000 0000 0000 1111 = 15
hash1  1111 1111 1111 1111 0000 1111 0000 0101
&结果  0000 0000 0000 0000 0000 0000 0000 0101  = 5(index = 5)

n -1    0000 0000 0000 0000 0000 0000 0000 1111 = 15
hash2  1111 1111 1111 1111 0000 1111 0001 0101
&结果  0000 0000 0000 0000 0000 0000 0000 0101  = 5(index = 5)

此时上边的俩个hash值会出现hash碰撞的问题,此时就会使用链表或者红黑树来解决,如果扩容的话,那么会出现什么情况

n -1    0000 0000 0000 0000 0000 0000 0001 1111 = 31
hash1  1111 1111 1111 1111 0000 1111 0000 0101
&结果  0000 0000 0000 0000 0000 0000 0000 0101  = 5(index = 5)

n -1    0000 0000 0000 0000 0000 0000 0001 1111 = 31
hash2  1111 1111 1111 1111 0000 1111 0001 0101
&结果  0000 0000 0000 0000 0000 0000 0001 0101  = 5(index = 5 + 16)

也就是说,JDK1.8,扩容一定是2的倍数,从163264128,这样的话,就可以保证说,每次扩容后,你的每个hash值要么是停留在原来的那个index的地方,要么是变成了原来index(5)+ oldCap(16) = 21;
hashMap的底层原理
1.hash算法:为什么要高位和地位做^运算
2.hash寻址:为什么hash值和数组.length-1来运算
3.hash冲突的机制:链表 + 红黑树
4.扩容机制:数组俩倍扩容,重新寻址(rehash), hash & (n-1),判断二进制结果中是否多出来一个bit的1,如果没多,那么就是原来的index,如果多出来了,那么就是index+oldCap,通过这个方式就避免了rehash的时候,用每个hash对新数组的.length取模,取模性能不搞,位运算性能比较高
3.8.2 rehash代码实现
if (++size > threshold)
    resize();
每次put新的key-value对后,都会++size,每次都会比较一下threshold(数组长度*负载因子)resize()方法就是在扩容

else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
         oldCap >= DEFAULT_INITIAL_CAPACITY)
    newThr = oldThr << 1; // double threshold
newThr = oldThr << 1; 就是乘以2,新数组的大小是老数组的2Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
if (e.next == null)
    newTab[e.hash & (newCap - 1)] = e;

如果e.next是null的话,这个位置的元素就不是链表,也不是红黑树,那么此时就是用e.hash&newCap(新数组的大小) - 1,进行&运算,直接定位大新数组的某个位置,放到了新的数组中

else if (e instanceof TreeNode)
    ((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
如果这个位置是一个红黑树的话,此时会调用split方法,肯定会去里面遍历这颗红黑树,然后将里面每个节点都进行重新hash寻址,找到新数组的某个位置

进入下一个分支,那么就是链表了
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;
}
大概就是说,判断一下,
(e.hash & oldCap) == 0)主要是这里的判断,不是和oldCap-1进行&运算,而是和 oldCap进行&运算,来判断是否&后最高位还有1,如果没有,那么就是放到数组原来的位置上, 如果有的话,那么就是放在index+ cap上
如果是链表里的元素的话,要不是放在新数组的原来的index,要不就是原来的index+oldCap

3.9 get与remove操作的原理分析

3.9.1 get()
public V get(Object key) {
    Node<K,V> e;
    return (e = getNode(hash(key), key)) == null ? null : e.value;
}
final Node<K,V> getNode(int hash, Object key) {
    Node<K,V>[] tab; Node<K,V> first, e; int n; K k;
    if ((tab = table) != null && (n = tab.length) > 0 &&
        (first = tab[(n - 1) & hash]) != null) {
        if (first.hash == hash && // always check first node
            ((k = first.key) == key || (key != null && key.equals(k))))
            return first;
        if ((e = first.next) != null) {
            if (first instanceof TreeNode)
                return ((TreeNode<K,V>)first).getTreeNode(hash, key);
            do {
                if (e.hash == hash &&
                    ((k = e.key) == key || (key != null && key.equals(k))))
                    return e;
            } while ((e = e.next) != null);
        }
    }
    return null;
}
if (first.hash == hash && // always check first node
            ((k = first.key) == key || (key != null && key.equals(k))))
首先判断,是否hash值一样,然后key是否一样,如果都一样的话,那么就直接将node返回了,
if ((e = first.next) != null) {
            if (first instanceof TreeNode)
                return ((TreeNode<K,V>)first).getTreeNode(hash, key);
            do {
如果是一棵树,那么就从树上找
do {
                if (e.hash == hash &&
                    ((k = e.key) == key || (key != null && key.equals(k))))
                    return e;
            } while ((e = e.next) != null);
如果是链表的话,那么就循环遍历这个链表来找
3.9.2 remove()
public V remove(Object key) {
    Node<K,V> e;
    return (e = removeNode(hash(key), key, null, false, true)) == null ?
        null : e.value;
}

4 LinkedHashMap

4.1LinkedHashMap和HashMap的区别

HashMap
比如你放了一堆key-value对进去后,后面的话,如果你要遍历这个HashMap的话,遍历的顺序并不是按照你插入的key-value的顺序来的
LinkedHashMap
你放入的是什么顺序,然后你遍历的顺序是一样的

4.2LinkedHashMap和TreeMap的区别

他们都可以维持key的顺序,知识LinkedHashMap底层是基于链表来实现的,TreeMap是基于红黑树来实现的

4.3LinkedHashMap原理

public V put(K key, V value) {
    return putVal(hash(key), key, value, false, true);
}
在调用LinkedHashMapput()方法的时候,一定会调用到HahsMap的put()方法里面去,插入一个key-value对后,其实会调用afterNodeInsertion();这个方法来回调LinkedHashMap里面的子类的实现
void afterNodeInsertion(boolean evict) { // possibly remove eldest
    LinkedHashMap.Entry<K,V> first;
    if (evict && (first = head) != null && removeEldestEntry(first)) {
        K key = first.key;
        removeNode(hash(key), key, null, false, true);
    }
}
所以说这里就是,回调了这个方法,这个方法里面,它就是实现了LinkedHashMap的逻辑,来记录插入key-value对的顺序,用一个链表来记录

覆盖,如果我们是做key值的覆盖,可以看到,你多次覆盖一个值,不会改变他的顺序,
LinkedHashMap有一个参数的,你可以在构造的时候传进去,accessOrder,默认他是false,如果默认是false的话,那么你比如说get一个key,或者覆盖这个key的值,都不会改变他在链表里的顺序

但是如果accessOrder是true的话,那么如果你get一个key,或者是覆盖这个key的值,就会导致key-value的顺序会在链表里改变,导致挪动到尾部去

5 TreeMap

底层是基于红黑树的数据结构,不是传统意义上的那种HashMap,他天然就可以按照key的自然顺序来排序
public TreeMap(Comparator<? super K> comparator) {
    this.comparator = comparator;
}
可以根据传入的comparator来进行自定义的排序

static final class Entry<K,V> implements Map.Entry<K,V> {
    K key;
    V value;
    Entry<K,V> left;
    Entry<K,V> right;
    Entry<K,V> parent;
    boolean color = BLACK;
}
基本的数据结构,entry

do {
        parent = t;
        cmp = cpr.compare(key, t.key);
        if (cmp < 0)
            t = t.left;
        else if (cmp > 0)
            t = t.right;
        else
            return t.setValue(value);
    } while (t != null);
}
插入前比较,然后看是左叶子节点还是右叶子节点
形成了一棵树
fixAfterInsertion(e);
然后走到这个方法,给它自平衡,变成一个红黑树

6 Set

Set其实底层就是基于Map来实现的

HashSet,他其实就是一个集合,里面的元素是无须的,然后里面的元素是没有重复的,HashMap的key是无顺序的,你插入进去的顺序,跟你迭代遍历的顺序是不一样的,而且HashMap的key是没有重复的,HashSet是直接基于HashMap来实现的

LinkedHashSet,它是有顺序的set,也就是维持了插入set的这个顺序,你迭代LinkedHashSet的顺序跟你插入的顺序是一样的,底层可以直接基于LinkedHashMap来实现

TreeSet,默认是根据你插入进去的元素来排序的,而且可以定制Comparator,自己决定排序的算法和逻辑,他底层就可以基于TreeMap来实现

7 Iterator迭代器应对多线程并发修改的fail_fast机制

Java集合迭代的fail_fast机制
ConcurrentModificationException 并发修改异常,这个机制就叫做fail fast
modCount就是用来实现fail fast机制的,各个集合都有modCount的概念,只要集合进行修改了,那么就对modCount++,这个是什么意思呢?就是modificationCount,修改次数,只要你修改一次,就会更新这个modCount,add(),remove,set...
比如说在迭代一个ArrayList之前,已经插入了4个元素,此时modCount = 4,在你获取和初始化一个迭代器的时候,里面的expectedModCount就会被初始化为modCount,当另外一个线程再修改这个集合,那么modCount++,此时expectedModCount != modCount 那么就会抛出并发修改异常
其实java集合包下的类,都是非线程安全的,所以说里面都设计了针对并发修改集合的问题,有fail-fast机制,modCount
  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值