深入理解HashMap

深入理解HashMap

什么是HashMap

HashMap作为Java语言中一种重要的类型,其存储数据通过键值对的形式存储,即<key,value>的形式。HashMap继承AbstractMap类,最终实现的是Map接口。HashMap中数据的Key值不允许重复,HashMap中存储的数据可以为null,由于其key值不能重复,则也只有一个对象的key值可以为null。HashMap通过hashcode()来获取哈希值来决定在Map中的存储位置,所以HashMap中数据的存储顺序与存入顺序无关。

02深入理解HashMap_HashMap继承关系图.png

HashMap的基本用法

class StudentInfo
{
    private int id;
    private String homeTown;
    private int score;

    private final static StudentInfo INSTANCE = new StudentInfo();

    StudentInfo(){}

    StudentInfo(int id, String homeTown, int score)
    {
        this.id = id;
        this.homeTown = homeTown;
        this.score = score;
    }

    public StudentInfo getInstance(){return INSTANCE;}

    public int getId() {return this.id;}
    public void setId(int id) {this.id = id;}

    public String getHomeTown() {return this.homeTown;}
    public void setHomeTown(String homeTown) {this.homeTown = homeTown;}

    public int getScore() {return this.score;}
    public void setScore(int score) {this.score = score;}
}

这里首先定义一个学生信息类,主要包含学生的id、家乡和成绩三条属性以及set和get方法

   Map<String,StudentInfo> map = new HashMap<String, StudentInfo>();
   StudentInfo Tom = new StudentInfo(1,"New York",90);
   StudentInfo Abell = new StudentInfo(2,"Houston",95);

   map.put("Tom",Tom);
   map.put("Abell",Abell);

   StudentInfo aStudent = map.get("Abell");
   System.out.println("Id: " + aStudent.getId() + " Hometown: " + 
            aStudent.getHomeTown() + " Score: " + aStudent.getScore());

HashMap的类定义是一种范型的形式public class HashMap<K,V>所以HashMap的Key和Value值可以是各种类型。调用HashMap的put方法进行数据的存储,调用get方法依靠key值来获取对应的value值。
该程序的输出为:

Id: 2 Hometown: Houston Score: 95

HashMap中put()和get()方法的实现

想要更好的理解HashMap,阅读其源码是很有必要的,put()和get()方法又是HashMap数据操作中的最基本的两个方法。put()和get()方法中又调用了我们常说的hashcode()以及equal()的方法,其中hashcode()用来获取对象的哈希值,equal()用来比较两个对象是否相等;这两个方法都是从Object类中继承。这里我们逐步给出分析。首先我们来看一下put()方法。

put()方法的实现

    /**
     * Associates the specified value with the specified key in this map.
     * If the map previously contained a mapping for the key, the old
     * value is replaced.
     *
     * @param key key with which the specified value is to be associated
     * @param value value to be associated with the specified key
     * @return the previous value associated with {@code key}, or
     *         {@code null} if there was no mapping for {@code key}.
     *         (A {@code null} return can also indicate that the map
     *         previously associated {@code null} with {@code key}.)
     */
    public V put(K key, V value) {
        return putVal(hash(key), key, value, false, true);
    }

简单来说,在执行put操作时,将根据插入的key值与之前的数据进行比较,且该方法以范型的形式进行定义,其返回值与value的类型相同。如果入参Key值相同(当然Key的hash值也需要相同)但value值不同的话,则方法返回老的value值,且将用新的value代替老的value值。如果新插入的数据与老数据没有发生碰撞,即没有因为key值相同重复,则put()方法返回null。

putVal()方法底层是通过数组+链表+红黑树算法实现的。

final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
                   boolean evict)

putVal()方法的入参如上,分别保存为hash值、key和value;其中onlyIfAbsent,evict参数再HashMap格式中都没有使用

    if ((p = tab[i = (n - 1) & hash]) == null)
         tab[i] = newNode(hash, key, value, null);

在put()方法中先根据传入的hash值进行判断,通过(n-1) & hash来决定数据的存储位置,其中n为当前存储数据表的大小,若传入的hash值与已存在的数据的hash值相同,则发生碰撞,将不走该if的逻辑。若tab[i] == null,则未发生碰撞直接将数据储存。在此判断中同时给p赋值,Node<K,V> p。可以理解p保存了发生碰撞的数据的值,Node<K,V>为链表的格式,用来存储单条数据。
如果没有碰撞,将数据存储之后,直接跳到以下代码进行处理

    ++modCount;
    if (++size > threshold)
        resize();
    afterNodeInsertion(evict);
    return null;

modCount为int类型,记录HashMap结构变更的次数,在执行put()方法时,只有新增条目的时候才会发生结构的变更,同时该变量在remove()等操作中也将记录次数的增加。size变量记录HashMap中有多少个键值对,其大小限制为threshold,查看resize()的源码,我们可以知道其初始值为16*0.75,当size大小超过threshold后再此调用resize()方法,将容量扩带为当前的两倍。
如果数据发生碰撞,则执行else的逻辑,这里定义了Node<K,V> e; K k;,为存储数据的临时变量。afterNodeInsertion(evict)方法再HashMap中为空操作;执行return null结束put()方法的调用。

如果判断hash值时,发生了数据的碰撞,将转入下面的逻辑进行处理

    else {
        Node<K,V> e; K k;
        if (p.hash == hash &&
            ((k = p.key) == key || (key != null && key.equals(k))))
            e = p;
        else if (p instanceof TreeNode)
            e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
        else {
            for (int binCount = 0; ; ++binCount) {
                if ((e = p.next) == null) {
                    p.next = newNode(hash, key, value, null);
                  if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
                        treeifyBin(tab, hash);
                    break;
                }
                if (e.hash == hash &&
                    ((k = e.key) == key || (key != null && key.equals(k))))
                    break;
                p = e;
            }
        }
        if (e != null) { // existing mapping for key
            V oldValue = e.value;
            if (!onlyIfAbsent || oldValue == null)
                e.value = value;
            afterNodeAccess(e);
            return oldValue;
        }
    }
if (p.hash == hash &&
            ((k = p.key) == key || (key != null && key.equals(k))))
            e = p;

我们分段来看一下这部分程序,如果入参与碰撞的数据hash值、并且key值相同,则直接用临时变量来保存发生碰撞的老数据,然后跳到后面对碰撞数据的处理

    if (e != null) { // existing mapping for key
            V oldValue = e.value;
            if (!onlyIfAbsent || oldValue == null)
                e.value = value;
            afterNodeAccess(e);
            return oldValue;
        }

在该部分中,首先用变量oldValue保存老的value数据,然后通过将传入的value赋值给e.value完成数据的覆盖;随后afterNodeAccess(e)在HashMap中操作为空(在LinkedHashMap中对该方法进行了重写),return了发生碰撞老的value值。其余分支的代码为存储桶逻辑和红黑树的逻辑,从源码中我们可以看到如果桶的大小增长到8之后,将转成红黑树进行处理。

get()方法的实现

看完put()方法之后,再来看一下get()方法的实现

/**
     * Returns the value to which the specified key is mapped,
     * or {@code null} if this map contains no mapping for the key.
     *
     * <p>More formally, if this map contains a mapping from a key
     * {@code k} to a value {@code v} such that {@code (key==null ? k==null :
     * key.equals(k))}, then this method returns {@code v}; otherwise
     * it returns {@code null}.  (There can be at most one such mapping.)
     *
     * <p>A return value of {@code null} does not <i>necessarily</i>
     * indicate that the map contains no mapping for the key; it's also
     * possible that the map explicitly maps the key to {@code null}.
     * The {@link #containsKey containsKey} operation may be used to
     * distinguish these two cases.
     *
     * @see #put(Object, Object)
     */
    public V get(Object key) {
        Node<K,V> e;
        return (e = getNode(hash(key), key)) == null ? null : e.value;
    }

从上面的源码可以看到,get()方法入参味key值,这里没用范型的形式,而是直接定义为Object类型参数,通过调用getNode()完成进一步的数据查询操作。因为这部分源码也比较简单,这里直接给出分析

   /**
     * Implements Map.get and related methods.
     *
     * @param hash hash for key
     * @param key the key
     * @return the node, or null if none
     */
    final Node<K,V> getNode(int hash, Object key) {
        Node<K,V>[] tab; Node<K,V> first, e; int n; K k;
        if ((tab = table) != null && (n = tab.length) > 0 &&
            (first = tab[(n - 1) & hash]) != null) {
            if (first.hash == hash && // always check first node
                ((k = first.key) == key || (key != null && key.equals(k))))
                return first;
            if ((e = first.next) != null) {
                if (first instanceof TreeNode)
                    return ((TreeNode<K,V>)first).getTreeNode(hash, key);
                do {
                    if (e.hash == hash &&
                        ((k = e.key) == key || (key != null && key.equals(k))))
                        return e;
                } while ((e = e.next) != null);
            }
        }
        return null;
    }
 if ((tab = table) != null && (n = tab.length) > 0 &&
            (first = tab[(n - 1) & hash]) != null)

首先也是进行临时变量的定义,然后进行判空,如果数据表为空直接返回null,进一步在这个判断中也通过hash值对数据的位置进行判断(所以说如果要用自定义的类做key值的时候,重写hashcode()是很必要的)。

if (first.hash == hash && // always check first node
                ((k = first.key) == key || (key != null && key.equals(k))))
                return first;
            if ((e = first.next) != null) {
                if (first instanceof TreeNode)
                    return ((TreeNode<K,V>)first).getTreeNode(hash, key);
                do {
                    if (e.hash == hash &&
                        ((k = e.key) == key || (key != null && key.equals(k))))
                        return e;
                } while ((e = e.next) != null);
            }

这部分将以链表的形式对整个数据表进行遍历,首先检查链表中的第一个Node,之后通过链表的指针逐个指向后面的元素,进行数据的查找,直到找到匹配的数据或者指针指到最后一个元素。

重写hashcode()和equal()

当我们使用自定义的类型作为HashMap的key值的时候,我们需要重写一下hashcode()equal()方法。在说原因之前,我们先看一个例子

class People
{
    private String name;

    People(){}
    People(String name){this.name = name;}

    public void setName(String name){this.name = name;}
    public String getName(){return this.name;}

首先定义一个简单的People类,其中有对应的name属性

        People jack = new People("Tony");
        int age = 10;

        Map<People,Integer> hasmap = new HashMap<People,Integer>();
        hasmap.put(jack,age);

        System.out.print("The Tony's age is : ");
        System.out.print(hasmap.get(new People("Tony")));

之后我们定义一个People的对象,名叫做Tony,然后将它放到hashmap中,然后用get方法来取出这个名叫Jack的People变量,看一下输出结果

The Tony's age is : null

为什么是null,而不是我们期望的10。这里就可以想一下刚才看到的get()方法的源码。对,因为两个People对象并不是同一个,所以其对应的HashCode也不相同,然而我们就像通过相同的名字来取数据怎么办。这里就需要在People类中重写hashcode()和euqal()方法了。

    @Override
    public int hashCode()
    {
        return name.hashCode();
    }

    @Override
    public boolean equals(Object obj)
    {
        if(obj instanceof People)
        {
            People inPeople = (People)obj;
            return this.name.equals(inPeople.getName());
        }
        return false;
    }

重写之后,将不在调用Object类中的方法,再次执行,结果符合预期

The Tony's age is : 10

总结

我们都知道如果用数组的形式存储数据,执行插入或者删除的操作的时候,数组中所有元素都要移动,该操作对于频繁的插入、删除操作将十分耗内存;而使用链表的形式,虽不需要去移动数据,但执行查找遍历整个链表又是十分耗时的。

HashMap通过hash值的方式很好的解决了链表查找耗时的缺点,同时通过动态的扩容以及灵活的指针操作,解决了内存损耗的问题。可以说是两种基本存储结构的结合体,也是在Java中频繁使用的一种数据存储格式。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 162,825评论 4 377
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 68,887评论 2 308
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 112,425评论 0 255
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,801评论 0 224
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 53,252评论 3 299
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 41,089评论 1 226
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 32,216评论 2 322
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 31,005评论 0 215
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,747评论 1 250
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,883评论 2 255
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,354评论 1 265
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,694评论 3 265
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,406评论 3 246
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,222评论 0 9
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,996评论 0 201
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 36,242评论 2 287
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 36,017评论 2 281

推荐阅读更多精彩内容