深入理解HashMap
什么是HashMap
HashMap作为Java语言中一种重要的类型,其存储数据通过键值对的形式存储,即
的形式。HashMap继承AbstractMap类,最终实现的是Map接口。HashMap中数据的Key值不允许重复,HashMap中存储的数据可以为null,由于其key值不能重复,则也只有一个对象的key值可以为null。HashMap通过hashcode()来获取哈希值来决定在Map中的存储位置,所以HashMap中数据的存储顺序与存入顺序无关。
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
保存了发生碰撞的数据的值,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
,为存储数据的临时变量。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中频繁使用的一种数据存储格式。