Map--Hashtable
Map
Map集合类用于存储元素对(称作“键”和“值”),其中每个键映射到一个值。Java自带了各种Map类,主要分为以下三类:
1、通用Map,用于在应用程序中管理映射,通常放置在java.util包中:HashMap、Hashtable、Properties、LinkedHashMap、IdentityHashMap、TreeMap、WeakHashMap、ConcurrentHashMap
2、专用Map,通常我们不必亲自创建此类Map,而是通过某些其他类对其进行访问:java.util.jar.Attributes、javax.print.attribute.standard.PrinterStateReasons、java.security.Provider、java.awt.RenderingHints、javax.swing.UIDefaults
3、一个用于帮助我们实现自己的Map类的抽象类AbstractMap
Map的常用方法
1 | void clear( ) 从此映射中移除所有映射关系(可选操作)。 |
---|---|
2 | boolean containsKey(Object k) 如果此映射包含指定键的映射关系,则返回 true。 |
3 | boolean containsValue(Object v) 如果此映射将一个或多个键映射到指定值,则返回 true。 |
4 | Set entrySet( ) 返回此映射中包含的映射关系的 Set 视图。 |
5 | boolean equals(Object obj) 比较指定的对象与此映射是否相等。 |
6 | Object get(Object k) 返回指定键所映射的值;如果此映射不包含该键的映射关系,则返回 null。 |
7 | int hashCode( ) 返回此映射的哈希码值。 |
8 | boolean isEmpty( ) 如果此映射未包含键-值映射关系,则返回 true。 |
9 | Set keySet( ) 返回此映射中包含的键的Set视图。 |
10 | Object put(Object k, Object v) 将指定的值与此映射中的指定键关联(可选操作)。 |
11 | void putAll(Map m) 从指定映射中将所有映射关系复制到此映射中(可选操作)。 |
12 | Object remove(Object k) 如果存在一个键的映射关系,则将其从此映射中移除(可选操作)。 |
13 | int size( ) 返回此映射中的键-值映射关系数。 |
14 | Collection values( ) 返回此映射中包含的值的 Collection 视图。 |
Hashtable
public class Hashtable<K,V>
extends Dictionary<K,V>
implements Map<K,V>, Cloneable, java.io.Serializable
实现了Map接口,继承了Dictionary类,Dictionary类是任何可以将键映射到相应值的类的父类。每个键和每个值都是一个对象。在任何一个Dictionary对象中,每个键至多与一个值相关联。
重要参数
HashTable采用"拉链法"实现哈希表,它定义了几个重要的参数:table、count、threshold、loadFactor、modCount。
// 为一个Entry[]数组类型,Entry代表了“拉链”的节点,每一个Entry代表了一个键值对,哈希表的"key-value键值对"都是存储在Entry数组中的。
private transient Entry<?,?>[] table;
// HashTable的大小,这个大小并不是HashTable的容器大小,而是它所包含Entry键值对的数量。
private transient int count;
// Hashtable的阈值,用于判断是否需要调整Hashtable的容量。threshold的值="容量*加载因子"。
private int threshold;
// 加载因子
private float loadFactor;
// 用来实现“fail-fast”机制的(也就是快速失败)。所谓快速失败就是在并发集合中,其进行迭代操作时,若有其他线程对其进行结构性的修改,这时迭代器会立马感知到,并且立即抛出ConcurrentModificationException异常,而不是等到迭代完成之后才告诉你
private transient int modCount = 0;
构造方法
public Hashtable(int initialCapacity) {
this(initialCapacity, 0.75f);
}
public Hashtable() {
this(11, 0.75f);
}
public Hashtable(Map<? extends K, ? extends V> t) {
this(Math.max(2*t.size(), 11), 0.75f);
putAll(t);
}
默认构造函数,容量为11,加载因子为0.75,所以初始化时,HashTable的初始容量为11。也可以在初始化,只指定初始容量,此时默认的加载因子依旧是0.75。
也可以直接传入一个Map集合,此时的容量会设置为11与传入Map集合大小的两倍之间的最大值。
这三个构造方法调用的都是下面这个构造方法:
public Hashtable(int initialCapacity, float loadFactor) {
if (initialCapacity < 0)
throw new IllegalArgumentException("Illegal Capacity: "+
initialCapacity);
if (loadFactor <= 0 || Float.isNaN(loadFactor))
throw new IllegalArgumentException("Illegal Load: "+loadFactor);
if (initialCapacity==0)
initialCapacity = 1;
this.loadFactor = loadFactor;
table = new Entry<?,?>[initialCapacity];
threshold = (int)Math.min(initialCapacity * loadFactor, MAX_ARRAY_SIZE + 1);
}
Hashtable是线程安全的,因为其内部的公有方法基本上都被synchronized修饰了,所以是线程安全的。
存数据流程
put方法是往Hashtable中存入数据的方式,下面看一下put方法的源码:
public synchronized V put(K key, V value) {
// Make sure the value is not null
if (value == null) {
throw new NullPointerException();
}
// Makes sure the key is not already in the hashtable.
Entry<?,?> tab[] = table;
int hash = key.hashCode();
int index = (hash & 0x7FFFFFFF) % tab.length; //确认该key的索引位置
@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;
}
1、首先要确保存入的键值对的value不为null,如果为null,直接抛出异常
2、然后就是要确保集合中的key是不重复的,处理过程是:
(1)计算key的hash值,确认在table[]中的索引位置
(2)迭代index索引位置,如果该位置处的链表中存在一个一样的key,则替换其value,返回旧值。
3、确认要添加的key不是重复的以后,就将这个新的键值对添加到Entry数组中,即执行addEntry方法,顺带传入键值对以及哈希值,还有这个键值对在Entry数组中的索引值。
private void addEntry(int hash, K key, V value, int index) {
modCount++;
Entry<?,?> tab[] = table;
//如果容器中的元素数量已经达到阀值,则进行扩容操作
if (count >= threshold) {
// Rehash the table if the threshold is exceeded
rehash();
tab = table;
hash = key.hashCode();
index = (hash & 0x7FFFFFFF) % tab.length;
}
// Creates the new entry.
@SuppressWarnings("unchecked")
Entry<K,V> e = (Entry<K,V>) tab[index];
tab[index] = new Entry<>(hash, key, value, e);
count++;
}
4、添加之前要确认容器中的元素数量是否达到阈值,如果大于等于阈值,则进行扩容,扩容的机制在下面有讲解。扩容之后,会重新为要添加的键值对计算哈希值和索引值。
5、依据对象的哈希值,确定键值对在数组中的位置,然后在这个位置上插入一个新的节点。
hash值确定键值对在Entry数组中的位置,index值确定键值对在这个位置的链表中位于链表的哪个具体位置。
总结一下
put方法的整个处理流程是:计算key的hash值,根据hash值获得key在table数组中的索引位置,然后迭代该key处的Entry链表(我们暂且理解为链表),若该链表中存在一个这个的key对象,那么就直接替换其value值即可,否则将该key-value节点插入该index索引位置处。
当程序试图将一个key-value对放入Hashtable中时,程序首先根据该对象的key的 hashCode() 返回值决定该 Entry 的存储位置:如果两个Entry的key的hashCode() 返回值相同,那它们的存储位置相同。如果这两个Entr的 key通过equals比较返回true,新添加 Entry的value将覆盖集合中原有Entry的value,但key不会覆盖。如果这两个 Entry的key通过equals比较返回false,新添加的Entry将与集合中原有Entry形成Entry链,而且新添加的Entry位于 Entry链的头部。
扩容机制
在put方法中,如果需要向table[]中添加Entry元素,会首先进行容量校验,如果容量已经达到了阀值,HashTable就会进行扩容处理rehash():
protected void rehash() {
int oldCapacity = table.length;
Entry<?,?>[] oldMap = table;
// overflow-conscious code
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;
}
}
}
从源码可以看出HashTable的扩容操作是:新容量=旧容量 * 2 + 1。同时需要将原来HashTable中的元素一一复制到新的HashTable中,这个过程是比较消耗时间的,同时还需要重新计算hashSeed的,毕竟容量已经变了。
其中还需要注意的是,扩容之后,阈值发送了改变。