Map实现之HashMap(结构及原理)(转)

java.util包中的集合类包含 Java 中某些最常用的类。最常用的集合类是 List 和 Map。List 的具体实现包括 ArrayList 和 Vector,它们是可变大小的列表,比较适合构建、存储和操作任何类型对象元素列表。List 适用于按数值索引访问元素的情形。

Map 则提供了一个更通用的元素存储方法。Map 集合类用于存储元素对(称作“键”和“值”),其中每个键映射到一个值。从概念上而言,您可以将 List 看作是具有数值键的 Map。而实际上,除了 List 和 Map 都在定义 java.util 中外,两者并没有直接的联系。

Map接口的实现类有很多,其中HashMap就是比较重要的一个实现,本文就以HashMap为主重点介绍。

HashMap是基于哈希表的 Map 接口的实现。此实现提供所有可选的映射操作,并允许使用 null 值和 null 键。(除了非同步和允许使用 null 之外,HashMap 类与 Hashtable 大致相同。)此类不保证映射的顺序,特别是它不保证该顺序恒久不变。

HashMap结合了ArrayList与LinkedList两个实现的优点,,虽然HashMap并不会向List的两种实现那样在某项操作上性能较高,但是在基本操作(get 和 put)上具有稳定的性能。

首先从成员变量开始一点点的来了解HashMap和上述几个概念。

1.HashMap的成员变量:

Java代码  

  1. /**
  2. * 初始默认容量(必须为2的幂次方)
  3. */
  4. static final int DEFAULT_INITIAL_CAPACITY = 16;
  5. /**
  6. * 最大容量,如果被指定为一个更高的值必须为2的幂次方,并且小于1073741824.(1<<30)
  7. */
  8. static final int MAXIMUM_CAPACITY = 1 << 30;
  9. /**
  10. * 默认负载因子/负载系数
  11. */
  12. static final float DEFAULT_LOAD_FACTOR = 0.75f;
  13. /**
  14. * 内部实现表, 必要时调整大小,其长度亦为2的幂次方
  15. */
  16. transient Entry[] table;
  17. /**
  18. * map中添加的元素个数
  19. */
  20. transient int size;
  21. /**
  22. * 扩容临界值,当size达到此值时进行扩容 (容量乘以负载因子).
  23. */
  24. int threshold;
  25. /**
  26. * 内部实现表的负载因子
  27. */
  28. final float loadFactor;
  29. /**
  30. * 操作数,可以理解为map实例被操作的次数,包括添加,删除等等
  31. */
  32. transient volatile int modCount;

HashMap其内部实现是一个Entry数组table,而Entry就是保存相应键值的实体。table数组默认大小为16,我们也可以在初始化时指定更大的值,但指定值必须为2的幂次方。

通过对ArrayList的学习了解到ArrayList其内部实现也是数组,当被添加的元素超出数组的容纳极限时,ArrayList会对内部数组进行一次“扩容”,从而可以添加新的元素。

在HashMap中也有类似的概念,HashMap并不会像ArrayList一样直到数组都满了的情况下才去“扩容”,而是根据负载因子(load factor)来进行判断。

举例来说:HashMap实例中table数组的默认大小为16,负载因子为0.75,当添加元素个数大于等于12(16*0.75)时就会进行扩容。

所以说容量和负载因子直接影响着table数组是否扩容,什么时机扩容,进而影响这HashMap实例的性能。

当我们在初始化时可以指定HashMap实例的容量大小,当指定大小不为2的幂次方时,如下:

Java代码  

  1. Map map=new HashMap(131);

请问初始化完成HashMap内table的长度是多少? 答案为:256

其实只要打开HashMap的构造函数源代码就明白为什么了,以下为源代码:

Java代码  

  1. public HashMap(int initialCapacity, float loadFactor) {
  2. if (initialCapacity < 0)
  3. throw new IllegalArgumentException("Illegal initial capacity: "
  4. + initialCapacity);
  5. if (initialCapacity > MAXIMUM_CAPACITY)
  6. initialCapacity = MAXIMUM_CAPACITY;
  7. if (loadFactor <= 0 || Float.isNaN(loadFactor))
  8. throw new IllegalArgumentException("Illegal load factor: "
  9. + loadFactor);
  10. // Find a power of 2 >= initialCapacity
  11. int capacity = 1;
  12. while (capacity < initialCapacity)
  13. capacity <<= 1;
  14. this.loadFactor = loadFactor;
  15. threshold = (int) (capacity * loadFactor);
  16. table = new Entry[capacity];
  17. init();
  18. }

关键在于这两行:

Java代码  

  1. while (capacity < initialCapacity)
  2. capacity <<= 1;

如果initialCapacity(指定大小)大于capacity(原或初始化大小)时,就会不断循环进行位移赋值计算,相当于capacity=capacity *2.直至capacity 大于或等于我们指定的大小。如果指定的大小正好为2的N次幂时两个值便会相等,进而终止计算;如果指定大小不符合条件时,capacity 就会是刚好大于指定大小的那个2的N次幂的数。

所以,在上面我们指定大小为131,大于131并且为2的的N次幂的数就为256,所以此时就会按256来初始化table.

2.Entry 元素

与LinkedList类似,HashMap也是采用Entry内部类来存储实际元素信息,以下是Entry的源代码(省略部分代码):

Java代码  

  1. static class Entry<K, V> implements Map.Entry<K, V> {
  2. final K key;
  3. V value;
  4. Entry<K, V> next;
  5. final int hash;
  6. }

Entry中包括4个成员变量,其中key为键,value为值,next指向下一个节点元素,hash为hash值。Entry通过next属性可以寻找到下一个节点的元素,进而通过遍历就可以找到相应key下存储的信息。

3.HashMap设置元素

Map通过put方法来在Map实例中关联指定值与指定键。如果该实例已经包含了一个该键的映射关系,则旧值被替换。

示例如下:

Java代码  

  1. Map map = new HashMap();
  2. map.put("user1", "小明");
  3. map.put("user2", "小强");
  4. map.put("user3", "小红");
  5. System.out.println("user1:" + map.get("user1"));
  6. System.out.println("user2:" + map.get("user2"));
  7. System.out.println("user3:" + map.get("user3"));
  8. map.put("user2", "小龙");
  9. System.out.println("user1:" + map.get("user1"));
  10. //打印结果
  11. user1:小明
  12. user2:小强
  13. user3:小红
  14. user1:小明

首先,创建了一个HashMap的实例map,此时map实例中的table数组会默认初始化,创建一个长度为DEFAULT_INITIAL_CAPACITY=16的空数组。

然后,调用put方法将一对键、值(key,value)保存。当已存在Map实例中已存在指定key的映射时,会将新指定的value覆盖原value。

与LIst的相关实现add方法一样,HashMap的put方法是设置元素的入口,在put的过程中会进行一系列的判断与操作,所以只有将put方法理解透彻后HashMap的内部结构与机制才会更加清晰。

HashMap进行put操作时按以下步骤执行:

1)判断key是否为空,如果为空则调用设置null的专有方法。

2)计算key的hash值。

3)通过hash与table数组的长度计算出该元素所要放置的数组下标。

4)遍历该下标下的Entry元素链,如果找到与指定key相同的Entry则直接替换该Entry的value值并返回。

5)如果未找到则添加一个新元素至该下标下的元素链前端。

以下是一张官网上对于put操作流程的描述图片,可以作为参考:

以下是put方法的源代码,其中我已经加入了相关描述便于大家理解:

Java代码  

  1. /**
  2. * 设置指定值
  3. */
  4. public V put(K key, V value) {
  5. //1.首先判断key是否为null
  6. if (key == null)
  7. //如果为null则调用putForNullKey方法
  8. return putForNullKey(value);
  9. //2.计算key的hash值
  10. int hash = hash(key.hashCode());
  11. //3.根据计算后的hash值与table数组长度计算该key应放置到table数组的那个下标位置
  12. int i = indexFor(hash, table.length);
  13. //4.遍历该下标下的所有Entry,如果key已存在则覆盖该key所在Entry的value值
  14. for (Entry<K, V> e = table[i]; e != null; e = e.next) {
  15. Object k;
  16. if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {
  17. V oldValue = e.value;
  18. e.value = value;
  19. e.recordAccess(this);
  20. return oldValue;
  21. }
  22. }
  23. modCount++;
  24. //5.如果该key不存在则新添加Entry元素至数组指定位置,并且该Entry作为此下标元素链的头部
  25. addEntry(hash, key, value, i);
  26. return null;
  27. }

4.HashMap内部结构

通过对put方法的流程分析,我们基本已经了解HashMap其内部实现的机制与原理,那么来总结一下HashMap初始化及添加元素的过程(以默认值为例):

(1) 初始化HashMap实例,初始化其内部数组table:

Java代码  

  1. this.loadFactor = DEFAULT_LOAD_FACTOR;//0.75f
  2. threshold = (int)(DEFAULT_INITIAL_CAPACITY * DEFAULT_LOAD_FACTOR);//16*0.75=12
  3. table = new Entry[DEFAULT_INITIAL_CAPACITY];//16

此时table被初始化创建,长度为16。

(2) 当第一次put元素时,此时HashMap实例中并没有添加任何元素,所以put方法会直接调用addEntry方法:

Java代码  

  1. Entry<K,V> e = table[bucketIndex];
  2. table[bucketIndex] = new Entry<K,V>(hash, key, value, e);

首先,会先获取该下标(bucketIndex)下原Entry信息,因为table并未设置任何值,所以此时e为null。

然后,创建一个新的Entry实例,其next属性指向e,并将此实例赋值给table[bucketIndex]。

(3) 当更新HashMap实例中已有key的value内容时:

Java代码  

  1. for (Entry<K,V> e = table[i]; e != null; e = e.next) {
  2. Object k;
  3. if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {
  4. V oldValue = e.value;
  5. e.value = value;
  6. e.recordAccess(this);
  7. return oldValue;
  8. }
  9. }

如果HashMap实例中已经put了该key则只需遍历找到该节点Entry,更新其value并返回,所以更新已有key的操作不会调用addEntry方法。

(4) 此时HashMap实例的内部结构如下图所示:

        HashMap采用此种存储元素的方式是结合了ArrayList与LinkedList两者的优点,虽然单纯某项操作的性能上并不比二者之一高,但这种方式的好处就是存储与获取性能平稳,并不会出现剧烈波动的情况。

        5.HashMap获取元素

既然已经了解了HashMap的内部结构已经设置元素时的相关操作步骤,那么获取元素其实也就比较容易理解了,首先根据指定的key去计算数组下标,然后遍历该下标下的Entry链,最后返回。

以下是get方法的源代码,与put方法的基本流程大致相同:

Java代码  

  1. /**
  2. * 返回指定key的value
  3. */
  4. public V get(Object key) {
  5. // 1.判断可以是否为null
  6. if (key == null)
  7. return getForNullKey();
  8. // 2.计算key的hash值
  9. int hash = hash(key.hashCode());
  10. // 3.遍历table指定下标下的Entry链
  11. for (Entry<K, V> e = table[indexFor(hash, table.length)]; e != null; e = e.next) {
  12. Object k;
  13. // 4.如果找到则返回该Entry的value
  14. if (e.hash == hash && ((k = e.key) == key || key.equals(k)))
  15. return e.value;
  16. }
  17. // 5.未找到则返回null
  18. return null;
  19. }

        6.HashMap移除元素

HashMap实现了Map接口的remove方法,所以可以通过remove方法移除已经添加的元素:

Java代码  

  1. Map map = new HashMap();
  2. map.put("user1", "小明");
  3. map.put("user2", "小强");
  4. map.put("user3", "小红");
  5. map.remove("user2");
  6. System.out.println("user1:" + map.get("user1"));
  7. System.out.println("user2:" + map.get("user2"));
  8. System.out.println("user3:" + map.get("user3"));
  9. //打印结果:
  10. user1:小明
  11. user2:null
  12. user3:小红

当主动调用remove方法时,会根据指定的key删除该节点元素。

以下是remove方法的源代码:

Java代码  

  1. /**
  2. * 删除指定key下内容
  3. */
  4. public V remove(Object key) {
  5. Entry<K, V> e = removeEntryForKey(key);
  6. return (e == null ? null : e.value);
  7. }
  8. /**
  9. * 根据指定key删除元素
  10. */
  11. final Entry<K, V> removeEntryForKey(Object key) {
  12. int hash = (key == null) ? 0 : hash(key.hashCode());
  13. int i = indexFor(hash, table.length);
  14. Entry<K, V> prev = table[i];
  15. Entry<K, V> e = prev;
  16. while (e != null) {
  17. Entry<K, V> next = e.next;
  18. Object k;
  19. if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k)))) {
  20. modCount++;
  21. size--;
  22. if (prev == e)
  23. table[i] = next;
  24. else
  25. prev.next = next;
  26. e.recordRemoval(this);
  27. return e;
  28. }
  29. prev = e;
  30. e = next;
  31. }
  32. return e;
  33. }

remove方法调用了另一个方法removeEntryForKey,removeEntryForKey方法会循环遍历指定下标下所有Entry节点元素,如果该key存在则修改该节点前一个节点的next指向,从而达到把该Entry节点移除Entry链的目的。

注意HashMap的remove操作一样不会引起“减容”操作,这样就不会影响性能。

        7.HashMap的遍历

通常情况下Map的使用者清楚该Map实例中有那些key,通过get(key)方法就可以直接将所有元素取出,但某些情况下这种做法产生的代码将是一次性代码,无法共用。

HashMap的遍历通常采用以下几种方式:

1)通过entrySet()方法可以获取HashMap实例所有Entry的Set返回,所以通过entrySet方法返回并迭代可以获取所有Entry元素:

Java代码  

  1. Map map = new HashMap();
  2. map.put("user1", "小明");
  3. map.put("user2", "小强");
  4. map.put("user3", "小红");
  5. Iterator iter = map.entrySet().iterator();
  6. while (iter.hasNext()) {
  7. Map.Entry entry = (Map.Entry) iter.next();
  8. Object key = entry.getKey();
  9. Object value = entry.getValue();
  10. System.out.println("key:" + key + ";value:" + value);
  11. // 然后移除元素
  12. if (key.toString().equals("user1")) {
  13. iter.remove();
  14. } else if (key.toString().equals("user2")) {
  15. entry.setValue("小海");
  16. }
  17. }
  18. System.out.println(map.get("user1"));
  19. System.out.println(map.get("user2"));
  20. System.out.println(map.get("user3"));
  21. // 打印结果:
  22. key:user2;value:小强
  23. key:user1;value:小明
  24. key:user3;value:小红
  25. null
  26. 小海
  27. 小红

此种方式操作简单,代码量少,效率较高,且可以直接操作元素,是常用的手段之一。

2)Map还提供了keySet方法,用于返回所有key的Set形式,然后迭代此Set再通过get方法就可以获取相应元素的value:

Java代码  

  1. Map map = new HashMap();
  2. map.put("user1", "小明");
  3. map.put("user2", "小强");
  4. map.put("user3", "小红");
  5. Iterator iter = map.keySet().iterator();
  6. while (iter.hasNext()) {
  7. Object key = iter.next();
  8. Object value = map.get(key);
  9. System.out.println("key:" + key + ";value:" + value);
  10. // 然后移除元素
  11. if (key.toString().equals("user1")) {
  12. iter.remove();
  13. }
  14. }
  15. System.out.println(map.get("user1"));
  16. System.out.println(map.get("user2"));
  17. System.out.println(map.get("user3"));
  18. // 打印结果:
  19. key:user2;value:小强
  20. key:user1;value:小明
  21. key:user3;value:小红
  22. null
  23. 小强
  24. 小红

此种方式先需要将所有key遍历后返回,再通过get方法来获取元素,如果单纯需要操作Map实例中的个别节点元素时效率尚可,如果需要大规模获取和修改时效率不如第一种。所以两种方式选择那种需要视情况而言,并没有绝对。

3)通过values方法直接返回所有value:

Java代码  

  1. Map map = new HashMap();
  2. map.put("user1", "小明");
  3. map.put("user2", "小强");
  4. map.put("user3", "小红");
  5. //转换成数组
  6. String[] names= (String[]) map.values().toArray(new String[map.size()]);
  7. for (String name : names){
  8. System.out.println(name);
  9. }
  10. //采用迭代
  11. Collection nameArray =  map.values();
  12. Iterator iter = nameArray.iterator();
  13. while (iter.hasNext()) {
  14. String name=iter.next().toString();
  15. System.out.println(name);
  16. }
  17. // 打印结果:
  18. 小强
  19. 小明
  20. 小红

此种方式简单明了,适用于直接获取所有value的情况,可以直接迭代或者转换成数组,当直接显示value的情况下比较适用。

HashMap的基本结构及内部实现原理至此已经比较清晰,下一篇着重来了解下HashMap其内部几种算法的原理及相关性能。

http://286.iteye.com/blog/2187873

时间: 2024-10-15 01:35:06

Map实现之HashMap(结构及原理)(转)的相关文章

HashMap的实现原理和底层结构

哈希表(hash table)也叫散列表,是一种非常重要的数据结构,应用场景及其丰富,许多缓存技术(比如memcached)的核心其实就是在内存中维护一张大的哈希表,而HashMap的实现原理也常常出现在各类的面试题中,重要性可见一斑.本文会对java集合框架中的对应实现HashMap的实现原理进行讲解,然后会对JDK7的HashMap源码进行分析. 一.什么是哈希表 在讨论哈希表之前,我们先大概了解下其他数据结构在新增,查找等基础操作执行性能 数组:采用一段连续的存储单元来存储数据.对于指定下

从头认识java-15.7 Map(4)-介绍HashMap的工作原理-hash碰撞(经常作为面试题)

这一章节我们来讨论一下hash碰撞. 1.什么是hash碰撞? 就是两个对象的key的hashcode是一样的,这个时候怎么get他的value呢? 答案是通过equals遍历table那个位置上面的Entry链表. 2.例子 正常的例子: package com.ray.ch14; import java.util.HashMap; public class Test { public static void main(String[] args) { HashMap<Person, Dog>

从头认识java-15.7 Map(2)-介绍HashMap的工作原理-put方法

这一章节我们来介绍HashMap的工作原理. 1.HashMap的工作原理图 下图引用自:http://www.admin10000.com/document/3322.html 2.HashMap初始化的时候我们可以这样理解:一个数组,每一个位置存储的是一个链表,链表里面的每一个元素才是我们记录的元素 3.下面我们来看put的源码: public V put(K key, V value) { if (key == null) return putForNullKey(value); int

HashMap的工作原理

这是一节让你深入理解hash_map的介绍,如果你只是想囫囵吞枣,不想理解其原理,你倒是可以略过这一节,但我还是建议你看看,多了解一些没有坏处. hash_map基于hash table(哈希表).哈希表最大的优点,就是把数据的存储和查找消耗的时间大大降低,几乎可以看成是常数时间:而代价仅仅是消耗比较多的内存.然而在当前可利用内存越来越多的情况下,用空间换时间的做法是值得的.另外,编码比较容易也是它的特点之一. 其基本原理是:使用一个下标范围比较大的数组来存储元素.可以设计一个函数(哈希函数,也

HashMap的实现原理 HashMap底层实现,hashCode如何对应bucket?

韩梦飞沙  韩亚飞  [email protected]  yue31313  han_meng_fei_sha 数组和链表组合成的链表散列结构,通过hash算法,尽量将数组中的数据分布均匀,如果hashcode相同再比较equals方法,如果equals方法返回false,那么就将数据以链表的形式存储在数组的对应位置,并将之前在该位置的数据往链表的后面移动,并记录一个next属性,来指示后移的那个数据.注意数组中保存的是entry,其中保存的是键值. ======= HashMap的数据结构是

Java中HashMap底层实现原理(JDK1.8)源码分析

这几天学习了HashMap的底层实现,但是发现好几个版本的,代码不一,而且看了Android包的HashMap和JDK中的HashMap的也不是一样,原来他们没有指定JDK版本,很多文章都是旧版本JDK1.6.JDK1.7的.现在我来分析一哈最新的JDK1.8的HashMap及性能优化. 在JDK1.6,JDK1.7中,HashMap采用位桶+链表实现,即使用链表处理冲突,同一hash值的链表都存储在一个链表里.但是当位于一个桶中的元素较多,即hash值相等的元素较多时,通过key值依次查找的效

深入Java集合学习系列:HashMap的实现原理

参考文献 引用文献:深入Java集合学习系列:HashMap的实现原理,大部分参考这篇博客,只对其中进行稍微修改 自己曾经写过的:Hashmap实现原理 1. HashMap概述: HashMap是基于哈希表的Map接口的非同步实现(Hashtable跟HashMap很像,唯一的区别是Hashtalbe中的方法是线程安全的,也就是同步的).此实现提供所有可选的映射操作,并允许使用null值和null键.此类不保证映射的顺序,特别是它不保证该顺序恒久不变. 2. HashMap的数据结构: 在ja

HashMap底层实现原理/HashMap与HashTable区别/HashMap与HashSet区别

Hash算法 Hash,一般翻译做"散列",也有直接音译为"哈希"的,就是把任意长度的输入(又叫做预映射, pre-image),通过散列算法,变换成固定长度的输出,该输出就是散列值.这种转换是一种压缩映射,也就是,散列值的空间通常远小于输入的空间,不同的输入可能会散列成相同的输出,而不可能从散列值来唯一的确定输入值.简单的说就是一种将任意长度的消息压缩到某一固定长度的消息摘要的函数. HASH主要用于信息安全领域中加密算法,它把一些不同长度的信息转化成杂乱的128

转载: HashMap的工作原理

摘要 HashMap在java项目中占有举足轻重的地位,所以了解HashMap的工作原理很有必要. 1.前言 在探讨HashMap源码之前,先说一下HashCode,为什么呢?因为HashMap有一个特性是Key是唯一值,如何确定key的唯 一性呢,这就用到了hash算法.在HashMap(jdk1.7)的put方法实现中首先利用了hash()生成key的hashCode,然后比较 key的hashCode是否已经存在集合,如果不存在,就插入到集合,如果已存在,则返回null. 1.1 hash

Java中HashMap的实现原理

最近面试中被问及Java中HashMap的原理,瞬间无言以对,因此痛定思痛觉得研究一番. 一.Java中的hashCode和equals 1.关于hashCode hashCode的存在主要是用于查找的快捷性,如Hashtable,HashMap等,hashCode是用来在散列存储结构中确定对象的存储地址的 如果两个对象相同,就是适用于equals(java.lang.Object) 方法,那么这两个对象的hashCode一定要相同 如果对象的equals方法被重写,那么对象的hashCode也