Map接口
Map接口
1 概述
Map接口中都存储的是key(键)和value(值)。每一个key都是不重复的,值是可以重复的。每一个key只能对应一个value,称之为键值对
一个Map由多个键值对组成。
将每一个键值对都看作一个对象,抽取出一个代表键值对的类,Map.Entry
2 Map常用方法
public static void main(String[] args) {
// Map接口常用的方法
Map<String,String> map = new HashMap<>();
// 添加数据
map.put("郭德纲","于谦");
map.put("舒克","贝塔");
map.put("李晨","范冰冰");
map.put("贾乃亮","李小璐");
map.put("丁真","王源");
map.put("丁真","雪豹");
map.put("王宝强","马蓉");
map.put("pgone","李小璐");
// 清空映射
// map.clear();
// 判断是否包含某个key
System.out.println(map.containsKey("舒克"));
// 判断是否包含某个value
System.out.println(map.containsValue("王源"));
// 获取键值对的集合
Set<Map.Entry<String, String>> entries = map.entrySet();
System.out.println(entries);
// 快速遍历
map.forEach((k,v) -> System.out.println(k + v));
// 根据key获取value
String s = map.get("pgone");
System.out.println(s);
// 判断映射是否为空
System.out.println(map.isEmpty());
// 获取所有的key
Set<String> strings = map.keySet();
System.out.println(strings);
map.remove("贾乃亮");
// 键值对的个数
System.out.println(map.size());
// 获取所有value的集合
Collection<String> values = map.values();
System.out.println(values);
System.out.println(map);
}
课堂练习:统计字符串中字符出现的次数 abfgafaba
public static void test(){
// 统计字符串中字符出现的次数
String str = new Scanner(System.in).next();
// 创建Map
Map<Character,Integer> map = new HashMap<>();
for (int i = 0; i < str.length(); i++) {
// 获取字符
char c = str.charAt(i);
// 判断Map中是否有对应的字符的key
if (map.containsKey(c)){
// 把出现次数+1
Integer value = map.get(c);
value += 1;
map.put(c,value);
}else {
map.put(c,1);
}
}
map.forEach((k,v) -> System.out.println(k + "出现的次数是" + v));
}
3 HashMap类
HashMap是哈希表的一个实现,是无序的,key是唯一的。并且key和value可以是null,是线程不安全的,而且其中的数据的位置可能会发生改变。
底层的数据结构是数组+单向链表+红黑树
3.1 构造方法
// 无参构造 把默认的0.75赋值给加载因子 第一次添加数据的时候会创建长度为16的数组
HashMap()
// 指定初始容量 默认的加载因子0.75
HashMap(int initialCapacity)
// 可以指定加载因子和初始容量 初始容量一定是2的n次方 第一次添加数据的时候会创建长度为2的n次方的数组
HashMap(int initialCapacity, float loadFactor)
3.2 底层源码
- 成员变量
// 默认加载因子
float DEFAULT_LOAD_FACTOR = 0.75f;
// 加载因子
float loadFactor;
// 底层的数组
Node<K,V>[] table;
// 扩容的阈值
int threshold;
// 默认初始容量
int DEFAULT_INITIAL_CAPACITY = 1 << 4;
// 最小扭转红黑树的容量
int MIN_TREEIFY_CAPACITY = 64;
// 红黑树扭转链表
int UNTREEIFY_THRESHOLD = 6
// 链表扭转红黑树
int TREEIFY_THRESHOLD = 8
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
boolean evict) {
// tab 底层数组
Node<K,V>[] tab;
// 数组中的一个结点
Node<K,V> p;
// n 数组的长度
// i 索引
int n, i;
// 判断是否是首次添加数据
if ((tab = table) == null || (n = tab.length) == 0)
// 首次添加数据 resize方法中会创建数组
n = (tab = resize()).length;
// 计算数据要放的索引,然后判断索引处是否有内容 如果没有内容就放在索引处
if ((p = tab[i = (n - 1) & hash]) == 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))))
// key相同 用新值覆盖旧值
e = p;
else if (p instanceof TreeNode)// 判断是否是红黑树
e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
else {
// 循环的拿到链表上的每一个节点进行比较。
for (int binCount = 0; ; ++binCount) {
if ((e = p.next) == null) {
// 如果都不相同,节点放到链表的后面
p.next = newNode(hash, key, value, null);
// 判断链表的长度,如果>8 就扭转成红黑树
if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
treeifyBin(tab, hash);
break;
}
// key相同 用新值覆盖旧值
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
break;
p = e;
}
}
if (e != null) { // existing mapping for key
// key相同,用新值覆盖旧值,返回旧值
V oldValue = e.value;
if (!onlyIfAbsent || oldValue == null)
e.value = value;
afterNodeAccess(e);
return oldValue;
}
}
++modCount;
// 判断是否需要扩容
if (++size > threshold)
resize();
afterNodeInsertion(evict);
return null;
}
final Node<K,V>[] resize() {
Node<K,V>[] oldTab = table;
int oldCap = (oldTab == null) ? 0 : oldTab.length;
int oldThr = threshold;
int newCap, newThr = 0;
if (oldCap > 0) {
if (oldCap >= MAXIMUM_CAPACITY) {
threshold = Integer.MAX_VALUE;
return oldTab;
}
// 计算扩容后的新长度和新阈值
else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
oldCap >= DEFAULT_INITIAL_CAPACITY)
newThr = oldThr << 1; // double threshold
}
else if (oldThr > 0) // initial capacity was placed in threshold
newCap = oldThr;
else { // zero initial threshold signifies using defaults
// 默认的初始容量和阈值
newCap = DEFAULT_INITIAL_CAPACITY;
newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
}
if (newThr == 0) {
float ft = (float)newCap * loadFactor;
newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
(int)ft : Integer.MAX_VALUE);
}
threshold = newThr;
@SuppressWarnings({"rawtypes","unchecked"})
// 创建底层的数组
Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
table = newTab;
// 把数据从旧数组移动到新数组上
if (oldTab != null) {
for (int j = 0; j < oldCap; ++j) {
Node<K,V> e;
if ((e = oldTab[j]) != null) {
oldTab[j] = null;
if (e.next == null)
newTab[e.hash & (newCap - 1)] = e;
else if (e instanceof TreeNode)
((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
else { // preserve order
Node<K,V> loHead = null, loTail = null;
Node<K,V> hiHead = null, hiTail = null;
Node<K,V> next;
do {
next = e.next;
if ((e.hash & oldCap) == 0) {
if (loTail == null)
loHead = e;
else
loTail.next = e;
loTail = e;
}
else {
if (hiTail == null)
hiHead = e;
else
hiTail.next = e;
hiTail = e;
}
} while ((e = next) != null);
if (loTail != null) {
loTail.next = null;
newTab[j] = loHead;
}
if (hiTail != null) {
hiTail.next = null;
newTab[j + oldCap] = hiHead;
}
}
}
}
}
return newTab;
}
4 Hashtable
Hashtable的key和value不能是null,是同步线程安全的映射。
默认的初始容量是11,默认的加载因子是0.75
指定的初始容量给多少就是多少
扩容是容量扩大一倍再加1
| 实现类 | 底层数据结构 | key/value能否为null | 线程是否安全 |
|---|---|---|---|
| HashMap | 数组+单向链表+红黑树 | 都可以 | 否 |
| LinkedHashMap | 数组+双向链表+红黑树 | 都可以 | 否 |
| TreeMap | 红黑树 | key不能为null,value可以是null | 否 |
| Hashtable | 哈希表 | 都不能为null | 同步线程安全 |
| ConcurrentHashMap | 哈希表 | 都不能为null | 异步线程安全 |
总结:
- 当创建HashMap时,会把加载因子赋值为0.75
- 当第一次put时,会创建一个长度为16的Node类型的数组,默认的阈值是12,然后根据计算把数组中的某一个索引处添加一个Node对象
- 如果key相同,hash值肯定相同,会把旧值替换成新值,并且返回旧值
- 如果key不相同,但是hash相同,会对对应的桶上的位置进行一个个的逐位比较,直到链表结点的next是null,然后连接在这个链表的最后一个。如果链表长度大于8,并且数组长度大于等于64,这时会将链表转换成红黑树,从而提高查询效率
- 如果链表长度大于8,数组长度小于64,会进行一次扩容。数组长度翻倍,并且重新进行进行高低位分配
- 初始容量一定是2的n次方,保证(n - 1) & hash 的结果是 0到数组长度-1的范围内
- 扩容时容量翻倍,扩容后结点要么放在原来位置,要么放在原来位置+原来的数组长度处
- 扩容后如果桶上是一个红黑树,经过运算发现桶上的长度小于等于6,会把红黑树转换成链表
注意:使用HashMap时的key一般不要使用自定义类型。如果要使用,建议重写hashCode和equals方法

浙公网安备 33010602011771号