Java集合框架总图
Map接口和具体实现类
Java集合总体分为两个根接口,Map和Collection,其中Collection是单列集合,Map是双列集合。
Map与List、Set接口不同,它并不继承自Collection,它是由一系列键值对组成的集合,提供了key到value的映射。在Map中一个key对应一个value,所以key的存储不可重复,但value可以。
哈希结构——通过关键码找到值的数据结构
哈希函数——建立关键字和值的映射关系
注:好的哈希函数能使值均匀的分步在哈希结构中
生成哈希函数的两种方式
(1)直接寻址法:f(x)=kx+b(k和b是常数)
(2)除留余数法:f(x)=x mod m (m<p)(m小于哈希长度p)
哈希冲突的两种解决方式
(1)链地址法:当发生哈希冲突时,将哈希到对应位置的值连接在该位置的数据后面。
(2)线性探测法
Map接口常用方法
int size(); //map集合中存储的键值对的个数
boolean isEmpty(); //判断map集合是否为空 true:空 false:不为空
boolean containsKey(Object key) //判断集合中是否存在该键key
boolean containsValue(Object value); 判断集合中是否存在该值value
V get(Object key) //通过键获取对应的值
V put(K key, V value); //添加键值对
V remove(Object key); //删除操作 通过键删除键值对
常用三种遍历方式
System.out.println("通过键值对遍历");
//1、通过键值对遍历
Iterator <Map.Entry <String, String>> iterator = hashMap.entrySet().iterator();
while (iterator.hasNext()) {
Map.Entry <String, String> entry = iterator.next();
String key = entry.getKey();
String value = entry.getValue();
System.out.println(key+":"+value);
}
System.out.println();
System.out.println("通过键遍历");
//通过键遍历
Iterator <String> iterator1 = hashMap.keySet().iterator();
while (iterator1.hasNext()) {
System.out.println(iterator1.next());
}
System.out.println();
System.out.println("通过值遍历");
//通过值遍历
Iterator <String> iterator2 = hashMap.values().iterator();
while (iterator2.hasNext()) {
System.out.println(iterator2.next());
}
一、HashMap
HashMap底层是一个Entry数组,Entry是HashMap的基本组成单元,每一个Entry包含一个key-value键值对。当发生哈希冲突时,HashMap采用链表的方式来解决,在对应的数组位置存放链表的头结点。对链表而言,新加入的节点会从头结点加入。
1、继承关系
public class HashMap<K,V> extends AbstractMap<K,V>
implements Map<K,V>, Cloneable, Serializable
2、属性分析
//默认的初始容量是16
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4;
//最大容量
static final int MAXIMUM_CAPACITY = 1 << 30;
//默认的填充因子
static final float DEFAULT_LOAD_FACTOR = 0.75f;
//当桶(bucket)上的结点数大于这个值时会转成红黑树
static final int TREEIFY_THRESHOLD = 8;
//当桶(bucket)上的结点数小于这个值时树转链表
static final int UNTREEIFY_THRESHOLD = 6;
//桶中结构转化为红黑树对应的table的最小大小
static final int MIN_TREEIFY_CAPACITY = 64;
//存储元素的数组,总是2的幂次倍
transient Node<k,v>[] table;
//存放具体元素的集
transient Set<map.entry<k,v>> entrySet;
//存放元素的个数,注意这个不等于数组的长度。
transient int size;
//每次扩容和更改map结构的计数器
transient int modCount;
//临界值
//实际大小(容量*填充因子)超过临界值时,会进行扩容
//threshold = capacity * loadFactor,
//当Size>=threshold的时候,就要考虑对数组扩容
//即这是衡量数组是否需要扩容的一个标准
int threshold;
//加载因子
//用来控制数组存放数据的疏密程度
//loadFactor越趋近于1,数组中存放的数据(entry)就越多,也就越密,会让链表的长度增加
//loadFactor越小,趋近于0,数组中存放的数据(entry)也就越少,就越稀疏
//loadFactor太大导致查找元素效率低
//loadFactor太小导致数组的利用率低,存放的数据会很分散
//loadFactor的默认值为0.75f是官方给出的一个比较好的临界值
//给定的默认容量为16,负载因子为 0.75
//Map在使用过程中不断的往里面存放数据,当数量达到了16*0.75=12
//就需要将当前16的容量进行扩容
//扩容过程涉及到rehash、复制数据等操作,非常消耗性能
final float loadFactor;
HashMap内部定义了一个hash表数组,元素通过哈希转换函数将哈希地址转换成数组中存放的索引,如果有冲突,则使用散列链表的形式将所有相同哈希地址的元素串起来,通过查看HashMap.Entry的源码可以看出是一个单链表结构:
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;
}
}
3、构造函数
//如果指定了加载因子和初始容量,就调用这个构造方法
public HashMap(int initialCapacity, float loadFactor) {
//判断合法性
if (initialCapacity < 0)
throw new IllegalArgumentException("Illegal initial capacity: " +
initialCapacity);
if (initialCapacity > MAXIMUM_CAPACITY)
initialCapacity = MAXIMUM_CAPACITY;
if (loadFactor <= 0 || Float.isNaN(loadFactor))
throw new IllegalArgumentException("Illegal load factor: " +
loadFactor);
// Find a power of 2 >= initialCapacity
//初始容量
int capacity = 1;
//确保容量为2的n次幂,使capacity为大于initialCapacity的最小的2的n次幂
while (capacity < initialCapacity)
capacity <<= 1;
this.loadFactor = loadFactor;
threshold = (int)(capacity * loadFactor);
table = new Entry[capacity];
init();
}
public HashMap(int initialCapacity) {
this(initialCapacity, DEFAULT_LOAD_FACTOR);
}
public HashMap() {
this.loadFactor = DEFAULT_LOAD_FACTOR;
threshold = (int)(DEFAULT_INITIAL_CAPACITY * DEFAULT_LOAD_FACTOR);
table = new Entry[DEFAULT_INITIAL_CAPACITY];
init();
}
4、常用方法
(1)put()
key不可重复但可以为空,value可重复可为空,插入无序。
public V put(K key, V value) {
//如果table数组为空数组,进行数组填充(为table分配实际内存空间),入参为threshold,此时threshold为initialCapacity 默认是1<<4
if (table == EMPTY_TABLE) {
inflateTable(threshold);
}
//如果key为null,存储位置为table[0]或table[0]的冲突链上
if (key == null)
return putForNullKey(value);
int hash = hash(key);//对key的hashcode进一步计算,确保散列均匀
int i = indexFor(hash, table.length);//获取在table中的实际位置
for (Entry<K,V> e = table[i]; e != null; e = e.next) {
//如果该对应数据已存在,执行覆盖操作。用新value替换旧value,并返回旧value
Object k;
if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {
V oldValue = e.value;
e.value = value;
e.recordAccess(this);
return oldValue;
}
}
modCount++;//保证并发访问时,若HashMap内部结构发生变化,快速响应失败
addEntry(hash, key, value, i);//新增一个entry
return null;
}
注:
①inflateTable方法
用于为主干数组table在内存中分配存储空间,通过roundUpToPowerOf2(toSize)可以确保capacity为大于或等于toSize的最接近toSize的二次幂,比如toSize=13,则capacity=16;to_size=16,capacity=16;to_size=17,capacity=32。
private void inflateTable(int toSize) {
int capacity = roundUpToPowerOf2(toSize);//capacity一定是2的次幂
threshold = (int) Math.min(capacity * loadFactor, MAXIMUM_CAPACITY + 1);//此处为threshold赋值,取capacity*loadFactor和MAXIMUM_CAPACITY+1的最小值,capaticy一定不会超过MAXIMUM_CAPACITY,除非loadFactor大于1
table = new Entry[capacity];
initHashSeedAsNeeded(capacity);
}
②roundUpToPowerOf2方法
使数组长度一定为2的次幂,Integer.highestOneBit用来获取最左边的bit(其他bit位为0)所代表的数值。
private static int roundUpToPowerOf2(int number) {
// assert number >= 0 : "number must be non-negative";
return number >= MAXIMUM_CAPACITY
? MAXIMUM_CAPACITY
: (number > 1) ? Integer.highestOneBit((number - 1) << 1) : 1;
}
③hash函数
用异或、移位等运算,对key的hashcode进一步计算以及并调整二进制位,保证最终获取的存储位置尽量分布均匀。
final int hash(Object k) {
int h = hashSeed;
if (0 != h && k instanceof String) {
return sun.misc.Hashing.stringHash32((String) k);
}
h ^= k.hashCode();
h ^= (h >>> 20) ^ (h >>> 12);
return h ^ (h >>> 7) ^ (h >>> 4);
}
static int indexFor(int h, int length) {
//h&(length-1)保证获取的index一定在数组范围内
return h & (length-1);
}
以上两步获取到当前元素要存储的哈希表的索引位置。
拓展:不同场景下的put
hash (通过 key 计算出的 int 值)
key (传入的 key)
value (传入的 value)
场景1:存储时 HashMap的 Node数组未初始化。
初始化数组,直接放入。
场景2:当前坐标下不存在值。
直接存放到0。
场景3:当前坐标下只有一个值并且存储的 key相等。
将旧值覆盖。
场景4:当前坐标下只有一个值并且存储的 key不相等。
将新值存到下标为0的next属性处,形成链表结构。
场景5:当前坐标下有多数值(小于8)且其中有一个值和新传入的 key相同。
替换旧值,并复制旧值的next属性给新值。
场景6:当前坐标下有多数值(小于8)且 key 都不相同。
直接接到最后面,若连完大于8则转为红黑树。
场景7:当前坐标下有多数值(大于等于8(链表长度大于等于8的时候,当前数组长度大于64时才会转换成红黑树)),数据结构已经变为红黑树了。
(2)resize()
void resize(int newCapacity) {
Entry[] oldTable = table;
int oldCapacity = oldTable.length;
if (oldCapacity == MAXIMUM_CAPACITY) {
threshold = Integer.MAX_VALUE;
return;
}
Entry[] newTable = new Entry[newCapacity];
//↓hashMap线程不安全,容易形成死循环
transfer(newTable, initHashSeedAsNeeded(newCapacity));
table = newTable;
threshold = (int)Math.min(newCapacity * loadFactor, MAXIMUM_CAPACITY + 1);
}
void transfer(Entry[] newTable, boolean rehash) {
int newCapacity = newTable.length;
//for循环中的代码,逐个遍历链表,重新计算索引位置,将老数组数据复制到新数组中去(数组不存储实际数据,所以仅仅是拷贝引用而已)
for (Entry<K,V> e : table) {
while(null != e) {
Entry<K,V> next = e.next;
if (rehash) {
e.hash = null == e.key ? 0 : hash(e.key);
}
int i = indexFor(e.hash, newCapacity);
//将当前entry的next链指向新的索引位置,newTable[i]有可能为空,有可能也是个entry链,如果是entry链,直接在链表头部插入。
e.next = newTable[i];
newTable[i] = e;
e = next;
}
}
}
(3)get()
public V get(Object key) {
//如果key为null,则直接去table[0]处去检索即可。
if (key == null)
return getForNullKey();
Entry<K,V> entry = getEntry(key);
return null == entry ? null : entry.getValue();
}
final Entry<K,V> getEntry(Object key) {
if (size == 0) {
return null;
}
//通过key的hashcode值计算hash值
int hash = (key == null) ? 0 : hash(key);
//indexFor (hash&length-1) 获取最终数组索引,然后遍历链表,通过equals方法比对找出对应记录
for (Entry<K,V> e = table[indexFor(hash, table.length)];
e != null;
e = e.next) {
Object k;
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
return e;
}
return null;
}
操作过程:通过key算出存的位置然后查询。
(4)remove()
public V remove(Object key) {
//临时变量
Node<K,V> e;
/**调用removeNode(hash(key), key, null, false, true)进行删除,第三个value为null,表示,把key的节点直接都删除了,不需要用到值,如果设为值,则还需要去进行查找操作**/
return (e = removeNode(hash(key), key, null, false, true)) == null ?
null : e.value;
}
/**第一参数为哈希值,第二个为key,第三个value,第四个为是为true的话,则表示删除它key对应的value,不删除key,第四个如果为false,则表示删除后,不移动节点**/
final Node<K,V> removeNode(int hash, Object key, Object value,
boolean matchValue, boolean movable) {
//tab 哈希数组,p 数组下标的节点,n 长度,index 当前数组下标
Node<K,V>[] tab; Node<K,V> p; int n, index;
//哈希数组不为null,且长度大于0,然后获得到要删除key的节点所在是数组下标位置
if ((tab = table) != null && (n = tab.length) > 0 &&
(p = tab[index = (n - 1) & hash]) != null) {
//nodee 存储要删除的节点,e 临时变量,k 当前节点的key,v 当前节点的value
Node<K,V> node = null, e; K k; V v;
//如果数组下标的节点正好是要删除的节点,把值赋给临时变量node
if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k))))
node = p;
//也就是要删除的节点,在链表或者红黑树上,先判断是否为红黑树的节点
else if ((e = p.next) != null) {
if (p instanceof TreeNode)
//遍历红黑树,找到该节点并返回
node = ((TreeNode<K,V>)p).getTreeNode(hash, key);
else { //表示为链表节点,一样的遍历找到该节点
do {
if (e.hash == hash &&
((k = e.key) == key ||
(key != null && key.equals(k)))) {
node = e;
break;
}
/**注意,如果进入了链表中的遍历,那么此处的p不再是数组下标的节点,而是要删除结点的上一个结点**/
p = e;
} while ((e = e.next) != null);
}
}
//找到要删除的节点后,判断!matchValue,我们正常的remove删除,!matchValue都为true
if (node != null && (!matchValue || (v = node.value) == value ||
(value != null && value.equals(v)))) {
//如果删除的节点是红黑树结构,则去红黑树中删除
if (node instanceof TreeNode)
((TreeNode<K,V>)node).removeTreeNode(this, tab, movable);
//如果是链表结构,且删除的节点为数组下标节点,也就是头结点,直接让下一个作为头
else if (node == p)
tab[index] = node.next;
else /**为链表结构,删除的节点在链表中,把要删除的下一个结点设为上一个结点的下一个节点**/
p.next = node.next;
//修改计数器
++modCount;
//长度减一
--size;
/**此方法在hashMap中是为了让子类去实现,主要是对删除结点后的链表关系进行处理**/
afterNodeRemoval(node);
//返回删除的节点
return node;
}
}
//返回null则表示没有该节点,删除失败
return null;
}
(5)元素修改
也是使用put()方法,因为key是唯一的,所以修改元素,可以通过传入新值将旧值覆盖。
5、应用
用HashMap统计第一个出现重复的数据和重复次数最多的数据。
import java.util.*;
public class CountNumber {
public static void main(String[] args) {
// 初始化数据
int cap = 100;
ArrayList<Integer> arrayList = new ArrayList<Integer>(cap);
Random random = new Random();
for (int i = 0; i < cap; i++) {
arrayList.add(random.nextInt(101)); //(0,1000)
}
//统计出现的次数
//用key存arrayList数据,用value存出现次数
HashMap<Integer, Integer> hashMap = new HashMap<Integer, Integer>();
Iterator<Integer> iterator = arrayList.iterator();
while (iterator.hasNext()) {
Integer value = iterator.next();
if (hashMap.containsKey(value)) {
hashMap.put(value, hashMap.get(value) + 1);
} else {
hashMap.put(value, 1);
}
}
//打印
Iterator<Map.Entry<Integer, Integer>> iterator1 = hashMap.entrySet().iterator();
while (iterator1.hasNext()) {
Map.Entry<Integer, Integer> entry = iterator1.next();
Integer key = entry.getKey();
Integer value = entry.getValue();
System.out.println(key + ":" + value);
}
//寻找第一个重复的数据
Iterator<Map.Entry<Integer, Integer>> iterator2 =hashMap.entrySet().iterator();
for (int i = 0; i < hashMap.size(); i++) {
Map.Entry<Integer, Integer> entry = iterator2.next();
if (entry.getValue() > 1) {
System.out.println("第一个重复的数据是" + entry.getKey() + ",重复了" + entry.getValue() + "次");
}
break;
}
//找出最大重复次数对应的数据
Iterator<Map.Entry<Integer, Integer>> iterator3 = hashMap.entrySet().iterator();
int key = 0;
int max = 0;
while (iterator3.hasNext()) {
Map.Entry<Integer, Integer> entry = iterator3.next();
// if (key == 0) {
// key = entry.getKey();
// max = entry.getValue();
// } else {
if (entry.getValue() > max) {
key = entry.getKey();
max = entry.getValue();
}
}
// }
System.out.println(key + "重复最多,重复了" + max + "次");
}
}
6、HashMap的优点
(1)HashMap采用了数组和链表的数据结构,能在查询和修改方便继承了数组的线性查找和链表的寻址修改。
(2)HashMap可以接受键和值null,而Hashtable则不能(原因就是equlas()方法需要对象,因为HashMap是后出的API经过处理才可以)
7、HashMap的工作原理
(1)将键值对传递给put()方法时调用键对象的hashCode()方法来计算hashcode,找到bucket位置来储存值对象。
(2)获取对象时,通过键对象的equals()方法找到正确的键值对,然后返回值对象。HashMap使用链表来解决碰撞问题,当发生碰撞了,对象将会储存在链表的下一个节点中。 HashMap在每个链表节点中储存键值对对象。
二、Hashtable
Hashtable是一个散列表,存储键值对(key-value)映射,且映射不是有序的。Hashtable函数是同步的,所以它是线程安全的。
1、继承关系
extends Dictionary<K,V>
implements Map<K,V>, Cloneable, java.io.Serializable
2、属性
//table:Entry[]数组类型
//每一个Entry代表了一个键值对,哈希表的key-value键值对都存储在Entry数组中
private transient HashtableEntry table[];
//Hashtable所包含Entry键值对的数量
private transient int count;
//阈值,用于判断是否需要调整Hashtable的容量
//阈值=容量×加载因子
//初始容量和加载因子影响其性能。
//容量是哈希表中桶的数量,初始容量是哈希表创建时的容量
private int threshold;
//加载因子
//衡量哈希表在其容量自动增加之前可以达到多满的尺度
//默认0.75,加载因子如果过高虽然减少了空间开销,但同时也增加了查找某个条目的时间
private float loadFactor;
注:Entry数组里的属性有hash、prev、next、value,该Entry数组采用哈希表的存储结构,采用链地址法解决哈希冲突。
3、构造函数
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);
initHashSeedAsNeeded(initialCapacity);
}
/**
* Constructs a new, empty hashtable with the specified initial capacity
* and default load factor (0.75).
*
* @param initialCapacity the initial capacity of the hashtable.
* @exception IllegalArgumentException if the initial capacity is less
* than zero.
*/
public Hashtable(int initialCapacity) {
this(initialCapacity, 0.75f);
}
/**
* Constructs a new, empty hashtable with a default initial capacity (11)
* and load factor (0.75).
*/
public Hashtable() {
this(11, 0.75f);
}
/**
* Constructs a new hashtable with the same mappings as the given
* Map. The hashtable is created with an initial capacity sufficient to
* hold the mappings in the given Map and a default load factor (0.75).
*
* @param t the map whose mappings are to be placed in this map.
* @throws NullPointerException if the specified map is null.
* @since 1.2
*/
public Hashtable(Map<? extends K, ? extends V> t) {
this(Math.max(2*t.size(), 11), 0.75f);
putAll(t);
}
4、常用方法
(1)put()
可以看出key、value都不可以null,且key重复会覆盖旧值,value可以重复。
`public synchronized V put(K key, V value) {
// Make sure the value is not null
if (value == null) {//value不可为null
throw new NullPointerException();
}
// Makes sure the key is not already in the hashtable.
Entry tab[] = table;
int hash = hash(key);
int index = (hash & 0x7FFFFFFF) % tab.length;
for (Entry<K,V> e = tab[index] ; e != null ; e = e.next) {
if ((e.hash == hash) && e.key.equals(key)) {
V old = e.value;
e.value = value;
return old;
}
}
modCount++;
if (count >= threshold) {
// Rehash the table if the threshold is exceeded
rehash();
tab = table;
hash = hash(key);
index = (hash & 0x7FFFFFFF) % tab.length;
}
// Creates the new entry.
Entry<K,V> e = tab[index];
tab[index] = new Entry<>(hash, key, value, e);
count++;
return null;
}
private int hash(Object k) {
// hashSeed will be zero if alternative hashing is disabled.
return hashSeed ^ k.hashCode();//此处可证明key不可为null,否则会抛空指针异常
}
操作过程:计算key的hash值,确认在table[]中的索引位置,迭代index索引位置,如果该位置处的链表中存在一个一样的key,则替换其value,返回旧值;不存在则将该key-value节点插入该index索引位置处。
注:
①synchronized关键字表示这个方法加锁,当它锁定一个方法或者一个代码块的时候,同一时刻最多只有一个线程可以执行这段代码。即例如一个线程A运行到这个方法时都要检查有没有其它线程B正在用这个方法(或者该类的其他同步方法),有的话要等正在使用synchronized方法的线程B运行完这个方法后再运行此线程A,没有的话,锁定调用者,然后直接运行。
该关键字表明同一时刻只能有一个线程访问Hashtable实例,所以Hashtable是线程安全的。
②计算hash值时,HashMap重新计算了key的hash值,而Hashtable直接用key的hashCode();
求对应的位置索引时,HashMap在求位置索引时,则用与运算,而Hashtable用取模运算,且一般先用hash&0x7FFFFFFF后,再对length取模。(&0x7FFFFFFF的目的是为了将负的hash值转化为正值,因为hash值有可能为负数,而&0x7FFFFFFF后,只有符号外改变,而后面的位都不变)
(2)remove()
public synchronized V remove(Object key) {
Entry tab[] = table;
int hash = hash(key);
int index = (hash & 0x7FFFFFFF) % tab.length;
for (Entry<K,V> e = tab[index], prev = null ; e != null ; prev = e, e = e.next) {
if ((e.hash == hash) && e.key.equals(key)) {
modCount++;
if (prev != null) {
prev.next = e.next;
} else {
tab[index] = e.next;
}
count--;
V oldValue = e.value;
e.value = null;
return oldValue;
}
}
return null;
}
(3)get()
public synchronized V get(Object key) {
Entry tab[] = table;
int hash = hash(key);
int index = (hash & 0x7FFFFFFF) % tab.length;
for (Entry<K,V> e = tab[index] ; e != null ; e = e.next) {
if ((e.hash == hash) && e.key.equals(key)) {
return e.value;
}
}
return null;
}
三、HashMap和Hashtable
1、相同点
①都是存储键值对的散列表。
②都是通过table数组存储,数组的每一个元素都是一个Entry;一个Entry就是一个单向链表,Entry链表中的每一个节点就保存了key-value键值对数据。
③添加key-value键值对时:
先根据key计算出哈希值,再计算出数组索引;
然后根据数组索引找到Entry,再遍历单向链表,将key和链表中的每一个节点的key进行对比;
若key已存在于Entry链表中,则用该value值取代旧value值;
若key不存在,则新建一个key-value节点,并将该节点插入Entry链表的表头位置。
④删除key-value键值对时:
先根据key计算出哈希值,再计算出数组索引;
然后根据索引找出Entry;
若节点key-value存在与链表Entry中,则删除链表中的节点。
⑤都实现了Map、Cloneable、java.io.Serializable接口。
2、不同点
①继承和实现方式不同
HashMap继承于AbstractMap;
Hashtable继承于Dictionary。
②线程安全不同
HashMap的函数是非同步的,不是线程安全的;
Hashtable几乎所有函数都是同步的,它是线程安全的,支持多线程,主要是通过在方法上加关键字synchronized。
③key和value是否可以null值
HashMap的key、value都可以为null;
Hashtable的key、value都不可以为null。
注:如果在Hashtable中有put(null,null)操作,编译同样可以通过,因为key和value都是Object类型,但运行时会抛出NullPointerException异常。
④遍历方式不同
HashMap只支持Iterator遍历;
Hashtable支持Iterator和Enumeration两种方式。
并且通过迭代器遍历时,HashMap是从前往后,Hashtable是从后往前。
⑤添加元素时的hash值算法不同
HashMap添加元素时,是使用自定义的哈希算法;
Hashtable没有自定义哈希算法,而直接采用的key的hashCode()。
⑥初始不同
HashMap默认容量大小是16,扩容时“原始容量x2”;
Hashtable默认的容量大小是11,增加容量时,每次将容量变为“原始容量x2+1”。
注:HashTable在不指定容量的情况下的默认容量为11,而HashMap为16,Hashtable不要求底层数组的容量一定要为2的整数次幂,而HashMap则要求一定为2的整数次幂。
⑦效率
单线程情况下HashMap效率高。
⑧扩容
HashMap扩容时2倍table.length;
Hashtable扩容时2倍table.length+1。
四、LinkedHashMap
LinkedHashMap是Map接口的哈希表和链接列表实现,具有可预知的迭代顺序。此实现提供所有可选的映射操作,并允许使用null值和null键。此类不保证映射的顺序,特别是它不保证该顺序恒久不变。
1、继承关系
extends HashMap<K,V>
implements Map<K,V>
2、关于插入有序
private static class Entry<K,V> extends HashMap.Entry<K,V> {
// These fields comprise the doubly linked list used for iteration.
Entry<K,V> before, after;
Entry(int hash, K key, V value, HashMap.Entry<K,V> next) {
super(hash, key, value, next);
}
/**
* Removes this entry from the linked list.
*/
private void remove() {
before.after = after;
after.before = before;
}
/**
* Inserts this entry before the specified existing entry in the list.
*/
private void addBefore(Entry<K,V> existingEntry) {
after = existingEntry;
before = existingEntry.before;
before.after = this;
after.before = this;
}
/**
* This method is invoked by the superclass whenever the value
* of a pre-existing entry is read by Map.get or modified by Map.set.
* If the enclosing Map is access-ordered, it moves the entry
* to the end of the list; otherwise, it does nothing.
*/
void recordAccess(HashMap<K,V> m) {
LinkedHashMap<K,V> lm = (LinkedHashMap<K,V>)m;
if (lm.accessOrder) {
lm.modCount++;
remove();
addBefore(lm.header);
}
}
void recordRemoval(HashMap<K,V> m) {
remove();
}
}
LinkedHashMap.Entry继承自HashMap.Node,除了Node本身有的属性外,还额外增加了before、after用于指向前一个Entry、后一个Entry。即元素之间维持着一条总的链表数据结构,也因此保证了LinkedHashMap的有序性。
3、常用方法
(1)put()
public V put(K key, V value) {
if (table == EMPTY_TABLE) {
inflateTable(threshold);
}
if (key == null)
return putForNullKey(value);
int hash = hash(key);
int i = indexFor(hash, table.length);
for (Entry<K,V> e = table[i]; e != null; e = e.next) {
Object k;
if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {
V oldValue = e.value;
e.value = value;
e.recordAccess(this);//this代表一个集合对象
return oldValue;
}
}
modCount++;
addEntry(hash, key, value, i);
return null;
}
//父类的实现
void addEntry(int hash, K key, V value, int bucketIndex) {
if ((size >= threshold) && (null != table[bucketIndex])) {
resize(2 * table.length);
hash = (null != key) ? hash(key) : 0;
bucketIndex = indexFor(hash, table.length);
}
createEntry(hash, key, value, bucketIndex);
}
//子类的实现
void createEntry(int hash, K key, V value, int bucketIndex) {
HashMap.Entry<K,V> old = table[bucketIndex];
Entry<K,V> e = new Entry<>(hash, key, value, old);
table[bucketIndex] = e;
e.addBefore(header);
size++;
}
//子类的实现
private void addBefore(Entry<K,V> existingEntry) {
after = existingEntry;
before = existingEntry.before;
before.after = this;
after.before = this;
}
//子类的实现
void addEntry(int hash, K key, V value, int bucketIndex) {
super.addEntry(hash, key, value, bucketIndex);
// Remove eldest entry if instructed
Entry<K,V> eldest = header.after;
if (removeEldestEntry(eldest)) {
removeEntryForKey(eldest.key);
}
}
当put元素时,不但把它加入到HashMap中去,还要加入到双向链表中,所以LinkedHashMap就是HashMap+双向链表,header是一个Entry类型的双向链表表头,本身不存储数据。
(2)get()
//accessOrder:true按访问顺序组织数据,false:按插入顺序,默认值false
LinkedHashMap<Integer,Integer> linkedHashMap=new LinkedHashMap<Integer,Integer>(16,0.75f,true);
4、使用场景
LinkedHashMap是HashMap的一个子类,保留了插入的顺序,如果需要输出的顺序和输入时的相同,那么就选用LinkedHashMap。
存储的数据是键值对,并且需要按顺序存储。
5、与HashMap比较
(1)LinkedHashMap维护着一个运行于所有条目的双重链接列表。此链接列表定义了迭代顺序,该迭代顺序可以是插入顺序或者是访问顺序。根据链表中元素的顺序可以分为:按插入顺序的链表,和按访问顺序(调用get方法)的链表。默认是按插入顺序排序,如果指定按访问顺序排序,那么调用get方法后,会将这次访问的元素移至链表尾部,不断访问可以形成按访问顺序排序的链表。
注意,此实现不是同步的。如果多个线程同时访问链接的哈希映射,而其中至少一个线程从结构上修改了该映射,则它必须保持外部同步。
(2)由于LinkedHashMap需要维护元素的插入顺序,因此性能略低于HashMap的性能,但在迭代访问Map里的全部元素时将有很好的性能,因为它以链表来维护内部顺序。
五、TreeMap
1、继承关系
extends AbstractMap<K,V>
implements NavigableMap<K,V>, Cloneable, java.io.Serializable
2、重点
(1)TreeMap是一个有序的key-value集合,基于红黑树实现,每一个key-value作为红黑树的一个节点。
(2)TreeMap存储时会根据key对key-value键值对进行排序,排序方式分自然排序、定制排序两种。
注:
自然排序:TreeMap中所有的key必须实现Comparable接口,并且所有的key都应该是同一个类的对象,否则会报ClassCastException异常。
定制排序:定义TreeMap时,创建一个comparator对象,该对象对所有的treeMap中所有的key值进行排序,采用定制排序的时候不需要TreeMap中所有的key必须实现Comparable接口。
(3)TreeMap判断两个元素相等的标准:两个key通过equals()方法返回为true,并且通过compareTo()方法比较应该返回为0。因此在使用自定义的类作为TreeMap中的key值时,则要重写自定义类中的equals()方法。
六、WeakHashMap
1、继承关系
extends AbstractMap<K,V>
implements Map<K,V>
2、重点
插入的数据会随着不断的后续的插入渐渐消失,本质是数据被垃圾回收机制回收,使用到弱引用。
3、拓展
关于java的四种引用——强引用、弱引用、软引用、虚引用
(1)强引用StrongReference<>
垃圾回收器不会回收,且内存不足时,jvm即使抛异常也不回收。
例:通过new创建对象的引用。
(2)软引用SoftReference<>
垃圾回收器在若内存不足时回收,充足时不回收。
(3)弱引用WeakReference<>
无论内存是否充足,只要触发垃圾回收,该引用作用的对象都会被回收。
(4)虚引用PhantomReference<>
并不决定对象的回收周期,如果一个对象持有虚引用,在任何时候都有可能被回收。