您的位置:

Java HashMap深入解析

一、HashMap简介

HashMap是Java中非常常用的一种数据结构,我们可以用它来存储数据,并可以通过key来快速查找对应的value。在HashMap中,每个key都对应着一个唯一的value,而且这个key-value对是以无序的方式存储在HashMap中的。其中,key必须是唯一的,而value不必唯一。

在Java中,HashMap是基于哈希表实现的,具体来说,它通过哈希函数把关键字映射到存储位置,以实现快速的查找。在哈希表中,通常使用桶(bucket)的方式来存储数据,即数组+链表或者数组+红黑树的方式。

二、HashMap常用方法

在了解HashMap的内部实现之前,我们先来看看HashMap中常用的方法。以下是其常用方法:

    // 向map中添加元素,如果key已存在,则更新value并返回旧value值;如果key不存在,则返回null
    V put(K key, V value);
    
    // 获取key对应的value值,如果key不存在则返回null
    V get(Object key);
    
    // 从map中移除该key及其对应的value,如果key不存在则返回null
    V remove(Object key);
    
    // 返回map中key的个数
    int size();
    
    // 判断map是否为空
    boolean isEmpty();
    
    // 判断map是否包含指定的key
    boolean containsKey(Object key);
    
    // 判断map是否包含指定的value
    boolean containsValue(Object value);
    
    // 清空map中所有key-value对
    void clear();

三、HashMap实现原理

了解HashMap的实现原理是很有必要的,可以更好的理解HashMap的内部结构和具体使用场景。在Java中,HashMap的内部实现主要基于数组+链表(或者数组+红黑树)的数据结构。

首先,HashMap内部定义了一个Node类用于表示链表或者红黑树的节点,它包含了key、value和指向下一个节点的指针。在HashMap中,key的hashCode()方法决定了该key在数组中的存储位置。当我们往HashMap中添加一个key-value对时,首先会计算key的hashCode值,然后根据hashCode值找到对应的数组下标,如果该下标处已经有了元素,则遍历对应的链表查找是否已经存在相同的key,如果找到则直接更新value值,否则插入到链表中。

但是,当链表中的元素太多时,遍历链表的效率会变得非常低,所以当链表中的元素个数大于某个阈值时,JDK1.8版本中会将链表转换为红黑树,这样可以保证在大规模元素时仍然能够保证速度。同时,当红黑树节点的个数小于某个阈值时,又会把红黑树转换回链表,这样可以在元素数量减少时回归到链表的高效性。

另外,由于哈希函数有可能将不同的key映射到同一个数组下标,所以当多个key映射到同一个下标时会形成一个链表。当链表中的元素太多时则会转换成红黑树或者进行扩容操作,后面会详细解释。

四、HashMap初始化

当我们创建一个HashMap对象时,JVM会为其分配数组空间,代码如下:

    public HashMap() {
        this.loadFactor = DEFAULT_LOAD_FACTOR; // 负载因子,默认为0.75
    }
    
    public HashMap(int initialCapacity) {
        this(initialCapacity, DEFAULT_LOAD_FACTOR);
    }
    
    public HashMap(int initialCapacity, float loadFactor) {
        if (initialCapacity < 0)
            throw new IllegalArgumentException("Illegal initial capacity: " + initialCapacity);
        if (initialCapacity > MAXIMUM_CAPACITY)
            initialCapacity = MAXIMUM_CAPACITY;
        if (loadFactor <= 0 || Float.isNaN(loadFactor))
            throw new IllegalArgumentException("Illegal load factor: " + loadFactor);
        this.loadFactor = loadFactor;
        this.threshold = tableSizeFor(initialCapacity);
    }
    
    /**
     * 返回大于等于给定初始容量的最小2次幂
     */
    static final int tableSizeFor(int cap) {
        int n = -1 >>> Integer.numberOfLeadingZeros(cap - 1);
        return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1;
    }

在不指定初始容量和负载因子的情况下,默认初始容量为16,默认负载因子是0.75。其中,初始容量必须是2的幂,如果给定的初始容量不是2的幂时,HashMap会自动将初始容量调整为大于等于给定初始容量的最小2次幂,如下:

    static final int MAXIMUM_CAPACITY = 1 << 30; // 最大容量为2^30
    
    static final int tableSizeFor(int cap) {
        int n = -1 >>> Integer.numberOfLeadingZeros(cap - 1);
        return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1;
    }

当HashMap中的元素个数达到了容量的0.75倍时,JVM就会进行扩容操作。扩容时会将数组长度加倍,并将原有元素重新分配到新的数组中。如果元素过多,还会将链表或者红黑树转换为新的链表或者红黑树。

五、HashMap线程安全性

HashMap不是线程安全的,如果在多线程环境下修改HashMap的结构,可能会导致链表出现断裂,或者出现环形链表等问题,导致循环遍历时产生死循环。因此,在多线程环境下使用HashMap需要进行同步控制。

六、示例代码

下面是一个简单的HashMap示例,展示了HashMap的基本用法。

    public class HashMapDemo {
        public static void main(String[] args) {
            // 创建HashMap
            Map<String, Integer> map = new HashMap<>(16);
            
            // 添加元素
            map.put("Apple", 10);
            map.put("Banana", 20);
            map.put("Orange", 30);
            
            // 获取元素
            Integer value = map.get("Banana");
            System.out.println("Banana对应的value为:" + value); // 输出20
            
            // 遍历元素
            for (Map.Entry<String, Integer> entry : map.entrySet()) {
                System.out.println(entry.getKey() + "-->" + entry.getValue());
            }
        }
    }