HashMap学习

作者: 盼旺 | 来源:发表于2019-08-20 14:38 被阅读101次

首先HasMap在JDK 1.7 和 1.8是稍有不同的。

简介

HashMap是一个散列桶(数组和链表,1.8还有红黑树),它存储的内容是键值对(key-value)映射
HashMap底层就是一个数组结构,数组中的每一项又是一个链表。当新建一个HashMap的时候,就会初始化一个数组

了解hashCode和equals

就是hashCode是用于查找使用的,而equals是用于比较两个对象的是否相等的
1.hashCode的存在主要是用于查找的快捷性,如Hashtable,HashMap等,hashCode是用来在散列存储结构中确定对象的存储地址

2.如果两个对象相同,就是适用于equals方法,那么这两个对象的hashCode一定要相同

3.如果对象的equals方法被重写,那么对象的hashCode也尽量重写,并且产生hashCode使用的对象,一定要和equals方法中使用的一致,否则就会违反上面提到的第2点

4.两个对象的hashCode相同,并不一定表示两个对象就相同,也就是不一定适用于equals方法,只能够说明这两个对象在散列存储结构中,如Hashtable,他们“存放在同一个篮子里“

HashMap构造函数

HashMap有4个构造函数
参数说明
初始容量
初始容量表示哈希表中桶(数组长度)的数量
负载因子
它衡量的是一个散列表的空间的使用程度,负载因子越大表示散列表的装填程度越高。如果加载因子越大,对空间的利用更充分,然而后果是查找效率的降低;如果加载因子太小,那么散列表的数据将过于稀疏,对空间造成严重浪费。系统默认加载因子为0.75,一般情况下我们是无需修改的。

// ********1.无参构造方法
// // 构造一个空的HashMap,初始容量为16,负载因子为0.75
public HashMap() {
        this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted
    }
//HashMap<String, Integer> myHashMap = new HashMap<>();

// ********2.构造一个空的初始容量为initialCapacity,负载因子为0.75的空的HashMap其实它就是调用下面的第三个构造方法
public HashMap(int initialCapacity) {
        this(initialCapacity, DEFAULT_LOAD_FACTOR);
    }
//HashMap<String, Integer> myHashMap = new HashMap<>(10);

// ********3.构造一个空的初始容量为initialCapacity,负载因子为loadFactor的HashMap
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;
        this.threshold = tableSizeFor(initialCapacity);
    }
//其中最大容量
static final int MAXIMUM_CAPACITY = 1 << 30;
tableSizeFor()的主要功能是返回一个比给定整数大且最接近的2的幂次方整数,如给定10,返回2的4次方16.看下面
//HashMap<String, Integer> myHashMap = new HashMap<>(16,0.75f);

// ********4构造一个和指定Map有相同mappings的HashMap,初始容量能充足的容下指定的Map,负载因子为0.75
public HashMap(Map<? extends K, ? extends V> m) {
        this.loadFactor = DEFAULT_LOAD_FACTOR;
        putMapEntries(m, false);
    }
//例子
//Map myMap = new HashMap<String,Integer>();
//HashMap<String, Integer> thisMap = new HashMap<>(myMap);

tableSizeFor()实现的源码

static final int tableSizeFor(int cap) {
        int n = cap - 1;
        n |= n >>> 1;
        n |= n >>> 2;
        n |= n >>> 4;
        n |= n >>> 8;
        n |= n >>> 16;
        return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1;
    }

tableSizeFor以输入10为例

tableSizeFor示例图
注意

1.当输入为0的时候,方法的输出为1。
(因为计算机中数字是由补码存储的,-1的补码是 0xffffffff。所以无符号右移之后再进行或运算之后还是 -1。)
2.n = n - 1; 是为了防止输入已经是2的幂次的数,不减1的话,得到结果就会扩大

HashMap的重要的成员变量

1.实际存储key,value的数组,只不过key,value被封装成Node了,第一次使用的时候被初始化,根据需要可以重新分配长度。分配的长度总是2的幂
    transient Node<K,V>[] table;
2.平衡因子,上面有说一下的
    final float loadFactor;
3.阈值(当前数组长度乘以加载因子的值)。因为 tableSizeFor(int) 返回值给了threshold.当需要resize时的阈值。即当HashMap中KV映射的数量(即size)超过了threshold就会resize。threshold=capacity*loadFactor。 
    int threshold;
4.当被调用entrySet时被赋值。通过keySet()方法可以得到map key的集合,通过values方法可以得到map value的集合。 
    transient Set<Map.Entry<K,V>> entrySet;
5.存放在map中的KV映射的总数。 
    transient int size;
6.HashMap被结构性修改的次数。(结构性修改是指改变了KV映射数量的操作或者修改了HashMap的内部结构(如 rehash)。这个用于fail-fast(快速失败)。
    transient int modCount;
7.将链表转换为红黑树的长度TREEIFY_THRESHOLD 和红黑树转回链表的长度UNTREEIFY_THRESHOLD
static final int TREEIFY_THRESHOLD = 8;
static final int UNTREEIFY_THRESHOLD = 6;

fail-fast(快速失败):在用迭代器遍历一个集合对象时,如果遍历过程中对集合对象的内容进行了修改(增加、删除、修改),则会抛出Concurrent Modification Exception
原理:迭代器在遍历时直接访问集合中的内容,并且在遍历过程中使用一个modCount 变量。集合在被遍历期间如果内容发生变化,就会改变modCount的值。每当迭代器使用hashNext()/next()遍历下一个元素之前,都会检测modCount变量是否为expectedmodCount值,是的话就返回遍历;否则抛出异常,终止遍历

HashMap的数据结构图jdk1.8

HashMap put方法jdk1.8

final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
                   boolean evict) {
        Node<K,V>[] tab; Node<K,V> p; int n, i;
        //如果table为空或者长度为0,则resize()
        if ((tab = table) == null || (n = tab.length) == 0)
            n = (tab = resize()).length;
        //确定插入table的位置,算法是(n - 1) & hash,在n为2的幂时,相当于取摸操作。
        //找到key值对应的槽并且是第一个,直接加入
        if ((p = tab[i = (n - 1) & hash]) == null)
            tab[i] = newNode(hash, key, value, null);
        //在table的i位置发生碰撞,有两种情况,1、key值是一样的,替换value值,
        //2、key值不一样的有两种处理方式:2.1、存储在i位置的链表;2.2、存储在红黑树中
        else {
            Node<K,V> e; K k;
            //第一个node的hash值即为要加入元素的hash
            if (p.hash == hash &&
                ((k = p.key) == key || (key != null && key.equals(k))))
                e = p;
            //2.2
            else if (p instanceof TreeNode)
                e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
            //2.1
            else {
                //不是TreeNode,即为链表,遍历链表
                for (int binCount = 0; ; ++binCount) {
                ///链表的尾端也没有找到key值相同的节点,则生成一个新的Node,
                //并且判断链表的节点个数是不是到达转换成红黑树的上界达到,则转换成红黑树。
                    if ((e = p.next) == null) {
                         // 创建链表节点并插入尾部
                        p.next = newNode(hash, key, value, null);
                        ////超过了链表的设置长度8就转换成红黑树
                        if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
                            treeifyBin(tab, hash);
                        break;
                    }
                    if (e.hash == hash &&
                        ((k = e.key) == key || (key != null && key.equals(k))))
                        break;
                    p = e;
                }
            }
            //如果e不为空就替换旧的oldValue值
            if (e != null) { // existing mapping for key
                V oldValue = e.value;
                if (!onlyIfAbsent || oldValue == null)
                    e.value = value;
                afterNodeAccess(e);
                return oldValue;
            }
        }
        ++modCount;
        if (++size > threshold)
            resize();
        afterNodeInsertion(evict);
        return null;
    }

注:hash 冲突发生的几种情况:
1.两节点key 值相同(hash值一定相同),导致冲突;
2.两节点key 值不同,由于 hash 函数的局限性导致hash 值相同,冲突;
3.两节点key 值不同,hash 值不同,但 hash 值对数组长度取模后相同,冲突;

1.7和1.8的HashMap的不同点

①JDK1.7用的是头插法,而JDK1.8及使用是尾插法,因为JDK1.7是用单链表进行的纵向延伸,当采用头插法就是能够提高插入的效率,但是也会容易出现逆序且环形链表死循环问题。但是在JDK1.8之后是因为加入了红黑树使用尾插法,能够避免出现逆序且链表死循环的问题。

②扩容后数据存储位置的计算方式也不一样
在JDK1.7的时候是直接用hash值和需要扩容的二进制数进行&(与运算)(这里就是为什么扩容的时候为啥一定必须是2的多少次幂的原因所在,因为如果只有2的n次幂的情况时最后一位二进制数才一定是1,这样能最大程度减少hash碰撞)(hash值 & length-1) 。
而在JDK1.8的时候直接用了JDK1.7的时候计算的规律,也就是扩容前的原始位置+扩容的大小值=JDK1.8的计算方式,而不再是JDK1.7的那种异或的方法。但是这种方式就相当于只需要判断Hash值的新增参与运算的位是0还是1就直接迅速计算出了扩容后的储存方式。
其中hash值的计算不想去研究,网上总结说JDK1.7用了9次扰动处理=4次位运算+5次异或,而JDK1.8只用了2次扰动处理=1次位运算+1次异或。

③JDK1.7的时候使用的是数组+ 单链表的数据结构。但是在JDK1.8及之后时,使用的是数组+链表+红黑树的数据结构(当链表的深度达到8的时候,也就是默认阈值,就会自动扩容把链表转成红黑树的数据结构来把时间复杂度从O(N)变成O(logN)提高了效率)。

HashMap为什么是线程不安全的?

HashMap 在并发时可能出现的问题主要是两方面:

①put的时候导致的多线程数据不一致
比如有两个线程A和B,首先A希望插入一个key-value对到HashMap中,首先计算记录所要落到的 hash桶的索引坐标,然后获取到该桶里面的链表头结点,此时线程A的时间片用完了,而此时线程B被调度得以执行,和线程A一样执行,只不过线程B成功将记录插到了桶里面,假设线程A插入的记录计算出来的 hash桶索引和线程B要插入的记录计算出来的 hash桶索引是一样的,那么当线程B成功插入之后,线程A再次被调度运行时,它依然持有过期的链表头但是它对此一无所知,以至于它认为它应该这样做,如此一来就覆盖了线程B插入的记录,这样线程B插入的记录就凭空消失了,造成了数据不一致的行为。
②resize而引起死循环
这种情况发生在HashMap自动扩容时,当2个线程同时检测到元素个数超过数组大小 × 负载因子。此时2个线程会在put()方法中调用了resize(),两个线程同时修改一个链表结构会产生一个循环链表(JDK1.7中,会出现resize前后元素顺序倒置的情况)。接下来再想通过get()获取某一个元素,就会出现死循环。

HashMap和HashTable的区别

主要的区别有:线程安全性,同步(synchronization),以及速度。
1.HashMap可以接受为null的键值(key)和值(value),而Hashtable则不行
2.HashMap是非synchronized,而Hashtable是synchronized,这意味着Hashtable是线程安全的,多个线程可以共享一个Hashtable;而如果没有正确的同步的话,多个线程是不能共享HashMap的。
3.另一个区别是HashMap的迭代器(Iterator)是fail-fast迭代器,而Hashtable的enumerator迭代器不是fail-fast的。所以当有其它线程改变了HashMap的结构(增加或者移除元素),将会抛出ConcurrentModificationException,但迭代器本身的remove()方法移除元素则不会抛出ConcurrentModificationException异常。但这并不是一个一定发生的行为,要看JVM。这条同样也是Enumeration和Iterator的区别。
4.由于Hashtable是线程安全的也是synchronized,所以在单线程环境下它比HashMap要慢。如果你不需要同步,只需要单一线程,那么使用HashMap性能要好过Hashtable。
5.HashMap不能保证随着时间的推移Map中的元素次序是不变的。(红黑树呀)

HashMap例子

键Student类型(自定义类型),值String类型
注意

这里就要想一下自定义对象Student作为键时,什么叫同一个键? 
假定要求Student的成员变量值都相同,则为同一个对象。 
所以要在Student类里面重写equals()方法和hashCode()方法。 
Integer类和String类本身都重写了equals()方法。
Student.java
package wgzyx;

public class Student {

    private Long id;
    private String name;
    
    public Student() {
        super();
    }
    
    public Student(Long id, String name) {
        super();
        this.id = id;
        this.name = name;
    }

    public Long getId() {
        return id;
    }

    public void setId(Long id) {
        this.id = id;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    @Override
    public String toString() {
        return "Student [id=" + id + ", name=" + name + "]";
    }

    @Override
    public int hashCode() {
        final int prime = 31;
        int result = 1;
        result = prime * result + ((id == null) ? 0 : id.hashCode());
        result = prime * result + ((name == null) ? 0 : name.hashCode());
        return result;
    }

    @Override
    public boolean equals(Object obj) {
        if (this == obj)
            return true;
        if (obj == null)
            return false;
        if (getClass() != obj.getClass())
            return false;
        Student other = (Student) obj;
        if (id == null) {
            if (other.id != null)
                return false;
        } else if (!id.equals(other.id))
            return false;
        if (name == null) {
            if (other.name != null)
                return false;
        } else if (!name.equals(other.name))
            return false;
        return true;
    }   
}
HashMapDemo.java
package wgzyx;
import java.util.HashMap;
import java.util.Set;
public class HashMapDemo {
    public static void main(String[] args) {
        HashMap<Student,String> mymap= new HashMap<Student,String>();
        mymap.put(new Student(20L,"小红"),"考研");
        mymap.put(new Student(20L,"小橙"), "保研");
        mymap.put(new Student(21L,"小橙"), "考研");
        mymap.put(new Student(20L,"小紫"), "保研");
        mymap.put(new Student(20L,"小红"), "保研");
        Set<Student> keys= mymap.keySet();
        for(Student key: keys){
            String value= mymap.get(key);
            System.out.println(key.toString()+"---"+value);
        }
    }
}

参考原文:https://www.jianshu.com/p/ee0de4c99f87

相关文章

  • Java源码学习--HashMap

    Java源码学习--HashMap 由于HashSet的实现原理是HashMap,所以我们先从HashMap开始学...

  • Java基础_HashMap源码分析

    该篇文章主要从如下几个方面来学习下HashMap HashMap是啥 HashMap的代码实操 HashMap的g...

  • 你真的了解LinkedHashMap吗

    一、前言 LinkedHashMap 继承于 HashMap,因此,建议在学习本篇内容前,先学习 HashMap系...

  • 你真的了解LinkedHashMap吗?进来看看

    一、前言 LinkedHashMap 继承于 HashMap,因此,建议在学习本篇内容前,先学习 HashMap系...

  • Android 面试准备进行曲(数据结构 Map / List)

    Java数据结构 之 HashMap 重温学习1. HashMap2. hash() 方法3. HashMap 的...

  • HashMap学习

    HashMap 这个实现对get和put提供了固定时间的性能迭代完全部集合的时间是与HashMap的容量+键值对的...

  • HashMap学习

    调用如下: HashMap map=new HashMap();map.put("1","1");String ...

  • HashMap学习

    概述 线程非安全,并且允许key与value都为null值,HashTable与之相反,为线程安全,key与val...

  • HashMap学习

    首先HasMap在JDK 1.7 和 1.8是稍有不同的。 简介 HashMap是一个散列桶(数组和链表,1.8还...

  • HashMap学习

    HashMap有两个重要参数:初始容量已经加载因子。这两个参数对HashMap的性能有较大影响。 一些变量说明: ...

网友评论

    本文标题:HashMap学习

    本文链接:https://www.haomeiwen.com/subject/ofausctx.html