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的更多相关文章

  1. JAVA集合LIST MAP SET详解

    1. 集合框架介绍 我们知道,计算机的优势在于处理大量的数据,在编程开发中,为处理大量的数据,必须具备相应的存储结构,之前学习的数组可以用来存储并处理大量类型相同的数据,但是通过上面的课后练习,会发现 ...

  2. Java集合之Map和Set

    以前就知道Set和Map是java中的两种集合,Set代表集合元素无序.不可重复的集合:Map是代表一种由多个key-value对组成的集合.然后两个集合分别有增删改查的方法.然后就迷迷糊糊地用着.突 ...

  3. Java集合之Map和Set源码分析

    以前就知道Set和Map是java中的两种集合,Set代表集合元素无序.不可重复的集合:Map是代表一种由多个key-value对组成的集合.然后两个集合分别有增删改查的方法.然后就迷迷糊糊地用着.突 ...

  4. java集合框架——Map

    一.概述 1.Map是一种接口,在JAVA集合框架中是以一种非常重要的集合.2.Map一次添加一对元素,所以又称为“双列集合”(Collection一次添加一个元素,所以又称为“单列集合”)3.Map ...

  5. 《Java基础知识》Java集合(Map)

    Java集合主要由2大体系构成,分别是Collection体系和Map体系,其中Collection和Map分别是2大体系中的顶层接口. 今天主要讲:Map主要有二个子接口,分别为HashMap.Tr ...

  6. Java集合框架——Map接口

    第三阶段 JAVA常见对象的学习 集合框架--Map集合 在实际需求中,我们常常会遇到这样的问题,在诸多的数据中,通过其编号来寻找某一些信息,从而进行查看或者修改,例如通过学号查询学生信息.今天我们所 ...

  7. Java集合之Map

    Map架构: 如上图: (1)Map是映射接口,Map中存储的内容是键值对(key-value) (2)AbstractMap是继承于Map的抽象类,实现了Map中的大部分API. (3)Sorted ...

  8. Java集合框架Map接口

    集合框架Map接口 Map接口: 键值对存储一组对象 key不能重复(唯一),value可以重复 常用具体实现类:HashMap.LinkedHashMap.TreeMap.Hashtable Has ...

  9. Java集合中Map接口的使用方法

    Map接口 Map提供了一种映射关系,其中的元素是以键值对(key-value)的形式存储的,能够实现根据key快速查找value: Map中的键值对以Entry类型的对象实例形式存在: 建(key值 ...

随机推荐

  1. 同一个IIS绑定多个Htts 站点问题

    默认情况一个服务器的IIS只能绑定一个HTTPS也就是443端口 要实现多个站点对应HTTPS只能更改IIS配置 地址:C:Windowssystem32inetsrvconfigapplicatio ...

  2. JQuery的选择器对控件ID含有特殊字符的解决方法-涨姿势了!

    1.jquery类库在我们实际项目中用的很多,大家经常需要根据控件的id,获取对应的html元素. 但是:当id含有特殊字符的时候,是不能选中的. 2. 自己简单的测试了下,jquery的id选择器只 ...

  3. ETL testing

    https://www.tutorialspoint.com/etl_testing/index.htm querysurge-installer-6.0.5-linux-x64  测试ETL的工具.

  4. Eclipse中如何调整字体

    Eclipse 字体有两处,一处是控制台的字体,一处是主窗口.这里分别介绍控制台和主窗口字体的调节方法. Window -> Preferences -> General -> Ap ...

  5. 5 - django-csrf-session&cookie

    目录 1 CSRF跨站请求伪造 1.1 CSRF攻击介绍及防御 1.2 防御CSRF攻击 1.2.1 验证 HTTP Referer 字段 1.2.2 在请求地址中添加 token 并验证 1.2.3 ...

  6. 【内核】几个重要的linux内核文件【转】

    转自:http://www.cnblogs.com/lcw/p/3159394.html Preface 当用户编译一个linux内核代码后,会产生几个文件:vmlinz.initrd.img, 以及 ...

  7. replication-manager 搭建

    replication-manager 搭建 介绍 replication-manager 主要用于mysql主从结构的监控和主从切换. 安装 vi /etc/yum.repos.d/signal18 ...

  8. WCF ServiceContract,OperationContract

    代码如下 [ServiceContract] //服务协定定义 using System.ServiceModel; public interface IInterface1 { [Operation ...

  9. 大数据系列之数据仓库Hive中分区Partition如何使用

    Hive系列博文,持续更新~~~ 大数据系列之数据仓库Hive原理 大数据系列之数据仓库Hive安装 大数据系列之数据仓库Hive中分区Partition如何使用 大数据系列之数据仓库Hive命令使用 ...

  10. webpack轻松入门教程

    webpack之傻瓜式教程及前端自动化入门 接触webpack也有挺长一段时间了,公司的项目也是一直用着webpack在打包处理,但前几天在教新人的情况下,遇到了一个问题,那就是:尽管网上的webpa ...