【Java入门】数据结构(二)Map


首先,看Map的主要的实现类的简化 继承层次图:
在这里插入图片描述

Map

public interface Map<K,V>

映射到的对象。

一个映射不能包含重复的键;每个键最多只能映射到一个值。

此接口取代 Dictionary 类,后者完全是一个抽象类,而不是一个接口。

Map 接口提供三种 collection 视图,允许以键集、值集或键-值映射关系集的形式查看某个映射的内容。映射顺序定义为迭代器在映射的 collection 视图上返回其元素的顺序。(某些映射实现可明确保证其顺序,如 TreeMap 类;另一些映射实现则不保证顺序,如 HashMap 类)

主要方法
在这里插入图片描述

Map.Entry

public static interface Map.Entry<K,V>

映射项(键-值对)。可看作是Map中存储的一个单位。

方法摘要
在这里插入图片描述
获得映射项引用的唯一方法是通过此 collection 视图的迭代器来实现。但可以自己创建新的实现类对象(充当类似pair来使用)。

Map.Entry有两个主要的实现类:AbstractMap.SimpleEntry<K,V> 和 AbstractMap.SimpleImmutableEntry<K,V> .

AbstractMap.SimpleEntry

维护键和值的 Entry。可以使用 setValue 方法更改值。此类简化了构建自定义映射实现的过程。例如,可以使用 Map.entrySet().toArray 方法方便地返回 SimpleEntry 实例数组。
在这里插入图片描述
留意 toString() 方法。返回此映射项的 String 表示形式。此实现返回此项的键的字符串表示形式,后跟等号 ("="),然后是此项的值的字符串表示形式。

AbstractMap.SimpleImmutableEntry

维护不可变的键和值的 Entry。此类不支持 setValue 方法。在返回线程安全的键-值映射关系快照的方法中,此类也许很方便。

构造方法和主要方法与前者一样。除了setValue()方法会现抛出 UnsupportedOperationException。

TreeMap

public class TreeMap<K,V>
	extends AbstractMap<K,V>
	implements NavigableMap<K,V>, Cloneable, Serializable

特点

  • 基于红黑树实现。
  • 根据其键的自然顺序进行排序,或者根据创建映射时提供的 Comparator 进行排序。
  • 继承了NavigableMap接口(继承了SortedMap接口),可支持一系列的导航定位以及导航操作的方法。
  • 为 containsKey、get、put 和 remove 操作提供受保证的 log(n) 时间开销
  • 不是同步的。解决方法:SortedMap m = Collections.synchronizedSortedMap(new TreeMap(...));

构造方法
在这里插入图片描述
常用方法:(参考

增添元素:
V put(K key, V value):将指定映射放入该TreeMap中
V putAll(Map map):将指定map放入该TreeMap中
删除元素:
void clear():清空TreeMap中的所有元素
V remove(Object key):从TreeMap中移除指定key对应的映射
修改元素:
V replace(K key, V value):替换指定key对应的value值
boolean replace(K key, V oldValue, V newValue):当指定key的对应的value为指定值时,替换该值为新值
查找元素:
boolean containsKey(Object key):判断该TreeMap中是否包含指定key的映射
boolean containsValue(Object value):判断该TreeMap中是否包含有关指定value的映射
Map.Entry<K, V> firstEntry():返回该TreeMap的第一个(最小的)映射
K firstKey():返回该TreeMap的第一个(最小的)映射的key
Map.Entry<K, V> lastEntry():返回该TreeMap的最后一个(最大的)映射
K lastKey():返回该TreeMap的最后一个(最大的)映射的key
v get(K key):返回指定key对应的value
SortedMap<K, V> headMap(K toKey):返回该TreeMap中严格小于指定key的映射集合
SortedMap<K, V> subMap(K fromKey, K toKey):返回该TreeMap中指定范围的映射集合(大于等于fromKey,小于toKey)

遍历方式:

for (Map.Entry entry : treeMap.entrySet()) {
      System.out.println(entry);
}
Iterator iterator = treeMap.entrySet().iterator();
while (iterator.hasNext()) {
      System.out.println(iterator.next());
}

原理看这篇

HashMap

public class HashMap<K,V>
	extends AbstractMap<K,V>
	implements Map<K,V>, Cloneable, Serializable

基于哈希表的 Map 接口的实现。

特点

  • 有两个参数影响其性能:初始容量和加载因子。
    • 容量是哈希表中桶的数量,初始容量只是哈希表在创建时的容量。
    • 加载因子是哈希表在其容量自动增加之前可以达到多满的一种尺度。
    • 当哈希表中的条目数超出了加载因子与当前容量的乘积时,则要对该哈希表进行 rehash 操作(即重建内部数据结构),从而哈希表将具有大约两倍的桶数。
  • 不是同步的。
    • 解决方法:Map m = Collections.synchronizedMap(new HashMap(...));
  • 底层基于数组+链表+红黑树。(如下图)(当因哈希冲突使得链表深度达到8时,链表就转换为红黑树)
    图示
    构造方法
    在这里插入图片描述
    主要方法
    在这里插入图片描述
    原理以后单独写一篇。可以先看看这两篇,分析得非常详细:(图片来源同)
    https://www.jianshu.com/p/ee0de4c99f87
    https://blog.csdn.net/woshimaxiao1/article/details/83661464
    在这里插入图片描述

LinkedHashMap

public class LinkedHashMap<K,V>
	extends HashMap<K,V>
	implements Map<K,V>

继承HashMap,Map 接口的哈希表和链接列表实现,具有可预知的迭代顺序。

特点

  • 维护着一个运行于所有条目的双重链接列表。此链接列表定义了迭代顺序,该迭代顺序通常就是将键插入到映射中的顺序(插入顺序)。图解LinkedHashMap原理

  • 不是同步的。解决方法:Map m = Collections.synchronizedMap(new LinkedHashMap(...));

构造方法:(最后一种常用于生成一个与原来顺序相同的映射副本)
在这里插入图片描述
样例代码:(源同上)

Map<String, String> linkedHashMap = new LinkedHashMap<>();
linkedHashMap.put("name1", "aaa");
linkedHashMap.put("name2", "bbb");
linkedHashMap.put("name3", "ccc");
Set<Entry<String, String>> set = linkedHashMap.entrySet();
Iterator<Entry<String, String>> iterator = set.iterator();
while(iterator.hasNext()) {
    Entry entry = iterator.next();
    String key = (String) entry.getKey();
    String value = (String) entry.getValue();
    System.out.println("key:" + key + ",value:" + value);
}

HashTable

public class Hashtable<K,V>
	extends Dictionary<K,V>
	implements Map<K,V>, Cloneable, Serializable

此类(继承了传统的Dictionary类)实现一个哈希表,该哈希表将键映射到相应的值。

Hashtable同样是基于哈希表实现的,同样每个元素是一个key-value对,其内部也是通过单链表解决冲突问题,容量不足(超过了阀值)时,同样会自动增长。是线程安全的,能用于多线程环境中。实现了Serializable接口,它支持序列化,实现了Cloneable接口,能被克隆。

构造方法
在这里插入图片描述
主要方法
在这里插入图片描述

put方法的实现:
1.确定value值不可为null
2.若key已经在table中存在,通过for循环,查找符合条件的key,赋予新的Value 返回 旧值
3.若不存在则进行新增操作:
3.1 修改次数+1,判断HashTable是否需要扩容
3.2 获取tab索引下的Entry 赋给 e
3.3 创建一个HashTableEntry赋给tab指定索引位置
3.4 tab的条目数 +1

源码及分析:https://www.jianshu.com/p/b776e05954f9https://blog.csdn.net/ns_code/article/details/36191279

HashTable和HashMap的区别

散列表 实现方式 数据安全 数据安全实现方式 值是否可为Null
HashMap 数组+单向链表+红黑树 不安全 可为Null
HashTable 数组+单向链表 安全 Synchronized 不可为 Null

参考自:https://www.cnblogs.com/williamjie/p/9099141.html

1、继承的父类不同。
Hashtable继承自Dictionary类,而HashMap继承自AbstractMap类。但二者都实现了Map接口。

2、线程安全性不同
Hashtable 中的方法是Synchronize的,而HashMap中的方法在缺省情况下是非Synchronize的。在多线程并发的环境下,可以直接使用Hashtable,不需要自己为它的方法实现同步,但使用HashMap时就必须要自己增加同步处理。

HashMap线程不安全的原因
当发生hash冲突的时候,hashmap是采用链表的方式来解决的,在对应的数组位置存放链表的头结点。对链表而言,新加入的节点会从头结点加入。
put操作:现在假如A线程和B线程同时对同一个数组位置调用addEntry,两个线程会同时得到现在的头结点,然后A写入新的头结点之后,B也写入新的头结点,那B的写入操作就会覆盖A的写入操作造成A的写入操作丢失。
*remove操作:当多个线程同时操作同一个数组位置的时候,也都会先取得现在状态下该位置存储的头结点,然后各自去进行计算操作,之后再把结果写会到该数组位置去,其实写回的时候可能其他的线程已经就把这个位置给修改过了,就会覆盖其他线程的修改。
resize操作:当多个线程同时检测到总数量超过门限值的时候就会同时调用resize操作,各自生成新的数组并rehash后赋给该map底层的数组table,结果最终只有最后一个线程生成的新数组被赋给table变量,其他线程的均会丢失。

3、是否提供contains方法
HashMap去掉contains方法(底层还会调用),只有containsValue和containsKey。
Hashtable则保留了contains,containsValue和containsKey三个方法,其中contains和containsValue功能相同。

4、key和value是否允许null值
HashTable中,key和value都不允许出现null值。(编译可以通过,但运行时会抛出NullPointerException异常)
HashMap中,null可以作为键,这样的键只有一个;可以有一个或多个键所对应的值为null。

5、hash值不同
HashTable直接使用对象的hashCode。而HashMap重新计算hash值。

hashCode是jdk根据对象的地址或者字符串或者数字算出来的int类型的数值。
Hashtable计算hash值,直接用key的hashCode();在求hash值对应的位置索引时,用取模运算。
HashMap重新计算了key的hash值;而HashMap在求位置索引时,则用与运算,且这里一般先用hash&0x7FFFFFFF(将负的hash值转化为正值)后,再对length取模。

6、内部实现使用的数组初始化和扩容方式不同
HashTable在不指定容量的情况下的默认容量为11,而HashMap为16。
Hashtable不要求底层数组的容量一定要为2的整数次幂,而HashMap则要求一定为2的整数次幂。
Hashtable扩容时,将容量变为原来的2倍加1;而HashMap扩容时,将容量变为原来的2倍。

ConcurrentHashMap

public class ConcurrentHashMap<K,V>
	extends AbstractMap<K,V>
	implements ConcurrentMap<K,V>, Serializable

支持获取的完全并发和更新的所期望可调整并发的哈希表。此类遵守与 Hashtable 相同的功能规范,并且包括对应于 Hashtable 的每个方法的方法版本(所以构造方法和主要方法不再赘述)。继承自AbstractMap类,实现了ConcurrentMap和Serializable接口。

为什么需要ConcurrentHashMap?
HashMap是线程不安全的:get时会形成环状链表,死循环;
HashTable线程安全但代价太大:get/put所有相关操作都是synchronized的,相当于整个哈希表加了一把大锁,多线程访问时候,只要有一个线程访问或操作该对象,那其他线程只能阻塞,相当于将所有的操作串行化,在竞争激烈的并发场景中性能就会非常差。
ConcurrentHashMap则在前者基础上采用"分段锁"思想,将map拆分成多个Segment(默认16个)。在多线程环境下,不同线程操作不同的Segment,他们互不影响,这便可实现并发操作。

在这里插入图片描述
ConcurrentHashMap由一个Segment[]数组组成,而每个Segment维护着一个HashEntry数组:

public class ConcurrentHashMap<K, V> extends AbstractMap<K, V> 
			implements ConcurrentMap<K, V>, Serializable {
  final Segment<K,V>[] segments;
  ...
}

static final class Segment<K,V> extends ReentrantLock implements Serializable {
  transient volatile HashEntry<K,V>[] table;
}

static final class HashEntry<K,V> {
   final int hash;
   final K key;
   volatile V value;
   volatile HashEntry<K,V> next;
 }

Segment继承了ReentrantLock,所以它就是一种可重入锁(ReentrantLock)。在ConcurrentHashMap,一个Segment就相当于一个子哈希表,Segment里维护了一个HashEntry数组,并发环境下,对于不同Segment的数据进行操作是不用考虑锁竞争的。
ConcurrentHashMap的扩容是Segment中的HashEntry数组扩容。当HashEntry达到某个临界点后,会扩容2为之前的2倍, 原理跟HashMap扩容类似。

get方法无需加锁,由于其中涉及到的共享变量都使用volatile修饰,volatile可以保证内存可见性,所以不会读取到过期数据;Segment中的put方法是要加锁的,不过锁粒度细。

JDK8版本的变动
jdk8版本的ConcurrentHashMap直接抛弃了Segment的设计,采用了较为轻捷的Node + CAS + Synchronized设计,来保证线程安全。
在这里插入图片描述
ConcurrentHashMap的大体结构为一个node数组(默认为16,可以自动扩展,扩展速度为0.75),每一个节点挂载一个链表。当链表挂载数据大于8时,链表自动转换成红黑树。此时,node数组中存放的不是TreeNode对象,而是就是TreeBin对象(TreeNode节点的包装对象,可以认为是红黑树对象,代替了TreeNode的根节点)。

这部分参考:https://www.jianshu.com/p/1e1a96075256https://blog.csdn.net/helei810304/article/details/79786606

总结

参考了这一篇:https://www.cnblogs.com/skywang12345/p/3308833.html

HashMap
最常用的Map,它根据键的HashCode 值存储数据,根据键可以直接获取它的值,具有很快的访问速度。HashMap最多只允许一条记录的键为Null(多条会覆盖);允许多条记录的值为 Null。非同步的。

TreeMap
能够把它保存的记录根据键(key)排序,默认是按升序排序,也可以指定排序的比较器,当用Iterator 遍历TreeMap时,得到的记录是排过序的。TreeMap不允许key的值为null。非同步的。

Hashtable
与 HashMap类似,不同的是:key和value的值均不允许为null;它支持线程的同步,即任一时刻只有一个线程能写Hashtable,因此也导致了Hashtale在写入时会比较慢。

LinkedHashMap
保存了记录的插入顺序,在用Iterator遍历LinkedHashMap时,先得到的记录肯定是先插入的.在遍历的时候会比HashMap慢。key和value均允许为空,非同步的。

ConcurrentHashMap
与HashTable的用法和实现很相似,也支持线程的同步,而且采用分段锁的思想,并发性能更佳。

性能比较
在这里插入图片描述

原创文章 16 获赞 17 访问量 2125

猜你喜欢

转载自blog.csdn.net/weixin_42368748/article/details/104763648