Java之Map的使用以及简单原理

类图结构

image-20210227163402845

介绍

  • 将键映射到值的对象。映射不能包含重复的键;每个键最多可以映射到一个值。key-value结构。
  • Map接口代替了Dictionary这个抽象类。
  • 常用实现类有,HashMap,TreeMap,LinkedHashMap,ConcurrentHashMap(线程安全)。

常用API

方法说明
int size();返回此映射中的键值映射数
boolean isEmpty();此Map是否为空
boolean containsKey(Object key);是否包含某个key
boolean containsValue(Object value);是否包含某个Value
V get(Object key);获取这个key对应的value
V put(K key, V value);插入一个键值对
V remove(Object key);移除一个键值对
Set keySet();获取所有的key
Collection values();获取所有value
Set<Map.Entry<K, V>> entrySet();获取所有键值对
V getOrDefault(Object key, V defaultValue);获取这个key对应的value,没有则返回默认值

一些1.8的新方法

forEach
  • 相当于for循环
//源码
default void forEach(BiConsumer<? super K, ? super V> action) {
    Objects.requireNonNull(action);
    for (Map.Entry<K, V> entry : entrySet()) {
        K k;
        V v;
        try {
            k = entry.getKey();
            v = entry.getValue();
        } catch(IllegalStateException ise) {
            // this usually means the entry is no longer in the map.
            throw new ConcurrentModificationException(ise);
        }
        action.accept(k, v);
    }
}
//使用
map.forEach((key,value)->{
   System.out.println(key+":"+value) ;
});
compute
  • 相当于put时,对key与旧value先进行计算产生新value,再put。如果新value为空,则移除映射。
//源码
default V compute(K key,BiFunction<? super K, ? super V, ? extends V> remappingFunction) {
    Objects.requireNonNull(remappingFunction);
    V oldValue = get(key);
    //通过旧value计算新value
    V newValue = remappingFunction.apply(key, oldValue);
    if (newValue == null) {
        // 新value为空,则移除映射。
        if (oldValue != null || containsKey(key)) {
            // something to remove
            remove(key);
            return null;
        } else {
            // nothing to do. Leave things as they were.
            return null;
        }
    } else {
        //新value不为空,则添加映射。
        put(key, newValue);
        return newValue;
    }
}
//使用
map.compute("key",(key,oldValue)->{
    //对key(既传入的"key")和oldValue(既map.get(key))进行计算
    Object newValue={...};
    return newValue;
})
merge
  • 和compute差不多,merge是用旧value与传入value计算出新value,然后put。
//源码
default V merge(K key, V value,BiFunction<? super V, ? super V, ? extends V> remappingFunction) {
    Objects.requireNonNull(remappingFunction);
    Objects.requireNonNull(value);
    V oldValue = get(key);
    //如果旧value为空,则新value直接为传入的value,不为空则进行计算
    V newValue = (oldValue == null) ? value :
               remappingFunction.apply(oldValue, value);
    if(newValue == null) {
        remove(key);
    } else {
        put(key, newValue);
    }
    return newValue;
}
//使用
map.merge("key","value",(oldValue,value)->{
    //对key(既传入的"key")和oldValue(既map.get(key))进行计算
    String newValue={...};
    return newValue;
})

HashMap原理

  • 底层结构是:数组+链表+红黑树。链表与红黑树是用于解决hash冲突的,当链表长度大于等于8且数组长度大于64时会转换为红黑树,当红黑树大小大于等于6时会转换为链表。
    image-20210227215344558
  • 无参构造器的底层数组初始化大小为16,也和ArrayList一样是在第一次put的时候才初始化。
  • 底层数组的大小为2的幂,因为HashMap是通过tab.length%hash(key)来确定key对应的数组下标的,而当hash是2的幂时数组长度-1和key的hash值求与是和数组长度与hash求余是相同结果的。既在数组长度为2的幂时,满足tab.length%hash(key)=(tab.length-1)&hash(key)因为求与效率要比取模效率高,所有这样做。
  • 构造器中传入的初始数组大小,都会被转换为更大的2的幂。比如输入17,返回32,输入40,返回64。如果知道这个map大概要装多少数据,可以用new HashMap((int)(size/0.75f)-1)实例化HashMap。
  • 构造器中loadFactor:负载系数,默认0.75f,在时间和空间成本之间提供了很好的折中,值越高,查找开销越高,但是会减少空间开销。
  • put流程(get和remove都差不多):
    1. 通过上面说的**(tab.length-1)&hash(key)**找到key对应的数组下标。
    2. 如果数组此下标处为null,则直接构造一个Node放入。否则产生了hash冲突。就需要解决hash冲突。
    3. 如果次下标位置的oldKey刚好和newKey相同,既oldHash==newHash&&oldKey.equals(new key),则直接替换新value,返回旧value。
    4. 如果oldKey与newKey不相同,且下标位置的Node为TreeNode,则调用红黑树的方法插入。
    5. 如果oldKey与newKey不相同,且下标位置的Node为Node,则遍历链表,如果没有找到相同key替换,则插入到链表最后。
  • 如果是批量插入,使用putAll会有更高的效率。因为putAll会首先检查是否需要扩容。
  • 扩容:条件为数组大小大于阈值(数组大小*loadFactor),会扩容为之前的2倍。
  • 红黑树,左大右小的二叉搜索树,相比于平衡二叉树,对于平衡的要求不是那么严格,在插入和查找的效率上更加均衡。他满足以下5个条件:
    • 节点是红色或黑色。
    • 根是黑色。
    • 所有叶子都是黑色。
    • 从任一节点到其每个叶子的所有简单路径都包含相同数目的黑色节点。
    • 从每个叶子到根的所有路径上不能有两个连续的红色节点。

TreeMap原理

  • 底层是红黑树。TreeMap利用红黑树左大右小的性质对key进行了排序
  • 因为底层使用的是红黑树的结构,所以 containsKey、get、put、remove 等方法的时间复杂度都是 log(n)。

LinkedHashMap原理

  • HashMap 是无序的,TreeMap 可以按照 key 进行排序,而LinkedHashMap是可以按插入顺序排序的。
  • LinkedHashMap是继承HashMap的,本身有HashMap的性质,同时也有链表的性质,所以通过链表还有2大特性:
    • 按照插入顺序进行访问(默认)。
    • 实现了LRU功能,可以把很久都没有访问的 key 自动删除。
  • 有个accessOrder字段控制了排序规则,false(默认)按照插入顺序;true则按照访问顺序,访问一个元素就会把次元素移到尾巴,头部就是最久没访问的,LRU就是移除的头部元素。

简单的LRU示例:

LinkedHashMap<Integer, Integer> map = new LinkedHashMap<Integer, Integer>(16,0.75f,true) {
    @Override
    // 覆写了删除策略的方法,我们设定当节点个数大于 100 时,就开始删除头节点(既最久没访问的)
    protected boolean removeEldestEntry(Map.Entry<Integer, Integer> eldest) {
      return size() > 100;
    }
};

ConcurrentHashMap原理

  • 所有的操作都是线程安全的遍历过程中的修改不会抛出并发修改错误,而上面的几个map会
  • 多个线程同时进行 put、remove 等操作时并不会阻塞,ConcurrentHashMap只会锁住单个槽点,可以同时进行,和 HashTable 不同,HashTable 在操作时,会锁住整个 Map
  • 数组 + 链表 + 红黑树的结构。和HashMap特点差不多一致。下面是不一致的地方:
    • 红黑树结构略有不同,HashMap 的红黑树中的节点叫做 TreeNode,TreeNode 不仅仅有属性,还维护着红黑树的结构,比如说查找,新增等等;ConcurrentHashMap 中红黑树被拆分成两块,TreeNode 仅仅维护的属性和查找功能新增了 TreeBin,来维护红黑树结构,并负责根节点的加锁和解锁
    • 新增 ForwardingNode (转移)节点,扩容的时候会使用到,通过使用该节点,来保证扩容时的线程安全。
  • put过程和HashMap差不多,区别是put过程中发现正在扩容,当前线程会取帮助扩容,因为ConcurrentHashMap的扩容过程是把数组划分为几个部分,每个线程来拿一部分进行扩容
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值