`
wen866595
  • 浏览: 268991 次
  • 性别: Icon_minigender_1
  • 来自: 广州
社区版块
存档分类
最新评论

java.util.HashMap 源码解读及其进化

    博客分类:
  • java
阅读更多

 

原文: http://coderbee.net/index.php/java/20131018/519

 

概述

java.util.HashMap 是JDK里散列的一个实现,JDK6里采用位桶+链表的形式实现,Java8里采用的是位桶+链表/红黑树的方式,非线程安全。关于散列可以看这篇文章

 

这篇文章主要是对JDK6和Java8里java.util.HashMap的一些源码的解读。Java8里的改进主要是为了解决哈希碰撞攻击。

 

这个源码解读主要关注基础数据结构、put(key,value)逻辑 和遍历所有键值对的逻辑。

 

JDK1.6

基础数据结构

先看下JDK1.6里面HashMap的数据结构,先是链表的结构:

static class Entry<K,V> implements Map.Entry<K,V> {
    final K key ;
    V value;
    Entry<K,V> next;  // 指向链表的下一个节点。
    int hash ;    // 根据key的hashCode计算得到,即使进行resize也不会改变。
        //  其他方法省略
}

单向链表。

 

位桶的结构:

public class HashMap<K, V> extends AbstractMap<K, V> implements Map<K, V>,
   Cloneable, Serializable {

     // 位桶,每个键值对都封装为Entry结构。
     // 位桶会初始化为一个大于用户指定的初始容量的initialCapacity的
   transient Entry<K,V>[] table;

     // HashMap发生结构性修改的次数,也用于判断是否存在并发修改或迭代视图的快速失败,
     // 如果存在这两种情况,抛出ConcurrentModificationException异常。
     // 用volatile保证了内存可见性
   transient volatile int modCount;

     // 实际存储的对键值对的数量
   transient int size;

     // 负载因子
     final float loadFactor;

     // 当键值对的数量达到或超过这个阀值时,将对位桶进行扩展,再把所有元素重新散列到新位桶。
     int threshold;

      // 下面从构造函数看看上面几个属性的关系。
     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);

          // 位桶的长度必须是2的幂,大于initialCapacity的最小的且是2的幂的数。
          // Find a power of 2 >= initialCapacity
          int capacity = 1;
          while (capacity < initialCapacity)
               capacity <<= 1;

          this.loadFactor = loadFactor;                                  
          threshold = ( int) (capacity * loadFactor); // 每次resize都会改变
          table = new Entry[capacity];
          init();          // 空方法,留给子类实现
     }

     // 其他方法和常量省略
}

 

基础方法

// 计算hash值h在长度为length的位桶的位置。
static int indexFor(int h, int length) {
     // 用位运算替代求模,这就是为什么位桶的长度必须是2的幂。
     return h & (length - 1);
}

// 根据key的hashCode h来计算一个新的hash值。
// 按源码里的说明,只用key的hashCode的低位字节容易碰撞冲突。
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);
}

 

put方法

public V put(K key, V value) {
     // 由于HashMap运行key为空,所以需要对key为null的进行特殊处理。
     if (key == null)
          return putForNullKey(value);

     // 根据key的hashCode h来计算一个新的hash值。
     int hash = hash(key.hashCode());

     // 确定key所在位桶的位置。
     int i = indexFor(hash, table.length);

     // 遍历链表,hashCode相等的不一定就是同一个对象。
     for (Entry<K, V> e = table[i]; e != null; e = e.next) {
          Object k;
          // hashCode相等的不一定就是同一个对象。
          // 先判断对象引用相等,一般情况下更新一个key-value的key
         //    都是同一个对象的,所以可以减少一次方法调用。
          if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {
               // 如果key已存在,则更新对应的值
               V oldValue = e.value;
               e.value = value;
               e.recordAccess( this);     // 空方法
               return oldValue;
          }
     }

     // 更新计数器,如果正在迭代,则可以快速失败,
     // 抛出ConcurrentModificationException异常。
     modCount++;     
     addEntry(hash, key, value, i);     // 添加新节点
     return null ;
}

private V putForNullKey(V value) {
     // key为null的Entry总是放在位桶0。
     for (Entry<K, V> e = table[0]; e != null; e = e.next) {
           if (e.key == null) {
               V oldValue = e.value;
               e.value = value;
               e.recordAccess( this);
               return oldValue;
          }
     }
     modCount++;
     addEntry(0, null, value, 0);
     return null ;
}
// 感觉对key为null的Entry比较蛋疼,如果这个Entry作为一个独立的属性,
// 则在遍历的时候需要特殊处理下。


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);
}

get方法

 

与put方法的逻辑很相似。

resize方法

resize时原有的Entry不会变,直接散列到新的位桶。

void resize(int newCapacity) {
     Entry[] oldTable = table;
     int oldCapacity = oldTable.length;
     if (oldCapacity == MAXIMUM_CAPACITY) {
          threshold = Integer.MAX_VALUE;
          return;
     }

     Entry[] newTable = new Entry[newCapacity]; // 创建指定容量的新的位桶
     transfer(newTable);                  // 把Entry散列到新的位桶
     table = newTable;
     threshold = ( int) (newCapacity * loadFactor);// 计算新的阀值
}

void transfer(Entry[] newTable) {
     // 这里把对象的属性赋值给本地变量是为了提升性能,方法的本地变量是存放在方法栈上的,
     // 对象的属性是存放在堆上的,访问栈比访问堆要高效。这种做法在很多地方都可以看到。
     Entry[] src = table;
     int newCapacity = newTable.length;
     for (int j = 0; j < src.length; j++) {     // 遍历位桶
          Entry<K, V> e = src[j];
          if (e != null) {
               src[j] = null;
               // 遍历链表
               do {
                    Entry<K, V> next = e.next;
                    // 计算Entry在新位桶的位置
                    int i = indexFor(e.hash, newCapacity);
                    // 下面两行把Entry插到链表的首部
                    e.next = newTable[i];
                    newTable[i] = e;
                    e = next;
               } while (e != null);
          }
     }
}

逻辑不算复杂。

 

遍历

这里只说遍历entrySet,它是通过一个HashIterator的内部类来实现的。

private abstract class HashIterator<E> implements Iterator<E> {
     Entry<K, V> next; // 待返回的下一个entry
     int expectedModCount; // 用于快速失败
     int index; // 遍历的当前槽位,默认从0号槽位开始
     Entry<K, V> current; // 当前已返回的最后的entry

     HashIterator() {
          expectedModCount = modCount;
          if (size > 0) { // advance to first entry
               Entry[] t = table;
              //  定位到第一个不为null的Entry
               while (index < t.length && (next = t[index++]) == null)
                    ;
          }
     }

     public final boolean hasNext() {
          return next != null;
     }

     final Entry<K, V> nextEntry() {
          // 检测是否存在并发修改。
          if (modCount != expectedModCount)
               throw new ConcurrentModificationException();

          Entry<K, V> e = next;

          // 防止不调用hasNext()方法判断是否还有更多元素而直接调用next()方法的情况。
          if (e == null)  
               throw new NoSuchElementException();

          if ((next = e.next) == null) {
               // 当前所在链表遍历完成,定位到下一个不为null的entry
               Entry[] t = table;
               while (index < t.length && (next = t[index++]) == null)
                    ;
           }
          current = e;
          return e;
     }

     // 其他方法省略
}

在Java8的进化

说明下:这个是基于Java 8 EA 版里的源码。

Java 8 里HashMap的一个重大改进是为了解决哈希碰撞攻击,这个哈希碰撞攻击简单说就是通过精心构建一组key,使这些key都映射到同一个槽位,使整个HashMap退化为一个单链表,这会大大劣化HashMap的操作性能,更多信息可以google之。

改进主要有下面这些:

  • 对于key为null的entry,放在独立的属性里。
  • 当某个槽位的链表的长度达到某个阀值时,这个链表将被转换为红黑树。
  • modCount去除了volatile修饰符,毕竟这个类是声明为非线程安全的。volatile变量的读写需要一些内存关卡,开销大点。
  • 增加了一个hashSeed属性,用于提升key散列到槽的随机性。

在下面的代码逻辑里,红黑树内部如何处理,这里不涉及。

基础数据结构

// 链表节点
static class Entry<K, V> implements Map.Entry<K, V> {
     final K key;
     V value;
     Object next; // 可以是 Entry 或 TreeNode,类型不再是Entry
     final int hash;

     // 其他方法省略
}

// 红黑树的节点
final static class TreeNode<K, V> {
     TreeNode parent; // red-black tree links
     TreeNode left;
     TreeNode right;
     TreeNode prev; // needed to unlink next upon deletion
     boolean red;
     final HashMap.Entry<K, V> entry;     // 

     // 其他方法省略
}


// 红黑树
final class TreeBin {
     static final int TREE_THRESHOLD = 16;  // 链表长度达到这个阀值时将转换为红黑树
     TreeNode<K, V> root; // 红黑树的根节点
     TreeNode<K, V> first; // 有序遍历红黑树时的第一个节点
     // 其他方法省略
}

// HashMap的主要属性
public class HashMap<K, V> extends AbstractMap<K, V> implements Map<K, V>,
               Cloneable, Serializable {
     // 省略一些常量定义
     // 槽数组的类型变为Object,不再是Entry,因为槽还可以放TreeBin了。
     transient Object[] table = EMPTY_TABLE;
     transient int size;
     int threshold;
     final float loadFactor;

     // 去除了volatile修饰符,毕竟这个类是声明为非线程安全的。
     // volatile变量的读写需要一些内存关卡,开销大点
     transient int modCount;

     // 随机种子,增加key散列到槽的随机性。
     transient final int hashSeed;

     // 存储key为null的entry。
     transient Entry<K, V> nullKeyEntry = null;

     // 其他方法省略
}

基础方法

final int hash(Object k) {
     // hashSeed 是每个实例一个的,用于增加随机性。
     int h = hashSeed ^ k.hashCode();     

     // 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);
}

// indexFor 方法没有改变
static int indexFor(int h, int length) {
     // assert Integer.bitCount(length) == 1 :
     // "length must be a non-zero power of 2";
     return h & (length - 1);
}

put方法

public V put(K key, V value) {
     // 如果草数组为空则初始化
     if (table == EMPTY_TABLE) {
          inflateTable(threshold);
     }
     // 特殊处理key为null的键值对
     if (key == null)
          return putForNullKey(value);

     // 根据key计算哈希值
     int hash = hash(key);

     // 定位槽位
     int i = indexFor(hash, table.length);

     boolean checkIfNeedTree = false; // 是否需要把链表转换为 TreeBin?

     if (table[i] instanceof Entry) {// 是个链表
          // Bin contains ordinary Entries. Search for key in the linked list
          // of entries, counting the number of entries. Only check for
          // TreeBin conversion if the list size is >= TREE_THRESHOLD.
          // (The conversion still may not happen if the table gets resized.)
          int listSize = 0;     // 计算链表长度
          Entry<K, V> e = (Entry<K, V>) table[i];
          for (; e != null; e = (Entry<K, V>) e.next) {
               Object k;
               // 查找key,如果找到则更新对应的值
               if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {
                    V oldValue = e.value;
                    e.value = value;
                    e.recordAccess( this);
                    return oldValue;
               }
               listSize++;
          }
          // 没有找到,需要添加Entry
          // 判断是否需要把链表转换为红黑树
          checkIfNeedTree = listSize >= TreeBin.TREE_THRESHOLD;

     } else if (table[i] != null) {
          // 这个槽为存放的不是链表,且不为空,则必然是红黑树
          TreeBin e = (TreeBin) table[i];
          TreeNode p = e.putTreeNode(hash, key, value, null);
          if (p == null) { // putTreeNode() 添加了一个新节点
               modCount++;
               size++;
               // 如果键值对的数量已达到阀值,重散列
               if (size >= threshold) {
                    resize(2 * table.length);
               }
               return null ;
          } else { // putTreeNode() 发现这个key已存在
               Entry<K, V> pEntry = (Entry<K, V>) p.entry;
               V oldVal = pEntry.value;
               pEntry.value = value;
               pEntry.recordAccess( this);
               return oldVal;
          }
     }
     modCount++;
     // 槽位为空或在链表里没有找到对应的key,新增节点。
     addEntry(hash, key, value, i, checkIfNeedTree);
     return null ;
}

void addEntry(int hash, K key, V value, int bucketIndex,
                boolean checkIfNeedTree) {
     // assert key != null;
     if ((size >= threshold) && (null != table[bucketIndex])) {
          resize(2 * table.length);
          hash = hash(key);  // 应该不需要重新计算哈希值啊??
          bucketIndex = indexFor(hash, table.length);
     }
     createEntry(hash, key, value, bucketIndex, checkIfNeedTree);
}

void createEntry(int hash, K key, V value, int bucketIndex,
               boolean checkIfNeedTree) {
      // assert key != null;
     @SuppressWarnings( "unchecked")
     Entry<K, V> e = (Entry<K, V>) table[bucketIndex];
     // 把新的Entry查到链表首部先
     table[bucketIndex] = newEntry(hash, key, value, e);
     size++;

     if (checkIfNeedTree) {// 需要把链表转换为红黑树
          int listSize = 0;
          for (e = (Entry<K, V>) table[bucketIndex]; e != null; e = (Entry<K, V>) e.next) {
               listSize++;
               if (listSize >= TreeBin.TREE_THRESHOLD) { // Convert to TreeBin
                    // 如果key是String类型或实现了java.lang.Comparable接口才转换。
                    if (comparableClassFor(key) != null) {
                         TreeBin t = new TreeBin();// 创建一个红黑树
                         t.populate((Entry) table[bucketIndex]);// 把链表里的Entry填充到红黑树
                         table[bucketIndex] = t;// 把红黑树放到槽位上。
                    }
                    break;
               }
          }
     }
}

// 用独立的Entry来存储key为null的情况,简单多了。
private V putForNullKey(V value) {
     if (nullKeyEntry != null) {
          V oldValue = nullKeyEntry.value;
          nullKeyEntry.value = value;
          nullKeyEntry.recordAccess( this);
          return oldValue;
     }
     modCount++;
     size++; // newEntry() skips size++
     nullKeyEntry = newEntry(0, null, value, null);
     return null ;
}

get逻辑

final Entry<K, V> getEntry(Object key) {
     // 没有键值对
     if (size == 0) {
          return null ;
     }
     // key为null,直接返回对应的Entry
     if (key == null) {
          return nullKeyEntry;
     }

     // 计算哈希值,定位槽位
     int hash = hash(key);
     int bin = indexFor(hash, table.length);

     if (table[bin] instanceof Entry) {
          // 槽位存放的链表,逻辑与旧的相同
          Entry<K, V> e = (Entry<K, V>) table[bin];
          for (; e != null; e = (Entry<K, V>) e.next) {
               Object k;
               if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {
                    return e;
               }
          }
     } else if (table[bin] != null) {
          // 存放的是红黑树,委托给红黑树去查找
          TreeBin e = (TreeBin) table[bin];
          TreeNode p = e.getTreeNode(hash, (K) key);
          if (p != null) {
               // assert p.entry.hash == hash && p.entry.key.equals(key);
               return (Entry<K, V>) p.entry;
          } else {
               return null ;
          }
     }
     return null ;
}

遍历所有Entry的逻辑

遍历所有Entry仍然借助类HashIterator。

private abstract class HashIterator<E> implements Iterator<E> {
     Object next; // 下一个待返回的键值对,可以是Entry 或 TreeNode
     int expectedModCount; // 用于快速失败
     int index; // 当前遍历的槽位,默认从槽位0开始
     Object current; // 当前返回的最后一个键值对,可以是 Entry 或 TreeNode

     HashIterator() {
          expectedModCount = modCount;
          if (size > 0) { // advance to first entry
               if (nullKeyEntry != null) {
                    // nullKeyEntry作为一个独立属性,不会下一个节点,需要特殊处理,优先返回。
                    next = nullKeyEntry;
               } else {
                    findNextBin();
               }
          }
     }

     public final boolean hasNext() {
          return next != null;
     }

     @SuppressWarnings( "unchecked")
     final Entry<K, V> nextEntry() {
          if (modCount != expectedModCount) {
               throw new ConcurrentModificationException();
          }
          Object e = next;
          Entry<K, V> retVal;

          if (e == null)
               throw new NoSuchElementException();

          if (e instanceof TreeNode) { // 正在遍历红黑树
               retVal = (Entry<K, V>) ((TreeNode) e).entry;
               next = retVal.next;
          } else { // 正在遍历链表
               retVal = (Entry<K, V>) e;
               next = ((Entry<K, V>) e).next;
          }

          // 如果当前链表或红黑树已遍历完,定位到下一个槽。
          if (next == null) {
               findNextBin();
          }
          current = e;
          return retVal;
     }

     private void findNextBin() {
          // assert next == null;
          Object[] t = table;

          // 定位到下一个不为空的槽位
          while (index < t.length && (next = t[index++]) == null)
               ;
          if (next instanceof HashMap.TreeBin) {
               // 如果槽位上存放的是红黑树,则用next指向红黑树的第一个节点。
               next = ((TreeBin) next).first;
               // assert next != null; // There should be no empty TreeBins
          }
     }

     // 其他方法省略
}

 

 

0
0
分享到:
评论

相关推荐

    java.util.Date与java.sql.Date互转及字符串转换为日期时间格式.docx

    ### Java.util.Date与Java.sql.Date互转及字符串转换为日期时间格式 #### 一、Java.util.Date与Java.sql.Date的基本概念 在Java编程语言中,处理日期和时间时经常使用到`java.util.Date`和`java.sql.Date`这两个类...

    java.util.Date与java.sql.Date相互转换

    ### Java.util.Date与Java.sql.Date相互转换 #### 知识点概述 在Java开发中,经常需要处理日期和时间相关的操作。Java标准库提供了两个重要的日期类:`java.util.Date` 和 `java.sql.Date`。虽然它们名字相似,但...

    用java.util.zip包现数据压缩与解压

    ### 使用 Java.util.zip 包实现数据压缩与解压 在计算机科学领域,数据压缩技术是一项重要的功能,它能够帮助减少存储空间的需求以及提高网络传输效率。本文将通过一系列的示例来详细介绍如何利用 Java 中的 `java....

    java并发工具包 java.util.concurrent中文版用户指南pdf

    1. java.util.concurrent - Java 并发工具包 2. 阻塞队列 BlockingQueue 3. 数组阻塞队列 ArrayBlockingQueue 4. 延迟队列 DelayQueue 5. 链阻塞队列 LinkedBlockingQueue 6. 具有优先级的阻塞队列 ...

    java.util.ConcurrentModificationException 解决方法

    java.util.ConcurrentModificationException 解决方法 ... at java.util.HashMap$HashIterator.nextEntry(HashMap.java:793) at java.util.HashMap$KeyIterator.next(HashMap.java:828) 例如以下程序(转

    Tomcat内存溢出的解决方法(java.util.concurrent.ExecutionException)

    "java.util.concurrent.ExecutionException: java.lang.OutOfMemoryError" 是一个典型的错误提示,它表明在并发执行过程中遇到了内存不足的问题。下面我们将深入探讨这个问题的原因、影响以及如何解决。 内存溢出...

    java.util.ConcurrentModificationException 异常问题详解1

    Java.util.ConcurrentModificationException 异常问题详解 ConcurrentModificationException 异常是 Java 中一个常见的异常,它发生在 Iterator 遍历集合时,集合同时被修改引起的异常。在 Java 中,集合类如 ...

    java.util包源码pdf版

    通过阅读`java.util`包源码,不仅可以深入了解Java集合框架的内部工作原理,还能学习到高级的数据结构设计模式。对于想要提高自己编程技能的开发者来说,这是一个非常宝贵的学习资源。同时,熟悉这些源码也有助于在...

    java+apache完成zip压缩源码(包括修改后的java.util.zip下的源码)

    为了支持中文文件名,我们需要对`java.util.zip`下的源码进行修改,尤其是`ZipOutputStream`类。主要的修改点可能在于设置正确的字符编码,例如使用`UTF-8`。在创建`ZipEntry`对象时,需要确保文件名被正确地转换为...

    java.util包

    Java提供日期(Data)类、日历(Calendar)类,随机数(Random)类,堆栈(Stack)、向量(Vector) 、位集合(Bitset)以及哈希表(Hashtable)等类来表示相应的数据结构

    java.util.logging.Logger使用详解

    ### Java.util.logging.Logger 使用详解 #### 一、创建Logger对象 在Java中,`java.util.logging.Logger` 是标准的日志框架之一,它提供了基础的日志记录功能。为了使用这一功能,首先需要获得 `java.util.logging...

    java.util.pdf

    标题“java.util.pdf”暗示这是一个关于Java编程语言中util包的文档。由于描述和标签均重复标题,我们可以推断文档重点在于解释和示例展示java.util包中的类与接口。java.util是Java的标准库中的一个包,主要用于...

    java.sql.与java.util

    Java编程语言提供了两个重要的日期处理类,分别是`java.util.Date`和`java.sql.Date`,它们在处理日期和时间上有着不同的特性和用途。 `java.util.Date`是更通用的日期时间类,它包含了日期和时间的信息,可以精确...

    java.sql.date与java.util.date.pdf

    Java.sql.Date与Java.util.Date的区别和转换 Java.util.Date和Java.sql.Date是Java中两种不同的日期和时间表示方式,虽然它们都是表示日期和时间,但是它们之间存在着一些重要的区别。 首先,Java.util.Date是Java...

    java base64源码+jar包

    在Java中,Base64编码和解码通常通过`java.util.Base64`类进行操作,该类自Java 8开始引入。这个类提供了多种方法,如`encodeBytes()`用于编码字节数组,`decode()`用于解码Base64字符串。然而,描述中提到的是一个...

    无法解析类型 java.util.Map$Entry。从必需的 .class 文件间接引用了它

    这是我在编写struts2中遇到的问题,整理出来,包括截图,希望可以帮到大家

Global site tag (gtag.js) - Google Analytics