`
g21121
  • 浏览: 685512 次
  • 性别: Icon_minigender_1
  • 来自: 北京
社区版块
存档分类
最新评论

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的成员变量:

/**
 * 初始默认容量(必须为2的幂次方)
 */
static final int DEFAULT_INITIAL_CAPACITY = 16;

/**
 * 最大容量,如果被指定为一个更高的值必须为2的幂次方,并且小于1073741824.(1<<30)
 */
static final int MAXIMUM_CAPACITY = 1 << 30;

/**
 * 默认负载因子/负载系数
 */
static final float DEFAULT_LOAD_FACTOR = 0.75f;

/**
 * 内部实现表, 必要时调整大小,其长度亦为2的幂次方
 */
transient Entry[] table;

/**
 * map中添加的元素个数
 */
transient int size;

/**
 * 扩容临界值,当size达到此值时进行扩容 (容量乘以负载因子).
 */
int threshold;

/**
 * 内部实现表的负载因子
 */
final float loadFactor;

/**
 * 操作数,可以理解为map实例被操作的次数,包括添加,删除等等
 */
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的幂次方时,如下:

Map map=new HashMap(131);

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

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

public HashMap(int initialCapacity, float loadFactor) {
	if (initialCapacity < 0)
		throw new IllegalArgumentException("Illegal initial capacity: "
				+ initialCapacity);
	if (initialCapacity > MAXIMUM_CAPACITY)
		initialCapacity = MAXIMUM_CAPACITY;
	if (loadFactor <= 0 || Float.isNaN(loadFactor))
		throw new IllegalArgumentException("Illegal load factor: "
				+ loadFactor);

	// Find a power of 2 >= initialCapacity
	int capacity = 1;
	while (capacity < initialCapacity)
		capacity <<= 1;

	this.loadFactor = loadFactor;
	threshold = (int) (capacity * loadFactor);
	table = new Entry[capacity];
	init();
}

        关键在于这两行:

while (capacity < initialCapacity)
	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的源代码(省略部分代码):

static class Entry<K, V> implements Map.Entry<K, V> {
	final K key;
	V value;
	Entry<K, V> next;
	final int hash;
}

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

 

        3.HashMap设置元素

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

        示例如下:

Map map = new HashMap();
map.put("user1", "小明");
map.put("user2", "小强");
map.put("user3", "小红");
System.out.println("user1:" + map.get("user1"));
System.out.println("user2:" + map.get("user2"));
System.out.println("user3:" + map.get("user3"));
map.put("user2", "小龙");
System.out.println("user1:" + map.get("user1"));
//打印结果
user1:小明
user2:小强
user3:小红
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方法的源代码,其中我已经加入了相关描述便于大家理解:

/**
 * 设置指定值
 */
public V put(K key, V value) {
	//1.首先判断key是否为null
	if (key == null)
		//如果为null则调用putForNullKey方法
		return putForNullKey(value);
	//2.计算key的hash值
	int hash = hash(key.hashCode());
	//3.根据计算后的hash值与table数组长度计算该key应放置到table数组的那个下标位置
	int i = indexFor(hash, table.length);
	//4.遍历该下标下的所有Entry,如果key已存在则覆盖该key所在Entry的value值
	for (Entry<K, V> e = table[i]; e != null; e = e.next) {
		Object k;
		if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {
			V oldValue = e.value;
			e.value = value;
			e.recordAccess(this);
			return oldValue;
		}
	}

	modCount++;
	//5.如果该key不存在则新添加Entry元素至数组指定位置,并且该Entry作为此下标元素链的头部
	addEntry(hash, key, value, i);
	return null;
}
 
        4.HashMap内部结构
        通过对put方法的流程分析,我们基本已经了解HashMap其内部实现的机制与原理,那么来总结一下HashMap初始化及添加元素的过程(以默认值为例):
        (1) 初始化HashMap实例,初始化其内部数组table:
this.loadFactor = DEFAULT_LOAD_FACTOR;//0.75f
threshold = (int)(DEFAULT_INITIAL_CAPACITY * DEFAULT_LOAD_FACTOR);//16*0.75=12
table = new Entry[DEFAULT_INITIAL_CAPACITY];//16
        此时table被初始化创建,长度为16。
        (2) 当第一次put元素时,此时HashMap实例中并没有添加任何元素,所以put方法会直接调用addEntry方法:
Entry<K,V> e = table[bucketIndex];
table[bucketIndex] = new Entry<K,V>(hash, key, value, e);
        首先,会先获取该下标(bucketIndex)下原Entry信息,因为table并未设置任何值,所以此时e为null。
        然后,创建一个新的Entry实例,其next属性指向e,并将此实例赋值给table[bucketIndex]。
        (3) 当更新HashMap实例中已有key的value内容时:
for (Entry<K,V> e = table[i]; e != null; e = e.next) {
    Object k;
    if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {
        V oldValue = e.value;
        e.value = value;
        e.recordAccess(this);
        return oldValue;
    }
}
        如果HashMap实例中已经put了该key则只需遍历找到该节点Entry,更新其value并返回,所以更新已有key的操作不会调用addEntry方法。
        (4) 此时HashMap实例的内部结构如下图所示:


        HashMap采用此种存储元素的方式是结合了ArrayList与LinkedList两者的优点,虽然单纯某项操作的性能上并不比二者之一高,但这种方式的好处就是存储与获取性能平稳,并不会出现剧烈波动的情况。
 
        5.HashMap获取元素
        既然已经了解了HashMap的内部结构已经设置元素时的相关操作步骤,那么获取元素其实也就比较容易理解了,首先根据指定的key去计算数组下标,然后遍历该下标下的Entry链,最后返回。
        以下是get方法的源代码,与put方法的基本流程大致相同:
/**
 * 返回指定key的value
 */
public V get(Object key) {
	// 1.判断可以是否为null
	if (key == null)
		return getForNullKey();
	// 2.计算key的hash值
	int hash = hash(key.hashCode());
	// 3.遍历table指定下标下的Entry链
	for (Entry<K, V> e = table[indexFor(hash, table.length)]; e != null; e = e.next) {
		Object k;
		// 4.如果找到则返回该Entry的value
		if (e.hash == hash && ((k = e.key) == key || key.equals(k)))
			return e.value;
	}
	// 5.未找到则返回null
	return null;
}

        6.HashMap移除元素
        HashMap实现了Map接口的remove方法,所以可以通过remove方法移除已经添加的元素:
Map map = new HashMap();
map.put("user1", "小明");
map.put("user2", "小强");
map.put("user3", "小红");
map.remove("user2");
System.out.println("user1:" + map.get("user1"));
System.out.println("user2:" + map.get("user2"));
System.out.println("user3:" + map.get("user3"));
//打印结果:
user1:小明
user2:null
user3:小红
        当主动调用remove方法时,会根据指定的key删除该节点元素。
        以下是remove方法的源代码:
/**
 * 删除指定key下内容
 */
public V remove(Object key) {
	Entry<K, V> e = removeEntryForKey(key);
	return (e == null ? null : e.value);
}

/**
 * 根据指定key删除元素
 */
final Entry<K, V> removeEntryForKey(Object key) {
	int hash = (key == null) ? 0 : hash(key.hashCode());
	int i = indexFor(hash, table.length);
	Entry<K, V> prev = table[i];
	Entry<K, V> e = prev;

	while (e != null) {
		Entry<K, V> next = e.next;
		Object k;
		if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k)))) {
			modCount++;
			size--;
			if (prev == e)
				table[i] = next;
			else
				prev.next = next;
			e.recordRemoval(this);
			return e;
		}
		prev = e;
		e = next;
	}

	return e;
}
        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元素:
Map map = new HashMap();
map.put("user1", "小明");
map.put("user2", "小强");
map.put("user3", "小红");
Iterator iter = map.entrySet().iterator();
while (iter.hasNext()) {
	Map.Entry entry = (Map.Entry) iter.next();
	Object key = entry.getKey();
	Object value = entry.getValue();
	System.out.println("key:" + key + ";value:" + value);
	// 然后移除元素
	if (key.toString().equals("user1")) {
		iter.remove();
	} else if (key.toString().equals("user2")) {
		entry.setValue("小海");
	}

}
System.out.println(map.get("user1"));
System.out.println(map.get("user2"));
System.out.println(map.get("user3"));

// 打印结果:
key:user2;value:小强
key:user1;value:小明
key:user3;value:小红
null
小海
小红
        此种方式操作简单,代码量少,效率较高,且可以直接操作元素,是常用的手段之一。
        2)Map还提供了keySet方法,用于返回所有key的Set形式,然后迭代此Set再通过get方法就可以获取相应元素的value:
Map map = new HashMap();
map.put("user1", "小明");
map.put("user2", "小强");
map.put("user3", "小红");
Iterator iter = map.keySet().iterator();
while (iter.hasNext()) {
	Object key = iter.next();
	Object value = map.get(key);
	System.out.println("key:" + key + ";value:" + value);
	// 然后移除元素
	if (key.toString().equals("user1")) {
		iter.remove();
	}
}
System.out.println(map.get("user1"));
System.out.println(map.get("user2"));
System.out.println(map.get("user3"));

// 打印结果:
key:user2;value:小强
key:user1;value:小明
key:user3;value:小红
null
小强
小红
        此种方式先需要将所有key遍历后返回,再通过get方法来获取元素,如果单纯需要操作Map实例中的个别节点元素时效率尚可,如果需要大规模获取和修改时效率不如第一种。所以两种方式选择那种需要视情况而言,并没有绝对。
        3)通过values方法直接返回所有value:
Map map = new HashMap();
map.put("user1", "小明");
map.put("user2", "小强");
map.put("user3", "小红");
//转换成数组
String[] names= (String[]) map.values().toArray(new String[map.size()]);
for (String name : names){
	System.out.println(name);
}
//采用迭代
Collection nameArray =  map.values();

Iterator iter = nameArray.iterator();

while (iter.hasNext()) {

 String name=iter.next().toString();

 System.out.println(name);

}
// 打印结果:
小强
小明
小红
        此种方式简单明了,适用于直接获取所有value的情况,可以直接迭代或者转换成数组,当直接显示value的情况下比较适用。
        HashMap的基本结构及内部实现原理至此已经比较清晰,下一篇着重来了解下HashMap其内部几种算法的原理及相关性能。
8
1
分享到:
评论
1 楼 夏日娃 2015-03-04  

相关推荐

    图解hashMap工作原理

    hashMap基本工作原理,图解分析,基础Map集合

    HashMap底层原理.pdf

    大家在面试中,最常见的问题肯定包含对hashmap相关问题,源码、多线程安全、1.7和1.8区别等等,本文详细总结了以上问题,希望对你有帮助!!

    HashMap-面试必过

    1.说一下 HashMap 的实现原理? 2.HashMap在JDK1.7和JDK1.8中有哪些不同?HashMap的底层实现? 3.HashMap的put方法的具体流程? 4.HashMap的扩容操作是怎么实现的? 5.HashMap是怎么解决哈希冲突的? 6.什么是哈希?...

    java集合类原理面试题

    Map接口有哪些实现类? 描述一下Map put的过程 如何得到一个线程安全的Map? HashMap有什么特点? ConcurrentHashMap是怎么分段分组的? ConcurrentHashMap是怎么分段分组的? 介绍LinkedHashMap的底层原理 请介绍...

    java中HashMap的原理分析

    HashMap在Java开发中有着非常重要的角色地位,每一个Java程序员都应该了解HashMap。详细地阐述HashMap中的几个概念,并深入探讨HashMap的内部结构和实现细节,讨论HashMap的性能问题

    Golang 语言map底层实现原理解析

    在开发过程中,map是必不可少的数据结构,在Golang中,使用map或多或少会遇到与其他语言不一样的体验,比如访问不存在的元素会返回其类型的空值、map的大小究竟是多少,为什么会报”cannot take the address of”...

    HashMap(二)原理讲解

    1.HashMap的继承体系是怎么样的? 2.Node数据结构的分析? Node的成员变量 final int hash; final K key; V value; Node next; hash:存放哈希值 key、value:就是map.put(key,value) next:哈希碰撞后形成的...

    java7hashmap源码-JAVA-:JAVA-

    定义的这个数据结构中,如果每次都hashMap.put(string, new hashtable&lt;&gt;)的话会覆盖掉之前的hashtable,所以最好先定义hashtable,存储完成后再放入map中,或者hashtable先get 出value再put进入新value。 5 new ...

    阿里面试经历感想回顾总结

    Java的数据结构相关的类实现原理,比如LinkedList,ArrayList,HashMap, TreeMap这一类的。以下简单模拟一个数据结构的连环炮。 比如,面试官先问你HashMap是不是有序的? 你肯定回答说,不是有序的。那面试官就会...

    java高级工程师、技术专家、架构师、项目经理面试宝典.rar

    类装载器(ClassLoader)(用来装载.class文件) 执行引擎(执行字节码,或者执行本地方法) 运行时数据区(方法区、堆、java栈、PC寄存器...3.Map或者HashMap的存储原理 答:HashMap是由数组+链表的一个结构组成。

    java面试宝典

    75、socket通信(tcp/udp区别及JAVA的实现方式) 18 76、什么是java序列化,如何实现java序列化? 18 77、简述synchronized和java.util.concurrent.locks.Lock的异同 ? 18 78、abstract class Name { private ...

    Backend_development:JAVA进阶代码实例&最新面试题(看完涨薪2k+)

    #2019--JAVA 新征程 看完涨薪2K+ ——&gt;必刷面试题 面试题模块结构图 11. 抽象类必须要有抽象方法...23.说一下 HashMap 的实现原理? 24.说一下 HashSet 的实现原理? 25.rrayList 和 LinkedList 的区别是什么? 26.如何

    JAVA面试题最全集

    写一个方法,实现字符串的反转,如:输入abc,输出cba 写一个方法,实现字符串的替换,如:输入bbbwlirbbb,输出bbbhhtccc。 3.数据类型之间的转换 如何将数值型字符转换为数字(Integer,Double) 如何将数字...

    sesvc.exe 阿萨德

    开始的时候就提到 HashMap 是由数组和链表组成,所以这个 next 就是用于实现链表结构。 hash 存放的是当前 key 的 hashcode。 知晓了基本结构,那来看看其中重要的写入、获取函数: put 方法 public V put(K key, V...

    java面试常见基础(深层次,高级研发)

    14. Map数据结构? 35 14.1. 一、定义 36 14.2. 二、构造函数 36 14.3. 三、数据结构 36 14.4. 四、存储实现:put(key,vlaue) 38 14.5. 五、读取实现:get(key) 41 15. 一百万数据放Arraylist数组,怎么放? 在哪个...

    java基础案例与开发详解案例源码全

    6.8.1 接口的定义及实现174 6.8.2 接口中的常量174 6.8.3 接口的多重实现174 6.9 本章练习175 第7章 7.1 面向对象的分析与设计简介180 7.1.1 类的设计建议180 7.1.2 类名.变量名.方法名的选取181 7.1.3 类的属性设计...

    千方百计笔试题大全

    75、socket通信(tcp/udp区别及JAVA的实现方式) 18 76、什么是java序列化,如何实现java序列化? 18 77、简述synchronized和java.util.concurrent.locks.Lock的异同 ? 18 78、abstract class Name { private ...

    java内核源码-JavaCompass:「Java指南针」为你学习Java指明方向。内容涵盖互联网Java工程师所需要掌握的核心知识,涉及J

    java内核源码 Java指南针 基础核心 基础知识 反射 泛型 动态代理 JDK8新特性 集合容器 多线程与并发 ...数据结构与算法 ...基础数据结构 ...排序及其源码实现 ...高级数据结构 ...二分&HashMap ...SQL执行原理详解 ...并发Map、Lis

    jdk1.8_64.7z

    解压缩版,不是安装版 jdk1.8 中对集合的底层结构做了调整。 如HashMap从1.7的数据+链表的形式调整为数据+链表+红黑树。 ConcurrentHashMap从分段机制+数组+链表+红黑树...这里先简要记录,后续会详解Map的原理与区别。

Global site tag (gtag.js) - Google Analytics