热门标签 | HotTags
当前位置:  开发笔记 > 编程语言 > 正文

HashMap及HashTable源码解析

HashMap及HashTable源码解析HashMap在java和Android经常使用到,之前学过数据结构,理解了它的原理,却很
HashMap及HashTable源码解析

HashMap在java和Android经常使用到,之前学过数据结构,理解了它的原理,却很少花时间去阅读它的源码,今天索性对进行分析。
本篇文章主要分析HashMap与HashTable的实现原理及区别,关于ConcurrentHashMap本篇文章暂时不分析,有兴趣了解的课参考 这篇文章:http://www.cnblogs.com/ITtangtang/p/3948786.html,本人觉得不错。

转载请注明原博客地址:http://blog.csdn.net/gdutxiaoxu/article/details/51492390

一、HashTable与HashMap的区别

HashTable和HashMap采用相同的存储机制,二者的实现基本一致,不同的是:

1、HashMap是非线程安全的,HashTable是线程安全的,内部的方法基本都是synchronized。

2、HashTable不允许有null值的存在。

在HashTable中调用put方法时,如果key为null,直接抛出NullPointerException。其它细微的差别还有,比如初始化Entry数组的大小等等,但基本思想和HashMap一样


二:下面我们将从源码的角度来分析HashMap:


阅读源码之前我们先了解两个基本知识
1)它是一种用键映射值的数据结构,在源码里面的体现是用静态内部类Entry封装
 
   
   
[java] view plain copy 在CODE上查看代码片派生到我的代码片
  1. static class Entry implements Map.Entry {  
  2.         final K key;  
  3.         V value;  
  4.         Entry next;  
  5.         int hash;  
  6.   
  7.         /** 
  8.          * Creates new entry. 
  9.          */  
  10.         Entry(int h, K k, V v, Entry n) {  
  11.             value = v;  
  12.             next = n;  
  13.             key = k;  
  14.             hash = h;  
  15.         }  
  16. }  

2)里面是怎样实现存储多个节点的?其实就是用数组来存储的 Entry[] table
  
    
    
[java] view plain copy 在CODE上查看代码片派生到我的代码片
  1. static final Entry[] EMPTY_TABLE = {};  
  2.   
  3.   /** 
  4.    * The table, resized as necessary. Length MUST Always be a power of two. 
  5.    */  
  6.   transient Entry[] table = (Entry[]) EMPTY_TABLE;  
  7.   
  8.   /** 
  9.    * The number of key-value mappings contained in this map. 
  10.    */  
  11.   transient int size;  
  12.   
  13.   /** 
  14.    * The next size value at which to resize (capacity * load factor). 
  15.    * @serial 
  16.    */  
  17.   // If table == EMPTY_TABLE then this is the initial capacity at which the  
  18.   // table will be created when inflated.  
  19.   int threshold;  


下面讲解几个比较重要的方法

1)当我们进行put操作的时候
先贴出源码
   
   
[java] view plain copy 在CODE上查看代码片派生到我的代码片
  1. public V put(K key, V value) {  
  2.         if (key == null)  
  3.             return putForNullKey(value); //处理null值  
  4.         int hash = hash(key.hashCode());//计算hash  
  5.         int i = indexFor(hash, table.length);//计算在数组中的存储位置  
  6.     //遍历table[i]位置的链表,查找相同的key,若找到则使用新的value替换掉原来的oldValue并返回oldValue  
  7.         for (Entry e = table[i]; e != null; e = e.next) {  
  8.             Object k;  
  9.             if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {  
  10.                 V oldValue = e.value;  
  11.                 e.value = value;  
  12.                 e.recordAccess(this);  
  13.                 return oldValue;  
  14.             }  
  15.         }  
  16.     //若没有在table[i]位置找到相同的key,则添加key到table[i]位置,新的元素总是在table[i]位置的第一个元素,原来的元素后移  
  17.         modCount++;  
  18.         addEntry(hash, key, value, i);  
  19.         return null;  
  20.     }  



a HashMap会对null值key进行特殊处理,总是放到table[0]位置
b计算has值
c 找到在table数组中的索引
   1)遍历table[i]位置的链表,查找相同的key,若找到则使用新的value替换掉原来的oldValue并返回oldValue
   2)若没有在table[i]位置找到相同的key,则添加key到table[i]位置,新的元素总是在table[i]位置的第一个元素,原来的元素后调用addEntry方法

   
   
[java] view plain copy 在CODE上查看代码片派生到我的代码片
  1. void addEntry(int hash, K key, V value, int bucketIndex) {  
  2.         if ((size >= threshold) && (null != table[bucketIndex])) {  
  3.             resize(2 * table.length);  
  4.             hash = (null != key) ? hash(key) : 0;  
  5.             bucketIndex = indexFor(hash, table.length);  
  6.         }  
  7.   
  8.         createEntry(hash, key, value, bucketIndex);  
  9.     }  


    
    
[java] view plain copy 在CODE上查看代码片派生到我的代码片
  1. void createEntry(int hash, K key, V value, int bucketIndex) {  
  2.     Entry e = table[bucketIndex];  
  3.     table[bucketIndex] = new Entry<>(hash, key, value, e);  
  4.     size++;  
  5. }  
所做的工作就是:
1)判断当元素数量达到临界值(capactiy*factor)时,则进行扩容,是table数组长度变为table.length*2
2)当table[index]已存在其它元素时,会在table[index]位置形成一个链表,将新添加的元素放在table[index],原来的元素通过Entry的next进行链接,这样以链表形式解决hash冲突问题,

2)get方法
同样当key为null时会进行特殊处理,在table[0]的链表上查找key为null的元素


    
    
[java] view plain copy 在CODE上查看代码片派生到我的代码片
  1. public V get(Object key) {  
  2.        if (key == null)  
  3.            return getForNullKey();  
  4.        Entry entry = getEntry(key);  
  5.        return null == entry ? null : entry.getValue();  
  6.    }  

get的过程是先计算hash然后通过hash与table.length取摸计算index值,然后遍历table[index]上的链表,直到找到key,
然后返回,找不到返回null

   
   
[java] view plain copy 在CODE上查看代码片派生到我的代码片
  1. final Entry getEntry(Object key) {  
  2.         if (size == 0) {  
  3.             return null;  
  4.         }  
  5.   
  6.         int hash = (key == null) ? 0 : hash(key);  
  7.         for (Entry e = table[indexFor(hash, table.length)];  
  8.              e != null;  
  9.              e = e.next) {  
  10.             Object k;  
  11.             if (e.hash == hash &&  
  12.                 ((k = e.key) == key || (key != null && key.equals(k))))  
  13.                 return e;  
  14.         }  
  15.         return null;  
  16.     }  


3)remove方法
remove方法和put get类似,计算hash,计算index,然后遍历查找,将找到的元素从table[index]链表移除
 
   
   
[java] view plain copy 在CODE上查看代码片派生到我的代码片
  1. public V remove(Object key) {  
  2.         Entry e = removeEntryForKey(key);  
  3.         return (e == null ? null : e.value);  
  4.     }  

[java] view plain copy 在CODE上查看代码片派生到我的代码片
  1. final Entry removeEntryForKey(Object key) {  
  2.         if (size == 0) {  
  3.             return null;  
  4.         }  
  5.         int hash = (key == null) ? 0 : hash(key);  
  6.         int i = indexFor(hash, table.length);  
  7.         Entry prev = table[i];  
  8.         Entry e = prev;  
  9.   
  10.         while (e != null) {  
  11.             Entry next = e.next;  
  12.             Object k;  
  13.             if (e.hash == hash &&  
  14.                 ((k = e.key) == key || (key != null && key.equals(k)))) {  
  15.                 modCount++;  
  16.                 size--;  
  17.                 if (prev == e)  
  18.                     table[i] = next;  
  19.                 else  
  20.                     prev.next = next;  
  21.                 e.recordRemoval(this);  
  22.                 return e;  
  23.             }  
  24.             prev = e;  
  25.             e = next;  
  26.         }  
  27.   
  28.         return e;  
  29.     }  

4)clear()方法

clear方法非常简单,就是遍历table然后把每个位置置为null,同时修改元素个数为0
需要注意的是clear方法只会清楚里面的元素,并不会重置capactiy

   
   
[java] view plain copy 在CODE上查看代码片派生到我的代码片
  1. public void clear() {  
  2.         modCount++;  
  3.         Arrays.fill(table, null);  
  4.         size = 0;  
  5.     }  


5)containsKey和containsValue

containsKey方法是先计算hash然后使用hash和table.length取摸得到index值,遍历table[index]元素查找是否包含key相同的值

   
   
[java] view plain copy 在CODE上查看代码片派生到我的代码片
  1. public boolean containsKey(Object key) {  
  2.        return getEntry(key) != null;  
  3.    }  

   
   
[java] view plain copy 在CODE上查看代码片派生到我的代码片
  1. final Entry getEntry(Object key) {  
  2.         if (size == 0) {  
  3.             return null;  
  4.         }  
  5.   
  6.         int hash = (key == null) ? 0 : hash(key);  
  7.         for (Entry e = table[indexFor(hash, table.length)];  
  8.              e != null;  
  9.              e = e.next) {  
  10.             Object k;  
  11.             if (e.hash == hash &&  
  12.                 ((k = e.key) == key || (key != null && key.equals(k))))  
  13.                 return e;  
  14.         }  
  15.         return null;  
  16.     }  

6)containsValue方法就比较粗暴了,就是直接遍历所有元素直到找到value,由此可见HashMap的containsValue方法本质上和普通数组和list的contains方法没什么区别,你别指望它会像containsKey那么高效
 
   
   
[java] view plain copy 在CODE上查看代码片派生到我的代码片
  1. public boolean containsValue(Object value) {  
  2.         // Same idea as size()  
  3.         if (value == null)  
  4.             throw new NullPointerException();  
  5.         final Segment[] segments = this.segments;  
  6.         boolean found = false;  
  7.         long last = 0;  
  8.         int retries = -1;  
  9.         try {  
  10.             outer: for (;;) {  
  11.                 if (retries++ == RETRIES_BEFORE_LOCK) {  
  12.                     for (int j = 0; j < segments.length; ++j)  
  13.                         ensureSegment(j).lock(); // force creation  
  14.                 }  
  15.                 long hashSum = 0L;  
  16.                 int sum = 0;  
  17.                 for (int j = 0; j < segments.length; ++j) {  
  18.                     HashEntry[] tab;  
  19.                     Segment seg = segmentAt(segments, j);  
  20.                     if (seg != null && (tab = seg.table) != null) {  
  21.                         for (int i = 0 ; i < tab.length; i++) {  
  22.                             HashEntry e;  
  23.                             for (e = entryAt(tab, i); e != null; e = e.next) {  
  24.                                 V v = e.value;  
  25.                                 if (v != null && value.equals(v)) {  
  26.                                     found = true;  
  27.                                     break outer;  
  28.                                 }  
  29.                             }  
  30.                         }  
  31.                         sum += seg.modCount;  
  32.                     }  
  33.                 }  
  34.                 if (retries > 0 && sum == last)  
  35.                     break;  
  36.                 last = sum;  
  37.             }  
  38.         } finally {  
  39.             if (retries > RETRIES_BEFORE_LOCK) {  
  40.                 for (int j = 0; j < segments.length; ++j)  
  41.                     segmentAt(segments, j).unlock();  
  42.             }  
  43.         }  
  44.         return found;  
  45.     }  
2 HashTable源码分析

1)构造方法有三个 ,带零个、一个、两个参数的,最终都会调用两个参数的构造方法
其思想也比较简单,检查参数的合法性

 public Hashtable(int initialCapacity, float loadFactor) {
if (initialCapacity <0)
throw new IllegalArgumentException("Illegal Capacity: "+
initialCapacity);
if (loadFactor <= 0 || Float.isNaN(loadFactor))
throw new IllegalArgumentException("Illegal Load: "+loadFactor);

if (initialCapacity==0)
initialCapacity = 1;
this.loadFactor = loadFactor;
table = new Entry[initialCapacity];
threshold = (int)Math.min(initialCapacity * loadFactor, MAX_ARRAY_SIZE + 1);
initHashSeedAsNeeded(initialCapacity);
}

2)put操作,直接在方法的前面加上synchronized 关键字,简单粗暴,在并发激烈的情况下,效率较低,所以才会有
ConcurrentHashMap的诞生。

public synchronized V put(K key, V value) {        // Make sure the value is not null        if (value == null) {            throw new NullPointerException();        }        // Makes sure the key is not already in the hashtable.        Entry tab[] = table;        int hash = hash(key);        int index = (hash & 0x7FFFFFFF) % tab.length;        for (Entry e = tab[index] ; e != null ; e = e.next) {            if ((e.hash == hash) && e.key.equals(key)) {                V old = e.value;                e.value = value;                return old;            }        }        modCount++;        if (count >= threshold) {            // Rehash the table if the threshold is exceeded            rehash();            tab = table;            hash = hash(key);            index = (hash & 0x7FFFFFFF) % tab.length;        }        // Creates the new entry.        Entry e = tab[index];        tab[index] = new Entry<>(hash, key, value, e);        count++;        return null;    }

3)get操作也一样,直接在访问该方法的时候上锁,确保同一个时刻只有一个线程能访问

    public synchronized V get(Object key) {        Entry tab[] = table;        int hash = hash(key);        int index = (hash & 0x7FFFFFFF) % tab.length;        for (Entry e = tab[index] ; e != null ; e = e.next) {            if ((e.hash == hash) && e.key.equals(key)) {                return e.value;            }        }        return null;    }

4)其他方法就不说了,基本更HashMap里面的方法一样,只是加上同步机制而已





3自己实现的一个简单HashMap
public class MyHashMap {  

//默认初始化大小 16
private static final int DEFAULT_INITIAL_CAPACITY = 16;
//默认负载因子 0.75
private static final float DEFAULT_LOAD_FACTOR = 0.75f;

//临界值
private int threshold;

//元素个数
private int size;

//扩容次数
private int resize;

private HashEntry[] table;

public MyHashMap() {
table = new HashEntry[DEFAULT_INITIAL_CAPACITY];
threshold = (int) (DEFAULT_INITIAL_CAPACITY * DEFAULT_LOAD_FACTOR);
size = 0;
}

private int index(Object key) {
//根据key的hashcode和table长度取模计算key在table中的位置
return key.hashCode() % table.length;
}

public void put(Object key, Object value) {
//key为null时需要特殊处理,为简化实现忽略null值
if (key == null) return;
int index = index(key);

//遍历index位置的entry,若找到重复key则更新对应entry的值,然后返回
HashEntry entry = table[index];
while (entry != null) {
if (entry.getKey().hashCode() == key.hashCode() && (entry.getKey() == key || entry.getKey().equals(key))) {
entry.setValue(value);
return;
}
entry = entry.getNext();
}
//若index位置没有entry或者未找到重复的key,则将新key添加到table的index位置
add(index, key, value);
}

private void add(int index, Object key, Object value) {
//将新的entry放到table的index位置第一个,若原来有值则以链表形式存放
HashEntry entry = new HashEntry(key, value, table[index]);
table[index] = entry;
//判断size是否达到临界值,若已达到则进行扩容,将table的capacicy翻倍
if (size++ >= threshold) {
resize(table.length * 2);
}
}

private void resize(int capacity) {
if (capacity <= table.length) return;

HashEntry[] newTable = new HashEntry[capacity];
//遍历原table,将每个entry都重新计算hash放入newTable中
for (int i = 0; i HashEntry old = table[i];
while (old != null) {
HashEntry next = old.getNext();
int index = index(old.getKey());
old.setNext(newTable[index]);
newTable[index] = old;
old = next;
}
}
//用newTable替table
table = newTable;
//修改临界值
threshold = (int) (table.length * DEFAULT_LOAD_FACTOR);
resize++;
}

public Object get(Object key) {
//这里简化处理,忽略null值
if (key == null) return null;
HashEntry entry = getEntry(key);
return entry == null ? null : entry.getValue();
}

public HashEntry getEntry(Object key) {
HashEntry entry = table[index(key)];
while (entry != null) {
if (entry.getKey().hashCode() == key.hashCode() && (entry.getKey() == key || entry.getKey().equals(key))) {
return entry;
}
entry = entry.getNext();
}
return null;
}

public void remove(Object key) {
if (key == null) return;
int index = index(key);
HashEntry pre = null;
HashEntry entry = table[index];
while (entry != null) {
if (entry.getKey().hashCode() == key.hashCode() && (entry.getKey() == key || entry.getKey().equals(key))) {
if (pre == null) table[index] = entry.getNext();
else pre.setNext(entry.getNext());
//如果成功找到并删除,修改size
size--;
return;
}
pre = entry;
entry = entry.getNext();
}
}

public boolean containsKey(Object key) {
if (key == null) return false;
return getEntry(key) != null;
}

public int size() {
return this.size;
}

public void clear() {
for (int i = 0; i table[i] = null;
}
this.size = 0;
}


@Override
public String toString() {
StringBuilder sb = new StringBuilder();
sb.append(String.format("size:%s capacity:%s resize:%s\n\n", size, table.length, resize));
for (HashEntry entry : table) {
while (entry != null) {
sb.append(entry.getKey() + ":" + entry.getValue() + "\n");
entry = entry.getNext();
}
}
return sb.toString();
}
}

class HashEntry {
private final Object key;
private Object value;
private HashEntry next;

public HashEntry(Object key, Object value, HashEntry next) {
this.key = key;
this.value = value;
this.next = next;
}

public Object getKey() {
return key;
}

public Object getValue() {
return value;
}

public void setValue(Object value) {
this.value = value;
}

public HashEntry getNext() {
return next;
}

public void setNext(HashEntry next) {
this.next = next;
}
}




推荐阅读
  • HashTable与ConcurrentHashMap均可实现HashMap的功能,对外提供了键值对存储的数据结构。但是在内部结构及实现上有何区别,性能上的差异到底在哪里又是如何导致的 ... [详细]
  • 这篇“HashMap实例分析”文章的知识点大部分人都不太理解,所以小编给大家总结了以下内容,内容详细,步骤清晰,具有一定的借鉴价值,希望大家阅 ... [详细]
  • 集合类中只能存放对象,而不能存放原始数据类型的元素,所以当有原始数据类型需要存放时,只能将其转换成相应的包装类对象。1)访问和遍历数组元素时,ArrayList的 ... [详细]
  • 根据官方定义,RxJava是一种用于异步编程和可观察数据流的API。其核心特性在于流式处理能力和丰富的操作符支持。 ... [详细]
  • Map主要用于存储健值对,根据键得到值,因此不允许键重复(重复了覆盖了),但允许值重复hashMap是hashtable的轻量级实现(非线程安全的实现),他们都完成了Map接口,主要区 ... [详细]
  • Java之HashMap在多线程情况下导致死循环的问题
    PS:不得不说Java编程思想这本书是真心强大..学习内容:1.HashMap<K,V>在多线程的情况下出现的死循环现象当初学Java的时候只是知道HashMap< ... [详细]
  • 缓存这个东西就是为了提高运行速度的,由于缓存是在寸土寸金的内存里面,不是在硬盘里面,所以容量是很有限的。LRU这个算法就是把最近一次使用时间离现在时间最远的数据删除掉。先说说List:每 ... [详细]
  • 本篇文章给大家分享的是有关Java中怎么对HashMap按键值排序,小编觉得挺实用的,因此分享给大家学习,希望大家阅读完这篇文章后可以有所收获,话 ... [详细]
  • hashmap线程不安全允许有null的键和值效率高一点、方法不是Synchronize的要提供外同步有containsvalue和containsKey方法HashMap是Java1 ... [详细]
  • HashMap JDK1.8原理分析
    HashMap、Hashtable、LinkedHashMap和TreeMap下面针对各个实现类的特点做一些说明:(1)HashMap:它根据键的hashCode值存储数据,大多数 ... [详细]
  • 01Map集合概述A:Map集合概述:我们通过查看Map接口描述,发现Map接口下的集合与Collection接口下的集合,它们存储数据的形式不同a:Collection中的集合 ... [详细]
  • 手写HashMap,快手面试官直呼内行
    手写HashMap,快手面试官直呼内行-手写HashMap?这么狠,面试都卷到这种程度了?第一次见到这个面试题,是在某个不方便透露姓名的Offer收割机大佬的文章:这……我当 ... [详细]
  • Java面试 HashMap、HashSet源码解析
    本章所有源代码基于JDK1.8版本HashMap和HashSet是JavaCollectionFramework的两个重要成员,其中HashMap是Map接口的常用实现类,Hash ... [详细]
  • HashMap的介绍以及扩容为什么是2的n次幂
    本篇内容介绍了“HashMap的介绍以及扩容为什么是2的n次幂”的有关知识,在实际案例的操作过程中,不少人都会遇到这样的困境,接下来就让小编带 ... [详细]
  • 在Android应用开发过程中,经常要对图片做cache处理,比如从网络上下载图片后保存到sd卡里,然后放到HashMap<String,Bitmap>里同过ListView显示,当Has ... [详细]
author-avatar
一直都在囚禁
这个家伙很懒,什么也没留下!
PHP1.CN | 中国最专业的PHP中文社区 | DevBox开发工具箱 | json解析格式化 |PHP资讯 | PHP教程 | 数据库技术 | 服务器技术 | 前端开发技术 | PHP框架 | 开发工具 | 在线工具
Copyright © 1998 - 2020 PHP1.CN. All Rights Reserved | 京公网安备 11010802041100号 | 京ICP备19059560号-4 | PHP1.CN 第一PHP社区 版权所有