Java 集合之 Map
Map 就是另一个顶级接口了,总感觉 Map 是 Collection 的子接口呢。Map 主要用于表示那些含有映射关系的数据,存储的是一组一组的键值对。Map 是允许你将某些对象与其它一些对象关联起来的关联数组。
举个例子感受一下:我想通过学生的学号来找到对应的姓名就可以使用 Map 来存储 Map< Integer ,String > 。我想知道每个学生一共选了几门课可以这样存储 Map < Student ,List < Course > > 。这样我们就将 Student 这个类和课程的集合 List < Course > 关联起来了 。
下面来说说 Map 这个顶级的接口都有哪些具体的实现。
HashMap :它根据键的 hashCode 值存储数据,大多数情况下可以直接定位到它的值,因而具有很快的访问速度,但遍历顺序却是不确定的。 HashMap 最多只允许一条记录的键为 null ,允许多条记录的值为 null 。HashMap 非线程安全,即任一时刻可以有多个线程同时写 HashMap ,可能会导致数据的不一致。如果需要满足线程安全,可以用 Collections 的 synchronizedMap 方法使HashMap 具有线程安全的能力,或者使用 ConcurrentHashMap 。
Hashtable :Hashtable 是遗留类,很多映射的常用功能与 HashMap 类似,不同的是它承自 Dictionary 类,并且是线程安全的,任一时间只有一个线程能写 Hashtable,并发性不如 ConcurrentHashMap,因为 ConcurrentHashMap 引入了分段锁。Hashtable 不建议在新代码中使用,不需要线程安全的场合可以用 HashMap 替换,需要线程安全的场合可以用 ConcurrentHashMap 替换。
LinkedHashMap :LinkedHashMap 是 HashMap 的一个子类,保存了记录的插入顺序,在用 Iterator 遍历 LinkedHashMap 时,先得到的记录肯定是先插入的,也可以在构造时带参数,按照访问次序排序 。
TreeMap :TreeMap 实现 SortedMap 接口,能够把它保存的记录根据键排序,默认是按键值的升序排序,也可以指定排序的比较器,当用 Iterator 遍历 TreeMap 时,得到的记录是排过序的。如果使用排序的映射,建议使用 TreeMap 。在使用 TreeMap 时,key 必须实现 Comparable 接口或者在构造 TreeMap 时传入自定义的 Comparator ,否则会在运行时抛出ClassCastException 类型的异常。
对于上述四种 Map 类型的类,要求映射中的 key 是不可变对象。不可变对象是该对象在创建后它的哈希值不会被改变 。可以参考这篇文章理解,String 与不可变对象。如果对象的哈希值发生变化,Map 对象很可能就定位不到映射的位置了
HashMap 的底层主要是基于 hash 表,首先来介绍一下 hash 相关的知识。hash 又名散列,hash 表也就是散列表,hash 表的出现是为了使得数据的查找变得简单,快速,原理是根据关键字的 hashCode 值来确定该关键字存储的位置。而计算出 hashCode 值的方法也就是 hash 算法,若是不同的关键字计算出同一个 hashCode 值,那么就会存储在同一个位置上,此时也就发生了冲突。
我们想要在空间有限的前提下,尽量减少冲突的发生,从而保证我们的查找效率不受影响,就需要设计一个好的 hash 算法,也要充分考虑到当发生冲突了应该怎么办。
那就来看看 HashMap 中是怎么来设计的,主要体现在 hash 算法的设计,使用链表结构和 resize 。首先看一下有哪些重要的属性
public class HashMap<K,V>
extends AbstractMap<K,V>
implements Map<K,V>, Cloneable, Serializable
{
// 默认长度
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16
// 最大容量
static final int MAXIMUM_CAPACITY = 1 << 30;
// 默认负载因子
static final float DEFAULT_LOAD_FACTOR = 0.75f;
// 初始化为空
static final Entry<?,?>[] EMPTY_TABLE = {};
// 存放键值对的 entry 数组
transient Entry<K,V>[] table = (Entry<K,V>[]) EMPTY_TABLE;
// 实际长度
transient int size;
// rehash 之前的最大长度 等于 DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY
int threshold; final float loadFactor; /**
* The number of times this HashMap has been structurally modified
* the HashMap fail-fast. (See ConcurrentModificationException).
*/
transient int modCount; static class Entry<K,V> implements Map.Entry<K,V> {
final K key;
V value;
Entry<K,V> next;
int hash; /**
* Creates new entry.
*/
Entry(int h, K k, V v, Entry<K,V> n) {
value = v;
next = n;
key = k;
hash = h;
}
HashMap 用一个数组来表示 Hash 表,而数组中的每一个元素就是一个entry,也就是一个键值对。当发生冲突的时候,我们可以在同一个位置中存放多个 entry ,此时的结构是链表,通过 entry 中的 next 指向下一个 entry 。以上源码均来自 JDK 1.7 ,在 JDK 1.8 中 entry 变成了 Node 节点,而且若是当同一位置中的元素数量大于 8 这个阀值的时候,链表结构会变成红黑树,这样做的原因是可以大大加快元素的查找速度。
说完了 HashMap 中的结构,我们再来看看具体的操作。主要是 entry 的 put 和 get 过程,put 的过程,我放一张图,过程可一目了然。
map.put("name" , "YJK923"); 这个过程就是通过 hashCode 方法计算 name 这个 String 对象的 hashCode 值(说一句,hashCode 这个方法是 Object 对象的,且是一个 native 方法)得到这个 hashCode 值之后还不能直接进行映射数组下标存储数据,为了使数据尽量不散落在同一位置,还多了一步 hash 值和index 值转化的步骤。
public V put(K key, V value) {
if (table == EMPTY_TABLE) {
inflateTable(threshold);
}
if (key == null)
return putForNullKey(value);
int hash = hash(key);
int i = indexFor(hash, table.length);
for (Entry<K,V> e = table[i]; e != null; e = e.next) {
Object k;
if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {
V oldValue = e.value;
e.value = value;
e.recordAccess(this);
return oldValue;
}
} modCount++;
addEntry(hash, key, value, i);
return null;
} static int indexFor(int h, int length) {
return h & (length-1);
}
看完了 put 看 get ,get 方法通过传入的 key 值计算出 hash 值再得出索引值,若是同一位置有多个元素,则再使用 key 的 equals 方法找到指定的 entry 。最终取出相应的 entry ,但是返回我我们的就是一个 value 值而已。
public V get(Object key) {
if (key == null)
return getForNullKey();
Entry<K,V> entry = getEntry(key); return null == entry ? null : entry.getValue();
} final Entry<K,V> getEntry(Object key) {
if (size == 0) {
return null;
} int hash = (key == null) ? 0 : hash(key);
for (Entry<K,V> e = table[indexFor(hash, table.length)];
e != null;
e = e.next) {
Object k;
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
return e;
}
return null;
}
最后说一下 rehash 的过程,为什么会出现 rehash ,是因为实际长度已经达到了 threshold ,也就是 loadFactor * capacity 。设置这个值的原因就是为了防止过多的元素落在同一个桶中,增加了冲突的发生,及时的增加长度。我们知道 HashMap 的默认长度是 16 ,而若是发生了 rehash ,长度直接翻倍。且 resize 的过程中会重新创建一个新的 entry 数组来存放原有的数据,且所有的 entry 都会重新计算 hash 值。resize 也就是扩容,在扩容的时候会 rehash 。
void addEntry(int hash, K key, V value, int bucketIndex) {
if ((size >= threshold) && (null != table[bucketIndex])) {
resize(2 * table.length);
hash = (null != key) ? hash(key) : 0;
bucketIndex = indexFor(hash, table.length);
} createEntry(hash, key, value, bucketIndex);
} void resize(int newCapacity) {
Entry[] oldTable = table;
int oldCapacity = oldTable.length;
if (oldCapacity == MAXIMUM_CAPACITY) {
threshold = Integer.MAX_VALUE;
return;
} Entry[] newTable = new Entry[newCapacity];
transfer(newTable, initHashSeedAsNeeded(newCapacity));
table = newTable;
threshold = (int)Math.min(newCapacity * loadFactor, MAXIMUM_CAPACITY + 1);
} void transfer(Entry[] newTable, boolean rehash) {
int newCapacity = newTable.length;
for (Entry<K,V> e : table) {
while(null != e) {
Entry<K,V> next = e.next;
if (rehash) {
e.hash = null == e.key ? 0 : hash(e.key);
}
int i = indexFor(e.hash, newCapacity);
e.next = newTable[i];
newTable[i] = e;
e = next;
}
}
}
通过改进 hash 算法,增加链表的存储结构,rehash 等操作,我们就可以将原始的 hash 表和 hash 算法应用到 JDK 1.7 中的 HashMap 中,然而在 JDK 1.8 中又对 HashMap 的功能进行了增强,主要体现在以下方面 1 当链表的长度大于 8 的时候,就将链表转化为红黑树 。2 改进了 hash 的过程,也就是 key 映射到 index 这个过程进行增强,降低了冲突发生的可能。 3 对 rehash 的增强,使其不用重新计算之前 entry 的 index 值。
最后还补充一点关于 Map 的遍历,有几下几种方式:
1 获取 key 的集合 keySet 。
2 获取 value 的集合 Collection 。
3 获取 entry 的集合 entrySet 。
Set<Integer> keySet = map.keySet();
Iterator<Integer> it2 = keySet.iterator();
while(it2.hasNext()){
System.out.println(it2.next());
} Collection<String> values = map.values();
Iterator<String> it = values.iterator();
while(it.hasNext()){
System.out.println(it.next());
} Set<Entry<Integer,String>> entrySet = map.entrySet();
Iterator<Entry<Integer, String>> iterator = entrySet.iterator();
while (iterator.hasNext()) {
Entry<Integer, String> entry = iterator.next();
System.out.println(entry.getKey() + " ----> "+ entry.getValue());
}
最后我想问,还有谁 ?还在使用 JDK1.7 。
我 ~
推荐阅读:Java 集合之 Collection
参考资料:Java8系列之重新认识HashMap
Java 集合之 Map的更多相关文章
- JAVA集合LIST MAP SET详解
1. 集合框架介绍 我们知道,计算机的优势在于处理大量的数据,在编程开发中,为处理大量的数据,必须具备相应的存储结构,之前学习的数组可以用来存储并处理大量类型相同的数据,但是通过上面的课后练习,会发现 ...
- Java集合之Map和Set
以前就知道Set和Map是java中的两种集合,Set代表集合元素无序.不可重复的集合:Map是代表一种由多个key-value对组成的集合.然后两个集合分别有增删改查的方法.然后就迷迷糊糊地用着.突 ...
- Java集合之Map和Set源码分析
以前就知道Set和Map是java中的两种集合,Set代表集合元素无序.不可重复的集合:Map是代表一种由多个key-value对组成的集合.然后两个集合分别有增删改查的方法.然后就迷迷糊糊地用着.突 ...
- java集合框架——Map
一.概述 1.Map是一种接口,在JAVA集合框架中是以一种非常重要的集合.2.Map一次添加一对元素,所以又称为“双列集合”(Collection一次添加一个元素,所以又称为“单列集合”)3.Map ...
- 《Java基础知识》Java集合(Map)
Java集合主要由2大体系构成,分别是Collection体系和Map体系,其中Collection和Map分别是2大体系中的顶层接口. 今天主要讲:Map主要有二个子接口,分别为HashMap.Tr ...
- Java集合框架——Map接口
第三阶段 JAVA常见对象的学习 集合框架--Map集合 在实际需求中,我们常常会遇到这样的问题,在诸多的数据中,通过其编号来寻找某一些信息,从而进行查看或者修改,例如通过学号查询学生信息.今天我们所 ...
- Java集合之Map
Map架构: 如上图: (1)Map是映射接口,Map中存储的内容是键值对(key-value) (2)AbstractMap是继承于Map的抽象类,实现了Map中的大部分API. (3)Sorted ...
- Java集合框架Map接口
集合框架Map接口 Map接口: 键值对存储一组对象 key不能重复(唯一),value可以重复 常用具体实现类:HashMap.LinkedHashMap.TreeMap.Hashtable Has ...
- Java集合中Map接口的使用方法
Map接口 Map提供了一种映射关系,其中的元素是以键值对(key-value)的形式存储的,能够实现根据key快速查找value: Map中的键值对以Entry类型的对象实例形式存在: 建(key值 ...
随机推荐
- Fiddler 使用
一.模拟post请求 User-Agent: FiddlerContent-Type: application/json; charset=utf-8Content-Length: 138Conten ...
- CSS基础之选择器
一:CSS介绍 CSS(Cascading Style Sheet,层叠样式表)定义如何显示HTML元素. 当浏览器读到,一个样式表时,就会按照. 二:CSS语法 每个CSS有两部分组成:选择器和声明 ...
- zookeeper集群查看状态时报错Error contacting service. It is probably not running的一些坑以及解决办法
最近在搭建mq集群时候需要用到,zookeeper,可是启动的时候显示成功了,查看状态的时候却报错了: 碰到这个问题也是研究好好半天才解决,这里就总结出一个快速解决办法! 首先,必须看日志: 报错信息 ...
- getattr的使用
from requests_html import HTMLSession class UrlGenerator(object): def __init__(self, root_url): self ...
- openjudge-NOI 2.5-1700 八皇后问题
题目链接:http://noi.openjudge.cn/ch0205/1700/ 题解: 经典深搜题目…… #include<cstdio> ][]; int num; void pri ...
- Southwestern Europe Regional Contest 2015 题解
题目链接:http://codeforces.com/gym/101128 题目数7/10 Rank 34/209 A: 题意:给出一张n个点的有向图表示一家有n个员工的公司的隶属图,u->v表 ...
- js对金额浮点数运算精度的处理方案
浮点数产生的原因 浮点数转二进制,会出现无限循环数,计算机又对无限循环小数进行舍入处理 js弱语言的解决方案 方法一: 指定要保留的小数位数(0.1+0.2).toFixed(1) = 0.3;这个方 ...
- Ubuntu下使用virtualenv
Ubuntu 18.04,Python 3.6.5(最新3.7),virtualenv 16.0.0, 即将在Ubuntu上大张旗鼓地干活啦!那么,将之前安装的virtualenv运行起来吧(前面都是 ...
- 创建第一个MySQL数据库earth及表area
Windows 10家庭中文版,MySQL 5.7.20 for Win 64,2018-05-08 数据库earth描述: 用于记录地球上的事物,一期包含地理区域信息——表area. 字符集编码:u ...
- ArcMap2SLD添加中文支持
首先,你可以从作者提供的链接下载ArcMap2SLD.zip(支持ArcMap10.2) 1.打开LUT_sld_mapping_file.xml文件(上传文件中已经修改)修改文件<LUT> ...