Java常用的List、Map、Set集合整理
文章目录
- 一、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动态扩容 https://blog.csdn.net/zymx14/article/details/78324464
Java 中 ArrayList 的实现解析 https://blog.csdn.net/hjf_huangjinfu/article/details/60607250
- 在进行插入和删除操作的时候,都需要移动其余元素。
1.2 LinkedList
底层是通过双向链表来实现的,适用于经常进行变化的动态数据
- 优点:插入/删除不需要移动数据,只需要增减相应的节点即可
插入数据方法如下:
在执行add(E e)
方法时,会调用如下的linkLast(E e)
,采用尾插法加入新节点。
删除数据方法如下:
调用remove()
方法之后,首先会调用removeFirst()
方法,意思为删除头结点;
接着该方法会调用unlinkFirst(f)
方法来删除头结点:
- 缺点:查找的时候,只能通过节点的遍历来实现,最糟糕的情况查找指定节点的时间复杂度为 O ( n 2 ) O(\frac{n}{2}) O(2n)(即需要遍历一半的链表)。
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 的实现解析 https://blog.csdn.net/hjf_huangjinfu/article/details/64123255
二、Map
Map
结构主要有如下特性:
- 不允许重复的元素存在,重复的时候相同的key对应的data数据会进行覆盖;
- 插入的数据在存储的时候是按照hashcode顺序存储的,所以get的属性和put的顺序可能不一致;
- 作为key的对象,必须要同时复写
equals()
和hashcode()
方法。
2.1 HashMap
- 实现原理
- JDK 1.7的实现
- 存储结构:数组+链表(图片来自网络,侵删)
HashMap实现原理及源码分析 https://www.cnblogs.com/chengxiao/p/6059914.html
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++;//添加新的EntryaddEntry(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数据的时候时间复杂度达到了 O ( n ) O(n) O(n)。
谈谈HashMap线程不安全的体现 https://my.oschina.net/hosee/blog/673521
疫苗:JAVA HASHMAP的死循环 https://coolshell.cn/articles/9606.html
- JDK 1.8的实现
- 存储结构:数组+链表/红黑树
1)当达到节点阀值(默认为8)的时候,会将链表转化成红黑树;
2)当存在大量的hash冲突的时候,对map的查找会退化成一个红黑树,这样相对于链表来说时间查找的时间复杂度更优,可以达到 O ( l o g n ) O(log_n) O(logn)。
(图片来自网络,侵删)
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)// 将索引位置的头节点赋值给ptab[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覆盖,返回oldValueif (e != null) { V oldValue = e.value;if (!onlyIfAbsent || oldValue == null)e.value = value;afterNodeAccess(e); // 用于LinkedHashMapreturn oldValue;}}++modCount;if (++size > threshold) // 插入节点后超过阈值则进行扩容resize();afterNodeInsertion(evict); // 用于LinkedHashMapreturn 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_VALUEreturn 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的老表头节点赋值给eoldTab[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 orderNode<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,则扩容后的索引位置为:老表的索引位置+oldCapelse { if (hiTail == null) // 如果hiTail为空, 代表该节点为第一个节点hiHead = e; // 则将hiHead赋值为第一个节点elsehiTail.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,则扩容后的索引位置为:老表的索引位置+oldCapelse { if (hiTail == null) // 如果hiTail为空, 代表该节点为第一个节点hiHead = e; // 则将hiHead赋值为第一个节点elsehiTail.next = e; // 否则将节点添加在hiTail后面hiTail = e; // 并将hiTail赋值为新增的节点}
中实现的原来的transfer
方法中转移hash冲突链表上的数据,转移数据到新的hash表是采用尾插法(即原来是1->2->3
,那么新的hash表中还是1->2->3
)。
Java集合:HashMap详解(JDK 1.8) https://blog.csdn.net/v123411739/article/details/78996181
- 如何转换成线程安全
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总结 https://my.oschina.net/hosee/blog/675884
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算法中,会比较内存中的值与你指定的这个值是否相等,如果相等才接受你的修改,否则拒绝你的修改//因此当前线程中的值并不是最新的值,这种修改可能会覆盖掉其他线程的修改结果 有点类似于SVNstatic 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为nullif (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为空的话,初始化tableif (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的元素数量+1addCount(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总结 https://my.oschina.net/hosee/blog/675884
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详细介绍(源码解析)和使用示例 https://www.cnblogs.com/skywang12345/p/3311268.html
Java常用的List、Map、Set集合整理相关推荐
- Java常用基础-String和各类集合
String public class StringExample {public static void main(String[] args) {// 1.Java定义字符串StringExamp ...
- java 性能 排序_Java常用排序算法及性能测试集合
package algorithm.sort; import java.lang.reflect.Method; import java.util.Arrays; import java.util.D ...
- Java 常用集合类学习
Java 常用集合类学习 1 Collection集合 1.1 Collection集合简介 1.2 Collection集合基本操作 1.3 Collection集合迭代 2 List集合 2.1 ...
- 笔记整理4----Java语言高级(四)16 JAVA常用API-高级+17 泛型与常见数据结构+18 Map与Set集合+19 异常处理+20 IO流-高级
16 JAVA常用API-高级+17 泛型与常见数据结构+18 Map与Set集合+19 异常处理+20 IO流-高级 第05天 API 今日内容介绍 Object类 & System类 ...
- java map常用类及其方法_Day50.Map类常用的方法 -Java常用类、集合#、IO
Day50.Map类常用的方法 -Java常用类.集合#.IO Day50.Map类常用的方法 -Java常用类.集合#.IO Map类常用的方法 . 总结: 常用方法 添加: put(Object ...
- 笔记整理2----Java语言基础(二)06 断点调试与数据加密+07 面向对象-类与对象+08 java常用API-基础+09 java集合+10 IO流-基础
06 断点调试与数据加密+07 面向对象-类与对象+08 java常用API-基础+09 java集合+10 IO流-基础 第06天 java基础语法 今日内容介绍 Eclipse断点调试 基 ...
- java常用工具类和Hutool常用的工具类整理
java常用工具类和Hutool常用的工具类整理 1.java常用工具类 1.1 Scanner类 /*** Scanner 类*/@Testpublic void testScanner() {Sc ...
- Java、Set、Map集合框架知识大全,收藏备用
前言 Java集合框架的知识在Java基础阶段是极其重要的,我平时使用List.Set和Map集合时经常出错,常用方法还记不牢, 于是就编写这篇博客来完整的学习一下Java集合框架的知识,如有遗漏和错 ...
- java常用的集合对象_java常用实体类、集合类
java常用实体类.集合类 [转自51cto博客jichangwei的BLOG] 1:String类,字符串是常量,他们的值在创建之后不能更改,可以共享. equals()用来比较两个字符串的值,== ...
- JAVA day20、21 双列集合Map<K,V>:HashMap,LinkedHashMap,TreeMap,Hashtable, ConcurrentHashMap;JDK1.9新特性
一.Map<K,V> Java提供了专⻔的集合类⽤来存放这种这种⼀⼀对应的关系,叫做映射对象,即 java.util.Map 接⼝. 类型参数: K - 此映射所维护的键的类型 V - 映 ...
最新文章
- 华为路由器 android,华为智能路由器亮相 Android系统很强大
- BZOJ1801: [Ahoi2009]chess 中国象棋
- EF三种编程方式详细图文教程(C#+EF)之Database First
- mysqlsql怎么比较当前月与去年的这个月的同比_多数房企前10月业绩稳步增长 这12家企业为何“负增长”?...
- java整数的因式分解_如何在Java中找到整数的质数-因式分解
- java md5 utf-8_Jquery与java MD5加密不同
- python查看函数参数快捷键_python查看函数源代码快捷键_pycharm中查看源码的快捷键...
- Java设计模式之工厂方法模式与抽象工厂模式
- 毕业设计项目,微博语料情感分析,文本分类
- WinCE --- 调试RS485串口
- 【数据结构】二分查找代码模板
- magento2 发邮件
- 更改网络计算机ip,教你快速修改电脑IP地址
- RuntimeError: latex was not able to process the following string: b‘lp‘
- postman不跨域 本地开发跨域_为什么postman调接口不会跨域而浏览器会
- 为Jumpserver 配置企业微信
- 海湾主机汉字注释表打字出_海湾消防主机字根表-海湾消防主机
- excel转vcf 易语言免费版
- Redmi AirDots只有一边有声音?? 如何进行双耳连接?
- Android Studio使用技巧