Map是Java中的接口,Map.Entry是Map中的一个内部接口; Map.Entry是Map声明的一个内部接口,此接口为泛型,定义为Entry<K,V>。它表示Map中的一个实体(一个key-value对)。接口中有getKey(),getValue方法。
遍历map的四种方法及Map.entry详解
java中Map及Map.Entry详解
得到HashMap的长度
HashMap 中元素索引的确定: idx = hashcode & (tab.length - 1)
深入理解 HashMap put 方法(JDK 8逐行剖析)
Java集合–TreeMap完全解析 java集合系列——Map之TreeMap介绍(九)
介绍java中Pair 【小家java】Java实用数据结构Pair、MutablePair、ImmutablePair详解(推荐apache的commons组件提供) java.util.TreeMap.tailMap()方法实例
面试28k职位,老乡面试官从HashCode到HashMap给我讲了一下午!「回家赶忙整理出1.6万字的面试材料」 Java8 HashMap源码分析 java8 HashMap源码 详细研读 Java 基础:hashCode方法 深入解析Java对象的hashCode和hashCode在HashMap的底层数据结构的应用 Java提高篇——equals()与hashCode()方法详解
全网把Map中的hash()分析的最透彻的文章,别无二家。
HashMap 的设计就是为了在元素的查找上获得最优的性能(get()时时间复杂度达到O(1)),因此 HashMap 主要使用了数组作为核心的数据存储结构,同时使用链地址法来解决K-V映射时产生的碰撞,同时在 Java 8 中引入红黑树来进一步优化哈希碰撞时的数据查找性能; HashMap 通过 hash() 方法确定元素在哈希数组中的下标,并将元素放入数组对应下标处指向的链表中(尾插法),当元素个数超过 8 时,会将链表转换为红黑树;当元素个数小于6时,会将红黑树转换为链表;
/** * The table, initialized on first use, and resized as * necessary. When allocated, length is always a power of two. * (We also tolerate length zero in some operations to allow * bootstrapping mechanics that are currently not needed.) * */ transient Node<K,V>[] table;Java8 HashMap源码分析 面试:HashMap 夺命二十一问!鸡哥都扛不住~ 问题1
请你解释HashMap的容量为什么是2的n次幂? Java集合必会14问(精选面试题整理)面试官:为什么数组长度要保证为2的幂次方呢?
从原理上来说,HashMap 为了存取高效,要尽量减少碰撞,就是要尽量把数据均匀分配,使得每个链表上分配的数据大致相同。在 HashMap 中这个实现就是取模,即 hash % tab.length,计算机中取模效率比不上位移运算,所以在源码的实现上采用了位移运算,即 hash & (tab.length - 1),而 hash & (tab.length - 1) == hash % tab.length 的前提就是哈希数组的长度必须是 2 的 n 次幂。
深入理解 HashMap put 方法(JDK 8逐行剖析) 面试28k职位,老乡面试官从HashCode到HashMap给我讲了一下午!「回家赶忙整理出1.6万字的面试材料」 1.2 插入流程和源码分析 HashMap面试必问的数据结构相关知识总结 7.HashMap中put方法的过程?
深入理解 HashMap put 方法(JDK 8逐行剖析) 5. 判断是否超过阀值,如果超过,则重新散列数组。 面试28k职位,老乡面试官从HashCode到HashMap给我讲了一下午!「回家赶忙整理出1.6万字的面试材料」 1.2 插入流程和源码分析
HashMap 扩容过程主要实现为 resize() 方法,初始化一个默认大小的 HashMap 时会调用该方法;同事,当 HashMap 中链表长度超过 8 但是哈希数组的长度小于 64 的时候,也会调用该方法进行哈希数组的重新散列;HashMap 的扩容过程主要如下:
判断老的容量是否大于 0: 如果老的容量大于0,并且老的容量大于容器的最大值,则设置阈值为容器的最大值并返回老的哈希数组,哈希数组不再扩容;如果老的容量乘以2小于容器的最大容量,并且老的容量大于等于容器默认大小,则更新新的阈值为原阈值的两倍; 如果老的阈值大于 0,则新的容量等于老的阈值;注意:这里很重要!!!还记得我们之前使用new 操作符的时候,会设置阀值为 2 的幂次方,那么这里就用上了那个计算出来的数字,也就是说,就算我们设置的不是2的幂次方,HashMap 也会自动将你的容量设置为2的幂次方。如果老的阈值和容量都不大于0,则是一个新的数组,设置新的容量为默认初始容量 16,新的阈值为 16 * 0.75 = 12如果新的阈值是0,则重新计算新的阈值,使用新的容量乘以负载因子得到新的阈值,然后判断新的阈值是否合法(新的阈值和新的容量都小于容器的最大值):如果新的阈值合法则使用该阈值;否则设置新的阈值为容器的最大值;将新阈值赋值给当前 HashMap 对象的阈值;创建新的Node数组,大小为新的容量(新的容量要么为旧的容量,要么为旧的容量*2,要么为 16),并将新的 Node 数组赋值给当前 HashMap 对象的数组;如果老的数组不为空,则将老数组中的值重新散列到新数组中;如果老的数组为空,则直接返回新的数组;深入理解 HashMap put 方法(JDK 8逐行剖析) 5. 判断是否超过阀值,如果超过,则重新散列数组。 面试28k职位,老乡面试官从HashCode到HashMap给我讲了一下午!「回家赶忙整理出1.6万字的面试材料」 1.2 插入流程和源码分析
将老数组中的元素重哈希到新数组的过程如下:
循环遍历老的数组,下标从 0 开始,并判断对应下标的值是否为空:如果对应下标的值不为空,则判断对应下标处的 next 节点是否为空,也就是判断对应下标的数组元素是否为链表:如果不是链表,说明该下标处实际只有一个元素,则重新将该值散列(hash & (newTab.length - 1))到新数组中;判断对应下标处是否为树的节点,如果该节点是树,则调用红黑树的 split() 方法,传入当前对象、新的数组、当前下标、旧的容量,split() 方法会重新计算红黑树中每个节点的 hash 值,如果重新计算后,树中元素的哈希值与原值不同,则重新散列到不同的下标中,达到拆分红黑树的目的,提高性能;如果既不是树,next 节点也不为空,则是链表,将旧链表中的元素重新计算哈希值,如果得到的哈希值和之前的不同,则将该元素从链表中拆出,重新放到对应的下标关于链表元素重哈希的过程为:如果元素哈希值 & 旧的数组容量为 0,那么该元素下标不变,直接尾插法插入新数组;如果元素哈希值 & 旧的数组容量不为 0,那么该元素下标为原下标+旧数组的长度在 HashMap 中,当两个对象的 hashcode 相同时,这两个对象在哈希数组中的下标相同,会发生"哈希碰撞";当发生哈希碰撞时,这两个对象会被存储到数组下标对应的链表中。 HashMap面试必问的数据结构相关知识总结 3.当两个对象的 hashCode 相同会发生什么?
hashCode() 方法的返回值是 int 类型,int 类型的取值范围为 -2^31 ~ 2^31 - 1,而 HashMap 的哈希数组的大小范围在 16~1^30; 很明显,hashcode() 方法生成的哈希值是大于 HashMap 的哈希数组的有效空间的,也就是说,通过 hashcode() 方法生成的哈希值可能落不到哈希数组的有效空间上,因此 HashMap 没有直接使用 hashcode() 的哈希值作为哈希数组的下标
HashMap自己实现了自己的hash()方法,通过两次扰动使得它自己的哈希值高低位自行进行异或运算,降低哈希碰撞概率也使得数据分布更平均; 在保证数组长度为2的幂次方的时候,使用hash()运算之后的值与运算(&)(数组长度 - 1)来获取数组下标的方式进行存储,这样一来是比取余操作更加有效率,二来也是因为只有当数组长度为2的幂次方时,h&(length-1)才等价于h%length,三来解决了“哈希值与数组大小范围不匹配”的问题;
JDK 1.8 中是通过 hashCode() 的高 16 位异或低 16 位实现的。
保证了对象的 hashCode 的 32 位值只要有一位发生改变,整个 hash() 返回值就会改变。尽可能的减少碰撞。
①、table 数组大小是由 capacity 这个参数确定的,默认是16,也可以构造时传入,最大限制是1<<30; ②、loadFactor 是装载因子,主要目的是用来确认table 数组是否需要动态扩展,默认值是0.75,比如table 数组大小为 16,装载因子为 0.75 时,threshold 就是12,当 table 的实际大小超过 12 时,table就需要动态扩容; ③、扩容时,调用 resize() 方法,将 table 长度变为原来的两倍(注意是 table 长度,而不是 threshold) ④、如果数据很大的情况下,扩展时将会带来性能的损失,在性能要求很高的地方,这种损失很可能很致命。
HashMap 是线程不安全的,而Hashtable 是线程安全的; HashMap 是 1.2 版本开始加入 JDK 的,是线程不安全的 Map 接口的实现类,具体体现为 HashMap 中的所有方法都没有使用 synchronized 进行修饰; Hashtable 是 1.0 版本开始加入 JDK 的,是线程安全的 Map 接口的实现类,具体体现为 Hashtable 中的所有方法都有使用 synchronized 进行修饰;
由于线程安全,所以 HashTable 的效率比不上 HashMap; 由于线程安全,Hashtable 中的所有方法都有使用 synchronized 进行修饰,而 HashMap 中的所有方法都没有使用 synchronized 进行修饰;synchronized 虽然可以保证线程的安全性,但是其性能消耗较大,因此,Hashtable 相对 HashMap 来说性能更差;
HashMap 默认初始话大小为16,而 Hashtable 默认初始化大小为 11; HashMap 默认初始化大小如下:
/** * The default initial capacity - MUST be a power of two. */ static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16Hashtable 默认初始化大小如下:
/** * Constructs a new, empty hashtable with a default initial capacity (11) * and load factor (0.75). */ public Hashtable() { this(11, 0.75f); }HashMap 扩容时,resize 为原大小的 2 倍;Hashtable 扩容时,rehash 为原大小的 2 倍+1; HashMap 扩若时大小如下:
/** * Initializes or doubles table size. If null, allocates in * accord with initial capacity target held in field threshold. * Otherwise, because we are using power-of-two expansion, the * elements from each bin must either stay at same index, or move * with a power of two offset in the new table. * * @return the table */ 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) { if (oldCap >= MAXIMUM_CAPACITY) { threshold = Integer.MAX_VALUE; return oldTab; } // HashMap 扩容时,扩容为原大小的 2 倍 else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY && oldCap >= DEFAULT_INITIAL_CAPACITY) newThr = oldThr << 1; // double threshold } else if (oldThr > 0) // initial capacity was placed in threshold newCap = oldThr; else { // zero initial threshold signifies using defaults newCap = DEFAULT_INITIAL_CAPACITY; newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY); } // ...... // ...... // ...... return newTab; }Hashtable 扩若时大小如下:
/** * Increases the capacity of and internally reorganizes this * hashtable, in order to accommodate and access its entries more * efficiently. This method is called automatically when the * number of keys in the hashtable exceeds this hashtable's capacity * and load factor. */ @SuppressWarnings("unchecked") protected void rehash() { int oldCapacity = table.length; Entry<?,?>[] oldMap = table; // Hashtable 扩容时扩容为原大小的 2倍+1 // overflow-conscious code int newCapacity = (oldCapacity << 1) + 1; if (newCapacity - MAX_ARRAY_SIZE > 0) { if (oldCapacity == MAX_ARRAY_SIZE) // Keep running with MAX_ARRAY_SIZE buckets return; newCapacity = MAX_ARRAY_SIZE; } Entry<?,?>[] newMap = new Entry<?,?>[newCapacity]; // ...... // ...... // ...... }在放入对象时,HashMap 会重新计算 hash 值,而 Hashtable 直接使用对象的 hashcode 作为 hash 值; HashMap 放入对象时 hashcode 的获取方式如下:
/** * Associates the specified value with the specified key in this map. * If the map previously contained a mapping for the key, the old * value is replaced. * * @param key key with which the specified value is to be associated * @param value value to be associated with the specified key * @return the previous value associated with <tt>key</tt>, or * <tt>null</tt> if there was no mapping for <tt>key</tt>. * (A <tt>null</tt> return can also indicate that the map * previously associated <tt>null</tt> with <tt>key</tt>.) */ public V put(K key, V value) { // 在放入对象时,HashMap 会重新计算 hash 值 return putVal(hash(key), key, value, false, true); }Hashtable 放入对象时 hashcode 的获取方式如下:
private void addEntry(int hash, K key, V value, int index) { modCount++; Entry<?,?> tab[] = table; if (count >= threshold) { // Rehash the table if the threshold is exceeded rehash(); // 在放入对象时,Hashtable 直接使用对象的 hashcode 作为 hash 值 tab = table; hash = key.hashCode(); index = (hash & 0x7FFFFFFF) % tab.length; } // Creates the new entry. @SuppressWarnings("unchecked") Entry<K,V> e = (Entry<K,V>) tab[index]; tab[index] = new Entry<>(hash, key, value, e); count++; }HashMap最多只允许一条记录的键为null,允许多条记录的值为null,而 Hashtable不允许键-值为空; 放入对象时,HashMap 的键-值要求如下:
/** * Associates the specified value with the specified key in this map. * If the map previously contained a mapping for the key, the old * value is replaced. * * 放入对象时,HashMap 的键-值都可为空 * * @param key key with which the specified value is to be associated * @param value value to be associated with the specified key * @return the previous value associated with <tt>key</tt>, or * <tt>null</tt> if there was no mapping for <tt>key</tt>. * (A <tt>null</tt> return can also indicate that the map * previously associated <tt>null</tt> with <tt>key</tt>.) */ public V put(K key, V value) { return putVal(hash(key), key, value, false, true); }放入对象时,Hashtable 的键-值要求如下:
/** * Maps the specified <code>key</code> to the specified * <code>value</code> in this hashtable. Neither the key nor the * value can be <code>null</code>. <p> * * 放入对象时,Hashtable 的键-值都不能为空 * * The value can be retrieved by calling the <code>get</code> method * with a key that is equal to the original key. * * @param key the hashtable key * @param value the value * @return the previous value of the specified key in this hashtable, * or <code>null</code> if it did not have one * @exception NullPointerException if the key or value is * <code>null</code> * @see Object#equals(Object) * @see #get(Object) */ public synchronized V put(K key, V value) {}面试:HashMap 夺命二十一问!鸡哥都扛不住~ 问题14
HashMap面试必问的数据结构相关知识总结 问题16
HashMap 参考其他问题; LinkedHashMap 保存了记录的插入顺序,在用 Iterator 遍历时,先取到的记录肯定是先插入的;遍历比 HashMap 慢; TreeMap 实现 SortMap 接口,能够把它保存的记录根据键排序(默认按键值升序排序,也可以指定排序的比较器) HashMap面试必问的数据结构相关知识总结 问题12
一般情况下,使用最多的是 HashMap。 HashMap:在 Map 中插入、删除和定位元素时; TreeMap:在需要按自然顺序或自定义顺序遍历键的情况下; LinkedHashMap:在需要输出的顺序和输入的顺序相同的情况下。 HashMap面试必问的数据结构相关知识总结 问题13
HashMap 是线程不安全的,而 ConcurrentHashMap 是线程安全的; HashMap 中没有使用任何加锁手段保证线程安全性,而 ConcurrentHashMap 在 JDK1.7 中主要使用分段锁进行加锁,在 JDK1.8 中主要使用 CAS(比较交换) + synchronized 进行加锁; ConcurrentHashMap 在 JDK1.8 中使用 CAS + synchronized 加锁的源码如下:
/** * Maps the specified key to the specified value in this table. * Neither the key nor the value can be null. * * <p>The value can be retrieved by calling the {@code get} method * with a key that is equal to the original key. * * @param key key with which the specified value is to be associated * @param value value to be associated with the specified key * @return the previous value associated with {@code key}, or * {@code null} if there was no mapping for {@code key} * @throws NullPointerException if the specified key or value is null */ 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) { if (key == null || value == null) throw new NullPointerException(); int hash = spread(key.hashCode()); int binCount = 0; for (Node<K,V>[] tab = table;;) { Node<K,V> f; int n, i, fh; if (tab == null || (n = tab.length) == 0) tab = initTable(); else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) { // ConcurrentHashMap 中使用 CAS 保证线程安全性的场景 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; // ConcurrentHashMap 中使用 synchronized 关键字进行加锁的场景 synchronized (f) { if (tabAt(tab, i) == f) { if (fh >= 0) { binCount = 1; for (Node<K,V> e = f;; ++binCount) { K ek; 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) { if (binCount >= TREEIFY_THRESHOLD) treeifyBin(tab, i); if (oldVal != null) return oldVal; break; } } } addCount(1L, binCount); return null; }放入对象时,HashMap 中键-值都可为空,而 ConcurrentHashMap 中键-值都不能为空; 放入对象时,HashMap 中键-值都可为空的源码如下:
/** * Associates the specified value with the specified key in this map. * If the map previously contained a mapping for the key, the old * value is replaced. * * HashMap 中的键-值都可为空 * * @param key key with which the specified value is to be associated * @param value value to be associated with the specified key * @return the previous value associated with <tt>key</tt>, or * <tt>null</tt> if there was no mapping for <tt>key</tt>. * (A <tt>null</tt> return can also indicate that the map * previously associated <tt>null</tt> with <tt>key</tt>.) */ public V put(K key, V value) { return putVal(hash(key), key, value, false, true); }放入对象时,ConcurrentHashMap 中键-值都不可为空的源码如下:
/** * Maps the specified key to the specified value in this table. * Neither the key nor the value can be null. * * ConcurrentHashMap 中键-值都不能为空 * * <p>The value can be retrieved by calling the {@code get} method * with a key that is equal to the original key. * * @param key key with which the specified value is to be associated * @param value value to be associated with the specified key * @return the previous value associated with {@code key}, or * {@code null} if there was no mapping for {@code key} * @throws NullPointerException if the specified key or value is null */ public V put(K key, V value) { return putVal(key, value, false); }HashMap面试必问的数据结构相关知识总结 问题15 Java 中另一个与 HashMap 极其相似并且线程安全的类是 ConcurrentHashMap 类。 ConcurrentHashMap 与 Hashtable 的不同主要体现在加锁的方式上: Hashtable 是使用 synchronized 关键字进行加锁,性能较差; 而 ConcurrentHashMap 在 JDK 1.7 中采用的是分段锁,在 JDK 1.8 中采用的是CAS(比较交换)+ synchronized。
HashMap面试必问的数据结构相关知识总结 问题15 HashTable 使用一把锁(锁住整个链表结构)处理并发问题,多个线程竞争一把锁,容易阻塞; ConcurrentHashMap JDK 1.7 中使用分段锁(ReentrantLock + Segment + HashEntry),相当于把一个 HashMap 分成多个段,每段分配一把锁,这样支持多线程访问。锁粒度:基于 Segment,包含多个 HashEntry。 JDK 1.8 中使用 CAS + synchronized + Node + 红黑树。锁粒度:Node(首结点)(实现 Map.Entry<K,V>)。锁粒度降低了。