HashMap 是 java 中用来存储 key-value 键值对的一种容器,其中的 key 和 value 都容许为 null。其底层的数据结构为数组 + 链表 + 红黑树,当链表长度达到 TREEIFY_THRESHOLD = 8 时,该链表会自动转化为红黑树,以提高 HashMap 的查询、插入效率,它实现了 Map<K, V>,Cloneable,Serializable接口。java
三个最主要的构造函数数组
public HashMap() { this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted } public HashMap(int initialCapacity) { this(initialCapacity, DEFAULT_LOAD_FACTOR); } 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); }
若是使用默认构造函数,则 HashMap 的初始化容量为 1 << 4 也就是 16,默认加载因子是 DEFAULT_LOAD_FACTOR = 0.75f,当 HashMap 底层的数组元素个数 > 数组容量 * 加载因子时,HashMap 将进行扩容操做,固然也能够在初始化时给定一个 loadFactor。若是在初始化时给定 initialCapacity,则初始化容量 C 需知足:C 是 2 的幂次方且 C >= initialCapacity 且 C <= (1 << 30)。其中的 tableSizeFor 方法保证函数返回值是大于等于给定参数 initialCapacity 最小的 2 的幂次方的数值,具体为:数据结构
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; }
参考app
public V put(K key, V value) { return putVal(hash(key), key, value, false, true); }
static final int hash(Object key) { int h; return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16); }
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; }
向一个 HashMap 中添加一个元素,大体流程以下:函数
假设 capacity = 10000,则 index = 1111 & hash。扩容为二倍后,capacity = 100000,则 index = 11111 & hash。扩容后 capacity - 1 的低四位没有变,而仅仅是多了一个最高位,而这个最高位(从右往左第五位)相对应的 hash 值的第五位只有 0 或 1 两种可能:若是为 0,则 index 不变;若是为 1,则 index = 原下标 + 原容量。post
扩容时,会新建一个哈希数组,而后将原来数组中的每一个元素移过来,这是一个很是耗时的操做。因此若是咱们预先知道了有多少个键值对,那么在初始化时咱们就能够给定一个容量,这样就能够减小 HashMap 扩容所带来的消耗。例如,若是咱们知道键值对大概有 1000 个,那么就能够获得 1000 / 0.75 ≈ 1333,比 1333 大且为 2 的幂次方的最小数是 2048。可是若是咱们将 HashMap 的初始化容量设置为 2048 就会可能会出现空间浪费的状况。由于当把一个键值对添加到 HashMap 时,可能有不少键值对都会发生哈希冲突,而后他们将会以链表或红黑树的方式链接到哈希数组中,因此哈希数组中不为空的元素不必定为 1000,可能为 200,也有多是 300。因此在给定 HashMap 初始换容量时,不只要考虑键值对的数量,还要考虑这些键值对发生哈希冲突的几率等等。this
HashSet 是 java 中用来存储不能重复且无序的数据的一种容器。但在本质上,HashSet 实际上是用 HashMap 来存储数据的。在 HashSet 的源码中,有以下两个成员:code
private transient HashMap<E,Object> map; // Dummy value to associate with an Object in the backing Map private static final Object PRESENT = new Object();
map 就是用来存储数据的容器,HashSet 将全部的数据存储在 map 的 key 中,由于 HashMap 中的 key 是惟一的,因此也就达到了 HashSet 存储不重复元素的目的。在向 HashSet 中添加一个元素时,其实是调用了 HashMap 的 put() 方法:接口
public boolean add(E e) { return map.put(e, PRESENT)==null; }
能够看到,在添加元素时,将元素做为 key 添加到 map 中,而 value 则放入一个 Object 常量(本质上 value 没什么卵用),也就是说 map 中存储的全部键值对的 key 都不相同,而 value 都相同。
HashSet 中的其它方法,其实也都是直接调用了 HashMap 中的方法,例如:ci
public boolean remove(Object o) { return map.remove(o)==PRESENT; } public boolean contains(Object o) { return map.containsKey(o); } public int size() { return map.size(); }