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 ) ;
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 ) ;
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 ;
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) {
int oldCapacity = elementData. length;
int newCapacity = oldCapacity + ( oldCapacity >> 1 ) ;
if ( newCapacity - minCapacity < 0 )
newCapacity = minCapacity;
if ( newCapacity - MAX_ARRAY_SIZE > 0 )
newCapacity = hugeCapacity ( minCapacity) ;
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节点是指向null 的
Offer ( ) == 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) {
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) {
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) {
final E element = f. item;
final Node < E > next = f. next;
f. item = null ;
f. next = null ;
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) {
final E element = l. item;
final Node < E > prev = l. prev;
l. item = null ;
l. prev = null ;
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) {
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 ;
这个是默认的数组的初始大小,应该是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往右移动了16 位
0000 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 )
treeifyBin ( tab, hash) ;
break ;
}
if ( e. hash == hash &&
( ( k = e. key) == key || ( key != null && key. equals ( k) ) ) )
break ;
p = e;
}
}
if ( e != null ) {
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 )
treeifyBin ( tab, hash) ;
break ;
}
if ( e. hash == hash &&
( ( k = e. key) == key || ( key != null && key. equals ( k) ) ) )
break ;
p = e;
}
}
if ( e != null ) {
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 ) {
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 )
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 )
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 的倍数,从16 到32 到64 到128 ,这样的话,就可以保证说,每次扩容后,你的每个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 ;
newThr = oldThr << 1 ; 就是乘以2 ,新数组的大小是老数组的2 倍
Node < 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 &&
( ( 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 &&
( ( 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 ) ;
}
在调用LinkedHashMap 的put ( ) 方法的时候,一定会调用到HahsMap 的put()方法里面去,插入一个key- value对后,其实会调用afterNodeInsertion ( ) ;这个方法来回调LinkedHashMap 里面的子类的实现
void afterNodeInsertion ( boolean evict) {
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