一、Map映射容器:
1、介绍:Map(映射)是一个可以根据键值进行存储的,它的一个 Key 对应的是一个存储的位置,所以Key值是唯一的,根据Key值可以获取到对应的存储的Value。这种存储的集合我们称为 “键-值” Map。
注意:
1)它不是集合Collection的子类;
2) 它的键值是唯一的,根据键值可以取出值;重复添加相同的key,后面的会覆盖前面的。
3) 根据值无法直接取出Key。
2、Map的几种实现方式:
(1)HashMap 按照散列存储,这样的存取较快,线程不安全的,允许存放null键,null值
(2)Hashtable :作为古老的实现类,线程安全,速度慢,不允许存放null键,null值
(3)TreeMap 使用二叉树的算法来实现键值的自然排序功能:key需要实现比较器。
(4)LinkedHashMap
3、Map的遍历:需要先取出所有的key,然后遍历key:
public static void main(String[] args) {
Map<String, String> map = new HashMap<>();
map.put("语文", "95分");
map.put("数学", "80分");
Set<Entry<String, String>> set = map.entrySet();
Iterator iterator = set.iterator();
//map的遍历
while (iterator.hasNext()) {
Entry<String, String> entry = (Entry<String, String>) iterator.next();
System.out.println(entry.getKey() + entry.getValue());
}
}
二、HashMap详解:
1、介绍:
无序,即key值不排序;默认初始长度是16。并且每次自动扩展或是手动初始化时,长度必须是2的幂。HashMap通过 链式地址法 来解决“哈希冲突”;(Java8以后,在哈希冲突过多的时候,为了降低元素查询的时间复杂度,会将链表改为红黑树结果进行存储。树化的本质原因)
2、特点:
- 允许null值和null键
- 动态扩容(扩容机制与ArrayList相同)
- 不存在重复的元素
- 无序的
3、底层原理:
HashMap通过hash算法来确定每个键值对的位置,实现对于元素的增加,删除,修改等操作。HashMap 的性能表现非常依赖于哈希码的有效性。
JDK1.8 之前的实现方式:数组+链表;JDK1.8之后的实现方式:数组+链表+红黑树。
① HashMap是一个存储key-value键值对的集合,每一个键值对也叫做entry,这些entry分散存储在一个数组中,这个数组也是HashMap的主干,这个数组每个元素的初始值都是null。
② 数组(键值对entry组成的数组主干)+ 链表(元素太多时为解决哈希冲突数组的一个元素上多个entry组成的链表)+ 红黑树(当链表的元素个数达到8链表存储改为红黑树存储)
(1)put方法原理:比如HashMap.put("zhangsan",0),这时需要利用哈希函数来确定entry的插入位置index,哈希函数算法:index=HashCode(zhangsan) & (Length - 1)。
这也是为什么数组是有序的而HashMap是无序的。
因为HashMap长度有限,当插入的entry越来越多时,再完美的哈希函数也难免会出现index冲突的情况(哈希冲突),这时就需要用到链表。HashMap数组的每一个元素上不只有一个entry对象,也是一个链表的头结点。每一个entry对象通过next指针指向它的下一个entry节点,新来的entry节点插入链表时是头插法插入到链表头结点(也就是数组那个位置)。之所以把新来的节点插入到头结点是因为HashMap作者认为新来的被查找的可能性更大,这就是HashMap的底层原理。
(2)get方法原理:比如HashMap.get("zhangsan"),根据key做哈希映射获得index,由于会有哈希冲突的情况,同一个index位置下可能有多个entry,这时候就需要顺着对应链表的头节点,一个一个向下来查找。
(3)元素位置算法:通过hashcode定位到要存储在数组的位置,为了实现一个尽量均匀分布的Hash函数,我们通过利用Key的HashCode值来做某种运算。比如把Key的HashCode值和HashMap长度做取模运算,取模运算的方式固然简单,但是效率很低,因为位运算直接对内存数据进行操作,不需要转成十进制,所以位运算要比取模运算的效率更高。所以为了实现高效的Hash算法,HashMap的发明者采用了位运算的方式。这样做不但效果上等同于取模,而且还大大提高了性能。index= HashCode(Key) & (Length - 1)。
这也是为什么数组是有序的而HashMap是无序的、HashMap默认长度是16或者2的幂。
4、线程安全问题:
线程不安全
public class HashMapDemo {
public static void main(String[] args) {
Map<String, String> map = new HashMap();
for (int i = 0; i < 10; i++) {
String key = String.valueOf(i);
new Thread(() -> {
//向集合中添加内容
map.put(key, UUID.randomUUID().toString().substring(0, 8));
//从集合中获取内容
System.out.println(map);
}, String.valueOf(i)).start();
}
/*
Exception in thread "7" Exception in thread "1" Exception in thread "3" Exception in thread "4" Exception in thread "0" Exception in thread "5" java.util.ConcurrentModificationException
*/
}
}
线程不安全解决方案:通过ConcurrentHashMap类解决
import java.util.concurrent.ConcurrentHashMap;
public class HashMapDemo {
public static void main(String[] args) {
// Map<String, String> map = new HashMap();
Map<String, String> map = new ConcurrentHashMap();
for (int i = 0; i < 10; i++) {
String key = String.valueOf(i);
new Thread(() -> {
//向集合中添加内容
map.put(key, UUID.randomUUID().toString().substring(0, 8));
//从集合中获取内容
System.out.println(map);
}, String.valueOf(i)).start();
}
}
}
5、HashMap主要实现的接口
(1)Map接口:得到了map中定义的所有接口
(2)Cloneable接口:实现Cloneable接口并重写Object类中的clone方法,Cloneable接口中没有任何的实现方法,它属于一个标识性接口。
(3)Serializable接口:可以进行序列化,通过序列化后进行传输,典型应用就是hessian协议
public class HashMap<K,V> extends AbstractMap<K,V>
implements Map<K,V>, Cloneable, Serializable {
//hashMap中的数组初始化大小:1 << 4=2^4=16
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4;
//1<<30 表示1左移30位,每左移一位乘以2,所以就是1*2^30=1073741824。
static final int MAXIMUM_CAPACITY = 1 << 30;
//默认装载因子:
static final float DEFAULT_LOAD_FACTOR = 0.75f;
//HashMap默认初始化的空数组:
static final java.util.HashMap.Entry<?,?>[] EMPTY_TABLE = {};
//HashMap中底层保存数据的数组:HashMap其实就是一个Entry数组
transient java.util.HashMap.Entry<K,V>[] table = (java.util.HashMap.Entry<K,V>[]) EMPTY_TABLE;
//Hashmap中元素的个数:
transient int size;
//threshold:等于capacity * loadFactory,决定了HashMap能够放进去的数据量
int threshold;
//loadFactor:装载因子,默认值为0.75,它决定了bucket填充程度;
final float loadFactor;
//HashMap被操作的次数:
transient int modCount;
}
6、扩容
6.1、扩容的条件:
table 是底层用于保存数据的数组,默认情况下会先赋值空数组,最终存储元素的是Entry[]数组,其中的对象就是Entry对象,在首次进行put的时候,才会进行数组的初始化,建立一个容量为16的数组。
在Java中,HashMap的扩容条件是基于当前HashMap容量(即内部数组的大小)和实际存储元素的数量。具体来说,在Java 7及以后版本中,以下两种情况都会触发HashMap扩容:
(1)装载因子阈值: 当HashMap中的元素数量(entry数量)超过当前容量与预设的负载因子(load factor)的乘积时,会触发扩容操作。默认负载因子DEFAULT_LOAD_FACTOR 为0.75,也就是说,当HashMap中的元素个数达到容量的75%时,就会进行扩容。
(2)插入新元素时: 在执行put()操作尝试插入一个新的键值对时,如果发现现有元素数量已经达到了扩容阈值,并且确实需要新增一个元素(不是替换已存在的元素),那么也会触发扩容操作。
扩容过程涉及创建一个新的、更大容量的数组,并将原数组中的所有键值对重新计算哈希值并移动到新的数组中。这个过程也称为“rehashing”,并且在Java 8中引入了优化,链表长度大于某个阈值时会转化为红黑树,以减少搜索、插入和删除的时间复杂度。
注意:HashMap初始化之后,并没有立即分配内存空间,初始化时并没有初始化数组 table,在 put 操作时才初始化。
6.2、扩容的过程
demo:
初始容量为16,当添加第13个元素时,
当初始容量为16的HashMap添加第13个元素时,由于默认负载因子是0.75,所以扩容阈值(threshold)计算公式为:capacity * loadFactor = 16 * 0.75 = 12。
因此,当添加第13个元素时,HashMap的实际存储元素数量超过了扩容阈值(即当前已存储12个元素+即将添加的第13个元素),将会触发扩容操作。具体扩容流程:
(1)创建新的Entry数组:
HashMap会创建一个新的Entry数组,其容量通常是原来容量的两倍,也就是newCapacity = oldCapacity << 1 = 16 * 2 = 32。
所以hashMap的长度一定是2的N次幂,对于Hash值的计算,hashMap中采用的是 与操作,相比于 取模运算 效率更高:
① 当length为2的N次方的时候,length一定是偶数,这样length-1一定是奇数,当奇数转换成二机制数的时候,最后一位永远是1,那么HashMap中每一个位置都是奇数,当通过Hash值 与 length-1 进行与操作的时候,结果可能是偶数,也可能是奇数,因此散列性比较好,可以有效的降低哈希冲突
② 当length为奇数是的时候,length-1 就一定是偶数,当偶数转换成二机制的时候,最后一位就是0,任何值和0进行与运算,结果都会是 0 ,因此无论是hash值是啥,进行与操作后结果都是偶数,从而造成一半的数组位都是浪费的。从而增加 哈希冲突的概率,从而降低HashMap的性能。
(2)重新哈希所有元素:
遍历原HashMap中的每个Entry(键值对),使用新的容量重新计算它们的索引位置,并将这些Entry放入新的数组中。这个过程被称为“rehash”。
(3)链表拆分或树形结构调整:
如果在扩容过程中某个桶(bucket)下形成了链表结构,那么在这个桶下的链表会根据新的索引被拆分成两个链表,分别存放在新数组的相应位置。在Java 8及以上版本中,如果链表长度超过8且HashMap允许转化为红黑树(通过treeifyThreshold控制,默认也是8),则链表会被转换为红黑树。
(4)替换引用:
扩容完成后,HashMap会将内部引用指向新的Entry数组,旧数组将不再使用,随后垃圾回收机制会在适当的时候回收旧数组。
三、 HashTable
1、介绍:
在 Hashtable 的类注释可以看到,Hashtable 是保留类不建议使用,推荐在单线程环境下使用 HashMap 替代,如果需要多线程使用则用 ConcurrentHashMap 替代。
HashTable逐渐被ConcurrentHashMap取代,但是Hashtable的子类Properties依然沿用,Properties集合也是唯一一个和IO流相结合的集合。
2、特点:
- 线程安全的,单线程集合
- 速度快
- 不允许null键和null值
3、原理
HashTable与HashMap的结构一致,都是哈希表实现。
与HashMap不同的是,在HashTable中,所有的方法都加上了synchronized锁,用锁来实现线程的安全性。由于synchronized锁加在了HashTable的每一个方法上,所以这个锁就是HashTable本身--this。因而效率不高。
四、ConcurrentHashMap
1、介绍:
ConcurrentMap是一个接口,支持并发访问的Map的集合。在Map接口上增加了4个扩展方法。主要的实现类就是ConcurrentHashMap。ConcurrentHashMap是一个线程安全并且高效的HashMap。
public interface ConcurrentMap<K, V> extends Map<K, V> {
//插入元素
V putIfAbsent(K key, V value);
//移除元素
boolean remove(Object key, Object value);
//替换元素
boolean replace(K key, V oldValue, V newValue);
//替换元素
V replace(K key, V value);
}
五、TreeMap:
1、介绍:
(1)唯一;
(2)有序,可排序。如获取一周菜单Map <String,MenuDTO>,希望按照周一到周日的顺序排列,就可以使用TreeMap。有序的前提是key需要实现比较器。
2、底层原理:
红黑树。TreeMap的键值对是存放在红黑树中的,key的顺序通过红黑树的自平衡实现的。
源码:
1.TreeMap每一个节点内部属性
K key; //键
V value; //值
Entry<K,V> left; //左子节点对象
Entry<K,V> right; //右子节点对象
Entry<K,V> parent; //父节点对象
boolean color; //节点颜色
2.TreeMap成员变量
public class TreeMap<K,V>{
//比较器对象
private final Comparator<? super K> comparator;
//根节点
private transient Entry<K,V> root;
//集合长度
private transient int size = 0;
3.空参数构造
public TreeMap() {
//表示没有比较器对象
comparator = null;
}
4.带参数构造
//自己传递的比较器对象
public TreeMap(Comparator<? super K> comparator) {
this.comparator = comparator;
}
5.添加元素
public V put(K key, V value) {
return put(key, value, true);
}
//参数1: 键
//参数2: 值
//参数3: 当键重复时,是否需要覆盖值
true:覆盖
false:不覆盖
private V put(K key, V value, boolean replaceOld) {
//获取根节点的地址值,赋值给局部变量t
Entry<K,V> t = root;
//判断根节点是否为null
//如果为null,表示第一次添加,会把当前要添加的元素,做根节点
//如果不为null,表示当前不是第一次添加,跳过这个判断,续执行下面的代码
if (t == null) {
//创建一个Entry对象,将键值传递过去,当做根节点
addEntryToEmptyMap(key, value);
//表示此时没有覆盖任何元素
return null;
}
//表示两个元素的键比较之后的结果
//负数/正数/0
int cmp;
//当前要添加节点的父节点
Entry<K,V> parent;
//表示记录了比较规则
//如果是自然排序,comparator是null,cpr也是null
//如果记录比较器排序方式,此时记录的是比较器
Comparator<? super K> cpr = comparator;
//判断当前是否有比较器对象
//如果传递了比较器对象,就执行if代码,此时以比较器为准
//没有传递比较器则执行else代码,此时以自然排序为准
if (cpr != null) {
do {
parent = t;
cmp = cpr.compare(key, t.key);
if (cmp < 0)
t = t.left;
else if (cmp > 0)
t = t.right;
else {
V oldValue = t.value;
if (replaceOld || oldValue == null) {
t.value = value;
}
return oldValue;
}
} while (t != null);
} else {
//把键进行强转成Comparable类型
//要求: 键 须实现Comparable接口,不然强转会报错
Comparable<? super K> k = (Comparable<? super K>) key;
do {
//把根节点当做当前节点的父节点
parent = t;
//调用compareTo比较根节点与当前要添加节点的大小关系
cmp = k.compareTo(t.key);
if (cmp < 0)
//结果负数
//去根节点左边去找
t = t.left;
else if (cmp > 0)
//结果为正(根节点比当前小)
//去根节点右边去找
t = t.right;
else {
//结果为0,会覆盖
V oldValue = t.value;
if (replaceOld || oldValue == null) {
t.value = value;
}
return oldValue;
}
} while (t != null);
}
//当前节点 按照 指定规则进行添加
addEntry(key, value, parent, cmp < 0);
return null;
}
private void addEntry(K key, V value, Entry<K, V> parent, boolean addToLeft) {
Entry<K,V> e = new Entry<>(key, value, parent);
if (addToLeft)
parent.left = e;
else
parent.right = e;
//添加完毕后,按照红黑树规则进行调整
fixAfterInsertion(e);
size++;
modCount++;
}
private void fixAfterInsertion(Entry<K,V> x) {
//红黑树节点默认为红色
x.color = RED;
//按照红黑规则调整
//parentOf:获取父节点
//parentOf(parentOf(x)): 爷爷节点获取
//leftOf:左子节点获取
//当前节点不为空,且不是根节点,且父节点为红色时,进入循环
while (x != null && x != root && x.parent.color == RED) {
//判断当前节点的父节点是爷爷节点的左子节点还是右子节点
//目的: 为了获取当前节点的叔叔节点
if (parentOf(x) == leftOf(parentOf(parentOf(x)))) {
//表示当前节点的父节点是爷爷节点的左子节点
//下面可以用rightOf获取当前节点的叔叔节点
Entry<K,V> y = rightOf(parentOf(parentOf(x)));
if (colorOf(y) == RED) {
//叔叔节点为红色
//将父节点设置为黑色
setColor(parentOf(x), BLACK);
//将叔叔节点设置为黑色
setColor(y, BLACK);
//将爷爷节点设置为红色
setColor(parentOf(parentOf(x)), RED);
//把爷爷节点设置为当前节点
x = parentOf(parentOf(x));
} else {
//叔叔节点为黑色
//判断当前节点是否为父节点的右子节点
if (x == rightOf(parentOf(x))) {
//表示当前节点是父节点的右子节点
x = parentOf(x);
//以要添加的节点的父节点 左旋,因为上面这个代码把当前节点变成了要添加元素的父节点
//所以下面直接是以x左旋 == 要添加的节点的父节点
rotateLeft(x);
}
//父节点设置为黑色
setColor(parentOf(x), BLACK);
//爷爷节点设置为红色
setColor(parentOf(parentOf(x)), RED);
//以要添加元素的祖父节点当做轴 右旋
rotateRight(parentOf(parentOf(x)));
}
} else {
//表示当前节点的父节点是爷爷节点的右节点
//下面可以用leftOf获取当前节点叔叔节点
Entry<K,V> y = leftOf(parentOf(parentOf(x)));
if (colorOf(y) == RED) {
//叔叔节点是红色时
//父节点设置为黑色
setColor(parentOf(x), BLACK);
//叔叔节点设置为黑色
setColor(y, BLACK);
//爷爷节点设置为红色
setColor(parentOf(parentOf(x)), RED);
//爷爷节点设置为当前节点,进行下一轮循环
x = parentOf(parentOf(x));
} else {
//叔叔节点为黑色
if (x == leftOf(parentOf(x))) {
//当前节点为父节点的左子节点时
//把当前节点设置为父节点
x = parentOf(x);
//并且以父节点右旋
rotateRight(x);
}
//设置父节点为黑色
setColor(parentOf(x), BLACK);
//设置爷爷节点为红色
setColor(parentOf(parentOf(x)), RED);
//以爷爷节点去左旋
rotateLeft(parentOf(parentOf(x)));
}
}
}
//如果当前添加节点是根节点
root.color = BLACK;
}
setColor(parentOf(parentOf(x)), RED);
//以爷爷节点去左旋
rotateLeft(parentOf(parentOf(x)));
}
}
}
//如果当前添加节点是根节点
root.color = BLACK;
}