剖析HashMap原理以及其他map类


集合就是一个容器
类似于数组,用来存储一组数据,但是数组一旦定义,长度将不能再变化。

然而在我们的实际开发中,经常需要保存一些变长的数据集合,于是,我们需 要一些能够动态增长长度的容器来保存我们的数据。

也有我们对数据的存储逻辑可能是各种各样的,于是就需要各种各样的数据结构。
Java当中对于各种数据结构的具体实现,就利用的是集合。
在这里插入图片描述

看一下机构体系图:
在这里插入图片描述

Collection接口是单例的。Map接口是双列的。List接口是数据可重复的。Set接口下的数据不可重复的。

Map接口

map是双列的的存储结构(key : value)key是不能重复的。

Map当种实现类公共的方法:

clear()方法 删除所有的元素
put(K key, V value) 对集合当中添加元素
putAll(Map<? extends K,? extends V> m) 将一个Map集合添加到新的集合当中
remove(Object key) 根据键值删除指定元素
remove(Object key, Object value) 根据键和值删除指定元素
replace(K key, V value) 根据指定的键值替换指定的元素
containsKey(Object key) 根据键查找是否包含这个元素返回Boolean值
containsValue(Object value) 根据值差值是否包含此元素,返回Boolean值
get(Object key) 通过get获得指定键的元素
isEmpty() 判断map当中是否为空 没有值返回true 有就返回false
keySet() 返回所有的key键

import java.util.Collection;
import java.util.HashMap;
import java.util.Map;

public class Demo01 {
    public static void main(String[] args) {
        Map<Integer, String> map = new HashMap<>();

        map.put(1, "Xin");
        map.put(2, "Chen");
        map.put(3, "Yu");
        map.put(4, "Chen");
        map.put(1, "Xi");
        System.out.println("当前map集合当中的元素:" + map);
        map.remove(4);
        System.out.println("删除键为4的元素后集合中的所有元素:" + map);
        map.replace(3, "XinChen");
        System.out.println("将键值为3的值进行替换" + map);
        System.out.println("确定集合是否为空,返回值为Boolean型:" + map.isEmpty());
        System.out.println("确定集合当中是否有键为2的元素,返回值为Boolean型:" + map.containsKey(2));
        System.out.println("确定集合当中是否有值为\"xi\"的元素:返回值为Boolean型:" + map.containsKey("Xi"));
        System.out.println("获得键为3的值:" + map.get(3));
        System.out.println("返回当前集合当中的所有元素的数量" + map.size());
        //得到map集合但在所有的值,返回值为collection型
        Collection<String> collection = map.values();
        System.out.println("得到集合当中所有的值:" + collection);
    }
}

在这里插入图片描述

HashMap

底层是数组+链表存储数据,它是线程不安全的

HashMap是基于键的HashCode值唯一标识的一条数据,同时基于键的HashCode值进行数据的存储,因此可以快速地更新和查询数据,但其每次遍历的顺序无法保证相同。HashMap的key和value允许为null。

HashMap是线程不安全的,即在同一时刻会有多个线程同时写HashMap时将可能导致数据的不一致。

如果需要满足线程安全的条件,则可以使用Collection的synchronizedMap方法使HashMap具有线程安全的能力,或者使用ConcurrentHashMap。

HashMap的数据结构其内部是一个数组,数组中的每一个元素都是一个单向链表,链表中的每个元素都是嵌套Entry的实例Entry实例包含4个属性:key、value、hash值和用于指向单向链表下一个元素的next

Entry当中常用的方法:

方法意义
equals()判断两个元素是否一样
getKey()返回Entry包含的键
getValue()返回Entry包含的值
hashCode()返回此entry的hash值
setValue(V value)替换对应key的value

在这里插入图片描述

在运行的时候:用key算出哈希值,用哈希值计算元素在哈希表中的位置,将元素天骄到对应的位置。
当有重复位置的元素再加进来的时候。以链表的形式存储(这就是避免哈希冲突的解决办法,也就是拉链法)

在HashMap当中要知道的一些常用参数

  1. DEFAULT_INITIAL_CAPACITY
    在这里插入图片描述
    表示当前的数组的容量,默认值为16,它是可以扩容的,扩容后数组的长度是之前数组大小长度的2倍。

  2. DEFAULT_LOAD_FACTOR
    在这里插入图片描述
    表示负载因子,默认为0.75.

  3. threshold
    表示扩容的阈值。
    表示当数组的长度达到一定的程度的时候,就会发生扩容的情况,它并非是当满的时候就扩容,在这注意一下。
    阈值就是达到当前的数组长度到达0.75倍的时候,就发生了扩容。
    比如数组的长度为16的时候,当数据占内容长度达到16*0.75为12的时候,就发生扩容达到32这个长度。

HashMap在查找数据的时候,会根据HashMap的Hash值可以快速定位到数组的具体下标,但是在找到数组下标后需要对链表进行顺序遍历直到找到需要的数据,时间复杂度为O(n)。

为了减少链表遍历的开销,Java8之后对HashMap进行了优化,将数据结构修改为了数组+链表或红黑树。在链表中的元素超过8个以后,HashMap会将聊表结构转换为红黑树结构以提高查询效率,因此其时间复杂度为O(logN)。
在这里插入图片描述

HashMap扩容说明:
在扩容的时候,是创建一个新的Entry的空数组,长度是原数组的2倍。是遍历原Entry数组,然后把所有的Entry 重新Hash到新数组

因为在长度扩大以后,Hash的规则也随之改变了。

Hash值的计算=HashCode值除以数组长度的 余数

举个例子,当有一个数的HashCode值为16,现在数组长度为4,那么这个数据就放到所要为3的位置,现在扩容后数组长度为8,那么它就一个放在索引为7的位置。
所以在扩容的时候,就要重新Hash到新数组当中。

给链表当中的插入方式:
在java 8 之前选择的是头插法,在Java8之后选择的是尾插法。

头插法:单链表的头插入方式,同一位置上新元素总会被放在链表的头部位置
尾插法 :单链表的尾插入方式,同一位置上新元素总会被放在链表的尾部位置

Java 7 在多线程操作HashMap时可能会引起死循环,原因就是在扩容转移前后链表顺序的位置时,在转移过程当中会导致链表顺序产生倒置,在转移过程中修改了原来链表当中节点的引用关系。

在Java 8 当中使用的是尾插发和红黑树,在底层代码里也可以看出有很多的if……else之类的判断,在同样的前提下并不会引起死循环,原因就是导致扩容转移前后链表的顺序基本不变,保证还是之前节点的引用关系。

但现在改成尾插发,并不就是说,它就是安全的,毕竟在看底层代码的时候,就很明显的看出它的方法并没有加synchronized这个同步锁进行修饰。所以也就会产生一种这样的情况:上一秒put的值,并不一定在下一秒就能get出准确的数,所以线程的安全并不是一定能保证的。

为什么底层数组的长度为16或者2的n次幂?

因为在源代码里我们可以明显的看出,给出数组的长度是用位运算进行判断的。位运算的效率比计算的效率可是高多的

且在源码当中计算数组位置的方式为:

index = HashCode(Key) & (Length- 1)

16-1结果的二进制就为1111,在进行位与运算直接就看所求Hash值的后四位就是运算结果。在这个情况之下,性能是大大提高的。

所以在这种情况下,只要输如的HashCode本身就是分布均匀的。那么得到他的Hash结果也是均匀的。这也就实现了均匀分布

因为HashMap是线程不安全的,所以在这有时为使它的线程安全,所有可以使用HashTable或者ConcurrentHashMap。

HashMap当中的hashCode和equals:
首先只用equals()方法进行判读,因为equals()方法是将里面的都一个一个拿出来相互进行比较,就拿字符串来说,当一个字符串特别长的时候,只拿equals进行判断,显然是特别浪费时间的。所以先来hashCode进行判断。但是hashCode进行判断的时候,会出现一个问题,那即是hashCode相同的两个元素并不一定就是相同的元素。

所以在进行判断两个值是否一样的时候,就将这两个判断方法向结合,当两个返回的结果都为true的时候,就可以断定这两个元素的值是相等的。

ConcurrentHashMap

在jdk 1.7之前版本的ConcurrentHashMap:实现分段锁实现,线程安全的。

与HashMap不同,Jdk1.7及之前的版本当中ConcurrentHashMap采用的是分段锁的思想实现并发操作,因此线程是安全的。jdk 1.7之前版本的ConcurrentHashMap是由多个Segment组成的(Segment的数量也是锁的并发度),每个Segment均继承自ReentrantLock并单独加锁,所以每次进行加锁操作时锁住的都是一个Segment,这样就保证了每个Segment都是线程安全的,也就实现了全局的线程安全的。
在这里插入图片描述

在jdk 1.7及之前的版本的ConcurrentHashMap中有个concurrencyLevel参数表示并行级别,默认是16,也就是说jdk 1.7及之前版本的ConcurrentHashMap默认是由16个Segments组成,在这种情况下最多可同时支持16个线程并发执行写操作,只要它们的操作分布在不同的Segment上即可。并行级别concurrencyLevel可以在初始化时设置,一旦初始化就不可再更改。在jdk 1.7及之前版本的ConcurrentHashMap的每个Segment内部的数据结构和HashMap是类似的。

在jdk1.8之后的版本当中,ConcurrentHashMap弃用了Segment分段锁,改用了Synchronized+CAS实现对多线程的安全操作。同时在jdk 1,8之后也引入了红黑树。

HashTable

HashTable是线程安全的。但它是一个遗留类,很多映射的常用功能都与HashMap类型,不同的是它继承Dictionary类,并且是线程安全的,同一时刻只能有一个线程可以写HashTable,并发性是不如ConcurrentHashMap的。不存储为null的键。

TreeMap

是基于二叉树数据结构进行数据的存储,同时实现lSortedMap接口以保障元素的顺序存储,默认是按键值的升序进行排序的。也可以自定义排序比较器。

TreeMap常用于实现排序的映射列表。在使用TreeMap时期key是必须实现Comparable接口或采用自定义的比机器,否则会抛出java.lang.ClassCastException异常。

Map遍历

  1. 使用keySet进行遍历
    keySet方法返回集合当中所有的键set
    然后通过get方法通过键获得键对应的值。
import java.util.HashMap;
import java.util.Set;

public class Demo02 {
    public static void main(String[] args) {
        HashMap<Integer, String> map = new HashMap<>();
        map.put(1, "Xin");
        map.put(2, "Chen");
        map.put(3, "Yu");
        map.put(4, "Chen");
        map.put(5, "Xi");
        Set<Integer> keySet = map.keySet();
        for (Integer key : keySet) {
            System.out.println(key + "===>" + map.get(key));
        }
    }
}

在这里插入图片描述

  1. entrySet()方法获得map当中所有的entry
    将底层保存键值的Entry对象直接封装到Set的集合当中去。
    然后调用entry类里面对应的方法有:
    getKey获得entry当中的键
    getValue获得entry当中的值
import java.util.HashMap;
import java.util.Map;
import java.util.Set;

public class Demo03 {
    public static void main(String[] args) {
        HashMap<Integer, String> map = new HashMap<>();
        map.put(1, "Xin");
        map.put(2, "Chen");
        map.put(3, "Yu");
        map.put(4, "Chen");
        map.put(5, "Xi");

        Set<Map.Entry<Integer, String>> entries = map.entrySet();
        for (Map.Entry<Integer, String> entry : entries) {
            System.out.println(entry.getKey() + "==>" + entry.getValue());
        }
    }
}

在这里插入图片描述

  1. 通过流的形式进行遍历
    forEach的形式获得
import java.util.HashMap;
import java.util.function.BiConsumer;

public class Demo04 {
    public static void main(String[] args) {
        HashMap<Integer, String> map = new HashMap<>();
        map.put(1, "Xin");
        map.put(2, "Chen");
        map.put(3, "Yu");
        map.put(4, "Chen");
        map.put(5, "Xi");

        map.forEach(new BiConsumer<Integer, String>() {
            @Override
            public void accept(Integer key, String value) {
                System.out.println(key + " => " + value);
            }
        });
    }
}

在这里插入图片描述

上一篇:===》Java当中Collection下的接口

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值