Map,HashMap,TreeMap

1. Map
Map接口中,键和值一一映射,可以通过键来获取值。
在数组中我们是通过数组下标来对其内容索引的,而在Map中我们通过对象来对对象进行索引,用来索引的对象叫做key,其对应的对象叫做value。这就是我们平时说的键值对。

  1. 给定一个键和一个值,你可以将该值存储在一个Map对象. 之后,你可以通过键来访问对应的值。
  2. 当访问的值不存在的时候,方法就会抛出一个NoSuchElementException异常.
  3. 当对象的类型和Map里元素类型不兼容的时候,就会抛出一个 ClassCastException异常。
  4. 当在不允许使用Null对象的Map中使用Null对象,会抛出一个NullPointerException 异常。
  5. 当尝试修改一个只读的Map时,会抛出一个UnsupportedOperationException异常。

Map接口的方法

序号 方法描述
1 void clear( )
 从此映射中移除所有映射关系(可选操作)。
2 boolean containsKey(Object k)
如果此映射包含指定键的映射关系,则返回 true。
3 boolean containsValue(Object v)
如果此映射将一个或多个键映射到指定值,则返回 true。
4 Set entrySet( )
返回此映射中包含的映射关系的 Set 视图。
5 boolean equals(Object obj)
比较指定的对象与此映射是否相等。
6 Object get(Object k)
返回指定键所映射的值;如果此映射不包含该键的映射关系,则返回 null。
7 int hashCode( )
返回此映射的哈希码值。
8 boolean isEmpty( )
如果此映射未包含键-值映射关系,则返回 true。
9 Set keySet( )
返回此映射中包含的键的 Set 视图。
10 Object put(Object k, Object v)
将指定的值与此映射中的指定键关联(可选操作)。
11 void putAll(Map m)
从指定映射中将所有映射关系复制到此映射中(可选操作)。
12 Object remove(Object k)
如果存在一个键的映射关系,则将其从此映射中移除(可选操作)。
13 int size( )
返回此映射中的键-值映射关系数。
14 Collection values( )
返回此映射中包含的值的 Collection 视图。

下面的关于HashMap的原文地址:https://baijiahao.baidu.com/s?id=1618550070727689060&wfr=spider&for=pc
2. HashMap

  1. HashMap是一个散列桶(数组和链表),它存储的内容是键值对(key-value)映射
  2. 最常用的Map,它根据键的HashCode 值存储数据,根据键可以直接获取它的值,
  3. HashMap最多只允许一条记录的键为Null(多条会覆盖);允许多条记录的值为 Null;允许键和值同时为空。
  4. 因为HashMap是非同步(非synchronized)的,所以具有很快的访问速度。
  5. HashMap采用了数组和链表的数据结构,能在查询和修改方便继承了数组的线性查找和链表的寻址修改
  6. key的hash值是先计算key的hashcode值,然后再进行计算,每次容量扩容会重新计算所以key的hash值,会消耗资源,要求key必须重写equals和hashcode方法
  7. 默认初始容量16,加载因子0.75,扩容为旧容量乘2,查找元素快,如果key一样则比较value,如果value不一样,则按照链表结构存储value,就是一个key后面有多个value;

HashMap的工作原理
HashMap是基于hashing的原理,我们使用put(key, value)存储对象到HashMap中,使用get(key)从HashMap中获取对象。当我们给put()方法传递键和值时,我们先对键调用hashCode()方法,计算并返回的hashCode是用于找到Map数组的bucket位置来储存Node 对象。这里关键点在于指出,HashMap是在bucket中储存键对象和值对象,作为Map.Node 。
在这里插入图片描述
a. 以下是HashMap初始化 ,简单模拟数据结构
Node[] table=new Node[16] 散列桶初始化,tableclass Node { hash;//hash值 key;//键 value;//值 node next;//用于指向链表的下一层(产生冲突,用拉链法)}

b.以下是具体的put过程(JDK1.8版)

  1. 对Key求Hash值,然后再计算下标

  2. 如果没有碰撞,直接放入桶中(碰撞的意思是计算得到的Hash值相同,需要放到同一个bucket中)

  3. 如果碰撞了,以链表的方式链接到后面

  4. 如果链表长度超过阀值( TREEIFY THRESHOLD==8),就把链表转成红黑树,链表长度低于6,就把红黑树转回链表

  5. 如果节点已经存在就替换旧值

  6. 如果桶满了(容量16*加载因子0.75),就需要 resize(扩容2倍后重排)

c. 以下是具体get过程(考虑特殊情况如果两个键的hashcode相同,你如何获取值对象?)
当我们调用get()方法,HashMap会使用键对象的hashcode找到bucket位置,找到bucket位置之后,会调用keys.equals()方法去找到链表中正确的节点,最终找到要找的值对象。

在这里插入图片描述
有什么方法可以减少碰撞?

  1. 扰动函数可以减少碰撞,原理是如果两个不相等的对象返回不同的hashcode的话,那么碰撞的几率就会小些,这就意味着存链表结构减小,这样取值的话就不会频繁调用equal方法,这样就能提高HashMap的性能。(扰动即Hash方法内部的算法实现,目的是让不同对象返回不同hashcode。)
  2. 使用不可变的、声明作final的对象,并且采用合适的equals()和hashCode()方法的话,将会减少碰撞的发生。不可变性使得能够缓存不同键的hashcode,这将提高整个获取对象的速度,使用String,Interger这样的wrapper类作为键是非常好的选择。为什么String, Interger这样的wrapper类适合作为键?因为String是final的,而且已经重写了equals()和hashCode()方法了。不可变性是必要的,因为为了要计算hashCode(),就要防止键值改变,如果键值在放入时和获取时返回不同的hashcode的话,那么就不能从HashMap中找到你想要的对象。

HashMap中hash函数怎么是是实现的?
我们可以看到在hashmap中要找到某个元素,需要根据key的hash值来求得对应数组中的位置。如何计算这个位置就是hash算法。前面说过hashmap的数据结构是数组和链表的结合,所以我们当然希望这个hashmap里面的元素位置尽量的分布均匀些,尽量使得每个位置上的元素数量只有一个,那么当我们用hash算法求得这个位置的时候,马上就可以知道对应位置的元素就是我们要的,而不用再去遍历链表。 所以我们首先想到的就是把hashcode对数组长度取模运算,这样一来,元素的分布相对来说是比较均匀的。但是,“模”运算的消耗还是比较大的,能不能找一种更快速,消耗更小的方式,我们来看看JDK1.8的源码是怎么做的(被楼主修饰了一下)

static final int hash(Object key)
 {
   if (key == null)
   { 
     return 0; 
   }
    int h;
    h=key.hashCode();
    return (n-1)&(h ^ (h >>> 16));
   }

返回散列值也就是hashcode // ^ :按位异或 // >>>:无符号右移,忽略符号位,空位都以0补齐 //其中n是数组的长度,即Map的数组部分初始化长度 return (n-1)&(h ^ (h >>> 16));}
在这里插入图片描述
简单来说就是

1、高16bt不变,低16bit和高16bit做了一个异或(得到的HASHCODE转化为32位的二进制,前16位和后16位低16bit和高16bit做了一个异或)

2、(n·1)&hash=->得到下标

如果HashMap的大小超过了负载因子(load factor)定义的容量,怎么办?

默认的负载因子大小为0.75,也就是说,当一个map填满了75%的bucket时候,和其它集合类(如ArrayList等)一样,将会创建原来HashMap大小的两倍的bucket数组,来重新调整map的大小,并将原来的对象放入新的bucket数组中。这个过程叫作rehashing,因为它调用hash方法找到新的bucket位置。这个值只可能在两个地方,一个是原下标的位置,另一种是在下标为<原下标+原容量>的位置

重新调整HashMap大小存在什么问题吗?

当重新调整HashMap大小的时候,确实存在条件竞争,因为如果两个线程都发现HashMap需要重新调整大小了,它们会同时试着调整大小。在调整大小的过程中,存储在链表中的元素的次序会反过来,因为移动到新的bucket位置的时候,HashMap并不会将元素放在链表的尾部,而是放在头部,这是为了避免尾部遍历(tail traversing)。如果条件竞争发生了,那么就死循环了。(多线程的环境下不使用HashMap)

为什么多线程会导致死循环,它是怎么发生的?
HashMap的容量是有限的。当经过多次元素插入,使得HashMap达到一定饱和度时,Key映射位置发生冲突的几率会逐渐提高。这时候,HashMap需要扩展它的长度,也就是进行Resize。1.扩容:创建一个新的Entry空数组,长度是原数组的2倍。2.ReHash:遍历原Entry数组,把所有的Entry重新Hash到新数组。

HashMap为什么是线程不安全的?
HashMap 在并发时可能出现的问题主要是两方面:

  1. put的时候导致的多线程数据不一致
    比如有两个线程A和B,首先A希望插入一个key-value对到HashMap中,首先计算记录所要落到的 hash桶的索引坐标,然后获取到该桶里面的链表头结点,此时线程A的时间片用完了,而此时线程B被调度得以执行,和线程A一样执行,只不过线程B成功将记录插到了桶里面,假设线程A插入的记录计算出来的 hash桶索引和线程B要插入的记录计算出来的 hash桶索引是一样的,那么当线程B成功插入之后,线程A再次被调度运行时,它依然持有过期的链表头但是它对此一无所知,以至于它认为它应该这样做,如此一来就覆盖了线程B插入的记录,这样线程B插入的记录就凭空消失了,造成了数据不一致的行为。
  2. resize而引起死循环
    这种情况发生在HashMap自动扩容时,当2个线程同时检测到元素个数超过 数组大小 × 负载因子。此时2个线程会在put()方法中调用了resize(),两个线程同时修改一个链表结构会产生一个循环链表(JDK1.7中,会出现resize前后元素顺序倒置的情况)。接下来再想通过get()获取某一个元素,就会出现死循环。

作者:小北觅
链接:https://www.jianshu.com/p/ee0de4c99f87

HashMap的三种遍历方法:

package HashMap_demo;

import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;
import java.util.Map.Entry;

public class HashMap_demo {
	public static void main(String[] args) {
		//Map<Integer, String> map=new HashMap<>();//这个和底下的一个写法都可以。
		Map<Integer, String> map=new HashMap<Integer, String>();
		map.put(1, "A");
		map.put(8, "D");
		map.put(3, "C");
		map.put(4, "D");
		map.put(3, "E");
		map.put(2, "");
		map.put(null, "C");
		//通过Map中的keySet()方法遍历获得key值进行输出
		//看别的大佬都管这叫增强for循环遍历
		System.out.println("通过Map中的keySet()方法遍历获得key值进行输出:");
		for(Integer s:map.keySet()) {
			System.out.print("Key="+s+":");
			System.out.print("value="+map.get(s)+"--");
		}
		
		//通过Map中的keySet()方法与valueSet()方法遍历获得key值和value值进行输出
		
		System.out.println("\n"+"通过Map中的keySet()方法与valueSet()方法遍历获得key值和value值进行输出");
		for(Integer s:map.keySet()) {
			System.out.print("Key="+s+":");
		}
		
		for(String v:map.values()) {
			System.out.print("value="+v+":");
		}
		
		//使用Iterator迭代器迭代输出
		System.out.println("\n"+"使用Iterator迭代器进行迭代输出:");
		Iterator<Entry<Integer, String>> iter=map.entrySet().iterator();
		while(iter.hasNext()) {
			Entry<Integer, String> entry=iter.next();
			System.out.print("Key="+entry.getKey()+":");
			System.out.print("value="+entry.getValue()+"--");
		}
	}

}

运行结果

通过Map中的keySet()方法遍历获得key值进行输出:
Key=null:value=C--Key=1:value=A--Key=2:value=--Key=3:value=E--Key=4:value=D--Key=8:value=D--
通过Map中的keySet()方法与valueSet()方法遍历获得key值和value值进行输出
Key=null:Key=1:Key=2:Key=3:Key=4:Key=8:value=C:value=A:value=:value=E:value=D:value=D:
使用Iterator迭代器进行迭代输出:
Key=null:value=C--Key=1:value=A--Key=2:value=--Key=3:value=E--Key=4:value=D--Key=8:value=D--

注意:hashMap的遍历输出结果应该是无序的,我这里不知道为啥,一直是有序的…

下段代码来自:https://www.cnblogs.com/biehongli/p/6443701.html 做了一点点小改动

package Map_demo;

import java.util.Collection;
import java.util.HashMap;
import java.util.Map;
import java.util.Set;

public class HashMap_demo {
	public static void main(String[] args) {
		// 1:key,value都是object类型的
		// 2:key必须是唯一的,不唯一,那么后面的value会把前面的value覆盖
		// 3:对于HashMap,key可以为空
		// 4:value可以为空,也可以为空
		// 5:HashTable的key和value不能为空
		// 6:properties的key和value必须为String类型的
		Map<String, String> map = new HashMap<>();
		map.put("null", "this is null 1");
		map.put("null", "this is null 2");
		map.put(null, "NO NUll");
		System.out.println(map.size());
		System.out.println(map.get("null"));
		System.out.println(map.get(null));

		System.out.println("=============================");
		// 循环显示map类型的key以及对应的value
		// 三个集合,key的集合,value的集合,键值对的集合
		Set<String> keys = map.keySet();
		for (String s : keys) {
			System.out.println(s);
		}
		System.out.println("=============================");
		Collection<String> values = map.values();// 值的集合
		System.out.println(values);
		System.out.println("=============================");
		Set<Map.Entry<String, String>> entrys = map.entrySet();// 键值对的集合
		for (Map.Entry<String, String> entry : entrys) {
			System.out.println(entry.getKey() + " " + entry.getValue());
		}

	}
}

输出结果:

2
this is null 2
NO NUll
=============================
null
null
=============================
[NO NUll, this is null 2]
=============================
null NO NUll
null this is null 2

总结:上段代码说明了

TreeMap:https://mp.csdn.net/mdeditor/98071869#

HashMap与TreeMap的区别

a.)Map是接口,HashMap与TreeMap是类(即HashMap和TreeMap实现了Map的所有的方法)
b.)HashMap非线程安全,TreeMap线程安全
c.)HashMap:适用于在Map中插入、删除和定位元素。
Treemap:适用于按自然顺序或自定义顺序遍历键(key)。

d.)HashMap通常比TreeMap快一点(树和哈希表的数据结构使然),建议多使用HashMap,在需要排序的Map时候才用TreeMap。
e.)HashMap的结果是没有排序的,而TreeMap输出的结果是排好序的。

HashMap和HashTable的区别
HashMap和Hashtable都实现了Map接口,但决定用哪一个之前先要弄清楚它们之间的分别。主要的区别有:线程安全性,同步(synchronization),以及速度。

HashMap几乎可以等价于Hashtable,除了HashMap是非synchronized的,并可以接受null(HashMap可以接受为null的键值(key)和值(value),而Hashtable则不行)。
HashMap是非synchronized,而Hashtable是synchronized,这意味着Hashtable是线程安全的,多个线程可以共享一个Hashtable;而如果没有正确的同步的话,多个线程是不能共享HashMap的。Java 5提供了ConcurrentHashMap,它是HashTable的替代,比HashTable的扩展性更好。
另一个区别是HashMap的迭代器(Iterator)是fail-fast迭代器,而Hashtable的enumerator迭代器不是fail-fast的。所以当有其它线程改变了HashMap的结构(增加或者移除元素),将会抛出ConcurrentModificationException,但迭代器本身的remove()方法移除元素则不会抛出ConcurrentModificationException异常。但这并不是一个一定发生的行为,要看JVM。这条同样也是Enumeration和Iterator的区别。
由于Hashtable是线程安全的也是synchronized,所以在单线程环境下它比HashMap要慢。如果你不需要同步,只需要单一线程,那么使用HashMap性能要好过Hashtable。
HashMap不能保证随着时间的推移Map中的元素次序是不变的。

作者:小北觅
链接:https://www.jianshu.com/p/ee0de4c99f87

ps:补充小知识
线程安全
在Java里,线程安全一般体现在两个方面:

1、多个thread对同一个java实例的访问(read和modify)不会相互干扰,它主要体现在关键字synchronized。如ArrayList和Vector,HashMap和Hashtable
(后者每个方法前都有synchronized关键字)。如果你在interator一个List对象时,其它线程remove一个element,问题就出现了。

2、每个线程都有自己的字段,而不会在多个线程之间共享。它主要体现在java.lang.ThreadLocal类,而没有Java关键字支持,如像static、transient那样。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值