Map集合
介绍
Map在Java⾥边是⼀个接⼝,常⻅的实现类有HashMap、LinkedHashMap、TreeMap和ConcurrentHashMap
内嵌类
Map是java中的接口,Map.Entry是Map的一个内部接口
Map集合封装键值对类型的数据,在Map集合的底层,每一个Key-Value键值对都被封装到一个Entry类型的子容器中,一个Entry对象只包含一个键值对,Map集合底层使用entry对象来存储这些键值对,在遍历Map集合时,可以通过遍历Key来遍历Map集合,还可以通过遍历entry对象来遍历集合。
Entry接口提供了getKey()方法和getValue()方法,用来获取entry对象存储的key和value值。
map 中的key使用set来进行存放的,注意由于是set来进行存储的, 那么不允许key是重复的
Map 接口提供三种collection 视图,允许以键集、值集或键-值映射关系集的形式查看某个映射的内容。
k,v默认是object类型
常见实现
常用API
包含键/值
存储/复制存储/如果不存在则存储否则返回当前值
移除
三种视图
结论:使用键值映射关系集,entry视图
键值映射关系集
推荐 直接通过Entry对象获取键和值,一个Entry只存储一个键值对
1 @Test 2 public void mapTest() { 3 HashMap<String,Object>map=new HashMap<>(); 4 map.put("1号",1); 5 map.put("2号","222"); 6 map.put("3号",666); 7 for(Map.Entry<String,Object> e:map.entrySet() 8 ){ 9 System.out.println(e.getKey()+"="+e.getValue()); 10 } 11 }
键集
只获取Map的键,返回Set
1 //返回键的set视图 2 Set<String>se=map.keySet(); 3 for(String a:se 4 ){ 5 System.out.println("k:"+a+map.get(a)); 6 }
值集
只获取Map的值,返回Collection,而且没有通过值找键的直接API
1 //返回值的Collection视图 2 Collection<Object>k=map.values(); 3 for(Object o:k 4 ){ 5 //该视图没有通过值找键的方法,Entry里面有直接获取键、值的方法 6 System.out.println(o); 7 }
HashMap
结果和hashset一样,无序,不能保证映射的顺序
通过hashcode和equals来判断键是否重复
基于hash表的Map接口实现,HashMap底层数据结构是数组+链表/红⿊树
此实现提供所有可选的映射操作,并允许使用null值和null键
HashMap类大致相当于Hashtable ,除了它是不同步的,并允许null
JDK1.8与JDK1.7的区别
数据结构:JDK1.8是数组+链表/红黑树;JDK1.7是数组+链表
存储对象:JDK1.8存的是节点对象,Node(单链),树节点(TreeNode,双向链表);JDK1.7存的是Entry对象
hash算法:JDK1.8是hashCode与高16位进行异或;JDK1.7是对String类型有个单独处理sun.misc.Hashing.stringHash32((String) k),再一堆异或尽量分散;
扩容:JDK1.8是插入后扩容,JDK1.7是插入前扩容
插入:JDK1.8是尾插法;JDK1.7是头插法,原因HashMap非线程安全,解决多线程死循环问题,多线程下推荐使用ConCurrentHashMap(线程安全的HashMap)
构造方法
负载因子是在容量自动增加之前允许哈希表得到满足的度量
通常,默认负载因子(0.75)提供了时间和空间成本之间的良好折衷。 更高的值会减少空间开销,但会增加查询条目的时间成本
常用API
HashMap new机制
HashMap有⼏个构造⽅法,但最主要的就是指定初始值⼤⼩和负载因⼦的⼤⼩。默认HashMap的⼤⼩为16,负载因⼦的⼤⼩为0.75,
HashMap的⼤⼩只能是2次幂的,假设你传⼀个10进去,实际上最终HashMap的⼤⼩是16,你传⼀个7进去,HashMap最终的⼤⼩是8,具体的实现在tableSizeFor可以看到。
把元素放进HashMap的时候,需要算出这个元素所在的位置(hash),在HashMap⾥⽤的是位运算来代替取模,能够更加⾼效地算出该元素所在的位置。
为什么HashMap的⼤⼩只能是2次幂,因为只有⼤⼩为2次幂时,才能合理⽤位运算替代取模(
右移是按2的倍数进行放缩, 2/4 =》2/ 2^2 =》2右移2位010 右移两位 =》 2/ 2^2的商,被移掉的两位=》2/ 2^2 的余数)
负载因⼦的⼤⼩决定着哈希表的扩容和哈希冲突,作为一般规则,默认负载因子(.75)提供了时间和空间成本之间的良好折中。 更高的值会降低空间开销,但会增加查找成本、发生hash碰撞的概率。
⽐如现在我默认的HashMap⼤⼩为16,负载因⼦为0.75,这意味着数组最多只能放12个元素,⼀旦超过12个元素,则哈希表需要扩容,HashMap的⼤⼩只能是2次幂,所以是2倍扩容。
--java3y, JDK8API文档
HashMap判断完全相同
hash一致&&(key地址一致或者keyequals相同)
1 first.hash == hash && // always check first node 2 ((k = first.key) == key || (key != null && key.equals(k)))
HashMap Put机制
Key的哈希值计算
先通过hashCode()正常计算哈希值,然后与⾼16位做异或运算,产⽣最终的哈希值。
增加随机性。
1 public V put(K key, V value) { 2 return putVal(hash(key), key, value, false, true); 3 }
1 static final int hash(Object key) { 2 int h;
// 低位和高位都参与运算 增加数据分散性 3 return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16); 4 }
生成Index
再与桶数-1做与运算 (取模运算的变种,前提是桶数为2的幂次,位运算更快) 以确定Index
put大致过程
计算hash后调用putval(),
数组为空就调resize初始化数组,通过(n-1)&hash位运算得到index
未发生碰撞直接新建Node存入
发生碰撞,判断是否是树,是的话遍历,按树结构新建TreeNode存入,发现完全相同的key则直接替换
不是树就是链表了,遍历按链表结构存入,发现相同的key则直接替换,尾插式,链表长度大于8,调treeifyBin如果数组长度大于64则会转为红黑树,否则调resize进行扩容(先存入节点再扩容)
最后会判断是否超过阈值,超过阈值也会扩容(先存入节点再扩容)
resize对树按高低位进行拆分成两个链表后,长度<6的树会退化成链表Node
链表和树的复杂度
链表查询时间复杂度O(N),插⼊时间复杂度O(1),红⿊树查询和插⼊时间复杂度O(logN)
源码分析
小破站视频 https://www.bilibili.com/video/BV1oR4y1N77G/?p=6&spm_id_from=pageDriver&vd_source=152ad2dc192867dca92d66a24472c851
1 /** 2 * put源码 3 */ 4 final V putVal(int hash, K key, V value, boolean onlyIfAbsent, 5 boolean evict) { 6 // 用局部变量操作,多次取值,从栈里面比从堆中取快 7 Node<K,V>[] tab; Node<K,V> p; int n, i; 8 // 判断索引数组是否为空 为空就初始化数组 resize可以初始化/扩容数组 9 if ((tab = table) == null || (n = tab.length) == 0) 10 n = (tab = resize()).length; 11 // (n-1)&hash 位运算代替取模(前提是容量是2的幂次) 算出index 12 if ((p = tab[i = (n - 1) & hash]) == null) 13 // 没有发生hash碰撞 直接存入node节点 14 tab[i] = newNode(hash, key, value, null); 15 // 发生hash碰撞 16 else { 17 Node<K,V> e; K k; 18 // 如果key完全相等 hash相同再比较(引用地址||equals) 19 if (p.hash == hash && 20 ((k = p.key) == key || (key != null && key.equals(k)))) 21 e = p; 22 // 如果key不是完全相等 23 // 判断是否是树节点 24 else if (p instanceof TreeNode) 25 // 红黑树的插入操作 26 e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value); 27 // 如果是链表 28 else { 29 // 遍历 binCount记录当前链表上的节点数 30 for (int binCount = 0; ; ++binCount) { 31 // 尾差 1.7是头插 32 if ((e = p.next) == null) { 33 p.next = newNode(hash, key, value, null); 34 // -1是因为 0-7是8 35 if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st 36 // 转红黑树,8 + 新要添加的该节点,实际是9个 treeIfyBin里面会进行扩容 37 treeifyBin(tab, hash); 38 break; 39 } 40 // 如果在当前链表下发现一样的key,更新链表节点 41 if (e.hash == hash && 42 ((k = e.key) == key || (key != null && key.equals(k)))) 43 break; 44 p = e; 45 } 46 } 47 // 补充:key相同的旧节点存在 48 if (e != null) { // existing mapping for key 49 V oldValue = e.value; 50 // onlyIfAbsent缺少才会存入 一个标记 对应putIfAbsent() 51 if (!onlyIfAbsent || oldValue == null) 52 // 更新节点值 53 e.value = value; 54 afterNodeAccess(e); 55 // 返回旧值 56 return oldValue; 57 } 58 } 59 ++modCount; 60 // 判断是否> 阈值 要进行扩容 61 if (++size > threshold) 62 resize(); 63 afterNodeInsertion(evict); 64 return null; 65 } 66 67 /------------------------------------------------------------树化处理 68 69 /** 70 * Replaces all linked nodes in bin at index for given hash unless 71 * table is too small, in which case resizes instead. 72 * 73 * 树化处理,真正处理是treeify() 74 * Node改为TreeNode,单项链表变成双向链表 75 */ 76 final void treeifyBin(Node<K,V>[] tab, int hash) { 77 int n, index; Node<K,V> e; 78 // 索引数组的长度小于64 会resize重排 79 if (tab == null || (n = tab.length) < MIN_TREEIFY_CAPACITY) 80 resize(); 81 else if ((e = tab[index = (n - 1) & hash]) != null) { 82 TreeNode<K,V> hd = null, tl = null; 83 do { 84 TreeNode<K,V> p = replacementTreeNode(e, null); 85 if (tl == null) 86 hd = p; 87 else { 88 // 单项链表改为双向链表 89 p.prev = tl; 90 tl.next = p; 91 } 92 tl = p; 93 // 遍历Node节点 生成TreeNode 94 } while ((e = e.next) != null); 95 if ((tab[index] = hd) != null) 96 // 真正改为红黑树 97 hd.treeify(tab); 98 } 99 } 100 101 102 203 204 205 206 /** 207 * Splits nodes in a tree bin into lower and upper tree bins, 208 * or untreeifies if now too small. Called only from resize; 209 * see above discussion about split bits and indices. 210 * 211 * @param map the map 212 * @param tab the table for recording bin heads 213 * @param index the index of the table being split 214 * @param bit the bit of hash to split on 215 * 216 * 分割树节点为高低位树节点,或者如果太小则取消树化。被resize方法调用 217 */ 218 final void split(HashMap<K,V> map, Node<K,V>[] tab, int index, int bit) { 219 TreeNode<K,V> b = this; 220 // Relink into lo and hi lists, preserving order 221 TreeNode<K,V> loHead = null, loTail = null; 222 TreeNode<K,V> hiHead = null, hiTail = null; 223 int lc = 0, hc = 0; 224 // 遍历 拆分高低位树节点 225 for (TreeNode<K,V> e = b, next; e != null; e = next) { 226 next = (TreeNode<K,V>)e.next; 227 e.next = null; 228 if ((e.hash & bit) == 0) { 229 if ((e.prev = loTail) == null) 230 loHead = e; 231 else 232 loTail.next = e; 233 loTail = e; 234 ++lc; 235 } 236 else { 237 if ((e.prev = hiTail) == null) 238 hiHead = e; 239 else 240 hiTail.next = e; 241 hiTail = e; 242 ++hc; 243 } 244 } 245 246 if (loHead != null) { 247 // 低位节点个数<6 转成链表 =》退化 TreeNode-> Node 248 if (lc <= UNTREEIFY_THRESHOLD) 249 tab[index] = loHead.untreeify(map); 250 else { 251 tab[index] = loHead; 252 if (hiHead != null) // (else is already treeified) 253 loHead.treeify(tab); 254 } 255 } 256 if (hiHead != null) { 257 if (hc <= UNTREEIFY_THRESHOLD) 258 tab[index + bit] = hiHead.untreeify(map); 259 else { 260 tab[index + bit] = hiHead; 261 if (loHead != null) 262 hiHead.treeify(tab); 263 } 264 } 265 }
扩容resize
大致过程
1.数组无数据就初始化,容量设置为16,返回数组
2.数组有数据,判断是否满载 >=MAXIMUM_CAPACITY 2^30,满载阈值拉满Integer.MAX_VALUE 2^32 -1,直接返回oldTab
3.否则按2倍容量新建一个Node数组 Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap]
4.遍历oldTab,重新计算节点index,存入新数组。这里有个规律,新index=旧index/旧index+oldCap,节点有四种情况,无数据,一个,多个(树/链表)。
5.只有一个节点的直接移过去就行,树和单链表各按高低位进行拆分(hash&oldTab==0表示低位,index不变的节点)再遍历,低位index不变,高位index=旧index+oldCap,另外拆分后长度<6的树会退化成链表
扩容后重新计算index规律/源码算法
新index = 旧index/旧index+oldCap
源码
1 /** 2 * Initializes or doubles table size. If null, allocates in 3 * accord with initial capacity target held in field threshold. 4 * Otherwise, because we are using power-of-two expansion, the 5 * elements from each bin must either stay at same index, or move 6 * with a power of two offset in the new table. 7 * 8 * @return the table 9 * 10 * 初始化或者双倍扩容数组 11 */ 12 final Node<K,V>[] resize() { 13 Node<K,V>[] oldTab = table; 14 int oldCap = (oldTab == null) ? 0 : oldTab.length; 15 int oldThr = threshold; 16 int newCap, newThr = 0; 17 // 老数组容量>0 18 if (oldCap > 0) { 19 // 判断老数组是不是已经非常大了 2^30 20 if (oldCap >= MAXIMUM_CAPACITY) { 21 // 0x7fffffff 2^32 - 1 ,容量拉满返回旧数组 22 threshold = Integer.MAX_VALUE; 23 return oldTab; 24 } 25 // 2倍扩容后容量<最大值且旧容量大于初始值16则阈值进行2倍扩容 26 else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY && 27 oldCap >= DEFAULT_INITIAL_CAPACITY) 28 // 阈值*2 29 newThr = oldThr << 1; // double threshold 30 } 31 // 旧数组非空 32 else if (oldThr > 0) // initial capacity was placed in threshold 33 // 初始容量设置为旧阈值 34 newCap = oldThr; 35 // 数组大小为0,还未初始化 36 else { // zero initial threshold signifies using defaults 37 // 初始化数组 大小为默认值16 38 newCap = DEFAULT_INITIAL_CAPACITY; 39 // 计算初始阈值 负载因子*初始容量 40 newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY); 41 } 42 // 新的阈值若还未计算,计算新的阈值 43 if (newThr == 0) { 44 // 新容量*负载因子 45 float ft = (float)newCap * loadFactor; 46 // 如果新容量<最大容量且计算阈值<最大容量 则使用计算阈值,否则拉满 47 newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ? 48 (int)ft : Integer.MAX_VALUE); 49 } 50 // 使用新阈值刷新阈值属性 51 threshold = newThr; 52 @SuppressWarnings({"rawtypes","unchecked"}) 53 Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap]; 54 table = newTab; 55 // 老数组不为空 56 if (oldTab != null) { 57 // 遍历数组,一个位置四种情况,无数据,1个数据,多数据(链表,树) 58 for (int j = 0; j < oldCap; ++j) { 59 Node<K,V> e; 60 // 该位置有数据, 一个数据,多数据(链表,树) 三种情况 61 if ((e = oldTab[j]) != null) { 62 oldTab[j] = null; 63 // next==nul,表示只有一个数据,直接转移位置 64 if (e.next == null) 65 // 重新计算index 存入新数组 66 newTab[e.hash & (newCap - 1)] = e; 67 // 如果是树 68 else if (e instanceof TreeNode) 69 // 对树按高低位拆分处理,得到两条链表,长度小于6树退化 70 ((TreeNode<K,V>)e).split(this, newTab, j, oldCap); 71 // 如果是链表 根据(n-1)&hash 得到规律=》 扩容的新index = 旧index(前提条件:e.hash & oldCap == 0)/旧index+旧数组容量(反之) 72 else { // preserve order 73 Node<K,V> loHead = null, loTail = null; 74 Node<K,V> hiHead = null, hiTail = null; 75 Node<K,V> next; 76 // 将链表拆成高位链表和低位链表,分别放到新的index位置 77 do { 78 next = e.next; 79 // 通过e.hash & oldCap == 0 区分高低位节点,该类节点重新计算index的结果是index不变 80 // e.hash & oldCap == 0 说明newCap-1的最高位&hash的对位是0,则,(newCap-1)&hash == (oldCap-1)&hash 81 if ((e.hash & oldCap) == 0) { 82 if (loTail == null) 83 // 作为低位链表的头结点 84 loHead = e; 85 else 86 loTail.next = e; 87 loTail = e; 88 } 89 // 该类节点新index = 旧index + oldCap 90 else { 91 if (hiTail == null) 92 // 作为高位链表的头结点 93 hiHead = e; 94 else 95 hiTail.next = e; 96 hiTail = e; 97 } 98 } while ((e = next) != null); 99 // 设置最后节点的指针指向null 100 if (loTail != null) { 101 loTail.next = null; 102 // 设置低位连边的头节点 103 newTab[j] = loHead; 104 } 105 if (hiTail != null) { 106 // 设置高位链表最后节点指针指向null 107 hiTail.next = null; 108 // 设置高位链表头结点 109 newTab[j + oldCap] = hiHead; 110 } 111 } 112 } 113 } 114 } 115 return newTab; 116 }
HashMap get机制
先通过hash计算index位置
当前位置为空则直接返回null,不为空判断key是否完全相同,是则直接返回
不是则根据树结构或者链表结构进行遍历,找不到仍然返回null
完全相同:
first.hash == hash && // always check first node ((k = first.key) == key || (key != null && key.equals(k)))
get源码
1 /** 2 * Returns the value to which the specified key is mapped, 3 * or {@code null} if this map contains no mapping for the key. 4 */ 5 public V get(Object key) { 6 Node<K,V> e; 7 return (e = getNode(hash(key), key)) == null ? null : e.value; 8 } 9 10 /** 11 * Implements Map.get and related methods. 12 * 13 * @param hash hash for key 14 * @param key the key 15 * @return the node, or null if none 16 */ 17 final Node<K,V> getNode(int hash, Object key) { 18 Node<K,V>[] tab; Node<K,V> first, e; int n; K k; 19 // 通过hash计算索引位置,该位置对象为空则直接返回null,否则继续 20 if ((tab = table) != null && (n = tab.length) > 0 && 21 (first = tab[(n - 1) & hash]) != null) { 22 // 如果key完全相同,返回该对象 23 if (first.hash == hash && // always check first node 24 ((k = first.key) == key || (key != null && key.equals(k)))) 25 return first; 26 // 否则判断next是否存在 27 if ((e = first.next) != null) { 28 // 如果为树则从树中取 29 if (first instanceof TreeNode) 30 return ((TreeNode<K,V>)first).getTreeNode(hash, key); 31 do { 32 // 遍历链表 33 if (e.hash == hash && 34 ((k = e.key) == key || (key != null && key.equals(k)))) 35 return e; 36 } while ((e = e.next) != null); 37 } 38 } 39 // 找不到返回null 40 return null; 41 }
HashMap和TreeMap选用
TreeMap是红黑树结构,按自然顺序或者自定义顺序排列,是有序的,适用于按自然顺序排列的场景。
HashMap是哈希表实现,数据时散列的,均匀的,不固定顺序,适用于在 Map中插入删除和定位数据,性能比TreeMap好。
concurrentHashMap
HashMap非线程安全,线程安全用ConcurrentHashMap
JUC下的线程安全的Map类实现(hashTable和用Collections包装出来的线程安全的Map都是用synchronize外套成 同步的,速度慢)
ConcurrentHashMap的底层数据结构是数组+链表/红⿊树,它能⽀持⾼并发的访问和更新,是线程安全的
ConcurrentHashMap通过在部分加锁和利⽤CAS算法来实现同步,在get的时候没有加锁,Node都⽤了volatile给修饰
在扩容时,会给每个线程分配对应的区间,并且为了防⽌putVal导致数据不⼀致,会给线程的所负责的区间加锁
--java3y《对线面试官》
get不加锁,volatile修饰提供对其他线程的可见性
不支持key或者value为null:因为多线程环境下,如果get(key)返回null,就无法判断值是null还是没有该key(A线程调containsKey时,B线程把值改了或者删掉了该key);而单线程HashMap却可以用containsKey(key)判断是否包含了这个key
迭代器是弱一致性的,在遍历过程中,内部元素可能发生变化,如果发生变化在已遍历部分,迭代器就不会反映出来,如果发生在未遍历过的部分,迭代器就会发现并反应出来,这就是弱一致性。这样迭代器可以使用原来的老数据,而写线程也可以并发的完成改变,保证了多个线程并发执行的连续性和扩展性,是性能提升的关键
JDK1.7
- ConcurrentHashMap 是通过数组 + 链表实现,由 Segment 数组和 Segment 元素里对应多个 HashEntry 组成
- value 和链表都是 volatile 修饰,保证可见性
- ConcurrentHashMap 采用了分段锁技术,分段指的就是 Segment 数组,其中 Segment 继承于 ReentrantLock
- 理论上 ConcurrentHashMap 支持 CurrencyLevel (Segment 数组数量)的线程并发,每当一个线程占用锁访问一个 Segment 时,不会影响到其他的 Segment
通过segment数组和hashEntry构成,ConcurrentHashMsp把Hash桶数组切分成小数组(segment),每个小数组有n个HashEntry组成
将数据分为一段一段的存储,然后给每一段数据配一把锁,当一个线程占用锁访问其中一端数据时,其它段数据也能被其他线程访问,实现并发访问
Segment是ConcurrentHashMap的一个内部类主要的组成,继承ReentrantLock,volatile修饰HashEntry<K,V>[] table可以保证在数组扩容时的可见性
volatile修饰HashEntry的数据value和下一个节点next,保证了多线程环境下数据获取时的可见性
JDK1.8
和HashMap的结构一样,采用Node数组+链表+红黑树
在锁的实现上,抛弃了原有的segment分段锁,采用CAS+synchronized实现更加细粒度的锁,将锁的级别控制在更加细粒度的哈希桶数组元素级别,只需要锁住这个链表头节点(/红黑树根节点),就不会影响其他的数组元素的读写
- 抛弃了原有的 Segment 分段锁,采用了 CAS + synchronized 来保证并发安全性
- HashEntry 改为 Node,作用相同
- val next 都用了 volatile 修饰
put 方法逻辑:
- 根据 key 计算出 hash 值
- 判断是否需要进行初始化
- 根据 key 定位出的 Node,如果为空表示当前位置可以写入数据,利用 CAS 尝试写入,失败则自旋
- 如果当前位置的 hashcode == MOVED == -1,则需要扩容
- 如果都不满足,则利用 synchronized 锁写入数据
- 如果数量大于 TREEIFY_THRESHOLD 则转换为红黑树
get 方法逻辑:
(不需要加锁,有volatile修饰HashEntry的元素value和next指针)
- 根据计算出来的 hash 值寻址,如果在桶上直接返回值
- 如果是红黑树,按照树的方式获取值
- 如果是链表,按链表的方式遍历获取值
JDK 1.7 到 JDK 1.8 中的 ConcurrentHashMap 最大的改动:
-
链表上的 Node 超过 8 个改为红黑树,查询复杂度 O(logn)
-
ReentrantLock 显示锁改为 synchronized,说明 JDK 1.8 中 synchronized 锁性能赶上或超过 ReentrantLock
--参考:https://www.cnblogs.com/fsychen/p/9361858.html
锁粒度不同:JDK7对需要进行数据操作的Segment进行加锁,JDK8为头结点进行加锁
线程安全机制不同:JDK7采用segment分段锁机制加锁,JDK8采用CAS+synchronized保证线程安全
时间复杂度不同:JDK7遍历链表0(n),JDK8遍历红黑树0(logn)
底层数据结构不同:JDK7底层使用数组+链表,JDK8数组+链表+红黑树
为什么用synchronized替换了ReentrantLock
synchronized在JDK6做了优化,引入了轻量级锁、偏向锁、锁消除、自适应自旋等。
CAS+synchronized加锁方式,只对头结点进行加锁,锁粒度更细,如果重入锁也这样操作,就需要大量继承字ReentrantLock的对象,造成内存浪费
HashTable
此类实现一个哈希表,该哈希表将键映射到相应的值。任何非 null 对象都可以用作键或值
为了成功地在哈希表中存储和获取对象,用作键的对象必须实现 hashCode 方法和 equals 方法。
维护着一个运行于所有条目的双重链接列表
Hashtable是同步的。 如果不需要线程安全的实现,建议使用HashMap代替Hashtable 。 如果需要线程安全的并发实现,那么建议使用ConcurrentHashMap代替Hashtable 。
HashTable与HashMap的区别
- HashTable是线程安全且同步的,方法用synchronized修饰,HashMap不是
- HashTable继承于Dictionary,HashMap继承于AbstractMap
- HashTable的key和value不支持null,HashMap支持
- HashTable的初始长度是11,扩容时两倍+1,HashMap初始值是16,扩容是2倍
- HashTable在构造方法里初始化,HashMap在第一次put元素的时候初始化
- HashTable是key.hashCode()&0x7FFFFFFF再%数组长度算index,HashMap是key.hashCode()与高16位异或再(n-1)&hash得到index
LinkedHashmap
Map 接口的哈希表和链接列表实现,具有可预知的迭代顺序
此链接列表定义了迭代顺序,该迭代顺序通常就是将键插入到映射中的顺序(插入顺序)。
底层数据结构是数组+链表/红⿊树+双向链表
TreeMap
根据键的自然排序进行排序
自定义类可以自定义排序(和treeset相似),
- 实现comparable接口,重写comparetTo方法
- 定义比较器,实现comparator接口,重写compare方法
底层数据结构是红⿊树
排序
例子对象类
1 package map; 2 //对象类 3 public class Users implements Comparable<Users>{ 4 private String name; 5 private Integer age; 6 7 @Override 8 public int compareTo(Users o) { 9 // this-传入对象 顺序排列 10 int i=this.age-o.age; 11 return i==0?this.name.compareTo(o.name):i; 12 } 13 14 @Override 15 public String toString() { 16 return "Users{" + 17 "name='" + name + '\'' + 18 ", age=" + age + 19 '}'; 20 } 21 22 public Users() { 23 } 24 25 public Users(String name, Integer age) { 26 this.name = name; 27 this.age = age; 28 } 29 30 public String getName() { 31 return name; 32 } 33 34 public void setName(String name) { 35 this.name = name; 36 } 37 38 public Integer getAge() { 39 return age; 40 } 41 42 public void setAge(Integer age) { 43 this.age = age; 44 } 45 }
两种排序
package map; import java.util.Comparator; import java.util.Map.Entry; import java.util.TreeMap; //TreeMap两种排序方法,同treeset public class TreeMaps { public static void main(String[] args) { // test1(); new TreeMaps().test2(); } //此方法创建TreeMap private static TreeMap<Users,Object> creat(){ TreeMap<Users,Object> tre=new TreeMap<>(); tre.put(new Users("张",22),1); tre.put(new Users("李",12),"2号"); tre.put(new Users("王",26),"3号"); return tre; } //遍历方法-通过映射的set视图,k对象实现comparable,重写compareTo,为自定义自然排序 public static void test1(){ for (Entry<Users,Object> en:creat().entrySet() ) { System.out.println(en.getKey()+":"+en.getValue()); } } //自定义排序,通过自定义的比较器 private void test2(){ //传入比较器时,传的是实例化的比较器 TreeMap<Users,Object> ttt=new TreeMap<>(new Com() ); //复制映射关系到此映射中 ttt.putAll(creat()); System.out.println(ttt); } } //自定义比较器类 class Com implements Comparator<Users> { @Override public int compare(Users o1, Users o2) { int i = o1.getName().length() - o2.getName().length(); return i== 0 ? o1.getAge()-o2.getAge() : i; } }
作者: deity-night
出处: https://www.cnblogs.com/deity-night/
关于作者:码农
本文版权归作者和博客园共有,欢迎转载,但未经作者同意必须保留此段声明,且在文章页面明显位置给出, 原文链接 如有问题, 可邮件(***@163.com)咨询.
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· DeepSeek 开源周回顾「GitHub 热点速览」
· 物流快递公司核心技术能力-地址解析分单基础技术分享
· .NET 10首个预览版发布:重大改进与新特性概览!
· AI与.NET技术实操系列(二):开始使用ML.NET
· 单线程的Redis速度为什么快?