一、Map
Java为我们提供了两大类可用的集合,一类是单列集合(Collection),另一类就是双列集合(Map)。Map接口是将映射到值的对象。一个映射不能包含重复的键,每个键最多只能映射一个值。为什么在学习Set集合之前,要先学习Map集合呢,因为Set集合的底层实现,很多都是借助Map集合来实现的。
(1)Map集合中常用的方法
方法名 | 描述 |
clear() | 从此映射中移除所有映射关系 |
containsKey(Object key) | 如果此映射包含指定键的映射关系,则返回true |
containsValue(Object value) | 如果此映射将一个或多个键映射到指定值,则返回true |
entrySet() | 返回此映射中包含的映射关系的Set视图 |
equals(Object o) | 比较指定的对象与此映射是否相等 |
get(Object key) | 返回指定键所映射的值;如果此映射不包含该键的映射关系,则返回null。 |
hashCode() | 返回此映射的哈希码值 |
isEmpty() | 如果此映射包含键-值映射关系,则返回true |
keySet() | 返回此映射中包含的键的Set视图 |
put(K key, V value) | 将指定的值与此映射中的指定键关联 |
putAll(Map<? extends K,? extends V> m) | 从指定映射中将所有映射关系复制到此映射中 |
remove(Object key) | 如果存在一个键的映射关系,则将其从此映射中移除 |
size() | 返回此映射中的键-值关系的个数 |
双列集合 Map 接口有两个重要的实现类,HashMap 和 TreeMap
二、HashMap
(1)简介
HashMap实现原理:数组+链表(jdk1.7及之前),数组+链表+红黑树(jdk1.8)
- HashMap 是一个散列表,它存储的内容是键值对(key-value)映射。
- HashMap 继承于AbstractMap,实现了Map、Cloneable、java.io.Serializable接口。
- HashMap 的实现不是同步的,这意味着它不是线程安全的。它的key、value都可以为null。此外,HashMap中的映射不是有序的。
- HashMap 的实例有两个参数影响其性能:“初始容量” 和 “加载因子”。容量 是哈希表中桶的数量,初始容量 只是哈希表在创建时的容量。加载因子 是哈希表在其容量自动增加之前可以达到多满的一种尺度。当哈希表中的条目数超出了加载因子与当前容量的乘积时,则要对该哈希表进行 rehash 操作(即重建内部数据结构),从而哈希表将具有大约两倍的桶数。
- 通常,默认加载因子是 0.75, 这是在时间和空间成本上寻求一种折衷。加载因子过高虽然减少了空间开销,但同时也增加了查询成本(在大多数 HashMap 类的操作中,包括 get 和 put 操作,都反映了这一点)。在设置初始容量时应该考虑到映射中所需的条目数及其加载因子,以便最大限度地减少 rehash 操作次数。如果初始容量大于最大条目数除以加载因子,则不会发生 rehash 操作。
(2)HashMap的构造函数
HashMap共有4个构造函数,如下:
构造方法 | 描述 |
HashMap() | 默认构造函数。 |
HashMap(int capacity) | 指定“容量大小”的构造函数 |
HashMap(int capacity, float loadFactor) | 指定“容量大小”和“加载因子”的构造函数 |
HashMap(Map<? extends K, ? extends V> map) | 包含“子Map”的构造函数 |
(3)HashMap的使用
package basis.MapStu;
import java.util.HashMap;
import java.util.Map;
public class TestHashMap {
public static void main(String[] args) {
//创建一个HashMap集合
HashMap<String,String> hashMap = new HashMap();
//添加映射关系,如果键重复,则把已存在的映射关系覆盖
hashMap.put("001","张三");
hashMap.put("002","李四");
hashMap.put("003","王五");
hashMap.put("003","赵六");
//映射关系的个数
System.out.println(hashMap.size());//3
//是否包含指定键
System.out.println(hashMap.containsKey("001"));//true
//是否存在一个或多个键映射到该值
System.out.println(hashMap.containsValue("王五"));//false
//比较两个映射对象是否相等
HashMap<String,String> hashMap1 = new HashMap<>();
hashMap1.put("004","冰冰");
System.out.println(hashMap.equals(hashMap1));//false
//查找指定键所映射的值,如果不包含该键的映射关系,返回null
System.out.println(hashMap.get("003"));//赵六
//此映射的hashcode值
System.out.println(hashMap.hashCode());
//判断此映射是否为空
System.out.println(hashMap.isEmpty());//false
//将指定映射的所有映射关系添加到此映射
hashMap.putAll(hashMap1);
//删除指定键的映射关系,并返回该映射关系的值,如果不存在返回null
System.out.println(hashMap.remove("004"));
//遍历Map集合
for (Map.Entry e : hashMap.entrySet()){
System.out.println(e.getKey()+":"+e.getValue());
}
}
}
三、HashMap源码分析
(1)HashMap的继承关系
java.lang.Object
↳ java.util.AbstractMap<K, V>
↳ java.util.HashMap<K, V>
public class HashMap<K,V>
extends AbstractMap<K,V>
implements Map<K,V>, Cloneable, Serializable { }
小插曲:为什么 HashMap 继承了 AbstractMap 还要去实现 Map接口呢???(这样的写法在集合框架里非常多)
答案一:动态代理中会用到 Class 的 getInterface() ,此方法用于获取该类实现的接口,但不能直接获取父类实现的接口。但是动态代理出生在jdk1.3,而集合框架在jdk1.2中就有了。
答案二:有人声称询问过java集合框架的创始人Josh Bloch,Josh Bloch承认这是一个错误的写法。
类图
Map中的常量:
//HashMap的初始容量默认为16
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16
//最大容量为2的30次方
static final int MAXIMUM_CAPACITY = 1 << 30;
//默认加载因子 0.75
static final float DEFAULT_LOAD_FACTOR = 0.75f;
//使用红黑树结构时,链表长度阙值为8
static final int TREEIFY_THRESHOLD = 8;
//改回链表时,链表长度阙值为6
static final int UNTREEIFY_THRESHOLD = 6;
//使用红黑树结构时,集合最小容量为64
static final int MIN_TREEIFY_CAPACITY = 64;
内部接口Entry:
Entry是 Map 接口内部的一个用于存放、获取和设置映射关系的键和值的接口
interface Entry<K,V> {
K getKey();
V getValue();
V setValue(V value);
boolean equals(Object o);
int hashCode();
public static <K extends Comparable<? super K>, V> Comparator<Map.Entry<K,V>> comparingByKey() {
return (Comparator<Map.Entry<K, V>> & Serializable)
(c1, c2) -> c1.getKey().compareTo(c2.getKey());
}
public static <K, V extends Comparable<? super V>> Comparator<Map.Entry<K,V>> comparingByValue() {
return (Comparator<Map.Entry<K, V>> & Serializable)
(c1, c2) -> c1.getValue().compareTo(c2.getValue());
}
public static <K, V> Comparator<Map.Entry<K, V>> comparingByKey(Comparator<? super K> cmp) {
Objects.requireNonNull(cmp);
return (Comparator<Map.Entry<K, V>> & Serializable)
(c1, c2) -> cmp.compare(c1.getKey(), c2.getKey());
}
public static <K, V> Comparator<Map.Entry<K, V>> comparingByValue(Comparator<? super V> cmp) {
Objects.requireNonNull(cmp);
return (Comparator<Map.Entry<K, V>> & Serializable)
(c1, c2) -> cmp.compare(c1.getValue(), c2.getValue());
}
}
内部类Node:
Node结点是 HashMap 中用于存放映射关系的内部类,实现了Map接口中的 Entry 接口,即实现getKey(), getValue(), setValue(V value), equals(Object o), hashCode()这些函数。这些都是基本的读取/修改key、value值的函数。
static class Node<K,V> implements Map.Entry<K,V> {
final int hash;
final K key;
V value;
Node<K,V> next;
Node(int hash, K key, V value, Node<K,V> next) {
this.hash = hash;
this.key = key;
this.value = value;
this.next = next;
}
public final K getKey() { return key; }
public final V getValue() { return value; }
public final String toString() { return key + "=" + value; }
public final int hashCode() {
return Objects.hashCode(key) ^ Objects.hashCode(value);
}
public final V setValue(V newValue) {
V oldValue = value;
value = newValue;
return oldValue;
}
public final boolean equals(Object o) {
if (o == this)
return true;
if (o instanceof Map.Entry) {
Map.Entry<?,?> e = (Map.Entry<?,?>)o;
if (Objects.equals(key, e.getKey()) &&
Objects.equals(value, e.getValue()))
return true;
}
return false;
}
}
put() 方法:
前面提到过,put()方法用于向映射中添加指定的映射关系,在该方法中调用了 putVal() 方法,并将键的Hash值作为参数传进去。
public V put(K key, V value) {
return putVal(hash(key), key, value, false, true);
}
putVal() 方法:
原文:https://blog.csdn.net/li_cangqiong/article/details/81871332
/**
* Implements Map.put and related methods
*
* @param hash 根据静态方法hash获得的hash值
* @param key 键
* @param value 值
* @param onlyIfAbsent if true,当键相同时,不修改已存在的值
* @param evict if false, the table is in creation mode.
* @return previous value, or null if none
*/
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
boolean evict) {
//
Node<K,V>[] tab; Node<K,V> p; int n, i;
//定义一个临时链表数组,对hashmap中的table进行操作,n是table的长度,为hashmap初始化时定义的值,是2的幂,i为tab的游标
if ((tab = table) == null || (n = tab.length) == 0)//如果这个hashmap是空的
n = (tab = resize()).length;//就resize一下这个hashmap,获取容量
if ((p = tab[i = (n - 1) & hash]) == null)//这行代码的意思是获取tab链表数组中的第length个链表,如果链表为空,就创建新节点添加进去,它的nextnode为null
tab[i] = newNode(hash, key, value, null);
else {
Node<K,V> e; K k;
if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k))))//如果hash相同,键相同,就直接把p指向e
e = p;
else if (p instanceof TreeNode)//如果p节点类型为红黑树,就添加到红黑树中
e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
else {
for (int binCount = 0; ; ++binCount) {//链表数组中如果不出现hash碰撞,最完美的情况应该是每个链表都是单结点,这里统计链表中节点的个数
if ((e = p.next) == null) {
p.next = newNode(hash, key, value, null);//把新来的结点加入到这条链表的最后
if (binCount >= TREEIFY_THRESHOLD - 1) //如果链表中节点个数大于TREEIFY_THRESHOLD-1,也就是7,为什么是7呢,因为第一个节点不算进去,所以应该算是8个结点了
treeifyBin(tab, hash);//把这个链表转化成红黑树,然后直接退出循环
break;
}
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))//在结点遍历的过程中,如果有hash值相同的情况,且key值相同,就直接退出循环,把这个找到的结点直接赋值
break;
p = e;//每次都指向下一个结点
}
}
if (e != null) { // existing mapping for key
V oldValue = e.value;
if (!onlyIfAbsent || oldValue == null)//在onlyIfAbsent为false时,可以覆盖键值对,或者onlyIfAbesent为true但是value为空时也可以覆盖
e.value = value;
afterNodeAccess(e);
return oldValue;//返回的是e.value的指向
}
}
++modCount;//操作数+1
if (++size > threshold)//如果链表数组大小大于了阈值,就扩容
resize();
afterNodeInsertion(evict);
return null;
}
解释一下
if ((p = tab[i = (n - 1) & hash]) == null)
hash()方法: 用于计算键的hash值
static final int hash(Object key) {
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
hashcode(): hashcode是Object类的一个本地方法,获取键的hash码。
public native int hashCode();
获得的hashcode是一个32位的int型数据,很明显从2的-31次幂到2的32次幂长度的数据远超过hashmap的最大容量,所以这里通过key的hashcode右移16位与原hashcode进行异或运算,实际上就是hashcode与前16位换成0的hashcode进行或非运算,如果高位不参与运算,发生hash冲撞的几率就会增大,从而影响性能。
至于为什么用n-1,举个例子,假设n为16,n-1为15,也就是0000 0000 0000 0000 0000 0000 0000 1111,和hash值进行与运算,也就是这后四位进行运算,结果就被限制在0000~1111之间,正好是数组的长度,游标从0到15。
四、TreeMap
(1)简介
- TreeMap 是一个有序的key-value集合,它是通过红黑树实现的。
- TreeMap 继承于AbstractMap,所以它是一个Map,即一个key-value集合。
- TreeMap 实现了NavigableMap接口,意味着它支持一系列的导航方法。比如返回有序的key集合。
- TreeMap 实现了Cloneable接口,意味着它能被克隆。
- TreeMap 实现了java.io.Serializable接口,意味着它支持序列化。
- TreeMap基于红黑树(Red-Black tree)实现。该映射根据其键的自然顺序进行排序,或者根据创建映射时提供的 Comparator 进行排序,具体取决于使用的构造方法。
- TreeMap的基本操作 containsKey、get、put 和 remove 的时间复杂度是 log(n) 。
- TreeMap是非同步的。 它的iterator 方法返回的迭代器是fail-fastl的。
(2)构造方法
构造方法 | 描述 |
TreeMap() | 默认构造函数。 |
public TreeMap(Comparator<? super K> comparator) | 指定TreeMap的比较器 |
public TreeMap(Map<? extends K, ? extends V> m) | 包含“子Map”的构造函数 |
public TreeMap(SortedMap<K, ? extends V> m) | 创建一个TreeMap包含copyFrom |
(3)继承关系:
public class TreeMap<K,V>
extends AbstractMap<K,V>
implements NavigableMap<K,V>, Cloneable, java.io.Serializable
{}
(4)TreeMap类图