java中的hashmap

Java hashmap原理

线性表:存储在连续的内存地址,查询快,插入和删除慢。
链式表:存储在间断的,大小不固定,插入和删除快,但是查询的速度慢。
hashmap是以上两种者折中的解决方案,插入或者删除只需要动一部分即可。

HashMap的基本原理:(jdk1.7)
HashMap是基于hashing的原理,我们使用put(key, value)存储对象到HashMap中,使用get(key)从HashMap中获取对象。
当我们给put()方法传递键和值时,我们先对键调用hashCode()方法,返回的hashCode用于找到bucket位置来储存Entry对象。

  1. 先判断key 是否为null
  2. 再根据key 调用hashCode()进行定位

在jdk1.8引入了红黑树

在这里插入图片描述
hashmap的主要参数

//默认初始容量16
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4;
//容量最大值
static final int MAXIMUM_CAPACITY = 1 << 30;
//默认加载因子0.75 (当前已占当前最大容量的75%,会进行扩容操作)
static final float DEFAULT_LOAD_FACTOR = 0.75f;
//树化的阈值,当桶中链表节点数大于8时,将链表转换为红黑树
static final int TREEIFY_THRESHOLD = 8;
//红黑树退化为链表的阈值,当桶中红黑树节点数小于6时,将红黑树转换为链表
static final int UNTREEIFY_THRESHOLD = 6;
//最小的树化容量,进行树化的时候,还有一次判断,只有键值对数量大于64时才会发生转换,
//这是为了避免在哈希表建立初期,多个键值对恰好被放入了同一个链表而导致不必要的转化
static final int MIN_TREEIFY_CAPACITY = 64;

单个结点的属性有:

hash:用于快速定位;
key:标识符
Value:存储的数值
next:引用地址,便于插入、删除操作

补充:
为什么hashmap的长度为2的n次方?
通过hash进行定位时,hash%length 速度偏慢
若是2的n次方 hash%length==hash&(length-1) 而&操作显然更快。

get操作:get(k)

1.判断表是否为空或者待查找的桶不为空 (会先使用hashCode()计算出hash值进行哈希桶)
2.首先检查待查找的桶的第一个元素是否是要找的元素,如果是直接返回
3.桶内红黑树,则调用getTreeNode()查找红黑树
4.桶内是链表,遍历链表寻找节点

public V get(Object key) {
    Node<K,V> e;
    return (e = getNode(hash(key), key)) == null ? null : e.value;
}
final Node<K,V> getNode(int hash, Object key) {
    Node<K,V>[] tab; Node<K,V> first, e; int n; K k;
    //表不为空&&表长大于0&&待查找的桶不为空
    if ((tab = table) != null && (n = tab.length) > 0 &&
        (first = tab[(n - 1) & hash]) != null) {
        //首先检查桶中的第一个节点,如果相等,则直接返回
        if (first.hash == hash && // always check first node
            ((k = first.key) == key || (key != null && key.equals(k))))
            return first;
        if ((e = first.next) != null) {
            //如果桶中是树结构
            if (first instanceof TreeNode)
                return ((TreeNode<K,V>)first).getTreeNode(hash, key);
            //桶中是链表,则遍历链表如果找到则直接返回
            do {
                if (e.hash == hash &&
                    ((k = e.key) == key || (key != null && key.equals(k))))
                    return e;
            } while ((e = e.next) != null);
        }
    }
    return null;
}

put操作:put(k,v)

1.如果表为空或者表的长度为0,调用resize初始化表,为表分配空间
2.①二次散列处的桶为空,直接插入元素
②桶不为空
a)桶处的第一个节点与待插入节点的哈希相同且key“相等”,直接赋给变量e
b)桶中是红黑树,调用putTreeVal插入红黑树中
c)桶中是链表,遍历链表,如果其中存在相同的key,则赋给变量e;不存在则尾插法加入链表,并判断节点数是否大于8,如果大于8则调用treeifyBin()转化为红黑树
3.①e不为空,替换其中的value值,并返回旧的value值
②e为空,表大小+1,判断是否达到了阈值,如果达到了则需要扩容

public V put(K key, V value) {
        return putVal(hash(key), key, value, false, true);
}
    final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
                   boolean evict) {
        Node<K,V>[] tab; Node<K,V> p; int n, i;
        //如果表为空或者表的容量为0,resize初始化表
        if ((tab = table) == null || (n = tab.length) == 0)
            n = (tab = resize()).length;
        //根据hash得到在表中索引位置的桶,如果桶为空,则将节点直接插入桶中
        if ((p = tab[i = (n - 1) & hash]) == null)
            tab[i] = newNode(hash, key, value, null);
        //桶不为空
        else {
            Node<K,V> e; K k;
            //首先判断桶中第一个节点的hash与待插入元素的key的hash值是否相同且key是否"相等",如果相等,赋给变量e
            if (p.hash == hash &&
                ((k = p.key) == key || (key != null && key.equals(k))))
                e = p;
            //是树节点,则调用putTreeVal添加到红黑树中
            else if (p instanceof TreeNode)
                e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
            //否则是链表,遍历链表,如果不存在相同的key,则插入链表尾部,并且判断节点数量是否大于树化阈值,如果大于则转换为红黑树;如果存在相同的key,break,遍历链表结束
            else {
                for (int binCount = 0; ; ++binCount) {
                    if ((e = p.next) == null) {
                        p.next = newNode(hash, key, value, null);
                        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不为空表示存在相同的key,替换value并返回旧值
            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;
    }

补充说明:
在插入新节点时,JDK7采用的是头插法,JDK8采用的是尾插法。
采用头插法的原因是为了热点数据(新插入的数据)能够优先被查找,但是这种做法有可能在多线程的情况下可能会导致死锁( 产生死循环)。
链表查询的时间复杂度是n,而红黑树查询的时间复杂度是log^n

其他方法:

链表转化为红黑树:treeifyBin()
扩容操作:resize()

hashmap解决哈希冲突

开放定址法( 线性探测再散列,二次探测再散列,伪随机探测再散列)
发生冲突时,形成探索序列,沿此序列逐个单元地查找,直到找到给定的哈希地址,或者碰到一个开放的地址(即该地址单元为空)为止(若要插入,在探查到开放的地址,则可将待插入的新结点存人该地址单元)。查找时探查到开放的 地址则表明表中无待查的哈希地址,即查找失败。

链地址法: Java hashmap就是这么做的
将所有哈希地址相同的结点链接在同一个单链表中。

线程安全

避免多线程对共享数据进行操作时,产生的脏读、误读。
Hash线程安全:
1.多线程对相同key进行put操作
2.多线程同时对Node数组进行扩容,导致数据丢失

三种线程安全的方法:
a.hashtable 通过关键字synchroized标记临界区来实现
b.ConcurrentHashMap jdk1.8引入CAS算法 它的性能最好 (CAS锁是一种乐观锁,即轻量级别的锁)
c.SyschroniedMap 通过类间接使用 syschroized
补充:syschronied的方式是一种悲观锁(是重量级锁)

hashmap的常用方法:

存值: map.put(key,value)
读值: map.get(key)
判断是否为空:map.isEmpty()
判断是否含有key:map.containsKey(key)
判断是否含有value:map.containsValue(value)
删除key值下的value:map.remove(key) //只删除了Value
显示所有value值:map.values()
元素的个数:map.size()
显示所有key:map.keySet()
显示所有key和value:map.entrySet()
合并2个类型相同的map:map.putAll(map1)
删除这个key和Value:map.remove(key,value)
替换key下的value:map.replace(key,newValue)
清洗整个map:map.clear()
map的克隆:map.clone()

使用hashmap时需要同时重写 HashCode和equals方法。

单一重写HashCode和equals都不能保证数据的唯一性。

Hashcode()的作用:将对象的内部地址转换成一个整数返回。(即获取hash桶的编号)
equals()的作用:只是比较2个对象之间的内容是否相同

重写时注意:
1. equals true ->hashcode返回的int要相同
2. equals false ->hashcode 返回的int一定不相同
3. hashcode int 相同 ->equals 可以是true 也可以是false
4. hashcode int 不同 ->equals一定要是false

这里结合 equals的知识,equals先比较 引用类型是否一致,如果一直则再比较其值是否相等。
由于HashMap不支持基本数据类型(如int),但是支持封装类(如 Integer)
HashMap 同一个Hash桶(hashcode)里的数据类型必须是相同的

比较 HashSet 和 HashMap

HashSet:
1.必须要重写hasdcode 和 equals 从而确保对象的唯一性
2.利用对象获取hashcode 元素以对象的形式
3.add()增加元素

HashMap:
1.必须要重写hashcode 和equals
2.利用key获取hashcode 以<key,value>的形式
3.put()增加元素

在这里插入图片描述

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值