Java集合(总结,面试使用)
Posted 钢铁-程序猿
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Java集合(总结,面试使用)相关的知识,希望对你有一定的参考价值。
文章目录
Java容器
List
一、ArrayList
- 1、初始大小为10,扩容为现在的1.5
- 2、扩容使用Arrays.copyOf()把原来的整个数组复制到新的数组中。
- 3、变量的方式有
for循环遍历
增强for循环
迭代器
fail-fast(快速失败)
使用modCount记录list发生变化(增加删除)的次数
if (modCount != expectedModCount)
throw new ConcurrentModificationException();
序列化和反序列化
private void readObject(java.io.ObjectInputStream s)
private void writeObject(java.io.ObjectOutputStream s)
Vector和ArrayList的相同和不同
相同点:
- 底层都是数组实现的
- 默认长度都是10
不同点:
- Vector是线程安全的,因为方法上加了Synchronized
- 扩容容量,Vector是两倍,ArrayList是1.5倍
二、LinkedList
- LinkedList是一个双向链表实现的List
- LinkedList是一个双端队列,可以实现队列、双端队列、栈的特点
- 包含头、尾引用
LinkedList对于查找的优化
若index < 双向链表长度的1/2,则从前向后查找; 否则,从后向前查找。
public E get(int index)
checkElementIndex(index);
return node(index).item;
Node<E> node(int index)
// assert isElementIndex(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;
三、CopyOnWriteArrayList(有一个属性是ReentrantLock对象)
CopyOnWriteArrayList会使用ReentrantLock进行加锁。
public class CopyOnWriteArrayList<E>
implements List<E>, RandomAccess, Cloneable, java.io.Serializable
final transient ReentrantLock lock = new ReentrantLock();
private transient volatile Object[] array;
public CopyOnWriteArrayList() // 构造一个空数组 setArray(new Object[0]);
public CopyOnWriteArrayList(Collection<? extends E> c) // 将 传入的 Collection 转为Object[] 赋值给 array
public CopyOnWriteArrayList(E[] toCopyIn) // 将传入的数组 toCopyIn 赋值给 array
add方法(会加锁)
先将数组拷贝到一个容量为之前数组容量+1的数组中,其他线程如果并发遍历的时候,可能就是遍历的是原数组,而不是新的数组。
public boolean add(E e)
public void add(int index, E element)
final ReentrantLock lock = this.lock;
lock.lock();
try
Object[] elements = getArray();
int len = elements.length;
//检查越界情况
if (index > len || index < 0)
throw new IndexOutOfBoundsException("Index: "+index+
", Size: "+len);
Object[] newElements;
//移动的元素的个数
int numMoved = len - index;
if (numMoved == 0) //插在末尾
newElements = Arrays.copyOf(elements, len + 1);
else
newElements = new Object[len + 1];
System.arraycopy(elements, 0, newElements, 0, index);
System.arraycopy(elements, index, newElements, index + 1,
numMoved);
newElements[index] = element;
setArray(newElements);
finally
lock.unlock();
public boolean addIfAbsent(E e)
private boolean addIfAbsent(E e, Object[] snapshot) ---加锁的添加方法,详情看 jdk souorceCode注释
remove和add一样,先复制再删除再赋值。
get方法(未加锁)
public E get(int index)
return get(getArray(), index);
总结
- 1、CopyOnWriteArrayList使用ReentrantLock进行枷锁,保证线程安全。
- 2、CopyOnWriteArrayList的写操作都要先拷贝一份新数组,在新数组中做修改,修改完了再用新数组替换老数组,性能比较低下;
- 3、CopyOnWriteArrayList的读操作支持随机访问,时间复杂度为O(1);
- 4、CopyOnWriteArrayList采用读写分离的思想,读操作不加锁,写操作加锁,且写操作占用较大内存空间,所以适用于读多写少的场合;
- CopyOnWriteArrayList只保证最终一致性,不保证实时一致性;缺陷,对于边读边写的情况,不一定能实时的读到最新的数据
Map
1.7 HashMap
put方法总结(一定要看,不在桶里面的话,先判断需不需要扩容(扩容条件:size的值是不是大于等于阈值且那个Hash桶元素不为null))
- 1、没初始化的时候进行初始化操作
- 2、如果key为null的话,往桶0中去put元素
- 3、不为null的话,计算应该放在哪个桶,遍历这个桶,如果存在key相同且hash相同的元素,修改返回原来的值,如果不存在的话。
- 4、如果值不存在的话,先判断size的值是不是大于等于阈值,如果大于且那个Hash桶元素不为null,则进行扩容,存放元素。否则直接存放元素。
get方法总结
- 1、如果key为null,则到桶0中去寻找元素
- 2、如果不为null,先计算在哪个桶,再去那个桶中去找,看是否有key相同且hash值相同的元素。
1.8HashMap
- 最大容量为2的30次方
- 当一个桶中元素个数大于等于8的时候树化
- 当一个桶中元素小于等于6的时候转化成链表
- 当桶中元素个数达到64的时候才树化
- 有modCount属性,用于迭代时候执行快速失败操作
put函数总结(1.7 单独判断key是否为null,1.8单独判断桶是否为null)
- 1、如果没初始化的话,先初始化
- 2、如果计算出的hash桶的位置为null,那么就会直接放进去
- 3、如果不为null,如果桶的第一个元素的key值和hash值相同,则直接替换
- 4、如果第一个不为null,会判断是树还是链表,如果是树,放入树,是链表放入链表(在遍历链表的时候要注意可能要转成红黑树,要求树的长度大于等于8,如果在转树的时候桶容量小于64,则会扩容到64)。
- 5、插入之后,如果size大于阈值,则进行resize操作。
ConcurrentHashMap 1.7
put(总结,面试看懂这个即可)
定位Segment:(((h >>> segmentShift) & segmentMask) << SSHIFT) + SBASE
定位HashEntry:(((tab.length - 1) & h)) << TSHIFT) + TBASE
(先计算在哪个Entry(hash值的高sshift位和size-1进行按位与),再计算在Entry的哪个位置
put操作(先计算在哪个Entry(hash值的高sshift位和size-1进行按位与),再计算在Entry的哪个位置
-
1、ConcurrentHashMap中包含Segment数组,每个Segment中又包含HashEntry数组,Segment继承自ReentrantLock
在JDK1.7中,Java使用分段锁机制实现ConcurrentHashMap,在ConcurrentHashMap对象中有一个Segment数组(Segment类继承ReentrantLock类),即将整个Hash表划分成了多个Segment分段,即每个分段则类似于一个Hashtable,这样在执行put操作的时候首先根据hash算法定位到属于哪个Segment,然后对Segment加锁即可。因此ConcurrentHashMap在多线程并发执行的过程中实现了多线程put操作。 -
2、segmentShift用于定位参与散列运算的位数,其等于32减去sshift,使用32是因为ConcurrentHashMap的hash()方法返回的最大数是32位的,hash >>> segmentShift所以就会只剩下低sshift位为1,其余都为0,因此,求key散列到长度为ssize的Segment数组的下标j,就是求key的hash值的高sshift位,总的来说,计算在哪个Segment就是计算hash的高sshift位和hash值进行按位与。
总的来说,计算在Segment的哪个位置就是计算Segment的高sshift位与segmentMask进行按位与得到元素位于位于Segment中的下标。
@SuppressWarnings("unchecked")
public ConcurrentHashMap(int initialCapacity,
float loadFactor, int concurrencyLevel)
if (!(loadFactor > 0) || initialCapacity < 0 || concurrencyLevel <= 0)
throw new IllegalArgumentException();
if (concurrencyLevel > MAX_SEGMENTS)
concurrencyLevel = MAX_SEGMENTS;
// Find power-of-two sizes best matching arguments
int sshift = 0;
//Segment的大小,分段数组的大小,如果小于concurrencyLevel
//会被调整成大于或等于concurrencyLevel的最小的2的N次方值来作为segments数组的长度
//2^sshift=ssize
int ssize = 1;
while (ssize < concurrencyLevel)
++sshift;
ssize <<= 1;
//一个键值对在Segment数组中下标j的计算公式为:
//比如ssize=16,则segmentMask为1111b,
//j = (hash >>> segmentShift) & segmentMask
//2^sshift=ssize
this.segmentShift = 32 - sshift;
this.segmentMask = ssize - 1;
if (initialCapacity > MAXIMUM_CAPACITY)
initialCapacity = MAXIMUM_CAPACITY;
int c = initialCapacity / ssize;
if (c * ssize < initialCapacity)
++c;
int cap = MIN_SEGMENT_TABLE_CAPACITY;
while (cap < c)
cap <<= 1;
// create segments and segments[0]
Segment<K,V> s0 =
new Segment<K,V>(loadFactor, (int)(cap * loadFactor),
(HashEntry<K,V>[])new HashEntry[cap]);
Segment<K,V>[] ss = (Segment<K,V>[])new Segment[ssize];
UNSAFE.putOrderedObject(ss, SBASE, s0); // ordered write of segments[0]
this.segments = ss;
get操作(不加锁)
JDK1.7的ConcurrentHashMap的get操作是不加锁的,因为在每个Segment中定义的HashEntry数组和在每个HashEntry中定义的value和next HashEntry节点都是volatile类型的,volatile类型的变量可以保证其在多线程之间的可见性,因此可以被多个线程同时读,从而不用加锁。而其get操作步骤也比较简单,定位Segment –> 定位HashEntry –> 通过getObjectVolatile()方法获取指定偏移量上的HashEntry –> 通过循环遍历链表获取对应值。
size函数
ConcurrentHashMap的size操作的实现方法也非常巧妙,一开始并不对Segment加锁(遍历segment),而是直接尝试将所有的Segment元素中的count相加,这样执行两次,然后将两次的结果对比,如果两次结果相等则直接返回;而如果两次结果不同,则再将所有Segment加锁,然后再执行统计得到对应的size值。
ConcurrentHashMap 1.8
sizeCtl
- 1、-1,表示有线程正在进行初始化操作
- 2、-(1 + nThreads),表示有n个线程正在一起扩容
- 3、0,默认值,后续在真正初始化的时候使用默认容量
- 4、> 0,初始化或扩容完成后下一次的扩容门槛
put总结
- 1、如果桶数组未初始化,则初始化;
- 2、如果待插入的元素所在的桶为空,则尝试把此元素直接插入到桶的第一个位置(通过CAS,初始化的时候也使用CAS锁控制只有一个线程初始化桶数组,sizeCtl在初始化后存储的是扩容门槛);
- 3、如果正在扩容,则当前线程一起加入到扩容的过程中;
- 4、如果待插入的元素所在的桶不为空且不在迁移元素,则锁住这个桶(分段锁);
- 5、如果当前桶中元素以链表方式存储,则在链表中寻找该元素或者插入元素(需要判断是否要转化成红黑树);
- 6、如果当前桶中元素以红黑树方式存储,则在红黑树中寻找该元素或者插入元素;
- 7、如果元素存在,则返回旧值;
- 8、如果元素不存在,整个Map的元素个数加1,并检查是否需要扩容(每次添加元素后,元素数量加1,并判断是否达到扩容门槛,达到了则进行扩容或协助扩容,判断是否需要扩容使用的LongAdder思想。);
使用的锁有自旋锁+CAS+synchronized+分段锁(相当于一个Node一个锁)
为什么使用synchronized而不是ReentrantLock?
- 因为synchronized已经得到了极大地优化,在特定情况下并不比ReentrantLock差。
在判断是否需要扩容的时候,使用的是LongAdder的思想。
一些难点
(1)新桶数组大小是旧桶数组的两倍;
(2)迁移元素先从靠后的桶开始;
(3)迁移完成的桶在里面放置一ForwardingNode类型的元素,标记该桶迁移完成;
(4)迁移时根据hash&n是否等于0把桶中元素分化成两个链表或树;
(5)低位链表(树)存储在原来的位置;
(6)高们链表(树)存储在原来的位置加n的位置;
(7)迁移元素时会锁住当前桶,也是分段锁的思想;
LinkedHashMap
数组+红黑树+单链表+双向链表
要重写下面的这个函数才能实现LRU,比如重写成:
public boolean removeEldestEntry(Map.Entry<K,V>eldest)
// 当元素个数大于了缓存的容量, 就移除元素
return size()>this.capacity;
public class LinkedHashMap<K,V>
extends HashMap<K,V>
implements Map<K,V>
重要的三个函数
- 1、afterNodeAccess
在节点被访问后调用,主要在put已经存在的元素的或者get()时候被调用,如果accessOrder为true,调用这个方法把访问到的节点移动到双向链表的末尾 - 2、afterNodeInsertion
在HashMap的putVal方法中被调用,可以看到HashMap中这个方法的实现为空,如果evict为true,则移除最老的头节点。 - 3、afterNodeRemoval
在节点被删除的时候调用,从双链表中将节点删除。
//HashMap 中,这三个方法都是没实现的,在 LinkedHashMap 中实现来维护结点顺序
//
void afterNodeAccess(Node<K,V> p)
void afterNodeInsertion(boolean evict)
void afterNodeRemoval(Node<K,V> p)
//LinkedHashMap
/*
在节点访问之后被调用,主要在put()已经存在的元素或get()时被调用,
如果accessOrder为true,调用这个方法把访问到的节点移动到双向链表的末尾。
*/
void afterNodeAccess(Node<K,V> e) // move node to last
LinkedHashMap.Entry<K,V> last;
// accessOrder = true则执行,否则结束
// accessOrder = true, e 不是 tail 尾结点
if (accessOrder && (last = tail) != e)
LinkedHashMap.Entry<K,V> p =
(LinkedHashMap.Entry<K,V>)e, b = p.before, a = p.after;
p.after = null;
if (b == null)
head = a;
else
b.after = a;
if (a != null)
a.before = b;
else
last = b;
if (last == null)
head = p;
else
p.before = last;
last.after = p;
tail = p;
++modCount;
/*
在节点插入之后做些什么,在HashMap中的putVal()方法中被调用,可以看到HashMap中这个方法的实现为空。
evict:驱逐的意思
如果 evict 为 true,则移除最老的元素(head)
默认removeEldestEntry()方法返回false,也就是不删除元素。
*/
void afterNodeInsertion(boolean evict) // possibly remove eldest
LinkedHashMap.Entry<K,V> first;
//如果evict为true,且头节点不为空,且 确定移除最老的元素,即移除 head
//head 为 双向链表的头结点
if (evict && (first = head) != null && removeEldestEntry(first))
K key = first.key;
//HashMap.removeNode()从HashMap中把这个节点移除之后,会调用 afterNodeRemoval() 方法;
removeNode(hash(key), key, null, false, true);
//传进来的参数 是 双向链表的头结点 (即最老的结点)
protected boolean removeEldestEntry(Map.Entry<K,V> eldest)
return false;
/*
在节点被删除之后调用的方法 afterNodeInsertion -> HashMap.removeNode() -> afterNodeRemoval
从双向链表中 删除结点 e
*/
void afterNodeRemoval(Node<K,V> e) // unlink
LinkedHashMap.Entry<K,V> p =
(LinkedHashMap.Entry<K,V>)e, b = p.before, a = p.after;
// 把节点p从双向链表中删除。
p.before = p.after = null;
if (b == null)
head = a;
else
b.after = a;
if (a == null)
tail = b;
else
a.before = b;
/*
因此使用 LinkedHashMap 实现 LRU,
1) 设置 accessOrder 为 true --> 把最近访问的结点移动到尾部
2) 重写 removeEldestEntry 方法 --> 返回 true 会删除该结点, false 不删除
*/
总结
(1)LinkedHashMap继承自HashMap,具有HashMap的所有特性;
(2)LinkedHashMap内部维护了一个双向链表存储所有的元素;
(3)如果accessOrder为false,则可以按插入元素的顺序遍历元素;
(4)如果accessOrder为true,则可以按访问元素的顺序遍历元素;
(5)LinkedHashMap的实现非常精妙,很多方法都是在HashMap中留的钩子(Hook),直接实现这些Hook就可以实现对应的功能了,并不需要再重写put()等方法;
(6)默认的LinkedHashMap并不会移除旧元素,如果需要移除旧元素,则需要重写removeEldestEntry()方法设定移除策略;
(7)LinkedHashMap可以用来实现LRU缓存淘汰策略;
使用LinkedHashMap实现LRU
LinkedHashMap如何实现LRU缓存淘汰策略呢?
首先,我们先来看看LRU是个什么鬼。LRU,Least Recently Used,最近最少使用,也就是优先淘汰最近最少使用的元素。
如果使用LinkedHashMap,我们把accessOrder设置为true是不是就差不多能实现这个策略了呢?答案是肯定的。请看下面的代码:
public class LRUTest
public static void main(String[] args)
LRU<Integer,Integer> lru = new LRU(5,0.75f);
lru.put(1,1);
lru.put(2,2);
lru.put(3,3);
lru.put(4,4);
lru.put(5,5);
lru.put(6,6);
lru.put(7,7);
System.out.println(lru.get(4));
lru.put(6,666);
System.out.println(lru);
class LRU extends LinkedHashMap<K,V>
private int capacity;
public LRU(int capacity,int loadFactor)
super(capacity,loadFactor,true);
this.capacity = capacity;
/**
* 重写removeEldestEntry()方法设置何时移除旧元素
* @param eldest
* @return
*/
public boolean removeEldestEntry(Map.Entry<K,V>eldest)
// 当元素个数大于了缓存的容量, 就移除元素
return size()>this.capacity;
以上是关于Java集合(总结,面试使用)的主要内容,如果未能解决你的问题,请参考以下文章