面试题系列:HashMap底层实现原理解析

持续创作,加速成长!这是我参与「掘金日新计划 · 6 月更文挑战」的第10天,点击查看活动详情

作者平台:

| CSDN:blog.csdn.net/qq_41153943…

| 掘金:juejin.cn/user/651387…

| 知乎:www.zhihu.com/people/1024…

| GitHub:github.com/JiangXia-10…

| 微信公众号:1024笔记

本文一共2939字,预计阅读12分钟

前言

前段时间在找工作,有一些问题问到的频率比较高,这里做一些整理和总结,希望能够帮助到也准备找工作的同学。

其中关于HashMap的问题被问到的频率很高,基本就是对于hashmap使用多吗?请说说看hashmap的底层原理? 所以这里对关于HashMap的知识点总结一下。所以如果有不正确的地方欢迎讨论指正!

常见集合类有哪些

说到HashMap还有一个绕不过的话题就是Collection集合类的使用。

常见的集合类有List、Set、Map它们有什么异同点。

我们知道List和set集合是接口,它们实现了Collection接口。

在Collection中定义了单列集合(List和Set)通用的一些方法,这些方法可用于操作所有的单列集合。方法如下:

public boolean add(E e) : 把给定的对象添加到当前集合中 。
public void clear() :清空集合中所有的元素。
public boolean remove(E e) : 把给定的对象在当前集合中删除。
public boolean contains(Object obj) : 判断当前集合中是否包含给定的对象。
public boolean isEmpty() : 判断当前集合是否为空。
public int size() : 返回集合中元素的个数。
public Object[] toArray() : 把集合中的元素,存储到数组中
复制代码

除了上述集合外还有一个集合就是Map,Map是键值对的集合接口,它的实现类主要包括:HashMap,TreeMap,Hashtable以及LinkedHashMap等。其中这四者的区别如下:

HashMap:HashMap是使用较多的一种Map,它是根据key的HashCode值来存储数据,并且可以根据key值来获取它的Value,并且它具有很快的访问速度。HashMap不允许有两条及以上记录的key值相同(多条会覆盖,可以为null,但是也只能有一个);但是允许多条记录的Value相同,也可以为Null。对于HashMap的操作是非同步的,所以是线程不安全的。

TreeMap: TreeMap是能够把它保存的记录根据key排序,默认是按升序排序,也可以指定排序的比较器,当用Iterator遍历TreeMap时,得到的记录是排过序的。TreeMap不允许key的值为null,也不允许多条记录的key值相同,对于treemap的操作也是非同步的,所以也不是线程安全的。

Hashtable: Hashtable是jdk1.2遗留的产物,它与HashMap类似,但是hashtable的key和value的值均不允许为null,并且它支持线程的同步,即任一时刻只有一个线程能写Hashtable,所以是线程安全的,但是同时也导致了Hashtale在写入时的效率比较慢。

LinkedHashMap:它保存了记录的插入顺序,并且在使用Iterator遍历LinkedHashMap时,先得到的记录是先插入的,LinkedHashMap在遍历的时候会比HashMap慢。key和value均允许为空,但是它的操作是非同步的,非线程安全的。

今天主要说的是HashMap!

HashMap的初始化大小、扩容、最大容量以及负载因子

首先可以看下HashMap的源码部分如下:

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

    private static final long serialVersionUID = 362498820763181265L;
    static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16
/**
 * The maximum capacity, used if a higher value is implicitly specified
 * by either of the constructors with arguments.
 * MUST be a power of two <= 1<<30.
 */
static final int MAXIMUM_CAPACITY = 1 << 30;
/**
 * The load factor used when none specified in constructor.
 */
static final float DEFAULT_LOAD_FACTOR = 0.75f;
复制代码

可以发现HashMap继承了AbstractMap抽象类并且实现了Map、Cloneable、Serializable接口,所以它实现了以上抽象类和接口的所有方法和具有它们的属性。

并且可以发现HashMap的默认初始容量大小是16,并且必须是2的N次幂,

static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16
复制代码

最大的容量是2的30次方

static final int MAXIMUM_CAPACITY = 1 << 30;
复制代码

默认的负载因子是0.75,即超过容量的75%,就进行扩容,当扩容时,数组的长度必须是2的N次幂,并且扩容是针对整个HashMap,每次扩容时,原来数组中的元素依次重新计算存放位置,并重新插入:

static final float DEFAULT_LOAD_FACTOR = 0.75f;
复制代码

HashMap的查询和插入原理

除此还有部分源码如下:

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;
    }

    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;
    }
}
复制代码

在jdk1.7中HashMap底层的数据结构采用的是数组+链表的形式,hashmap最主要的操作就是取(get)和存(put)。

put方法原理如下:

1、首先将key和value封装到Node对象当中;

2、然后会调用Key的hashCode()方法得出值;

public final int hashCode() {
    return Objects.hashCode(key) ^ Objects.hashCode(value);
}
复制代码

3、通过哈希算法,将hash值转换成数组的下标,如果下标位置上没有任何元素,就把该Node添加到这个位置上。但是如果下标对应的位置上已经有链表。就会拿着key和链表上每个节点的key进行equals比较。如果所有的equals方法返回都是false,那么这个新的节点将被添加到链表的末尾。如果有一个equals方法返回了true,那么这个节点的value将会被覆盖。

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;
}
复制代码

get方法实现原理:

1、首先调用key的hashCode()方法得出哈希值,并通过哈希算法转换成数组的下标;

2、根据得到的数组的下标的值,定位到数组的某个位置上。如果这个位置上什么都没有,则返回null。如果这个位置上有单向链表,那么它就会拿着Key和单向链表上的每一个节点的Key进行equals比较,如果所有equals方法都返回false,则get方法返回null。如果其中有一个节点的Key和参数Key进行equals返回true,那么此时该节点的value就是目标value了,那么get方法最终返回这个目标value。

可以发现HashMap集合的key,会先后调用hashCode 和 equals 这两个方法,所以这两个方法都需要重写。

HashMap的红黑树

相比 jdk1.7 的 HashMap 而言,jdk1.8最重要的就是引入了红黑树的设计,jdk1.8有如下源码:

static final int TREEIFY_THRESHOLD = 8;

static final int UNTREEIFY_THRESHOLD = 6;

static final int MIN_TREEIFY_CAPACITY = 64;
复制代码

红黑树是具有以下特征的树:

1、红黑树的每个节点要么是红色,要么是黑色,但是它的根节点一定是黑色的;

2、红黑树的每个红色节点的两个子节点一定是黑色;

3、红黑树的红色节点不能连续,也就是说红色节点的孩子和父亲都不能是红色;

4、在红黑树中从任一节点到其子树中每个叶子节点的路径都会包含相同数量的黑色节点;

5、红黑树的所有的叶节点都是是黑色的;

但是在发生插入和删除操作时,元素发生变化,所以会破坏上述条件3或条件4,这时候就需要通过平衡策略的调整使得查找树重新满足红黑树的条件。

平衡策略可以简单概括为三种: 左旋转 、 右旋转 ,以及 变色 。

左旋转:对于当前结点而言,如果右子结点为红色,左子结点为黑色,则执行左旋转;

右旋转:对于当前结点而言,如果左子、左孙子结点均为红色,则执行右旋转;

变色:对于当前结点而言,如果左、右子结点均为红色,则执行变色;

在插入或删除结点之后,只要我们沿着结点到根的路径上执行这三种操作,就可以让树重新满足红黑树的条件,使其成为红黑树。

红黑树除了插入操作比链表慢之外,其他的操作都比链表快,上述代码表示当hash表的某个链表的长度超过8时,数组长度大于64,hashmap底层的数组+链表的结构中的链表结构就会转为红黑树结构。而当红黑树上的节点数量小于6个,会重新把红黑树变成单向链表数据结构。

这样做的好处是为了避免在极端的情况下导致链表变得很长,从而导致查询效率变得很慢。因为红黑树结构是一种近似平衡的二叉查找树,即左右子树高度几乎一致,以此来防止树退化为链表,通过这种方式来保障查找的时间复杂度为 O(logn),而链表查询的时间复杂度为O(n);

总结

以上就是结合HashMap源码总结出的关于HashMap在面试中经常被问到的一些问题以及个人的一些总结和看法,如有任何问题或者不对的地方,欢迎指出和讨论!

相关推荐

猜你喜欢

转载自juejin.im/post/7105014692665851917