首页 Java java教程 详解JavaHashMap实现原理

详解JavaHashMap实现原理

Jan 19, 2017 am 10:34 AM

HashMap是基于哈希表的Map接口实现,提供了所有可选的映射操作,并允许使用null值和null建,不同步且不保证映射顺序。下面记录一下研究HashMap实现原理。

HashMap内部存储

在HashMap内部,通过维护一个 瞬时变量数组table (又称:桶) 来存储所有的键值对关系,桶 是个Entry对象数组,桶 的大小可以按需调整大小,长度必须是2的次幂。如下代码:

/**
  * 一个空的entry数组,桶 的默认值
  */
 static final Entry<?,?>[] EMPTY_TABLE = {};
 /**
  * 桶,按需调整大小,但必须是2的次幂
  */
 transient Entry<K,V>[] table = (Entry<K,V>[]) EMPTY_TABLE;
登录后复制

初始容量与负载因子

HashMap有两个参数影响性能,初始容量和负载因子。容量是哈希表中 桶 的数量,初始容量只是哈希表在创建时的容量,负载因子是哈希表在其容量自动增加之前可以达到多满的一种尺度。当哈希表中条目数超出了负载因子与当前容量的乘积时,则要对该Hash表进行rehash操作(即重建内部数据结构),重建时以当前容量的两倍数目新建。可以通过构造器设置初始容量与负载因子,默认初始容量是16个条目,最大容量是2^30次方个条目,默认负载因子是0.75

桶 就像一个存水的水桶,它默认的初始存水容量是16个单位的水,默认在灌水灌到16*0.75时,在下次添加数据时会先扩充容量,扩充到32单位。0.75就是负载因子,初始容量与负载因子可以通过创建水桶的时候进行设置。水桶最大的容量是2的30次方个单位的水。当初始容量设置的数量大于最大容量时,以最大容量为准。当扩展时如果大于等于最大容量时则直接返回。

如下为HashMap的部分源码,定义了默认初始容量、负载因子及其他一些常量:

/**
  * 默认初始化容量,必须为2的次幂The default initial capacity - MUST be a power of two.
  */
 static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16
 /**
  * 最大容量,如果通过构造函数参数中传递初始化容量大于该最大容量了,也会使用该容量为初始化容量  * 必须是2的次幂且小于等于2的30次方  */
 static final int MAXIMUM_CAPACITY = 1 << 30;
 /**
  * 默认的负载因子,可以通过构造函数指定
  */
 static final float DEFAULT_LOAD_FACTOR = 0.75f;
 /**
  * 一个空的数组表,当 桶没有初始化的时候
  */
 static final Entry<?,?>[] EMPTY_TABLE = {};
 /**
  * 桶 , 存储所有的键值对条目,可以按需调整大小,长度大小必须为2的次幂
  */
 transient Entry<K,V>[] table = (Entry<K,V>[]) EMPTY_TABLE;
 /**
  * Map中键值对的数量,在每次新增或删除的时候都会对size进行+1或者-1操作.
  */
 transient int size;
 /**
  * 负载值,需要调整大小的临界值,为:(capacity * load factor).在每次调整大小后会使用新的容量计算一下
  * @serial
  */
 // If table == EMPTY_TABLE then this is the initial capacity at which the
 // table will be created when inflated.
 int threshold;
 /**
  * 负载因子,如果构造函数中没有指定,则采用默认的负载因子,
  *
  * @serial
  */
 final float loadFactor;
 /**
  * HashMap结构修改次数,结构修改时改变HashMap中的映射数量或修改其内部结构(例如,* rehash方法,重建内部数据结构),此字段用于在  * HashMap的集合视图上生成的迭代器都处理成快速失败的
  */
 transient int modCount;
登录后复制

初始容量与负载因子性能调整

通常,默认负载因子(0.75)在时间和空间成本上寻求一种折中。负载因子过高虽然减少了空间开销,但同时也增加了查询成本(在大多数HashMap类的操作中,包括get和put操作,都反映了这一点)。在设置初始容量时应该考虑到映射中所需的条目数及其负载因子,以便最大限度的减少rehash操作次数。如果初始容量大于最大条目数除以加载因子,则不会发生rehash操作。

如果很多映射关系要存储在HashMap实例中,则相对于按需执行自动的rehash操作以增大表的容量来说,使用足够大的初始容量创建它将使得映射关系能更有效的存储。

如下为重建HashMap数据结构的代码:

void resize(int newCapacity) {
  Entry[] oldTable = table;
  int oldCapacity = oldTable.length;
  if (oldCapacity == MAXIMUM_CAPACITY) { // 如果容量已达最大限制,则设置下负载值后直接返回
   threshold = Integer.MAX_VALUE;
   return;
  }
  // 创建新的table存储数据
  Entry[] newTable = new Entry[newCapacity];
  // 将旧table中的数据转存到新table中去,这一步会花费比较多的时间
  transfer(newTable, initHashSeedAsNeeded(newCapacity));
  table = newTable;
  // 最后设置下下次调整大小的负载值
  threshold = (int) Math.min(newCapacity * loadFactor,
    MAXIMUM_CAPACITY + 1);
}
登录后复制


HashMap构造方法

201654161611529.png

第四个构造方法是以已经存在的Map创建一个新的HashMap,稍后再说,前三个构造方法,其实最终调用的都是第三个带两个参数的方法,如果没有传递参数则使用默认的数值,代码如下:

/**
   * Constructs an empty <tt>HashMap</tt> with the default initial capacity
   * (16) and the default load factor (0.75).
   */
  public HashMap() {
    this(DEFAULT_INITIAL_CAPACITY, DEFAULT_LOAD_FACTOR);
  }
  /**
   * 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);
  }
  /**
   * Constructs an empty <tt>HashMap</tt> with the specified initial
   * capacity and load factor.
   *
   * @param initialCapacity the initial capacity
   * @param loadFactor   the load factor
   * @throws IllegalArgumentException if the initial capacity is negative
   *     or the load factor is nonpositive
   */
  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);
    this.loadFactor = loadFactor;
    threshold = initialCapacity;
    init();
  }
登录后复制

由上可以看出,在构造函数中,如果初始容量给的大于最大容量,则直接以最大容量代替。

put方法

接下来就看看HashMap中比较重要的部分

/**
   * 在此映射中关联指定值与指定建。如果该映射以前包含了一个该键的映射关系,则旧值被替换
   *
   * @param 指定将要关联的键
   * @param 指定将要关联的值
   * @return 与key关联的旧值,如果key没有任何映射关系,则返回null(返回null还可能表示该映射之前将null与key关联)
   */
  public V put(K key, V value) {
    if (table == EMPTY_TABLE) {
      inflateTable(threshold);
    }
    if (key == null)
      return putForNullKey(value);
    int hash = hash(key);
    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;
  }
登录后复制

1. 首先put方法中,先判断 桶 是否为默认的未初始化状态,如果未初始化则调用 inflateTable 方法去初始化,然后判断参数key是否为null,如果为null,则调用putForNullKey专门进行放key为null的数据,putForNullKey方法与下面的第3步开始其实都是一样的,只不过key为null的数据默认存储位置就是第一个,即下标默认为0。

2. 如果key不是null,则调用hash()方法获取key的hash值,可以根据hash值、桶的长度通过indexFor方法计算该key可以放到桶的位置。

3. Entry对象中有一个属性next,可以形成一个单向链表,用来存储哈希值相同的元素。因此当计算出来key的hash值重复时,存储位置也会重复,只要判断一下存储位置的元素及该元素的next属性链表中是否与给定的key和key的hash值是否完全一致就可以了。如果有完全一致的,代表已经存在,则替换旧值,并把旧值做为返回值直接返回。

4. 把结构修改次数自增1

5. 调用addEntry方法将新的键值对增加到HashMap中。addEntity方法首先判断当前条目数据是否已经大于等于负载值(桶的容量*负载因子)且桶的指定位置不为null,如果已经大于且指定位置不为null,则调调整桶的容量为当前容量的2倍,调整桶的容量参照上面的初始容量与负载因子性能调整 目录。重新计算Hash值,计算存放位置。调用createEntry方法存放到 桶 中

void addEntry(int hash, K key, V value, int bucketIndex) {
    if ((size >= threshold) && (null != table[bucketIndex])) {
      resize(2 * table.length);
      hash = (null != key) ? hash(key) : 0;
      bucketIndex = indexFor(hash, table.length);
    }
    createEntry(hash, key, value, bucketIndex);
  }
  void createEntry(int hash, K key, V value, int bucketIndex) {
    Entry<K,V> e = table[bucketIndex];
    table[bucketIndex] = new Entry<>(hash, key, value, e);
    size++;
  }
  /**
  * Entry构造方法,创建一个新的Entry.
  */
  Entry(int h, K k, V v, Entry<K,V> n) {
    value = v;
    next = n;
    key = k;
    hash = h;
  }
登录后复制


6. 在 createEntry 方法中,首先获取指定位置的entry,然后新生成一个entry,在生成entry时把原有的entry存储到新生成的entry的next属性中(参考Entry的构造方法),并把指定位置的entry替换成新生成的。

因为新增条目的时候,需要计算hash值,长度不够时需要调整长度,当计算的存储位置已有元素的时候需要进行链表式的存储,所以使用HashMap新增操作的效率并不是太高。

get方法

首先看下get方法的源码:

/**
   * 返回指定键所映射的值;如果对于该键来说,此映射不包含任何映射关系,则返回null
   * 返回null值并不一定表明该映射不包含该键的映射,也可能改映射将该键显示的映射为null,可使用containsKey操作来区分这两种情况
   * @see #put(Object, Object)
   */
  public V get(Object key) {
    if (key == null)
      return getForNullKey();
    Entry<K,V> entry = getEntry(key);
    return null == entry ? null : entry.getValue();
  }
  final Entry<K,V> getEntry(Object key) {
    if (size == 0) {
      return null;
    }
    int hash = (key == null) ? 0 : hash(key);
    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 != null && key.equals(k))))
        return e;
    }
    return null;
}
登录后复制


get方法实现较简单,以下是几个步骤:

首先判断key是否为null,如果为null,则调用 getForNullKey 方法来获取,如果不为null则调用 getEntry 方法来获取。getForNullKey方法与getEntity基本上一致,只不过少了一个步骤,就是默认的key为null的存储位置在第一个,即下标为0,没有去计算位置而已。

getEntity方法根据key计算哈希值,然后用哈希值、桶的长度计算存储位置。

getEntity以获取指定位置的entry作为遍历的开始,遍历entry的next单链表,如果entry的哈希值与计算的哈希值一致且entry的key与指定的相等则返回entry

根据getEntity返回的值,get方法返回对应的值。

通过查看get的源码可以发现,get方法通过key的哈希值与桶的长度计算存储位置,基本上一下就能定位到要找的元素,即使再遍历几个重复哈希值的key,也是很快速的,因为哈希值相对唯一,所以HashMap对于查找性能是非常快的。

自定义对象作为HashMap的键

class User {
  // 身份证号码
  protected int idNumber;
  public User(int id){
    idNumber = id;
  }
}
public class TestUser{
  public static void main(String[] args) {
    Map<User, String> map = new HashMap<User, String>();
    for (int i=0; i<5; i++) {
      map.put(new User(i), "姓名: " + i);
    }
    System.out.println("User3 的姓名:" + map.get(new User(3)));
  }
}
输出:
User3 的姓名:null
登录后复制

如上代码,通过自定义的User类实例作为HashMap的对象时,在打印的时候是无法找到User3的姓名的,因为User类自动继承基类Object,所以这里会自动使用Object的hashCode方法生成哈希值,而它默认是使用对象的地址计算哈希值的。因此new User(3)生成的第一个实例的哈希值与生成的第二个实例的哈希值是不一样的。但是如果只需要简单的覆盖hashCode方法,也是无法正常运作的,除非同时覆盖equals方法,它也是Object的一部分。HashMap使用equals()判断当前的键是否与表中存在的键相同,可以参考上面的get或put方法。

正确equals()方法必须满足下列5个条件:---参考《Java编程思想》—489页

自反性。对任意x,x.equals(x)一定返回true

对称性。对任意x和y,如果有y.equals(x)返回true,则x.equals(y)也返回true

传递性。对任意x,y,z,如果有x.equals(y)返回true,y.equals(z)返回true,则x.equals(z)一定返回true

一致性,对任意x和y,如果对象中用于等价比较的信息没有改变,那么无论调用x.equals(y)多少次,返回的结果应该保持一致,要么一致是true,要么一致是false.

对任何不是null的x,x.equals(null)一定返回false

再次强调:默认的Object.equals()只是比较对象的地址,所以一个new User(3)并不等于另一个new User(3)。因此,如果要使用自己的类作为HashMap的键,必须同时重载hashCode()和equals().

如下代码可以正常运作:

 class User {
  // 身份证号码
  protected int idNumber;
  public User(int id){
    idNumber = id;
  }
  @Override
  public int hashCode() {
    return idNumber;
  }
  @Override
  public boolean equals(Object obj) {
    return obj instanceof User && (idNumber==((User)obj).idNumber);
  }
}
public class TestUser{
  public static void main(String[] args) {
    Map<User, String> map = new HashMap<User, String>();
    for (int i=0; i<5; i++) {
      map.put(new User(i), "姓名: " + i);
    }
    System.out.println("User3 的姓名:" + map.get(new User(3)));
  }
}
输出:
User3 的姓名:姓名: 3
登录后复制

上面只是简单的在hashCode中返回了idNumber作为唯一的判别,用户也可以根据自己的业务实现自己的方法。在equals方法中,instanceof会悄悄的检查对象是否为null,如果instanceof左边的参数为null,则会返回false,如果equals()的参数不为null且类型正确,则基于每个对象中的实际的idNumber进行比较。从输出可以看出,现在的方式是正确的。

以上就是本文的全部内容,希望本文的内容对大家的学习或者工作能带来一定的帮助,同时也希望多多支持PHP中文网!

更多详解JavaHashMap实现原理相关文章请关注PHP中文网!


本站声明
本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系admin@php.cn

热AI工具

Undresser.AI Undress

Undresser.AI Undress

人工智能驱动的应用程序,用于创建逼真的裸体照片

AI Clothes Remover

AI Clothes Remover

用于从照片中去除衣服的在线人工智能工具。

Undress AI Tool

Undress AI Tool

免费脱衣服图片

Clothoff.io

Clothoff.io

AI脱衣机

AI Hentai Generator

AI Hentai Generator

免费生成ai无尽的。

热门文章

R.E.P.O.能量晶体解释及其做什么(黄色晶体)
3 周前 By 尊渡假赌尊渡假赌尊渡假赌
R.E.P.O.最佳图形设置
3 周前 By 尊渡假赌尊渡假赌尊渡假赌
R.E.P.O.如果您听不到任何人,如何修复音频
3 周前 By 尊渡假赌尊渡假赌尊渡假赌

热工具

记事本++7.3.1

记事本++7.3.1

好用且免费的代码编辑器

SublimeText3汉化版

SublimeText3汉化版

中文版,非常好用

禅工作室 13.0.1

禅工作室 13.0.1

功能强大的PHP集成开发环境

Dreamweaver CS6

Dreamweaver CS6

视觉化网页开发工具

SublimeText3 Mac版

SublimeText3 Mac版

神级代码编辑软件(SublimeText3)

如何使用咖啡因或Guava Cache等库在Java应用程序中实现多层缓存? 如何使用咖啡因或Guava Cache等库在Java应用程序中实现多层缓存? Mar 17, 2025 pm 05:44 PM

本文讨论了使用咖啡因和Guava缓存在Java中实施多层缓存以提高应用程序性能。它涵盖设置,集成和绩效优势,以及配置和驱逐政策管理最佳PRA

Java的类负载机制如何起作用,包括不同的类载荷及其委托模型? Java的类负载机制如何起作用,包括不同的类载荷及其委托模型? Mar 17, 2025 pm 05:35 PM

Java的类上载涉及使用带有引导,扩展程序和应用程序类负载器的分层系统加载,链接和初始化类。父代授权模型确保首先加载核心类别,从而影响自定义类LOA

如何在Java中实施功能编程技术? 如何在Java中实施功能编程技术? Mar 11, 2025 pm 05:51 PM

本文使用lambda表达式,流API,方法参考和可选探索将功能编程集成到Java中。 它突出显示了通过简洁性和不变性改善代码可读性和可维护性等好处

如何将JPA(Java持久性API)用于具有高级功能(例如缓存和懒惰加载)的对象相关映射? 如何将JPA(Java持久性API)用于具有高级功能(例如缓存和懒惰加载)的对象相关映射? Mar 17, 2025 pm 05:43 PM

本文讨论了使用JPA进行对象相关映射,并具有高级功能,例如缓存和懒惰加载。它涵盖了设置,实体映射和优化性能的最佳实践,同时突出潜在的陷阱。[159个字符]

如何将Maven或Gradle用于高级Java项目管理,构建自动化和依赖性解决方案? 如何将Maven或Gradle用于高级Java项目管理,构建自动化和依赖性解决方案? Mar 17, 2025 pm 05:46 PM

本文讨论了使用Maven和Gradle进行Java项目管理,构建自动化和依赖性解决方案,以比较其方法和优化策略。

如何将Java的Nio(新输入/输出)API用于非阻滞I/O? 如何将Java的Nio(新输入/输出)API用于非阻滞I/O? Mar 11, 2025 pm 05:51 PM

本文使用选择器和频道使用单个线程有效地处理多个连接的Java的NIO API,用于非阻滞I/O。 它详细介绍了过程,好处(可伸缩性,性能)和潜在的陷阱(复杂性,

如何使用适当的版本控制和依赖项管理创建和使用自定义Java库(JAR文件)? 如何使用适当的版本控制和依赖项管理创建和使用自定义Java库(JAR文件)? Mar 17, 2025 pm 05:45 PM

本文使用Maven和Gradle之类的工具讨论了具有适当的版本控制和依赖关系管理的自定义Java库(JAR文件)的创建和使用。

如何使用Java的插座API进行网络通信? 如何使用Java的插座API进行网络通信? Mar 11, 2025 pm 05:53 PM

本文详细介绍了用于网络通信的Java的套接字API,涵盖了客户服务器设置,数据处理和关键考虑因素,例如资源管理,错误处理和安全性。 它还探索了性能优化技术,我

See all articles