1、Map接口简介
集合根据数据存储的不同分为两种形式:单值集合、二元偶对象集合,在之前所使用的Collection都属于单值集合。
现在所学习的Map属于二元偶对象集合,所谓的二元偶对象指的是存储的数据为“key=value”结构对,在使用的时候可以根据key查询出相应的value的内容。
所以Collection和Map存储数据的目的分别为:Collection是为了数据的输出而存储,而Map是为了数据的查询而存储。
java.util.Map是进行二元偶对象数据存储的最大父接口,在里面所有存放的内容会按照“key=value”的形式进行保存,所以在数据存放的时候就需要保存有两个内容,Map接口的常用方法如下:
V put(K key, V value)
将指定的值与此映射中的指定键关联(向集合中保存数据,如果key存在则发生替换,
同时返回旧的内容,如果key不存在返回的内容为空) (非常重要)
V get(Object key)
返回指定键所映射的值;如果此映射不包含该键的映射关系,则返回 null。 (非常重要)
V remove(Object key)
如果存在一个键的映射关系,则将其从此映射中移除
int size()
返回此映射中的键-值映射关系数(获取集合长度)
Collection<V> values()
返回此映射中包含的值的 Collection 视图 (返回所有内容)
Set<K> keySet()
返回此映射中包含的键的 Set 视图 (获取所有的key,key不可重复)
(key一旦重复,会使用新的内容去替换旧的内容)
Set<Map.Entry<K,V>> entrySet()
返回此映射中包含的映射关系的 Set 视图 (将所有的内容以Map.Entry集合的形式返回) (非常重要)
在JDK1.9之后在Map接口之中提供有许多的of)方法,利用此方法可以方便的创建一个key不重复的Map集合
**例:**创建Map集合
public class Demo05 {
public static void main(String[] args) {
Map<String,Integer> map =Map.of("one",1,"two",2,"three",3);
System.out.println(map);
}
}
程序执行结果:{two=2, three=3, one=1}
通过结果可以发现,此时的程序会按照“key=value”的形式进行保存,同时此时的存储是无序的(Map的功能是进行查询,是否有序意义不大),但是使用此种方式创建的Map集合,如果出现有key重复的问题,那么程序就会出现如下的异常:
Exception in thread"main"java.lang.Illegal ArgumentException:kluplicate key:
因为key作为Map操作的核心控制点,所以这个内容的重复实际上对于整个的Map而言就需要更新,如果想要正确的使用Map接口,那么就必须使用它的几个子类,常见的子类:HashMap、LinkedHashMap、TreeMap、HashTable。
2、HashMap
HashMap是Map接口中最为常见的一个子类,也是主要使用的一个子类,此类通过名称就可以发现,采用Hash的方式进行存储,所以其存储的时候都是无序的,此类的定义结构如下:
public class HashMap<K,V> extends AbstractMap<K,V>
implements Map<K,V>, Cloneable, Serializable
例:使用HashMap进行数据存储
public class Demo05 {
public static void main(String[] args) {
Map<String, String> map = new HashMap<>();
System.out.println("【未发生替换】"+map.put("hello","you"));
System.out.println("【已发生替换】"+map.put("hello","guys"));
System.out.println("【未发生替换】"+map.put("wow","gays"));
map.put("empty",null);
map.put(null,"empty");
System.out.println(map);
}
}
程序执行结果:
【未发生替换】null (由于没有相同的key,所以返回内容是null)
【已发生替换】you (返回旧的数据)
【未发生替换】null (由于没有相同的key,所以返回内容是null)
{null=empty, hello=guys, wow=gays, empty=null}
通过以上的存储可以发现,Map中的key绝对是唯一的标记,不可能重复,同时在Map集合里面也可以实现null的内容存储。 HashMap允许key 和 value 同时为空
使用Map集合的意义在于需要根据key进行内容的查找。
**例:**根据key查找数据
public class Demo05 {
public static void main(String[] args) {
Map<String, String> map = new HashMap<>();
map.put("hello","you");
map.put("q","you");
map.put("w","you");
map.put("e","you gays");
System.out.println(map.get("e"));
System.out.println(map.get("everyone"));
}
}
程序执行结果:
you gays (设置的key为e 可以正常查询)
null (由于key不存在返回的结果就是null)
如果只是进行方法的使用研究那么对于HashMap而言意义不大,因为必须要关注HashMap的源代码实现机制。(JDK1.8之后HashMap算法进行了重大的变更)。
分析HashMap源码实现机制
1、无参构造
public HashMap() {
this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted
}
static final float DEFAULT_LOAD_FACTOR = 0.75f; (默认的扩充阀值为“75%”)
final float loadFactor;
2、put()方法
public V put(K key, V value) {
return putVal(hash(key), key, value, false, true);
}
3、putVal()方法 实现了节点的相关创建以及扩容的调用(resize())
4、resize()
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16 (默认容量大小为16个)
transient Node<K,V>[] table; (默认根据数组形式存储)
static final int MAXIMUM_CAPACITY = 1 << 30; (最大存储30位)
oldCap<<1(每次扩容1倍)
5、性能保证 static final int TREEIFY_THRESHOLD=8; (树状阈值)
if (binCount>=TREEIFY THRESHOLD-1)
treeifyBin(tab,hash); //进行树状结构转换
break;
static final int MIN_TREEIFY_CAPACITY = 64;
final void treeifyBin(Node<K,V>[] tab, int hash) {
int n, index; Node<K,V> e;
//这里还有一个限制条件,当table的长度小于MIN_TREEIFY_CAPACITY(64)时,只是进行扩容
if (tab == null || (n = tab.length) < MIN_TREEIFY_CAPACITY)
resize(); //而扩容的时候,链表的长度有可能会变短
else if ((e = tab[index = (n - 1) & hash]) != null) {
TreeNode<K,V> hd = null, tl = null;
do {
//将链表中的结点转换为树结点,形成一个新链表
TreeNode<K,V> p = replacementTreeNode(e, null);
if (tl == null)
hd = p;
else {
p.prev = tl;
tl.next = p;
}
tl = p;
} while ((e = e.next) != null);
if ((tab[index] = hd) != null) //将新的树结点链表赋给第index个桶
hd.treeify(tab); //执行 TreeNode中的treeify()方法
}
}
TreeNode<K,V> replacementTreeNode(Node<K,V> p, Node<K,V> next) {
return new TreeNode<>(p.hash, p.key, p.value, next);
}
对于HashMap来讲,如果要进行扩容,则表示当前的存储容量达到“75%”的时候才会选择扩容。在JDK1.8之后如果),链表长度大于8而且整个map中的键值对大于等于MIN_TREEIFY_CAPACITY (64)时,才进行链表到红黑树的转换为了保证数据的查询性能,HashMap会将原始的链表存放结构转为红黑树结构进行保存,利用红黑树中自旋的处理实现树的平衡修复。
先看一下数组链表和哈希表的区别
再参考这里HashMap更详细的源码讲解
最后再看HashMap数据结构和存储过程详解看这里
3、LinkedHashMap
HashMap之中进行数据存储的时候并不会进行顺序的定义,所以如果现在要想实现顺序的存储,就可以利用LinkedHashMap子类来完成,这个类的定义如下:
public class LinkedHashMap <K,V> extends HashMap <K,V>
implements Map<K,V>
**例:**观察LinkedHashMap实现
public class Demo06 {
public static void main(String[] args) {
Map<String, String> map = new LinkedHashMap<>();
map.put("hello","you");
map.put("q","you");
map.put("w","you");
map.put("e","you gays");
System.out.println(map);
}
}
程序执行结果:
{hello=you, q=you, w=you, e=you gays}
4、TreeMap
java.util.TreeMap实现的是一个排序的树结构,可以依据key的自然顺序实现排序的处理,此类的定义如下:
public class TreeMap<K,V>extends AbstractMap<K,V>
implements NavigableMap<K,V>, Cloneable, Serializable
例:使用TreeMap进行排序
public class Demo06 {
public static void main(String[] args) {
Map<String, String> map = new TreeMap<>();
map.put("hello","you");
map.put("q","you");
map.put("empty",null);
// map.put(null,"empty");
System.out.println(map);
}
}
程序执行结果:
{empty=null, hello=you, q=you}
既然此时是要进行key数据的排序,那么在使用的过程之中key的数据内容就绝对不能为null
5、Hashtable
类集中有三老(出现时间很古老,让人聊起来觉得很显老,使用起来觉得资格很老),Vectory、Enumeration、Hashtable常见的三老,Hashtable是在JDK1.0的时候提出的集合操作最早偶对象存储。
Hashtable定义如下:
public class Hashtable<K,V>extends Dictionary<K,V>
implements Map<K,V>, Cloneable, Serializable
Dictionary是最早实现字典存储结构的父类。
例:使用Hashtable
public class Demo06 {
public static void main(String[] args) {
Map<String, String> map = new Hashtable<>();
map.put("hello","you");
map.put("q","you");
// map.put("empty",null);
// map.put(null,null); //key value 都为空
System.out.println(map);
}
}
程序执行结果:{hello=you, q=you}
Hashtable在进行数据存储的时候是不允许存放null数据的,不管是key还是value,如果发现有null,那么最终都会出现“NullPointerException”。
而相比较HashMap不管是在key还是在value上都没有关于null的限制。
面试题:请解释HashMap与Hashtable的区别?
- HashMap在进行存储的时候默认的大小为16,在桶的容量达到8位之后为了保证数据的查询性能使用红黑树进行存储;HashMap中的全部方法都使用异步处理,属于非线程的安全操作; HashMap存储的key和value都允许为null。
- Hashtable进行存储时默认的大小为11;Hashtable中的方法使用同步处理,属于线程安全的操作。Hashtable存储的key和value都不允许为null。
关于红黑树 推荐看这篇博文 漫画讲解红黑树
6、Map.Entry
通过一系列的分析之后实际上就可以得出整个Map接口的基本使用情况,但是对于数据的存储必须进行详细说明,Map集合与Collection集合的最大不同在于,它所存储的数据是二元偶对象。
Collection:
Map:
在Map里面由于需要存放有两个内容,很明显为了可以进行整体的处理方便,就在Map接口里面定义有一个Map.Entry的内部接口,此接口主要是进行key和value封装的,而且在Map接口里面也可以发现Map.Entry的子类
static class Node<K,V> implements Map. Entry<K,V>{
final int hash;
final K key;
V value;
Node<K,V>next;
}
Map.Entry 里面可以包装key和value,那么也可以通过Map.Entry获取对应的key和value此接口定义如下:
public static interface Map.Entry<K,V>
关键方法
K getKey()
返回与此项对应的键。
V getValue()
返回与此项对应的值。
例:创建Map.Entry对象
·JDK1.9之后增加的方法:static <K,V> Map.Entry <K,V> entry(K k,V v)
public class Demo06 {
public static void main(String[] args) {
Map.Entry<String,String> entry = Map.entry("hello","guys");
System.out.println("key:"+entry.getKey()+" value:"+entry.getValue());
}
}
程序执行结果:
key:hello value:guys
Map.Entry实质上定义了一个Map偶对象的存储标准,所有的Map接口的子类都依据此标准实现相应的节点数据的存储,可以直接利用此实例实现key与value的分离。
7、Iterator输出Map集合
面对于集合数据的输出,肯定要考虑使用Iterator接口完成,但是在Map接口中并没有任何一个方法可以直接获取到Iterator接口实例,所以此时就必须经过一系列的转换得来。
之所以在Map接口中没有提供有直接获取Iterator接口实例的方法,原因就在于它的存储不是一个普通的数据,而是一个偶对象,而Iterator每一次可以输出的全部都是单个实例为此基本的输出流程如下:
- 1、通过Map接口中的entrySet()方法,将Map实例转为Set接口实例;
Set<Map.Entry<K,V>> entrySet()
返回此映射中包含的映射关系的 Set 视图
-
2、获取了Set集合实例之后就可以调用iterator()方法获取Iterator接口实例,泛型类型为“Map.Entry<K,V>”;
-
3、通过Iterator进行迭代操作,获取每一组的“Map.Entry<K,V>”实例,进行key与value的分离。
**例:**通过entrySet()方法实现Iterator输出
public class Demo06 {
public static void main(String[] args) {
Map<String, String> all = new HashMap<>();
all.put("hello", "你好");
all.put("gyus", "伙计们");
all.put("happay", "非常开心见到你们");
Set<Map.Entry<String, String>> set = all.entrySet();
Iterator<Map.Entry<String,String>> it = set.iterator();
while (it.hasNext()){
Map.Entry<String,String> entry = it.next();
System.out.println("key="+entry.getKey()+" "+"value="+entry.getValue());
}
}
}
程序执行结果:
key=gyus value=伙计们
key=happay value=非常开心见到你们
key=hello value=你好
从JDK1.5之后Map集合也可以使用foreach进行输出,因为其内部实现了Iterator接口:
源代码:
final class EntryIterator extends HashIterator implements Iterator<Map. Entry<K,V>>{
public final Map. Entry<K,V> next(){
return nextNode();
}
}
例:使用foreach输出
public class Demo06 {
public static void main(String[] args) {
Map<String, String> all = new HashMap<>();
all.put("hello", "你好");
all.put("gyus", "伙计们");
all.put("happay", "非常开心见到你们");
for(Map.Entry<String,String> entry:all.entrySet()){
System.out.println("key="+entry.getKey()+" "+"value="+entry.getValue());
}
}
}
程序执行结果:
key=gyus value=伙计们
key=happay value=非常开心见到你们
key=hello value=你好
**例:**使用keySet()方法实现Iterator输出
public class Demo06 {
public static void main(String[] args) {
Map<String, String> all = new HashMap<>();
all.put("hello", "你好");
all.put("gyus", "伙计们");
all.put("happay", "非常开心见到你们");
Set<String> set = all.keySet();
Iterator<String> it = set.iterator();
while (it.hasNext()){
String key = it.next();
String value = all.get(key);
System.out.println("key="+key+" "+"value="+value);
}
}
}
程序执行结果:
key=gyus value=伙计们
key=happay value=非常开心见到你们
key=hello value=你好
不管是何种集合最终的输出的归宿只有一点就是通过Iterator,但是需要注意的是,Map一般很少直接输出,因为其功能主要是进行数据查询。
8、自定义key类型
对于此时的Map集合可以发现,设置的K和V两个泛型类型只要是引用数据类型就可以了,这也就包括了自定义的类,即自定义的类也可以成为Map中的key类型。
但是此时作为key类型所在的类一定要覆写hashCode()与equals()两个方法,因为牵扯到对象比较问题。
Map集合根据key获取数据时的流程:
1、利用hashCode()方法生成结果进行比较,因为这只是一个数字,它的比较速度会更加快;
2、如果发现哈希码相同的时候才会进行内容的比较。
class Member{
private String name;
private int age;
public Member(String name, int age) {
this.name = name;
this.age = age;
}
@Override
public String toString() {
return "姓名:"+this.name+" "+"年龄:"+this.age;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Member member = (Member) o;
return age == member.age &&
Objects.equals(name, member.name);
}
@Override
public int hashCode() {
return Objects.hash(name, age);
}
}
public class Demo07 {
public static void main(String[] args) {
Map<Member,String> map = new HashMap<>();
map.put(new Member("张三",10),new String("张三"));
System.out.println(map.get(new Member("张三",10)));
}
}
程序执行结果:
张三
在以后实际编写的代码过程之中,如果遇见了Map集合,则一般对于Map集合中的Key类型,最为常见的是String,其次是Long或者Integer。
哈希冲突(了解)
结论:
哈希码实际上是进行对象比较的关键所在,而在进行Map存储的时候实际上也是依靠哈希码得到的一个存储空间,但是在很多情况下依然有可能会出现哈希冲突的问题。
那么这个时候的解决方案(四种解决方案):
- 1、开放定址法
- 2、链地址法
- 3、再哈希法
- 4、建立公共溢出区
而Java是利用链地址法的形式解决的。把所有重复的内容放在一个链表之中进行保存。