现在的位置: 首页 > 综合 > 正文

《哈希表》

2017年12月20日 ⁄ 综合 ⁄ 共 17241字 ⁄ 字号 评论关闭

《十一、从头到尾彻底解析Hash 表算法http://blog.csdn.net/v_july_v/article/details/6256463

 

哈希表是怎么存储的?

 

 

 

 

哈希表    底层实现与使用

《STL源码剖析》5.7 hash table - 哈希表      核心思想:以空间换时间

  • 哈希函数(hash function)
  • 解决碰撞问题:线性探测,二次探测(H,H+1^2,H+2^2,H+3^2...),开链等等。

二次探查可以消除主集团(primary clustering),却可能造成次集团。解决次集团的方法有:复式散列(double hashing)

  • SGI STL的hash table便是采用开链方法。
  • buckets:桶

template <class Value>

struct __hashtable_node

{

    __hashtable_node* next;

    Value val;

};

至于Buckets聚合体,则以vector完成,以便有动态扩充能力。

 

哈希表的存储细节(怎么存放,存在哪里)

 

 

1、哈希表查找的时间复杂度?

2、哈希表如何处理冲突?(解决碰撞问题)

3、如果冲突得太多怎么办?

4、如果哈希表太小,但数据太多怎么办?

 

 

hash_map - C++ STL中的实现

  • hashmap是实现了map接口的类,它提供了键值对的保存方法。

《详细解说STL hash_map系列》http://www.cppblog.com/woaidongmao/archive/2008/10/15/64014.html

  • map的使用

#include<map> #include<string> usingnamespace std;

map<string,string> namemap;

namemap["岳不群"] = "华山派掌门人";

if(namemap.find("岳不群")!=namemap.end()){  }

 

  • hash_map基于hash table。最大优点,就是把数据的存储和查找消耗的时间大大降低,几乎可以看成是常数时间;而代价仅仅是消耗比较多的内存。然而在当前可利用内存越来越多的情况下,用空间换时间的做法是值得的。另外,编码比较容易也是它的特点之一。
  • 其基本原理是:使用一个下标范围比较大的数组来存储元素。可以设计一个函数(哈希函数,也叫做散列函数),使得每个元素的关键字都与一个函数值(即数组下标,hash值)相对应,于是用这个数组单元来存储这个元素;也可以简单的理解为,按照关键字为每一个元素“分类”,然后将这个元素存储在相应“类”所对应的地方,称为桶。
  • 但是,不能够保证每个元素的关键字与函数值是一一对应的,因此极有可能出现对于不同的元素,却计算出了相同的函数值,这样就产生了“冲突”,换句话说,就是把不同的元素分在了相同的“类”之中。 总的来说,“直接定址”与“解决冲突”是哈希表的两大特点。
  • hash_map,首先分配一大片内存,形成许多桶。是利用hash函数,对key进行映射到不同区域(桶)进行保存。
  • 其插入过程是:得到key。通过hash函数得到hash值。得到桶号(一般都为hash值对桶数求模)。存放key和value在桶内。
  • 其取值过程是:得到key。通过hash函数得到hash值。得到桶号(一般都为hash值对桶数求模)。比较桶的内部元素是否与key相等,若都不相等,则没有找到。取出相等的记录的value。
  • hash_map中直接地址用hash函数生成,解决冲突,用比较函数解决。这里可以看出,如果每个桶内部只有一个元素,那么查找的时候只有一次比较。当许多桶内没有值时,许多查询就会更快了(指查不到的时候).
  • 由此可见,要实现哈希表, 和用户相关的是:hash函数比较函数。这两个参数刚好是我们在使用hash_map时需要指定的参数。

 

hash_map的使用

  • #include <hash_map>    #include <string>  using namespace std;

int main(){   hash_map<int, string>mymap;

       mymap[9527]="唐伯虎点秋香"; mymap[1000000]="百万富翁的生活";

            if(mymap.find(10000) != mymap.end()){ }

  • 在你没有指定hash函数和比较函数的时候,你会有一个缺省的函数,看看hash_map的声明,你会更加明白。下面是SGI STL的声明:

template <class _Key, class _Tp, class _HashFcn= hash<_Key>,

class _EqualKey = equal_to<_Key>,

class _Alloc = __STL_DEFAULT_ALLOCATOR(_Tp) >

class hash_map{}

也就是说,在上例中,有以下等同关系:hash_map<int,string> mymap;//等同于:

hash_map<int, string, hash<int>,equal_to<int> > mymap;

Alloc我们就不要去关注太多了(深入了解Allocator可以参看标准库STL :Allocator能做什么)

 

hash_map 的hash函数

  • struct hash<int> {size_t operator()(int __x) const { return __x; }};

原来是个函数对象。在SGI STL中,提供了以下hash函数:

  • struct hash<char*>struct hash<const char*>struct hash<char> struct hash<unsigned char>

struct hash<signed char>structhash<short>struct hash<unsigned short> struct hash<int>

struct hash<unsigned int>struct hash<long>struct hash<unsigned long>

也就是说,如果你的key使用的是以上类型中的一种,你都可以使用缺省的hash函数。当然你自己也可以定义自己的hash函数。对于自定义变量,你只能如此,例如对于string,就必须自定义hash函数。例如:

struct str_hash{

        size_toperator()(const string& str) const

        {

               unsigned long __h = 0;

               for (size_t i = 0 ; i < str.size() ; i ++)

               __h = 5*__h + str[i];

               return size_t(__h);

        }

};

//如果你希望利用系统定义的字符串hash函数,你可以这样写:

struct str_hash{

        size_toperator()(const string& str) const

        {

               return return __stl_hash_string(str.c_str());

        }

};

在声明自己的哈希函数时要注意以下几点:

  • 使用struct,然后重载operator().
  • 返回是size_t
  • 参数是你要hash的key的类型。
  • 函数是const类型的。

如果这些比较难记,最简单的方法就是照猫画虎,找一个函数改改就是了。

 

map<string, string>namemap;     //改为:      hash_map<string, string,str_hash> namemap;

 

hash_map 的比较函数

 

在map中的比较函数,需要提供less函数。如果没有提供,缺省的也是less< Key> 。在hash_map中,要比较桶内的数据和key是否相等,因此需要的是是否等于的函数:equal_to<Key> 。先看看equal_to的源码:

 

//本代码可以从SGI STL

//先看看binary_function 函数声明,其实只是定义一些类型而已。

template <class _Arg1, class _Arg2, class _Result>

struct binary_function {

        typedef _Arg1first_argument_type;

        typedef _Arg2second_argument_type;

        typedef_Result result_type;

};

//看看equal_to的定义:

template <class _Tp>

struct equal_to : publicbinary_function<_Tp,_Tp,bool>

{

        booloperator()(const _Tp& __x, const _Tp& __y) const { return __x == __y; }

};

如果你使用一个自定义的数据类型,如struct mystruct, 或者const char* 的字符串,如何使用比较函数?使用比较函数,有两种方法. 第一种是:重载==操作符,利用equal_to;看看下面的例子:

 

struct mystruct{

        int iID;

        int  len;

        booloperator==(const mystruct & my) const{

               return (iID==my.iID) && (len==my.len) ;

        }

}; 

这样,就可以使用equal_to< mystruct>作为比较函数了。另一种方法就是使用函数对象。自定义一个比较函数体:

 

struct compare_str{

        booloperator()(const char* p1, const char*p2) const{

               return strcmp(p1,p2)==0;

        }

}; 

有了compare_str,就可以使用hash_map了。

 

typedef hash_map<const char*, string, hash<constchar*>, compare_str> StrIntMap;

StrIntMap namemap;

namemap["岳不群"]="华山派掌门人,人称君子剑";

namemap["张三丰"]="武当掌门人,太极拳创始人";

namemap["东方不败"]="第一高手,葵花宝典";

 

2.4 hash_map 函数

 

hash_map的函数和map的函数差不多。具体函数的参数和解释,请参看:STL 编程手册:Hash_map,这里主要介绍几个常用函数。

 

hash_map(size_type n) 如果讲究效率,这个参数是必须要设置的。n 主要用来设置hash_map 容器中hash桶的个数。桶个数越多,hash函数发生冲突的概率就越小,重新申请内存的概率就越小。n越大,效率越高,但是内存消耗也越大。

const_iterator find(const key_type& k) const. 用查找,输入为键值,返回为迭代器。

data_type& operator[](const key_type& k) . 这是我最常用的一个函数。因为其特别方便,可像使用数组一样使用。不过需要注意的是,当你使用[key ]操作符时,如果容器中没有key元素,这就相当于自动增加了一个key元素。因此当你只是想知道容器中是否有key元素时,你可以使用find。如果你希望插入该元素时,你可以直接使用[]操作符。

insert 函数。在容器中不包含key值时,insert函数和[]操作符的功能差不多。但是当容器中元素越来越多,每个桶中的元素会增加,为了保证效率,hash_map会自动申请更大的内存,以生成更多的桶。因此在insert以后,以前的iterator有可能是不可用的。

erase 函数。在insert的过程中,当每个桶的元素太多时,hash_map可能会自动扩充容器的内存。但在sgi stl中是erase并不自动回收内存。因此你调用erase后,其他元素的iterator还是可用的。

3 相关hash容器

 

hash 容器除了hash_map之外,还有hash_set, hash_multimap,has_multiset, 这些容器使用起来和set, multimap, multiset的区别与hash_map和map的区别一样,我想不需要我一一细说了吧。

 

set map multiset multimap

 

hash_set hash_map hash_multiset hash_multimap

 

4 其他

 

这里列几个常见问题,应该对你理解和使用hash_map比较有帮助。

 

4.1 hash_map和map的区别在哪里?

 

构造函数。hash_map需要hash函数,等于函数;map只需要比较函数(小于函数).

存储结构。hash_map采用hash表存储,map一般采用红黑树(RB Tree)实现。因此其memory数据结构是不一样的。

4.2 什么时候需要用hash_map,什么时候需要用map?

 

总体来说,hash_map 查找速度会比map快,而且查找速度基本和数据数据量大小,属于常数级别;而map的查找速度是log(n)级别。并不一定常数就比log(n)小,hash还有hash函数的耗时,明白了吧,如果你考虑效率,特别是在元素达到一定数量级时,考虑考虑hash_map。但若你对内存使用特别严格,希望程序尽可能少消耗内存,那么一定要小心,hash_map可能会让你陷入尴尬,特别是当你的hash_map对象特别多时,你就更无法控制了,而且hash_map的构造速度较慢。

 

现在知道如何选择了吗?权衡三个因素: 查找速度, 数据量, 内存使用。

 

这里还有个关于hash_map和map的小故事,看看:http://dev.csdn.net/Develop/article/14/14019.shtm

 

4.3 如何在hash_map中加入自己定义的类型?

 

你只要做两件事, 定义hash函数,定义等于比较函数。下面的代码是一个例子:

 

-bash-2.05b$ cat my.cpp

#include <hash_map>

#include <string>

#include <iostream>

 

using namespace std;

//define the class

class ClassA{

        public:

        ClassA(inta):c_a(a){}

        intgetvalue()const { return c_a;}

        voidsetvalue(int a){c_a;}

        private:

        int c_a;

};

 

//1 define the hash function

struct hash_A{

        size_toperator()(const class ClassA & A)const{

               //  return  hash<int>(classA.getvalue());

               return A.getvalue();

        }

};

 

//2 define the equal function

struct equal_A{

        booloperator()(const class ClassA & a1, const class ClassA & a2)const{

               return  a1.getvalue() == a2.getvalue();

        }

};

 

int main()

{

       hash_map<ClassA, string, hash_A, equal_A> hmap;

        ClassA a1(12);

       hmap[a1]="I am 12";

        ClassAa2(198877);

        hmap[a2]="Iam 198877";

       

       cout<<hmap[a1]<<endl;

       cout<<hmap[a2]<<endl;

        return 0;

}

-bash-2.05b$ make my

c++  -O -pipe -march=pentiumpro  my.cpp -o my

-bash-2.05b$ ./my

I am 12

I am 198877

 

 

typedef map<Key, Value> KeyMap;

当你希望使用hash_map来替换的时候,只需要修改:

 

typedef hash_map<Key, Value> KeyMap;

其他的基本不变。当然,你需要注意是否有Key类型的hash函数和比较函数。

 

4.5为什么hash_map不是标准的?

 

具体为什么不是标准的,我也不清楚,有个解释说在STL加入标准C++之时,hash_map系列当时还没有完全实现,以后应该会成为标准。如果谁知道更合理的解释,也希望告诉我。但我想表达的是,正是因为hash_map不是标准的,所以许多平台上安装了g++编译器,不一定有hash_map的实现。我就遇到了这样的例子。因此在使用这些非标准库的时候,一定要事先测试。另外,如果考虑到平台移植,还是少用为佳。

 

4.6 有学习使用hash_map的建议吗?

 

hash中文是哈希,也成为散列,听见别人说散列容器不要埋怨自己孤陋寡闻。

 

 

 

了解hash系列,你还可以看看这篇文章:effective STL 25: 熟悉非标准散列容器, 建议查看源代码。如果还有问题,那么你可以在STL论坛上提问,会有高手回答你的。

HashMap - Java中的哈希表实现

JDK:

public class HashMap<K,V>

extends AbstractMap<K,V>

implements Map<K,V>, Cloneable, Serializable

 

基于哈希表的 Map 接口的实现。此实现提供所有可选的映射操作,并允许使用 null 值和 null

键。(除了非同步和允许使用 null 之外,HashMap 类与 Hashtable

大致相同。)此类不保证映射的顺序,特别是它不保证该顺序恒久不变。

 

 

package li.suxuan;

 

import java.util.HashMap;

 

public class HashMapPractice {

     public static void main(String[]args){

         HashMap<String,Double> mark = new HashMap<String,Double>();

         

         mark.put("语文", 80.0);

         mark.put("数学", 90.0);

         mark.put("英语", 100.0);

         

         System.out.println("lisuxuan's hashcode is : " + "lisuxuan".hashCode());

         System.out.println(mark.get("语文"));

         System.out.println(mark.get("语"));

         System.out.println(mark.toString());

         mark.put(null,null );

         mark.put("英", 100.0);

         System.out.println(mark.size());

         System.out.println(mark.toString());

         

         System.out.println(mark.containsValue(80.0));

         System.out.println(mark.containsKey("英"));

          //返回此映射所包含的值的 Collection 视图

         System.out.println(mark.values());

         System.out.println(mark.keySet());

         

         System.out.println(" ------ " + mark.remove("yin"));

         System.out.println(" ------ " + mark.remove("英"));

         System.out.println(mark);

         

         mark.clear();

          System.out.println(mark);

     }

}

 

 

Output:

 

lisuxuan's hashcode is : 1347655945

80.0

null

{语文=80.0, 英语=100.0, 数学=90.0}

5

{null=null, 语文=80.0, 英=100.0, 英语=100.0, 数学=90.0}

true

true

[null, 80.0, 100.0, 100.0, 90.0]

[null, 语文, 英, 英语, 数学]

 ------ null

 ------ 100.0

{null=null, 语文=80.0, 英语=100.0, 数学=90.0}

{}

 

 

 

java中HashMap详解

 

HashMap 和 HashSet 是 Java Collection Framework 的两个重要成员,其中 HashMap 是 Map 接口的常用实现类,HashSet 是 Set 接口的常用实现类。虽然 HashMap 和 HashSet 实现的接口规范不同,但它们底层的 Hash 存储机制完全一样,甚至 HashSet 本身就采用 HashMap 来实现的。

 

 

 

通过 HashMap、HashSet 的源代码分析其 Hash 存储机制

 

实际上,HashSet 和 HashMap 之间有很多相似之处,对于 HashSet 而言,系统采用 Hash 算法决定集合元素的存储位置,这样可以保证能快速存、取集合HashSet元素;

 

对于 HashMap 而言,系统 key-value 当成一个整体进行处理,系统总是根据 Hash 算法来计算 key-value 的存储位置,这样可以保证能快速存、取 Map 的 key-value 对。

 

在介绍集合存储之前需要指出一点:虽然集合号称存储的是 Java 对象,但实际上并不会真正将 Java 对象放入 Set 集合中,只是在 Set 集合中保留这些对象的引用而言。

 

也就是说:Java 集合实际上是多个引用变量所组成的集合,这些引用变量指向实际的 Java 对象。

 

集合和引用

 

就像引用类型的数组一样,当我们把 Java 对象放入数组之时,并不是真正的把 Java 对象放入数组中,只是把对象的引用放入数组中,每个数组元素都是一个引用变量。

 

 

HashMap 的存储实现

 

当程序试图将多个 key-value 放入 HashMap 中时,以如下代码片段为例:

 

[java] view plaincopy

HashMap<String , Double> map = newHashMap<String , Double>();  

map.put("语文" , 80.0);  

map.put("数学" , 89.0);  

map.put("英语" , 78.2);  

 

HashMap 采用一种所谓的“Hash 算法”来决定每个元素的存储位置。

 

当程序执行 map.put("语文" , 80.0); 时,系统将调用"语文"的 hashCode() 方法得到其 hashCode 值——每个 Java 对象都有 hashCode() 方法,都可通过该方法获得它的 hashCode 值。得到这个对象的 hashCode 值之后,系统会根据该 hashCode 值来决定该元素的存储位置。

 

我们可以看 HashMap 类的 put(K key , V value) 方法的源代码:

 

[java] view plaincopy

public V put(K key, V value)  

{  

 // 如果 key 为 null,调用 putForNullKey 方法进行处理 

 if (key == null)  

     returnputForNullKey(value);  

 // 根据 key 的 keyCode 计算 Hash 值 

 int hash = hash(key.hashCode());  

 // 搜索指定 hash 值在对应 table 中的索引 

     int i = indexFor(hash,table.length); 

 // 如果 i 索引处的 Entry 不为 null,通过循环不断遍历 e 元素的下一个元素 

 for (Entry<K,V> e = table[i]; e != null; e =e.next)  

 {  

     Object k;  

     // 找到指定 key 与需要放入的 key 相等(hash 值相同 

     // 通过 equals 比较放回 true) 

     if (e.hash == hash &&((k = e.key) == key  

         ||key.equals(k)))  

     {  

         VoldValue = e.value;  

         e.value= value;  

        e.recordAccess(this);  

         returnoldValue;  

     }  

 }  

 // 如果 i 索引处的 Entry 为 null,表明此处还没有 Entry  

 modCount++;  

 // 将 key、value 添加到 i 索引处 

 addEntry(hash, key, value, i);  

 return null;  

}  

上面程序中用到了一个重要的内部接口:Map.Entry,每个 Map.Entry 其实就是一个 key-value 对。从上面程序中可以看出:当系统决定存储 HashMap 中的 key-value 对时,完全没有考虑 Entry 中的 value,仅仅只是根据 key 来计算并决定每个 Entry 的存储位置。这也说明了前面的结论:我们完全可以把 Map 集合中的 value 当成 key 的附属,当系统决定了 key 的存储位置之后,value 随之保存在那里即可。

 

上面方法提供了一个根据 hashCode() 返回值来计算 Hash 码的方法:hash(),这个方法是一个纯粹的数学计算,其方法如下: 

 

[java] view plaincopy

static int hash(int h)  

{  

    h ^= (h >>> 20) ^ (h>>> 12);  

    return h ^ (h >>> 7) ^ (h>>> 4);  

}  

对于任意给定的对象,只要它的 hashCode() 返回值相同,那么程序调用 hash(int h) 方法所计算得到的 Hash 码值总是相同的。接下来程序会调用 indexFor(int h, int length) 方法来计算该对象应该保存在 table 数组的哪个索引处。indexFor(int h,int length) 方法的代码如下:

 

[java] view plaincopy

static int indexFor(int h, int length)  

{  

    return h & (length-1);  

这个方法非常巧妙,它总是通过 h &(table.length-1) 来得到该对象的保存位置——而 HashMap 底层数组的长度总是 2 的 n 次方,这一点可参看后面关于 HashMap 构造器的介绍。

 

当 length 总是 2 的倍数时,h & (length-1) 将是一个非常巧妙的设计:假设 h=5,length=16, 那么 h &length - 1 将得到 5;如果 h=6,length=16, 那么 h & length - 1 将得到 6 ……如果 h=15,length=16, 那么 h &length - 1 将得到 15;但是当 h=16 时 , length=16 时,那么 h &length - 1 将得到 0 了;当 h=17 时 , length=16 时,那么 h &
length - 1 将得到 1 了……这样保证计算得到的索引值总是位于 table 数组的索引之内。

 

根据上面 put 方法的源代码可以看出,当程序试图将一个 key-value 对放入 HashMap 中时,程序首先根据该 key 的 hashCode() 返回值决定该 Entry 的存储位置:如果两个 Entry 的 key 的 hashCode() 返回值相同,那它们的存储位置相同。如果这两个 Entry 的 key 通过 equals 比较返回 true,新添加 Entry 的 value 将覆盖集合中原有 Entry 的 value,但 key 不会覆盖。如果这两个 Entry 的 key 通过 equals 比较返回 false,新添加的 Entry 将与集合中原有 Entry 形成 Entry 链,而且新添加的 Entry 位于 Entry 链的头部——具体说明继续看 addEntry() 方法的说明。

 

当向 HashMap 中添加 key-value 对,由其 key 的 hashCode() 返回值决定该 key-value 对(就是 Entry 对象)的存储位置。

 

当两个 Entry 对象的 key 的 hashCode() 返回值相同时,将由 key 通过 eqauls() 比较值决定是采用覆盖行为(返回 true),还是产生 Entry 链(返回 false)。

 

上面程序中还调用了 addEntry(hash, key,value, i); 代码,其中 addEntry 是 HashMap 提供的一个包访问权限的方法,该方法仅用于添加一个 key-value 对。下面是该方法的代码:

 

[java] view plaincopy

void addEntry(int hash, K key, V value, intbucketIndex)  

{  

    // 获取指定 bucketIndex 索引处的 Entry  

    Entry<K,V> e =table[bucketIndex];     // ① 

    // 将新创建的 Entry 放入 bucketIndex 索引处,并让新的 Entry 指向原来的 Entry  

    table[bucketIndex] = newEntry<K,V>(hash, key, value, e);  

    // 如果 Map 中的 key-value 对的数量超过了极限 

    if (size++ >=threshold)  

        // 把 table 对象的长度扩充到 2 倍。 

        resize(2 * table.length);   // ② 

}  

上面方法的代码很简单,但其中包含了一个非常优雅的设计:系统总是将新添加的 Entry 对象放入 table 数组的 bucketIndex 索引处——如果 bucketIndex 索引处已经有了一个 Entry 对象,那新添加的 Entry 对象指向原有的 Entry 对象(产生一个 Entry 链),如果 bucketIndex 索引处没有 Entry 对象,也就是上面程序①号代码的 e 变量是 null,也就是新放入的 Entry 对象指向 null,也就是没有产生 Entry 链。

 

 

 

JDK 源码

 

在 JDK 安装目录下可以找到一个 src.zip 压缩文件,该文件里包含了 Java 基础类库的所有源文件。只要读者有学习兴趣,随时可以打开这份压缩文件来阅读 Java 类库的源代码,这对提高读者的编程能力是非常有帮助的。需要指出的是:src.zip 中包含的源代码并没有包含像上文中的中文注释,这些注释是笔者自己添加进去的。

 

Hash 算法的性能选项

 

根据上面代码可以看出,在同一个 bucket 存储 Entry 链的情况下,新放入的 Entry 总是位于 bucket 中,而最早放入该 bucket 中的 Entry 则位于这个 Entry 链的最末端。

 

上面程序中还有这样两个变量:

 

    * size:该变量保存了该 HashMap 中所包含的 key-value 对的数量。

    * threshold:该变量包含了 HashMap 能容纳的 key-value 对的极限,它的值等于 HashMap 的容量乘以负载因子(load factor)。

 

从上面程序中②号代码可以看出,当 size++ >=threshold 时,HashMap 会自动调用 resize 方法扩充 HashMap 的容量。每扩充一次,HashMap 的容量就增大一倍。

 

上面程序中使用的 table 其实就是一个普通数组,每个数组都有一个固定的长度,这个数组的长度就是 HashMap 的容量。HashMap 包含如下几个构造器:

 

    * HashMap():构建一个初始容量为 16,负载因子为 0.75 的 HashMap。

    * HashMap(int initialCapacity):构建一个初始容量为 initialCapacity,负载因子为 0.75 的 HashMap。

    * HashMap(int initialCapacity, floatloadFactor):以指定初始容量、指定的负载因子创建一个 HashMap。

 

当创建一个 HashMap 时,系统会自动创建一个 table 数组来保存 HashMap 中的 Entry,下面是 HashMap 中一个构造器的代码:

 

[java] view plaincopy

// 以指定初始化容量、负载因子创建 HashMap  

 public HashMap(int initialCapacity, floatloadFactor)  

 {  

     // 初始容量不能为负数 

     if (initialCapacity <0)  

         thrownew IllegalArgumentException(  

        "Illegalinitial capacity: " +  

            initialCapacity);  

     // 如果初始容量大于最大容量,让出示容量 

     if (initialCapacity >MAXIMUM_CAPACITY)  

        initialCapacity = MAXIMUM_CAPACITY;  

     // 负载因子必须大于 0 的数值 

     if (loadFactor <= 0 ||Float.isNaN(loadFactor))  

         thrownew IllegalArgumentException(  

        loadFactor);  

     // 计算出大于 initialCapacity 的最小的 2 的 n 次方值。 

     int capacity = 1;  

     while (capacity <initialCapacity)  

         capacity<<= 1;  

     this.loadFactor =loadFactor;  

     // 设置容量极限等于容量 * 负载因子 

     threshold = (int)(capacity *loadFactor);  

     // 初始化 table 数组 

     table = newEntry[capacity];           // ① 

     init();  

 }  

上面代码中粗体字代码包含了一个简洁的代码实现:找出大于 initialCapacity 的、最小的 2 的 n 次方值,并将其作为 HashMap 的实际容量(由 capacity 变量保存)。例如给定 initialCapacity 为 10,那么该 HashMap 的实际容量就是 16。

程序①号代码处可以看到:table 的实质就是一个数组,一个长度为 capacity 的数组。

 

 

对于 HashMap 及其子类而言,它们采用 Hash 算法来决定集合中元素的存储位置。当系统开始初始化 HashMap 时,系统会创建一个长度为 capacity 的 Entry 数组,这个数组里可以存储元素的位置被称为“桶(bucket)”,每个 bucket 都有其指定索引,系统可以根据其索引快速访问该 bucket 里存储的元素。

 

无论何时,HashMap 的每个“桶”只存储一个元素(也就是一个 Entry),由于 Entry 对象可以包含一个引用变量(就是 Entry 构造器的的最后一个参数)用于指向下一个 Entry,因此可能出现的情况是:HashMap 的 bucket 中只有一个 Entry,但这个 Entry 指向另一个 Entry ——这就形成了一个 Entry 链。如图 1 所示:

 

 

 

图 1. HashMap 的存储示意

 

 

 

HashMap 的读取实现

 

当 HashMap 的每个 bucket 里存储的 Entry 只是单个 Entry ——也就是没有通过指针产生 Entry 链时,此时的 HashMap 具有最好的性能:当程序通过 key 取出对应 value 时,系统只要先计算出该 key 的 hashCode() 返回值,在根据该 hashCode 返回值找出该 key 在 table 数组中的索引,然后取出该索引处的 Entry,最后返回该 key 对应的 value 即可。看 HashMap 类的 get(K key) 方法代码:

 

[java] view plaincopy

public V get(Object key)  

{  

 // 如果 key 是 null,调用 getForNullKey 取出对应的 value  

 if (key == null)  

     returngetForNullKey();  

 // 根据该 key 的 hashCode 值计算它的 hash 码 

 int hash = hash(key.hashCode());  

 // 直接取出 table 数组中指定索引处的值, 

 for (Entry<K,V> e = table[indexFor(hash,table.length)];  

     e != null;  

     // 搜索该 Entry 链的下一个 Entr  

     e =e.next)         // ① 

 {  

     Object k;  

     // 如果该 Entry 的 key 与被搜索 key 相同 

     if (e.hash == hash &&((k = e.key) == key  

         ||key.equals(k)))  

         returne.value;  

 }  

 return null;  

}  

从上面代码中可以看出,如果 HashMap 的每个 bucket 里只有一个 Entry 时,HashMap 可以根据索引、快速地取出该 bucket 里的 Entry;

 

在发生“Hash 冲突”的情况下,单个 bucket 里存储的不是一个 Entry,而是一个 Entry 链,系统只能必须按顺序遍历每个 Entry,直到找到想搜索的 Entry 为止——如果恰好要搜索的 Entry 位于该 Entry 链的最末端(该 Entry 是最早放入该 bucket 中),那系统必须循环到最后才能找到该元素。

 

归纳起来简单地说,HashMap 在底层将 key-value 当成一个整体进行处理,这个整体就是一个 Entry 对象。

 

HashMap 底层采用一个 Entry[] 数组来保存所有的 key-value 对,当需要存储一个 Entry 对象时,会根据 Hash 算法来决定其存储位置;当需要取出一个 Entry 时,也会根据 Hash 算法找到其存储位置,直接取出该 Entry。由此可见:HashMap 之所以能快速存、取它所包含的 Entry,完全类似于现实生活中母亲从小教我们的:不同的东西要放在不同的位置,需要时才能快速找到它。

 

当创建 HashMap 时,有一个默认的负载因子(load factor),其默认值为 0.75,这是时间和空间成本上一种折衷:增大负载因子可以减少 Hash 表(就是那个 Entry 数组)所占用的内存空间,但会增加查询数据的时间开销,而查询是最频繁的的操作(HashMap 的 get() 与 put() 方法都要用到查询);减小负载因子会提高数据查询的性能,但会增加 Hash 表所占用的内存空间。

 

掌握了上面知识之后,我们可以在创建 HashMap 时根据实际需要适当地调整 load factor 的值;如果程序比较关心空间开销、内存比较紧张,可以适当地增加负载因子;如果程序比较关心时间开销,内存比较宽裕则可以适当的减少负载因子。通常情况下,程序员无需改变负载因子的值。

 

如果开始就知道 HashMap 会保存多个 key-value 对,可以在创建时就使用较大的初始化容量,如果 HashMap 中 Entry 的数量一直不会超过极限容量(capacity *load factor),HashMap 就无需调用 resize() 方法重新分配 table 数组,从而保证较好的性能。当然,开始就将初始容量设置太高可能会浪费空间(系统需要创建一个长度为 capacity 的 Entry 数组),因此创建 HashMap 时初始化容量设置也需要小心对待。

 

 

 

抱歉!评论已关闭.