Java集合分为四类分别为
Map:无序,具有key-value映射的集合,里面有一个entry类,里面有key和value
Set:无序,不可重复的集合
List:有序,可重复的集合
Queue:队列,先进先出
Map与Set集合、List集合的关系
1.与Set的关系,如果把Map中的所有key放到一起看,他就组成了一个set(无序且不可重复),其中hashSet的底层就是用hashMap实现的
2.与List的关系:如果把Map的所有value放到一起来看,他就组成了一个list(根据索引查找、可重复)
HashMap月HashSet的区别
1.接口不同:HashMap实现Map接口,HashSet实现Set接口
2.存储对象不同:HashMap存储键值对,HashSet存储一个对象
3.增加数据方式不同:HashMap使用put,HashSet使用add
4.hashcode对象不同:hashmap是key,hashSet使成员对象
5.hashmap较快,hashset较慢
6.hashset的底层是使用hashmap来实现的,就是add添加的value设置为键,hashmap的value为一个final static的object对象
数组和集合的区别
1.数组定长,集合不定长,数组实现的集合有自动扩容机制,集合可以保存具有映射关系的数据
2.数组可以是基本数据类型,也可以是对象。集合只能存储对象
ArrayList
已数组实现的集合,初始大小为10,当超出限制时会增加50%的容量,在进行扩容、删除时使用System.arrayCopy()来进行数据的而移动,大量移动性能不太高
自动扩容的代码:
private void ensureCapacityInternal(int minCapacity) {
if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
minCapacity = Math.max(DEFAULT_CAPACITY, minCapacity); }
ensureExplicitCapacity(minCapacity);
}
private void ensureExplicitCapacity(int minCapacity) {
modCount++;
// overflow-conscious code
if (minCapacity - elementData.length > 0)
grow(minCapacity);
}
private void grow(int minCapacity) {
// overflow-conscious code
int oldCapacity = elementData.length;
// 扩展为原来的1.5倍
int newCapacity = oldCapacity + (oldCapacity >> 1);
// 如果扩为1.5倍还不满足需求,直接扩为需求值
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);
}
LinkedList
已双向链表实现,容量没有空间限制,需要用指针来操作
里面主要的方法是node,set和get都利用了这个方法
Node node(int index) {
// assert isElementIndex(index);
if (index < (size >> 1)) {
Node x = first;
for (int i = 0; i < index; i++)
x = x.next; return x;
} else {
Node x = last;
for (int i = size - 1; i > index; i--)
x = x.prev;
return x;
}
}
这个方法将链表的时间复杂度从O(n)降到O(n/ 2)
HashMap
hashMap是由数组(默认为16)、链表、红黑树构成的集合,他存储的是具有映射关系的数据,其键不重复,loadfactor = 0.75,属于非同步的
1.工作原理:当put和get时,对key的hashcode做hash,即(key的hashcode高16位异或低16位),之后&上bucket的长度-1,(hash % length == (length -1) & hash),通过hash得到bucket的位置,取余的操作是使bucket平衡,下一步确定值通过equals来确定值
2.hash实现: h = (h.hashCode() ^ h >>>16);这样的目的是为了减少碰撞,
3.hashMap超过负载因子(load factor * 容量)大小时,将会resize一个原来长度的hashMap,就是bucket变为原来的两倍
put函数的代码
public V put(K key, V value) {
// 对key的hashCode()做hash
return putVal(hash(key), key, value, false, true);
}
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
boolean evict) {
Node[] tab; Node p; int n, i;
// tab为空则创建
if ((tab = table) == null || (n = tab.length) == 0)
n = (tab = resize()).length;
// 计算index,并对null做处理
if ((p = tab[i = (n - 1) & hash]) == null)
tab[i] = newNode(hash, key, value, null);
else {
Node 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)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
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;
// 超过load factor*current capacity,resize
if (++size > threshold)
resize();
afterNodeInsertion(evict);
return null;
整体的过程就是先判断数组是否为空,如果为空则创建,之后判断数组中是否存在该元素,如果不存在就存入数组,如果数组存在就判断是否为树,是树就调用putTreeval方法,如果是链表就经过for循环得到得到插入的位置,如果插入的个数达到转化为红黑树的数量就存为二叉树,如果已经存在则取代以前的值
get函数的代码
final Node getNode(int hash, Object key) {
Node[] tab; Node 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) { // 在树中get
if (first instanceof TreeNode)
return ((TreeNode)first).getTreeNode(hash,
// 在链表中get
do {
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.e
return e;
} while ((e = e.next) != null);
}
}
return null;
}
总的来说流程就是先找该bucket位置的,如果不是再去树种找,看是否是树,如果不是则去链表中找,如何后面都没有则返回null;
concurrentHashMap
为了解决hashmap在多线程下使用put操作会引起死循环
hashTable使用synchronized来保证线程安全的,但是在线程竞争激烈的情况下hashtable的效率低下,当一个线程访问hashtable的同步方法时,其他线程会进入阻塞状态
分段锁:
jdk1.7:
ConcurrrentHashmap是segment数组和hashEntry数组构成,setgment继承自reentrantLock,其中一个segmen里面包含一个 hashEntry数组,一个hashEntry数组里面包含多个hashEntry
HashTable容器在竞争激烈的并发环境下表现出效率低下的原因,是因为所有访问HashTable的线程都必须竞争同一把锁,那假如容器里有多把锁,每一把锁用于锁容器其中一部分数据,那么当多线程访问容器里不同数据段的数据时,线程间就不会存在锁竞争,从而可以有效的提高并发访问效率,这就是ConcurrentHashMap所使用的锁分段技术,首先将数据分成一段一段的存储,然后给每一段数据配一把锁,当一个线程占用锁访问其中一个段数据的时候,其他段的数据也能被其他线程访问,有些方法需要跨段,比如size()和containsValue(),它们可能需要锁定整个表而而不仅仅是某个段,这需要按顺序锁定所有段,操作完毕后,又按顺序释放所有段的锁。这里“按顺序”是很重要的,否则极有可能出现死锁,在ConcurrentHashMap内部,段数组是final的,并且其成员变量实际上也是final的,但是,仅仅是将数组声明为final的并不保证数组成员也是final的,这需要实现上的保证。这可以确保不会出现死锁,因为获得锁的顺序是固定的。(简书其他作者的理解)
put的流程:得到k的hash计算值 & segment数组,得到segment,之后在segment里面的put是hash值得到在segment里面的位置,然后为调用put方法,其中put方法调用tryLock方法,如果获取锁失败则地调用scanAndForput方法来就是循环调用tryLock,如果超过了我们设置的最大循环次数,就执行lock方法,阻塞等待,直到获取segment锁为止
public V put(K key, V value) {
Segment s;
//计算hash key值
int hash = hash(key);
//通过hash key值计算出存入Segment的位置
int j = (hash >>> segmentShift) & segmentMask;
if ((s = (Segment)UNSAFE.getObject
(segments, (j << SSHIFT) + SBASE)) == null)
//初始化Segment
s = ensureSegment(j);
//添加
return s.put(key, hash, value, false);
}
final V put(K key, int hash, V value, boolean onlyIfAbsent) {
//segment操作加锁,使用尝试获取锁方式。如果获取失败,进入scanAndLockForPut方法
HashEntry node = tryLock() ? null :
scanAndLockForPut(key, hash, value);
V oldValue;
try {
HashEntry[] tab = table;
int index = (tab.length - 1) & hash;
HashEntry first = entryAt(tab, index);
for (HashEntry e = first;;) {
if (e != null) {
K k;
if ((k = e.key) == key ||
(e.hash == hash && key.equals(k))) {
oldValue = e.value;
if (!onlyIfAbsent) {
e.value = value;
++modCount;
}
break;
}
e = e.next;
}
else {
if (node != null)
node.setNext(first);
else
node = new HashEntry(hash, key, value, first);
int c = count + 1;
if (c > threshold && tab.length < MAXIMUM_CAPACITY)
//扩容, 这是后续做解释
rehash(node);
else
setEntryAt(tab, index, node);
++modCount;
count = c;
oldValue = null;
break;
}
}
} finally {
//释放锁
unlock();
}
return oldValue;
}
get方法类似
总结:get方法不需要加锁,因为get不会改变map中数据
jdk1.8
采用Node + CAS + synchronizeds设计
public class ConcurrentHashMap extends AbstractMap
implements ConcurrentMap, Serializable {
transient volatile Node[] table;
}
static class Node implements Map.Entry {
final int hash;
final K key;
volatile V val;
volatile Node next;
}
总结:
Hashtable的任何操作都会把整个表锁住,是阻塞的。好处是总能获取最实时的更新,比如说线程A调用putAll写入大量数据,期间线程B调用get,线程B就会被阻塞,直到线程A完成putAll,因此线程B肯定能获取到线程A写入的完整数据。坏处是所有调用都要排队,效率较低。
ConcurrentHashMap 是设计为非阻塞的。在更新时会局部锁住某部分数据,但不会把整个表都锁住。同步读取操作则是完全非阻塞的。好处是在保证合理的同步前提下,效率很高。坏处是严格来说读取操作不能保证反映最近的更新。例如线程A调用putAll写入大量数据,期间线程B调用get,则只能get到目前为止已经顺利插入的部分数据。
应该根据具体的应用场景选择合适的HashMap。
TreeMap
这个是一个有顺序的map,可以根据key大小进行排序
put函数:如果存在的话,old value被替换,如果不存在,则新添一个节点,然后做红黑树的平衡操作
public V put(K key, V value) {
Entry t = root;
if (t == null) {
compare(key, key); // type (and possibly null) check
root = new Entry<>(key, value, null);
size = 1;
modCount++;
return null;
}
int cmp;
Entry parent;
// split comparator and comparable paths
Comparator super K> cpr = comparator;
// 如果该节点存在,则替换值直接返
if (cpr != null) {
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);
}
else {
if (key == null)
throw new NullPointerException();
@SuppressWarnings("unchecked")
Comparable super K> k = (Comparable super K>) key;
do {
parent = t;
cmp = k.compareTo(t.key);
if (cmp < 0)
t = t.left; else if (cmp > 0)
t = t.right;
else
return t.setValue(value);
} while (t != null);
}
// 如果该节点未存在,则新建
Entry e = new Entry<>(key, value, parent); if (cmp < 0)
parent.left = e;
else
parent.right = e;
// 红黑树平衡调整 fixAfterInsertion(e); size++;
modCount++;
return null;
}
get函数
final Entry getEntry(Object key) {
// Offload comparator-based version for sake of performance if (comparator != null)
return getEntryUsingComparator(key);
if (key == null)
throw new NullPointerException();
@SuppressWarnings("unchecked")
Comparable super K> k = (Comparable super K>) key;
Entry p = root;
// 按照二叉树搜索的方式进行搜索,搜到返回
while (p != null) {
int cmp = k.compareTo(p.key);
if (cmp < 0)
p = p.left;
else if (cmp > 0)
p = p.right;
else
return p;
}
return null;
}
public V get(Object key) {
Entry p = getEntry(key); return (p==null ? null : p.value);
}
successor
static TreeMap.Entry successor(Entry t) {
if (t == null)
return null;
else if (t.right != null) {
// 有右子树的节点,后继节点就是右子树的“最左节点”
// 因为“最左子树”是右子树的最小节点
Entry p = t.right;
while (p.left != null)
p = p.left;
return p;
} else {
// 如果右子树为空,则寻找当前节点所在左子树的第一个祖先节点
// 因为左子树找完了,根据LDR该D了
Entry p = t.parent;
Entry ch = t;
// 保证左子树
while (p != null && ch == p.right) {
ch = p;
p = p.parent;
}
return p;
}
}
successor就是中序遍历
LinkedHashMap
继承于HashMap的集合类,可以维持插入时的顺序和访问顺序
1.afterNodeAccess
void afterNodeAccess(Node e) { // move node to last
LinkedHashMap.Entry last;
// 如果定义了accessOrder,那么就保证最近访问节点放到最后
if (accessOrder && (last = tail) != e) { //将尾指针赋值给last
//b 为前一个元素 ,a为后一个元素
LinkedHashMap.Entry p = (LinkedHashMap.Entry)e, b = p.before, a = p.after;
//使访问后的元素为尾元素,即after为null
p.after = null;
//如果访问元素的前一个元素为null,那么他的后一个元素就为头指针
if (b == null)
head = a;
else
b.after = a; // 将前一个元素的下一个值指定为他的后一个元素
if (a != null) // 如果后一个元素不为空,那么后一个元素的前一个元素为b(访
问元素的前一个元素)
a.before = b;
else
last = b; //如果是空,则将last设置为b,即访问元素的前一个为尾指针
if (last == null)
head = p; //如果last的空,这样的情况是链表只存在一个数据,但是数据被访
问 了,则将head 设置成最后那个数据
else {
//否则将p的前指针指向尾指针,将尾指针的下一个元素指向访问的元素
p.before = last;
last.after = p;
}
tail = p;
++modCount;
}
}
afterNodeInsertion
void afterNodeInsertion(boolean evict) { // possibly remove elde
st
LinkedHashMap.Entry first;
// 如果定义了溢出规则,则执行相应的溢出
if (evict && (first = head) != null && removeEldestEntry(fir
st)) {
K key = first.key;
removeNode(hash(key), key, null, false, true);
}
}
afterNodeRemoval
void afterNodeRemoval(Node e) { // unlink // 从链表中移除节点
LinkedHashMap.Entry p =
(LinkedHashMap.Entry)e, b = p.before, a = p.after; p.before = p.after = null;
if (b == null)
head = a;
else
b.after = a;
if (a == null)
tail = b;
else
a.before = b;
}