面试必会之HashMap源码分析(上)

简介: 面试必会之HashMap源码分析(上)

简介


HashMap最早出现在JDK1.2中,底层基于散列算法实现。HashMap 允许 null 键和 null 值,是非线程安全类,在多线程环境下可能会存在问题。


1.8版本的HashMap数据结构:


image.png


为什么有的是链表有的是红黑树?


默认链表长度大于8时转为树


结构


Node是HhaspMap中的一个静态内部类 :


//Node是单向链表,实现了Map.Entry接口
static class Node<K,V> implements Map.Entry<K,V> {
    final int hash;
    final K key;
    V value;
    Node<K,V> next;
    //构造函数
    Node(int hash, K key, V value, Node<K,V> next) {
        this.hash = hash;
        this.key = key;
        this.value = value;
        this.next = next;
    }
    // getter and setter ... toString ...
    public final K getKey()        { return key; }
    public final V getValue()      { return value; }
    public final String toString() { return key + "=" + value; }
    public final int hashCode() {
        return Objects.hashCode(key) ^ Objects.hashCode(value);
    }
    public final V setValue(V newValue) {
        V oldValue = value;
        value = newValue;
        return oldValue;
    }
    public final boolean equals(Object o) {
        if (o == this)
            return true;
        if (o instanceof Map.Entry) {
            Map.Entry<?,?> e = (Map.Entry<?,?>)o;
            if (Objects.equals(key, e.getKey()) &&
                Objects.equals(value, e.getValue()))
                return true;
        }
        return false;
    }
}



TreeNode 是红黑树的数据结构。


static final class TreeNode<K,V> extends LinkedHashMap.Entry<K,V> {
    TreeNode<K,V> parent;  // red-black tree links
    TreeNode<K,V> left;
    TreeNode<K,V> right;
    TreeNode<K,V> prev;    // needed to unlink next upon deletion
    boolean red;
    TreeNode(int hash, K key, V val, Node<K,V> next) {
        super(hash, key, val, next);
    }
    /**
     * Returns root of tree containing this node.
     */
    final TreeNode<K,V> root() {
        for (TreeNode<K,V> r = this, p;;) {
            if ((p = r.parent) == null)
                return r;
            r = p;
        }
    }


类定义


public class HashMap<K,V> extends AbstractMap<K,V>
    implements Map<K,V>, Cloneable, Serializable


变量


/**
 * 默认初始容量16(必须是2的幂次方)
 */
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4;
/**
 * 最大容量,2的30次方
 */
static final int MAXIMUM_CAPACITY = 1 << 30;
/**
 * 默认加载因子,用来计算threshold
 */
static final float DEFAULT_LOAD_FACTOR = 0.75f;
/**
 * 链表转成树的阈值,当桶中链表长度大于8时转成树 
   threshold = capacity * loadFactor
 */
static final int TREEIFY_THRESHOLD = 8;
/**
 * 进行resize操作时,若桶中数量少于6则从树转成链表
 */
static final int UNTREEIFY_THRESHOLD = 6;
/**
 * 桶中结构转化为红黑树对应的table的最小大小
 当需要将解决 hash 冲突的链表转变为红黑树时,
 需要判断下此时数组容量,
 若是由于数组容量太小(小于 MIN_TREEIFY_CAPACITY )
 导致的 hash 冲突太多,则不进行链表转变为红黑树操作,
 转为利用 resize() 函数对 hashMap 扩容
 */
static final int MIN_TREEIFY_CAPACITY = 64;
/**
 保存Node<K,V>节点的数组
 该表在首次使用时初始化,并根据需要调整大小。 分配时,
 长度始终是2的幂。
 */
transient Node<K,V>[] table;
/**
 * 存放具体元素的集
 */
transient Set<Map.Entry<K,V>> entrySet;
/**
 * 记录 hashMap 当前存储的元素的数量
 */
transient int size;
/**
 * 每次更改map结构的计数器
 */
transient int modCount;
/**
 * 临界值 当实际大小(容量*填充因子)超过临界值时,会进行扩容
 */
int threshold;
/**
 * 负载因子:要调整大小的下一个大小值(容量*加载因子)。
 */
final float loadFactor;


构造方法


/**
 * 传入初始容量大小,使用默认负载因子值 来初始化HashMap对象
 */
public HashMap(int initialCapacity) {
    this(initialCapacity, DEFAULT_LOAD_FACTOR);
}
/**
 * 默认容量和负载因子
 */
public HashMap() {
    this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted
}
/**
 * 传入初始容量大小和负载因子 来初始化HashMap对象
 */
public HashMap(int initialCapacity, float loadFactor) {
    // 初始容量不能小于0,否则报错
    if (initialCapacity < 0)
        throw new IllegalArgumentException("Illegal initial capacity: " +
                                           initialCapacity);
    // 初始容量不能大于最大值,否则为最大值                                       
    if (initialCapacity > MAXIMUM_CAPACITY)
        initialCapacity = MAXIMUM_CAPACITY;
    //负载因子不能小于或等于0,不能为非数字    
    if (loadFactor <= 0 || Float.isNaN(loadFactor))
        throw new IllegalArgumentException("Illegal load factor: " +
                                           loadFactor);
    // 初始化负载因子                                       
    this.loadFactor = loadFactor;
    // 初始化threshold大小
    this.threshold = tableSizeFor(initialCapacity);
}
/**
 * 找到大于或等于 cap 的最小2的整数次幂的数。
 */
static final int tableSizeFor(int cap) {
    int n = cap - 1;
    n |= n >>> 1;
    n |= n >>> 2;
    n |= n >>> 4;
    n |= n >>> 8;
    n |= n >>> 16;
    return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1;
}

tableSizeFor方法详解:


用位运算找到大于或等于 cap 的最小2的整数次幂的数。比如10,则返回16


  1. 让cap-1再赋值给n的目的是使得找到的目标值大于或等于原值。例如二进制0100,十进制是4,若不减1而直接操作,答案是0001 0000十进制是16,明显不符合预期。
  2. 对n右移1位:001xx…xxx,再位或:011xx…xxx
  3. 对n右移2位:00011…xxx,再位或:01111…xxx
  4. 对n右移4位…
  5. 对n右移8位…
  6. 对n右移16位,因为int最大就2^32所以移动1、2、4、8、16位并取位或,会将最高位的1后面的位全变为1。
  7. 再让结果n+1,即得到了2的整数次幂的值了。


附带一个实例:


image.png




相关文章
|
1月前
|
存储 算法 Java
面试必备!一文搞懂HashMap如何优雅处理哈希冲突
大家好,我是小米,一个积极的程序员。今天聊聊Java面试中的常见问题——“HashMap是怎么解决哈希冲突的?”。通过一个小故事,我们了解到HashMap使用链地址法(JDK 1.8前)和红黑树(JDK 1.8后)来处理哈希冲突。链地址法用链表存储冲突的元素,而红黑树在链表长度超过8时启用,提升查找效率。希望这个讲解能帮助你更好地理解HashMap的工作原理。欢迎留言讨论,关注我的公众号“软件求生”,获取更多技术干货!
45 3
|
2月前
|
存储 Java 索引
HashMap高频面试题,让你掌握青铜回答与王者级回答,你值得拥有
《HashMap高频面试题,让你掌握青铜回答与王者级回答,你值得拥有》介绍了HashMap的实现原理、数据存储、哈希冲突处理及扩容机制。文章通过对比JDK 1.7和JDK 1.8版本,详细解析了不同版本中的链表与红黑树结构、Entry与Node的区别,以及put()方法的具体实现。特别指出JDK 1.8引入红黑树优化查询性能,并采用尾插法避免多线程环境下的链表环问题。负载因子和扩容机制确保了HashMap在不同场景下的高效运行。
48 2
|
4月前
|
存储 Java 程序员
Java面试加分点!一文读懂HashMap底层实现与扩容机制
本文详细解析了Java中经典的HashMap数据结构,包括其底层实现、扩容机制、put和查找过程、哈希函数以及JDK 1.7与1.8的差异。通过数组、链表和红黑树的组合,HashMap实现了高效的键值对存储与检索。文章还介绍了HashMap在不同版本中的优化,帮助读者更好地理解和应用这一重要工具。
115 5
|
4月前
|
Java
Java基础之 JDK8 HashMap 源码分析(中间写出与JDK7的区别)
这篇文章详细分析了Java中HashMap的源码,包括JDK8与JDK7的区别、构造函数、put和get方法的实现,以及位运算法的应用,并讨论了JDK8中的优化,如链表转红黑树的阈值和扩容机制。
59 1
|
4月前
|
存储 算法 安全
HashMap常见面试题(超全面):实现原理、扩容机制、链表何时升级为红黑树、死循环
HashMap常见面试题:红黑树、散列表,HashMap实现原理、扩容机制,HashMap的jd1.7与jdk1.8有什么区别,寻址算法、链表何时升级为红黑树、死循环
|
6月前
|
安全 Java
【Java集合类面试十五】、说一说HashMap和HashTable的区别
HashMap和Hashtable的主要区别在于Hashtable是线程安全的,不允许null键和值,而HashMap是非线程安全的,允许null键和值。
|
6月前
|
安全 Java
【Java集合类面试十三】、HashMap如何实现线程安全?
实现HashMap线程安全的方法包括使用Hashtable类、ConcurrentHashMap,或通过Collections工具类将HashMap包装成线程安全的Map。
|
6月前
|
安全 Java
【Java集合类面试十六】、HashMap与ConcurrentHashMap有什么区别?
HashMap是非线程安全的,而ConcurrentHashMap通过减少锁粒度来提高并发性能,检索操作无需锁,从而提供更好的线程安全性和性能。
|
6月前
|
Java
【Java集合类面试十四】、HashMap是如何解决哈希冲突的?
HashMap解决哈希冲突的方法是通过链表和红黑树:当链表长度超过一定阈值时,转换为红黑树以提高性能;当链表长度缩小到另一个阈值时,再转换回链表。
|
6月前
|
Java
【Java集合类面试十二】、HashMap为什么线程不安全?
HashMap在并发环境下执行put操作可能导致循环链表的形成,进而引起死循环,因而它是线程不安全的。