HashMap
引言:我一直有一个困惑?
- 数组怎么存数据的?
开辟一个 N * 类型所占内存 的内存空间,然后每次访问的时候就 数组起始地址 + i * 类型所占内存空间 这样就可以访问到指定的元素了
这样访问是快了,但是一旦要插入删除,都要改变数组大小,改变数组结构。你插入在中间,如果这个数组满了,还得扩容(创建一个新的更大的数组),然后在放进去,如果没满,那这个位置以及后面的元素都得动动,给它挪位置。是吧,划不来。
于是就有另一种结构。
2. 链表是怎么存数组的?
它不是一个连续的内存空间,他可以利用零碎的空间,反正只要比我要存的类型大就行。
那怎么访问每个元素呢?
存的时候第一个链表节点中,有一个属性指向下一个节点,每个都这样,就连在了一起。并且只有这个节点的只有这个节点的上一个元素知道它在哪(以单向链表为例)?因为不是存在一个连续空间的不能直接通过地址运算得出元素所在位置。
他有什么特点?
如果要删除一个元素,它不需要后面的元素都给它挪位置,只要让它的上一个节点指向下一个节点就行了。
所以删除和插入就比数组性能好了,但是访问慢了呀,没办法直接通过地址计算位置,那就要从头到尾去找。
总结一下,数组访问快,链表删除插入快。
为什么就不能找个折中的结构呢?
把数组和链表组合起来,能不能创造一个折中的结构呢?
答案就是能,HashTable,哈希表!!
HashMap<K,V>
**特点:**和map一样:
- 存储任意键值对
- 键:无序,无下标,不予许重复(唯一,有就覆盖)
- 值:无序,无下标,允许重复。
1. 实现原理
HashMap底层就是一个Hash表的结构;口说无凭,看源码。
怎么看呢?HashMap的源码看得头皮发麻!!
我们先确定它是不是Hash表实现的,就看它存数据取数据是不是用的链表加数组的结构
- 先去找它的属性里面有没有数组结构的:好,有!这真的是一个链表数组吗?去看看Node。
transient Node<K,V>[] table;
- Node<K,V>看着就是链表,存的一个hash,一个key,一个value,还有一个指向下一个节点的元素。
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;
}
- 去看它的构造器,你创建一个对象肯定会体现它的结构嘛。
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);
}
/**
* Constructs an empty <tt>HashMap</tt> with the default initial capacity
* (16) and the default load factor (0.75).
*/
public HashMap() {
this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted
}
/**
* Constructs a new <tt>HashMap</tt> with the same mappings as the
* specified <tt>Map</tt>. The <tt>HashMap</tt> is created with
* default load factor (0.75) and an initial capacity sufficient to
* hold the mappings in the specified <tt>Map</tt>.
*
* @param m the map whose mappings are to be placed in this map
* @throws NullPointerException if the specified map is null
*/
public HashMap(Map<? extends K, ? extends V> m) {
this.loadFactor = DEFAULT_LOAD_FACTOR;
putMapEntries(m, false);
}
无参构造器就把负载因子赋了一个默认值;
一参构造器调用了指定初始容量和和负载因子的二参构造器;
指定初始容量和和负载因子的二参构造器,做个判断,把负载因子赋给了loadFactor,再赋了一个threshold,也和数据没有关系;
还有一个传入Map类型的构造器,合着是套娃,还是没有看到数据结构的影子。
合着四个构造器都没有涉及到数据结构。
- 看put();
public V put(K key, V value) {
return putVal(hash(key), key, value, false, true);
}
/**
* Implements Map.put and related methods.
*
* @param hash hash for key
* @param key the key
* @param value the value to put
* @param onlyIfAbsent if true, don't change existing value
* @param evict if false, the table is in creation mode.
* @return previous value, or null if none
*/
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
boolean evict) {
Node<K,V>[] tab; Node<K,V> p; int n, i;
if ((tab = table) == null || (n = tab.length) == 0)
n = (tab = resize()).length;
if ((p = tab[i = (n - 1) & hash]) == null)
tab[i] = newNode(hash, key, value, null);
else {
Node<K,V> e; K k;
if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k))))
e = p;
else if (p instanceof TreeNode)
e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
else {
for (int binCount = 0; ; ++binCount) {
if ((e = p.next) == null) {
p.next = newNode(hash, key, value, null);
if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
treeifyBin(tab, hash);
break;
}
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
break;
p = e;
}
}
if (e != null) { // existing mapping for key
V oldValue = e.value;
if (!onlyIfAbsent || oldValue == null)
e.value = value;
afterNodeAccess(e);
return oldValue;
}
}
++modCount;
if (++size > threshold)
resize();
afterNodeInsertion(evict);
return null;
}
put方法是调用了自己的一个常量方法,final V putVal,其中使用的就是 Node<K,V>[] tab;这就是一个数组链表。
2.哈希表原理
前面说了哈希表是一个数组链表,那它到底是怎么存储数据的呢?
先要解释什么是哈希运算?简单来说,假设存入的都是数值,并且数组长度为N,在存入前,对该数值做一次哈希运算,得出来的数就是在0~N-1之间的一个值,也就是数组的下标。然后把这个值存入数组中对应的位置。
hash表中的元素是一个链表的节点。这个节点中会有一个数字,用来做hash运算,确定该存放在哪个下标中。
一个下标只能存一个元素,这是数组的特点,当两个元素存入同一个下标之后,新存入的下标只要放在当前元素的next指向的位置就好了。这样就实现了数据的存入。
取呢?首先访问去哈希,找到是哪个下标,然后在去遍历这个下标对应的链表。这样就实现了去。
实现hash表最重要的是哈希算法,最简单的取哈希的方式就是取余,这样就可以保证一定会在数组范围内了,但是这样可能会导致数据不均匀,也就是说可能有的下标下面一个元素都没有,有的下标下面的链表很长。哈希算法的关键是让数据尽可能的分布均匀。
这样一个结构的访问时间复杂度和链表的O(n)相比就是O(log(n))了,插入删除的时间复杂度和数组的O(n)相比就是O(log(n))了。
这有什么缺点呢?我觉得哈,主要是复杂!!哈哈
不过也不是很复杂。
3. 方法
Hashmap的方法几乎都是Map的。
1.存取
put() 和 get(), get是通过K get V的。
2.遍历
- Set keySet(); 很明显就是返回一个Set这个Set是一个包含当前HashMap中所有Key的Set
啊?为什么要用Set,因为Set是不可重复的无序的啊,Key也是不可重复的无序的啊,前面的文章中也说了,HashSet底层就是一个HashMap,它只是把这个HashMap封装起来了,只操作Key就成了一个Set。
它就设计了一个HashMap就产生了几种集合好聪明啊,有没有? - Collection values() ;返回一个Collection集合,这个集合中装的是的当前HashMap的所有Value。
啊?为什么用Collection啊,因为V是可重复的同时也是无序的,用不了Set也用不了List那就只有Collection了 - Set<Map.Entry<K,V>> entrySet() ;返回一个Set,这个Set是存放当前所有键值对的Set。
Set不是只能存一个元素吗?怎么存键值对?是把他们拼接之后存入的。
重头戏
Map.Entry 是个什么类型?
前面看源码的时候就注意到了,HashMap中除了有一个链表数组之外,还有一个
/**
* Holds cached entrySet(). Note that AbstractMap fields are used
* for keySet() and values().
*/
transient Set<Map.Entry<K,V>> entrySet;
这是一个类型为Map.Entry的Set,点进去发现,Entry是一个Map的一个内部接口。
那它是干嘛的呢?
HashMap 里的一个内部类继承了一个AbstractSet类,AbstractSet类实现了Map的Entry接口。
它里面一个属性都没有,我想要的是HashMap的键值对,它怎么什么都没给我?
实际上,EntrySet是直接操作Hashmap的属性,并且EntrySet用final修饰,也就是说一个EntrySet对象对应一个HashMap。
换而言之,对EntrySet对象的操作和直接对当前HashMap的对象的操作效果是一样的。
final class EntrySet extends AbstractSet<Map.Entry<K,V>> {
public final int size() { return size; }
public final void clear() { HashMap.this.clear(); }
public final Iterator<Map.Entry<K,V>> iterator() {
return new EntryIterator();
}
public final boolean contains(Object o) {
if (!(o instanceof Map.Entry))
return false;
Map.Entry<?,?> e = (Map.Entry<?,?>) o;
Object key = e.getKey();
Node<K,V> candidate = getNode(hash(key), key);
return candidate != null && candidate.equals(e);
}
public final boolean remove(Object o) {
if (o instanceof Map.Entry) {
Map.Entry<?,?> e = (Map.Entry<?,?>) o;
Object key = e.getKey();
Object value = e.getValue();
return removeNode(hash(key), key, value, true, true) != null;
}
return false;
}
public final Spliterator<Map.Entry<K,V>> spliterator() {
return new EntrySpliterator<>(HashMap.this, 0, -1, 0, 0);
}
public final void forEach(Consumer<? super Map.Entry<K,V>> action) {
Node<K,V>[] tab;
if (action == null)
throw new NullPointerException();
if (size > 0 && (tab = table) != null) {
int mc = modCount;
for (int i = 0; i < tab.length; ++i) {
for (Node<K,V> e = tab[i]; e != null; e = e.next)
action.accept(e);
}
if (modCount != mc)
throw new ConcurrentModificationException();
}
}
}
回过头来去想,这是什么回事。HashMap中定义了一个内部类,这个内部类的作用就是对HashMap的操作。
这可能就是设计的魅力吧,还需要慢慢理解,我现在是觉得何必呢?慢慢学吧。
Hashtable
这个t是小写哦
一笔带过吧,其实和HashMap一样,操作都差不多,底层也是哈希表。
区别就是: Hashtable线程安全,慢。
还有一个小区别,HashMap的K 和 V 都是可以为null的(这也是坑啊,使用时要判空)
Hashtable的V和K不能为null;
TreeMap
它实现了SortedMap,是有序的,它的有序是指的key。当然还有一点K的类一样要实现Comparable方法。