java基础中HashMap的原理详解

HashMap的数据结构:

  • 首先,HashMap中数据的存储是由数组链表一起实现的。
  • 数组是在内存中开辟一段连续的空间,因此,只要知道了数组首个元素的地址,在数组中寻址就会非常容易,其时间复杂度为O(1)。但是当要插入或删除数据时,时间复杂度就会变为O(n)
  • 链表是内存中一系列离散的空间,其插入和删除操作的内存复杂度为O(1),但是寻址操作的复杂度却是O(n)。那有没有一种方法可以结合两者的优点,即寻址,插入删除快呢?这个方法就是HashMap。
  • 我们看看HashMap的拉链式实现方法。如下图
    HashMap的拉链式实现方法
  • Entry是一个static class,其中包含了key和value,也就是键值对,另外还包含了一个next的Entry指针。可以看出:Entry就是数组中的元素,每个Entry其实就是一个key-value对,它特有一个指向下一个元素的引用,这就构成了链表

1.HashMap中定义了一个Entry类的数组table

  • 其中table数组就是buckets(桶),其中数组和链表的数据区保存的就是一个Entry对象,Entry对象中保存的就是HashMap中的键值对(key-value)

2.HashMap中两个重要的属性:capacity(容量)和load factor(加载因子)

  • capacitybuckets的容量,load factor是衡量buckets填满程度的比例。如果对迭代性能要求很高的话不要把capacity设置过大,也不要把load factor设置过小。当buckets(桶)填充的数组(即hashmap中元素的个数)大于capacity*load factor时就要把buckets扩充为原来的两倍。

3.put函数(存储)的实现

  • 先根据key的hashcode()计算出hash值,并用hash值计算出放在数组中的哪个位置
  • 判断该位置是否为空,若为空,则直接调用addEntry()方法将元素加入
  • 若该位置不为空(产生数据冲突),将这个位置上的元素统一向这个位置所连的链表后推一格,然后将要加入的元素放在链表头部(头插法
  • 如果数据冲突导致链表过长,就把链表转换成红黑树
  • 如果新添加对象中的key值与之前添加的Entry对象通过equals比较返回true时,新添加Entry的value将覆盖集合中原有Entry的value,但key不会覆盖。(保证key的唯一性,不能重复
  • 如果buckets满了(超过capacity*load factor),就要resize(在resize的过程,简单的说就是把buckets扩充为2倍,之后重新计算数组index,把节点再放到新的buckets中)

4.get函数(获取)的实现

  • 首先计算key的hashcode,找到数组中对应位置的某一元素
  • 如果有冲突,通过key的equals方法在对应位置的链表中找到需要的元素
    • 若为,则在树中通过key.equals()去查找,O(logn)
    • 若为链表,则在链表中通过key.equals()去查找,O(n)
  • 在java1.8之前的是实现中是用链表解决冲突的,在产生碰撞的情况下,进行get时,两步的时间复杂度是O(1)+O(n)。因此,当碰撞很厉害的时候n很大,O(n)的速度显然是影响速度的。
  • 因此在java1.8中,利用红黑树替换链表,这样复杂度就变成了O(1)+O(logn)了,这样在n很大的时候,能够比较理想的解决这个问题。

5.案例

import java.util.Collection;
import java.util.HashMap;
import java.util.Map;
import java.util.Set;

public class 测试HashMap {
    public static void main(String[] args) {
        test1();
    }

    public static void test1(){
        HashMap<String, String> map = new HashMap<>();
        map.put("CN", "中华人民共和国");
        map.put("US", "美利坚合众国");
        map.put("RU", "俄罗斯");
        map.put("FR", "法兰西共和国");
        map.put("JP", "日本");
        map.put("CN", "中国");//key相同,对前面覆盖
        map.put("aa","俄罗斯");
        map.put(null, null);

        //1.便利所有的key
        Set<String> set1 = map.keySet();
        for (String i : set1){
            System.out.println(i);
        }
        System.out.println("---------------");
        //2.遍历所有的values
        Collection<String> values = map.values();
        for (String a : values){
            System.out.println(a);
        }
        System.out.println("---------------");

        //3.遍历所有的key-value
        /*
            entrySet返回了一个set集合,set集合中放了Entry类型的对象
            Entry中放了key-value
         */
        Set<Map.Entry<String, String>> set = map.entrySet();
        for (Map.Entry<String, String> e : set){
            System.out.println(e.getKey()+"\t\t"+e.getValue());
        }
    }
}


  • 输出为:
aa
null
RU
JP
CN
FR
US
---------------
俄罗斯
null
俄罗斯
日本
中国
法兰西共和国
美利坚合众国
---------------
aa		俄罗斯
null		null
RU		俄罗斯
JP		日本
CN		中国
FR		法兰西共和国
US		美利坚合众国
  • 一般使用第三种遍历方法

参考资料:

发布了24 篇原创文章 · 获赞 12 · 访问量 1万+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章