`

HashMap实现原理之我的理解

 
阅读更多
开写之前,我想先问两个问题:为什么是数据结构,什么是算法?
【Java数据结构和算法(第二版)】写道:

 

数据结构是指对内存或硬盘中数据的一种安排。可以理解成数据存放或排列时的结构,如数组,链表,二叉树,哈希表等。而算法则是对数据结构中数据进行的处理。如查找,排序。

 

 

HashMap[键值对存储]ArrayList[顺序存储]Java集合中使用频率最高的两个集合。经常我们都会自然的使用它们来存放数据,然后用Java JDK自带方法来操作他们(添加,修改,删除,迭代等)来实现我们的业务逻辑。

在多线程环境下,需要考虑数据安全性,应该优先考虑其同步类,如HashTableVector。ConcurrentHashMap也值得推荐。但是如果不涉及修改和删除,仅有只读的情况下,使用HashMap将会使性能大大提升。

 

一.HashMap的数据结构。

数组的特点是:寻址容易,插入和删除困难;而链表的特点是:寻址困难,插入和删除容易。HashMap则是数组和链表的综合体,具有两者的优点,被称为链表的数组。

哈希算法是为了能够进行快速查找定位所使用的的一种技术。

哈希表是一个以空间换时间的数据结构。 

下图描述了HashMap底层的存储结构,为一个默认长度为16的数组,数组的每个元素存放的是一个链表。例如496%16=0,则存放在index=0的第一个位置,而896%16=0,也是存放在数组index=0的位置,由于第一个位置已经被496占据了,则存放在第二个位置,依次链式存储。【链地址法:java中解决hash冲突的方法】

 

还有个办法可以很清晰的看到HashMap内部结构。在Eclipse下写个测试类Debug下,inspect查看map对象,可看到各个属性的值。默认长度为16,加载因子为0.75,当前存放4个键值对。

map默认打印enrySet中键值对情况。

而选中table属性,更可以看到键值对的散列情况。

 

二.HashMap如何存放数据。

 查看JDK中源码:

    /**
     * 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 <tt>key</tt>, or
     *         <tt>null</tt> if there was no mapping for <tt>key</tt>.
     *         (A <tt>null</tt> return can also indicate that the map
     *         previously associated <tt>null</tt> with <tt>key</tt>.)
     */
    public V put(K key, V value) {
        if (key == null)
            return putForNullKey(value);
        int hash = hash(key.hashCode());
        int i = indexFor(hash, table.length);
        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++;
        addEntry(hash, key, value, i);
        return null;
    }
可以看到,当map键值对put进来的时候,如果key为空,则直接放在table[0]处。

不为空,则根据hash(key.hashCode())table的长度(默认16)的模。来确定该键值对所在table中的索引。然后遍历该索引处table[i]所对应的Entry链表,根据key的二次hash值和keyequals()的值,来判断是否已经存在该键值对。如果存在则覆盖原键值对的value,如果没有则添加至链表的最后。

 

HashMap如何读取数据。

查看JDK中源码:

    /**
     * Returns the value to which the specified key is mapped,
     * or {@code null} if this map contains no mapping for the key.
     *
     * <p>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.)
     *
     * <p>A return value of {@code null} does not <i>necessarily</i>
     * 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) {
        if (key == null)
            return getForNullKey();
        int hash = hash(key.hashCode());
        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.equals(k)))
                return e.value;
        }
        return null;
    }
 

可以看到,map在通过keyget对应的value时,和存储时很类似。当keynull时,直接去table[0]的链表去获得keynullvalue。不为空则根据二次hash值找到对应的索引,再在索引所在的链表中,通过先前的二次hash值和keyequal方法来确定链表节点,如果找到,则返回该value 

 

四.一些常见问题。

1.  两个对象比较equals()true,则hashCode也相同吗? 反之呢?

回答:不一定。在没有重写hashCode方法的前提下,他们比较的Object里的hashCode返回的值,如果重写了,则是使用自定义的hashCode方法的返回值。如果两个Stringequalstrue则他们hashCode相同,而实际项目中,我们会覆盖hashCode方法来重写我们业务的判断逻辑,这样即使equals相等,hashCode也不同。

反之,hashCode相同,equals比较一定相等。因为equals比较的依据就是hashCode的值。

 

2.  如何定位一个数值的index

回答:hashMap的处理方法是,将key二次hash之后的值,与table的长度进行取模运算。F返回值即为当前tableindex

       
        int hash = hash(key.hashCode());
        int i = indexFor(hash, table.length);
        //。。。。。。
    /**
     * Returns index for hash code h.
     */
    static int indexFor(int h, int length) {
        return h & (length-1);
    }
 

 

3.  HashMap存放数据,是否会发生数据冲撞?

回答:会。当他们对象的hashcode相同时,他们会保存在同一个table[i]的同一个索引上,此时发生碰撞。HashMap拥有一个LinkList机制,将发生碰撞的值以链表形式存储在同一table[i]上。

 

4.  hashMap是否会无限存放数据?

回答:会。当table的容量达到当前容量*加载因子(默认的capacity * load factor16*0.75)是,table的容量会增加一倍,来应对存储量的扩增。

    /**
     * Adds a new entry with the specified key, value and hash code to
     * the specified bucket.  It is the responsibility of this
     * method to resize the table if appropriate.
     *
     * Subclass overrides this to alter the behavior of put method.
     */
    void addEntry(int hash, K key, V value, int bucketIndex) {
	Entry<K,V> e = table[bucketIndex];
        table[bucketIndex] = new Entry<K,V>(hash, key, value, e);
        if (size++ >= threshold)
            resize(2 * table.length);
    }

 

5.  hashmap如何散列数值的?取模为什么是16,而不是15

回答:hashmap里的hash算法设计的目的是使元素的排列尽量分散,也就是散列。尽量使得每个位置上的元素数量只有一个。此算法加入了高位计算,防止低位不变,高位变化时,造成的hash冲突。

    /**
     * Applies a supplemental hash function to a given hashCode, which
     * defends against poor quality hash functions.  This is critical
     * because HashMap uses power-of-two length hash tables, that
     * otherwise encounter collisions for hashCodes that do not differ
     * in lower bits. Note: Null keys always map to hash 0, thus index 0.
     */
    static int hash(int h) {
        // This function ensures that hashCodes that differ only by
        // constant multiples at each bit position have a bounded
        // number of collisions (approximately 8 at default load factor).
        h ^= (h >>> 20) ^ (h >>> 12);
        return h ^ (h >>> 7) ^ (h >>> 4);
    }

    /**
     * Returns index for hash code h.
     */
    static int indexFor(int h, int length) {
        return h & (length-1);
    }

 

length总是2n次方时,h& (length-1)运算等价于对length取模,也就是h%length,但是&%具有更高的效率。只有是2n次方,才能保证每次二进制与运算之后的结果笼罩所有的数。

 

6.  何如合理设计一个hashMap

 

 自己设计一个HashMap

 代码copy至http://www.cnblogs.com/xwdreamer/archive/2012/05/14/2499339.html

基本与JDK源码一致:

Entry.java

public class Entry<K, V> {
	final K key;
	V value;
	Entry<K, V> next;// 下一个结点

	// 构造函数
	public Entry(K k, V v, Entry<K, V> n) {
		key = k;
		value = v;
		next = n;
	}

	public final K getKey() {
		return key;
	}

	public final V getValue() {
		return value;
	}

	public final V setValue(V newValue) {
		V oldValue = value;
		value = newValue;
		return oldValue;
	}

	public final boolean equals(Object o) {
		if (!(o instanceof Entry))
			return false;
		Entry e = (Entry) o;
		Object k1 = getKey();
		Object k2 = e.getKey();
		if (k1 == k2 || (k1 != null && k1.equals(k2))) {
			Object v1 = getValue();
			Object v2 = e.getValue();
			if (v1 == v2 || (v1 != null && v1.equals(v2)))
				return true;
		}
		return false;
	}

	public final int hashCode() {
		return (key == null ? 0 : key.hashCode())
				^ (value == null ? 0 : value.hashCode());
	}

	public final String toString() {
		return getKey() + "=" + getValue();
	}

}

 MyHashMap.java

public class MyHashMap<K, V> {
	private Entry[] table;// Entry数组表
	static final int DEFAULT_INITIAL_CAPACITY = 16;// 默认数组长度
	private int size;

	// 构造函数
	public MyHashMap() {
		table = new Entry[DEFAULT_INITIAL_CAPACITY];
		size = DEFAULT_INITIAL_CAPACITY;
	}

	// 获取数组长度
	public int getSize() {
		return size;
	}

	// 求index
	static int indexFor(int h, int length) {
		return h % (length - 1);
	}

	// 获取元素
	public V get(Object key) {
		if (key == null)
			return null;
		int hash = key.hashCode();// key的哈希值
		int index = indexFor(hash, table.length);// 求key在数组中的下标
		for (Entry<K, V> e = table[index]; e != null; e = e.next) {
			Object k = e.key;
			if (e.key.hashCode() == hash && (k == key || key.equals(k)))
				return e.value;
		}
		return null;
	}

	// 添加元素
	public V put(K key, V value) {
		if (key == null)
			return null;
		int hash = key.hashCode();
		int index = indexFor(hash, table.length);

		// 如果添加的key已经存在,那么只需要修改value值即可
		for (Entry<K, V> e = table[index]; e != null; e = e.next) {
			Object k = e.key;
			if (e.key.hashCode() == hash && (k == key || key.equals(k))) {
				V oldValue = e.value;
				e.value = value;
				return oldValue;// 原来的value值
			}
		}
		// 如果key值不存在,那么需要添加
		Entry<K, V> e = table[index];// 获取当前数组中的e
		table[index] = new Entry<K, V>(key, value, e);// 新建一个Entry,并将其指向原先的e
		return null;
	}

}

 MyHashMapTest.java

public class MyHashMapTest {

	public static void main(String[] args) {

		MyHashMap<Integer, Integer> map = new MyHashMap<Integer, Integer>();
		map.put(1, 90);
		map.put(2, 95);
		map.put(17, 85);

		System.out.println(map.get(1));
		System.out.println(map.get(2));
		System.out.println(map.get(17));
		System.out.println(map.get(null));
	}
}

 

 
  • 大小: 57.2 KB
  • 大小: 16.6 KB
  • 大小: 18.6 KB
分享到:
评论

相关推荐

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

    在Java编程语言中,集合框架是开发者日常工作中不可或缺的一部分,HashMap作为其中的重要成员,它的实现原理对于理解Java性能优化和数据结构有深远的意义。HashMap是一个基于哈希表的数据结构,它实现了Map接口,...

    Jdk1.8中的HashMap实现原理.docx

    《Jdk1.8中的HashMap实现原理》 HashMap作为Java编程语言中常用的数据结构,它在Jdk1.8中的实现结合了哈希表、链表以及红黑树的特性,提供高效且灵活的键值对存储功能。本文将深入探讨HashMap的内部结构、工作原理...

    Jdk1.8中的HashMap实现原理.pdf

    总结起来,JDK1.8中的HashMap实现原理主要包括以下几个要点: 1. 链表散列结构:数组+链表,链表用于处理哈希碰撞。 2. 红黑树优化:当链表长度超过8时,转换为红黑树,减少查找、插入和删除的时间复杂度。 3. 内部...

    HashMap底层原理

    HashMap的底层原理主要依赖于哈希表,这是一种数据结构,它通过计算键的哈希码来实现高效的查找操作。 在HashMap中,每个元素都是一个键值对,存储在一个Entry对象中。当向HashMap添加键值对时,首先会计算键的哈希...

    HashMap的实现原理

    ### HashMap的实现原理 #### 1. HashMap概述 HashMap 是 Java 集合框架中一个非常重要的类,它实现了 Map 接口,并提供了基于哈希表的存储方式。与其它 Map 实现不同的是,HashMap 允许使用 `null` 键和 `null` 值...

    HashMap原理的深入理解

    HashMap原理的深入理解 HashMap是基于哈希表的Map接口的非同步实现,提供了所有可选的映射操作,并允许使用null值和null键。HashMap储存的是键值对,HashMap很快。此类不保证映射的顺序,特别是它不保证该顺序恒久...

    HashMap底层实现原理共6页.pdf.zip

    在深入理解HashMap的底层实现原理之前,我们首先要明确其基本概念。 HashMap基于哈希表(也称为散列表)实现,哈希表是一种通过哈希函数将键映射到数组下标的存储结构。这种映射使得查找、插入和删除操作可以在平均...

    hashMap基本原理,内存知识

    HashMap 的底层实现原理可以分为两部分:数组 + 链表(jdk 1.7)和数组 + 链表/红黑树(jdk 1.8)。在jdk 1.7中,HashMap 使用数组 + 链表的结构来存储数据,而在 jdk 1.8 中,当链表的长度超过 8 个时,会将链表...

    基于JavaScript的HashMap实现

    总之,"基于JavaScript的HashMap实现"这篇博客深入讲解了如何在JavaScript环境中构建一个高效、灵活的HashMap数据结构,这对于理解JavaScript的数据存储机制和提升编程技巧是非常有价值的。通过阅读和理解HashMap.js...

    深入解析java HashMap实现原理

    综上所述,理解HashMap的实现原理对于优化Java程序性能至关重要,尤其是在处理大量数据或并发场景时。在实际应用中,应根据具体需求选择合适的数据结构,例如,如果需要线程安全,可以选择ConcurrentHashMap;如果对...

    用hashmap实现词典查询

    总的来说,通过HashMap实现词典查询,我们可以充分利用其高效的查找机制,为用户提供快速的查询服务。同时,理解HashMap的内部机制对于优化查询性能和处理潜在问题至关重要。在实际项目中,还需要考虑如何优雅地处理...

    电话本管理系统HashMap实现

    在这个系统中,使用HashMap实现是一种高效且灵活的方法,因为HashMap提供了快速的查找和插入操作。本文将深入探讨如何使用HashMap来构建一个电话本管理系统,并通过源码分析增强理解。 HashMap是Java集合框架中的一...

    详解Java HashMap实现原理

    以下是对HashMap实现原理的详细解析。 首先,HashMap内部使用了一个瞬时变量数组`table`(也称为“桶”)来存储键值对。桶是一个Entry对象数组,它的大小可以动态调整,并且长度必须是2的幂。初始时,`table`是一个...

    hashmap面试题_hashmap_

    答:在多线程环境下,可以使用ConcurrentHashMap,它是线程安全的HashMap实现。 五、HashMap与HashSet的关系 HashSet基于HashMap实现,每个元素作为HashMap的一个键,值为null。因此,HashSet的操作性能也依赖于...

    HashMap和HashTable底层原理以及常见面试题

    HashMap和HashTable底层原理以及常见面试题 HashMap和HashTable是Java中两个常用的数据结构,都是基于哈希表实现的,但它们之间存在着一些关键的区别。...理解它们的底层原理和区别是非常重要的。

Global site tag (gtag.js) - Google Analytics