1. 集合概述
Java集合主要可以分为单列集合Collection和双列集合Map
1.1. Collection
Collection,可以分为List、Set、Queue三类 List,代表有序、可重复的集合 ArrayList,动态可扩容数组 LinkedList,双向链表,可以当做队列或者栈使用 Set,代表不可重复的集合 HashSet,通过哈希表实现的无序不重复集合 LinkedHashSet,通过哈希表实现的先后有序的不重复集合 TreeSet,通过红黑树实现的有序不重复集合 Queue,代表有序、可重复、符合某种特定规则的集合 PriorityQueue,优先级队列-堆,分为大顶堆,小顶堆,默认为小顶堆 ArrayDeque,基于数组的双端队列
1.2. Map
Map代表以键值对形式存储的集合,key不能重复,value可以重复 HashTable,线程安全的哈希表,但官方更建议使用concurrentHashMap TreeMap,红黑树 HashMap,使用红黑树优化后的哈希表 LinkedHahsMap,在HashMap的基础上增加了双向链表来保持键值对的插入顺序
Java集合体系
2. List
2.1. ArrayList和Array的区别
2.1.1. 可变性
ArrayList是动态数组,因此会根据实际存储的元素个数进行动态地扩容,而对于Array类型被创建后就不能改变长度 ArrayList具备集合的各种常见操作,而Array只是一个固定长度的数组,不具备动态修改的能力
2.1.2. 泛型约束
ArrayList允许使用泛型确保类型安全,但也因此只能存储对象,Array可以存储对象也可以存储基本数据类型
2.1.3. 容量
ArrayList创建时不需要声明大小,Array创建时需要
2.2. ArrayList和LinkedList的区别
2.2.1. 随机访问性
ArrayList基于数组实现,具有随机访问性,可以通过get(int index)直接通过数组下标获取,时间复杂度O(1) LinkedList基于链表实现,不具备随机访问性,查找的平均时间复杂度是O(n)
2.2.2. 插入和删除操作
ArrayList更适合于查找,对于删除操作会涉及到元素的移动,因此平均时间复杂度是O(n) LinkedList,对于头尾插入和删除,时间复杂度是O(1),对于链表中间的插入和删除是O(n) 虽然ArrayList和LinkedList的增删的时间复杂度都是O(n),是因为遍历导致的时间复杂度,而增删的效率上,LinkedList只需要改变应用,而ArrayList需要移动元素
2.2.3. 内存占用
ArrayList基于数组实现,占用的是一块连续的内存空间,但是由于扩容机制会存在冗余的空间 LinkedList基于链表实现,存储的内存空间不连续,每个元素需要额外的空间去保存前后元素的引用
2.2.4. 使用场景
ArrayList适用于随机访问频繁、并且查找多于添加的场景 LinkedList适用于不需要随机访问、插入和删除操作更多的场景,实现队列和栈来满足FIFO和LIFO
ArrayList和LinkedList
2.3. ArrayList扩容机制
2.3.1. 数组初始容量
使用无参构造器,初始容量为0,而Vector较为特殊会设置初始容量为10 使用有参构造器,初始容量为n
2.3.2. 添加后数组容量
初次添加时,如果使用无参构造器,会默认取DEFAULT_CAPACITY=10作为添加后数组容量 如果使用有参构造器,则计算数组容量为size+1
2.3.3. 比对容量
数组计算容量大于数组容量,扩容为原先数组容量的1.5倍 数组计算容量小于等于数组容量,不变
2.3.4. 时间复杂度
如果只是末尾添加不扩容,时间复杂度O(1) 如果需要扩容才能添加,则需要数组拷贝,因此时间复杂度O(n)
ArrayList扩容机制
2.4. ArrayList序列化
ArrayList使用transient修饰了elementData数组,意味着该属性无法被序列化 ArrayList内部实现了2个方法ReadObject和writeObject来自定义序列化策略 之所以要自定义序列化策略,是为了提高序列化的效率,因为需要去掉扩容导致的冗余数组长度
transient Object [ ] elementData;
private void writeObject ( java. io. ObjectOutputStream s) throws java. io. IOException{ int expectedModCount = modCount; s. defaultWriteObject ( ) ; s. writeInt ( size) ; for ( int i= 0 ; i< size; i++ ) { s. writeObject ( elementData[ i] ) ; } if ( modCount != expectedModCount) { throw new ConcurrentModificationException ( ) ; }
}
private void readObject ( java. io. ObjectInputStream s) throws java. io. IOException, ClassNotFoundException { elementData = EMPTY_ELEMENTDATA; s. defaultReadObject ( ) ; s. readInt ( ) ; if ( size > 0 ) { int capacity = calculateCapacity ( elementData, size) ; SharedSecrets . getJavaOISAccess ( ) . checkArray ( s, Object [ ] . class , capacity) ; ensureCapacityInternal ( size) ; Object [ ] a = elementData; for ( int i= 0 ; i< size; i++ ) { a[ i] = s. readObject ( ) ; } }
}
2.5. 如何实现ArrayList线程安全
使用Vector,不推荐,所有的方法都使用了synchronized关键字,效率较低 使用Collections.synchronizedList(new ArrayList())包装 使用CopyOnWriteArrayList,线程安全版的ArrayList
2.6. CopyOnWriteArrayList
2.6.1. 设计思想
采用CopyOnWrite,即写时复制,底层采用ReentrantLock上锁保证同步和数组拷贝实现 采用读写分离策略,写写互斥,读写不互斥,读读不互斥 由于采用复制数值的形式修改,因此更适用于读多写少的情景
2.6.2. 具体操作细节
读操作时不加锁,因此其他线程写操作时仍可以执行并发读,时间复杂度O(1) 写操作时上锁,添加元素需要采用数组拷贝的方式,拷贝完使原引用指向新引用,因此时间复杂度是O(n)
2.6.3. 可能出现的问题
数据不一致性,如果线程1通过getArray()方法先获取了array引用A,线程2利用修改方法修改了array引用为B,然后线程1还是会从array引用A中读取数据 写操作开销大,需要数组拷贝,需要时间,也需要额外的内存空间
CopyOnWriteArrayList的读写操作过程
3. Map
3.1. HashMap和HashTable区别
3.1.1. 线程安全性
HashMap是线程不安全的,HashTable是线程安全的 HashTable内部都是通过synchronized修饰的,效率低下,更推荐使用ConcurrentHashMap HashMap多线程环境下问题 死循环问题 JDK1.7之前,HashMap在多线程环境下扩容操作可能存在死循环问题,多个线程同时对链表进行操作,由于头插法可能会导致链表中的节点指向错误位置,从而形成环形链表导致死循环问题 JDK1.8之后,HashMap采用尾插法改进后,保证插入的节点永远是放在链表末尾,从而避免了产生环形结构,但是多线程环境下还是推荐使用ConcurrentHashMap put导致元素丢失 多线程操作时,线程A放入的键值可能会被线程B放入的键值覆盖 put和get并发时,导致get获取为null 多线程操作时,线程A执行put并且还在扩容填充元素,而线程B此时执行get,就会出现此问题
3.1.2. 运行效率
HashMap比HashTable效率更高,因为HashTable方法基于synchronized修饰 如果没有发生哈希冲突,HashMap的查找效率是O(1)
3.1.3. 底层数据结构
HashTable采用数组+链表,不支持NULL键和NULL值 HashMap采用数组+链表+红黑树,红黑树用于解决链表过长问题,支持NULL键和NULL值
3.1.4. 哈希函数实现
HashTable直接使用key的hashCode值 HashMap将key的hashCode值进行了高位和低位的混合处理提高分布均匀性,从而减少碰撞 哈希函数有哪些构造方法 直接定址法,h(key) = key 除留余数法,h(key) = key % n,最常用 数字分析法,h(key) = key中某些位置(如十位和百味) 平方取中法,h(key) = key^2的中间几位 折叠法,h(key) = key_1 + key_2 + … key_i (分割成位数相同的i段,然后求叠加和) 解决哈希冲突的方法 再哈希法,使用多个哈希函数,某个哈希函数冲突,就换用下一个,直至找到空位置 开放地址法 线性探测法,h(key) = p + i 从冲突位置p开始,逐个往后找到空位置, 二次探测,h(key) = p + (-1)^(i+1) * d^2 ,从冲突位置p开始,以右左循环的方式探测,i代表探测次数,d代表距离 拉链法,发生哈希冲突时,使用链表将冲突的元素连接起来
static final int hash ( Object key) { int h; return ( key == null ) ? 0 : ( h = key. hashCode ( ) ) ^ ( h >>> 16 ) ;
}
3.1.5. 扩容机制
未设置初始容量 HashTable默认大小为11,之后每次扩容为原先的2n+1 HashMap默认大小为0,首次扩容长度为16,之后每次扩容为原先的2n 设置了初始容量 HashTable会直接使用给定的大小 HashMap会将其扩容至2的幂次方大小,如设置初始容量17,就会直接初始化为32 原理就是将最高位的1传播,传播范围从1->2->4->8->16->32 扩容机制 扩容长度是2的幂次方 可以更好的保证数组元素分配的更加均匀 更方便使用位运算操作&,来替代取余操作%,运算效率会更高 扩容时,旧的索引位置j会根据newCap重新调整为新的索引位置p,详细可看HashMap简单实现 由于newCap是oldCap的2倍,那么对于newCap-1和oldCap-1,newCap低位会多一位掩码1,设为第k位【比如oldCap-1为0…01111,那么newCap-1就为0…11111】 newCap = oldCap + 2^k oldCap = 2^k 由于只是多了1位掩码,低位的运算都不会变,只需要考虑第k位,那么当hashcode & (newCap-1) 假如hashcode二进制第k位是1,那么链表索引位置就会调整到p = j+2^k = j + oldCap 假如hashcode二进制第k位是0,那么原先链表的索引位置就不变,p = j
static final int tableSizeFor ( int cap) { int n = cap - 1 ; n |= n >>> 1 ; n |= n >>> 2 ; n |= n >>> 4 ; n |= n >>> 8 ; n |= n >>> 16 ; return ( n < 0 ) ? 1 : ( n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1 ;
}
如何扩容至最近的2的幂次方,传播过程示意图
3.2. HashMap的基础参数
loadFactor,负载因子,掌控HashMap数据分布的疏密程度 loadFactor默认因子是0.75,即当总节点个数超过容量的75%,就会扩容 loadFactor越大意味着越稠密,哈希冲突的概率就越高,查找元素的效率会比较低 loadFactor越小意味着越稀疏,数组的利用率就会越低,空间成本大 threshold,数组阈值,是数组是否扩容的标准 threshold=capacity * loadFactor,当所有元素个数size大于threshold,则扩容数组长度和阈值 TREEIFY_THRESHOLD,链表树化阈值,是链表是否转为红黑树的标准 链表树化阈值是8,并且需要满足数组长度大于等于64 UNTREEIFY_THRESHOLD ,红黑树链化阈值,是红黑树转为链表的标准 之所以设置为6而不是8,是因为树化的阈值刚好在8附近,如果节点不断增加,就会导致过度转化
public class HashMap < K , V > extends AbstractMap < K , V > implements Map < K , V > , Cloneable , Serializable { private static final long serialVersionUID = 362498820763181265L ; static final int DEFAULT_INITIAL_CAPACITY = 1 << 4 ; static final int MAXIMUM_CAPACITY = 1 << 30 ; static final float DEFAULT_LOAD_FACTOR = 0.75f ; static final int TREEIFY_THRESHOLD = 8 ; static final int UNTREEIFY_THRESHOLD = 6 ; static final int MIN_TREEIFY_CAPACITY = 64 ; transient Node < k, v> [ ] table; transient Set < map. entry< k, v> > entrySet; transient int size; transient int modCount; int threshold; final float loadFactor;
}
3.3. HashMap的put流程
通过hash方法计算key的哈希值 先获取key的hashcode,然后将hashcode的低16位与高16位做异或操作【扰动】 由于数组长度n是2的幂次,n-1就等于【2^k-1】,可以看成k位全1的地位掩码,那么当【n-1&hash】实际上就只利用了低k位,高32-k位就浪费了,因此为了利用这些高位,可以将hashcode右移16位,这样低16位和高16位就处在了同样的位置,此时再做异或,就成功融合了高位和低位的信息,进而减少了哈希冲突 根据(n-1&hash)计算元素所在数组下标 比较容器元素 如果是链表,遍历容器中的元素,如果某个元素hash值相等并且key值相等,直接覆盖,否则插入尾部 如果插入后,链表节点个数超过8,并且数组长度大于等于64,需要把链表转为红黑树 如果链表节点小于等于6,那么会把红黑树转为链表 如果是红黑树,直接插入 最后插入后,还需要比较哈希表中总节点个数size是否超过阈值threshold,超过则扩容
HashMap的put过程
3.4. HashMap的简单实现
import java. util. Arrays ;
public class SimpleHashMap < K , V > { private int INITIAL_LENGTH = 16 ; private double LOAD_FACTOR = 0.75f ; private int threshold; class Node < K , V > { int hash; K key; V val; Node < K , V > next; Node ( int hash, K key, V val) { this . hash = hash; this . key = key; this . val = val; this . next = null ; } } private Node < K , V > [ ] tables; private int size; public SimpleHashMap ( ) { this . tables = new Node [ INITIAL_LENGTH] ; this . threshold = ( int ) ( tables. length * LOAD_FACTOR) ; } public SimpleHashMap ( int cap) { this . tables = new Node [ getBinaryIndex ( cap) ] ; this . threshold = ( int ) ( tables. length * LOAD_FACTOR) ; } public Node < K , V > [ ] getTables ( ) { return this . tables; } public Integer getTableLength ( ) { return this . tables. length; } private int getSize ( ) { return this . size; } public int getBinaryIndex ( int x) { x -= 1 ; x |= x >>> 1 ; x |= x >>> 2 ; x |= x >>> 4 ; x |= x >>> 8 ; x |= x >>> 16 ; return x + 1 ; } private int hash ( K key) { if ( key == null ) { return 0 ; } int h = key. hashCode ( ) ; return h ^ ( h >>> 16 ) ; } private Node < K , V > newNode ( int hash, K key, V val) { return new Node < > ( hash, key, val) ; } private void resize ( ) { int oldCap = tables. length; int newCap = oldCap << 1 ; Node < K , V > [ ] newTabs = new Node [ newCap] ; for ( int i = 0 ; i < oldCap; i++ ) { Node < K , V > e = tables[ i] ; while ( e != null ) { int j = e. hash & ( newCap - 1 ) ; if ( newTabs[ j] == null ) { newTabs[ j] = newNode ( e. hash, e. key, e. val) ; } else { Node < K , V > p = newTabs[ j] ; while ( p. next != null ) { p = p. next; } p. next = newNode ( e. hash, e. key, e. val) ; } e = e. next; } } tables = newTabs; threshold <<= 1 ; } private void resizePlus ( ) { int oldCap = tables. length; int newCap = oldCap << 1 ; Node < K , V > [ ] newTabs = new Node [ newCap] ; for ( int j = 0 ; j < oldCap; j++ ) { Node < K , V > e = tables[ j] ; if ( e == null ) { continue ; } if ( e. next == null ) { newTabs[ e. hash & ( newCap - 1 ) ] = e; continue ; } Node < K , V > loHead = null , loTail = null ; Node < K , V > hiHead = null , hiTail = null ; do { if ( ( e. hash & oldCap) == 0 ) { if ( loHead == null ) { loHead = e; } else { loTail. next = e; } loTail = e; } else { if ( hiHead == null ) { hiHead = e; } else { hiTail. next = e; } hiTail = e; } e = e. next; } while ( e != null ) ; if ( loTail != null ) { loTail. next = null ; newTabs[ j] = loHead; } if ( hiTail != null ) { hiTail. next = null ; newTabs[ j + oldCap] = hiHead; } } tables = newTabs; threshold <<= 1 ; } private void put ( K key, V val) { putVal ( hash ( key) , key, val) ; } private void putVal ( int hash, K key, V val) { int n = tables. length; int i = hash & ( n - 1 ) ; if ( tables[ i] == null ) { tables[ i] = newNode ( hash, key, val) ; ++ size; return ; } Node < K , V > e = tables[ i] ; while ( e. next != null ) { if ( e. hash == hash && ( e. key == key || e. key. equals ( key) ) ) { e. val = val; return ; } e = e. next; } e. next = newNode ( hash, key, val) ; if ( ++ size > threshold) { resizePlus ( ) ; } } private V get ( K key) { return getVal ( hash ( key) , key) ; } private V getVal ( int hash, K key) { int n = tables. length; int i = hash & ( n - 1 ) ; Node < K , V > e = tables[ i] ; while ( e != null ) { if ( e. hash == hash && ( e. key == key || e. key. equals ( key) ) ) { return e. val; } e = e. next; } return null ; } public static void main ( String [ ] args) { SimpleHashMap < String , Integer > map = new SimpleHashMap < > ( 10 ) ; for ( int i = 1000000 ; i <= 1000100 ; i++ ) { map. put ( String . valueOf ( i) , i) ; System . out. println ( i + ":" + map. getTableLength ( ) ) ; SimpleHashMap < String , Integer > . Node< String , Integer > [ ] tables = map. getTables ( ) ; int j = 0 ; for ( SimpleHashMap < String , Integer > . Node< String , Integer > table : tables) { if ( table != null ) { System . out. printf ( "table[%d]=[%s:%d]" , j++ , table. key, table. val) ; SimpleHashMap < String , Integer > . Node< String , Integer > e = table; while ( e. next != null ) { e = e. next; System . out. printf ( "->[%s:%d]" , e. key, e. val) ; } } else { System . out. printf ( "table[%d]=null" , j++ ) ; } System . out. println ( ) ; } } }
}
3.5. LinkedHashMap
3.5.1. LinkedHashMap特点
LinkedHashMap继承了HashMap 并对每个entry使用指针额外维护了先后关系【before,after】 因此可以按照插入的顺序进行遍历
HashMap的Entry属性
3.5.2. LinkedHashMap实现LRU
LinkedHashMap继承了HashMap,并在原先entry的基础上额外添加了before和after两个指针,用于记录元素的插入顺序,因此LinkedHashMap能够按照插入顺序或访问顺序迭代元素,而HashMap迭代元素的顺序是不确定的 当accessOrder=true时,每次访问一个节点时,会将该节点移动到链表末尾 可以通过将accessOrder设置为true,并重写removeEldestEntry方法,来实现LRU缓存
设置accessOrder=true,会将节点移动到最后
public class Main { static class LRUCache < K , V > extends LinkedHashMap < K , V > { public final int cap; public LRUCache ( int cap) { super ( cap, 0.75f , true ) ; this . cap = cap; } @Override protected boolean removeEldestEntry ( Map. Entry < K , V > eldest) { return size ( ) > cap; } } public static void main ( String [ ] args) { Scanner scanner = new Scanner ( System . in) ; int n = scanner. nextInt ( ) ; int m = scanner. nextInt ( ) ; LRUCache < Integer , Integer > LRUCache = new LRUCache < > ( m) ; int cnt = 0 ; for ( int i= 1 ; i<= n; i++ ) { int x = scanner. nextInt ( ) ; LRUCache . put ( x, ++ cnt) ; List < Integer > list = new ArrayList < > ( LRUCache . keySet ( ) ) ; Collections . reverse ( list) ; System . out. println ( list) ; } }
}
import java. util. * ;
public class Main { public static void main ( String [ ] args) { Scanner scanner = new Scanner ( System . in) ; LinkedHashMap < Integer , Integer > linkedHashMap = new LinkedHashMap < > ( ) ; int n = scanner. nextInt ( ) ; int m = scanner. nextInt ( ) ; int cnt = 0 ; for ( int i= 1 ; i<= n; i++ ) { int x = scanner. nextInt ( ) ; if ( linkedHashMap. size ( ) < m) { } else if ( linkedHashMap. containsKey ( x) ) { linkedHashMap. remove ( x) ; } else { linkedHashMap. remove ( linkedHashMap. entrySet ( ) . iterator ( ) . next ( ) . getKey ( ) ) ; } linkedHashMap. put ( x, ++ cnt) ; List < Integer > list = new ArrayList < > ( linkedHashMap. keySet ( ) ) ; Collections . reverse ( list) ; System . out. println ( list) ; } }
}
3.6. TreeMap
3.6.1. TreeMap特点
TreeMap底层是红黑树,是一种自平衡的二叉查找树 平衡因子不会大于1(每个节点的左右子树高度最多相差1) 需要通过旋转的方式保持树的平衡 查找、插入、删除平均时间复杂度均是O(logn) 二叉查找树,每个节点都大于左子树的任何一个节点,小于右子树的任何一个节点,节点具有顺序性 TreeMap通过key的比较器决定元素的顺序,可以传入比较器Comparator
红黑树,自平衡的二叉查找树
3.6.2. 红黑树特点
根节点和叶子节点都是黑色 每个节点要么是红色,要么是黑色 红色节点的父节点和子节点必然是黑色的 从任意节点到其叶子节点的路径都包含相同数量的黑色节点 408口诀,根叶黑,不红红,路相同
3.6.3. 红黑树插入调整
3.7. ConcurrentHashMap
3.7.1. ConcurrentHashMap特点
JDK1.7 ConcurrentHashMap由Segment数组和哈希表组成,每个Segment对应一个可扩容哈希表 ConcurrentHashMap通过对Segment数组进行上锁从而实现同步 ConcurrentHashMap的Segment数组长度n一旦初始化就不可改变,默认是16,代表了可支持最大的线程并发数,每个Segment上同一时间只能有一个线程可以操作
JDK1.7,ConcurrentHashMap对Segment上锁
JDK1.8及以后 ConcurrentHashMap通过自旋、CAS、synchronized操作HashMap实现同步 ConcurrentHashMap通过对Node数组使用synchronized上锁从而实现同步
JDK1.8,Concurrent取消了Segment结构,改为数组、链表和红黑树,对数组上锁
3.7.2. ConcurrentHashMap源码分析
3.7.2.1. initTable,初始化Node数组
变量sizeCtl,用于判断table的状态 -1,表明table正在初始化,其他线程需要自旋,让出CPU 0,表明table初始大小,表明没有被初始化 n,表示table扩容的阈值,表明table已经初始化
private final Node < K , V > [ ] initTable ( ) { Node < K , V > [ ] tab; int sc; while ( ( tab = table) == null || tab. length == 0 ) { if ( ( sc = sizeCtl) < 0 ) Thread . yield ( ) ; else if ( U . compareAndSwapInt ( this , SIZECTL, sc, - 1 ) ) { try { if ( ( tab = table) == null || tab. length == 0 ) { int n = ( sc > 0 ) ? sc : DEFAULT_CAPACITY; @SuppressWarnings ( "unchecked" ) Node < K , V > [ ] nt = ( Node < K , V > [ ] ) new Node < ? , ? > [ n] ; table = tab = nt; sc = n - ( n >>> 2 ) ; } } finally { sizeCtl = sc; } break ; } } return tab;
}
3.7.2.2. putVal,插入数据
如果插入的数据,key或者value为空,则会直接抛出空指针异常 如果数组位置为空,使用CAS机制尝试写入,失败则继续自旋保证写入成功 如果数组位置不为空,需要遍历链表,此时利用synchronized上锁写入数据
public V put ( K key, V value) { return putVal ( key, value, false ) ;
}
final V putVal ( K key, V value, boolean onlyIfAbsent) { if ( key == null || value == null ) throw new NullPointerException ( ) ; int hash = spread ( key. hashCode ( ) ) ; int binCount = 0 ; for ( Node < K , V > [ ] tab = table; ; ) { Node < K , V > f; int n, i, fh; if ( tab == null || ( n = tab. length) == 0 ) tab = initTable ( ) ; else if ( ( f = tabAt ( tab, i = ( n - 1 ) & hash) ) == null ) { if ( casTabAt ( tab, i, null , new Node < K , V > ( hash, key, value, null ) ) ) break ; } else if ( ( fh = f. hash) == MOVED) tab = helpTransfer ( tab, f) ; else { V oldVal = null ; synchronized ( f) { if ( tabAt ( tab, i) == f) { if ( fh >= 0 ) { binCount = 1 ; for ( Node < K , V > e = f; ; ++ binCount) { K ek; if ( e. hash == hash && ( ( ek = e. key) == key || ( ek != null && key. equals ( ek) ) ) ) { oldVal = e. val; if ( ! onlyIfAbsent) e. val = value; break ; } Node < K , V > pred = e; if ( ( e = e. next) == null ) { pred. next = new Node < K , V > ( hash, key, value, null ) ; break ; } } } else if ( f instanceof TreeBin ) { Node < K , V > p; binCount = 2 ; if ( ( p = ( ( TreeBin < K , V > ) f) . putTreeVal ( hash, key, value) ) != null ) { oldVal = p. val; if ( ! onlyIfAbsent) p. val = value; } } } } if ( binCount != 0 ) { if ( binCount >= TREEIFY_THRESHOLD) treeifyBin ( tab, i) ; if ( oldVal != null ) return oldVal; break ; } } } addCount ( 1L , binCount) ; return null ;
}
3.7.2.3. get,查找元素
ConcurrentHashMap读时无需获取锁,支持写时并发读
public V get ( Object key) { Node < K , V > [ ] tab; Node < K , V > e, p; int n, eh; K ek; int h = spread ( key. hashCode ( ) ) ; if ( ( tab = table) != null && ( n = tab. length) > 0 && ( e = tabAt ( tab, ( n - 1 ) & h) ) != null ) { if ( ( eh = e. hash) == h) { if ( ( ek = e. key) == key || ( ek != null && key. equals ( ek) ) ) return e. val; } else if ( eh < 0 ) return ( p = e. find ( h, key) ) != null ? p. val : null ; while ( ( e = e. next) != null ) { if ( e. hash == h && ( ( ek = e. key) == key || ( ek != null && key. equals ( ek) ) ) ) return e. val; } } return null ;
}
4. Set
4.1. HashSet
4.1.1. HashSet特点
本质是通过哈希映射h(key)判断元素是否存在 底层使用HashMap实现 key用来存具体的值,value默认为PRESENT = new Object() 判断是否存在,如果key的hashcode相等,并且key相等,就意味着这个值在hashMap中存在
public class HashSet < E > extends AbstractSet < E > implements Set < E > , Cloneable , java. io. Serializable { private transient HashMap < E , Object > map; private static final Object PRESENT = new Object ( ) ; public HashSet ( ) { map = new HashMap < > ( ) ; } public HashSet ( Collection < ? extends E > c) { map = new HashMap < > ( Math . max ( ( int ) ( c. size ( ) / .75f ) + 1 , 16 ) ) ; addAll ( c) ; } public HashSet ( int initialCapacity, float loadFactor) { map = new HashMap < > ( initialCapacity, loadFactor) ; } public HashSet ( int initialCapacity) { map = new HashMap < > ( initialCapacity) ; } HashSet ( int initialCapacity, float loadFactor, boolean dummy) { map = new LinkedHashMap < > ( initialCapacity, loadFactor) ; } public boolean add ( E e) { return map. put ( e, PRESENT) == null ; } public boolean contains ( Object o) { return map. containsKey ( o) ; } public boolean remove ( Object o) { return map. remove ( o) == PRESENT; }
}
4.2. TreeSet
4.2.1. TreeSet特点
本质是利用红黑树对键值的排序规则 底层使用TreeMap实现 key用来存具体的值,value默认为PRESENT = new Object() 利用二叉查找树的性质,在TreeMap上查找具体的key值返回
public class TreeSet < E > extends AbstractSet < E > implements NavigableSet < E > , Cloneable , java. io. Serializable { private transient NavigableMap < E , Object > m; private static final Object PRESENT = new Object ( ) ; TreeSet ( NavigableMap < E , Object > m) { this . m = m; } public TreeSet ( ) { this ( new TreeMap < E , Object > ( ) ) ; } public TreeSet ( Comparator < ? super E > comparator) { this ( new TreeMap < > ( comparator) ) ; } public TreeSet ( Collection < ? extends E > c) { this ( ) ; addAll ( c) ; } public TreeSet ( SortedSet < E > s) { this ( s. comparator ( ) ) ; addAll ( s) ; } public boolean add ( E e) { return m. put ( e, PRESENT) == null ; } public boolean contains ( Object o) { return m. containsKey ( o) ; } public boolean remove ( Object o) { return m. remove ( o) == PRESENT; }
}
5. Queue
5.1. PriorityQueue
5.1.1. PriorityQueue特点
优先队列,底层通过堆的方式实现,默认是小顶堆 大顶堆,是一颗完全二叉树,任意节点均满足大于左子树和右子树的任一节点的性质 小顶堆,是一颗完全二叉树,任意节点均满足小于左子树和右子树的任一节点的性质
5.1.2. PriorityQueue源码分析
5.1.2.1. siftUp,沿着链往上调整堆
从堆节点k出发,k位置一开始是空的节点,希望往堆中插入值为x的元素 沿着k所在的链往上逐个与父节点parent比较 如果parent节点的值比x大就将parent元素下移,否则将x插入到当前位置并停止
private void siftUp ( int k, E x) { if ( comparator != null ) siftUpUsingComparator ( k, x) ; else siftUpComparable ( k, x) ;
}
private void siftUpComparable ( int k, E x) { Comparable < ? super E > key = ( Comparable < ? super E > ) x; while ( k > 0 ) { int parent = ( k - 1 ) >>> 1 ; Object e = queue[ parent] ; if ( key. compareTo ( ( E ) e) >= 0 ) break ; queue[ k] = e; k = parent; } queue[ k] = key;
}
private void siftUpUsingComparator ( int k, E x) { while ( k > 0 ) { int parent = ( k - 1 ) >>> 1 ; Object e = queue[ parent] ; if ( comparator. compare ( x, ( E ) e) >= 0 ) break ; queue[ k] = e; k = parent; } queue[ k] = x;
}
siftUp,算法过程
5.1.2.2. siftDown,向下调整堆
从堆节点k出发,k位置一开始是空的节点,希望往堆中插入值为x的元素 先比较k位置的2个子节点,选择更小的子节点child作为下一位置 如果x值比child节点的值大,那么child节点上移并继续向下比较,否则将x插入当前位置并停止
private void siftDown ( int k, E x) { if ( comparator != null ) siftDownUsingComparator ( k, x) ; else siftDownComparable ( k, x) ;
}
private void siftDownComparable ( int k, E x) { Comparable < ? super E > key = ( Comparable < ? super E > ) x; int half = size >>> 1 ; while ( k < half) { int child = ( k << 1 ) + 1 ; Object c = queue[ child] ; int right = child + 1 ; if ( right < size && ( ( Comparable < ? super E > ) c) . compareTo ( ( E ) queue[ right] ) > 0 ) c = queue[ child = right] ; if ( key. compareTo ( ( E ) c) <= 0 ) break ; queue[ k] = c; k = child; } queue[ k] = key;
}
private void siftDownUsingComparator ( int k, E x) { int half = size >>> 1 ; while ( k < half) { int child = ( k << 1 ) + 1 ; Object c = queue[ child] ; int right = child + 1 ; if ( right < size && comparator. compare ( ( E ) c, ( E ) queue[ right] ) > 0 ) c = queue[ child = right] ; if ( comparator. compare ( x, ( E ) c) <= 0 ) break ; queue[ k] = c; k = child; } queue[ k] = x;
}
siftDown,算法过程
5.1.2.3. add,往堆中添加元素
在堆的末尾额外添加一个位置i,堆大小加一,然后从位置i出发插入x,siftUp向上调整堆
public boolean add ( E e) { return offer ( e) ;
}
public boolean offer ( E e) { if ( e == null ) throw new NullPointerException ( ) ; modCount++ ; int i = size; if ( i >= queue. length) grow ( i + 1 ) ; size = i + 1 ; if ( i == 0 ) queue[ 0 ] = e; else siftUp ( i, e) ; return true ;
}
5.1.2.4. poll,弹出堆顶
将堆顶弹出,并将堆末尾拿出x,堆大小减一,然后从堆顶位置0出发插入x,siftDown向下调整堆
public E poll ( ) { if ( size == 0 ) return null ; int s = -- size; modCount++ ; E result = ( E ) queue[ 0 ] ; E x = ( E ) queue[ s] ; queue[ s] = null ; if ( s != 0 ) siftDown ( 0 , x) ; return result;
}
5.1.2.5. remove,删除某元素
有坑,删除固定元素需要遍历整个堆,平均时间复杂度是O(n)的,如果有这个使用场景,更推荐使用TreeSet 删除某元素所在的位置k,并将堆末尾拿出x,堆大小减一,然后从位置k出发插入x,siftDown向下调整堆 如果x插入的位置不在k,首先对于未删除时k所在堆顶的堆,肯定是符合堆性质的(也就是说这个堆中任何元素都会比k的父节点大),所以堆调整后,堆顶仍然是比k的父节点大的,符合整体堆性质 如果x插入的位置在k,只能说明x是x所在堆顶的堆中最小的,那么还需要从k位置往上调整堆(因为无法判断x是否比k的父节点大)
public boolean remove ( Object o) { int i = indexOf ( o) ; if ( i == - 1 ) return false ; else { removeAt ( i) ; return true ; }
}
private int indexOf ( Object o) { if ( o != null ) { for ( int i = 0 ; i < size; i++ ) if ( o. equals ( queue[ i] ) ) return i; } return - 1 ;
}
private E removeAt ( int i) { modCount++ ; int s = -- size; if ( s == i) queue[ i] = null ; else { E moved = ( E ) queue[ s] ; queue[ s] = null ; siftDown ( i, moved) ; if ( queue[ i] == moved) { siftUp ( i, moved) ; if ( queue[ i] != moved) return moved; } } return null ;
}
5.1.3. 找出List中前M个最大的数字
import java. util. * ;
public class Main { public static void main ( String [ ] args) { int n = 10000000 ; int m = 10 ; List < Integer > list = new ArrayList < > ( ) ; for ( int i= 1 ; i<= n; i++ ) { list. add ( i) ; } Collections . shuffle ( list) ; PriorityQueue < Integer > priorityQueue = new PriorityQueue < > ( ) ; for ( Integer x : list) { if ( priorityQueue. size ( ) < m) { priorityQueue. add ( x) ; continue ; } if ( x > priorityQueue. peek ( ) ) { priorityQueue. poll ( ) ; priorityQueue. add ( x) ; } } priorityQueue. forEach ( System . out:: println ) ; }
}