文章目录
一、Map结构
1、架构
2、接口
1、Map接口是所有实现类和其他接口的父接口(键值对的存储方式),提供了最基本的接口方法:容量、判断是否为空、增加(单个或者集合)、删除、获取键集合、获取值集合、entrySet迭代键值对、jdk1.8中添加的lambda表达式
2、SortedMap接口:增加了排序功能,定义了比较器(默认或者自定义)、提供了三个方法:
subMap(K fromKey, K toKey)
获取在该范围内的SortedMap
headMap(K toKey)
获取小于该值的
SortedMap tailMap(K fromKey)
获取大于等于该值的SortedMap
所有SortedMap 实现类都应该提供 4 个“标准”构造方法:
(01) void(无参数)构造方法,它创建一个空的有序映射,按照键的自然顺序进行排序。
(02) 带有一个 Comparator 类型参数的构造方法,它创建一个空的有序映射,根据指定的比较器进行排序。
(03) 带有一个 Map 类型参数的构造方法,它创建一个新的有序映射,其键-值映射关系与参数相同,按照键的自然顺序进行排序。
(04) 带有一个 SortedMap 类型参数的构造方法,它创建一个新的有序映射,其键-值映射关系和排序方法与输入的有序映射相同。无法保证强制实施此建议,因为接口不能包含构造方法。
3、NavigableMap接口:
所有关于获取小于的键值对或者key,都是获取小于中的最大值
所有关于获取大于的键值对或者key,都是获取大于中的最小值
1、提供操作键-值对的方法。
lowerEntry、floorEntry、ceilingEntry 和 higherEntry 方法,它们分别返回与小于、小于等于、大于等于、大于给定键的键关联的 Map.Entry 对象。
firstEntry、pollFirstEntry、lastEntry 和 pollLastEntry 方法,它们返回和/或移除最小和最大的映射关系(如果存在),否则返回 null。
2、提供操作键的方法。这个和第1类比较类似
lowerKey、floorKey、ceilingKey 和 higherKey 方法,它们分别返回与小于、小于等于、大于等于、大于给定键的键。
3、获取键集。
navigableKeySet、descendingKeySet分别获取正序/反序的键集。
4、获取键-值对的子集。
3、抽象类
1、AbstractMap实现了Map公共类方法:实现了部分方法,除了entrySet()返回键值对的方式(至于为什么没有实现,我还没想到解释)和put()每个map实现类插入方式不同,必须重写此类的 put 方法(否则将抛出 UnsupportedOperationException)
2、Dictionary:包括了基本的增删改查,但是这些方法都需要重写
4、子类
1、HashTable:继承自Dictionary,jdk1.1就发布,线程安全地存储键值对
2、HashMap:继承自AbstractMap,同样可以实现HashTable中的功能,唯一一点遗憾就是只适合于单线程,多线程环境可以通过静态方法synchronizedMap实现线程安全(该方法以后有机会补充),或者使用ConcurrentHashMap替代
3、TreeMap:底层实现红黑树,能够实现键值对的排序,针对排序的键值对有着得天独厚的优势
4、WeakHashMap:消除陈旧对象,即在我们使用短时间内就过期的缓存时最好使用weakHashMap,它包含了一个自动调用的方法expungeStaleEntries,这样就会在值被引用后直接执行这个隐含的方法,将不用的键清除掉。弱引用需要配合一个引用队列,将JVM回收的对象引用放置到队列中
5、LInkedHashMap:保证hashMap集合有序,存入的顺序与取出的顺序一致。
二、HashMap
我的其他博客已经说明过:https://blog.csdn.net/qq_32679835/article/details/90447248
三、HashTable
1、属性
1、Entry<?,?>[] table
用来存储元素,默认初始值为11,下一次扩容为原来2倍+1
2、int count
:总数
3、int threshold
:扩容界限
4、float loadFactor
:加载因子,默认为0.75
5、int modCount = 0
:用于实现fail-fast机制
2、构造方法
1、 Hashtable(int initialCapacity, float loadFactor)
设置初始容量与加载因子
2、Hashtable(Map<? extends K, ? extends V> t)
传入Map作为加载因子
3、其余两个构造函数都是默认调用1的构造函数
3、核心方法
HashTable中键值对不容许为null,设置为null便会抛出NullPointerException,需要关注的是所有方法都是线程安全
1、put操作与扩容操作,扩容为原来的2倍+1
//对外方法
public synchronized V put(K key, V value) {
// 值不为空
if (value == null) {
throw new NullPointerException();
}
Entry<?,?> tab[] = table;
int hash = key.hashCode();//计算hash使用默认计算方式
int index = (hash & 0x7FFFFFFF) % tab.length;//查找数组中的位置
@SuppressWarnings("unchecked")
Entry<K,V> entry = (Entry<K,V>)tab[index];
//key值已经存在,则替换旧值
for(; entry != null ; entry = entry.next) {
if ((entry.hash == hash) && entry.key.equals(key)) {
V old = entry.value;
entry.value = value;
return old;
}
}
addEntry(hash, key, value, index);
return null;
}
//私有方法
private void addEntry(int hash, K key, V value, int index) {
modCount++;
Entry<?,?> tab[] = table;
if (count >= threshold) {
rehash(); // 扩容
tab = table;
hash = key.hashCode();
index = (hash & 0x7FFFFFFF) % tab.length;
}
// 创建新的实体
@SuppressWarnings("unchecked")
Entry<K,V> e = (Entry<K,V>) tab[index];
tab[index] = new Entry<>(hash, key, value, e);
count++;
}
@SuppressWarnings("unchecked")
protected void rehash() {
int oldCapacity = table.length;
Entry<?,?>[] oldMap = table;
//扩容为原来2倍+1
int newCapacity = (oldCapacity << 1) + 1;
if (newCapacity - MAX_ARRAY_SIZE > 0) {
if (oldCapacity == MAX_ARRAY_SIZE)//大于最大容量则数组容量等于最大容量
// Keep running with MAX_ARRAY_SIZE buckets
return;
newCapacity = MAX_ARRAY_SIZE;
}
Entry<?,?>[] newMap = new Entry<?,?>[newCapacity];
modCount++;
threshold = (int)Math.min(newCapacity * loadFactor, MAX_ARRAY_SIZE + 1);
table = newMap;
//将旧数组中的元素移动到新数组中
for (int i = oldCapacity ; i-- > 0 ;) {
for (Entry<K,V> old = (Entry<K,V>)oldMap[i] ; old != null ; ) {
Entry<K,V> e = old;
old = old.next;
int index = (e.hash & 0x7FFFFFFF) % newCapacity;//新数组的容量
e.next = (Entry<K,V>)newMap[index];
newMap[index] = e;
}
}
}
2、putAll:添加一个Map集合进Map
public synchronized void putAll(Map<? extends K, ? extends V> t) {
for (Map.Entry<? extends K, ? extends V> e : t.entrySet())
put(e.getKey(), e.getValue());//迭代内部调用put方法
}
3、get(Object key)获取key值对应的value
public synchronized V get(Object key) {
Entry<?,?> tab[] = table;
int hash = key.hashCode();
int index = (hash & 0x7FFFFFFF) % tab.length;
for (Entry<?,?> e = tab[index] ; e != null ; e = e.next) {
if ((e.hash == hash) && e.key.equals(key)) {
return (V)e.value;
}
}
return null;
}
4、remove:删除Hashtable中键为key的元素
4、遍历方式
1、keySet()返回key值集合
public Set<K> keySet() {
if (keySet == null)
keySet = Collections.synchronizedSet(new KeySet(), this);//新建内部类KeySet
return keySet;
}
private class KeySet extends AbstractSet<K> {
public Iterator<K> iterator() {
return getIterator(KEYS);//返回key值的迭代对象,通过Enumerator来迭代,遍历数组的方式是倒序的
}
public int size() {
return count;
}
public boolean contains(Object o) {
return containsKey(o);
}
public boolean remove(Object o) {
return Hashtable.this.remove(o) != null;
}
public void clear() {
Hashtable.this.clear();
}
}
Enumerator来迭代,遍历数组的方式是倒序的
int index = table.length;//数组长度
public boolean hasMoreElements() {
Entry<?,?> e = entry;
int i = index;
Entry<?,?>[] t = table;
/* Use locals for faster loop iteration */
while (e == null && i > 0) {
e = t[--i];//逆序遍历数组,正序遍历数组所对应的链表
}
entry = e;
index = i;
return e != null;
}
2、values()遍历Map中的所有值
public Collection<V> values() {
if (values==null)
//通过ValueCollection实现遍历,使用synchronizedCollection同步方法,内部同样通过Enumerator来迭代,遍历数组的方式是倒序的
values = Collections.synchronizedCollection(new ValueCollection(),
this);
return values;
}
3、entrySet()遍历键值对:
public Set<Map.Entry<K,V>> entrySet() {
if (entrySet==null)
entrySet = Collections.synchronizedSet(new EntrySet(), this);//内部同样使用Enumerator遍历
return entrySet;
}
4、elements获取所有值的Enumerator枚举类(属于土著方法,最直接调用枚举迭代)
5、总结
首先,Hashtable底层是通过数组加链表实现的
其次,Hashtable是不允许key或者value为null的。
并且,Hashtable的计算索引方法(int index = (hash & 0x7FFFFFFF) % tab.length;
),默认容量大小(default = 11),扩容方法都与HashMap不太一样(两层循环扩容)。
其实我们可以看到,Hashtable之所以线程安全,大部分方法都是使用了synchronized关键字,虽然JDK优化了synchronized,但在方法上使用该关键字,无疑仍旧是效率低下的操作。
四、TreeMap
由于TreeMap篇幅过长,单独写了另一篇博客:https://blog.csdn.net/qq_32679835/article/details/93502092
五、WeakHashMap
WeakHashMap 也是一个散列表,它存储的内容也是键值对(key-value)映射,而且键和值都可以是null;其中的key值是弱键,也就意味着该键映射的键值对可以被JVM回收。
弱键是通过WeakReference和ReferenceQueue实现的
1、新建WeakHashMap,将“键值对”添加到WeakHashMap中。
2、当“弱键”不再被其他对象引用,并被GC回收,这个“弱键”也同时会被添加到ReferenceQueue(queue)队列中。
3、下一次操作WeakHashMap前,同步table和queue队列,删除table中被GC回收的键值对
1、属性
1、默认初始容量:DEFAULT_INITIAL_CAPACITY = 16
2、隐式最大容量:MAXIMUM_CAPACITY = 1 << 30 (2^n<最大容量)
3、引用队列用来记录GC的键值对: ReferenceQueue queue = new ReferenceQueue<>()
其余属性与正常Map相同
4、private static final Object NULL_KEY = new Object();
由于WeakHashMap容许null键,此时可能会被GC回收,避免被回收的危险,将null键统一替换为NULL_KEY
2、构造函数
// 默认构造函数。
WeakHashMap()
// 指定“容量大小”的构造函数
WeakHashMap(int capacity)
// 指定“容量大小”和“加载因子”的构造函数
WeakHashMap(int capacity, float loadFactor)//每一次扩容为原来的2倍
// 包含“子Map”的构造函数
WeakHashMap(Map<? extends K, ? extends V> map)
3、核心方法
1、indexFor()定位桶位置与HashMap相同
private static int indexFor(int h, int length) {
return h & (length-1);
}
2、expungeStaleEntries()遍历引用队列,同步table中的键值对
private void expungeStaleEntries() {
for (Object x; (x = queue.poll()) != null; ) {
synchronized (queue) {
@SuppressWarnings("unchecked")
Entry<K,V> e = (Entry<K,V>) x;
int i = indexFor(e.hash, table.length);
Entry<K,V> prev = table[i];
Entry<K,V> p = prev;
while (p != null) {
Entry<K,V> next = p.next;
if (p == e) {
if (prev == e)
table[i] = next;
else
prev.next = next;
e.value = null; // Help GC
size--;
break;
}
prev = p;
p = next;
}
}
}
}
3、get()获取对应key值的value值
public V get(Object key) {
Object k = maskNull(key);
int h = hash(k);//hash计算
Entry<K,V>[] tab = getTable();
int index = indexFor(h, tab.length);//数组位置
Entry<K,V> e = tab[index];
while (e != null) {
if (e.hash == h && eq(k, e.get()))
return e.value;
e = e.next;
}
return null;
}
4、put()添加键值对
public V put(K key, V value) {
Object k = maskNull(key);
int h = hash(k);
Entry<K,V>[] tab = getTable();
int i = indexFor(h, tab.length);
// 若“该key”对应的键值对已经存在,则用新的value取代旧的value。然后退出!
for (Entry<K,V> e = tab[i]; e != null; e = e.next) {
if (h == e.hash && eq(k, e.get())) {
V oldValue = e.value;
if (value != oldValue)
e.value = value;
return oldValue;
}
}
// 若“该key”对应的键值对不存在于WeakHashMap中,则将“key-value”添加到table中
modCount++;
Entry<K,V> e = tab[i];
tab[i] = new Entry<>(k, value, queue, h, e);
if (++size >= threshold)
resize(tab.length * 2);//扩容为原来两倍,重新计算元素所在的桶位置
return null;
}
5、remove()删除键为key的元素
public V remove(Object key) {
Object k = maskNull(key);
int h = hash(k);
Entry<K,V>[] tab = getTable();
int i = indexFor(h, tab.length);
Entry<K,V> prev = tab[i];
Entry<K,V> e = prev;
while (e != null) {
Entry<K,V> next = e.next;
if (h == e.hash && eq(k, e.get())) {
modCount++;
size--;
if (prev == e)
tab[i] = next;
else
prev.next = next;//删除链表的节点
return e.value;
}
prev = e;
e = next;
}
return null;
}
4、迭代方式
1、entrySet()迭代
Iterator iter = map.entrySet().iterator();
while(iter.hasNext()) {
Map.Entry entry = (Map.Entry)iter.next();
// 获取key
key = entry.getKey();
// 获取value
integ =entry.getValue();
}
2、keySet()迭代
3、values()迭代
5 使用demo
可以用作缓存等,内存不足的时候回收
import java.util.WeakHashMap;
public class testWeakHashMap {
public static void main(String[] args) {
WeakHashMap<String, String> map = new WeakHashMap<String, String>();
WeakHashMap<String, String> map2 = new WeakHashMap<String, String>();
map.put(new String("s"), "robin");
map2.put("s", "robin");
System.gc();
System.out.println(map);
System.out.println(map2);//常量池不会被回收
}
}
六、Map总结
1、HashMap和Hashtable异同
- 相同点
1、table存储结构,采取数组+链表的存储形式,添加数据与删除数据步骤:计算Hash码,之后计算对应数组索引,之后遍历遍历链表插入元素
2、都实现了实现了Map、Cloneable、java.io.Serializable接口,可以被克隆对象与序列化
- 不同点
1、继承方式不同:HashMap 继承于AbstractMap,Hashtable 继承于Dictionary,AbstractMap是一个抽象类,它实现了Map接口的绝大部分API函数,而Hashtable 继承于Dictionary,最大不同在于支持Enumeration遍历
2、线程安全方式:HashMap 非同步,不是线程安全;Hashtable 同步线程安全
3、null值处理:HashMap的key、value都可以为null。Hashtable的key、value都不可以为null。
4、支持遍历方式:HashMap只支持Iterator(迭代器)遍历。而Hashtable支持Iterator(迭代器)和Enumeration(枚举器)两种方式遍历,其实HashTable内部迭代,还是封装了枚举器的遍历方法
5、通过Iterator迭代器遍历时,遍历的顺序不同:HashMap是“从前向后”的遍历数组;再对数组具体某一项对应的链表,从表头开始进行遍历;Hashtabl是“从后往前”的遍历数组;再对数组具体某一项对应的链表,从表头开始进行遍历。
6、容量的初始值 和 增加方式都不一样:HashMap默认的容量大小是16;增加容量时,每次将容量变为“原始容量x2”;Hashtable默认的容量大小是11;增加容量时,每次将容量变为“原始容量x2 + 1”。
7、添加key-value时的hash值算法不同:HashMap自定义Hash方法,HashTable直接采用的key的hashCode()。
HashMap自定义hash方法,添加了hash扰乱:
2、HashMap和WeakHashMap异同
- 相同点
1 它们都是散列表,存储的是“键值对”映射。
2 它们都继承于AbstractMap,并且实现Map基础。
3 它们的构造函数都一样。 它们都包括4个构造函数,而且函数的参数都一样。
4 默认的容量大小是16,默认的加载因子是0.75。
5 它们的“键”和“值”都允许为null。
6 它们都是“非同步的”。
- 不同点
1、HashMap实现了Cloneable和Serializable接口,而WeakHashMap没有
2、HashMap的“键”是“强引用(StrongReference)”,而WeakHashMap的键是“弱引用(WeakReference)
3、HashMap和TreeMap的区别
1、底层实现方式:HashMap是Hash表,TreeMap是红黑树
2、是否有序:HashMap无序,TreeMap根据键排序
3、适用场景:HashMap:Map中添加、删除和查找元素;TreeMap适用于按照自然排序或者自定义排序遍历键
4、HashMap通常会比treeMap快一点
4、使用场景
1、需要基于排序的统计功能:TreeMap底层采用红黑树,时间复杂度为O(logn)
2、需要快速增删改查的存储功能:hashMap,时间复杂度为O(1)
3、需要快速增删改查而且需要保证遍历和插入顺序一致的存储功能 ,LinkedhashMap 时间复杂度O(1),并且保证按照插入顺序
七、引用
【1】Map系列文章:https://www.cnblogs.com/skywang12345/p/3323085.html