JAVA HashMap 是怎么工作的

1. HashMap是什么

在数组中我们是通过数组下标来对其内容索引的,而在Map中我们通过对象来对对象进行索引,用来索引的对象叫做key,其对应的对象叫做value

基于哈希表的 Map 接口的实现。此实现提供所有可选的映射操作,并允许使用 null 值和 null 键。(除了非同步和允许使用 null 之外,HashMap 类与 Hashtable 大致相同。)此类不保证映射的顺序,特别是它不保证该顺序恒久不变。 此实现假定哈希函数将元素适当地分布在各桶之间,可为基本操作(get 和 put)提供稳定的性能。迭代 collection 视图所需的时间与 HashMap 实例的“容量”(桶的数量)及其大小(键-值映射关系数)成比例。所以,如果迭代性能很重要,则不要将初始容量设置得太高(或将加载因子设置得太低)。


2. HashMap与Hashtable有什么区别

HashTable的应用非常广泛,HashMap是新框架中用来代替HashTable的类,也就是说建议使用HashMap,不要使用HashTable。可能你觉得HashTable很好用,为什么不用呢?这里简单分析他们的区别。 
   HashTable的方法是同步的,HashMap未经同步,所以在多线程场合要手动同步HashMap这个区别就像Vector和ArrayList一样。
   HashTable不允许null值(key和value都不可以),HashMap允许null值(key和value都可以)。
   HashTable有一个contains(Object value),功能和containsValue(Object value)功能一样。
   HashTable使用Enumeration,HashMap使用Iterator。
   HashTable中hash数组默认大小是11,增加的方式是 old*2+1。HashMap中hash数组的默认大小是16,而且一定是2的指数。
   哈希值的使用不同,HashTable直接使用对象的hashCode,而HashMap重新计算hash值,而且用与代替求模

3. HashMap与TreeMap的区别

再来看看HashMap和TreeMap有什么区别。HashMap通过hashcode对其内容进行快速查找,而TreeMap中所有的元素都保持着某种固定的顺序,如果你需要得到一个有序的结果你就应该使用TreeMap

下面先用一个例子来看, HashMap, Hashtable, TreeMap的区别。

import java.util.Map;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Hashtable;
import java.util.TreeMap;

public class Test
{
	public static void main(String[] args)
	{
		Map map = new HashMap();
		map.put("a","aa");
		map.put("b","bb");
		map.put("c","cc");
		map.put("d","dd");
		map.put("e","ee");

		Iterator it = map.keySet().iterator();
		while(it.hasNext())
		{
			Object key = it.next();
			System.out.println("map get(key) is :"+map.get(key));
		}

		Hashtable tab = new Hashtable();
		tab.put("a","aa");
		tab.put("b","bb");
		tab.put("c","cc");
		tab.put("d","dd");
		tab.put("e","ee");

		Iterator it_ht = tab.keySet().iterator();
		while(it_ht.hasNext())
		{
			Object key = it_ht.next();
			System.out.println("tab get(key) is :"+tab.get(key));
		}

		TreeMap tmp=new TreeMap();            
		tmp.put("a", "aaa");
		tmp.put("b", "bbb");
		tmp.put("c", "ccc");
		tmp.put("d", "ddd");
		Iterator iterator_2 = tmp.keySet().iterator();
		while (iterator_2.hasNext()) {
			Object key = iterator_2.next();
			System.out.println("tmp.get(key) is :"+tmp.get(key));
		}  


		
	}
}

可以 看出, TreeMap是顺序存放的, 而HashMap与 Hashtable是无序的, 

4. HashMap的get和put是如何工作的

最重要的一点是, 对于 put,我们传递key和value值给HashMap, 通过计算key的hashcode(), 得到在HashMap的位置, 然后把Key和Value一起存在节点中。 

为什么要把 Key的值也放在节点中呢?

这个是为了解决HashCode冲突的问题, 由于 HashMap是使用链表来存储的, 所以对于hashcode()相同的key,value, 会把它存到对应节点的下一个节点上。 

所以当用get 来获取key对应的value时, 我们先用hashcode()找到对应的位置,把所有的相同 hashcode的value都取出来,再使用key.equals()方法来确定到底是哪个value应该返回.

下面用一个例子来解释

import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;
public class Test2
{

	public static void main(String[] args)
	{
		Map map = new HashMap();
		map.put(new A(1),"aa");
		map.put(new A(2),"bb");
		map.put(new A(3),"cc");
		map.put(new A(4),"dd");
		map.put(new A(5),"ee");
		map.put(new A(6),"ff");
		map.put(new A(2),"gg");

		Iterator it = map.keySet().iterator();
		while(it.hasNext())
		{
			Object key = it.next();
			System.out.println("map get(key) is :"+map.get(key));
		}
	}
	static class A
	{
		int number;
		public A(int n)
		{
			this.number = n;
		}

		public int hashCode()
		{
			return number%5;
		}

		public boolean equals(Object o)
		{
			return (o instanceof A) && (number ==((A)o).number);
		}
	}
}
从结果中可以看到, 我们设置了7个元素,实际上只有6个key-value值, 因为new A(2)是相同的, 所以最后那个被认为是update的操作。 


上面的例子中,我们自己重写了equals 和hashCode函数,如果我们不重写equals,那会怎么样。 我们来看看。 

import java.util.HashMap;
import java.util.Map;
import java.util.Iterator;

public class Test3
{

	public static void main(String[] args)
	{
		Map map = new HashMap();
		for(int i=1;i<5;i++)
			map.put(new A(i),i);
		
		Iterator it = map.keySet().iterator();
		while(it.hasNext())
		{
			Object key = it.next();
			System.out.println("map get(key) is :"+map.get(key));
		}

		System.out.println("Now check the exists of new A(2)");
		A a = new A(2);
		if(map.containsKey(a))
			System.out.println("Exist: "+map.get(a));
		else
			System.out.println("Not found");
	}
}

class A
{
	int number;
	public A(int number)
	{
		this.number = number;
	}
}
结果如下: 认为 new A(2)不存在。 但是我们明明把new A(2) 放进去了啊。 


原因是这样的, A的HashCode方法继承自Object,而Object中的HashCode方法返回的HashCode对应于当前的地址,也就是说对于不同的对象,即使它们的内容完全相同,用HashCode()返回的值也会不同。这样实际上违背了我们的意图。因为我们在使用HashMap时,希望利用相同内容的对象索引得到相同的目标对象,这就需要HashCode()在此时能够返回相同的值。在上面的例子中,我们期望new A(i) (i=2)与 A a=new A(2)是相同的,而实际上这是两个不同的对象,尽管它们的内容相同,但它们在内存中的地址不同。因此很自然的,上面的程序得不到我们设想的结果。

现在我们把A的equals和hashMap修改一下

import java.util.HashMap;
import java.util.Map;
import java.util.Iterator;

public class Test3
{

	public static void main(String[] args)
	{
		Map map = new HashMap();
		for(int i=1;i<5;i++)
			map.put(new A(i),i);
		
		Iterator it = map.keySet().iterator();
		while(it.hasNext())
		{
			Object key = it.next();
			System.out.println("map get(key) is :"+map.get(key));
		}

		System.out.println("Now check the exists of new A(2)");
		A a = new A(2);
		if(map.containsKey(a))
			System.out.println("Exist: "+map.get(a));
		else
			System.out.println("Not found");
	}
}

class A
{
	int number;
	public A(int number)
	{
		this.number = number;
	}

	public int hashCode()
	{
		return this.number;
	}

	public boolean equals(Object o)
	{
		return (o instanceof A) && (number == ((A)o).number);
	}
}

现在可以找到这个对像了。 



请记住:如果你想有效的使用HashMap,你就必须重写在其的HashCode()。

还有两条重写HashCode()的原则:

  1. 不必对每个不同的对象都产生一个唯一的hashcode,只要你的HashCode方法使get()能够得到put()放进去的内容就可以了。即"不为一原则"。
  2. 生成hashcode的算法尽量使hashcode的值分散一些,不要很多hashcode都集中在一个范围内,这样有利于提高HashMap的性能。即"分散原则"。

掌握了这两条原则,你就能够用好HashMap编写自己的程序了。不知道大家注意没有,java.lang.Object中提供的三个方法:clone(),equals()和hashCode()虽然很典型,但在很多情况下都不能够适用,它们只是简单的由对象的地址得出结果。这就需要我们在自己的程序中重写它们,其实java类库中也重写了千千万万个这样的方法


5. 如何在多线程环境中使用HashMap

上面也讲到了HashMap不是同步的,这个区别于Hashtable。如果多个线程同时访问一个哈希映射,而其中至少一个线程从结构上修改了该映射,则它必须 保持外部同步。(结构上的修改是指添加或删除一个或多个映射关系的任何操作;仅改变与实例已经包含的键关联的值不是结构上的修改。)这一般通过对自然封装该映射的对象进行同步操作来完成。如果不存在这样的对象,则应该使用 Collections.synchronizedMap 方法来“包装”该映射。最好在创建时完成这一操作,以防止对映射进行意外的非同步访问,如下所示:

  Map m = Collections.synchronizedMap(new HashMap(...));


6. Java中对HashMap的深度分析

此部分转载自百度百科

HashMap可谓JDK的一大实用工具,把各个Object映射起来,实现了“键--值”对应的快速存取。但实际里面做了些什么呢?

  在这之前,先介绍一下负载因子和容量的属性。大家都知道其实一个 HashMap 的实际容量就 因子*容量,其默认值是 16×0.75=12; 这个很重要,对效率很一定影响!当存入HashMap的对象超过这个容量时,HashMap 就会重新构造存取表。这就是一个大问题,我后面慢慢介绍,反正,如果你已经知道你大概要存放多少个对象,最好设为该实际容量的能接受的数字。
  两个关键的方法,put和get:
  先有这样一个概念,HashMap是声明了 Map,Cloneable, Serializable 接口,和继承了 AbstractMap 类,里面的 Iterator 其实主要都是其内部类HashIterator 和其他几个 iterator 类实现,当然还有一个很重要的继承了Map.Entry 的 Entry 内部类,由于大家都有 源代码,大家有兴趣可以看看这部分,我主要想说明的是 Entry 内部类。它包含了hash,value,key 和next 这四个属性,很重要。put的源码如下
  public Object put(Object key, Object value) {
  Object k = maskNull(key);
  这个就是判断键值是否为空,并不很深奥,其实如果为空,它会返回一个static Object 作为键值,这就是为什么HashMap允许空键值的原因。
  int hash = hash(k);
  int i = indexFor(hash, table.length);

static int hash(Object x) {
  int h = x.hashCode();

  h += ~(h << 9);
  h ^= (h >>> 14);
  h += (h << 4);
  h ^= (h >>> 10);
  return h;
}
static int indexFor(int h, int length) {
  return h & (length-1);
}


  这连续的两步就是 HashMap 最牛的地方!研究完我都汗颜了,其中 hash 就是通过 key 这个Object的 hashcode 进行 hash,然后通过 indexFor 获得在Object table的索引值。
  table???不要惊讶,其实HashMap也神不到哪里去,它就是用 table 来放的。最牛的就是用 hash 能正确的返回索引。其中的hash算法,我跟JDK的作者 Doug 联系过,他建议我看看《The art of programing vol3》可恨的是,我之前就一直在找,我都找不到,他这样一提,我就更加急了,可惜口袋空空啊!!!
  不知道大家有没有留意 put 其实是一个有返回的方法,它会把相同键值的 put 覆盖掉并返回旧的值!如下方法彻底说明了 HashMap 的结构,其实就是一个表加上在相应位置的Entry的链表:
  for (Entry e = table[i]; e != null; e = e.next) {
  if (e.hash == hash && eq(k, e.key)) {
  Object oldvalue = e.value;
  e.value = value; //把新的值赋予给对应键值。
  e.recordAccess(this); //空方法,留待实现
  return oldvalue; //返回相同键值的对应的旧的值。
  }
  }
  modCount++; //结构性更改的次数
  addEntry(hash, k, value, i); //添加新元素,关键所在!
  return null; //没有相同的键值返回
  }
  我们把关键的方法拿出来分析:
  void addEntry(int hash, Object key, Object value, int bucketIndex) {
  table[bucketIndex] = new Entry(hash, key, value, table[bucketIndex]);
  因为 hash 的算法有可能令不同的键值有相同的hash码并有相同的table索引,如:key=“33”和key=Object g的hash都是-8901334,那它经过indexfor之后的索引一定都为i,这样在new的时候这个Entry的next就会指向这个原本的table[i],再有下一个也如此,形成一个链表,和put的循环对定e.next获得旧的值。到这里,HashMap的结构,大家也十分明白了吧?
  if (size++ >= threshold) //这个threshold就是能实际容纳的量
  resize(2 * table.length); //超出这个容量就会将Object table重构
  所谓的重构也不神,就是建一个两倍大的table(我在别的论坛上看到有人说是两倍加1,把我骗了),然后再一个个indexfor进去!注意!!这就是效率!!如果你能让你的HashMap不需要重构那么多次,效率会大大提高!
  说到这里也差不多了,get比put简单得多,大家,了解put,get也差不了多少了。对于collections我是认为,它是适合广泛的,当不完全适合特有的,如果大家的程序需要特殊的用途,自己写吧,其实很简单。(作者是这样跟我说的,他还建议我用LinkedHashMap,我看了源码以后发现,LinkHashMap其实就是继承HashMap的,然后override相应的方法,有兴趣的同人,自己looklook)建个 Object table,写相应的算法,就ok啦。





  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值