Map

Map也被称为映射表(关联数组),使得你可以用来查找对象,键所关联的对象被称为,因此你可以使用键来找值,用对象来查找对象。Map保存的键不重复,如果有相同的键被加入,那么原来的值将被加入的值覆盖。键必须是唯一的,而值可以有重复。标准的java类库中包含了Map的几种基本实现,包括:HashMap、TreeMap、LinkedHashMap、WeakHashMap、ConcurrentHashMap、IdentityHashMap。它们都有同样的基本接口Map,但是行为特征各不相同,这表现在效率、键值对的保存及呈现次序、对象的保存周期、映射表如何在多线程程序中工作和判断“键”等价的策略等方面。

  • HashMap:和HashSet一样,以散列的方式来存储数据,提供了快速查找的能力(它取代了HashTable)。插入和查询“键值对”的开销是固定的。可以通过构造方法的容量和负载因子,以调整容器的性能。
  • LinkHashMap: 类似于HashMap,但是迭代遍历它时,取得“键值对”的顺序是其插入顺序,或者是最近最少使用(LRU)的次序。查询时只比HashMap慢一点;而在迭代访问时反而更快,因为它使用了链表维护内部次序。
  • TreeMap:基于红黑树的实现。查看“键”或“键值对”时,它们会被排序(次序由Comparable或Comparator决定)。TreeMap的特点在于所得到的结果是经过排序的。TreeMap是唯一的带有subMap()方法的Map,它可以返回一个子数。
  • WeakHashMap:弱键(weak key)映射,允许释放映射表所指向的对象;这是为解决某类特殊问题而设计的。如果映射表之外没有引用指向这个”键“,则该”键“可以被垃圾收集器(GC)回收。
  • ConcurrentHashMap:一种线程安全的Map,它不涉及同步加锁。
  • IdentityHashMap:使用==代替equals(),对“键”进行比较的散列映射。专为解决某类特殊问题而设计的。

带有Hash字段的映射类都是利用散列来存储元素的,如果“键”被用于散列Map,那么它必须还具有恰当的hashCode方法;而TreeMap则是利用树来存储”键“的,该键必须实现Comparable接口。

HashMap如何进行快速查找:

HashMap专门对速度进行了优化,那么HashMap是如何在映射表中快速查找键对象呢,如果使用线性查找(for循环)那执行速度会相当地慢。因此HashMap使用了特殊的值散列码,来取代对键的缓慢搜索。散列码是通过将对象的某些信息进行转换而生成的,散列码是”相对唯一”的,是int类型。通过根类Object中的hashCode()方法就可以获取散列码,所以每个对象都能产生散列码。HashMap就是使用对象的hashCode()进行快速查询,此方法能够显著提高性能。因此散列是映射中存储元素时最常用的方式。 散列的价值在于速度:散列使得查询得以快速进行。

那么键值是如何利用散列进行快速查询的呢?首先我们必须要知道存储一组数据最快的数据结构是数组,我们可以通过数组下标就可以立即得到该数据,所以我可以通过数组来存储键的信息。但是Map中保存键值的数量是不确定的,而数组的容量却是固定的,这不就冲突了吗?其实数组并不是保存键本身。而是通过键对象生成一个数字,将其作为数组的下标。这个数字就是散列码。也是就是我们上面所说的,由hashCode()方法生成()。

每个对象的散列码是”相对唯一”的,如果有1w个对象就差不多有1w个散列码,难道我们就必须创建1w容量的数组?因为hashCode()返回的是int类型的值,所以我只要把散列值除以一个特定数字,结果的余数就是数组的下标,而这个特定数字就是数组容量的大小。 如果把余数当作数组的下标,那么不同的键就就可能产生相同的下标。也就是说,可能产生冲突。因此,数组多大就不重要了,任何键总能在数组中找到它的位置。

于是查询一个值的过程首先就是计算散列码,然后使用散列码查询数组。而数组中的元素并不是存储键对象本身,而是存储是链表,链表中存储的是所有余数相同的键对象。然后根据链表中的键对象使用equal()方法进行线性查询。这部分的查询自然会比较慢,但是,如果散列函数好的话,数组每个位置就只有较少的值。

利用hashCode值快速跳到数组某个位置,然后对该位置的链表进行较少的线性比较,这就是HashMap之所以快的原因,就如下表所示:

理解了散列的原理,我们就能够实现一个简单的散列Map了:

/**
* 简单的Map容器,继承AbstractMap省去我们重写很多通用的方法
*
*/
public class SimpleHashMap<K, V> extends AbstractMap<K, V> {/*** 实现Map.Entry接口,用于存储键值对的节点*/class Node<K, V> implements Map.Entry<K, V> {private K key;private V value;public Node(K key, V value) {this.key = key;this.value = value;}@Overridepublic K getKey() {return key;}@Overridepublic V getValue() {return value;}@Overridepublic V setValue(V v) {V result = v;value = v;return result;}@Overridepublic int hashCode() {return Objects.hashCode(key) ^ Objects.hashCode(value);}/*** 只有键和值一样才相等*/@Overridepublic boolean equals(Object obj) {if (obj == this)return true;if (obj instanceof Map.Entry) {Map.Entry<K, V> e = (Map.Entry<K, V>) obj;//key和value都相等才返回trueif (Objects.equals(key, e.getKey()) &&Objects.equals(value, e.getValue()))return true;}return false;}@Overridepublic String toString() {return key + "=" + value;}}/*** 使用2的次方作为哈希数组的大小*/static final int SIZE = 512;/*** 声明了一个LinkedList类型的数组,LinkedList存放的是Node类型的键值对元素*/LinkedList<Node<K, V>>[] buckets = new LinkedList[SIZE];/*** 继承Map接口,需要实现entrySet方法,用于返回所有的键值对集合。*/@Overridepublic Set<Entry<K, V>> entrySet() {Set<Map.Entry<K, V>> set = new HashSet<Map.Entry<K, V>>();for (LinkedList<Node<K, V>> bucket : buckets) {if (bucket == null) continue;for (Node<K, V> mpair : bucket) {set.add(mpair);}}return set;}/*** 把键值对存入Map中*/@Overridepublic V put(K key, V value) {V oldValue = null;//用key的hashCode余数作为数组的下标int index = Math.abs(key.hashCode()) % SIZE;if (buckets[index] == null) {buckets[index] = new LinkedList<Node<K, V>>();}//获取数组中LinkedList对象LinkedList<Node<K, V>> bucket = buckets[index];//创建键值对节点,存放键值对对象Node<K, V> pair = new Node<K, V>(key, value);boolean found = false;ListIterator<Node<K, V>> it = bucket.listIterator();//遍历LinkedList中是否有相等的keywhile (it.hasNext()) {Node<K, V> iPair = it.next();if (iPair.getKey().equals(key)) {//有相等的key//取出老value用于返回oldValue = iPair.getValue();//取代原来的键值对对象it.set(pair);found = true;break;}}//如果LinkedList中没有key于之相等,则把新的键值对对象加入。if (!found) {buckets[index].add(pair);}return oldValue;}/*** 根据key获取value*/public V get(Object key) {//用key的hashCode余数作为数组的下标int index = Math.abs(key.hashCode()) % SIZE;if (buckets[index] == null) return null;//返回和key相等的value对象for (Node<K, V> iPair : buckets[index]) {if (iPair.getKey().equals(key)) {return iPair.getValue();}}return null;}public static void main(String[] args) {SimpleHashMap<String, String> m = new SimpleHashMap<>();m.put("A", "C");m.put("B", "C++");m.put("C", "Java");m.put("D", "C#");m.put("E", "Go");m.put("E", "Swift");System.out.println(m);System.out.println("-----------------");System.out.println(m.get("A"));System.out.println("-----------------");System.out.println(m.entrySet());}/*Output:{B=C++, A=C, C=Java, D=C#, E=Swift}-----------------C-----------------[B=C++, A=C, C=Java, D=C#, E=Swift]*/
}

散列表中数组的每个元素我们称为”槽位”(slot) 也称为“桶位”(bucket),所以我们将表示实际散列表中的数组命令为bucket。桶的数量通常为2的整数次方。对现代处理器来说,除法与求余是最慢的操作。使用2的整数次方长度的散列表,可用掩码代替除法。因为get()是使用最多的操作,求余数的%操作是其开销最大的部分,而使用2的整数次方可以消除此开销。

get()和put()方法都用相同的方式计算在buckets数组中的索引,如果此位置有LinkedList存在,就对其查询。注意上面例子的实现并不意味着对性能进行了调优;它只是想要展示散列映射表执行的各种操作。你可以通过浏览HashMap的代码,看到一个调优过程的实现。比如我们可以修改Node使其成为一种自包含的单向链表(每个Node都有指向下一个的Node的引用),从而不用对每个桶位都使用LinkedList。

下面是用自定义的单向链表代替LinkedList:

public class SimpleHashMap<K, V> extends AbstractMap<K, V> {/*** 用于存储键值对的节点*/class Node<K, V> implements Map.Entry<K, V> {private K key;private V value;Node<K, V> next;//用于指向下一个节点public Node(K key, V value, Node<K, V> next) {this.key = key;this.value = value;this.next = next;}@Overridepublic K getKey() {return key;}@Overridepublic V getValue() {return value;}@Overridepublic V setValue(V newValue) {V oldValue = value;value = newValue;return oldValue;}@Overridepublic int hashCode() {return Objects.hashCode(key) ^ Objects.hashCode(value);}/*** 只有键和值一样才相等*/@Overridepublic boolean equals(Object obj) {if (obj == this)return true;if (obj instanceof Map.Entry) {Map.Entry<K, V> e = (Map.Entry<K, V>) obj;//key和value都相等才返回trueif (Objects.equals(key, e.getKey()) &&Objects.equals(value, e.getValue()))return true;}return false;}@Overridepublic String toString() {return key + "=" + value;}}/*** 使用2的整数次方作为数组的大小*/static final int SIZE = 512;/*** 声明了一个Node节点的数组*/Node<K, V>[] buckets = new Node[SIZE];/*** 继承Map接口,需要实现entrySet方法,用于返回所有的键值对集合。*/@Overridepublic Set<Entry<K, V>> entrySet() {Set<Map.Entry<K, V>> set = new HashSet<Map.Entry<K, V>>();for (Node bucket : buckets) {for (Node<K, V> e = bucket; e != null; e = e.next) {set.add(e);}}return set;}/*** 把键值对存入Map中*/@Overridepublic V put(K key, V value) {//用key的hashCode余数作为数组的下标int index = Math.abs(key.hashCode()) % SIZE;for (Node<K, V> e = buckets[index]; e != null; e = e.next) {//桶内有Node节点,并且key值相等则进行替换if (key.equals(e.key)) {V oldValue = e.value;e.value = value;return oldValue;}}//如果桶内没有Node节点 或 桶内有Node节点但是key值都不匹配//那么就创建一个新的Node节点,并插入链表的头部buckets[index] = new Node<K, V>(key, value, buckets[index]);return null;}/*** 根据key获取value*/public V get(Object key) {//用key的hashCode余数作为数组的下标int index = Math.abs(key.hashCode()) % SIZE;for (Node<K, V> e = buckets[index]; e != null; e = e.next) {K eKey = e.key;if (key.equals(eKey)) {return e.value;}}return null;}public static void main(String[] args) {SimpleHashMap<String, String> m = new SimpleHashMap<>();m.put("A", "C");m.put("B", "C++");m.put("C", "Java");m.put("D", "C#");m.put("E", "Go");m.put("E", "Swift");System.out.println(m);System.out.println("-----------------");System.out.println(m.get("A"));System.out.println("-----------------");System.out.println(m.entrySet());}/*Output:{B=C++, A=C, C=Java, D=C#, E=Swift}-----------------C-----------------[B=C++, A=C, C=Java, D=C#, E=Swift]*/
}

如何设计hashCode?

在明白了如何散列之后,如何编写自己的hashCode()就有十分重要的意义。设计hashCode()时最重要的因数是:如何何时,对同一个对象调用hashCode()都应该生成同样的值。如果在将一个对象用put()添加进去HashMap时产生一个hashCode()值,而用get()取出时却产生了另一个hashCode()值,那就就无法重现取得该对象了。所以,如何你的hashCode()方法依赖于对象中易变的数据,就要当心了,因为此数据发送变化时,hashCode()就会产生一个不同的散列码,相当于产生了一个不同的键。

此外,也不应该使用hashCode()依赖于具有唯一性的对象信息,尤其是使用this的值,因为这样做无法生存一个新的键,使之与put()中原始的键值对中的键相同。默认Object的hashCode()的值映射的就是对象的地址。所以我们应该使用对象内有意义的识别信息

以String为例,String有个特点只要字符串一样,那么不管多少个String对象,它们的hashCode的值都是一样的:

        String s1=new String("Hello");String s2=new String("Hello");String s3=new String("Hello");System.out.println(s1.hashCode()+" "+s2.hashCode()+" "+s3.hashCode());

输出的结果都为69609650,很明显String的hashCode的值是基于String内容的,下面是String类重写hashCode()的代码:

 public int hashCode() {int h = hash;if (h == 0 && value.length > 0) {char val[] = value;for (int i = 0; i < value.length; i++) {h = 31 * h + val[i];}hash = h;}return h;}

我们可以看出String的hashCode值就是基于每个字符产生的。

因此,想要使hashCode()实用,它必须速度快,并且必须有意义。也就是说,它必须基于对象的内容生成散列码。散列码不必是对一无二的(应该更关注生成速度,而不是唯一性),但是通过hashCode()和equals(),必须能够完全确定对象的身份。散列码的生成范围并不重要,只要int即可。好的hashCode()应该产生分布均匀的散列码。如果散列码都集中在一块,那么HashMap或者HashSet在某些区域的负载会很重,这样就不如分布均匀的散列函数快

在Effective Java Programming Language Guide这本书中,为怎样一份像样的hashCode()给出了基本的指导:

  1. 给int变量result赋予某个非零常量值,例如17
  2. 为对象内每个有意义的域f(即每个可以做equals()操作的域)计算出一个int散列吗hashCode:

    如果是数组类型则对每个元素应用上述规则
    
  3. 合并计算得到散列码
    result=37*result+hashCode;
  4. 返回result。

下面便是遵循这些指导的一个例子:

public class Animal {private String name;private int age;private char gender;public Animal(String name, int age, char gender) {this.name = name;this.age = age;this.gender = gender;}@Overridepublic String toString() {return "Animal{" +"name='" + name + '\'' +", age=" + age +", gender=" + gender +", hashCode=" + hashCode() +'}'+"\n";}@Overridepublic int hashCode() {int result = 17;result = 37 * result + name.hashCode();result = 37 * result + age;result = 37 * result + (int) gender;return result;}@Overridepublic boolean equals(Object o) {if (!(o instanceof Animal)) return false;Animal animal = (Animal) o;if (age != animal.age) return false;if (gender != animal.gender) return false;return name.equals(animal.name);}public static void main(String[] args) {Map<Animal, Integer> map = new HashMap<Animal, Integer>();for (int i = 0; i < 5; i++) {Animal animal = new Animal("Jack", i, '男');map.put(animal, i);}System.out.println(map);}/*Output:{Animal{name='Jack', age=4, gender=男, hashCode=-1144106977}=4, Animal{name='Jack', age=3, gender=男, hashCode=-1144107014}=3, Animal{name='Jack', age=0, gender=男, hashCode=-1144107125}=0, Animal{name='Jack', age=2, gender=男, hashCode=-1144107051}=2, Animal{name='Jack', age=1, gender=男, hashCode=-1144107088}=1}*/
}

上面的例子中hashCode和equals()都基于Animal中三个字段来生成结果的,只要发们其中一个发生改变,就会产生不同的结果。

HashMap的性能因子

我们可以通过手工调整HashMap来提高性能,从而满足我们特点应用的需求。为了调整HashMap时让你理解性能问题,需求了解以下的术语:

  • 容量:表中的桶的数量(数组的长度)
  • 初始容量:表在创建时所拥有的的桶位数。HashMap和HashSet创建时都可以指定初始容量。
  • 尺寸:表中当前存储的元素数量。
  • 负载因子:尺寸/容量。

空表的负载因子是0,而半满表的负载因子是0.5以此类推。负载轻(负载因子小)的表产生冲突的可能性小,因此对于插入和查找都是最理想的(但是会减慢使用迭代器进行遍历的过程)。HashMap和HashSet都具有指定负载因子的构造器:

 public HashMap(int capacity, float loadFactor) {this(capacity);if (loadFactor <= 0 || Float.isNaN(loadFactor)) {throw new IllegalArgumentException("Load factor: " + loadFactor);}/** Note that this implementation ignores loadFactor; it always uses* a load factor of 3/4. This simplifies the code and generally* improves performance.*/}

表示当负载情况达到该负载因子的水平时,容器将自动增加其容量(桶位数),HashMap中的实现方式是使容量(数组大小)变成之前的2倍,并重新将现有的对象分布到新的桶位中(这被称为再散列)

HashMap使用默认的负载因子是0.75,只有当表达到四分之三满时,才进行散列:

   static final float DEFAULT_LOAD_FACTOR = .75F;

这个因子在时间和空间代价之间达到了平衡。更高的负载因子可以降低表(数组)所需的空间,但是会增加查找代价,而查找操作是我们用得最多的操作(包括put()和put())。而更低的负载因子就会提高表(数组)所需的空间,造成空间浪费,迭代速度变慢。所以负载因子应该设置成合理的值。

参考

《Thinking in Java》

java集合大家族之Map相关推荐

  1. java集合课程,I学霸官方免费课程三十三:Java集合框架之Map集合

    I学霸官方免费教程三十三:Java集合框架之Map集合 Map接口 Map集合采用键值对(key-value)的方式存储数据,其中键不可以重复.值可以重复. 常用类有HashMap.TreeMap和P ...

  2. java 集合之Interface Map<K,V> HashMap实现类

    java 集合之Interface Map<K,V> 类型参数:K- 此映射所维护的键的类型:V- 映射值的类型 将键映射到值的对象. 一个映射不能包含重复的键: 每个键最多可以映射到一个 ...

  3. Java集合框架中Map接口的使用

    在我们常用的Java集合框架接口中,除了前面说过的Collection接口以及他的根接口List接口和Set接口的使用,Map接口也是一个经常使用的接口,和Collection接口不同,Map接口并不 ...

  4. java集合框架07——Map架构与源代码分析

    前几节我们对Collection以及Collection中的List部分进行了分析,Collection中还有个Set,因为Set是基于Map实现的,所以这里我们先分析Map,后面章节再继续学习Set ...

  5. (Java集合框架)Map集合

    Map集合 概述 现实生活中,常会看到这样的一种集合:IP地址与主机名,身份证号与个人,系统用户名与系统用户对象等,这种一一对应的关系,就叫做映射.Java提供了专门的集合类用来存放这种对象关系的对象 ...

  6. Java集合篇:Map总结

    相关阅读: Java基础篇:hashCode的作用 Java集合篇:HashMap原理详解(JDK1.7及之前的版本) Java集合篇:HashMap原理详解(JDK1.8) Java集合篇:Hash ...

  7. java 先进先出的map_「 深入浅出 」java集合Collection和Map

    本系列文章主要对java集合的框架进行一个深入浅出的介绍,使大家对java集合有个深入的理解. 本篇文章主要具体介绍了Collection接口,Map接口以及Collection接口的三个子接口Set ...

  8. Java集合查找Map,Java集合框架中Map接口的使用

    在我们常用的Java集合框架接口中,除了前面说过的Collection接口以及他的根接口List接口和Set接口的使用,Map接口也是一个经常使用的接口,和Collection接口不同,Map接口并不 ...

  9. list和map用法java,java 集合list和map的用法

    import java.util.ArrayList; import java.util.HashMap; import java.util.Iterator; import java.util.Li ...

  10. java集合(List,Set,Map)详细总结

    一,集合的由来: 数组是长度是固定的,当添加的元素超过数组的长度时需要对数组重新定义,太麻烦了,java内部给我们提供了集合类,能存储任意对象,长度是可以改变的,随着元素的增加而增加,随着元素的减少而 ...

最新文章

  1. 华中农大津田賢一组招植物微生物组、生物信息方向博士后
  2. OGG 同步报错 - TCP/IP error 111 (Connection refused)
  3. Android App自适应draw9patch不失真背景
  4. 2021-03-29 标准化函数
  5. c#web页面显示弹窗_C#中三种弹出信息窗口的方式
  6. 费解 | 为什么很多程序员工作时都戴耳机?
  7. C# 9.0中引入的新特性init和record的使用思考
  8. Linux管道函数使用
  9. Gbase相关学习总结
  10. php加密解密函数,PHP加密解密的函数
  11. 不随意付钱,不随意签字。
  12. Cesium:实现漫游飞行
  13. 阿里集团公布2022“研究型实习生”计划
  14. 稳定域的画法 matlab,基于MATLAB的铣削加工颤振稳定域仿真算法及实现
  15. Freescale mx27 DDR 初始化代码分析
  16. 【资源】同济线性代数教材(第五版)
  17. [资料] GIS资料汇总(GIS+Envi+erdas视频教程)
  18. Selenium +Python项目实践(注册流程)
  19. shell脚本大全-个人收藏
  20. python爬虫爬天眼查失信守信企业的名称数据。

热门文章

  1. 数据挖掘概念与技术(原书第三版)范明 孟小峰译-----第三章课后习题答案
  2. Ubuntu 20.10 wine、微信、QQ安装教程
  3. bulk insert java_从JAVA插入SQL Server时,我可以获得“BULK INSERT”般的速度吗?
  4. Android LED电子表时钟字体digital font
  5. 汽车防撞之FMCW(调频连续波)原理
  6. php防撞库,基于单片机的倒车防撞预警系统设计和实现
  7. 3步解决网盘限速,还能远程下载!
  8. Drool7s kmodule的作用--系列02课
  9. 在Anylogic建立自己的智能体
  10. 用一套键鼠控制两台主机