Java源码角度分析HashMap用法

—HashMap—

优点:超级快速的查询速度,时间复杂度可以达到O(1)的数据结构非HashMap莫属。动态的可变长存储数据(相对于数组而言)。

缺点:需要额外计算一次hash值,如果处理不当会占用额外的空间。

—HashMap如何使用—

平时我们使用hashmap如下

Map<Integer,String> maps=new HashMap<Integer,String>();
maps.put(1, "a");
maps.put(2, "b");

上面代码新建了一个HashMap并且插入了两个数据,这里不接受基本数据类型来做K,V

如果这么写的话,就会出问题了:

Map<int,double> maps=new HashMap<int,double>();

我们为什么要这样使用呢?请看源码:

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

这是HashMap实现类的定义。

—HashMap是一个动态变长的数据结构—

在使用HashMap的时候,为了提高执行效率,我们往往会设置HashMap初始化容量:

Map<String,String> rm=new HashMap<String,String>(2)

或者使用guava的工具类Maps,可以很方便的创建一个集合,并且,带上合适的大小初始化值。

Map<String, Object> map = Maps.newHashMapWithExpectedSize(7);

那么为什么要这样使用呢?我们来看他们的源码构造函数。

未带参的构造函数:

public HashMap() {
    this.loadFactor = DEFAULT_LOAD_FACTOR;
    threshold = (int)(DEFAULT_INITIAL_CAPACITY * DEFAULT_LOAD_FACTOR);
    table = new Entry[DEFAULT_INITIAL_CAPACITY];
    init();
  } 

public HashMap() {
this.loadFactor = DEFAULT_LOAD_FACTOR;
threshold = (int)(DEFAULT_INITIAL_CAPACITY * DEFAULT_LOAD_FACTOR);
table = new Entry[DEFAULT_INITIAL_CAPACITY];
init();
}

/**
   * Constructs an empty <tt>HashMap</tt> with the specified initial
   * capacity and the default load factor (0.75).
   *
   * @param initialCapacity the initial capacity.
   * @throws IllegalArgumentException if the initial capacity is negative.
   */
  public HashMap(int initialCapacity) {
    this(initialCapacity, DEFAULT_LOAD_FACTOR);
  }

名词解释:

DEFAULT_LOAD_FACTOR  //默认加载因子,如果不制定的话是0.75
DEFAULT_INITIAL_CAPACITY //默认初始化容量,默认是16
threshold //阈(yu)值 根据加载因子和初始化容量计算得出 ,<span style="color: rgb(54, 46, 43); font-family: "microsoft yahei";">threshold表示当HashMap的size大于threshold时会执行resize操作。

因此我们知道了,如果我们调用无参数的构造方法的话,我们将得到一个16容量的数组。

所以问题就来了:如果初始容量不够怎么办?

数组是定长的,如何用一个定长的数据来表示一个不定长的数据呢,答案就是找一个更长的,但是在resize的时候是很降低效率的。所以我们建议HashMap的初始化的时候要给一个靠谱的容量大小。

—HashMap的Put方法—

public V put(K key, V value) {
    if (key == null) //键为空的情况,HashMap和HashTable的一个区别
      return putForNullKey(value);
    int hash = hash(key.hashCode()); //根据键的hashCode算出hash值
    int i = indexFor(hash, table.length); //根据hash值算出究竟该放入哪个数组下标中
    for (Entry<K,V> e = table[i]; e != null; e = e.next) {//整个for循环实现了如果存在K那么就替换V
      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;
  }

如果插入的数据超过现有容量就会执行

addEntry(hash, key, value, i);
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)
     <span style="color:#ff0000;"><strong> resize(2 * table.length);
}

这里显示了如果当前 size++ >threshold 的话那么就会扩展当前的size的两倍,执行resize(2*table.length),那么他们是如何扩展的呢?

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]; <span style="color: rgb(51, 51, 51); font-family: Arial;">new 一个新的数组,</span>
    <strong> <span style="color:#ff0000;">transfer(newTable);</span> </strong> //将就数组转移到新的数组中
    table = newTable;
    threshold = (int)(newCapacity * loadFactor);  //重新计算容量
  }

对于转移数组transfer是如何转移的呢?

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;
          int i = <strong><span style="color:#ff0000;">indexFor(e.hash, newCapacity);  //根据hash值个容量重新计算下标</span></strong>
          e.next = newTable[i];
          newTable[i] = e;
          e = next;
        } while (e != null);
      }
    }
  } 

—hashmap扩容额外执行次数—

因此如果我们要添加一个1000个元素的hashMap,如果我们用默认值那么我么需要额外的计算多少次呢

当大于16*0.75=12的时候,需要从新计算12次

当大于16*2*0.75=24的时候,需要额外计算24次

……

当大于16*n*0.75=768的时候,需要额外计算768次

所以我们总共在扩充过程中额外计算12+24+48+……+768次

因此强力建议我们在项目中如果知道范围的情况下,我们应该手动指定初始大小像这样:

Map<Integer,String> maps=new HashMap<Integer,String>(1000);

总结:这就是为什么当hashmap使用过程中如果超出 初始容量后他的执行效率严重下降的原因。

以上就是本文关于Java源码角度分析HashMap用法的全部内容,希望对大家有所帮助。感兴趣的朋友可以继续参阅本站其他相关专题,如有不足之处,欢迎留言指出。感谢朋友们对本站的支持!

您可能感兴趣的文章:

  • 剖析Java中HashMap数据结构的源码及其性能优化
  • 在Java8与Java7中HashMap源码实现的对比
  • 深入理解Java之HashMap源码剖析
  • Java集合框架源码分析之LinkedHashMap详解
  • Java中LinkedHashMap源码解析
(0)

相关推荐

  • 剖析Java中HashMap数据结构的源码及其性能优化

    存储结构 首先,HashMap是基于哈希表存储的.它内部有一个数组,当元素要存储的时候,先计算其key的哈希值,根据哈希值找到元素在数组中对应的下标.如果这个位置没有元素,就直接把当前元素放进去,如果有元素了(这里记为A),就把当前元素链接到元素A的前面,然后把当前元素放入数组中.所以在Hashmap中,数组其实保存的是链表的首节点.下面是百度百科的一张图: 如上图,每个元素是一个Entry对象,在其中保存了元素的key和value,还有一个指针可用于指向下一个对象.所有哈希值相同的key(也就

  • Java集合框架源码分析之LinkedHashMap详解

    LinkedHashMap简介 LinkedHashMap是HashMap的子类,与HashMap有着同样的存储结构,但它加入了一个双向链表的头结点,将所有put到LinkedHashmap的节点一一串成了一个双向循环链表,因此它保留了节点插入的顺序,可以使节点的输出顺序与输入顺序相同. LinkedHashMap可以用来实现LRU算法(这会在下面的源码中进行分析). LinkedHashMap同样是非线程安全的,只在单线程环境下使用. LinkedHashMap源码剖析 LinkedHashM

  • 深入理解Java之HashMap源码剖析

    一.HashMap概述 HashMap基于哈希表的 Map 接口的实现.此实现提供所有可选的映射操作,并允许使用 null 值和 null 键.(除了不同步和允许使用 null 之外,HashMap 类与 Hashtable 大致相同.)此类不保证映射的顺序,特别是它不保证该顺序恒久不变. 值得注意的是HashMap不是线程安全的,如果想要线程安全的HashMap,可以通过Collections类的静态方法synchronizedMap获得线程安全的HashMap. Map map = Coll

  • 在Java8与Java7中HashMap源码实现的对比

    一.HashMap的原理介绍 此乃老生常谈,不作仔细解说. 一句话概括之:HashMap是一个散列表,它存储的内容是键值对(key-value)映射. 二.Java 7 中HashMap的源码分析 首先是HashMap的构造函数代码块1中,根据初始化的Capacity与loadFactor(加载因子)初始化HashMap. //代码块1 public HashMap(int initialCapacity, float loadFactor) { if (initialCapacity < 0)

  • Java中LinkedHashMap源码解析

    概述: LinkedHashMap实现Map继承HashMap,基于Map的哈希表和链该列表实现,具有可预知的迭代顺序. LinedHashMap维护着一个运行于所有条目的双重链表结构,该链表定义了迭代顺序,可以是插入或者访问顺序. LintHashMap的节点对象继承HashMap的节点对象,并增加了前后指针 before after: /** * LinkedHashMap节点对象 */ static class Entry<K,V> extends HashMap.Node<K,V

  • Java源码角度分析HashMap用法

    -HashMap- 优点:超级快速的查询速度,时间复杂度可以达到O(1)的数据结构非HashMap莫属.动态的可变长存储数据(相对于数组而言). 缺点:需要额外计算一次hash值,如果处理不当会占用额外的空间. -HashMap如何使用- 平时我们使用hashmap如下 Map<Integer,String> maps=new HashMap<Integer,String>(); maps.put(1, "a"); maps.put(2, "b&quo

  • Java源码解析之HashMap的put、resize方法详解

    一.HashMap 简介 HashMap 底层采用哈希表结构 数组加链表加红黑树实现,允许储存null键和null值 数组优点:通过数组下标可以快速实现对数组元素的访问,效率高 链表优点:插入或删除数据不需要移动元素,只需要修改节点引用效率高 二.源码分析 2.1 继承和实现 public class HashMap<K,V> extends AbstractMap<K,V> implements Map<K,V>, Cloneable, Serializable {

  • 从源码角度分析Android的消息机制

    前言 说到Android的消息机制,那么主要的就是指的Handler的运行机制.其中包括MessageQueue以及Looper的工作过程. 在开始正文之前,先抛出两个问题: 为什么更新UI的操作要在主线程中进行? Android中为什么主线程不会因为Looper.loop()里的死循环卡死? UI线程的判断是在ViewRootImpl中的checkThread方法中完成的. 对于第一个问题,这里给一个简单的回答: 如果可以在子线程中修改UI,多线程的并发访问可能会导致UI控件的不可预期性,采用

  • Java源码深度分析String与StringBuffer及StringBuilder详解

    目录 StringBuffer和StringBuild的区别 创建StringBuffer() 添加功能 删除功能 替换功能 反转功能 最后总结一下 String的字符串是不可变的,StringBuffer和StringBuilder是可变的 String:是字符常量,适用于少量的字符串操作的情况. StringBuilder:适用于单线程下在字符缓冲区进行大量操作的情况 . StringBuffer:适用多线程下在字符缓冲区进行大量操作的情况. StringBuffer和StringBuild

  • Java面试题 从源码角度分析HashSet实现原理

    面试官:请问HashSet有哪些特点? 应聘者:HashSet实现自set接口,set集合中元素无序且不能重复: 面试官:那么HashSet 如何保证元素不重复? 应聘者:因为HashSet底层是基于HashMap实现的,当你new一个HashSet时候,实际上是new了一个map,执行add方法时,实际上调用map的put方法,value始终是PRESENT,所以根据HashMap的一个特性: 将一个key-value对放入HashMap中时,首先根据key的hashCode()返回值决定该E

  • JAVA面试题 从源码角度分析StringBuffer和StringBuilder的区别

    面试官:请问StringBuffer和StringBuilder有什么区别? 这是一个老生常谈的话题,笔者前几年每次面试都会被问到,作为基础面试题,被问到的概率百分之八九十.下面我们从面试需要答到的几个知识点来总结一下两者的区别有哪些? 继承关系? 如何实现的扩容? 线程安全性? 继承关系 从源码上看看类StringBuffer和StringBuilder的继承结构: 从结构图上可以直到,StringBuffer和StringBuiler都继承自AbstractStringBuilder类 如何

  • 通过JDK源码角度分析Long类详解

    概况 Java的Long类主要的作用就是对基本类型long进行封装,提供了一些处理long类型的方法,比如long到String类型的转换方法或String类型到long类型的转换方法,当然也包含与其他类型之间的转换方法.除此之外还有一些位相关的操作. Java long数据类型 long数据类型是64位有符号的Java原始数据类型.当对整数的计算结果可能超出int数据类型的范围时使用. long数据类型范围是-9,223,372,036,854,775,808至9,223,372,036,85

  • Java从JDK源码角度对Object进行实例分析

    Object是所有类的父类,也就是说java中所有的类都是直接或者间接继承自Object类.比如你随便创建一个classA,虽然没有明说,但默认是extendsObject的. 后面的三个点"..."表示可以接受若干不确定数量的参数.老的写法是Objectargs[]这样,但新版本的java中推荐使用...来表示.例如 publicvoidgetSomething(String...strings)(){} object是java中所有类的父类,也就是说所有的类,不管是自己创建的类还是

  • Java集合源码全面分析

    Java集合工具包位于Java.util包下,包含了很多常用的数据结构,如数组.链表.栈.队列.集合.哈希表等.学习Java集合框架下大致可以分为如下五个部分:List列表.Set集合.Map映射.迭代器(Iterator.Enumeration).工具类(Arrays.Collections). 从上图中可以看出,集合类主要分为两大类:Collection和Map. Collection是List.Set等集合高度抽象出来的接口,它包含了这些集合的基本操作,它主要又分为两大部分:List和Se

  • java 中Buffer源码的分析

    java 中Buffer源码的分析 Buffer Buffer的类图如下: 除了Boolean,其他基本数据类型都有对应的Buffer,但是只有ByteBuffer才能和Channel交互.只有ByteBuffer才能产生Direct的buffer,其他数据类型的Buffer只能产生Heap类型的Buffer.ByteBuffer可以产生其他数据类型的视图Buffer,如果ByteBuffer本身是Direct的,则产生的各视图Buffer也是Direct的. Direct和Heap类型Buff

随机推荐