当前位置: 首页 > news >正文

【面试长文】HashMap的数据结构和底层原理以及在JDK1.6、1.7和JDK8中的演变差异

文章目录

  • HashMap的数据结构和底层原理以及在JDK1.6、1.7和JDK8中的演变差异
  • HashMap的数据结构和原理
  • JDK1.6、1.7和1.8中的HashMap源码演变
    • JDK1.6
    • JDK1.7
    • JDK1.8
  • 总结
  • 自己实现一个简单的HashMap
  • HashMap的时间复杂度分析
  • HashMap的空间复杂度分析
  • HashMap的应用场景
  • HashMap的弊端及解决方案
  • HashMap与HashSet的关系
  • HashMap与 Hashtable的区别
  • HashMap的长度为什么是2的幂次方
  • JDK1.8对HashMap的优化
  • HashMap的遍历方式
  • HashMap的常见面试题

HashMap的数据结构和底层原理以及在JDK1.6、1.7和JDK8中的演变差异

这里是一篇关于HashMap的数据结构、底层原理和代码演变的技术博客:

HashMap的数据结构和原理

HashMap的数据结构采用“链表散列”结构,即一个链表和一个数组,数组称为hash table,链表成为链表数组。HashMap通过key的hashCode来计算index,然后将key-value对存放在hash table的对应位置。如果出现hash冲突,就将数据存放在链表中。HashMap主要由Node[] table、size和loadFactor三个字段组成。

  • Node[] table:数组,默认大小为16,扩容时大小一般为原来的2倍。
  • size:HashMap中键值对的数量。
  • loadFactor:负载因子,默认为0.75。扩容条件是size大于loadFactor和table的长度的乘积。

获取数据的步骤:

  1. 计算key的hashCode,然后通过hashCode映射到table数组的index。

  2. 如果table的index位置没有数据,直接添加。

  3. 如果table的index位置有数据,查看key是equals()。如果equals,则覆盖旧值;如果不equals,进行加链表或树化操作。如果链表长度超过8,则转换为红黑树。

  4. 添加后,检查是否需要扩容。如果size超过loadFactor*table.length,进行扩容操作。扩容操作是创建一个新的table,新table的大小为旧table的2倍。

JDK1.6、1.7和1.8中的HashMap源码演变

JDK1.6

JDK1.6中的HashMap是非线程安全的,扩容使用synchronized来锁定整个table。

public V put(K key, V value) {// 扩容if (table == EMPTY_TABLE) {inflateTable(threshold);} else if (size >= threshold) {resize(2 * table.length);}// 如果key为null,存储在table[0]中if (key == null) {return putForNullKey(value); }  // 计算key的hash值int hash = hash(key);int i = indexFor(hash, table.length);// 进行拉链操作for (Entry<K,V> e = table[i]; e != null; e = e.next) { Object k;// 如果key存在,则覆盖旧值 if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {  V oldValue = e.value;e.value = value; return oldValue; }}// 添加新EntrymodCount++;addEntry(hash, key, value, i); return null; 
}

JDK1.7

JDK1.7中的HashMap是非线程安全的,采用“链表+红黑树”的结构。当链表长度超过8时会将链表转换为红黑树。扩容时使用synchronized锁定table[0]和table[table.length-1]。

public V put(K key, V value) {// 扩容if (table == EMPTY_TABLE)inflateTable(threshold);// 如果key为null,存储在table[0]中 if (key == null) return putForNullKey(value);int hash = hash(key);// 找到对应的桶int i = indexFor(hash, table.length);  // 先找出是否已经存在该key,如果存在则更新值并返回 for (Entry<K,V> e = table[i]; e != null; e = e.next) { if (e.hash == hash && eq(key, e.key)) { V oldValue = e.value; e.value = value; return oldValue; }}  // 如果不存在,添加到链表尾部或树中 modCount++;addEntry(hash, key, value, i);  // 确定是否需要扩容if (size >= threshold)resize(2 * table.length);   return null; 
}

JDK1.8

JDK1.8中HashMap是非线程安全的,默认采用"链表+红黑树"的结构,当链表长度超过8时会将链表转换为红黑树。

JDK1.8对扩容做了优化,不再锁定整个table,而是采用"synchronized+CAS"的方式来控制并发度。同时引入了红黑树,减少了链表过长的情况。

final V putVal(K key, V value, boolean onlyIfAbsent) {  // 或获取 key 的 hash 值  int hash = spread(key.hashCode());  // 找到对应桶的索引  int i = indexFor(hash, table.length);  // 链表或红黑树的节点  Node<K,V> p;  boolean newEntry = true;// 如果桶中第一个节点为 TreeNode,则为红黑树结构  if ((p = tab[i]) == null)  // CAS 控制并发,初始化首节点  tab[i] = newNode(hash, key, value, null);  else {  K k;  //  如果键 hash 值和节点 hash 值相等,并且键 equals 节点键,则更新节点键值对  if (p.hash == hash && ((k = p.key) == key || (key != null && key.equals(k))))    // 更新节点值  p.val = value; else {  // 判断是否是红黑树  if (p instanceof TreeNode)  // 调用红黑树插入方法插入新节点  ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);  else {  // 若为链表,则插入链表尾部  for (int binCount = 0; ; ++binCount) {  // 插入新节点到链表尾部  if (newEntry && binCount >= TREEIFY_THRESHOLD - 1)  // 链表长度达 TREEIFY_THRESHOLD 则转化为红黑树treeifyBin(tab, hash);  // 遍历至链表尾部,插入新节点  if (p.next == null) {  p.next = newNode(hash, key, value, null);  break;  }  p = p.next;  }  }  }  }  // 判断是否需要扩容  if (++size > threshold)  // 将容量扩大两倍  resize();  return onlyIfAbsent ? oldValue : null;  
}

总结

通过HashMap的代码演变,我们可以看出:

  1. JDK1.8引入了红黑树,降低了链表过长的可能性,提高了效率。
  2. JDK1.8使用synchronized+CAS控制并发扩容,避免锁定整个table,提高了并发度。
  3. JDK1.8对null key做了优化,null键值对存储在table[0]位置。
  4. 三个版本的HashMap都采用链表散列结构,通过key的hashCode映射到table,如果hash冲突就采用链表存储。
  5. 扩容的阈值loadFactor在三个版本都是0.75,阈值size和table大小的乘积超过loadFactor时触发扩容。

自己实现一个简单的HashMap

  1. 定义Node节点,存储键值对和下一个节点的引用。
static class Node {int key;int value;Node next;public Node(int key, int value) {this.key = key;this.value = value;}
}
  1. 初始化HashMap,定义数组table的初始大小和加载因子loadFactor。
private static int CAPACITY = 16;
private static float LOAD_FACTOR = 0.75f;
private Node[] table;
private int size = 0;
  1. 计算key的hash值,然后通过hash值&(table.length - 1)确定其在数组中的位置。
private int hash(int key) {return key & (table.length - 1); 
}
  1. 添加元素,先确定索引位置,如果不存在 hash 冲突则直接插入。如果存在 hash 冲突,则采用拉链法解决冲突。
 
public void put(int key, int value) {if (size >= LOAD_FACTOR * table.length) {// 进行扩容}int hash = hash(key);Node node = table[hash];if (node == null) {// 插入新节点,无hash冲突table[hash] = new Node(key, value);size++;} else {// 有hash冲突,采用拉链法解决Node pre = null; while (node != null) {if (node.key == key) {node.value = value;return;}  pre = node;node = node.next;}// 插入新节点到链表尾部pre.next = new Node(key, value);size++;}
}
  1. 扩容机制,新table的大小为原来的2倍,并重新计算每个键值对的位置,迁移数据。
private void resize() {Node[] newTable = new Node[table.length * 2];for (Node node : table) {if (node != null) {Node next = null;while (node != null) { next = node.next;  int hash = hash(node.key);if (newTable[hash] == null) {    newTable[hash] = node;    } else {  Node n = newTable[hash];    while (n.next != null) n = n.next;    n.next = node;    }      node = next;  }  }  }table = newTable;  
} 

这就是一个简单的HashMap的实现,通过数组+链表实现“拉链法”解决哈希冲突。当链表长度过长时进行扩容,采用重新计算位置的方式迁移节点。

HashMap的时间复杂度分析

我们来分析一下HashMap的时间复杂度:

  1. 查找:
    • 平均情况下,查找的时间复杂度为O(1)。因为我们可以通过hash值直接定位到数据所在的bucket,然后再通过key的equals方法判断是否命中。
    • 最坏情况下,如果HashMap中所有键值对所hash得到的index都是同一个,此时查找的时间复杂度为O(n),需要遍历整个链表。但这种情况的概率很小,可以忽略。
  2. 添加:
    • 平均情况下,添加的时间复杂度也为O(1)。和查找一样,可以通过hash值直接定位到数据所在的bucket,然后再插入。
    • 最坏情况下,如果HashMap中所有键值对所hash得到的index都是同一个,此时添加的时间复杂度为O(n),需要遍历整个链表。但这种情况的概率很小,可以忽略。
  3. 删除:
    • 平均情况下,删除的时间复杂度为O(1)。可以通过hash值直接定位到数据所在的bucket,然后再进行删除。
    • 最坏情况下,如果要删除的键值对在链表的尾部,需要遍历整个链表,时间复杂度为O(n)。但这种情况概率也很小,可以忽略。
  4. 扩容:
    • 扩容的时间复杂度为O(n),需要重新计算每个键值对的位置,然后迁移数据。但扩容操作很少发生,一般来说可以忽略扩容对时间复杂度的影响。

所以,总体来说,HashMap的时间复杂度可以看作O(1)。除非数据分布极不均匀,导致链表过长或频繁扩容,但这在实际使用中很少出现。HashMap之所以能达到O(1)的时间复杂度,主要得益于它采用的“拉链法”来解决哈希冲突。相比直接在数组中查找,拉链法大大减少了查找的时间复杂度。

HashMap的空间复杂度分析

空间复杂度主要取决于 HashMap 的容量(table的大小)和键值对的数量。

  1. 容量:HashMap的默认容量为16,之后每次扩容的时候容量都会翻倍。所以如果一共扩容了n次,HashMap的容量大小为16*(2^n)。
  2. 键值对数量:HashMap最大可以存储2^32个键值对。

所以:

  • 如果键值对数量远远小于2^32,则空间复杂度可以看作O(n),n为扩容次数。
  • 如果键值对数量接近上限232,则空间复杂度可以看作O(232)。

但由于扩容操作消耗性能,也会浪费一定的空间(相当于目前使用了容量的1/2),所以我们应该在初始化HashMap的时候就指定一个合适的容量,避免过多的扩容操作。

如果我们指定HashMap的初始容量,那么空间复杂度可以简单的分析如下:

  • 指定初始容量为n,键值对数量为m,如果m<<n,则空间复杂度为O(n)。
  • 如果m>n,又进行了k次扩容,则空间复杂度为O(n + 2^k)。
  • 如果一共扩容了32次,达到上限,则空间复杂度为O(2^32)。

所以我们可以得出结论:

  • 当键值对数量远小于初始容量时,空间复杂度可看作O(初始容量)。
  • 当键值对数量接近HashMap的上限时,空间复杂度为O(2^32)。
  • 其余情况下,空间复杂度介于O(初始容量)和O(2^32)之间。

因此,我们在初始化HashMap的时候,应该设置一个合适的初始容量,既不会造成过多扩容,也不会有太大的空间浪费,这需要根据实际场景来判断。

HashMap的应用场景

HashMap是Java中最常用的Map之一,它有以下主要应用场景:

  1. 缓存:HashMap是实现缓存机制的理想选择,因为它的时间复杂度为O(1),可以快速存取缓存数据。
  2. 大数据量的键值存储:HashMap是基于哈希表实现的,可以快速地完成大量键值对的存储和查找。
  3. 数据去重:我们可以把所有数据放入HashMap,然后判断put操作的返回值是否为null来判断该键值对是否已经存在,实现数据去重的功能。
  4. 数据搜索:我们可以把所有数据categorize到不同的HashMap中,这样可以通过键值快速搜索到所需的数据。例如,在leetcode第一次出现的问题可以存在一个HashMap,出现频率最高的10道题可以存在一个HashMap。这样我们就可以快速搜索到这两个信息。
  5. 保存在数据库中查重:在示例入库之前,可以先将数据放入HashMap,然后判断HashMap中是否已经存在该数据,如果存在则不入库,这样可以避免数据库中出现重复数据。
  6. 实现LRU缓存机制:我们可以使用HashMap与双向链表相结合的方式来实现LRU缓存机制。HashMap用来快速存取键值对,双向链表保证最近使用的节点靠近头部,这样我们就可以快速找到最近最少使用的节点进行删除。
class LRUCache {class Node {int key;int value;Node prev;Node next;}private HashMap<Integer, Node> map;private Node head;private Node tail;private int capacity;public LRUCache(int capacity) {this.capacity = capacity;map = new HashMap<>();}public int get(int key) {if (map.containsKey(key)) {// 如果key存在,先从链表中删除该节点Node node = map.get(key);removeFromList(node);// 然后重新插到头部addToHead(node);return node.value;} else {return -1;}}public void put(int key, int value) {if (map.containsKey(key)) {// 如果key已存在,先从链表中删除该节点Node node = map.get(key);removeFromList(node);} // 将新节点添加到头部Node node = new Node(key, value);addToHead(node);map.put(key, node);// 如果容量已满,删除链表尾部节点if (map.size() > capacity) {Node tail = removeTail();map.remove(tail.key);}}
}

这就是使用HashMap和双向链表实现的LRU缓存机制。我们利用HashMap的O(1)时间复杂度快速存取键值对,利用双向链表保证最近使用的数据靠近头部,这样在容量满的时候可以快速删除最近最少使用的节点。

HashMap的弊端及解决方案

HashMap也有一些弊端,主要包括:

  1. 线程不安全:HashMap是非线程安全的,在多线程环境下使用时需要外部同步机制来保证线程安全,这会影响性能。解决方案:可以使用ConcurrentHashMap,它是HashMap的线程安全版本,内部采用了锁分段技术来实现高度并发下的线程安全。
  2. 键值对顺序不保证:HashMap的键值对顺序是由hash值决定的,所以遍历HashMap时的顺序是不确定的。如果需要保证顺序,HashMap不太适用。解决方案:可以使用TreeMap,它是基于红黑树实现的,可以保证键值对的顺序。也可以使用LinkedHashMap,它是HashMap的子类,可以保证键值对的插入顺序。
  3. hash冲突:由于hash函数的局限性,不同的键可能会哈希到同一个位置,这种情况称为hash冲突。HashMap采用拉链法解决hash冲突,如果超出一定长度就会转化为红黑树,这样会消耗一定的时间和空间。解决方案:可以选择一个良好的hash函数来降低hash冲突的概率。也可以初始化HashMap的时候指定一个合理的容量,避免在容量过小的情况下就产生过多hash冲突。
  4. 空间占用可能很高:如果HashMap中的键值对数量远小于初始容量,就会造成空间的浪费。如果键值对数量不断增加,HashMap的空间占用会急剧上升(每次扩容为原来的2倍)。解决方案:初始化HashMap的时候指定一个合理的容量,既不会造成空间的浪费,也不会由于频繁扩容导致空间占用急剧上升。也可以使用ConcurrentHashMap,它的扩容机制可以避免空间的浪费。
  5. 键为null的问题:如果KeyValue中键为null,那么它会被存储在table[0]的bucket中,这会影响get和put的性能。解决方案:可以选择不允许null作为键,或者使用ConcurrentHashMap,它对null键进行了优化,不会影响性能。

综上,ConcurrentHashMap可以很好的解决HashMap的上述弊端,所以在高并发环境下,ConcurrentHashMap通常被用来替代HashMap。但当键值对数量较小,并发度不高的场景下,HashMap也是一种非常高效的数据结构。

HashMap与HashSet的关系

HashSet底层实际上是基于HashMap实现的。它使用HashMap的key来存储元素,value使用默认对象PRESENT。HashSet的主要方法底层实际上是调用的HashMap的方法。例如:

  • add(E e) -> put(e, PRESENT)

  • remove(E e) -> remove(e)

  • contains(E e) -> containsKey(e)

  • size() -> size()

  • isEmpty() -> isEmpty()

  • iterator() -> keySet().iterator()

所以我们可以总结:

  • HashSet底层使用HashMap来实现。

  • HashSet中的元素实际上被当作HashMap的键。

  • HashSet不允许存储重复元素,是由于HashMap的键也不允许重复导致的。

  • HashSet的迭代顺序取决于HashMap的键迭代顺序,这是不确定的。

  • HashSet不保证元素的顺序,这也是由HashMap决定的。

下面通过一个示例体现他们的关系:

HashSet<String> set = new HashSet<>();
set.add("a");
set.add("b");
set.add("c");HashMap<String, Object> map = (HashMap<String, Object>) set;
// map是 {a=PRESENT, b=PRESENT, c=PRESENT}  // set和map的方法调用是相同的
set.remove("a");  
map.remove("a");set.contains("b"); 
map.containsKey("b");set.size();       
map.size();  

从此示例可以很清楚的看出,HashSet仅仅是在HashMap上做了一层包装,它使用HashMap的键来存储元素,value使用默认的PRESENT对象。
所以每当我们调用HashSet的方法时,实际上都是在调用HashMap对应的方法,二者之间的关系十分密切。理解了HashSet和HashMap的关系,也就很容易理解HashSet的实现原理和工作机制了。所以这是一个比较重要的知识点,值得我们深入学习和理解。

HashMap与 Hashtable的区别

HashMap和Hashtable都是基于哈希表实现的Map,但是在实现上有一定的区别。主要区别如下:

  1. 线程安全性:HashMap是非线程安全的,Hashtable是线程安全的。Hashtable内部的方法基本都经过synchronized修饰,达到了线程安全。
  2. Null键和值:HashMap允许null键和null值,Hashtable不允许任何null键和null值。
  3. 容量和扩容:HashMap的默认初始容量是16,扩容时容量翻倍。Hashtable的默认初始容量是11,之后每次扩容时容量翻倍加1。
  4. 扩容方式:HashMap的扩容方式是在旧表中取值,Hashtable的扩容方式是创建一个新表,将旧表的值拷贝到新表中。HashMap的扩容方式更加高效。
  5. 迭代器:HashMap的迭代器是fail-fast的,而Hashtable的迭代器是fail-safe的。所以在遍历的时候,Hashtable更加适合高并发的场景。
  6. 底层数据结构:JDK1.8以前,HashMap和Hashtable的底层都采用数组+链表实现。JDK1.8后,HashMap采用了数组+链表+红黑树实现,Hashtable依然保持数组+链表。所以HashMap的性能优于Hashtable。

除了以上区别外,HashMap和Hashtable的方法使用方式基本相同,功能也基本相同。所以除非对线程安全有较高要求,否则更推荐使用HashMap来代替Hashtable。

下面通过一个示例来体现他们的一些区别:

// HashMap
HashMap<String, String> map = new HashMap<>();
map.put(null, null);
map.put("a", "a");
map.put(null, "b");// Hashtable 
Hashtable<String, String> table = new Hashtable<>();
table.put("a", "a");  // OK
table.put(null, null); // java.lang.NullPointerException
table.put(null, "b"); // java.lang.NullPointerException

从示例可以看出HashMap允许null键null值,而Hashtable则抛出异常。这就是二者在这一点的重要区别。

所以总体来说,除非对线程安全有较高要求,HashMap在大多数情况下都优于Hashtable。Hashtable相比来说耗时耗空间,体现不出现代计算机异构计算能力。

HashMap的长度为什么是2的幂次方

HashMap的长度为什么要取2的幂(一般初始化为2的4次方16),这是因为:

  1. hash算法的最后一步通常是对表长length取模运算(hash%length),如果length是2的幂,那么length-1的二进制码全部为1,这种情况下取模运算等价于与运算(hash&(length-1)),与运算的效率高于取模运算。所以将长度定为2的幂可以提高hash寻址的效率。
  2. 如果存在大量hash冲突,链表会变得较长。如果表长为2的幂,容易通过二进制码与运算定位到冲突的链表,否则需要取模运算,效率较低。
  3. 扩容时,如果新表长仍然是2的幂,原来的hash值仍然有效。只需要将原来的hash值对新表长取与运算,就可以直接定位到数据位置。否则所有的hash值都需要重新hash,效率较低。

举个例子,假设当前HashMap的容量为16(10000),扩容后容量变为32(100000)。

如果是2的幂,只需要将原本的hash值(如11011)与新的表长-1(11111)做与运算(11011)。得到新的位置(11011)。如果不是2的幂,需要重新hash。

比如原来位置是hash%16=11,新位置应该是hash%32=19。这需要重新计算hash值,效率较低。

所以总结来说,HashMap的长度取2的幂主要出于3个方面的考虑:

  1. hash寻址的效率。
  2. 2的幂的length-1全部为1,适合用与运算代替取模运算。解决hash冲突时,容易通过与运算定位到冲突链表。
  3. 扩容时,可以通过与运算重新定位,无需全表rehash。

这也就是HashMap容量为什么要选用2的幂的原因了。这在一定程度上也提高了HashMap的性能,值得我们学习和理解。

JDK1.8对HashMap的优化

在JDK1.8之前,HashMap采用数组+链表实现,数组是HashMap的主体,链表则是解决哈希冲突的手段。

但是当链表长度过长时,HashMap的性能会受到比较大的影响。JDK1.8对这一点进行了优化,引入了红黑树。

当链表长度太长(默认超过8)时,链表就转化为红黑树,利用红黑树快速增删改查的特点提高HashMap的性能。

所以JDK1.8的HashMap底层采用数组+链表+红黑树实现,当链表长度不足8时仍然采用链表解决冲突,当链表长度超过8时转化为红黑树,这样既发挥了链表在冲突少的情况下性能高的优点,又利用了红黑树在冲突多的情况下性能高的优点。

tips: 其中红黑树的插入、删除、查找时间复杂度均为O(logN),效率很高。

除此之外,JDK1.8的HashMap还有其他优化:

  1. 引入红黑树之前,HashMap扩容是一个比较消耗性能的操作(rehash),JDK1.8在扩容时做了优化,不再像之前那样全部rehash,而是采用相对高效的方式进行rehash。
  2. JDK1.8的HashMap中,链表转红黑树会选择比较高的阈值(8),这样可以尽量减少红黑树结点过少导致性能不如链表的情况出现。
  3. JDK1.8的HashMap中,链表转红黑树和红黑树转链表都采取了较为高效的方式,而不是全部重新构建,这也提高了性能。
  4. JDK1.8的HashMap废除了原来的Entry对象,取而代之的是Node对象。其中,Node是一个泛型类。这也带来了一定的性能提高。

通过以上几点优化,JDK1.8的HashMap在时间和空间两方面都取得较大提高。它既解决了之前版本在大容量和高冲突率下性能下降的问题,也不失在一般场景下的高性能,这也是它成为如今最主流的Map实现的原因。

所以如果你的程序需要支持JDK1.8以上的环境,强烈建议使用JDK1.8及之后的HashMap,这会让你的程序既高效又具有很好的拓展性。理解JDK1.8对HashMap的优化,也有助于我们更深入地理解HashMap这个数据结构。

HashMap的遍历方式

HashMap提供了几种方式遍历所有键值对:

  1. entrySet()遍历:通过HashMap.entrySet()获得key-value的Set集合,然后进行遍历。这是最常用的遍历方式。
HashMap<String, Integer> map = new HashMap<>();
for (Map.Entry<String, Integer> entry : map.entrySet()) {String key = entry.getKey();Integer value = entry.getValue();// do something
}
  1. keySet()遍历:通过HashMap.keySet()获得所有的key,然后通过get(key)获得对应的value。
HashMap<String, Integer> map = new HashMap<>();
for (String key : map.keySet()) {Integer value = map.get(key);// do something
}
  1. values()遍历:通过HashMap.values()获得所有的value,这种方式我们无法获取到key,只能遍历value。
HashMap<String, Integer> map = new HashMap<>();
for (Integer value : map.values()) {// do something
}
  1. 迭代器遍历:通过HashMap.entrySet().iterator()获取迭代器进行遍历。
HashMap<String, Integer> map = new HashMap<>();
Iterator<Map.Entry<String, Integer>> iter = map.entrySet().iterator();
while (iter.hasNext()) {Map.Entry<String, Integer> entry = iter.next();String key = entry.getKey();Integer value = entry.getValue();// do something
}
  1. Lambda表达式遍历(JDK1.8+):JDK1.8提供了Lambda表达式,使我们可以更简洁地遍历HashMap。
HashMap<String, Integer> map = new HashMap<>();
map.forEach((k, v) -> {// do something
});

以上就是HashMap提供的几种遍历方式,entrySet()方法是最常用的方式,我们在学习和使用HashMap时可以灵活运用这几种遍历方式。

需要注意的是,无论使用哪种遍历方式,遍历过程中如果对HashMap进行修改(增加、删除键值对),都有可能会产生 ConcurrentModificationException,这是因为遍历时使用的迭代器或是HashMap的快照,并不反映在遍历过程中对原HashMap的修改。

所以在遍历HashMap的过程中,如果对其进行修改,应该使用Iterator.remove()等保证迭代器稳定的方法,或者捕获ConcurrentModificationException并在catch块中进行相应处理。

HashMap的常见面试题

  1. HashMap的工作原理?HashMap底层采用哈希表实现,它包含数组+链表/红黑树的结构。HashMap会根据键的hashCode计算出对应的数组下标,如果发生哈希冲突,就将键值对存储在链表或者红黑树中。JDK1.8之前,HashMap采用数组+链表结构,1.8之后引入了红黑树来优化链表过长的情况。
  2. HashMap的底层实现?JDK1.8之前,HashMap底层采用数组+链表实现,数组是HashMap的主体,链表用来解决哈希冲突。JDK1.8引入了红黑树,当链表长度过长时会转化为红黑树,以提高性能。
  3. HashMap的扩容机制是什么?当HashMap中存储的键值对个数超过数组大小*加载因子时,HashMap会进行扩容操作。扩容时,容量会翻倍,并进行rehash操作。JDK1.8在扩容时做了优化,只对哈希值和扩容后的索引不等的键值对进行rehash。
  4. HashMap的线程安全性?HashMap是非线程安全的,在多线程环境下使用时需要外部同步机制来保证线程安全。 HashTable是线程安全的,内部的方法基本都加了synchronized关键字进行同步。如果需要考虑线程安全,建议使用ConcurrentHashMap。
  5. HashMap判断两个键值对相等的标准?HashMap判断两个键值对相等的标准是两个键的hashCode相等,并且两个键通过equals方法比较也返回true。hashCode相等表明两个键可能相等,但需要进一步通过equals方法确定。
  6. HashMap允许存放null键null值吗?HashMap允许存放null键和null值。当键为null时,会存储在数组的第0个位置。
  7. HashMap的初始容量大小是多少?加载因子是多少?HashMap的默认初始容量是16,加载因子是0.75。加载因子用来控制HashMap的空间利用率,过小会导致HashMap大小增加过快,而过大会导致链表过长。

以上是HashMap常见的一些面试题,我们在学习和使用HashMap的时候需要理解这些概念和机制,这也有助于我们在面试时回答问题。如果有不理解的地方可以再回过头来复习。

相关文章:

【面试长文】HashMap的数据结构和底层原理以及在JDK1.6、1.7和JDK8中的演变差异

文章目录 HashMap的数据结构和底层原理以及在JDK1.6、1.7和JDK8中的演变差异HashMap的数据结构和原理JDK1.6、1.7和1.8中的HashMap源码演变JDK1.6JDK1.7JDK1.8 总结自己实现一个简单的HashMapHashMap的时间复杂度分析HashMap的空间复杂度分析HashMap的应用场景HashMap的弊端及解…...

【25】linux进阶——网络文件系统NFS

大家好&#xff0c;这里是天亮之前ict&#xff0c;本人网络工程大三在读小学生&#xff0c;拥有锐捷的ie和红帽的ce认证。每天更新一个linux进阶的小知识&#xff0c;希望能提高自己的技术的同时&#xff0c;也可以帮助到大家 另外其它专栏请关注&#xff1a; 锐捷数通实验&…...

JAVA入坑之JAVADOC(Java API 文档生成器)与快速生成

目录 一、JAVADOC&#xff08;Java API 文档生成器&#xff09; 1.1概述 1.2Javadoc标签 1.3Javadoc命令 1.4用idea自带工具生成API帮助文档 二、IDEA如何生成get和set方法 三、常见快捷方式 3.1快速生成main函数 3.2快速生成println()语句 3.3快速生成for循环 3.4“…...

React | React组件化开发

✨ 个人主页&#xff1a;CoderHing &#x1f5a5;️ React .js专栏&#xff1a;React .js React组件化开发 &#x1f64b;‍♂️ 个人简介&#xff1a;一个不甘平庸的平凡人&#x1f36c; &#x1f4ab; 系列专栏&#xff1a;吊打面试官系列 16天学会Vue 11天学会React Node…...

云计算的优势与未来发展趋势

一、前言二、云计算的基础概念2.1 云计算的定义2.2 云计算的发展历程2.3 云计算的基本架构2.4 云计算的主要服务模式 三、企业采用云计算的优势3.1 降低成本3.2 提高效率和灵活性3.3 提升信息系统的安全性和可靠性3.4 拥有更加丰富的应用和服务 四、行业应用案例4.1 金融行业4.…...

shell编程lesson01

命令行和脚本关系 命令行&#xff1a;单一shell命令&#xff0c;命令行中编写与执行&#xff1b; 脚本&#xff1a;众多shell命令组合成一个完成特定功能的程序&#xff0c;在脚本文件中进行编写维护。 脚本是一个文件&#xff0c;一个包含有一组命令的文件。 编写一个shel…...

看看人家的MyBatis批量插入数据优化,从120s到2.5s,那叫一个优雅!

粗略的实验 最后 最近在压测一批接口的时候&#xff0c;我发现接口处理速度比我们预期的要慢。这让我感到有点奇怪&#xff0c;因为我们之前已经对这些接口进行了优化。但是&#xff0c;当我们进行排查时&#xff0c;发现问题出在数据库批量保存这块。 我们的项目使用了 myb…...

软件和信息服务业专题讲座

软件和信息服务业专题讲座 单选题&#xff08;共 10 题&#xff0c;每题 3 分&#xff09; 1、根据本讲&#xff0c;我国要加强物联网应用领域&#xff08;&#xff09;开发和应用。 A、大数据 2、根据本讲&#xff0c;要充分发挥软件对城市管理和惠民服务的&#xff08;&am…...

由 ChatGPT 团队开发,堪称辅助神器!IntelliJ IDEA 神级插件

什么是Bito&#xff1f; 为什么要使用Bito&#xff1f; 如何安装Bito插件 如何使用Bito插件 什么是Bito&#xff1f; Bito是一款由ChatGPT团队开发的IntelliJ IDEA编辑器插件&#xff0c;旨在提高开发人员的工作效率。此插件强大之处在于它不仅可以帮助开发人员更快地提交…...

spass modeler

课时1&#xff1a;SPSS Modeler 简介 本课时一共分为五个模块&#xff0c;分别是Modeler概述、工具安装、窗口说明以及功能介绍和应用案例。相信通过本课时内容的学习&#xff0c;大家将会对SPSS Modeler有个基础的了解. 在学习本节课内容之前&#xff0c;先来看看本节课我们究…...

kafka的push、pull分别有什么优缺点

文章目录 kafka的push、pull分别有什么优缺点Push 模式优点缺点 Pull 模式优点缺点 实践操作 kafka的push、pull分别有什么优缺点 Kafka 是由 Apache 软件基金会开发的一个开源流处理平台&#xff0c;广泛应用于各大互联网公司的消息系统中。在 Kafka 中&#xff0c;生产者使用…...

【Canvas入门】从零开始在Canvas上绘制简单的动画

这篇文章是观看HTML5 Canvas Tutorials for Beginners教程做的记录&#xff0c;所以代码和最后的效果比较相似&#xff0c;教程的内容主要关于这四个部分&#xff1a; 创建并设置尺寸添加元素让元素动起来与元素交互 设置Canvas的大小 获取到canvas并设置尺寸为当前窗口的大…...

【技术整合】各技术解决方案与对应解决的问题

文章目录 基本实现性能安全 本文将框架分为三大类&#xff1a; 基本实现&#xff1a;包括某个供能或者提供web、移动端、桌面端、或者上述端上的某种功能性能&#xff1a;提升高可用、高并发的框架安全&#xff1a;包括网络安全、权限与容灾等 基本实现 .NET CORE、.NET web基…...

公网远程访问公司内网象过河ERP系统「内网穿透」

文章目录 概述1.查看象过河服务端端口2.内网穿透3. 异地公网连接4. 固定公网地址4.1 保留一个固定TCP地址4.2 配置固定TCP地址 5. 使用固定地址连接 概述 ERP系统对于企业来说重要性不言而喻&#xff0c;不管是财务、生产、销售还是采购&#xff0c;都需要用到ERP系统来协助。…...

Win11的两个实用技巧系列之修改c盘大小方法、功能快捷键大全

Win11 c盘无法更改大小什么原因?Win11修改c盘大小方法 有不少朋友反应Win11 c盘无法更改大小是怎么回事&#xff1f;本文就为大家带来了详细的更改方法&#xff0c;需要的朋友一起看看吧 Win11 c卷无法更改大小什么原因&#xff1f;有用户电脑的系统盘空间太小了&#xff0c;…...

离散数学下--- 代数系统

代数系统 定义&#xff1a; 代数系统是用代数运算构造数学模型的方法。 • 通过构造手段生成&#xff0c;所以也称代数结构 • 代数运算&#xff1a;在集合上建立满足一定规则的运算系统 &#xff08;一&#xff09;二元运算 二元运算的定义 二元运算需要满足的两个条件&a…...

java基础入门-04

Java基础入门-04 11、集合&学生管理系统11.1.ArrayList集合和数组的优势对比&#xff1a;11.1.1 ArrayList类概述11.1.2 ArrayList类常用方法11.1.2.1 构造方法11.1.2.2 成员方法11.1.2.3 示例代码 11.1.3 ArrayList存储字符串并遍历11.1.3.1 案例需求11.1.3.2 代码实现 11…...

《面试1v1》java反射

我是 javapub&#xff0c;一名 Markdown 程序员从&#x1f468;‍&#x1f4bb;&#xff0c;八股文种子选手。 面试官&#xff1a; 你好&#xff0c;请问你对 Java 反射有了解吗&#xff1f; 候选人&#xff1a; 是的&#xff0c;我了解一些。 面试官&#xff1a; 那你能简单…...

【C语言】struct结构体

文章目录 一. 结构体简述二. 结构体的声明和定义1、简单地声明一个结构体和定义结构体变量2、声明结构体的同时也定义结构体变量3、匿名结构体4、配合typedef&#xff0c;声明结构体的同时为结构体取别名5、在声明匿名结构体时&#xff0c;使用typedef给这个匿名结构体取别名 三…...

Docker代码环境打包

1. 介绍 Docker是一种开源的容器化平台&#xff0c;它可以在操作系统级别运行应用程序。通过将应用程序及其依赖项封装成一个可移植的容器&#xff0c;Docker使得应用程序可以在任何环境中轻松部署、运行和管理。使用Docker&#xff0c;开发人员可以避免在不同环境中出现的配置…...

现代CMake高级教程 - 第 6 章:输出与变量

双笙子佯谬老师的【公开课】现代CMake高级教程课程笔记 第 6 章&#xff1a;输出与变量 在运行 cmake -B build 时&#xff0c;打印字符串&#xff08;用于调试&#xff09; message("Hello world!")❯ cmake --build buildHello world! -- Configuring done -- G…...

windows/linux文件传输

windows系统下文件传输-FTP python安装pyftpdlib模块 pip install pyftpdlib 这里可能会出现报错&#xff0c;自己看着更换源解决 然后运行python&#xff0c;在2121端口监听 python -m pyftpdlib 然后我们可以使用windows命令行进行操作&#xff0c;自己可以去看下相关文…...

Anoconda安装笔记+win10 更改中文用户名为英文

win10 更改中文用户名为英文 ① WinR打开命令窗口&#xff0c;输入regedit 打开注册表&#xff0c; 手动找到 HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\WindowsNT\CurrentVersion\ProfileList 在这个目录下面有几个S-1-5-的项&#xff0c;挨个检查每一项&#xff0c; 找到“…...

Java Web应用开发 ——作业七

一.单项选择题&#xff08;共7题,28.7分&#xff09; 1 Servlet程序的入口点是&#xff08; &#xff09;。 A、 init&#xff08;&#xff09; B、 main&#xff08;&#xff09; C、 service&#xff08;&#xff09; D、 doGet&#xff08;&#xff09; 正确答案&#…...

echo,date,bc命令详解

文章目录 echo&#xff0c;date&#xff0c;bc命令详解echo(输出文本)date(显示日期的命令)date命令的--date选项date命令 bc(高精度计算器) echo&#xff0c;date&#xff0c;bc命令详解 echo(输出文本) echo命令是一个常用的Shell命令&#xff0c;用于在终端上输出文本。它…...

【Java笔试强训 29】

&#x1f389;&#x1f389;&#x1f389;点进来你就是我的人了博主主页&#xff1a;&#x1f648;&#x1f648;&#x1f648;戳一戳,欢迎大佬指点! 欢迎志同道合的朋友一起加油喔&#x1f93a;&#x1f93a;&#x1f93a; 目录 一、选择题 二、编程题 &#x1f525;求正数数…...

如何在微服务下保证事务的一致性

随着业务的快速发展、业务复杂度越来越高&#xff0c;传统单体应用逐渐暴露出了一些问题&#xff0c;例如开发效率低、可维护性差、架构扩展性差、部署不灵活、健壮性差等等。而微服务架构是将单个服务拆分成一系列小服务&#xff0c;且这些小服务都拥有独立的进程&#xff0c;…...

华为OD机试 - 新学校选址(Python)

题目描述 为了解新学期学生暴涨的问题,小乐村要建立所新学校, 考虑到学生上学安全问题,需要所有学生家到学校的距离最短。 假设学校和所有学生家都走在一条直线之上,请问学校建立在什么位置, 能使得到学校到各个学生家的距离和最短。 输入描述 第一行: 整数 n 取值范围 [1…...

thinkphp6结合layui增删改查综合案列

文章目录 技术栈实现代码实现数据库 本案例适合新手&#xff0c;特别是杠刚入门thinkphp和layui&#xff0c;但又不是特别熟悉这类 主要实现登录退出功能&#xff0c;用户模块的增删改查功能&#xff0c;分页功能是layui表单自带功能 效果图 左侧的菜单栏我没有写对应的页面&am…...

PostgreSQL数据库以任意时间间隔聚合查询group by

文章目录 业务场景以固定时间&#xff08;年/月/日/时/分/秒&#xff09;聚合to_char聚合date_trunc聚合 以任意时间聚合date_bin聚合实际应用 业务场景 我们做的是交通信控平台&#xff0c;需要根据实时采集到的交通大数据&#xff0c;计算出一些指标&#xff0c;存储到数据库…...

网站建设模板源代码/搜索广告优化

文章目录Q11.1 题目1.2 思路1.3 代码Q22.1 题目2.2 思路2.3 代码Q33.1 题目3.2 思路3.3 代码Q44.1 题目4.2 思路4.3 代码Q55.1 题目5.2 思路5.3 代码Q1 1.1 题目 利用递归方法求5!。 1.2 思路 递归公式&#xff1a;fnfn_1*4! 1.3 代码 def fact(j):sum 0if j 0:sum 1e…...

新疆乌鲁木齐哪家做网站好/重庆可靠的关键词优化研发

从单个计算机到互联网 在中国大陆&#xff0c;计算机有一个更为形象的名字&#xff0c;电脑。从字面可以看出&#xff0c;它是一件帮助人类进行思考的机器。但决定计算机思考什么&#xff0c;如何进行思考的&#xff0c;就是程序。 把人类脑袋中的可重复的、机械性的脑力劳动&a…...

上海网站建设维护/seo是什么服

Flutter基础—你好&#xff0c;Flutter&#xff01; Flutter基础—开发环境与入门 Flutter基础—第一个Flutter实例 Flutter基础—质感设计 Flutter基础—手势处理 Flutter基础—应用实例 Flutter基础—根据用户输入改变控件 Flutter基础—常用控件之容器 Flutter基础—…...

凡客之家推广平台/seo快速排名软件平台

以前听这人说genymotion好&#xff0c;听那人说genymotion模拟器好&#xff0c;身为开发者&#xff0c;使用google原生模拟器确实有点慢&#xff0c;所以本人就到genymotion官网下了个带vitrualbox的安装包&#xff0c;然后下一步下一步安装&#xff0c;安装完成后启动genymoti…...

电脑上做网站/培训机构

使用for(String item&#xff1a;list)会很好,但是它只会迭代一个列表,而你需要一个显式迭代器用于另一个列表.或者,您可以为两者使用显式迭代器.以下是问题的示例,以及使用索引for循环的解决方案&#xff1a;import java.util.*;public class ListsToMap {static public void …...

洛阳做网站的公司/sem竞价托管公司

0x01、基于mysql实现分布式锁基于分布式锁的实现&#xff0c;首先肯定是想单独分离出一台mysql数据库&#xff0c;所有服务要想操作文件(共享资源)&#xff0c;那么必须先在mysql数据库中插入一个标志&#xff0c;插入标志的服务就持有了锁&#xff0c;并对文件进行操作&#x…...