文章目录
- 一、List
- 1.1 ArrayList
- 1.2 LinkedList
- 1.3 Vector
- 1.4 CopyOnWriteArrayList
- 二、Map
- 2.1 HashMap
- 2.3 HashTable
- 2.4 ConcurrentHashMap
- JDK 1.7的实现
- JDK 1.8的实现
- 2.5 TreeMap
- 三、Set
- 3.1 HashSet
- 3.2 TreeSet
如下所示的为Java集合的框架图(图片来自网络,侵删):
下面我们主要介绍其中的List
、Map
以及Set
以及各类型常用的类。
一、List
特性:允许重复元素的存在,数据的插入顺序是有序的。
如下demo所示:
public class ListDemo {
public static void main(String[] args) {
List<Integer> demo = Lists.newArrayList();
demo.add(1);
demo.add(2);
demo.add(3);
demo.add(1);
demo.add(4);
demo.add(5);
demo.add(3);
demo.forEach(x -> System.out.print(x + " "));
}
}
运行结果:
1.1 ArrayList
底层是通过数组(Object[]
)来实现的,适用于静态数据的查找。
如上图所示,在执行new ArrayList()
时,会初始化一个空的object数组,但是在首次执行add(E e)
方法之后,会初始化Object数组的默认大小为10;如下图所示:
- 优点:
- 顺序查找、随机查找都非常的快(通过索引即数组下标可以在O(1)时间内找到指定元素),如下所示的
elementData
数组存储的为ArrayList
的数据。 - 顺序存储,存储的利用率非常的高(使用数组进行连续存储)。
- 缺点:
- 在进行插入数据达到阀值的时候,需要构建新的数组并将旧数组中的元素移动到新的数组中(使用
Arrays.copy(oldArr,newArrLength)
);
浅谈ArrayList动态扩容
Java 中 ArrayList 的实现解析
- 在进行插入和删除操作的时候,都需要移动其余元素。
1.2 LinkedList
底层是通过双向链表来实现的,适用于经常进行变化的动态数据
- 优点:插入/删除不需要移动数据,只需要增减相应的节点即可
插入数据方法如下:
在执行add(E e)
方法时,会调用如下的linkLast(E e)
,采用尾插法加入新节点。
删除数据方法如下:
调用remove()
方法之后,首先会调用removeFirst()
方法,意思为删除头结点;
接着该方法会调用unlinkFirst(f)
方法来删除头结点:
- 缺点:查找的时候,只能通过节点的遍历来实现,最糟糕的情况查找指定节点的时间复杂度为(即需要遍历一半的链表)。
1.3 Vector
Vector与ArrayList的实现原理类似,都是基于Object[] 数组实现的,但是Vector默认扩容为原来的2倍(或是根据指定值进行扩容),而ArrayList则为1.5倍。
而Vector和ArrayList最大的区别是同步(synchronized)的使用,Vector对外暴露的大部分方法都是同步方法,所以Vector是线程安全的,如下的get和set方法所示:
synchronized方法为重量级同步,当某一个线程在调用vector对象的同步方法的同时,其它线程调用该vector对象的同步方法会导致线程阻塞,因此vector效率并不高。
1.4 CopyOnWriteArrayList
由于Vector
在多线程场景下效率并不高,所以对于读多写少的场景,可以使用CopyOnWriteArrayList
:
Copy-on-Write,也就是“写时复制”,当有 写类型的操作作用到 CopyOnWriteArrayList
对象的时候,它们都会先获取锁,然后复制一份当前数据作为副本,然后在当前的数据副本上做修改,最后把修改提交,然后释放锁。如下所示的方法:
未加锁的读方法:
加了锁的修改方法:
CopyOnWriteArrayList
比Vector
高效,主要有以下2个原因:
-
Vector
中,读写操作都被加锁了,而CopyOnWriteArrayList
中,只有写操作才被加锁,而读操作没有进行加锁。这样,当读线程数量远大于写线程数量的时候(多读写少),CopyOnWriteArrayList
尤为高效。 -
Vector
中,使用的是内置锁,而CopyOnWriteArrayList
中,使用的是jdk1.5引入的ReentrantLock
,相比于内置锁,ReentrantLock
的性能还是有所提升的。
如何线程安全地遍历List:Vector、CopyOnWriteArrayList http://xxgblog.com/2016/04/02/traverse-list-thread-safe/
Java 中 Vector 、Stack 、CopyOnWriteArrayList 的实现解析
二、Map
Map
结构主要有如下特性:
- 不允许重复的元素存在,重复的时候相同的key对应的data数据会进行覆盖;
- 插入的数据在存储的时候是按照hashcode顺序存储的,所以get的属性和put的顺序可能不一致;
- 作为key的对象,必须要同时复写
equals()
和hashcode()
方法。
2.1 HashMap
- 实现原理
- JDK 1.7的实现
- 存储结构:数组+链表(图片来自网络,侵删)
HashMap实现原理及源码分析
2.主要的方法:put()
方法:
如果发现有相同key值的Entry
存在,则使用本次值覆盖旧值并返回旧值,否则继续新增Entry
。
public V put(K key, V value) {
if (table == EMPTY_TABLE) {
inflateTable(threshold);
}
if (key == null)
return putForNullKey(value);//key为null时的处理
int hash = hash(key);//计算hash值
int i = indexFor(hash, table.length);//计算在hash桶中的索引位置
for (Entry<K,V> e = table[i]; e != null; e = e.next) {
Object k;
//如果当前索引位置有值,切key相等则使用当前value覆盖旧值,并返回
if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {
V oldValue = e.value;
e.value = value;
e.recordAccess(this);
return oldValue;
}
}
modCount++;
//添加新的Entry
addEntry(hash, key, value, i);
return null;
}
示意图如下所示:
put方法可能导致,hash表的扩容,扩容时需要将原hash表中的数据重新hash到新的表中,如下所示:
如下代码是使用的头插法,转移就链表的数据:
e.next = newTable[i];
newTable[i] = e;
示意图如下所示:
get()
方法:
get方法主要的思路就是首先对key值计算出在hash桶中的索引位置(table[indexFor(hash, table.length)
),接着在该索引位置找出key值相等的Entry的value值。get
方法主要是调用了如下的getEntry
方法:
3. HashMap存在的问题:
1)由于hashmap不是线程安全的所以在多线程情况下,读取和写入数据时会出现非预期结果;
2)在扩容的时候需要进行transfer
以及rehash
的操作,在这个过程中线程1可能会读到线程2transfer
后的结果,这样在transfer
节点的时候可能会造成环装链表,那么我们在get
方法的时候,对于这个存在hash冲突的情况下,去遍历链表就会导致死循环。
3)在hash大量冲突的时候数组+链表结构会倒退为链表结构,这样get数据的时候时间复杂度达到了。
谈谈HashMap线程不安全的体现
疫苗:JAVA HASHMAP的死循环 https://coolshell.cn/articles/9606.html
- JDK 1.8的实现
- 存储结构:数组+链表/红黑树
1)当达到节点阀值(默认为8)的时候,会将链表转化成红黑树;
2)当存在大量的hash冲突的时候,对map的查找会退化成一个红黑树,这样相对于链表来说时间查找的时间复杂度更优,可以达到。
(图片来自网络,侵删)
put方法主要步骤:
1)新判断hash数组Node<k,v>[] table
是否为空,为空则初始化;
2)通过hash值计算在table中的位置,如果当前位置没有其他节点存在,则不存在hash冲突,则直接新增新的节点;
3)如果存在hash冲突,则会有如下三种操作:
-----a.第一次存在hash冲突,则判断key值,如果相等,则记录该节点e;
-----b.如果冲突节点为树节点,则调用putTreeVal
方法进行put
操作;
-----c.如果不是以上两种则,遍历链表,并判断是否存在key相等的情况,相等则记录该节点e,否则使用尾插法(如果结点数等于8则将链表转换成红黑树)。
4)最终对于key相等的节点e,使用新的value
覆盖oldValue
并返回。
源码解读如下所示:
public V put(K key, V value) {
return putVal(hash(key), key, value, false, true);
}
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
boolean evict) {
Node<K,V>[] tab; Node<K,V> p; int n, i;
// table是否为空或者length等于0, 如果是则调用resize方法进行初始化
if ((tab = table) == null || (n = tab.length) == 0)
n = (tab = resize()).length;
// 通过hash值计算索引位置, 如果table表该索引位置节点为空则新增一个
if ((p = tab[i = (n - 1) & hash]) == null)// 将索引位置的头节点赋值给p
tab[i] = newNode(hash, key, value, null);
else { // table表该索引位置不为空
Node<K,V> e; K k;
if (p.hash == hash && // 判断p节点的hash值和key值是否跟传入的hash值和key值相等
((k = p.key) == key || (key != null && key.equals(k))))
e = p; // 如果相等, 则p节点即为要查找的目标节点,赋值给e
// 判断p节点是否为TreeNode, 如果是则调用红黑树的putTreeVal方法查找目标节点
else if (p instanceof TreeNode)
e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
else { // 走到这代表p节点为普通链表节点
for (int binCount = 0; ; ++binCount) { // 遍历此链表, binCount用于统计节点数
if ((e = p.next) == null) { // p.next为空代表不存在目标节点则新增一个节点插入链表尾部
p.next = newNode(hash, key, value, null);
// 计算节点是否超过8个, 减一是因为循环是从p节点的下一个节点开始的
if (binCount >= TREEIFY_THRESHOLD - 1)
treeifyBin(tab, hash);// 如果超过8个,调用treeifyBin方法将该链表转换为红黑树
break;
}
if (e.hash == hash && // e节点的hash值和key值都与传入的相等, 则e即为目标节点,跳出循环
((k = e.key) == key || (key != null && key.equals(k))))
break;
p = e; // 将p指向下一个节点
}
}
// e不为空则代表根据传入的hash值和key值查找到了节点,将该节点的value覆盖,返回oldValue
if (e != null) {
V oldValue = e.value;
if (!onlyIfAbsent || oldValue == null)
e.value = value;
afterNodeAccess(e); // 用于LinkedHashMap
return oldValue;
}
}
++modCount;
if (++size > threshold) // 插入节点后超过阈值则进行扩容
resize();
afterNodeInsertion(evict); // 用于LinkedHashMap
return null;
}
- 是否存在如
JDK1.7
的死循环问题:
上面已经提到了,导致死循环的问题主要是,在进行扩容进行节点的transfer
的时候,节点的顺序会反掉(如:1->2->3
会变成3->2->1
)。而Jdk1.8的普通链表的扩容如下所示:
final Node<K,V>[] resize() {
Node<K,V>[] oldTab = table;
int oldCap = (oldTab == null) ? 0 : oldTab.length;
int oldThr = threshold;
int newCap, newThr = 0;
if (oldCap > 0) { // 老table不为空
if (oldCap >= MAXIMUM_CAPACITY) { // 老table的容量超过最大容量值
threshold = Integer.MAX_VALUE; // 设置阈值为Integer.MAX_VALUE
return oldTab;
}
// 如果容量*2<最大容量并且>=16, 则将阈值设置为原来的两倍
else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
oldCap >= DEFAULT_INITIAL_CAPACITY)
newThr = oldThr << 1; // double threshold
}
else if (oldThr > 0) // 老表的容量为0, 老表的阈值大于0, 是因为初始容量被放入阈值
newCap = oldThr; // 则将新表的容量设置为老表的阈值
else { // 老表的容量为0, 老表的阈值为0, 则为空表,设置默认容量和阈值
newCap = DEFAULT_INITIAL_CAPACITY;
newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
}
if (newThr == 0) { // 如果新表的阈值为空, 则通过新的容量*负载因子获得阈值
float ft = (float)newCap * loadFactor;
newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
(int)ft : Integer.MAX_VALUE);
}
threshold = newThr; // 将当前阈值赋值为刚计算出来的新的阈值
@SuppressWarnings({"rawtypes","unchecked"})
// 定义新表,容量为刚计算出来的新容量
Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
table = newTab; // 将当前的表赋值为新定义的表
if (oldTab != null) { // 如果老表不为空, 则需遍历将节点赋值给新表
for (int j = 0; j < oldCap; ++j) {
Node<K,V> e;
if ((e = oldTab[j]) != null) { // 将索引值为j的老表头节点赋值给e
oldTab[j] = null; // 将老表的节点设置为空, 以便垃圾收集器回收空间
// 如果e.next为空, 则代表老表的该位置只有1个节点,
// 通过hash值计算新表的索引位置, 直接将该节点放在该位置
if (e.next == null)
newTab[e.hash & (newCap - 1)] = e;
else if (e instanceof TreeNode)
// 调用treeNode的hash分布(跟下面最后一个else的内容几乎相同)
((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
else { // preserve order
Node<K,V> loHead = null, loTail = null; // 存储跟原索引位置相同的节点
Node<K,V> hiHead = null, hiTail = null; // 存储索引位置为:原索引+oldCap的节点
Node<K,V> next;
do {
next = e.next;
//如果e的hash值与老表的容量进行与运算为0,则扩容后的索引位置跟老表的索引位置一样
if ((e.hash & oldCap) == 0) {
if (loTail == null) // 如果loTail为空, 代表该节点为第一个节点
loHead = e; // 则将loHead赋值为第一个节点
else
loTail.next = e; // 否则将节点添加在loTail后面
loTail = e; // 并将loTail赋值为新增的节点
}
//如果e的hash值与老表的容量进行与运算为1,则扩容后的索引位置为:老表的索引位置+oldCap
else {
if (hiTail == null) // 如果hiTail为空, 代表该节点为第一个节点
hiHead = e; // 则将hiHead赋值为第一个节点
else
hiTail.next = e; // 否则将节点添加在hiTail后面
hiTail = e; // 并将hiTail赋值为新增的节点
}
} while ((e = next) != null);
if (loTail != null) {
loTail.next = null; // 最后一个节点的next设为空
newTab[j] = loHead; // 将原索引位置的节点设置为对应的头结点
}
if (hiTail != null) {
hiTail.next = null; // 最后一个节点的next设为空
newTab[j + oldCap] = hiHead; // 将索引位置为原索引+oldCap的节点设置为对应的头结点
}
}
}
}
}
return newTab;
}
以上代码主要可以分为两部分,一部分为计算的hash表的容量,另一部分为将老表的数据转移到新表中。而对于存在hash冲突的链表则是在:
do {
next = e.next;
//如果e的hash值与老表的容量进行与运算为0,则扩容后的索引位置跟老表的索引位置一样
if ((e.hash & oldCap) == 0) {
if (loTail == null) // 如果loTail为空, 代表该节点为第一个节点
loHead = e; // 则将loHead赋值为第一个节点
else
loTail.next = e; // 否则将节点添加在loTail后面
loTail = e; // 并将loTail赋值为新增的节点
}
//如果e的hash值与老表的容量进行与运算为1,则扩容后的索引位置为:老表的索引位置+oldCap
else {
if (hiTail == null) // 如果hiTail为空, 代表该节点为第一个节点
hiHead = e; // 则将hiHead赋值为第一个节点
else
hiTail.next = e; // 否则将节点添加在hiTail后面
hiTail = e; // 并将hiTail赋值为新增的节点
}
中实现的原来的transfer
方法中转移hash冲突链表上的数据,转移数据到新的hash表是采用尾插法(即原来是1->2->3
,那么新的hash表中还是1->2->3
)。
Java集合:HashMap详解(JDK 1.8)
- 如何转换成线程安全
Collections.synchronizedMap
将HashMap
包装起来,返回支持同步map结构(线程安全的SynchronizedMap
)。为了保证按顺序访问,必须通过返回的SynchronizedMap
完成对底层HashMap
的所有访问,如下所示:
Map map = Collections.synchronizedMap(hashMap);
map.size();
map.containsKey();
......
2.3 HashTable
HashMap
和HashTable
总体的实现逻辑相仿,但是存在如下的不同:
-
HashMap
允许key和value为null,Hashtable
不允许。 -
HashMap
的默认初始容量为16,Hashtable
为11。 -
HashMap
的扩容为原来的2倍,Hashtable
的扩容为原来的2倍加1。 -
HashMap
是非线程安全的,Hashtable
是线程安全的。 -
HashMap
的hash值重新计算过,Hashtable
直接使用hashCode。 -
HashMap
去掉了Hashtable
中的contains方法。 -
HashMap
继承自AbstractMap
类,Hashtable
继承自Dictionary
类。
2.4 ConcurrentHashMap
JDK 1.7的实现
1.存储结构
一个segmen
t数组+多个HashEntry
组成+链表形式;
使用锁分离技术,将锁的粒度降低,利用多个锁来控制多个小的table。如下图所示,Segment
数组的意义就是将一个大的table分割成多个小的table(HashEntry
数组)来进行加锁。 (图片来自网络,侵删)
2.实现原理
相比于对整个Map
加锁的设计,分段锁大大的提高了高并发环境下的处理能力。但同时,由于不是对整个Map
加锁,导致一些需要扫描整个Map
的方法(如size()
, containsValue()
)需要使用特殊的实现,另外一些方法(如clear()
)甚至放弃了对一致性的要求(因此ConcurrentHashMap
不能完全替代HashTable
,因为HashTable
是属于强一致性的)。Segment:
Segment
继承了ReentrantLock
,所以其也具有了相应的锁特性:
HashEntry:
HashEntry
的value和next使用volatile
修饰,保证了数据的可见性:
并发度:
并发度可以理解为程序运行时能够同时更新ConccurentHashMap
且不产生锁竞争的最大线程数,实际上就是ConcurrentHashMap
中的分段锁个数,即Segment[]
的数组长度,默认为16,也可以用户在构造器中自定义:
并发度需要设置合理的值,过小会带来严重的锁竞争问题;如果并发度设置的过大,原本位于同一个Segment
内的访问会扩散到不同的Segment
中,CPU cache
命中率会下降,从而引起程序性能下降。3.常用方法解析:
put方法:
在put节点之前,先用tryLock()
尝试获取锁,如果成功获取,则插入新值或覆盖旧值:
如果获取锁失败,则调用scanAndLockForPut
方法,该方法会循环遍历链表(主要目的是希望遍历的链表被CPU cache所缓存,为后续实际put过程中的链表遍历操作提升性能),如果不存在相同的key则可以提前建立新的节点,循环遍历达到指定次数之后,会显示获取锁:
remove方法:
先定位在segment
数组中的位置,然后再尝试获取锁,如果未获取锁成功,则遍历链表,提高后去的缓存命中率。
size方法:
如果连续两次所有Segment
的modcount
和相等,则过程中没有发生其他线程修改ConcurrentHashMap
的情况,返回获得的值。否则再锁定所有的segment
,重新计算:
get方法:
get方法没有使用锁,而是通过Unsafe
对象的getObjectVolatile()
方法提供的原子读语义,来获得Segment
以及对应的链表,然后对链表遍历判断是否存在key相同的节点以及获得该节点的value。
ConcurrentHashMap总结
JDK 1.8的实现
1.存储结构:
Node数组+链表+红黑树(图片来自网络,侵删)
2.实现原理:
使用final
、volatile
、synchronized
、native的CAS
方法以及引入一些辅助类(如TreeBin
,Traverser
等对象内部类)来控制并发,将锁的粒度降低到了节点层面。
3.主要的方法
CAS方法:ConcurrentHashMap
定义了三个原子操作,用于对指定位置的节点进行操作。正是这些原子操作保证了ConcurrentHashMap
的线程安全。
//获得在i位置上的Node节点
static final <K,V> Node<K,V> tabAt(Node<K,V>[] tab, int i) {
return (Node<K,V>)U.getObjectVolatile(tab, ((long)i << ASHIFT) + ABASE);
}
//利用CAS算法设置i位置上的Node节点。之所以能实现并发是因为他指定了原来这个节点的值是多少
//在CAS算法中,会比较内存中的值与你指定的这个值是否相等,如果相等才接受你的修改,否则拒绝你的修改
//因此当前线程中的值并不是最新的值,这种修改可能会覆盖掉其他线程的修改结果 有点类似于SVN
static final <K,V> boolean casTabAt(Node<K,V>[] tab, int i,
Node<K,V> c, Node<K,V> v) {
return U.compareAndSwapObject(tab, ((long)i << ASHIFT) + ABASE, c, v);
}
//利用volatile方法设置节点位置的值
static final <K,V> void setTabAt(Node<K,V>[] tab, int i, Node<K,V> v) {
U.putObjectVolatile(tab, ((long)i << ASHIFT) + ABASE, v);
}
put方法
JDK8中的实现也是锁分离的思想,只是锁住的是一个Node
,而不是JDK7中的Segment
,而锁住Node之前的操作是无锁的并且也是线程安全的,建立在之前提到的3个原子操作上。
public V put(K key, V value) {
return putVal(key, value, false);
}
/** Implementation for put and putIfAbsent */
final V putVal(K key, V value, boolean onlyIfAbsent) {
//不允许 key或value为null
if (key == null || value == null) throw new NullPointerException();
//计算hash值
int hash = spread(key.hashCode());
int binCount = 0;
//死循环 何时插入成功 何时跳出
for (Node<K,V>[] tab = table;;) {
Node<K,V> f; int n, i, fh;
//如果table为空的话,初始化table
if (tab == null || (n = tab.length) == 0)
tab = initTable();
//根据hash值计算出在table里面的位置
else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) {
//如果这个位置没有值 ,直接放进去,不需要加锁
if (casTabAt(tab, i, null,
new Node<K,V>(hash, key, value, null)))
break; // no lock when adding to empty bin
}
//当遇到表连接点时,需要进行整合表的操作
else if ((fh = f.hash) == MOVED)
tab = helpTransfer(tab, f);
else {
V oldVal = null;
//结点上锁 这里的结点可以理解为hash值相同组成的链表的头结点
synchronized (f) {
if (tabAt(tab, i) == f) {
//fh〉0 说明这个节点是一个链表的节点 不是树的节点
if (fh >= 0) {
binCount = 1;
//在这里遍历链表所有的结点
for (Node<K,V> e = f;; ++binCount) {
K ek;
//如果hash值和key值相同 则修改对应结点的value值
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) {
//如果链表长度已经达到临界值8 就需要把链表转换为树结构
if (binCount >= TREEIFY_THRESHOLD)
treeifyBin(tab, i);
if (oldVal != null)
return oldVal;
break;
}
}
}
//将当前ConcurrentHashMap的元素数量+1
addCount(1L, binCount);
return null;
}
我们可以发现JDK8中的实现也是锁分离的思想,只是锁住的是一个Node
,而不是JDK7中的Segment
,而锁住Node
之前的操作是无锁的并且也是线程安全的,建立在之前提到的3个原子操作上。
get方法:
get方法比较简单,给定一个key来确定value的时候,必须满足两个条件 key相同 hash值相同,对于节点可能在链表或树上的情况,需要分别去查找。
public V get(Object key) {
Node<K,V>[] tab; Node<K,V> e, p; int n, eh; K ek;
//计算hash值
int h = spread(key.hashCode());
//根据hash值确定节点位置
if ((tab = table) != null && (n = tab.length) > 0 &&
(e = tabAt(tab, (n - 1) & h)) != null) {
//如果搜索到的节点key与传入的key相同且不为null,直接返回这个节点
if ((eh = e.hash) == h) {
if ((ek = e.key) == key || (ek != null && key.equals(ek)))
return e.val;
}
//如果eh<0 说明这个节点在树上 直接寻找
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;
}
ConcurrentHashMap总结
2.5 TreeMap
- 特性:存储的数据会根据key值进行排序
- 实现原理:红黑树(调整:左旋、右旋、着色)
Java提高篇(二七)—–TreeMap http://cmsblogs.com/?p=1013
三、Set
3.1 HashSet
- 特性:插入元素不可重复,无须的,允许null的存在
- 实现:
1.所有方法都是通过hashmap
来实现的,只将hashmap
的key暴露给用户使用
2.而底层的hashmap
的value为:private static final Object PRESENT = new Object();
HashMap和HashSet的区别 http://www.importnew.com/6931.html
3.2 TreeSet
- 特性:插入元素不重复,有序
- 实现:底层通过
treemap
来实现.
Java 集合系列17之 TreeSet详细介绍(源码解析)和使用示例