欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页  >  IT编程

java中HashMap的原理分析

程序员文章站 2024-03-09 17:35:35
我们先来看这样的一道面试题: 在 hashmap 中存放的一系列键值对,其中键为某个我们自定义的类型。放入 hashmap 后,我们在外部把某一个 key 的属性进行更改...

我们先来看这样的一道面试题:

在 hashmap 中存放的一系列键值对,其中键为某个我们自定义的类型。放入 hashmap 后,我们在外部把某一个 key 的属性进行更改,然后我们再用这个 key 从 hashmap 里取出元素,这时候 hashmap 会返回什么?

文中已给出示例代码与答案,但关于hashmap的原理没有做出解释。

1. 特性

我们可以用任何类作为hashmap的key,但是对于这些类应该有什么限制条件呢?且看下面的代码:

public class person {
  private string name;

  public person(string name) {
    this.name = name;
  }
}

map<person, string> testmap = new hashmap<>();
testmap.put(new person("hello"), "world");
testmap.get(new person("hello")); // ---> null

本是想取出具有相等字段值person类的value,结果却是null。对hashmap稍有了解的人看出来——person类并没有override hashcode方法,导致其继承的是object的hashcode(返回是其内存地址)。这也是为什么常用不变类如string(或integer等)做为hashmap的key的原因。那么,hashmap是如何利用hashcode给key做快速索引的呢?

2. 原理

首先,我们来看《thinking in java》中一个简单hashmap的实现方案:

//: containers/simplehashmap.java
// a demonstration hashed map.
import java.util.*;
import net.mindview.util.*;

public class simplehashmap<k,v> extends abstractmap<k,v> {
 // choose a prime number for the hash table size, to achieve a uniform distribution:
 static final int size = 997;
 // you can't have a physical array of generics, but you can upcast to one:
 @suppresswarnings("unchecked")
 linkedlist<mapentry<k,v>>[] buckets =
  new linkedlist[size];
 public v put(k key, v value) {
  v oldvalue = null;
  int index = math.abs(key.hashcode()) % size;
  if(buckets[index] == null)
   buckets[index] = new linkedlist<mapentry<k,v>>();
  linkedlist<mapentry<k,v>> bucket = buckets[index];
  mapentry<k,v> pair = new mapentry<k,v>(key, value);
  boolean found = false;
  listiterator<mapentry<k,v>> it = bucket.listiterator();
  while(it.hasnext()) {
   mapentry<k,v> ipair = it.next();
   if(ipair.getkey().equals(key)) {
    oldvalue = ipair.getvalue();
    it.set(pair); // replace old with new
    found = true;
    break;
   }
  }
  if(!found)
   buckets[index].add(pair);
  return oldvalue;
 }
 public v get(object key) {
  int index = math.abs(key.hashcode()) % size;
  if(buckets[index] == null) return null;
  for(mapentry<k,v> ipair : buckets[index])
   if(ipair.getkey().equals(key))
    return ipair.getvalue();
  return null;
 }
 public set<map.entry<k,v>> entryset() {
  set<map.entry<k,v>> set= new hashset<map.entry<k,v>>();
  for(linkedlist<mapentry<k,v>> bucket : buckets) {
   if(bucket == null) continue;
   for(mapentry<k,v> mpair : bucket)
    set.add(mpair);
  }
  return set;
 }
 public static void main(string[] args) {
  simplehashmap<string,string> m =
   new simplehashmap<string,string>();
  m.putall(countries.capitals(25));
  system.out.println(m);
  system.out.println(m.get("eritrea"));
  system.out.println(m.entryset());
 }
}

simplehashmap构造一个hash表来存储key,hash函数是取模运算math.abs(key.hashcode()) % size,采用链表法解决hash冲突;buckets的每一个槽位对应存放具有相同(hash后)index值的map.entry,如下图所示:

java中HashMap的原理分析

jdk的hashmap的实现原理与之相类似,其采用链地址的hash表table存储map.entry:

/**
 * the table, resized as necessary. length must always be a power of two.
 */
transient entry<k,v>[] table = (entry<k,v>[]) empty_table;

static class entry<k,v> implements map.entry<k,v> {
  final k key;
  v value;
  entry<k,v> next;
  int hash;
  …
}

map.entry的index是对key的hashcode进行hash后所得。当要get key对应的value时,则对key计算其index,然后在table中取出map.entry即可得到,具体参看代码:

public v get(object key) {
  if (key == null)
    return getfornullkey();
  entry<k,v> entry = getentry(key);

  return null == entry ? null : entry.getvalue();
}

final entry<k,v> getentry(object key) {
  if (size == 0) {
    return null;
  }

  int hash = (key == null) ? 0 : hash(key);
  for (entry<k,v> e = table[indexfor(hash, table.length)];
     e != null;
     e = e.next) {
    object k;
    if (e.hash == hash &&
      ((k = e.key) == key || (key != null && key.equals(k))))
      return e;
  }
  return null;
}

可见,hashcode直接影响hashmap的hash函数的效率——好的hashcode会极大减少hash冲突,提高查询性能。同时,这也解释开篇提出的两个问题:如果自定义的类做hashmap的key,则hashcode的计算应涵盖构造函数的所有字段,否则有可能得到null。