深入理解HashMap

深入理解HashMap

什么是HashMap

HashMap作为Java语言中一种重要的类型,其存储数据通过键值对的形式存储,即的形式。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 map = new HashMap();
   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所以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 p。可以理解p保存了发生碰撞的数据的值,Node为链表的格式,用来存储单条数据。
如果没有碰撞,将数据存储之后,直接跳到以下代码进行处理

    ++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 e; K k;,为存储数据的临时变量。afterNodeInsertion(evict)方法再HashMap中为空操作;执行return null结束put()方法的调用。

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

    else {
        Node 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)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.
     *
     * 

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.) * *

A return value of {@code null} does not necessarily * 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 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 getNode(int hash, Object key) {
        Node[] tab; Node 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)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)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 hasmap = new HashMap();
        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中频繁使用的一种数据存储格式。

你可能感兴趣的:(深入理解HashMap)