文章目录

  • 一、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集合的框架图(图片来自网络,侵删):


下面我们主要介绍其中的ListMap以及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;如下图所示:

  • 优点:
  1. 顺序查找、随机查找都非常的快(通过索引即数组下标可以在O(1)时间内找到指定元素),如下所示的elementData数组存储的为ArrayList的数据。
  2. 顺序存储,存储的利用率非常的高(使用数组进行连续存储)。
  • 缺点:
  1. 在进行插入数据达到阀值的时候,需要构建新的数组并将旧数组中的元素移动到新的数组中(使用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. 在进行插入和删除操作的时候,都需要移动其余元素。

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 对象的时候,它们都会先获取锁,然后复制一份当前数据作为副本,然后在当前的数据副本上做修改,最后把修改提交,然后释放锁。如下所示的方法:

未加锁的读方法:

加了锁的修改方法:

CopyOnWriteArrayListVector高效,主要有以下2个原因:

  1. Vector中,读写操作都被加锁了,而CopyOnWriteArrayList中,只有写操作才被加锁,而读操作没有进行加锁。这样,当读线程数量远大于写线程数量的时候(多读写少),CopyOnWriteArrayList尤为高效。
  2. 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的实现
    1. 存储结构:数组+链表(图片来自网络,侵删)

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. 存储结构:数组+链表/红黑树
    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;
}
  1. 是否存在如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.synchronizedMapHashMap包装起来,返回支持同步map结构(线程安全的SynchronizedMap)。为了保证按顺序访问,必须通过返回的SynchronizedMap完成对底层HashMap的所有访问,如下所示:
Map map = Collections.synchronizedMap(hashMap);
map.size();
map.containsKey();
......

2.3 HashTable

HashMapHashTable总体的实现逻辑相仿,但是存在如下的不同:

  • 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.存储结构
一个segment数组+多个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方法:
如果连续两次所有Segmentmodcount和相等,则过程中没有发生其他线程修改ConcurrentHashMap的情况,返回获得的值。否则再锁定所有segment,重新计算:

get方法:
get方法没有使用锁,而是通过Unsafe对象的getObjectVolatile()方法提供的原子读语义,来获得Segment以及对应的链表,然后对链表遍历判断是否存在key相同的节点以及获得该节点的value。

ConcurrentHashMap总结 https://my.oschina.net/hosee/blog/675884

JDK 1.8的实现

1.存储结构:
Node数组+链表+红黑树(图片来自网络,侵删)

2.实现原理:
使用finalvolatilesynchronized、native的CAS方法以及引入一些辅助类(如TreeBinTraverser等对象内部类)来控制并发,将锁的粒度降低到了节点层面。

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集合整理相关推荐

  1. Java常用基础-String和各类集合

    String public class StringExample {public static void main(String[] args) {// 1.Java定义字符串StringExamp ...

  2. java 性能 排序_Java常用排序算法及性能测试集合

    package algorithm.sort; import java.lang.reflect.Method; import java.util.Arrays; import java.util.D ...

  3. Java 常用集合类学习

    Java 常用集合类学习 1 Collection集合 1.1 Collection集合简介 1.2 Collection集合基本操作 1.3 Collection集合迭代 2 List集合 2.1 ...

  4. 笔记整理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类 ...

  5. java map常用类及其方法_Day50.Map类常用的方法 -Java常用类、集合#、IO

    Day50.Map类常用的方法 -Java常用类.集合#.IO Day50.Map类常用的方法 -Java常用类.集合#.IO Map类常用的方法 . 总结: 常用方法 添加: put(Object ...

  6. 笔记整理2----Java语言基础(二)06 断点调试与数据加密+07 面向对象-类与对象+08 java常用API-基础+09 java集合+10 IO流-基础

    06 断点调试与数据加密+07 面向对象-类与对象+08 java常用API-基础+09 java集合+10 IO流-基础 第06天 java基础语法 今日内容介绍  Eclipse断点调试  基 ...

  7. java常用工具类和Hutool常用的工具类整理

    java常用工具类和Hutool常用的工具类整理 1.java常用工具类 1.1 Scanner类 /*** Scanner 类*/@Testpublic void testScanner() {Sc ...

  8. Java、Set、Map集合框架知识大全,收藏备用

    前言 Java集合框架的知识在Java基础阶段是极其重要的,我平时使用List.Set和Map集合时经常出错,常用方法还记不牢, 于是就编写这篇博客来完整的学习一下Java集合框架的知识,如有遗漏和错 ...

  9. java常用的集合对象_java常用实体类、集合类

    java常用实体类.集合类 [转自51cto博客jichangwei的BLOG] 1:String类,字符串是常量,他们的值在创建之后不能更改,可以共享. equals()用来比较两个字符串的值,== ...

  10. JAVA day20、21 双列集合Map<K,V>:HashMap,LinkedHashMap,TreeMap,Hashtable, ConcurrentHashMap;JDK1.9新特性

    一.Map<K,V> Java提供了专⻔的集合类⽤来存放这种这种⼀⼀对应的关系,叫做映射对象,即 java.util.Map 接⼝. 类型参数: K - 此映射所维护的键的类型 V - 映 ...

最新文章

  1. 华为路由器 android,华为智能路由器亮相 Android系统很强大
  2. BZOJ1801: [Ahoi2009]chess 中国象棋
  3. EF三种编程方式详细图文教程(C#+EF)之Database First
  4. mysqlsql怎么比较当前月与去年的这个月的同比_多数房企前10月业绩稳步增长 这12家企业为何“负增长”?...
  5. java整数的因式分解_如何在Java中找到整数的质数-因式分解
  6. java md5 utf-8_Jquery与java MD5加密不同
  7. python查看函数参数快捷键_python查看函数源代码快捷键_pycharm中查看源码的快捷键...
  8. Java设计模式之工厂方法模式与抽象工厂模式
  9. 毕业设计项目,微博语料情感分析,文本分类
  10. WinCE --- 调试RS485串口
  11. 【数据结构】二分查找代码模板
  12. magento2 发邮件
  13. 更改网络计算机ip,教你快速修改电脑IP地址
  14. RuntimeError: latex was not able to process the following string: b‘lp‘
  15. postman不跨域 本地开发跨域_为什么postman调接口不会跨域而浏览器会
  16. 为Jumpserver 配置企业微信
  17. 海湾主机汉字注释表打字出_海湾消防主机字根表-海湾消防主机
  18. excel转vcf 易语言免费版
  19. Redmi AirDots只有一边有声音?? 如何进行双耳连接?
  20. Android Studio使用技巧

热门文章

  1. Win11 桌面图标突然无法点击如何解决
  2. linux 实验楼Linux 基础入门 作业
  3. centos 7 systemctl restart network失败处理
  4. Http多线程下载文件的处理机制
  5. 敏感词检测算法review
  6. Nvidia官方视频编解码性能
  7. 企业级SaaS市场暗流涌动 风口之上谁能问鼎2016
  8. 开船后做cqc_用Simulaqron和CQC模拟分布式量子隐形传态
  9. 学术论文写作 | (3) Experiment写作套路
  10. IO流(字节流和字符流)