学习笔记之HashMap、HashTable、HashSet的区别
1. HashMap与HashTable的区别
1.1 线程是否安全:
- HashMap 是⾮线程安全的
- HashTable 是线程安全的,
- 因为 HashTable 内部的⽅法基本都经过 synchronized 修饰。
- 如果你要保证线程安全的话可以使用ConcurrentHashMap ,具体可以参考ArrayList、HashSet、HashMap是线程不安全
1.2 效率:
- 因为HashTable通过synchronized 保证了线程安全,所以从效率上看 ,HashMap 要⽐ HashTable 效率⾼⼀点。另外, HashTable现在基本被淘汰(不要在代码中使⽤它);
1.3 对 Null key 和 Null value 的⽀持:
-
HashMap 可以存储 null 的 key 和 value,但 null 作为键(key)只能有⼀个,null 作为值(value)可以有多个;
-
HashTable 不允许有 null 键(key)和 null 值(key),否则会抛出NullPointerException 。
Exception in thread "main" java.lang.NullPointerException
1.4 初始容量⼤⼩和每次扩充容量⼤⼩的不同 :
-
① 创建时如果不指定容量初始值, Hashtable默认的初始⼤⼩为 11,之后每次扩充,容量变为原来的 2n+1。 HashMap 默认的初始化⼤小为 16。之后每次扩充,容量变为原来的 2 倍。
-
② 创建时如果给定了容量初始值,那么Hashtable 会直接使⽤你给定的⼤⼩,⽽ HashMap 会将其扩充为 2 的幂次⽅⼤⼩。也就是说 HashMap 总是使⽤ 2 的幂作为哈希表的⼤⼩。
/**
* Constructs an empty <tt>HashMap</tt> with the specified initial
* capacity and load factor.
*
* @param initialCapacity the initial capacity
* @param loadFactor the load factor
* @throws IllegalArgumentException if the initial capacity is negative
* or the load factor is nonpositive
*/
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);
this.loadFactor = loadFactor;
this.threshold = tableSizeFor(initialCapacity);
}
/**
* Constructs an empty <tt>HashMap</tt> with the specified initial
* capacity and the default load factor (0.75).
*
* @param initialCapacity the initial capacity.
* @throws IllegalArgumentException if the initial capacity is negative.
*/
public HashMap(int initialCapacity) {
this(initialCapacity, DEFAULT_LOAD_FACTOR);
}
下⾯这个⽅法保证了 HashMap 总是使⽤2的幂作为哈希表的⼤⼩。
/**
* Returns a power of two size for the given target capacity.
*/
static final int tableSizeFor(int cap) {
int n = cap - 1;
n |= n >>> 1;
n |= n >>> 2;
n |= n >>> 4;
n |= n >>> 8;
n |= n >>> 16;
return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1;
}
1.5 底层数据结构:
- JDK1.8 以后的 HashMap 在解决哈希冲突时有了⼤的变化,当链表⻓度⼤于阈值(默认为 8)(不过将链表转换成红⿊树前会判断,如果当前数组的⻓度⼩于 64,那么会选择先进⾏数组扩容,⽽不是转换为红⿊树)时,将链表转化为红⿊树,以减少搜索时间。
- 而Hashtable 没有这样的机制。
2. HashMap与HashSet的区别
HashSet 底层就是基于 HashMap 实现的。( HashSet 的源码⾮常⾮常少,因为除了 clone() 、 writeObject() 、 readObject() 是 HashSet⾃⼰不得不实现之外,其他⽅法都是直接调⽤ HashMap 中的⽅法。
2.1 接口
- HashMap实现了Map接口,HashSet实现了Set接口
public class HashMap<K,V> extends AbstractMap<K,V>
implements Map<K,V>, Cloneable, Serializable
public class HashSet<E> extends AbstractSet<E>
implements Set<E>, Cloneable, java.io.Serializable
HashSet实现了Set接口,其内部不允许出现重复的值,如果我们将一个对象存入HashSet,必须重写equals()和hashCode()方法,这样才能确保集合中不存在同一个元素。HashSet的内部是无序的,因此不能使用 hashset.get(index) 来获取元素。
HashMap实现了Map接口,其内容是键值对的映射(key->value),不允许出现相同的键(key)。在查询的时候会根据给出的键来查询对应的值。
我们可以认为,HashSet和HashMap增查操作的时间复杂度都是常数级的。
2.2 存储值的方式
- HashMap以key,value的形式存储对象,HashSet仅以value的形式存储存储
2.3 添加元素的方式
- HashMap调⽤ put() 向 map 中添加元素,HashSet调⽤ add() ⽅法向 Set 中添加元素(不过底层调用的还是HashMap的put方法)
//HashMap的:
public V put(K key, V value) {
return putVal(hash(key), key, value, false, true);
}
//HashSet的:
public boolean add(E e) {
return map.put(e, PRESENT)==null;
}
2.4 HashCode计算的选取值
- HashMap 使⽤键(Key)计算HashCode;
- HashSet 使⽤成员对象来计算 HashCode值,对于两个对象来说HashCode可能相同,所以 equals() ⽅法⽤来判断对象的相等性,如果两个对象不同的话,那么返回false。
2.5 存储对象的过程
2.5.1 HashMap存储对象的过程
- 对HashMap的Key调用hashCode方法,返回int值,即对应的hashCode;
- 把此HashCode作为哈希表的索引,查询哈希表的相应位置,若当前位置内容为null,则把HashMap的Key、Value包装成Entry数组,放入当前位置;
- 若当前位置内容不为空,则继续查询当前索引存放的链表,利用equals方法,找到Key相对应的Entry数组,则用当前Value去替换旧的Value;
- 若未找到与当前Key值相同的对象,则把当前位置的链表后移(Entry数组持有一个指向下一个元素的引用),把新的Entry数组放在链表表头。
2.5.2 HashSet存储对象的过程
- 往HashSet添加元素的时候,HashSet会调用元素的hashCode方法得到元素的哈希值;
- 然后通过元素的哈希值经过移位等运算,就可以算出该元素在哈希表的存储位置,存储时会出现两种情况:
- 情况1:如果算出元素存储位置目前没有任何元素存储,那么该元素可以直接存储到该位置上。
- 情况2:如果算出该元素的存储位置目前已经存在有其他的元素,那么会调用该元素的equals方法与该位置的元素再比较一次,如果equals返回的是true,那么该元素与这个位置上的元素就视为重复元素,不需要添加,如果equals方法返回的是false,那么该元素运行添加。