引言

好了,步入正题,上篇文章Java 集合框架(2)---- List 相关类解析中我们一起看了一下 List 接口的相关具体类(ArrayListLinkedList….),这篇开始我们开始探索 Java 集合框架中的 Map接口及其相关子类。可能有些小伙伴会问了:为什么不先讲 Set接口而讲Map 接口呢?确实在集合框架的第一篇文章中我介绍接口的顺序是先 ListSet 然后才是Map接口,不过在这里还是决定先讲 Map 接口,因为 Set 接口下的一些具体类(HashSet….)是通过 Map 接口下的一些具体类(HashMap)实现的,而 Map 接口中具体类却不是通过 Set 接口(有些许依赖,但是主要逻辑上不是)来实现的。所以我们掌握了 Map 接口的一些具体类之后,再去看 Set接口就很容易上手了。

导航

  • AbstractMap
  • SortedMap
  • HashMap
    • HashMap的构造方法详解
    • HashMap插入元素详解
      • HashMap的put方法的具体流程
      • putVal方法执行流程图
    • HashMap获取元素详解
    • HashMap移除元素详解
    • HashMap遍历元素详解
    • HashMap的扩容操作是怎么实现的
    • HashMap在JDK1.7和JDK1.8中有哪些不同?HashMap的底层实现
      • JDK1.8之前
      • JDK1.8之后
      • JDK1.7 VS JDK1.8 比较
    • HashMap是怎么解决哈希冲突的
    • 简单总结一下HashMap是使用了哪些方法来有效解决哈希冲突的
    • 扩展知识
      • 能否使用任何类作为 Map 的 key?
      • 为什么HashMap中String、Integer这样的包装类适合作为Key?
      • 如果使用Object作为HashMap的Key,应该怎么办呢?
      • HashMap为什么不直接使用hashCode()处理后的哈希值直接作为table的下标?
      • HashMap 的长度为什么是2的幂次方?
      • 那为什么是两次扰动呢?
    • 总结
  • TreeMap
    • TreeMap的构造方法详解
    • TreeMap插入元素详解
    • TreeMap获取元素详解
    • 扩展知识
      • 如何指定 TreeMap 的元素排序方式?
      • 如何决定使用 HashMap 还是 TreeMap?
      • TreeMap 和 TreeSet 在排序时如何比较元素?Collections 工具类中的 sort()方法如何比较元素?
    • 总结
  • LinkedHashMap
    • LinkedHashMap插入元素详解
    • LinkedHashMap删除元素详解
    • LinkedHashMap遍历元素详解
    • 缓存控制
  • Hashtable
    • Hashtable详解
    • ConcurrentHashMap 和 Hashtable 的区别?
    • ConcurrentHashMap 底层具体实现知道吗?实现原理是什么?
      • JDK1.7
      • JDK1.8
    • 扩展知识
      • HashMap 与 Hashtable 有什么区别?
    • 总结
  • WeakHashMap
    • 弱引用对象和弱引用队列详解
    • 总结
  • IdentityHashMap
    • IdentityHashMap获取元素详解
    • IdentityHashMap添加元素及扩容机制详解
    • 总结
  • 辅助工具类
    • Array 和 ArrayList 有何区别?
    • 如何实现 Array 和 List 之间的转换?
    • comparable 和 comparator的区别?
    • Collection 和 Collections 有什么区别?

好了, 老规矩,先来看一下 Map 接口的继承关系图:

我们现在可以看到,Map 接口是独立存在的,我们之前看的 List接口是继承于Collection 接口的子接口。但是 Map接口并不依赖 Collection 接口。关于 Map 接口的一些基本概念在 Java 集合框架(2)---- List 相关类解析 中已经介绍过了。下面来看一下 Map 接口下的相关类和接口:

AbstractMap

从上面的图中我们知道这个类是一个抽象类,还是先从官方对它的描述开始:

This class provides a skeletal implementation of the Map interface, to minimize the effort required to implement this interface.
To implement an unmodifiable map, the programmer needs only to extend this class and provide an implementation for the entrySet method, which returns a set-view of the map’s mappings. Typically, the returned set will, in turn, be implemented atop AbstractSet. This set should not support the add or remove methods, and its iterator should not support the remove method.

To implement a modifiable map, the programmer must additionally override this class’s put method (which otherwise throws an UnsupportedOperationException), and the iterator returned by entrySet().iterator() must additionally implement its remove method.

The programmer should generally provide a void (no argument) and map constructor, as per the recommendation in the Map interface specification.

The documentation for each non-abstract method in this class describes its implementation in detail. Each of these methods may be overridden if the map being implemented admits a more efficient implementation.

大概意思是:
  这个类提供了 Map 接口的骨架实现,以最小化实现Map接口功能所需的要求。
  如果要实现一个不可更改的 map 对象,开发者只需要继承这个类并实现 entrySet() 方法,返回一个包含当前 Map 对象中所有键值对的集合。通常,这个集合应该基于 AbstractSet 类来实现,并且不应该支持添加删除元素的方法,其迭代器不应该支持移除元素的方法。
  如果要实现可更改的 map 对象,开发者必须重写 put() 方法(默认抛出 UnsupportedOperationException异常),并且通过 entrySet().iterator()方法返回的迭代器必须实现移除元素的方法。
开发者应该提供一个无参构造方法,和接受另一个 map 对象的作为参数的构造方法。
  这个文档描述了每个非 abstract 方法的实现细节,在继承过程中,如果对应方法有更适应当前类的实现,我们应该重写这些方法,并添加更好的实现逻辑。
  
有了基本的了解之后,我们再来看看这个类的部分源码

AbstractMap.java:

public abstract class AbstractMap<K,V> implements Map<K,V> {protected AbstractMap() {}// Query Operations/*** 返回当前 map 中键值对元素的数目*/public int size() {return entrySet().size();}/*** 判断当前 map 是否已经没有任何键值对元素*/public boolean isEmpty() {return size() == 0;}/*** 判断参数所给 值 是否存在当前 map 中的某一个键值对元素中(通过 equals 方法判断),* 如果存在,返回 true,否则返回 false*/public boolean containsValue(Object value) {Iterator<Entry<K,V>> i = entrySet().iterator();if (value==null) {while (i.hasNext()) {Entry<K,V> e = i.next();if (e.getValue()==null)return true;}} else {while (i.hasNext()) {Entry<K,V> e = i.next();if (value.equals(e.getValue()))return true;}}return false;}/*** 和上个方法类似,判断参数所给 键 是否存在当前 map 中的某一个键值对元素中(通过 equals 方法判断),* 如果存在,返回 true,否则返回 false*/public boolean containsKey(Object key) {Iterator<Map.Entry<K,V>> i = entrySet().iterator();if (key==null) {while (i.hasNext()) {Entry<K,V> e = i.next();if (e.getKey()==null)return true;}} else {while (i.hasNext()) {Entry<K,V> e = i.next();if (key.equals(e.getKey()))return true;}}return false;}/*** 获取参数所给的 键 所对应的值,如果当前 map 中不存在这个键,那么返回 null,* 这是默认的实现,通过迭代器遍历,效率低,不同的实体类都会重写该方法*/public V get(Object key) {Iterator<Entry<K,V>> i = entrySet().iterator();if (key==null) {while (i.hasNext()) {Entry<K,V> e = i.next();if (e.getKey()==null)return e.getValue();}} else {while (i.hasNext()) {Entry<K,V> e = i.next();if (key.equals(e.getKey()))return e.getValue();}}return null;}// Modification Operations/*** 在当前 map 中存入一个新的键值对元素,默认抛出 UnsupportedOperationException 异常,* 即操作不支持*/public V put(K key, V value) {throw new UnsupportedOperationException();}/*** 从当前 map 中移除参数代表的 键 所对应的键值对元素,并返回对应的 值* 通过迭代器找到对应键值对元素,然后调用迭代器的 remove 方法,* 如果返回的迭代器没有重写这个方法,则抛出 UnsupportedOperationException 异常*/public V remove(Object key) {Iterator<Entry<K,V>> i = entrySet().iterator();Entry<K,V> correctEntry = null;if (key==null) {while (correctEntry==null && i.hasNext()) {Entry<K,V> e = i.next();if (e.getKey()==null)correctEntry = e;}} else {while (correctEntry==null && i.hasNext()) {Entry<K,V> e = i.next();if (key.equals(e.getKey()))correctEntry = e;}}V oldValue = null;if (correctEntry !=null) {oldValue = correctEntry.getValue();i.remove();}return oldValue;}// Bulk Operations/*** 将参数所代表的 map 对象中所有的键值对元素放入当前 map 对象中*/public void putAll(Map<? extends K, ? extends V> m) {for (Map.Entry<? extends K, ? extends V> e : m.entrySet())put(e.getKey(), e.getValue());}/*** 移除当前 map 中的所有键值对元素*/public void clear() {entrySet().clear();}// Views/*** 储存键和值的集合* Each of these fields are initialized to contain an instance of the* appropriate view the first time this view is requested.  The views are* stateless, so there's no reason to create more than one of each.*/transient volatile Set<K>        keySet;transient volatile Collection<V> values;/*** 返回一个包含了当前 map 对象中所有的 “键” 的 Set 对象,* 返回一个匿名内部类对象,其实现了 Set 接口的基本功能*/public Set<K> keySet() {if (keySet == null) {keySet = new AbstractSet<K>() {public Iterator<K> iterator() {return new Iterator<K>() {private Iterator<Entry<K,V>> i = entrySet().iterator();public boolean hasNext() {return i.hasNext();}public K next() {return i.next().getKey();}public void remove() {i.remove();}};}public int size() {return AbstractMap.this.size();}public boolean isEmpty() {return AbstractMap.this.isEmpty();}public void clear() {AbstractMap.this.clear();}public boolean contains(Object k) {return AbstractMap.this.containsKey(k);}};}return keySet;}/*** 返回一个包含了当前 map 对象中所有的 “值” 的 Collection 对象,* 返回一个匿名内部类对象,其实现了 Collection 接口的基本功能*/public Collection<V> values() {if (values == null) {values = new AbstractCollection<V>() {public Iterator<V> iterator() {return new Iterator<V>() {private Iterator<Entry<K,V>> i = entrySet().iterator();public boolean hasNext() {return i.hasNext();}public V next() {return i.next().getValue();}public void remove() {i.remove();}};}public int size() {return AbstractMap.this.size();}public boolean isEmpty() {return AbstractMap.this.isEmpty();}public void clear() {AbstractMap.this.clear();}public boolean contains(Object v) {return AbstractMap.this.containsValue(v);}};}return values;}// 抽象方法,返回储存了当前 map 对象的所有键值对元素的 Set 对象public abstract Set<Entry<K,V>> entrySet();// Comparison and hashing/*** 比较当前 map 对象和参数所指定的 map 对象,* 如果当前 map 对象中所有的键值对元素和参数所指定的 map 对象* 中的所有键值对元素都相同(通过 equals 方法判断)*/public boolean equals(Object o) {if (o == this)return true;if (!(o instanceof Map))return false;Map<?,?> m = (Map<?,?>) o;if (m.size() != size())return false;try {Iterator<Entry<K,V>> i = entrySet().iterator();while (i.hasNext()) {Entry<K,V> e = i.next();K key = e.getKey();V value = e.getValue();if (value == null) {if (!(m.get(key)==null && m.containsKey(key)))return false;} else {if (!value.equals(m.get(key)))return false;}}} catch (ClassCastException unused) {return false;} catch (NullPointerException unused) {return false;}return true;}/*** 重写 Object 类的方法, 返回当前 map 对象的 hash 值*/public int hashCode() {int h = 0;Iterator<Entry<K,V>> i = entrySet().iterator();while (i.hasNext())h += i.next().hashCode();return h;}/*** 重写 Object 的方法,返回当前 map 对象的 String 对象表示*/public String toString() {Iterator<Entry<K,V>> i = entrySet().iterator();if (! i.hasNext())return "{}";StringBuilder sb = new StringBuilder();sb.append('{');for (;;) {Entry<K,V> e = i.next();K key = e.getKey();V value = e.getValue();sb.append(key   == this ? "(this Map)" : key);sb.append('=');sb.append(value == this ? "(this Map)" : value);if (! i.hasNext())return sb.append('}').toString();sb.append(',').append(' ');}}/*** 重写 Object 类的方法,返回一个当前 map 对象的复制对象,* 键值对不会被复制*/protected Object clone() throws CloneNotSupportedException {AbstractMap<?,?> result = (AbstractMap<?,?>)super.clone();result.keySet = null;result.values = null;return result;}// ...}

和我们之前文章中介绍的 AbstractList 很类似,利用 Java 多态的特性,提供了对应接口的基本骨架实现,而其他的扩展功能留给子类去实现,我们从开头的图中也知道,图中的 Map 接口下的具体类都是继承于这个AbstractMap,即为这个类的子类

SortedMap

下面我们来看 Map 接口下的另外一个接口 SortedMap ,这个接口官方对它的描述文档有点长,在这里就不贴了,总结一下这个接口的主要功能就是声明一些方法,用于给实现这个接口的容器指定一个约定:
实现这个接口的容器应该要按某个规则对容器内的元素进行排序,并且可以通过这个接口提供的方法获取容器特定的一些元素。但是接口本身不干预容器的排序规则,具体的排序方式由容器自己决定。

我们来看一下这个接口的源码 SortedMap.java:

public interface SortedMap<K,V> extends Map<K,V> {/*** 返回这个 map 对象中用于通过 “键” 来对元素进行排序的 Comparator(比较器)对象,* 如果当前 map 对象使用 “键” 的自然升序规则排序元素(即未指定排序所用的 Comparator 对象),* 那么返回 null */Comparator<? super K> comparator();/*** 获取当前 map 对象中元素 “键” 的范围在 [fromKey, toKey) 之中的键值对元素,* 将这些键值对放在一个 SortedMap 对象中并返回*/SortedMap<K,V> subMap(K fromKey, K toKey);/*** 获取当前 map 中 “键” 小于 toKey 的键值对元素,* 将这些键值对放在一个 SortedMap 对象中并返回*/SortedMap<K,V> headMap(K toKey);/*** 获取当前 map 中 “键” 不小于fromKey 的键值对元素,* 将这些键值对放在一个 SortedMap 对象中并返回*/SortedMap<K,V> tailMap(K fromKey);/*** 返回当前 map 中的第一个 键*/K firstKey();/*** 返回当前 map 中的最后一个 键*/K lastKey();/*** 返回一个 Set 对象,其中元素为当前 map 的键值对中的 “键”,* 元素顺序按当前 map 对象的排序规则对 “键” 升序的规则排列*/Set<K> keySet();/*** 返回一个 Collection 对象,其中元素为当前 map 的键值对中的 “值”,* 元素顺序按当前 map 对象的排序规则对 “键” 升序的规则排列*/Collection<V> values();/*** 返回一个 Set 对象,其中元素为当前 map 中的所有键值对元素,* 元素顺序按当前 map 对象的排序规则对 “键” 升序的规则排列*/Set<Map.Entry<K, V>> entrySet();
}

我们之后将会看到,Map 接口中的具体类 TreeMap 就是一个实现了 SortedMap 接口(实现的是 NavigableMap 接口,NavigableMap 继承了 SortedMap 接口)方法的类。因此我们已经可以知道 TreeMap 是一个按照某个排序规则对 “键”进行比较并以此作为依据来对键值对元素进行排序的 map 容器。关于这个类的具体实现细节,我们等下再一起探索哈。

HashMap

HashMap 应该是 Java 集合框架中我们在开发中最常用的容器类之一了,它提供了保存多个键值对的能力,并对其保存的键值对提供获取和操作的相关 API,相信小伙伴们对这个类的用法已经很熟悉了,那么我们从源码入手,来一起看看 HashMap是怎么实现的:

public class HashMap<K,V> extends AbstractMap<K,V>implements Map<K,V>, Cloneable, Serializable {// ... // 默认的初始化容量(16),HashMap 的容量必须是 2 的次幂static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16// HashMap 的最大容量static final int MAXIMUM_CAPACITY = 1 << 30;// 默认的负载因子(用于计算出下一次进行扩容时的容量)static final float DEFAULT_LOAD_FACTOR = 0.75f;/*** 将链表树化的最小长度,当有多个 key 的 hashCode 相同时,* 先采用链地址法处理冲突,即将多个相同的元素按先后顺序排成一条链表,* 当这个链表的元素不小于当前字段的值时,为了保证效率,* 将这一部分链表转换成平衡二叉树*/static final int TREEIFY_THRESHOLD = 8;// 在调整 HashMap 容量时取消树化链表的长度阀值static final int UNTREEIFY_THRESHOLD = 6;// 树化一个链表时要求 HashMap 的最小容量static final int MIN_TREEIFY_CAPACITY = 64;

在上面的代码中,有一个 DEFAULT_LOAD_FACTOR 常量,意为负载因子,这个值用于计算出下一次需要对 HashMap 进行扩容时 HashMap 中包含的最大元素(即键值对,下同)数,即可以理解为对 HashMap 对象进行下一次扩容的容量阀值,这个阀值也由一个名为 threshold 的成员变量保存。

举个例子:
假当前设置的 HashMap 对象的容量为默认容量,即 16,那么当前的 threshold值为 16 * 0.75 = 12,那么如果当前 HashMap 中装的元素个数到达了 12个时,就要进行下一次扩容了。(即阀值=负载因子*HashMap的总容量)可能有小伙伴会问了,为什么要这么做呢?这样不是浪费内存吗?确实,这样做确实会浪费一部分内存,但是主要目的是为了减少元素冲突:当当前的 HashMap 容量越大的时候,给元素的key计算出来的 hashCode的选择也就越多,这样就越不容易产生冲突

举个例子:如果当前 HashMap 还剩下 16 个空位置,我们要存 10 个元素,那么平均下来每个元素有1.6 个位置,可能产生冲突,但如果当前 HashMap 只有 8个位置,那么把 10 个元素存进去,必然产生冲突,这样的话就增加了插入和查询元素的时间复杂度。一个可能产生冲突,一个必然产生冲突,而 HashMap 的任务其实主要是致力于保证在尽可能低的时间复杂度(O(1))插入查询元素。所以从这个角度上来说牺牲一点内存是值得的。

需要注意的是,我们在创建 HashMap 对象的时候可以自己定义这个负载因子,但是我们很难去准确的找到一个最适用我们程序中的负载因子如果太小,那么会浪费太多的内存空间,如果太大,又可能会在插入元素时产生较多冲突,提高了插入和查询操作的时间复杂度,因此除非你很有把握,否则的话我们可以直接用默认的值,无需特殊指定。

下面来看一下 HashMap 是用Node类来表示每个元素(键值对)的:

// 描述 HashMap 元素的键值对
static class Node<K,V> implements Map.Entry<K,V> {final int hash; // 元素的键的  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; }// 获取当前元素 “键” 的 hashCode 值public final int hashCode() {return Objects.hashCode(key) ^ Objects.hashCode(value);}public final V setValue(V newValue) {V oldValue = value;value = newValue;return oldValue;}// 判断对象 o 是否和当前对象在值上相等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;}
}

我们可以HashMap 中通过一个名为 Node静态内部类来实现这个Map.Entry接口并实现接口中的方法,而这个接口它是一个描述了 HashMap 中键值对元素信息并提供了一些方法来获取这些键值对

我们先来看下Map.Entry接口的相关方法:

/*** Entry 接口代表一个 key-value 对(键值对),形成的数据结构,即为映射元素,* 这个接口为 Map 接口中的子接口,* 泛型 K 代表键的类型,泛型 V 代表值的类型*/
interface Entry<K,V> {/*** 返回当前键值对中的 键 对象,* 如果当前键值对不在对应的 Map 中,抛出一个 IllegalStateException 异常(可选)*/K getKey();/*** 返回当前键值对中的 值 对象,* 如果当前键值对不在对应的 Map 中,抛出一个 IllegalStateException 异常(可选)*/V getValue();/*** 设置当前键值对中的 值 对象,* 如果设置的值参数对象为 null,抛出一个 NullpointException 异常(可选),* 如果设置的值参数对象不能转换为当前键值对中对应的 值 类型,抛出一个 ClassCastException 异常,* 如果当前键值对不在对应的 Map 中,抛出一个 IllegalStateException 异常(可选)*/V setValue(V value);/*** 如果参数对象和当前键值对等价,那么返回 true,否则返回 false,一般可以通过以下代码实现:* <pre>*     (e1.getKey()==null ?*      e2.getKey()==null : e1.getKey().equals(e2.getKey())) &&*     (e1.getValue()==null ?*      e2.getValue()==null : e1.getValue().equals(e2.getValue()))* </pre>*/boolean equals(Object o);/*** 返回当前键值对的 hashCode ,用于 Map 中形成数组下标值,一般可以通过以下代码实现:* <pre>*     (e.getKey()==null   ? 0 : e.getKey().hashCode()) ^*     (e.getValue()==null ? 0 : e.getValue().hashCode())* </pre>* 设计 hashCode 方法时,确保当两个对象的 equals 方法返回 true 时,* 这两个对象的 hashCode 方法返回值相同*/int hashCode();// ......
}

这个接口提供了一些方法,用于描述一个 键值对 的行为,即通过这些方法来获取 / 设置键值对的相关信息。
而在 Map(HashMapLinkedHashMap…) 中正是通过实现了这个接口的类对象来储存键值对的信息。

好了,其实在整个 HashMap实际上是通过一个 Node类型的数组来保存键值对信息的,来看看相关的字段定义:

// 保存所有键值对元素信息的表,其长度必须为 2 的次幂
transient Node<K,V>[] table;// 当前 HashMap 对象包含的键值对元素集合
transient Set<Map.Entry<K,V>> entrySet;// 当前 HashMap 对象中包含的元素(键值对)的数目
transient int size;// 记录当前 HashMap 对象已经更改的次数(重新分配尺寸、添加、删除元素...)
transient int modCount;// 下一次进行重新分配当前 HashMap 容量时 HashMap 中存在的最大元素数目 (capacity * loadFactor).
int threshold;// 当前 HashMap 的负载因子
final float loadFactor;

相关字段看完了,也算是为了下面的内容打基础,下面就开始分析一下相关的方法,首先从构造方法开始:

一、HashMap的构造方法详解:

// 构造一个带有 initialCapacity 初始容量和 loadFactor 负载因子的 HashMap 对象
public HashMap(int initialCapacity, float loadFactor) {if (initialCapacity < 0)throw new IllegalArgumentException("Illegal initial capacity: " +initialCapacity);// 指定的初始容量不能大于 HashMap 允许的最大容量if (initialCapacity > MAXIMUM_CAPACITY)initialCapacity = MAXIMUM_CAPACITY;// 如果当前指定的负载因子小于 0 或者是一个非数字(0.0/0.0 的情况)if (loadFactor <= 0 || Float.isNaN(loadFactor))throw new IllegalArgumentException("Illegal load factor: " +loadFactor);this.loadFactor = loadFactor;// 通过 tableSizeFor 方法得到下一次要进行扩容时 HashMap 对象包含的元素数目(这个是第一次的)this.threshold = tableSizeFor(initialCapacity);
}

在这个方法里有两个方法可能有点不太熟悉,我们来看一下,首先是 Float.isNaN ,这个方法在 Float.java中定义:

// 判断一个数字是否是非数字值(not a number),如果是,返回 true,否则返回 false
public static boolean isNaN(float v) {return (v != v);
}

这是一个Float类中的静态方法,为了判断出一个值是否为 “非数字” 值,可能有小伙伴会问了,这句话怎么说的通呢?其实这里说的 “非数字”值指的是类似于 0.0 / 0.0得到的值。是的,在 Java 中,小数除以 0 不会抛出 ArithmeticException 异常,但是每次0.0 / 0.0得到的结果都是不同的值(对象),我们来做个小实验:

public static void main(String[] args) {System.out.println(0.0/0.0 == 0.0/0.0);
}

看到这个程序,可能有些小伙伴第一反应是:这个输出肯定是 true,那么我们来看看结果:

很遗憾,它输出的是false,因为每次表达式 0.0/0.0得到的值都不一样,回到我们的上面的代码,isNaN方法直接返回的是(v != v),我们也就可以理解这段代码的含义了,如果 v 不是某次 0.0/0.0 的结果,那么 (v != v) 的值肯定为 false (是吧,自己怎么会不等于自己呢),否则,如果v 是某次 0.0/0.0 的结果,根据我们上面做的实验,它会返回 true,那么将会抛出IllegalArgumentException异常。

那么再回到上面的 HashMap 的构造方法中,我们已经知道第一个方法的作用,下面来看看下一个方法:tableSizeFor()

// 对当前 cap 指定的容量进行操作,返回第一个大于等于 cap 的 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;
}

这个方法用到了位运算,|按位或,而 >>>无符号右移,和 >>有符号右移)的区别在于无符号右移在左边填充的是 0,而有符号右移在左边填充的是符号位(正数为 0,负数为 1)。而所有位运算均是争对二进制而言,这个方法的作用是什么呢?我们用一个具体的数值带进去走一遍流程,假设当前 cap 为 6:

n = cap - 1 = 5
n 的二进制:000..00(29个0) 101,之后省略前导零
n |= n >>> 1 = (101) | (010) = 111
n |= n >>> 2 = (111) | (001) = 111
n |= n >>> 4 = (0) | (111) = 111
n |= n >>> 8 = (0) | (111) = 111
n |= n >>> 16 = (0) | (111) = 111

看到这可能有小伙伴已经反应过来了:这个其实就是把 n 的二进制数中最左边的那一位 1 之后的 0 全变为 1。接下来在 return 语句中如果 n 的值正常,那么返回 n + 1,这样经过进位将左边所有的 1 变为 0 ,并将最左边的 1 的前一位 0 变为 1,比如当前得到了 n 为 7(111),那么 n + 1 就为 8 (1000)得到的值就是 2 的次幂,那在开始为什么要将 n 赋值为 cap - 1 呢?其实是为了防止当前 cap 值本身就是 2 的次幂的情况会使得得到的值为 cap * 2。整个过程 n 向右移动的位数为(1+2+4+8+16 = 31,正好是 int 类型的位数(32)- 1)。

这里说个题外话:如果操作数的类型是 long 呢?该如何改进?
小伙伴如果对这段代码还不是很清楚的话,我们来看一下这段代码在程序中的表现:

static final int tableSizeFor(int cap) {int maxValue = Integer.MAX_VALUE - 8;int n = cap - 1;n |= n >>> 1;n |= n >>> 2;n |= n >>> 4;n |= n >>> 8;n |= n >>> 16;return (n < 0) ? 1 : (n >= maxValue) ? maxValue : n + 1;
}public static void main(String[] args) {for (int i = 0; i < 33; i++) {System.out.println("i: " + i + ", value: " + tableSizeFor(i));}
}

我把这段代码复制到了程序中,并将 MAXIMUM_CAPACITY 值用 Integer.MAX_VALUE - 8 代替了,值本身不变,我们来看看结果:

对比结果,这个结论也很容易理解。关于位运算不懂的小伙伴们,可以参考这篇博客:Java基础-一文搞懂位运算

好了,回到我们最初的构造方法,构造方法执行完这个方法所在的代码行就结束了,但是我们并没有看到其为table 字段(储存键值对元素的数组)申请内存空间,我们看看别的构造方法:

// 创建一个具有指定的初始容量和默认负载因子(0.75)的 HashMap 对象
public HashMap(int initialCapacity) {this(initialCapacity, DEFAULT_LOAD_FACTOR);
}// 创建一个没有任何元素并带有默认容量(16)和默认负载因子(0.75)的 HashMap 对象
public HashMap() {this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted
}/*** 创建一个具有默认负载因子的 HashMap 对象,并将 Map 类型的参数 m 中的元素存入这个对象中,* 如果 m 为 null,抛出一个 NullpointException 异常*/
public HashMap(Map<? extends K, ? extends V> m) {this.loadFactor = DEFAULT_LOAD_FACTOR;putMapEntries(m, false);
}

前面两个构造方法也没看到其为 table 字段申请内存空间,但是第三个构造方法中我们看到了一个putMapEntries()方法,那么我们跟进去看一下:

/*** Implements Map.putAll and Map constructor* @param m the map* @param evict false when initially constructing this map, else* true (relayed to method afterNodeInsertion).*/
final void putMapEntries(Map<? extends K, ? extends V> m, boolean evict) {int s = m.size();if (s > 0) {if (table == null) { // pre-sizefloat ft = ((float)s / loadFactor) + 1.0F;int t = ((ft < (float)MAXIMUM_CAPACITY) ?(int)ft : MAXIMUM_CAPACITY);if (t > threshold)threshold = tableSizeFor(t);}else if (s > threshold)resize();for (Map.Entry<? extends K, ? extends V> e : m.entrySet()) {K key = e.getKey();V value = e.getValue();putVal(hash(key), key, value, false, evict);}}
}

二、HashMap插入元素详解:

在这里,我们看到了两个新的方法:resizeputVal,而且不管如何这个方法最后都会调用putVal 方法,我们很容易猜到这个方法是存放键值对进入当前 HashMap的方法,但是我们平时都是用 put 方法来存放键值对的,我们看看 put 方法的源码:

public V put(K key, V value) {return putVal(hash(key), key, value, false, true);
}

原来 put方法就是直接调用了 putVal方法,那么我们来看看这个 putVal 方法:

/*** 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;// 如果当前的 table 为 null,证明还未给 table 申请内存空间,// 那么通过 resize 方法申请并调整 table 容量if ((tab = table) == null || (n = tab.length) == 0)n = (tab = resize()).length;// 如果当前存入的键值对中 (“键” 的 hash 值 % table.length 得到的结果),// 为什么可以用 & 运算符来模拟 % 操作?// 上文已经说过,HashMap 的容量必须是 2 的次幂,所以其容量 n 转换成二进制中必然只有一位是 1,// 那么 n - 1,就是将最左边的那一位 1 变为 0,并且将其右边的 0 变成 1 ,// 再将得到的值和 hash 通过 & 按位相与,这样的话得到的结果必然不会大于 n-1,// 即通过位运算达到了 % 操作的目的,还减小了 CPU 资源的消耗(位操作速度一般的操作符快多了)// 如果得到的结果作为下标在 table 数组所代表的数组元素为 null,// 即 table[hash(key) % table.length] = null// 证明这个下标可用(不会产生冲突),那么直接赋值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;// 如果产生已存在的键值对元素为 TreeNode 类型,证明当前链表已经被树化(变成一颗红黑树),// 那么把节点(键值对元素)插入树中,(涉及到红黑树的维护)     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);// 如果当前链表长度不小于 TREEIFY_THRESHOLD(8),那么树化链表(变成一颗红黑树)// 注意 bitCount 从 0 开始if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1sttreeifyBin(tab, hash);break;}// 同样的,如果插入的键值对元素的 “键” 和某个已经存在的键值对的 “键” 等价,// 那么直接跳出循环,之后会更新这个等价的键值对的值if (e.hash == hash &&((k = e.key) == key || (key != null && key.equals(k))))break;p = e;}}// 如果 e 不为 null,证明有某个键值对的 “键” 和插入的键值对的 “键” 是等价的,// 更新已经存在的那个键值对的值if (e != null) { // existing mapping for keyV oldValue = e.value;if (!onlyIfAbsent || oldValue == null)e.value = value;// 回调方法,供子类实现afterNodeAccess(e);// 返回被替换得 “值”return oldValue;}}++modCount;// 如果插入键值对后元素数目大于重新分配 HashMap 容量的阀值,那么再次分配 HashMap 容量if (++size > threshold)resize();// 回调方法,供子类实现afterNodeInsertion(evict);// 如果没有更新任何键值对的值,证明成功插入了一个新的键值对,此时返回 nullreturn null;
}

这个方法实际上为在 HashMap中插入新元素的核心方法,在上面的代码注释中涉及到一些新的概念,首先是冲突,这里的冲突是指两个键值对元素的 “键” 的 hashCode 相同,这种情况下有两种情况:
  1、要插入的键值对的 “键” 和冲突的键值对的 “键” 等价(两个引用指向一个对象或者两个引用指向的对象的 equals 方法返回 true)。此时,记录这个键值对,到后面更新替换一下它的值即可。
  2、要插入的键值对的 “键” 和冲突的键值对的 “键” 不等价(两个引用指向的对象的 equals 方法返回 false)。这种情况下就需要进行特殊处理(链化或者树化节点),来看张图:

这是处理冲突的第一种方式,将键的 hashCode 值冲突但键本身又不等价的键值对按插入先后顺序链化,那么还有 “树化” 呢?树化其实是将链化的链表转成一颗红黑树(一种平衡二叉树的实现,节点的左子树节点值都小于该节点值,右子树节点值都大于该节点值,并且每个节点的左右子树高度差不超过 1),可能有些小伙伴对红黑树不太熟悉,但是红黑树的相关操作(主要是节点的旋转比较麻烦)并不是一两句话能够说清楚的,想要了解红黑树的小伙伴们,可以参考本博主的这篇博客jkf

看完的小伙伴,应该对红黑树有了一定的了解了吧,在红黑树中插入和查询操作的时间复杂度都是 O(logn),即为树的高度,这里 n 为树的节点总数,我们知道在一个链表中查找某个节点的时间复杂度为 O(n),这样的话如果节点数很多的话就会造成插入和查询节点过于耗时的情况,而 HashMap 本身就是用来提供对象和对象之间的映射关系的,即减小通过对象来查询对象的时间复杂度,当我们上面链化的节点过多的时候,链表太长就会影响 HashMap 的插入和查询操作的时间复杂度

因此在上面的代码中,如果冲突的元素组成的链表长度不小于 TREEIFY_THRESHOLD(8),(即大于等于8 时),就需要将链表树化,以减小相关操作的时间复杂度(O(n) -> O(logn))。

还是简单用一张图来看一下树化的过程:

2、1 HashMap的put方法的具体流程?

当我们put的时候,首先计算 key的hash值,这里调用了 hash方法,hash方法实际是让key.hashCode()key.hashCode()>>>16进行异或操作,高16bit补0,一个数和0异或不变,所以 hash 函数大概的作用就是:高16bit不变,低16bit和高16bit做了一个异或,目的是减少碰撞。按照函数注释,因为bucket数组大小是2的幂,计算下标index = (table.length - 1) & hash,如果不做 hash 处理,相当于散列生效的只有几个低 bit 位,为了减少散列的碰撞,设计者综合考虑了速度、作用、质量之后,使用高16bit和低16bit异或来简单处理减少碰撞,而且JDK8中用了复杂度 O(logn)的树结构来提升碰撞下的性能

static final int hash(Object key) {int h;return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);}

2、2 putVal方法执行流程图:

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);
}//实现Map.put和相关方法
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,boolean evict) {Node<K,V>[] tab; Node<K,V> p; int n, i;// 步骤①:tab为空则创建 // table未初始化或者长度为0,进行扩容if ((tab = table) == null || (n = tab.length) == 0)n = (tab = resize()).length;// 步骤②:计算index,并对null做处理  // (n - 1) & hash 确定元素存放在哪个桶中,桶为空,新生成结点放入桶中(此时,这个结点是放在数组中)if ((p = tab[i = (n - 1) & hash]) == null)tab[i] = newNode(hash, key, value, null);// 桶中已经存在元素else {Node<K,V> e; K k;// 步骤③:节点key存在,直接覆盖value // 比较桶中第一个元素(数组中的结点)的hash值相等,key相等if (p.hash == hash &&((k = p.key) == key || (key != null && key.equals(k))))// 将第一个元素赋值给e,用e来记录e = p;// 步骤④:判断该链为红黑树 // hash值不相等,即key不相等;为红黑树结点// 如果当前元素类型为TreeNode,表示为红黑树,putTreeVal返回待存放的node, e可能为nullelse 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);//判断链表的长度是否达到转化红黑树的临界值,临界值为8if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st//链表结构转树形结构treeifyBin(tab, hash);// 跳出循环break;}// 判断链表中结点的key值与插入的元素的key值是否相等if (e.hash == hash &&((k = e.key) == key || (key != null && key.equals(k))))// 相等,跳出循环break;// 用于遍历桶中的链表,与前面的e = p.next组合,可以遍历链表p = e;}}//判断当前的key已经存在的情况下,再来一个相同的hash值、key值时,返回新来的value这个值if (e != null) { // 记录e的valueV oldValue = e.value;// onlyIfAbsent为false或者旧值为nullif (!onlyIfAbsent || oldValue == null)//用新值替换旧值e.value = value;// 访问后回调afterNodeAccess(e);// 返回旧值return oldValue;}}// 结构性修改++modCount;// 步骤⑥:超过最大容量就扩容 // 实际大小大于阈值则扩容if (++size > threshold)resize();// 插入后回调afterNodeInsertion(evict);return null;
}

具体步骤:
    ①.判断键值对数组table[i]是否为空或为null,否则执行resize()进行扩容。

②.根据键值key计算hash值得到插入的数组索引i,如果table[i]==null,直接新建节点添加,转向⑥,如果table[i]不为空,转向③。

③.判断table[i]的首个元素是否和key一样,如果相同直接覆盖value,否则转向④,这里的相同指的是两个元素的hashCode以及equals都一样。

④.判断table[i] 是否为treeNode,即table[i] 是否是红黑树,如果是红黑树,则直接在树中插入键值对,否则转向⑤。

⑤.遍历table[i],判断链表长度是否大于8大于8的话把链表转换为红黑树,在红黑树中执行插入操作,否则进行链表的插入操作;遍历过程中若发现key已经存在直接覆盖value即可。

⑥.插入成功后,判断实际存在的键值对数量size是否超多了最大容量threshold,如果超过,进行扩容

补充:

  • 如果两个元素的hashCode值不等,则两个元素必定是不相同的
  • 如果两个元素的hashCode值相等,则可能发生了哈希碰撞,可以通过equals()方法再进一步的判断:如果equals()方法返回true,则两个元素必定是相同的。如果返回false,则两个元素必定是不同的
  • 可以通过hashCode()equals()确定一个元素的唯一性,只有hashCode()和equals()都返回true,才表示是同一个对象。

三、 HashMap获取元素详解:

好了,到这里我们已经把 HashMap插入元素的流程分析完了,下面来看看取键值对 “值” 的方法(即 get 方法):

public V get(Object key) {Node<K,V> e;return (e = getNode(hash(key), key)) == null ? null : e.value;
}

好吧,get方法通过调用 getNode 方法来得到对应的键值对元素,如果为 null,那么返回null,否则返回对应的值,我们来看看 getNode方法:

/*** Implements Map.get and related methods** @param hash hash for key* @param key the key* @return the node, or null if none*/
final Node<K,V> getNode(int hash, Object key) {Node<K,V>[] tab; Node<K,V> first, e; int n; K k;// 如果当前 HashMap 的 table 数组不为 null 并且处理 hash 值得到的结果(hash %= n)作为下标,// 所指向的数组元素不为 null,那么证明这个 hash 值存在对应的键值对if ((tab = table) != null && (n = tab.length) > 0 &&(first = tab[(n - 1) & hash]) != null) {// 如果第一个键值对的键就和要查询的键等价,那么直接返回键值对元素if (first.hash == hash && // always check first node((k = first.key) == key || (key != null && key.equals(k))))return first;// 如果第一个键值对元素的 next 不为 null,证明这个键值对和别的键值对产生冲突if ((e = first.next) != null) {// 如果是树节点,那么从红黑树中获取键值对元素if (first instanceof TreeNode)return ((TreeNode<K,V>)first).getTreeNode(hash, key);do {// 如果是链表并且键值对的键和当前要查询的键等价,那么返回这个键值对if (e.hash == hash &&((k = e.key) == key || (key != null && key.equals(k))))return e;} while ((e = e.next) != null);}}// 没有查询到证明当前要查询的 key 不和任何一个键值对的键等价,则返回 nullreturn null;
}

四、HashMap移除元素详解:

OK,get 方法的流程我们也知道了,下面看看移除一个键值对元素的方法 remove

public V remove(Object key) {Node<K,V> e;return (e = removeNode(hash(key), key, null, false, true)) == null ?null : e.value;
}

同样的,调用了 removeNode方法来进行移除,我们赶紧看看这个方法:

/*** Implements Map.remove and related methods** @param hash hash for key* @param key the key* @param value the value to match if matchValue, else ignored* @param matchValue if true only remove if value is equal* @param movable if false do not move other nodes while removing* @return the node, or null if none*/
final Node<K,V> removeNode(int hash, Object key, Object value,boolean matchValue, boolean movable) {Node<K,V>[] tab; Node<K,V> p; int n, index;// 同样的,先确保 table 不为 null,并且处理后的 hash 作为下标所指向的元素不为 nullif ((tab = table) != null && (n = tab.length) > 0 &&(p = tab[index = (n - 1) & hash]) != null) {Node<K,V> node = null, e; K k; V v;// 这里的逻辑和上面 getNode 方法的逻辑很相似,先得取到要移除的键值对元素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 = e;} while ((e = e.next) != null);}}// 如果上面得到的要移除的键值对元素不为 null,那么进行移除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;// 移除的不是链表头结点元素,// 将 node 的上一个节点元素(即为 p)的 next 字段赋值为 node.nextelsep.next = node.next;++modCount;--size;// 供子类实现的回调方法afterNodeRemoval(node);// 返回被移除的元素return node;}}// 没有找到要移除的键值对元素,返回 nullreturn null;
}

五、HashMap遍历元素详解:

看完了移除键值对元素的操作,最后来看一下遍历元素的方法:

// 1、通过 entrySet() 方法得到 HashMap 的键值对集合,再通过集合提供的迭代器来遍历元素,
// 这个遍历过程其实就是顺序遍历 HashMap 中的 table 数组
public Set<Map.Entry<K,V>> entrySet() {Set<Map.Entry<K,V>> es;return (es = entrySet) == null ? (entrySet = new EntrySet()) : es;
}
// 2、通过得到 HashMap 的所有键值对中 “键” 的集合,然后通过 get() 方法得到值来遍历元素
public Set<K> keySet() {Set<K> ks;return (ks = keySet) == null ? (keySet = new KeySet()) : ks;
}
// 3、通过 forEach 方法来实现 HashMap 中的元素遍历(JDK 1.8 以上支持)
@Override
public void forEach(BiConsumer<? super K, ? super 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.key, e.value);}if (modCount != mc)throw new ConcurrentModificationException();}
}

HashMap 虽然是通过提供集合(entrySet)的形式来对外提供遍历元素的接口,但实际上这个集合(entrySet)遍历元素的顺序就是直接顺序遍历其 HashMap 对象的 table 数组,关于这点,可以参考以下源码:

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();}// ...
}

这个 EntrySet类是 HashMap 的一个内部类,而 HashMapentrySet 方法中返回的也是一个 EntrySet 对象,也就是说我们通过 entrySet 方法得到的其实是一个EntrySet对象,我们对 Set 进行遍历时是通过其提供的迭代器进行的,所以我们重点关注其iterator方法,发现其返回的是一个 EntryIterator 对象,我们看看这个类:

final class EntryIterator extends HashIteratorimplements Iterator<Map.Entry<K,V>> {public final Map.Entry<K,V> next() { return nextNode(); }
}

同样是一个 HashMap内部类,继承自 HashIterator类,其next 方法直接返回了其父类对象HashIteratornextNode 方法的调用结果,我们继续跟进这个方法:

final Node<K,V> nextNode() {Node<K,V>[] t;// 当前要返回的元素为上一次调用 nextNode 方法后的 next 引用指向的元素对象Node<K,V> e = next;if (modCount != expectedModCount)throw new ConcurrentModificationException();// 如果当前要返回的元素为 null,抛出一个异常if (e == null)throw new NoSuchElementException();// 如果当前元素的下一个元素为 null,并且当前 HashMap 对象的 table 数组不为 null,// 则进入循环,这里其实是为了下一个元素做准备,next 引用指向下一个要返回的元素对象if ((next = (current = e).next) == null && (t = table) != null) {// 这里循环为了排除 table 数组的 null 元素,index 下标一直++,// 直到遇到一个不为 null 的元素时结束循环,do {} while (index < t.length && (next = t[index++]) == null);}return e;
}

这个方法在 HashIterator 类中声明,方法的作用在注释中已经写的很清楚了,我们再来看看 HashIterator类的其他信息:

abstract class HashIterator {Node<K,V> next;        // next entry to returnNode<K,V>current;     // current entryint expectedModCount;  // for fast-failint index;             // current slotHashIterator() {expectedModCount = modCount;// HashMap 对象的 table 数组Node<K,V>[] t = table;current = next = null;index = 0;// 在构造方法中初始化 next 引用为 table 数组中第一个不为 null 的元素if (t != null && size > 0) { // advance to first entrydo {} while (index < t.length && (next = t[index++]) == null);}}// ...
}

通过 entrySet 方法来遍历元素的关键代码就是这些。其他的元素遍历方式小伙伴们可以自己参考源码,通过上面的分析我们应该知道,HashMap 中元素的遍历顺序和元素的插入顺序是没有任何关系的,因为插入元素时主要依据的是元素的键的 hashCode 值,而每个元素的键的hashCode 没有什么规则(根据键所属的类的实现而定),所以我们并不能试图按照插入元素的顺序来取出元素。如果需要使得取出的元素顺序是按照插入元素的先后顺序排序的话,请使用 LinkedHashMap 。关于 LinkedHashMap,我们下面再进行讲解。

六、HashMap的扩容操作是怎么实现的?

①.在jdk1.8中,resize方法是在hashmap中的键值对个数大于或等于阀值时或者初始化时,就调用resize方法进行扩容

②.每次扩展的时候,都是扩展为原来的2倍

③.扩展后Node对象的位置要么在原位置,要么移动到原偏移量两倍的位置

final Node<K,V>[] resize() {Node<K,V>[] oldTab = table;//oldTab指向hash桶数组int oldCap = (oldTab == null) ? 0 : oldTab.length;int oldThr = threshold;int newCap, newThr = 0;if (oldCap > 0) {//如果oldCap不为空的话,就是hash桶数组不为空if (oldCap >= MAXIMUM_CAPACITY) {//如果大于最大容量了,就赋值为整数最大的阀值threshold = Integer.MAX_VALUE;return oldTab;//返回}//如果当前hash桶数组的长度在扩容后仍然小于最大容量 并且oldCap大于默认值16else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&oldCap >= DEFAULT_INITIAL_CAPACITY)newThr = oldThr << 1; // double threshold 双倍扩容阀值threshold}// 旧的容量为0,但threshold大于零,代表有参构造有cap传入,threshold已经被初始化成最小2的n次幂// 直接将该值赋给新的容量else if (oldThr > 0) // initial capacity was placed in thresholdnewCap = oldThr;// 无参构造创建的map,给出默认容量和threshold 16, 16*0.75else {               // zero initial threshold signifies using defaultsnewCap = DEFAULT_INITIAL_CAPACITY;newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);}// 新的threshold = 新的cap * 0.75if (newThr == 0) {float ft = (float)newCap * loadFactor;newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?(int)ft : Integer.MAX_VALUE);}threshold = newThr;// 计算出新的数组长度后赋给当前成员变量table@SuppressWarnings({"rawtypes","unchecked"})Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];//新建hash桶数组table = newTab;//将新数组的值复制给旧的hash桶数组// 如果原先的数组没有初始化,那么resize的初始化工作到此结束,否则进入扩容元素重排逻辑,使其均匀的分散if (oldTab != null) {// 遍历新数组的所有桶下标for (int j = 0; j < oldCap; ++j) {Node<K,V> e;if ((e = oldTab[j]) != null) {// 旧数组的桶下标赋给临时变量e,并且解除旧数组中的引用,否则就数组无法被GC回收oldTab[j] = null;// 如果e.next==null,代表桶中就一个元素,不存在链表或者红黑树if (e.next == null)// 用同样的hash映射算法把该元素加入新的数组newTab[e.hash & (newCap - 1)] = e;// 如果e是TreeNode并且e.next!=null,那么处理树中元素的重排else if (e instanceof TreeNode)((TreeNode<K,V>)e).split(this, newTab, j, oldCap);// e是链表的头并且e.next!=null,那么处理链表中元素重排else { // preserve order// loHead,loTail 代表扩容后不用变换下标,见注1Node<K,V> loHead = null, loTail = null;// hiHead,hiTail 代表扩容后变换下标,见注1Node<K,V> hiHead = null, hiTail = null;Node<K,V> next;// 遍历链表do {             next = e.next;if ((e.hash & oldCap) == 0) {if (loTail == null)// 初始化head指向链表当前元素e,e不一定是链表的第一个元素,初始化后loHead// 代表下标保持不变的链表的头元素loHead = e;else                                // loTail.next指向当前eloTail.next = e;// loTail指向当前的元素e// 初始化后,loTail和loHead指向相同的内存,所以当loTail.next指向下一个元素时,// 底层数组中的元素的next引用也相应发生变化,造成lowHead.next.next.....// 跟随loTail同步,使得lowHead可以链接到所有属于该链表的元素。loTail = e;                           }else {if (hiTail == null)// 初始化head指向链表当前元素e, 初始化后hiHead代表下标更改的链表头元素hiHead = e;elsehiTail.next = e;hiTail = e;}} while ((e = next) != null);// 遍历结束, 将tail指向null,并把链表头放入新数组的相应下标,形成新的映射。if (loTail != null) {loTail.next = null;newTab[j] = loHead;}if (hiTail != null) {hiTail.next = null;newTab[j + oldCap] = hiHead;}}}}}return newTab;
}

putVal()中,我们看到在这个函数里面使用到了2次resize()方法resize()方法表示的在进行第一次初始化时会对其进行扩容,或者当该数组的实际大小大于或等于其阀值(第一次为12),这个时候在扩容的同时也会伴随的桶上面的元素进行重新分发,这也是JDK1.8版本的一个优化的地方,在1.7中扩容之后需要重新去计算其Hash值,根据Hash值对其进行分发,但在1.8版本中,则是根据在同一个桶的位置中进行判断(e.hash & oldCap)是否为0,重新进行hash分配后,该元素的位置要么停留在原始位置,要么移动到原始位置+增加的数组大小这个位置上

说到jdk1.7或jdk1.8,HashMap变化还挺大的,所以我们来探讨下两个jdk版本的区别:

七、HashMap在JDK1.7和JDK1.8中有哪些不同?HashMap的底层实现:

在Java中,保存数据有两种比较简单的数据结构:数组链表数组的特点是:寻址容易,插入和删除困难;链表的特点是:寻址困难,但插入和删除容易;所以我们将数组和链表结合在一起,发挥两者各自的优势,使用一种叫做拉链法的方式可以解决哈希冲突。

7、1 JDK1.8之前:

JDK1.8之前采用的是拉链法。拉链法:将链表和数组相结合。也就是说创建一个链表数组,数组中每一格就是一个链表。若遇到哈希冲突,则将冲突的值加到链表中即可。

7、2 JDK1.8之后:

相比于之前的版本,jdk1.8在解决哈希冲突时有了较大的变化,当链表长度大于或等于阈值(默认为8)时,将链表转化为红黑树,以减少搜索时间。

7、3 JDK1.7 VS JDK1.8 比较:

JDK1.8主要解决或优化了一下问题:

不同 JDK 1.7 JDK 1.8
存储结构 数组 + 链表 数组 + 链表 + 红黑树
初始化方式 单独函数:inflateTable() 直接集成到了扩容函数resize()中
hash值计算方式 扰动处理 = 9次扰动 = 4次位运算 + 5次异或运算 扰动处理 = 2次扰动 = 1次位运算 + 1次异或运算
存放数据的规则 无冲突时,存放数组;冲突时,存放链表 无冲突时,存放数组;冲突 & 链表长度 < 8:存放单链表;冲突 & 链表长度 > 8:树化并存放红黑树
插入数据方式 头插法(先讲原位置的数据移到后1位,再插入数据到该位置) 尾插法(直接插入到链表尾部/红黑树)
扩容后存储位置的计算方式 全部按照原来方法进行计算(即hashCode ->> 扰动函数 ->> (h&length-1)) 按照扩容后的规律计算(即扩容后的位置=原位置 or 原位置 + 旧容量)

八、HashMap是怎么解决哈希冲突的?

在解决这个问题之前,我们首先需要知道什么是哈希冲突,而在了解哈希冲突之前我们还要知道什么是哈希才行??

什么是哈希??

Hash,一般翻译为“散列”,也有直接音译为“哈希”的,这就是把任意长度的输入通过散列算法,变换成固定长度的输出,该输出就是散列值(哈希值);这种转换是一种压缩映射,也就是,散列值的空间通常远小于输入的空间,不同的输入可能会散列成相同的输出,所以不可能从散列值来唯一的确定输入值。简单的说就是一种将任意长度的消息压缩到某一固定长度的消息摘要的函数。

所有散列函数都有如下一个基本特性:根据同一散列函数计算出的散列值如果不同,那么输入值肯定也不同。但是,根据同一散列函数计算出的散列值如果相同,输入值不一定相同。

什么是哈希冲突?

当两个不同的输入值,根据同一散列函数计算出相同的散列值的现象,我们就把它叫做碰撞(哈希碰撞)。

HashMap的数据结构:

在Java中,保存数据有两种比较简单的数据结构:数组链表数组的特点是:寻址容易,插入和删除困难;链表的特点是:寻址困难,但插入和删除容易;所以我们将数组和链表结合在一起,发挥两者各自的优势,使用一种叫做链地址法的方式可以解决哈希冲突

这样我们就可以将拥有相同哈希值的对象组织成一个链表放在hash值所对应的bucket下,但相比于hashCode返回的int类型,我们HashMap初始的容量大小DEFAULT_INITIAL_CAPACITY = 1 << 4(即2的四次方16)要远小于int类型的范围,所以我们如果只是单纯的用hashCode取余来获取对应的bucket这将会大大增加哈希碰撞的概率,并且最坏情况下还会将HashMap变成一个单链表,所以我们还需要对hashCode作一定的优化。

hash()函数:

上面提到的问题,主要是因为如果使用hashCode取余,那么相当于参与运算的只有hashCode的低位,高位是没有起到任何作用的,所以我们的思路就是让hashCode取值出的高位也参与运算,进一步降低hash碰撞的概率,使得数据分布更平均,我们把这样的操作称为扰动,在JDK 1.8中的hash()函数如下:

static final int hash(Object key) {int h;return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);// 与自己右移16位进行异或运算(高低位异或)
}

这比在JDK 1.7中,更为简洁,相比在1.7中的4次位运算5次异或运算(9次扰动),在1.8中,只进行了1次位运算1次异或运算(2次扰动)

JDK1.8新增红黑树:

通过上面的链地址法(使用散列表)和扰动函数我们成功让我们的数据分布更平均哈希碰撞减少,但是当我们的HashMap中存在大量数据时,加入我们某个bucket下对应的链表有n个元素,那么遍历时间复杂度就为O(n),为了针对这个问题,JDK1.8HashMap中新增了红黑树的数据结构,进一步使得遍历复杂度降低至O(logn),即树的高度。

九、简单总结一下HashMap是使用了哪些方法来有效解决哈希冲突的:

十、扩展知识:

10、1 能否使用任何类作为 Map 的 key?

可以使用任何类作为 Map 的 key,然而在使用之前,需要考虑以下几点:

10、2 为什么HashMap中String、Integer这样的包装类适合作为Key?

答:String、Integer等包装类的特性能够保证Hash值的不可更改性和计算准确性,能够有效的减少Hash碰撞的几率。

10、3 如果使用Object作为HashMap的Key,应该怎么办呢?

答:重写hashCode()equals()方法。

10、4 HashMap为什么不直接使用hashCode()处理后的哈希值直接作为table的下标?

答:hashCode()方法返回的是int整数类型,其范围为-(2 ^ 31)~(2 ^ 31 - 1),约有40亿个映射空间,而HashMap的容量范围是在16(初始化默认值)~2 ^ 30,HashMap通常情况下是取不到最大值的,并且设备上也难以提供这么多的存储空间,从而导致通过hashCode()计算出的哈希值可能不在数组大小范围内,进而无法匹配存储位置。

那怎么解决呢?

10、5 HashMap 的长度为什么是2的幂次方?

为了能让 HashMap 存取高效,尽量较少碰撞,也就是要尽量把数据分配均匀,每个链表/红黑树长度大致相同。这个实现就是把数据存到哪个链表/红黑树中的算法。

这个算法应该如何设计呢?

我们首先可能会想到采用%取余的操作来实现。但是,重点来了:“取余(%)操作中如果除数是2的幂次则等价于与其除数减一的与(&)操作(也就是说 hash%length==hash&(length-1)的前提是 length 是2的 n 次方;)。” 并且 采用二进制位操作 &,相对于%能够提高运算效率,这就解释了 HashMap 的长度为什么是2的幂次方。

10、6 那为什么是两次扰动呢?

答:这样就是加大哈希值低位的随机性,使得分布更均匀,从而提高对应数组存储下标位置的随机性&均匀性,最终减少Hash冲突,两次就够了,已经达到了高位低位同时参与运算的目的。

十一、总结:

TreeMap

这个类名字里面有个 “Tree”,难道又是和树相关?没错,这个具体类就是依赖于红黑树构建的,可能有小伙伴会说了,怎么又是红黑树啊,其实红黑树是一种很有用的数据结构,只是维护的时候比一般的二叉搜索树复杂一点(主要是为了维护高度平衡以保证较高的查找效率),关于代码中怎么去维护它的高度我们就不去深追究了,为了篇幅简洁,同 HashMap 一样,我们在这里主要分析对键值对元素的相关操作原理和一些 TreeMap特有的性质。先看看它的源码声明:

public class TreeMap<K,V>extends AbstractMap<K,V>implements NavigableMap<K,V>, Cloneable, java.io.Serializable {// ...
}

TreeMap 本身继承自 AbstractMap抽象类,实现了 NavigableMap接口,这个 NavigableMap 接口实际上是继承了 SortedMap 接口,并声明了几个额外的方法。

下面看看 TreeMap的相关字段属性:

/*** The comparator used to maintain order in this tree map, or* null if it uses the natural ordering of its keys.** @serial*/
private final Comparator<? super K> comparator;// 红黑树的根结点
private transient Entry<K,V> root;// 当前 TreeMap 中节点(即键值对元素,下同)的数目
private transient int size = 0;// 当前 TreeMap 结构化修改(插入、删除元素...)的次数
private transient int modCount = 0;

这里再说一下 comparator 属性,我们知道:TreeMap 的实现原理是红黑树,即为一种二叉搜索树,为了简单起见,我们后面就把它当成二叉搜索树处理,二叉搜索树本身有一个特点:节点的左子树中的节点值都小于该节点值,而节点的右子树节点值都大于该节点值,那么我们在插入元素的时候如何判断插入的节点值和当前的节点值的大小呢?其实就是利用这个comparator字段,说白了,这个对象的任务就是为了判断出两个节点的大小关系的。

Comparator是提供了一个泛型参数的接口,我们来看看这个接口的定义:

public interface Comparator<T> {/*** @param o1 the first object to be compared.* @param o2 the second object to be compared.* @return a negative integer, zero, or a positive integer as the*         first argument is less than, equal to, or greater than the*         second.* 比较两个对象的大小,如果 o1 大于 o2,返回大于 0 的值,如果 o1 等于 o2,返回 0,* 如果 o1 小于 o2,返回小于 0 的值*/int compare(T o1, T o2);// ...
}

这个接口正好提供了一个方法,用于比较两个对象的大小
接下来看看 TreeMap 是用什么数据结构来描述键值对元素的:

static final class Entry<K,V> implements Map.Entry<K,V> {K key;V value;// 左右子节点和父节点Entry<K,V> left;Entry<K,V> right;Entry<K,V> parent;// 当前节点是否为红色节点(任意一个节点颜色要么红色要么黑色,因此叫红黑树),默认为黑色boolean color = BLACK;/*** Make a new cell with given key, value, and parent, and with* {@code null} child links, and BLACK color.*/Entry(K key, V value, Entry<K,V> parent) {this.key = key;this.value = value;this.parent = parent;}public K getKey() {return key;}public V getValue() {return value;}public V setValue(V value) {V oldValue = this.value;this.value = value;return oldValue;}public boolean equals(Object o) {if (!(o instanceof Map.Entry))return false;Map.Entry<?,?> e = (Map.Entry<?,?>)o;return valEquals(key,e.getKey()) && valEquals(value,e.getValue());}public int hashCode() {int keyHash = (key==null ? 0 : key.hashCode());int valueHash = (value==null ? 0 : value.hashCode());return keyHash ^ valueHash;}public String toString() {return key + "=" + value;}
}

一、TreeMap的构造方法详解:

我们上面已经说过了 TreeMap是基于红黑树的原理实现的,类比树节点的所需的字段,我们很好理解这个 Entry 类。下面来看看 TreeMap的构造方法:

public TreeMap() {comparator = null;
}public TreeMap(Comparator<? super K> comparator) {this.comparator = comparator;
}public TreeMap(SortedMap<K, ? extends V> m) {// 遵从指定的 SortedMap 的排序规则comparator = m.comparator();try {buildFromSorted(m.size(), m.entrySet().iterator(), null, null);} catch (java.io.IOException cannotHappen) {} catch (ClassNotFoundException cannotHappen) {}
}public TreeMap(Map<? extends K, ? extends V> m) {comparator = null;putAll(m);
}

仍然是 4 个构造方法,第一个和第二个构造方法没有什么讲的,我们先看看第三个构造方法,很明显这个构造方法用于创建一个具有和参数指定的 SortedMap 相同元素的TreeMap对象,实现上主要是通过调用 buildFromSorted方法,我们来看看这个方法:

private void buildFromSorted(int size, Iterator<?> it,java.io.ObjectInputStream str,V defaultVal)throws  java.io.IOException, ClassNotFoundException {this.size = size;root = buildFromSorted(0, 0, size-1, computeRedLevel(size),it, str, defaultVal);
}

这里调用了 buildFromSorted 参数重载的一个方法,并把返回值赋值给了 root,可以猜到这个是一个构建二叉树(其实就是构建红黑树)的过程,我们继续跟进去:

private final Entry<K,V> buildFromSorted(int level, int lo, int hi,int redLevel,Iterator<?> it,java.io.ObjectInputStream str,V defaultVal)throws  java.io.IOException, ClassNotFoundException {/*** 1、这其实是一个递归构建平衡二叉树(红黑树)的过程,* 在这个过程中,我们先需要构建出左子树并建立当前节点与左子树的父子关系,* 然后再构建出右子树并建立当前节点与右子树的父子关系系,* 最后再返回这个节点,显然,第一次调用这个方法时返回的节点即为整个树的根节点 root。* 2、我们已经知道这个方法调用的时候传入的是一个 SortedMap 的元素集合的迭代器,* 而本身 SortedMap 中的元素按照迭代器访问的时候是按照某种规则排好序的,即这个序列是有序的,* 那么我们在构建平衡二叉树(红黑树)的时候自然要将当前节点的左右子树的高度差不超过 1 ,* 即在构造的时候我们应该取中间元素作为当前的树的根结点,对于左右子树亦是如此*/if (hi < lo) return null;// 选取中间节点作为左右子树的根结点int mid = (lo + hi) >>> 1;Entry<K,V> left  = null;if (lo < mid)// 如果中间节点的左边有节点,那么先递归构造左子树left = buildFromSorted(level+1, lo, mid - 1, redLevel,it, str, defaultVal);// extract key and/or value from iterator or stream// 取迭代器元素的键和值用于创建节点K key;V value;// 从迭代器中取if (it != null) {if (defaultVal==null) {Map.Entry<?,?> entry = (Map.Entry<?,?>)it.next();key = (K)entry.getKey();value = (V)entry.getValue();} else {key = (K)it.next();value = defaultVal;}// 从流中读取} else { // use streamkey = (K) str.readObject();value = (defaultVal != null ? defaultVal : (V) str.readObject());}// 利用取到的键和值创建当前树的根结点(中间节点)Entry<K,V> middle =  new Entry<>(key, value, null);// color nodes in non-full bottommost level redif (level == redLevel)middle.color = RED;// 如果构造出来的左子树不为 null,那么建立当前根结点和左子树的父子关系if (left != null) {middle.left = left;left.parent = middle;}if (mid < hi) {// 如果中间节点的右边有节点,那么递归构造右子树Entry<K,V> right = buildFromSorted(level+1, mid+1, hi, redLevel,it, str, defaultVal);// 建立当前根结点和右子树的父子关系middle.right = right;right.parent = middle;}// 返回建立的树的根结点return middle;
}

二、TreeMap插入元素详解:

OK,我们将递归构造平衡二叉树的过程捋了一遍,第三个构造方法也就结束了,最后是第四个构造方法,可以看到直接调用了 putAll方法:

public void putAll(Map<? extends K, ? extends V> map) {int mapSize = map.size();// 如果当前 map 对象是 SortedMap 类型的对象,// 证明通过迭代器访问它的元素可以得到一个有序的元素序列,// 同样的使用 buildFromSorted 方法创建红黑树if (size==0 && mapSize!=0 && map instanceof SortedMap) {Comparator<?> c = ((SortedMap<?,?>)map).comparator();if (c == comparator || (c != null && c.equals(comparator))) {++modCount;try {buildFromSorted(mapSize, map.entrySet().iterator(),null, null);} catch (java.io.IOException cannotHappen) {} catch (ClassNotFoundException cannotHappen) {}return;}}// 否则利用调用父类的 putAll 方法super.putAll(map);
}

我们再一次看看其父类( AbstractMap)的 putAll方法:

public void putAll(Map<? extends K, ? extends V> m) {for (Map.Entry<? extends K, ? extends V> e : m.entrySet())put(e.getKey(), e.getValue());
}

很简单的逻辑,对每个元素调用了put方法,由于多态的特性,它会调用子类(即 TreeMap )的 put 方法,那么我们再在 TreeMap 中看一下其 put方法:

public V put(K key, V value) {Entry<K,V> t = root;if (t == null) {// 这一句是为了防止 key 为 null 的情况compare(key, key); // type (and possibly null) check// 如果当前 root 字段为 null,那么先创建 root 节点root = new Entry<>(key, value, null);size = 1;modCount++;return null;}int cmp;Entry<K,V> parent;// split comparator and comparable pathsComparator<? super K> cpr = comparator;// 如果当前 TreeMap 指定的 Comparator 对象不为 null,// 那么使用 TreeMap 的 Comparator 进行比较if (cpr != null) {do {parent = t;cmp = cpr.compare(key, t.key);// 结果小于 0 则作为左子结点if (cmp < 0)t = t.left;// 大于 0 则作为右子节点else if (cmp > 0)t = t.right;// 等于 0 则认为是键冲突,直接更新值并返回旧值elsereturn t.setValue(value);} while (t != null);}// 否则则调用 key 对象的 compareTo 方法比较(key 的类型必须实现 Comparable 接口)else {if (key == null)throw new NullPointerException();@SuppressWarnings("unchecked")Comparable<? super K> k = (Comparable<? super K>) key;do {parent = t;cmp = k.compareTo(t.key);// 小于 0 则作为左子结点,大于 0 则作为右子节点,等于 0 则直接更新值并返回旧值if (cmp < 0)t = t.left;else if (cmp > 0)t = t.right;elsereturn t.setValue(value);} while (t != null);}// 新建节点来储存这个键值信息Entry<K,V> e = new Entry<>(key, value, parent);// 建立父子关系if (cmp < 0)parent.left = e;elseparent.right = e;// 调整红黑树,保证其高度平衡fixAfterInsertion(e);size++;modCount++;return null;
}

三、TreeMap获取元素详解:

好了,看完了这个方法意味着我们把 TreeMap的构造方法看完了,下面来看看 get方法吧,其实如果你熟悉红黑树的话,基本上就能猜到 get 方法是怎么实现的了:

public V get(Object key) {Entry<K,V> p = getEntry(key);return (p==null ? null : p.value);
}

看来是通过 getEntry 方法实现的,那么继续跟进:

final Entry<K,V> getEntry(Object key) {// Offload comparator-based version for sake of performanceif (comparator != null)// 如果当前 TreeMap 指定的 Comparator 对象不为 null,// 那么通过当前 Treemap 的 Comparator 对象来进行元素大小比较return getEntryUsingComparator(key);if (key == null)throw new NullPointerException();@SuppressWarnings("unchecked")Comparable<? super K> k = (Comparable<? super K>) key;Entry<K,V> p = root;// 循环向下查找,如果要查找的 key 小于当前节点,// 那么向左子树继续查找,如果要查找的 key 大于当前节点,// 那么向右子树继续查找,否则的话证明找到了,返回当前节点while (p != null) {int cmp = k.compareTo(p.key);if (cmp < 0)p = p.left;else if (cmp > 0)p = p.right;elsereturn p;}// 没找到返回 nullreturn null;
}

TreeMapComparator对象不为 null 的时候是通过 getEntryUsingComparator方法查找的,那么继续看一下这个方法:

final Entry<K,V> getEntryUsingComparator(Object key) {@SuppressWarnings("unchecked")K k = (K) key;Comparator<? super K> cpr = comparator;if (cpr != null) {Entry<K,V> p = root;// 同样的,小于向左子树找,大于向右子树找,等于证明找到了,直接返回while (p != null) {int cmp = cpr.compare(k, p.key);if (cmp < 0)p = p.left;else if (cmp > 0)p = p.right;elsereturn p;}}// 没找到返回 nullreturn null;
}

到了这里,我们把 get方法也看完了,我想你也明白了为什么 get 方法查找值的时间复杂度为 O(logn)了,因为 get 方法的时间复杂度主要取决于 while 循环的执行次数,很明显,这里的 while 循环的执行次数为树的高度,即 logn。最后,来看一下移除元素的方法:

public V remove(Object key) {// 现寻找要删除的节点,找到了进行删除,没找到就直接返回Entry<K,V> p = getEntry(key);if (p == null)return null;V oldValue = p.value;// 进行节点的删除并调整树的结构以保证红黑树的高度平衡deleteEntry(p);// 返回删除的节点的 “值”return oldValue;
}

好了,remove方法的流程上就到这里了,这里就不过细的介绍红黑树平衡的维护具体过程了,想要了解的小伙伴们可以参考本博主的这篇博客:sff。
在元素遍历方面, TreeMapHashMap提供的遍历元素的方法还是差不多的:通过 entrySetkeySetforEach(JDK 1.8)方法,这里就不列举了,有兴趣的小伙伴参考一下 HashMap 部分的元素遍历: HashMap遍历元素详解。

四、扩展知识:

4、1 如何指定 TreeMap 的元素排序方式?

我们已经知道 TreeMap默认会依据键值对元素的键来对元素进行排序。我们也可以通过自定义的Comparator 接口对象来指定其对键的排序方式,那么可不可以通过指定对元素的值的排序方式来对元素进行排序呢?答案是可以的,不过需要动一点脑筋:我们可以利用 TreeMap 会利用键来对键值对元素进行排序的特点,来自定义一个“键的包装类”来作为新的键,我们就叫它 KeyWrap 吧,这个KeyWrap内部有两个引用,分别指向原本的 KeyValue两个属性,我们使得这个类实现Comparable 接口,并且重写其 compareTo方法,这个方法直接调用 ValuecompareTo 方法作为返回值。同时,因为 TreeMap 本身需要用到 Key 的 equals 方法来进行键的等价比较,因此我们实现这两个方法并且调用对应键的方法来作为返回值。

好了,思路就到这里了,下面来看看代码:

import java.util.Map;
import java.util.TreeMap;public class CustomTreeMapSortMethod {// 自定义的描述键的类static class MyKey {int i;MyKey(int i) {this.i = i;}@Overridepublic int hashCode() {return i;}@Overridepublic boolean equals(Object obj) {return obj == this || obj instanceof MyKey && ((MyKey) obj).i == i;}@Overridepublic String toString() {return String.valueOf(i);}}// 自定义的描述值的类,实现 Comparable 接口来自定义排序方式static class MyValue implements Comparable<MyValue> {char c;MyValue(char c) {this.c = c;}// 按属性 c 的值从小到大排序@Overridepublic int compareTo(MyValue o) {if (o == null) {throw new IllegalArgumentException("The argument other can not be null!");}return c - o.c;}@Overridepublic String toString() {return String.valueOf(c);}}// 自定义的 KeyWrap 类,实现 Comparable 接口来自定义排序方式static class KeyWrap implements Comparable<KeyWrap> {// 指向 key 的引用,这里也可以采用直接继承 MyKey 类的方法,// 这样的可以不用重写 equals 方法MyKey key;MyValue val; // 指向 value 的引用KeyWrap(MyKey key, MyValue val) {this.key = key;this.val = val;}int compare(KeyWrap other) {if (other == null) {throw new IllegalArgumentException("The argument other can not be null!");}// 因为要依据“值”来进行排序,所以返回 “值”比较的结果return val.compareTo(other.val);}@Overridepublic int compareTo(KeyWrap o) {return compare(o);}// 因为是自定义的 KeyWrap ,真正的 key 还是 MyKey 类对象,// 所以 equals 方法还得用 MyKey 类的对象来进行比较,即 key 属性@Overridepublic boolean equals(Object obj) {return obj == this || obj instanceof KeyWrap && ((KeyWrap) obj).key.equals(key);}}public static void main(String[] args) {int eleLen = 10;// 注意这里使用的 KeyWrap 类型来作为键TreeMap<KeyWrap, MyValue> map = new TreeMap<>();MyKey[] keys = new MyKey[eleLen];MyValue[] values = new MyValue[eleLen];KeyWrap[] keyWraps = new KeyWrap[eleLen];for (int i = 0; i < eleLen; i++) {keys[i] = new MyKey(eleLen - 1 - i);values[i] = new MyValue((char) (i + '0'));keyWraps[i] = new KeyWrap(keys[i], values[i]);map.put(keyWraps[i], values[i]);}for (Map.Entry<KeyWrap, MyValue> entry : map.entrySet()) {System.out.println("key: " + entry.getKey().key + ", value: " + entry.getValue());}}
}

来看看结果:

OK,成功的验证了我们的想法,代码中也给出了详细的注释,借助这个思想,我们完全可以通过继承 TreeMap来封装一个按照值来对元素进行排序的 ValueSortTreeMap,有兴趣的小伙伴们可以自己尝试实现一下。

4、2 如何决定使用 HashMap 还是 TreeMap?

对于在Map中插入删除定位元素这类操作,HashMap是最好的选择。然而,假如你需要对一个有序的key集合进行遍历TreeMap是更好的选择。基于你的collection的大小,也许向HashMap中添加元素会更快,将map换为TreeMap进行有序key的遍历

4、3 TreeMap 和 TreeSet 在排序时如何比较元素?Collections 工具类中的 sort()方法如何比较元素?

TreeSet要求存放的对象所属的必须实现 Comparable 接口,该接口提供了比较元素的 compareTo()方法,当插入元素时会回调该方法比较元素的大小。TreeMap 要求存放的键值对映射的必须实现 Comparable接口从而根据键对元素进行排序。

Collections 工具类的 sort 方法有两种重载的形式:

五、总结:

LinkedHashMap

回想我们使用 HashMap 时,元素的遍历顺序和插入顺序是不一定相同的,通过前篇的源码解读我们也知道了原因:HashMap 内部使用键值对数组来储存元素,对于每一个键值对,其在数组中的下标完全取决于其键的哈希值(hashCode),而我们在通过迭代器遍历 HashMap 的时候实际上相当于顺序遍历其内部的元素储存数组。那么如果我们需要使得元素的遍历顺序和插入顺序相同时 HashMap 就不能很好的实现这个功能了。这个时候就可以通过 LinkedHashMap 来完成这个功能。下面我们一起来看看这个类:

PS:再附上这个图,方便理解下哈!!!

public class LinkedHashMap<K,V> extends HashMap<K,V> implements Map<K,V> {/*** LinkedHashMap 中表示键值对元素节点的类,继承于 HashMap.Node*/static class Entry<K,V> extends HashMap.Node<K,V> {Entry<K,V> before, after; // 当前键值对元素节点的前继和后继节点Entry(int hash, K key, V value, Node<K,V> next) {super(hash, key, value, next);}}// 和序列化和反序列化有关,暂时不管private static final long serialVersionUID = 3801124242820219131L;/***  LinkedHashMap 中双向链表的头结点(首元结点)*/transient LinkedHashMap.Entry<K,V> head;/*** LinkedHashMap 中双向链表的尾节点*/transient LinkedHashMap.Entry<K,V> tail;/*** 链表元素的排序依据:按访问顺序排序(true),按插入顺序排序(false)*/final boolean accessOrder;// ...
}

从这里我们大概可以知道了,LinkedHashMap 内部通过双向链表来维持元素的顺序,同时其继承于 HashMap ,因此可以猜测 LinkedHashMap 的一些操作时复用父类的。

而查看 LinkedHashMap 的结构,发现很多对元素的操作方法都没有直接提供:

可以看到,类似于 putremove 方法都没有在 LinkedHashMap中提供,但是我们在使用 LinkedHashMap 的时候都是直接使用这些方法来操作元素,那么很显然其是复用了父类(HashMap) 的相关方法

如此一来,新的问题又产生了:HashMap 本身在操作元素(插入、删除)时候是并没有考虑元素的插入顺序的,其是通过要插入的键值对元素的键的hashCode (哈希值)来决定元素的插入位置,那么 LinkedHashMap 是怎么实现元素访问顺序和插入顺序相同的功能呢?

一、LinkedHashMap插入元素详解:

对于上面的问题,我们还是看看 HashMap 的源码:

// Callbacks to allow LinkedHashMap post-actions
void afterNodeAccess(Node<K,V> p) { }
void afterNodeInsertion(boolean evict) { }
void afterNodeRemoval(Node<K,V> p) { }

可以发现,HashMap 中提供了 3 个方法供 LinkedHashMap重写,在 HashMap 的 putVal(HashMap 的 put 方法中会调用)方法中还会调用 newNodenewTreeNode 方法,我们来看看:

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)// 如果是树节点,那么创建 TreeNodee = ((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 1sttreeifyBin(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 keyV oldValue = e.value;if (!onlyIfAbsent || oldValue == null)e.value = value;afterNodeAccess(e);return oldValue;}}// ...

在上述源码中:

// 如果是树节点,那么创建 TreeNode
e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);

putTreeVal 方法中会调用 newTreeNode 方法,显然,这两个方法(newNode、newTreeNode)都是用来新建键值对元素的,关于上述流程如果小伙伴们还不清楚,可以参考上面对HashMap的讲解: HashMap

而对于这5 个方法LinkedHashMap 中也对他们进行了重写

顺便提一下,这 5个方法在 HashMap 中都是默认修饰符的,我们知道,默认修饰符的属性只能被同一个类文件或者同一个包中的其他类访问,子类是没办法访问的(没有可见性),这里 LinkedHashMap 是 HashMap 的子类,从这个角度上来说,其是没有对这 5 个方法的访问权的(可以理解为它根本看不到父类的这 5 个方法),但是它还有另外一重身份和 HashMap 同包(HashMap 和 LinkedHashMap 都是在 java.utl 包中),因此从这方面来说,其可以对这 5 个方法进行重写

那么我们看一下这 5 个方法在 LinkedHashMap中的源码,先是 newNode

Node<K,V> newNode(int hash, K key, V value, Node<K,V> e) {LinkedHashMap.Entry<K,V> p =new LinkedHashMap.Entry<K,V>(hash, key, value, e);linkNodeLast(p);return p;
}

这个方法新建的是LinkedHashMap 提供的表示双向链表节点的类对象,之后调用了 linkNodeLast 方法来连接这个新建的链表元素,不用看我们也知道这个方法将新建的节点连接到链表尾部

// link at the end of list
private void linkNodeLast(LinkedHashMap.Entry<K,V> p) {LinkedHashMap.Entry<K,V> last = tail;tail = p;// 如果当前链表的尾节点为 null,证明当前链表还没有元素,// 因此将 head 赋值为 p,这里换成 head == null 判断也可以,写法不同而已。if (last == null)head = p;else {p.before = last;last.after = p;}
}

这样一来在调用 LinkedHashMap 的 put 方法(实际上调用的是 HashMap 的 put 方法)时 LinkedHashMap 就可以初步保证元素的顺序和插入顺序是相同的(put -> putVal -> newNode -> linkNodeLast)。

二、LinkedHashMap删除元素详解:

为什么是初步保证?因为能改变链表的还有删除元素的操作呀。那么我们来看看 HashMap 的 remove 方法

public V remove(Object key) {Node<K,V> e;return (e = removeNode(hash(key), key, null, false, true)) == null ?null : e.value;
}

可以看到,直接调用了 removeNode方法:

final Node<K,V> removeNode(int hash, Object key, Object value,boolean matchValue, boolean movable) {Node<K,V>[] tab; Node<K,V> p; int n, index;if ((tab = table) != null && (n = tab.length) > 0 &&(p = tab[index = (n - 1) & hash]) != null) {Node<K,V> node = null, e; K k; V v;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 = e;} while ((e = e.next) != null);}}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;elsep.next = node.next;++modCount;--size;// !! 这里afterNodeRemoval(node);return node;}}return null;
}

在移除元素之后,HashMap调用了afterNodeRemoval方法,这不就是在 LinkedHashMap 中重写方法吗:

// LinkedHashMap 类的方法:
void afterNodeRemoval(Node<K,V> e) { // unlinkLinkedHashMap.Entry<K,V> p =(LinkedHashMap.Entry<K,V>)e, b = p.before, a = p.after;p.before = p.after = null;if (b == null)head = a;elseb.after = a;if (a == null)tail = b;elsea.before = b;}

不用看我们也能猜到:既然移除了一个元素,自然要把这个元素从链表中移除,这样才能维护链表顺序的正确性。

好了,通过这里的几个方法,LinkedHashMap 就可以保证链表中元素的顺序是按照插入元素的顺序来排序的。到这里可能又有小伙伴会问了:那么还有两个方法(afterNodeInsertionafterNodeAccess)呢?还记得在开头给大家介绍 LinkedHashMap的源码构成中有一个 accessOrder 属性吗?这两个方法就是和这个属性有关,这里允许我小小的买个官子。

我们先来看 LinkedHashMap 是怎么遍历元素的,之后在来看这两个方法:

三、LinkedHashMap遍历元素详解:

和其他 Map 一样,LinkedHashMap 也是通过迭代器(Iterator)来遍历元素的,当然,在以迭代器作为基础的情况下,其为我们提供了两种方式来遍历元素:

// 得到键的集合,之后通过 get 方法取到对应值
public Set<K> keySet() {Set<K> ks;return (ks = keySet) == null ? (keySet = new LinkedKeySet()) : ks;
}// 得到键值对的集合,之后通过 getKey() 和 getValue() 方法得到键值
public Set<Map.Entry<K,V>> entrySet() {Set<Map.Entry<K,V>> es;return (es = entrySet) == null ? (entrySet = new LinkedEntrySet()) : es;
}

可以看到:两个方法分别返回了一个 LinkedKeySet 对象和 LinkedEntrySet对象,我们来看看这两个类:

LinkedKeySet:

final class LinkedKeySet extends AbstractSet<K> {public final int size()                 { return size; }public final void clear()               { LinkedHashMap.this.clear(); }// 返回了一个 LinkedKeyIterator 迭代器对象public final Iterator<K> iterator() {return new LinkedKeyIterator();}public final boolean contains(Object o) { return containsKey(o); }public final boolean remove(Object key) {return removeNode(hash(key), key, null, false, true) != null;}public final Spliterator<K> spliterator()  {return Spliterators.spliterator(this, Spliterator.SIZED |Spliterator.ORDERED |Spliterator.DISTINCT);}public final void forEach(Consumer<? super K> action) {if (action == null)throw new NullPointerException();int mc = modCount;for (LinkedHashMap.Entry<K,V> e = head; e != null; e = e.after)action.accept(e.key);if (modCount != mc)throw new ConcurrentModificationException();}
}

LinkedEntrySet:

final class LinkedEntrySet extends AbstractSet<Map.Entry<K,V>> {public final int size()                 { return size; }public final void clear()               { LinkedHashMap.this.clear(); }// 返回了一个 LinkedEntryIterator 对象public final Iterator<Map.Entry<K,V>> iterator() {return new LinkedEntryIterator();}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 Spliterators.spliterator(this, Spliterator.SIZED |Spliterator.ORDERED |Spliterator.DISTINCT);}public final void forEach(Consumer<? super Map.Entry<K,V>> action) {if (action == null)throw new NullPointerException();int mc = modCount;for (LinkedHashMap.Entry<K,V> e = head; e != null; e = e.after)action.accept(e);if (modCount != mc)throw new ConcurrentModificationException();}
}

可以看到,两个类返回了两个迭代器对象,我们来看看这两个类:

final class LinkedKeyIterator extends LinkedHashIterator implements Iterator<K> {public final K next() { return nextNode().getKey(); }
}final class LinkedEntryIterator extends LinkedHashIterator implements Iterator<Map.Entry<K,V>> {public final Map.Entry<K,V> next() { return nextNode(); }
}

都继承于 LinkedHashIterator类,并且都调用了nextNode 方法,那么来看看吧(LinkedHashIterator中定义的方法):

final LinkedHashMap.Entry<K,V> nextNode() {LinkedHashMap.Entry<K,V> e = next;if (modCount != expectedModCount)throw new ConcurrentModificationException();if (e == null)throw new NoSuchElementException();current = e;next = e.after;return e;
}

这个方法就很容易理解,就是根据已经有的双向链表的来顺序遍历元素。至此 LinkedHashMap 储存元素的方式保持元素的遍历顺序和插入顺序相同的元素原理分析完毕。最后配张图来加深理解一下:

四、缓存控制:

最后,来看一下上面说的 accessOrder 属性,LinkedHashMap 的构造支持我们对这个属性进行赋值

public LinkedHashMap(int initialCapacity,float loadFactor,boolean accessOrder) {super(initialCapacity, loadFactor);this.accessOrder = accessOrder;
}

那么这个值到底有什么作用呢?我们通过实验来看一下:

public class Main {public static void main(String[] args) {int capacity = 4;LinkedHashMap<String, Integer> linkedHashMap =new LinkedHashMap<>(capacity, 0.75f, true);for (int i = 0; i < capacity; i++) {linkedHashMap.put(i + "", i);}System.out.println("第一次遍历:");Set<Map.Entry<String, Integer>> set = linkedHashMap.entrySet();for (Map.Entry e : set) {System.out.println(e.getKey() + ", " + e.getValue());}// 这里读取了一次元素值linkedHashMap.get("0");linkedHashMap.get("1");System.out.println("第二次遍历:");for (Map.Entry e : set) {System.out.println(e.getKey() + ", " + e.getValue());}}
}

结果:

可以看到,第二次遍历时,元素顺序不是我们插入的顺序了,“0” 和 “1” 对应的元素被放到后面去了,由此我们也知道了,accessOrder 为 true 时会将已经访问的元素放到链表末尾。在 LinkedHashMap 对应的实现方法如下(从 HashMap 中继承而来):

// 该方法只是改变了双向链表中节点的顺序,将节点移至链表尾部
void afterNodeAccess(Node<K,V> e) { // move node to lastLinkedHashMap.Entry<K,V> last;if (accessOrder && (last = tail) != e) {LinkedHashMap.Entry<K,V> p =(LinkedHashMap.Entry<K,V>)e, b = p.before, a = p.after;p.after = null;if (b == null)head = a;elseb.after = a;if (a != null)a.before = b;elselast = b;if (last == null)head = p;else {p.before = last;last.after = p;}tail = p;++modCount;}
}

最后,上面的 LinkedHashMap 中重写的HashMap5 个方法中剩下最后一个 afterNodeInsertion 方法了,我们来看看:

void afterNodeInsertion(boolean evict) { // possibly remove eldestLinkedHashMap.Entry<K,V> first;// 移除链表最老的节点(因为采用尾插法建立双向链表,因此头结点是最老的节点)if (evict && (first = head) != null && removeEldestEntry(first)) {K key = first.key;removeNode(hash(key), key, null, false, true);}
}

这个方法在插入元素之后(putVal 之中会调用,即在元素插入完成之后会调用),并且传递的参数evict 值为 true,这里面将 removeEldestEntry 方法的返回值作为一个条件判断,看看这个方法返回什么:

/*** Returns <tt>true</tt> if this map should remove its eldest entry.* This method is invoked by <tt>put</tt> and <tt>putAll</tt> after* inserting a new entry into the map.  It provides the implementor* with the opportunity to remove the eldest entry each time a new one* is added.  This is useful if the map represents a cache: it allows* the map to reduce memory consumption by deleting stale entries.** <p>Sample use: this override will allow the map to grow up to 100* entries and then delete the eldest entry each time a new entry is* added, maintaining a steady state of 100 entries.* <pre>*     private static final int MAX_ENTRIES = 100;**     protected boolean removeEldestEntry(Map.Entry eldest) {*        return size() &gt; MAX_ENTRIES;*     }* </pre>** <p>This method typically does not modify the map in any way,* instead allowing the map to modify itself as directed by its* return value.  It <i>is</i> permitted for this method to modify* the map directly, but if it does so, it <i>must</i> return* <tt>false</tt> (indicating that the map should not attempt any* further modification).  The effects of returning <tt>true</tt>* after modifying the map from within this method are unspecified.** <p>This implementation merely returns <tt>false</tt> (so that this* map acts like a normal map - the eldest element is never removed).*/
protected boolean removeEldestEntry(Map.Entry<K,V> eldest) {return false;
}

这个方法直接返回了 false,并且它是 protected 修饰的,因此可以被子类重写,假设我们自定义一个子类并且将这个方法重写返回 true的话,在上面的代码中就会调用(当链表头结点不为 null 时removeNode 将新添加的节点移除,这样的话LinkedHashMap 就是一个没有任何元素的空链表,来看看实践代码:

public class Main {static class MyLinkedHashMap<K, V> extends LinkedHashMap<K, V> {MyLinkedHashMap(int capacity) {super(capacity);}@Overrideprotected boolean removeEldestEntry(Map.Entry eldest) {return true;}}public static void main(String[] args) {int capacity = 4;LinkedHashMap<String, Integer> linkedHashMap =new MyLinkedHashMap<>(capacity);for (int i = 0; i < capacity; i++) {linkedHashMap.put(i + "", i);}Set<Map.Entry<String, Integer>> set = linkedHashMap.entrySet();for (Map.Entry e : set) {System.out.println(e.getKey() + ", " + e.getValue());}}
}

结果:

确实证实了我们的想法,至于这个有什么作用,根据 removeEldestEntry 的介绍来看,主要是用于当 LinkedHashMap 作为缓存映射时,可以节省内存而设计的

其实我们熟悉的 LRU 缓存算法就可以通过 LinkedHashMap 中提供的 accessOrderremoveEldestEntry 方法来实现,关于 LRU 算法的相关介绍小伙伴们可以参考这篇文章:缓存淘汰算法–LRU算法。

我们知道 LRU 算法的缓存的思想是每次有新元素加入时,淘汰最近最少被使用的元素。其核心思想是 如果数据最近被访问过,那么将来被访问的几率也更高。也就是说每当元素被访问时,LRU 就将该元素移至缓存队列顶部,而每次如果需要淘汰元素时,LRU 将缓存队列底部的元素淘汰。而在 LinkedHashMap 中,我们可以通过accessOrder属性来控制将每次访问的元素移至链表尾部,通过 removeEldestEntry 方法来控制是否移除链表头部节点,只是将链表尾部看成了 LRU 中缓存队列的顶部,将链表头部看成了 LRU 中缓存队列的底部。关于通过 LinkedHashMap 实现 LRU 的具体代码可以参考:缓存算法的实现 。

LinkedHashMap 的相关介绍就到这里了,下面来看看Hashtable 的实现机制:

Hashtable

一、Hashtable详解:

这个类其实已经被标为遗留类(Legacy),也就是说这个类已经不建议使用了。这里还是简单介绍一下这个类。其实我一直对这个类的类名有些见解:按照驼峰式命名的方法,其应该是 HashTable ,但是它现在就是被命名为了Hashtable 。这个类类似于 HashMap,不过它相对于 HashMap而言其中的相关操作元素的方法名前多用了一个 synchronized 关键字修饰,也就是说这个类是多线程安全的,来看看一些方法:

// 使用了 synchronized 关键字修饰,使得方法是线程安全的
public synchronized V put(K key, V value) {// Make sure the value is not nullif (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;@SuppressWarnings("unchecked")Entry<K,V> entry = (Entry<K,V>)tab[index];// 处理当前的 hash 值可能存在 Entry 对象冲突的情况,这里其实就是遍历单链表for(; entry != null ; entry = entry.next) {if ((entry.hash == hash) && entry.key.equals(key)) {V old = entry.value;entry.value = value;return old;}}// 在这个方法中添加新的 Entry 对象并考虑扩容addEntry(hash, key, value, index);return null;
}

再看看 addEntry方法:

// 因为 addEntry 方法的调用方法中已经做了线程同步处理(例如 put 方法),
// 因此这里无需再用 synchronized 关键字修饰
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// 进行扩容,这个方法中每次扩容的容量为之前容量的 2 倍 + 1rehash();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++;
}

同样的,其内部也提供了Entry 类来描述键值对元素

/*** Hashtable bucket collision list entry*/
private static class Entry<K,V> implements Map.Entry<K,V> {final int hash;final K key;V value;Entry<K,V> next; // hash 值冲突的下一个元素(采用单链表来处理冲突)protected Entry(int hash, K key, V value, Entry<K,V> next) {this.hash = hash;this.key =  key;this.value = value;this.next = next;}// ...
}

好了,这个类的介绍就到这里了,这里总结一下这个类的相关信息:默认初始容量为 11扩容因子为 0.75,每次扩容后的容量变为之前容量的 2 倍 + 1。上面说这个类已经不被推荐使用了。

那么有什么类可以替代这个类吗?答案是有的。这里介绍两个方法来得到可以保证线程安全的 Map :

1、通过 Collections类中的 synchronizedMap方法来得到一个保证线程安全的 Map,方法声明如下:

public static <K,V> Map<K,V> synchronizedMap(Map<K,V> m);

这是一个静态的方法,返回一个线程安全的 Map,这个方法只是对参数中的 Map 对象进行了一下包装,返回了一个新的 Map 对象,将参数中的 Map 对象的相关操作方法都通过使用 synchronized 关键字修饰的方法包装了一下,但是具体的操作流程还是和原来的 Map 对象一样,来看一个方法的源码:

public V put(K key, V value) {synchronized (mutex) {return m.put(key, value);}
}

可以看到,m才是参数指定的Map 对象put 方法是返回的 Map对象的方法。

2、使用ConcurrentHashMap 类,这个类是JDK1.5 新增的一个类,可以非常高效的进行相关的元素操作,同时还保证多线程安全。内部实现非常巧妙,简单来说就是内部有多个互斥锁每个互斥锁负责一段区域,举个例子:

假设现在内部有 100 个元素,即有一个长度为 100 的元素数组,那么 ConcurrentHashMap 提供了10 个锁,每个锁负责 10 个元素0~9, 10~19, , 90~99),每当有线程操作某个元素时,通过这个元素的键的 hash 值可以得到其操作的是哪个区域,之后就锁住对应区域的锁对象即可,而其他区域的元素依然可以被其他线程访问。这就好比一个厕所,里面有多个位置,每个位置每次只能有一个人上厕所,但是不能因为这一个人上厕所就把整个厕所给锁掉,所以就是每个位置设置一把锁,对应只负责这个位置即可。

二、ConcurrentHashMap 和 Hashtable 的区别?

ConcurrentHashMapHashtable 的区别主要体现在实现线程安全的方式上不同

两者的对比图:

Hashtable:

JDK1.7的ConcurrentHashMap:

JDK1.8的ConcurrentHashMap(TreeBin: 红黑二叉树节点 Node: 链表节点):

总结:ConcurrentHashMap 结合了 HashMapHashtable 二者的优势。HashMap 没有考虑同步Hashtable 考虑了同步的问题。但是 Hashtable 在每次同步执行时都要锁住整个结构ConcurrentHashMap 锁的方式是稍微细粒度的。

三、ConcurrentHashMap 底层具体实现知道吗?实现原理是什么?

JDK1.7:

首先将数据分为一段一段的存储,然后给每一段数据配一把锁,当一个线程占用锁访问其中一个段数据时,其他段的数据也能被其他线程访问。

JDK1.7中,ConcurrentHashMap采用Segment + HashEntry的方式进行实现,结构如下:

一个 ConcurrentHashMap 里包含一个Segment 数组Segment的结构和HashMap类似,是一种数组和链表结构,一个 Segment 包含一个 HashEntry 数组,每个 HashEntry 是一个链表结构的元素,每个 Segment 守护着一个HashEntry数组里的元素,当对 HashEntry 数组的数据进行修改时,必须首先获得对应的 Segment的锁。

1、该类包含两个静态内部类 HashEntrySegment;前者用来封装映射表的键值对,后者用来充当锁的角色

2、Segment 是一种可重入的锁 ReentrantLock,每个 Segment 守护一个HashEntry 数组里得元素,当对 HashEntry 数组的数据进行修改时,必须首先获得对应的 Segment 锁。

JDK1.8:

JDK1.8中,放弃了Segment臃肿的设计,取而代之的是采用Node + CAS + Synchronized来保证并发安全进行实现,synchronized只锁定当前链表红黑二叉树的首节点,这样只要hash不冲突,就不会产生并发,效率又提升N倍。

结构如下:

附加源码,有需要的可以看看:

插入元素过程(建议去看看源码):

else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) {if (casTabAt(tab, i, null, new Node<K,V>(hash, key, value, null)))break;                   // no lock when adding to empty bin
}

如果相应位置的Node还没有初始化,则调用CAS插入相应的数据。

if (fh >= 0) {binCount = 1;for (Node<K,V> e = f;; ++binCount) {K ek;if (e.hash == hash &&((ek = e.key) == key ||(ek != null && key.equals(ek)))) {oldVal = e.val;if (!onlyIfAbsent)e.val = value;break;}Node<K,V> pred = e;if ((e = e.next) == null) {pred.next = new Node<K,V>(hash, key, value, null);break;}}
}

1、如果相应位置的Node不为空,且当前该节点不处于移动状态,则对该节点加synchronized锁,如果该节点的hash不小于0,则遍历链表更新节点或插入新节点。
2、如果该节点是TreeBin类型的节点,说明是红黑树结构,则通过putTreeVal方法往红黑树插入节点;如果binCount不为0,说明put操作对数据产生了影响,如果当前链表的个数达到8个,则通过treeifyBin方法转化为红黑树,如果oldVal不为空,说明是一次更新操作,没有对元素个数产生影响,则直接返回旧值
3、如果插入的是一个新节点,则执行addCount()方法尝试更新元素个数baseCount

四、扩展知识:

4、1 HashMap 与 Hashtable 有什么区别?

推荐使用:在 Hashtable 的类注释可以看到,Hashtable保留类不建议使用,推荐在单线程环境下使用HashMap替代,如果需要多线程使用则用 ConcurrentHashMap替代。

五、总结:

WeakHashMap

一、弱引用对象和弱引用队列详解:

我们知道, WeakHashMap是基于弱引用实现的,在开始看 WeakHashMap 之前,希望小伙伴们对 Java 中的弱引用和引用队列有一定的了解,如果对弱引用及引用队列相关的知识点还不太熟悉,可以参考 详解 Java 的四种引用。为了方便理解接下来的内容,这里简单的介绍一下弱引用的作用:在 Java 中,弱引用是强度次于软引用的一种引用形式,JVM 垃圾回收器(Garbage Collector)在每一次执行垃圾回收动作时会将所有 有且仅有被引用强度不高于弱引用(即弱引用和虚引用) 指向的对象回收。那么我们很容易知道:一个仅被弱引用指向的对象时是不会导致OutOfMenoryError 异常的。在 JDK 1.2 之后,提供了 WeakReference类来实现弱引用,相关源码如下:

public class WeakReference<T> extends Reference<T> {public WeakReference(T referent) {super(referent);}/*** Creates a new weak reference that refers to the given object and is* registered with the given queue.** @param referent object the new weak reference will refer to* @param q the queue with which the reference is to be registered,*          or <tt>null</tt> if registration is not required*/public WeakReference(T referent, ReferenceQueue<? super T> q) {super(referent, q);}}

可以看到: WeakReference 类提供了两个构造方法,其中第二个构造方法提供了一个 ReferenceQueue 类型的参数,顾名思义,这个参数代表的是引用队列,即指定当前弱引用对象的引用队列,这个队列有什么用呢?简单来说就是当 JVM 回收某个弱引用指向的对象时,先会将该弱引用加入其构造时指定的引用队列(如果有的话)中去(这个过程由 JVM 的垃圾回收线程完成,无需开发者控制),这样的话我们就可以通过这个引用队列取到对应的引用对象,就可以知道哪个对象被回收了,进而做出对应的处理。知道了这个概念之后,我们来看看 WeakHashMap 是如何利用弱引用来管理元素的:

先来看看在 WeakHashMap 中如何表示一个键值对元素:

/*** The entries in this hash table extend WeakReference, using its main ref* field as the key.*/
private static class Entry<K,V> extends WeakReference<Object> implements Map.Entry<K,V> {V value;final int hash;// 指向和当前 Entry 具有相同 hash 值的下一个 Entry 对象,// WeakHashMap 采用链地址法处理 hash 值冲突的情况Entry<K,V> next; Entry(Object key, V value,ReferenceQueue<Object> queue,int hash, Entry<K,V> next) {// 注意这里的 super 调用,为 key 对象建立一个弱引用对象指向 key,// 这样当 key 对象被回收之后,JVM 会将此处指向 key 对象的弱引用加入 queue 引用队列中super(key, queue);this.value = value;this.hash  = hash;this.next  = next;}// ...
}

WeakHashMap 内部提供了一个继承于 WeakReference 的类 Entry 来表示一个键值对元素。而其构造方法中也提供了一个 ReferenceQueue类型的参数,即为指定当前 Entrykey引用队列。而这个方法仅在 WeakHashMapput方法中调用:

public V put(K key, V value) {Object k = maskNull(key);int h = hash(k);Entry<K,V>[] tab = getTable();int i = indexFor(h, tab.length);for (Entry<K,V> e = tab[i]; e != null; e = e.next) {// 如果当前的 key 已经存在 table 中,那么直接更新对应的 valueif (h == e.hash && eq(k, e.get())) {V oldValue = e.value;1if (value != oldValue)e.value = value;return oldValue;}}modCount++;Entry<K,V> e = tab[i];// 新建一个 Entry 对象表示键值对元素,同时处理可能存在的 hash 值冲突情况(头插法建立冲突链表)tab[i] = new Entry<>(k, value, queue, h, e);// 判断是否需要扩容(当前容量是否达到扩容阀值)if (++size >= threshold)// 翻倍扩容resize(tab.length * 2);return null;
}

关于 WeakHashMap 如何插入元素这里不再细讲,之前的文章中已经非常详细的讲解了相关的 Map 具体类如何进行元素插入。这里创建 Entry对象时传入的引用队列对象是一个 WeakHashMap 的类成员变量:

/*** Reference queue for cleared WeakEntries*/
private final ReferenceQueue<Object> queue = new ReferenceQueue<>();

也就是说:WeakHashMap 中所有的 Entry 对象中指向key 对象的弱引用共用一个引用队列,既然这样我们可以知道:WeakHashMap 中任何一个 Entry 对象中的 key 对象将要被回收时,这里创建的弱引用对象都会被加入 queue 引用队列中。我们之后就可以从 queue 引用队列中获取到对应的弱引用

那么这有什么作用呢?来看一幅图:

图中给了一个思考题:当某个 Entry 对象的 key回收了,该怎么处理?如果一个 Entry对象的 key回收了,证明该 Entry 对象已经不再可用,我们此时显然需要将这个 Entry 对象从 Entry 数组(table) 中清除。这样才能保证整个 WeakHashMap正确性。那么我们怎样知道某个 Entrykey 要被回收了呢?这时候就体现出上面说的引用队列 queue的用处了,我们可以通过它来获取指向当前将要被回收的 Entry 对象的 key 对象的弱引用。那么在 WeakHashMap 中是怎么处理的呢?我们来看对应的源码:

/*** Expunges stale entries from the table.* 从 table 中清除过期的 Entry 对象*/
private void expungeStaleEntries() {/*通过 ReferenceQueue 的 poll 方法取得当前队列中第一个引用对象并将该引用对象从队列中移除,如果 queue 中没有引用对象,则返回 null,该方法不会阻塞线程。这里采用循环则是当 queue 中还存在引用对象时就一直处理 queue 中的引用对象。*/for (Object x; (x = queue.poll()) != null; ) {synchronized (queue) {@SuppressWarnings("unchecked")Entry<K,V> e = (Entry<K,V>) x;// 得到对应 key 的 hash 值int i = indexFor(e.hash, table.length);// 通过 key 的 hash 值得到对应的 Entry 对象,即为要清理的 Entry 对象Entry<K,V> prev = table[i];Entry<K,V> p = prev;// 处理可能存在 hash 值冲突的情况,对应于上图中的情况。while (p != null) {Entry<K,V> next = p.next;if (p == e) {if (prev == e)table[i] = next;elseprev.next = next;// Must not null out e.next;// stale entries may be in use by a HashIteratore.value = null; // Help GCsize--;break;}prev = p;p = next;}}}
}

这个方法的作用就是清除 WeakHashMap中所有将要被回收key 对象所对应的 Entry 对象的,即清除无用对象。这个方法会在三个地方调用:

// 获取储存元素的 table 数组
private Entry<K,V>[] getTable() {expungeStaleEntries();return table;
}// 得到当前 WeakHashMap 中元素的个数
public int size() {if (size == 0)return 0;expungeStaleEntries();return size;
}// 扩容
void resize(int newCapacity) {Entry<K,V>[] oldTable = getTable();int oldCapacity = oldTable.length;if (oldCapacity == MAXIMUM_CAPACITY) {threshold = Integer.MAX_VALUE;return;}Entry<K,V>[] newTable = newTable(newCapacity);transfer(oldTable, newTable);table = newTable;/** If ignoring null elements and processing ref queue caused massive* shrinkage, then restore old table.  This should be rare, but avoids* unbounded expansion of garbage-filled tables.*/if (size >= threshold / 2) {threshold = (int)(newCapacity * loadFactor);} else {expungeStaleEntries();transfer(newTable, oldTable);table = oldTable;}
}

Ok,到了这里,相关的流程都跑通了,关于 WeakHashMap 如何取元素和如何遍历元素等操作就不再介绍了,这些操作在之前的篇幅中介绍 HashMap 等相关类时已经详细介绍了,小伙伴们可以参考之前的文章。

二、总结:

IdentityHashMap:

这个 Map 和我们之前介绍的一些 Map 有较大的区别,当然,总的思想不会变(为了更快的读取键值对元素)。和之前介绍的 Map 的不同点在于其存取键值对的方式:我们之前看到的 Map 具体类都会自定义一个名为 Entry 的内部类来表示储存的键值对元素,而在IdentityHashMap 中我们找不到对应的类:

是把类名换了吗?其实并不是,因为 IdentityHashMap键对象和值对象储存在同一个数组中,我们来看看这个数组

// 保存键值对对象的数组,数组大小一定是 2 的次幂
transient Object[] table; // non-private to simplify nested class access

一、IdentityHashMap获取元素详解:

可以看到:table 数组是 Object 类型的,意味着这个数组可以储存任何非基本数据类型的对象。那么IdentityHashMap 是如何根据所给的键得到对应的值得呢?来看看其get 方法:

public V get(Object key) {// 如果 key 不为 null,则返回 key,否则返回 NULL_KEY,// NULL_KEY 是一个 Object 对象,即代表 key 为 null 时的键Object k = maskNull(key);Object[] tab = table;int len = tab.length;// 求出 key 的 hash 值,传入 len 防止得到的 hash 值越界int i = hash(k, len);while (true) {// 获取 key 的 hash 下标在 table 中所对应的对象Object item = tab[i];// 如果该对象和当前的 key 是同一个对象,认为值是数组当前下标中下一个对象if (item == k)// 这里会有数组下标越界的风险?return (V) tab[i + 1];// 如果数组中当前下标的值为 null,则说明该键在当前 Map 中没有对应的值if (item == null)return null;// 如果当前的 item != k,将 i 往后移i = nextKeyIndex(i, len);}
}

上述代码中注释已经写得很清楚了,留下了一个疑问:return (V) tab[i + 1]; 会有数组下标越界的可能吗?要解决这个问题,我们得看一下 hash 方法和 nextKeyIndex方法的源码,先看看hash 方法的源码:

/*** Returns index for Object x.*/
private static int hash(Object x, int length) {// 这里直接得到的是 x 的 Object 父类对象的 hashCode() 方法的返回值int h = System.identityHashCode(x);// Multiply by -127, and left-shift to use least bit as part of hashreturn ((h << 1) - (h << 8)) & (length - 1);
}

注意到这里 System.identityHashCode方法,来看看这个方法的说明:

/*** Returns the same hash code for the given object as* would be returned by the default method hashCode(),* whether or not the given object's class overrides* hashCode().* The hash code for the null reference is zero.** @param x object for which the hashCode is to be calculated* @return  the hashCode* @since   JDK1.1*/
public static native int identityHashCode(Object x);

通过注释我们了解到这个方法的返回值就相当于直接调用 x.hashCode() 的返回值,也就是相当于调用Object.hashCode方法。而对于 Object.hashCode方法来说,对于不同的对象,其返回值就不一样。回到 IdentityHashMap.hash方法中:得到了 keyhash之后,返回了((h << 1) - (h << 8)) & (length - 1); 的值,我们知道 << 运算符即为左移运算符左移 x 位相当于 * 2^x,所以原式可以写成 ((h * 2^1) - (h * 2^8)) & (length - 1); 而我们知道:任何一个数乘以 2,得到的结果都是偶数,那么我们可以认为(h * 2^1) - (h * 2^8) 是一个偶数,但是结果可能是一个负数,所以接下来进行 & (length - 1); 运算。我们知道 length 即为 IdentityHashMap 的 table数组的长度,这个值肯定是大于 0的,而计算机用补码来表示数字,正数的二进制最高为为 0负数的二进制最高位为 1(最高位为符号位:正数为 0,负数为 1),此时再进行按位与(&)运算(只要是位运算都是现将操作数转换成二进制,再进行相应的运算),按位与的规则是对两个操作数的二进制位按位比较,如果两个位的值都是 1,那么结果就是1,否则为0,那么可知一个负数和一个正数进行& 运算,得到的值一定是非负数(第一位符号位为 0)。这是第一个。第二个是可以通过 & 来模拟 % 运算,在这里通过 hash方法得到的值是要作为数组下标的,那么数组下标肯定不能越界,我们可以通过% 来确保值不大于某个数,为什么这里可以通过 & 来模拟 % 操作呢?我们注意到上面说过:table的长度是2 的次幂值,熟悉二进制的小伙伴知道:2 的次幂值中化为二进制只有一个1 ,举个例子(32 位 int 值):

2^0 = 1; -> 000...(31 个 0)1
2^1 = 2; -> 000...(30 个 0)10
2^2 = 4; -> 000...(29 个 0)100
...

那么将某个 2 的次幂值 - 1,得到的值是什么呢?

2^0 - 1= 0; -> 000...(31 个 0)0
2^1 - 1= 1; -> 000...(30 个 0)01
2^2 - 1= 3; -> 000...(29 个 0)011

此时再进行按位 &运算,得到结果的最大值也就是该 2 的次幂值 - 1。而反过来想:% 运算不就是为了让运算的到的值不大于目标数吗?所以这里的用法很巧妙。那么为什么要用 & 来代替 %呢?因为位运算的效率比 % 高很多。

好了,现在我们应该知道:hash方法返回的值是一个正数,也是一个不大于 table.length 的偶数。又因为 table.length 本身就是一个偶数,那么 hash 方法得到的值和 table.length 至少相差 2 ,也就是说 hash(key, len) <= table.length - 2; 是成立的。那么回到 IdentityHashMapget 方法:得到的数组下标是小于 table.length 的偶数return (V) tab[i + 1]; 也就不会有越界的风险。下面是 nextKeyIndex方法:

/*** Circularly traverses table of size len.*/
private static int nextKeyIndex(int i, int len) {return (i + 2 < len ? i + 2 : 0);
}

二、IdentityHashMap添加元素及扩容机制详解:

理解了上面的,这个也就非常理解了:其实就是为了找到i 的所代表数组下标的下一个键的下标,如果到了数组末尾就从头来过。下面来看看 IdentityHashMapput 方法:

public V put(K key, V value) {final Object k = maskNull(key);retryAfterResize: for (;;) {final Object[] tab = table;final int len = tab.length;// 得到 key 的 hash 值int i = hash(k, len);// 找到 i 下标的对应的 table 数组元素,如果不为 null 则处理 hash 冲突for (Object item; (item = tab[i]) != null;i = nextKeyIndex(i, len)) {if (item == k) {@SuppressWarnings("unchecked")// 先保存被替换的旧值V oldValue = (V) tab[i + 1];// 旧值换成新值tab[i + 1] = value;// 返回被替换的旧值return oldValue;}}// 如果到了这里说明没有冲突,需要在 table 数组中新加入两个对象(key、value),// 这时需要考虑扩容final int s = size + 1;// Use optimized form of 3 * s.// Next capacity is len, 2 * current capacity.if (s + (s << 1) > len && resize(len))continue retryAfterResize;modCount++;// 键值对象赋值tab[i] = k;tab[i + 1] = value;size = s;return null;}
}

下面看看 IdentityHashMap扩容机制

private boolean resize(int newCapacity) {// assert (newCapacity & -newCapacity) == newCapacity; // power of 2// 每次扩容数组容量翻倍int newLength = newCapacity * 2;Object[] oldTable = table;int oldLength = oldTable.length;// 如果当前数组容量达到最大,扩容失败if (oldLength == 2 * MAXIMUM_CAPACITY) { // can't expand any furtherif (size == MAXIMUM_CAPACITY - 1)throw new IllegalStateException("Capacity exhausted.");return false;}if (oldLength >= newLength)return false;Object[] newTable = new Object[newLength];for (int j = 0; j < oldLength; j += 2) {Object key = oldTable[j];if (key != null) {Object value = oldTable[j+1];// 将原数组中键值对引用赋为 null,方便 JVM 进行垃圾回收oldTable[j] = null;oldTable[j+1] = null;// 得到当前键在新数组中的下标int i = hash(key, newLength);while (newTable[i] != null)i = nextKeyIndex(i, newLength);// 找到空位将键值对象插入新的数组中newTable[i] = key;newTable[i + 1] = value;}}table = newTable;return true;
}

OK,整个 IdentityHashMap 的设计思想到这里就很清晰了。

三、 总结:

  • IdentityHashMap·将键和值都储存在 table 数组中,读元素的时候通过先通过键的hash值得到所在数组的下标,而对应的值的下标为键的下标 + 1。即存储键值对元素的方式为table[i]=key,table[i+1]=value,这样交替存储。

  • 键所在的数组下标一定是偶数(0、2、4…)值所在的数组下标一定是奇数(对应的键下标+1)。同时,存入元素也按照相同的规则。

  • 如果当前元素个数 * 3 > table.length。那么进行扩容,扩容是数组容量翻倍。

  • table 数组的最大容量1 << 29最小容量大小是4默认初始容量32。

辅助工具类:

一、Array 和 ArrayList 有何区别?

对于基本类型数据,集合使用自动装箱来减少编码工作量。但是,当处理固定大小基本数据类型的时候,这种方式相对比较慢

二、如何实现 Array 和 List 之间的转换?

三、comparable 和 comparator的区别?

一般我们需要对一个集合使用自定义排序时,我们就要重写compareTo方法或compare方法,当我们需要对某一个集合实现两种排序方式,比如一个song对象中的歌名歌手名分别采用一种排序方法的话,我们可以重写compareTo方法和使用自制的Comparator方法或者以两个Comparator来实现歌名排序和歌星名排序,第二种代表我们只能使用两个参数版的Collections.sort()。

四、Collection 和 Collections 有什么区别?

好了,到这里我们已经基本把 Java 中 Map 的具体类介绍完了,关于Map集合线程安全及关于Key和Value能否为null的问题,请参考集合专栏的第一篇博客Java 集合框架 (1)---- 概述,还有个别 Map 具体类会在后面的文章中和其他的知识点一起介绍。

如果博客中有什么不正确的地方,还请多多指点。如果这篇文章对您有帮助,请不要吝啬您的赞,欢迎继续关注本专栏。

谢谢观看。。。

感谢博主大佬,昵称为:Hiro的支持和昵称为:ThinkWon的支持。

史上最全的集合框架讲解 ----- Java 集合框架(3)---- Map 相关类最全解析相关推荐

  1. Java 集合框架分析:JAVA集合中的一些边边角角的知识

    相关文章: Java 集合框架分析:Set http://blog.csdn.net/youyou1543724847/article/details/52733723 Java 集合框架分析:Lin ...

  2. 【数据结构】史上最好理解的红黑树讲解,让你彻底搞懂红黑树

    目录 一.红黑树简介 二.为什么需要红黑树? 三.红黑树的特性 四.红黑树的效率 4.1 红黑树效率 4.2 红黑树和AVL树的比较 五.红黑树的等价变换 六.红黑树的操作 6.1 旋转操作 6.2 ...

  3. 史上最好理解的Unicode编码讲解(Unicode的前世今生)

    一.了解编码 在计算机中存储的数据都是01二进制的数据串,我们再电脑屏幕上看到的一切可视化的东西最终在计算机存储的都是01二进制串,现在看到我博客上的文字也是. 这里就需要一个映射关系,将我们看到的看 ...

  4. 人脸识别:史上最详细人脸识别adaface讲解-ckpt转onnx模型--第三节

    这章节我会讲解的是我在工作上的项目,人脸识别adaface,以下的讲解为个人的看法,若有地方说错的我会第一时间纠正,如果觉得博主讲解的还可以的话点个赞,就是对我最大的鼓励~ 上一章节我们讲到了模型的训 ...

  5. Java 集合框架(5)---- Map 相关类解析(中)

    本文标题大纲: 文章目录 前言 HashMap TreeMap 指定 TreeMap 的元素排序方式 前言 还是先上那张图吧,我又偷懒了,还是只关注 Map 接口下的类就行了: 在上上篇文章中我们一起 ...

  6. -1-3 java集合框架基础 java集合体系结构 Collection 常用java集合框架 如何选择集合 迭代器 泛型 通配符概念 Properties 集合 迭代器...

    集合又称之为容器存储对象的一种方式 •数组虽然也可以存储对象,但长度是固定的:显然需要可变长度的容器 集合和数组的区别?                 A:长度区别                  ...

  7. java上传ddi_Android平台dalvik模式下java Hook框架ddi的分析(2)--dex文件的注入和调用...

    前面的博客<Android平台dalvik模式下java Hook框架 ddi 的分析(1)>中,已经分析了dalvik模式下 ddi 框架Hook java方法的原理和流程,这里来学习一 ...

  8. SE API第10/11天:集合 ——>ArrayListals、Iterator遍历迭代器、增强for、List集→subList、集合间转换asList、排序sort | Map相关

    一.Java 集合框架 0.介绍: (0)什么是集合 -集合与数组一样,可以保存一组元素,并且提供了操作元素的相关方法,使用更方便. (1)集合框架被设计成要满足以下几个目标: 该框架必须是高性能的. ...

  9. Java集合(一):Java集合概述

    注:本文基于JDK 1.7 1 概述 Java提供了一个丰富的集合框架,这个集合框架包含了许多接口.虚拟类和实现类.这些接口和类提供了丰富的功能,能够满足基本的聚合需求.下图就是这个框架的整体结构图: ...

最新文章

  1. ffmpeg 常用命令
  2. 英特尔CPU控制机制存在隐秘开关 可被黑客利用成为后门
  3. ios学习--TableView详细解释
  4. C语言 abort 函数 - C语言零基础入门教程
  5. bimmercode刷隐藏教程_PS教程:快速提取人物像素,制作人物海报主体,简单易学...
  6. 关于火狐3,怎么会这样??
  7. scrapy crawlspider
  8. beats耳机用安卓手机影响音效么_500元以下的头戴式耳机超高性价比推荐
  9. java中如何使用jdk_java – 如何在JDK7中使用目录globbing
  10. 大量原创视频教程分享(01)---XSL语法教程
  11. Windows phone 7
  12. 信息安全学习笔记(五)------计算机病毒
  13. 恩山斐讯论坛k2p_K2P A1 A2 路由器刷机教程 最详细教程,适合新手!
  14. 幽灵蛛(pholcus)规则(二)
  15. 基于Python的COVID-19背景下的网络社会心态变化数据分析
  16. 车辆ECU需要更新软件
  17. 时间戳服务器作用,时间戳服务器
  18. mysql icp(Index Condition Pushdown) using index condition
  19. Field xxxMapper in xxxxxxx required a bean of type ‘xxxxMapper‘that could not be found.
  20. Qt程序无法启动,debug时提示During startup program exited with code 0xc0000135

热门文章

  1. 天微TM1650数码管驱动IC新旧版 驱动和注意事项
  2. uni-App快速开发一个安卓应用
  3. sublime text3--js智能提示插件以及其他常用插件
  4. clonezilla(再生龙)克隆linux系统 操作指南
  5. 2018/12/19 oracle-sql练习
  6. 对象转换成字符串/字符串和对象的相互转换
  7. Shader学习之Cg语言三(Cg表达式与控制语句)
  8. 面向对象三大特性,五大原则
  9. 通过MatLab将传递函数离散化
  10. 混沌工程之ChaosBlade(一):建立混沌工程思想