`
ydwcn
  • 浏览: 65200 次
  • 性别: Icon_minigender_1
  • 来自: 北京
社区版块
存档分类
最新评论

java 集合框架之二:HashMap

阅读更多

java.util.HashMap 是一个很常用的集合类,它用数组来存储数据。

transient Entry[] table;

 

它会对KEY进行HASH运算,如果哈希值冲突,HashMap采用链表来解决

Entry就是HashMap存储数据所用的类

final K key;
V value;
final int hash;
Entry<K,V> next;

 next就是为了哈希冲突而存在的,比如一个新对象通过哈希运算应该在第10位置,可10位置已存放Entry对象,那么把新对象也放到第10位置,将第10个位置的原有Entry赋值给当前新加的 Entry的next属性,数组存储的是链表,链表是为了解决哈希冲突的。

 

几个关键的属性
transient Entry[] table;

默认容量
static final int DEFAULT_INITIAL_CAPACITY = 16;
最大容量
static final int MAXIMUM_CAPACITY = 1 << 30; 1073741824
默认加载因子,加载因子是一个比例,当HashMap的数据大小>=容量*加载因子时,HashMap会将容量扩容
static final float DEFAULT_LOAD_FACTOR = 0.75f;
当实际数据大小超过threshold时,HashMap会将容量扩容,threshold=容量*加载因子
int threshold;
加载因子
final float loadFactor;

哈希算法

HashMap并不是直接将对象的hashcode作为哈希值的,而是要把key的hashcode作一些运算以得到最终的哈希值,并且得到的哈希值也不是在数组中的位置

无论是get还是put还是别的方法,计算哈希值都是这一句:

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

 hash函数如下:
  

static int hash(int h) {
     return useNewHash ? newHash(h) : oldHash(h);
     }

 

哈希函数

 private static int oldHash(int h) {
         h += ~(h << 9);
         h ^=   (h >>> 14);
         h +=   (h << 4);
         h ^=   (h >>> 10);
         return h;
}

 确定数据的位置

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

 第一行,上面讲过了,是得到哈希值,第二行,则是根据哈希指计算元素在数组中的位置了,位置的计算是将哈希值和数组长度按位与运算。

static int indexFor(int h, int length) {
         return h & (length-1);
     }

首先我们要确定一下,HashMap的数组长度永远都是偶数,即使你在初始化的时候是这样的new HashMap(15,0.75);因为在构造函数内部,上面也讲过,有这样的一段代码:

while (capacity < initialCapacity)
             capacity <<= 1;

 

所以length-1一定是个奇数,假设现在长度为16,减去1后就是15,对应的二进制是:1111。

假设有两个元素,一个哈希值是8,二进制是1000,一个哈希值是9,二进制是1001。和1111与运算后,分别还是1000和1001,它们被分配在了数组的不同位置,这样,哈希的分布非常均匀。

那么,如果数组长度是奇数,减去1后就是偶数了,偶数对应的二进制最低位一定是0了,例如14二进制1110。对上面两个数子分别与运算,得到1000和1000。看到了吗?都是一样的值,哈希值8和9的元素多被存储在数组同一个位置的链表中。在操作的时候,链表中的元素越多,效率越低,因为要不停的对链表循环比较。所以,一定要哈希均匀分布,尽量减少哈希冲突,减少了哈希冲突,就减少了链表循环,就提高了效率。
put方法到底作了什么?

public V put(K key, V value) {
        if (key == null)
            return putForNullKey(value);
        int hash = hash(key.hashCode());
        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;
    }

 如果key为NULL,则是单独处理的,看看putForNullKey方法:

private V putForNullKey(V value) {
        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;
    }

 

NULL_KEY的声明:static final Object NULL_KEY = new Object();
这一段代码是处理哈希冲突的,就是说,在数组某个位置的对象可能并不是唯一的,它是一个链表结构,根据哈希值找到链表后,还要对链表遍历,找出key相等的对象,替换它,并且返回旧的值。

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

 如果遍历完了该位置的链表都没有找到有key相等的,那么将当前对象增加到链表里面去

modCount++;
addEntry(0, null, value, 0);
return null;

 

且看看addEntry方法

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

 table[bucketIndex] = new Entry<K,V>(hash, key, value, e);新建一个Entry对象,并放在当前位置的Entry链表的头部,看看下面的 Entry构造函数就知道了,注意红色部分

Entry(int h, K k, V v, Entry<K,V> n) {
             value = v;
             next = n;
             key = k;
             hash = h;
         }

 

何扩容?
         当put一个元素时,如果达到了容量限制,HashMap就会扩容,新的容量永远是原来的2倍。
上面的put方法里有这样的一段:
if (size++ >= threshold)
             resize(2 * table.length);
这是扩容判断,要注意,并不是数据尺寸达到HashMap的最大容量时才扩容,而是达到 threshold指定的值时就开始扩容, threshold=最大容量*加载因子。 看看resize方法

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);
         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;
                    int i = indexFor(e.hash, newCapacity);
                    e.next = newTable[i];
                    newTable[i] = e;
                    e = next;
                } while (e != null);
            }
        }
    }

 tranfer方法将所有的元素重新哈希,因为新的容量变大,所以每个元素的哈希值和位置都是不一样的。

如何寻找元素

通过上面代码的分析,应该可以很清楚的看到,HashMap的元素查找大致分为三步:

根据key的hasocde()得到哈希值

根据哈希值确定元素在数组中的位置

找到指定位置的链表,循环比较,先“==”比较,如果不等,再“equals”比较,如果有一个比较相等,就说明找到元素了。

所以说到这里,我想大家也明白了,为什么要把一个对象放进HashMap的时候,最好是重写hashcode()方法和equals 方法呢?根据前面的分析,hashcode()可以确定元素在数组中的位置,而equals方法在链表的比较时要用到。

正确的使用HashMap
1:不要在并发场景中使用HashMap
HashMap是线程不安全的,如果被多个线程共享的操作,将会引发不可预知的问题,据sun的说法,在扩容时,会引起链表的闭环,在get元素时,就会无限循环,后果是cpu100%。

public V get(Object key) {
     if (key == null)
         return getForNullKey();
         int hash = hash(key.hashCode());
         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.equals(k)))
                 return e.value;
         }
         return null;
     }

 

2:如果数据大小是固定的,那么最好给HashMap设定一个合理的容量值
根据上面的分析,HashMap的初始默认容量是16,默认加载因子是0.75,也就是说,如果采用HashMap的默认构造函数,当增加数据时,数据实际容量超过10*0.75=12时,HashMap就扩容,扩容带来一系列的运算,新建一个是原来容量2倍的数组,对原有元素全部重新哈希,如果你的数据有几千几万个,而用默认的HashMap构造函数,那结果是非常悲剧的,因为HashMap不断扩容,不断哈希,在使用HashMap的场景里,不会是多个线程共享一个HashMap,除非对HashMap包装并同步,由此产生的内存开销和cpu开销在某些情况下可能是致命的。

分享到:
评论

相关推荐

    Java集合框架(JCF:Java Collections Framework)之概述

    Java 集合框架(JCF:Java Collections Framework)之概述 Java 集合框架(JCF:Java Collections Framework)是 Java 语言中的一组类库,用于实现集合操作的统一标准。集合是计算机科学中的一种基本概念,来源于...

    深入Java集合学习系列:HashMap的实现原理

    在Java编程语言中,集合框架是开发者日常工作中不可或缺的一部分,HashMap作为其中的重要成员,它的实现原理对于理解Java性能优化和数据结构有深远的意义。HashMap是一个基于哈希表的数据结构,它实现了Map接口,...

    Java集合框架深度解析:Map, List, Set

    深入的洞察到Java集合框架的核心组件:Map, List, 和 Set。首先,深入分析了HashMap的内部结构,包括它的数组+链表+红黑树的数据结构。重要的是理解如何处理并发问题,特别是在Java 8中对HashMap的优化,如高低位...

    源码解析jdk7.0集合(3):HashMap的底层实现原理.pdf

    HashMap是Java集合框架中Map接口的典型实现之一,主要用于存储键值对(Key-Value)数据。其特性如下: - 线程不安全,方法为非同步方法,因此多线程环境下访问可能会出现并发问题。 - 允许使用null作为键(Key)和...

    Java集合框架总结

    #### 二、Java集合框架结构 Java集合框架的核心部分包括以下几类: - **集合接口**:主要包括`Collection`、`Set`、`List`和`Map`等接口。 - **抽象类**:提供了部分接口的默认实现,如`AbstractCollection`、`...

    java集合框架图

    ### Java集合框架详解 #### 一、Java集合框架概述 Java集合框架是Java标准库的重要组成部分,它提供了存储和操作对象的各种数据结构。通过使用集合框架,开发人员可以轻松地管理不同类型的数据集,并且能够利用...

    Java集合框架常见面试题.pdf

    根据提供的文档内容,文件是关于Java集合框架的面试题知识点总结。以下是Java集合框架的知识点详述: Java集合框架主要包括Collection接口和Map接口两大分支。Collection接口主要包括List、Set以及Queue三个子接口...

    Java集合框架详解

    Java集合框架是Java语言提供的用于存储数据和操作数据的集合类库。在Java集合框架中,Map接口的实现类广泛用于存储键值对数据结构。主要实现类包括HashMap、Hashtable、LinkedHashMap和TreeMap。 1. HashMap:它...

    集合框架,java中常用的集合类和接口

    Java 集合框架概述 Java 集合框架是 Java 语言中提供的一种统一的标准体系结构,用于表示和操作集合。集合框架包含三大块内容:对外的接口、接口的实现和对集合运算的算法。 1. 接口:Collection 顶层接口是集合...

    Java集合框架学习笔记

    Java集合框架是Java编程语言中一个至关重要的组成部分,它提供了数据结构和算法的抽象,使得开发者可以方便地存储和管理各种类型的数据。本篇将详细探讨Java集合框架的基础知识,包括核心接口、类的层级结构以及Java...

    Java集合框架及泛型

    Java集合框架是Java编程语言中一个非常重要的组成部分,它提供了一组高效的数据结构和操作这些数据结构的方法。泛型则是Java在J2SE 5.0版本引入的一个特性,极大地提高了代码的类型安全性和可读性。下面我们将深入...

    学士后Java集合框架和泛型课后习题答案

    Java集合框架是Java编程语言中的一个核心组成部分,它为数据存储和操作提供了丰富的类库。在Java中,集合框架主要包括接口(如List、Set、Queue)和实现这些接口的类(如ArrayList、HashSet、LinkedList等)。这个...

    java集合 框架 泛型

    首先,Java集合框架包括List、Set、Queue等接口,以及ArrayList、LinkedList、HashSet、HashMap等实现类。这些接口和类提供了存储、检索、遍历和修改元素的方法。例如,ArrayList是以动态数组为基础的列表实现,适合...

    Java集合框架源码剖析:HashSet 和 HashMap

     之所以把HashSet和HashMap放在一起讲解,是因为二者在Java里有着相同的实现,前者仅仅是对后者做了一层包装,也是说HashSet里面有一个HashMap(适配器模式)。因此本文将重点分析HashMap。  HashMap实现了Map...

    java集合框架详解以及区别

    Java集合框架是Java编程语言中一个重要的组成部分,它提供了一组高级数据结构,使得程序员能够高效地管理和操作数据。在本篇文章中,我们将深入探讨Java集合框架中的主要接口和类,以及它们之间的区别。 首先,Java...

    源码解析jdk8.0集合:HashMap的底层实现原理.pdf

    HashMap是Java集合框架的一部分,它实现了Map接口,用于存储键值对。其核心特点在于基于哈希表的映射机制,能够通过键快速检索对应的值。HashMap支持键和值为null,同时是非线程安全的,即其方法不是同步的。 2. ...

    JAVA学习Java集合框架.pptx

    JAVA集合框架学习笔记 JAVA集合框架是Java语言中提供的一种数据结构,用于存储和管理大量数据。它提供了多种接口和实现类,例如Collection、List、Map等,帮助开发者更方便地处理数据。 什么是异常? 在Java中,...

    java集合框架全面进阶.pdf

    Java集合框架是Java编程语言中处理对象集合的一套接口和类。该框架提供了用于存储和操作集合的标准方法。在Java集合框架中,基本的接口分为两大类:Collection和Map。 Collection接口用于表示一组对象,称为其元素...

Global site tag (gtag.js) - Google Analytics