🔥码云GVP开源项目 12k star Uniapp+ElementUI 功能强大 支持多语言、二开方便! 广告
# Map接口 Map结构用于存储键值对,是一个接口,其常见的实现类有HashMap、Hashtable、TreeMap等。在该接口中定义的统一操作有: 1. 添加元素: ~~~  // 用于键值对的绑定,当键不存在是返回null,当键存在是返回旧值。  V put(K key, V value); ~~~ 2. 判断 ~~~  // 是否包含键  boolean containsKey(Object key);  // 是否包含值  boolean containsValue(Object value);  // 是否为空  boolean isEmpty();  // 判断两个map是否相同  boolean equals(Object o); ~~~ 3. 获取元素 ~~~  // 根据键获取value值,不存在则返回null  V get(Object key);  // 获取所有键  Set<K> keySet();  // 获取所有的值  Collection<V> values();  // 获取所有键值对  Set<Map.Entry<K,V>> entrySet();  // 大小  int size(); ~~~ 4. 移除元素 ~~~  // 移除某个键  V remove(object key);  // 清空map的内容  void clear(); ~~~ &nbsp; ****** Map接口的实现类: ## HashMap HashMap是JDK1.2版本之后开始增加的,在1.8版本之前其底层使用数组+链表的方式来作为哈希表的存储结构,在1.8版本及之后改成了数组+链表/红黑树的方式来作为哈希表的存储结构,当链表的长度大于8的时候会将链表转换成一颗红黑树。 在分析HashMap的源码之前,先来看看一个键值对放入一个Map中应该经过什么步骤? :-: ![](https://img.kancloud.cn/ae/38/ae3813ea821024765a670c077f3c8675_1056x710.png) **源码分析** 1. 几个重要成员属性 ~~~  // 初始容量,必须为2的幂次方,默认为16  static final int DEFAULT_INITIAL_CAPACITY = 1 << 4;  ​  // 最大的容量  static final int MAXIMUM_CAPACITY = 1 << 30;  ​  // 默认的负载因子  static final float DEFAULT_LOAD_FACTOR = 0.75f;  ​  // 链表的长度的阈值,大于该长度会转化成红黑树  static final int TREEIFY_THRESHOLD = 8;  // 红色树收缩成链表的阈值  static final int UNTREEIFY_THRESHOLD = 6;  // 哈希表的桶大于64时才会转换成树结构  static final int MIN_TREEIFY_CAPACITY = 64;  ​  //----------------字段-----------------------  // 底层数组结构  transient Node<K,V>[] table;  // 键值对集合,可以用于键值对的遍历  transient Set<Map.Entry<K,V>> entrySet;  // 键值对的数量  transient int size;  // 修改容器的次数  transient int modCount;  // 容量阈值  int threshold;  // 负载因子  final float loadFactor; ~~~ 2. 构造方法 ~~~  public HashMap() {      this.loadFactor = DEFAULT_LOAD_FACTOR; // 设置为默认的负载因子  }  ​  // 设置初始化容量,在阿里的开发手册中要求在创建HashMap时要指定初始化容量的大小  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;      // 这里会将初始化容量转化成第一个比其大的一个2的幂次方的数      this.threshold = tableSizeFor(initialCapacity);      // n = -1 >>> Integer.numberOfLeadingZeros(cap - 1);      // numberOfLeadingZeros可以获取前导0的数量。  } ~~~ 3. 关键操作 * put添加元素方法 ~~~  // 不存在映射则会关联值,存在则直接替换值  public V put(K key, V value) {      return putVal(hash(key), key, value, false, true);  }  // 计算hash值,从这里可以看出HashMap的key可以为null,其hash值为0 --> 面试可能会问。  static final int hash(Object key) {      int h;      return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);  }  /**   hash – 密钥的散列   key——钥匙   value – 要放置的值   onlyIfAbsent – 如果为真,则不更改现有值   evict – 如果为 false,则表处于创建模式。   返回:以前的值,如果没有,则为 null  */  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))))              // 进一步判断key是否相等,或者是key重写equals方法后相等              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)                          // 链表长度大于8的时候转化成树结构,但是在里面还会判断桶的数量是否大于64,如果没有的话则进行扩容操作                          treeifyBin(tab, hash);                      break;                 }                  if (e.hash == hash &&                     ((k = e.key) == key || (key != null && key.equals(k))))                      break;                  p = e;             }         }          if (e != null) { // key相等会直接替换值并返回旧值              V oldValue = e.value;              if (!onlyIfAbsent || oldValue == null)                  e.value = value;              // 给LinkedHashMap扩展用的              afterNodeAccess(e);              return oldValue;         }     }      ++modCount;      if (++size > threshold)          resize();      afterNodeInsertion(evict);      return null;  }  ​  // 初始化容量或加倍表的大小 HashMap的底层数组table并不会在构造方法中创建,而是延迟到添加元素时创建  final Node<K,V>[] resize() {      Node<K,V>[] oldTab = table;      // 第一次添加元素进来resize时table为空      int oldCap = (oldTab == null) ? 0 : oldTab.length;      int oldThr = threshold;      int newCap, newThr = 0;      if (oldCap > 0) {          if (oldCap >= MAXIMUM_CAPACITY) {              threshold = Integer.MAX_VALUE;              return oldTab;         }          else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&                   oldCap >= DEFAULT_INITIAL_CAPACITY)              // 新的阈值×2              newThr = oldThr << 1; // double threshold     } else if (oldThr > 0) {          newCap = oldThr;     } else {          // 第一次添加元素,且使用空构造方法没有指定阈值大小时进入这个分支          newCap = DEFAULT_INITIAL_CAPACITY; // 初始化容量为16          // 设置阈值大小          newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);     }      if (newThr == 0) {          float ft = (float)newCap * loadFactor;          newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?                   (int)ft : Integer.MAX_VALUE);     }      threshold = newThr;      @SuppressWarnings({"rawtypes","unchecked"})      // 新建底层数组结构      Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];      table = newTab;      // 对老结构中所有的元素都进行重新hash      if (oldTab != null) {          for (int j = 0; j < oldCap; ++j) {              Node<K,V> e;              if ((e = oldTab[j]) != null) {                  oldTab[j] = null;                  if (e.next == null)                      // 如果当前桶只有一个Node节点,则不用起改变其在桶的位置                      newTab[e.hash & (newCap - 1)] = e;                  else if (e instanceof TreeNode)                     ((TreeNode<K,V>)e).split(this, newTab, j, oldCap);                  else { // e的后续仍有节点,且e不是树节点                      Node<K,V> loHead = null, loTail = null;                      Node<K,V> hiHead = null, hiTail = null;                      Node<K,V> next;                      do {                          next = e.next;                          if ((e.hash & oldCap) == 0) { // 索引为0的位置                              if (loTail == null)                                  loHead = e;                              else                                  loTail.next = e;                              loTail = e;                         } else { // 索引为非0的位置                              if (hiTail == null)                                  hiHead = e;                              else                                  hiTail.next = e;                              hiTail = e;                         }                     } while ((e = next) != null);                                            if (loTail != null) {                          loTail.next = null;                          newTab[j] = loHead;                     }                      if (hiTail != null) {                          hiTail.next = null;                          // 把一整条链都挂到新的位置去                          newTab[j + oldCap] = hiHead;                     }                 }             }         }     }      return newTab;  } ~~~ 总结put操作的过程: 1. 第一次添加元素时会调用resize方法扩容。该方法对所有的元素进行rehash,对于索引为0的桶的元素不改变其位置,对于其他元素则将整条链挂到当前索引+老容量的索引(j + oldCap)位置上去。 2. 如果桶中没有元素则直接放入。 3. 如果桶中有元素了,有相同的键值替换值,没有相同的键则发生冲突,采用尾插法的方式进行插入。 4. 在插入节点之后如果链表的长度大于8且桶的数量大于64则会转成一棵树结构。 &nbsp; ## TreeMap 底层是一个红黑树结构,能够保证数据根据key排序,注意key不能为空。其结构如下 ~~~ Entry = (key、value、左子树地址、右子树地址、父节点地址、颜色) ~~~ ~~~  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类型来表示红黑,BLACK和RED是一个常量      boolean color = BLACK;  ​   // 新增的节点默认为黑色      Entry(K key, V value, Entry<K,V> parent) {          this.key = key;          this.value = value;          this.parent = parent;     }  } ~~~ 源码: ~~~  public class TreeMap<K,V>      extends AbstractMap<K,V>      implements NavigableMap<K,V>, Cloneable, java.io.Serializable  {      // 比较器,TreeMap需要内部比较器或者外部比较器      private final Comparator<? super K> comparator;   // 红黑树的树根      private transient Entry<K,V> root;   // 红黑树节点数量          private transient int size = 0;   // 修改次数      private transient int modCount = 0;            // 构造方法,key需要实现Comparable接口来实现内部的比较      public TreeMap() {          comparator = null;     }      // 传入自定义外部比较器      public TreeMap(Comparator<? super K> comparator) {          this.comparator = comparator;     }                  // -------------常见操作-------------------------      // 添加元素      public V put(K key, V value) { // key和Value的类型在创建的时候就确定了          Entry<K,V> t = root;          if (t == null) {              // 比较两个元素,有外部比较器优先调用外部比较器,没有时调用内部比较器              compare(key, key);              root = new Entry<>(key, value, null);              size = 1;              modCount++;              return null;         }          int cmp;          Entry<K,V> parent;          // 将外部比较器赋值给cpr,看构造函数          Comparator<? super K> cpr = comparator;          // cpr不等于null,意味着创建对象时调用有参构造器          if (cpr != null) {              do {                  parent = t;                  cmp = cpr.compare(key, t.key); // 比较元素的key值                  // cmp返回的就是int类型的数据                  if (cmp < 0)                      t = t.left;                  else if (cmp > 0)                      t = t.right;                  else// cmp=0,即键相等,直接替换value的值,但是key不变,唯一的                      return t.setValue(value);             } while (t != null);         }          // 创建对象的时候没有指定外部比较器,使用的是内部比较器          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);                  if (cmp < 0)                      t = t.left;                  else if (cmp > 0)                      t = t.right;                  else                      return t.setValue(value);             } while (t != null);         }          Entry<K,V> e = new Entry<>(key, value, parent);          if (cmp < 0)              parent.left = e;          else              parent.right = e;          // 维护红黑树的性质          fixAfterInsertion(e);          size++;          modCount++;          return null;     }            // 获取key对应的值      public V get(Object key) {          Entry<K,V> p = getEntry(key);          return (p==null ? null : p.value);     }            final Entry<K,V> getEntry(Object key) {          // 由外部比较器则使用外部比较器比较获取值          if (comparator != null)              return getEntryUsingComparator(key);          if (key == null)              throw new NullPointerException();          @SuppressWarnings("unchecked")              Comparable<? super K> k = (Comparable<? super K>) key;          Entry<K,V> p = root;          while (p != null) {              int cmp = k.compareTo(p.key);              if (cmp < 0)                  p = p.left;              else if (cmp > 0)                  p = p.right;              else                  return p;         }          return null;     }    } ~~~ &nbsp; 总结: 1. TreeMap底层就是一颗红黑树结构。 2. 节点的key需要实现内部比较器或者有自定义的外部比较器,有自定义的外部比较器的时候先使用外部比较器。 3. 节点的key不管在put还是在get都不能为空。 &nbsp; ## LinkedHashMap LinkedHashMap继承HashMap,可以在HashMap的基础上实现顺序的访问,其底层使用了双向链表+HashMap来实现。由于新版本的HashMap会将链表转化成树结构,因此这里称为“Linked”有点混淆,但是官方仍然采用了这个命名方式。 同时使用头尾节点和回调函数来维护双向链表的结构。回调函数在操作:access、insertion、removal后触发。 更重要的一点是LinkedHashMap底层使用了LRU算法来维护节点的顺序。 源码: ~~~  public class LinkedHashMap<K,V>      extends HashMap<K,V>      implements Map<K,V>  {      /**       * 头节点、也是最久没有使用的节点       */      transient LinkedHashMap.Entry<K,V> head;  ​      /**       * 尾结点、也是最近使用的节点       */      transient LinkedHashMap.Entry<K,V> tail;            /**   * true:按照访问顺序遍历、LRU算法   * false:按照插入顺序遍历       */      final boolean accessOrder;                  public LinkedHashMap(int initialCapacity, float loadFactor) {          super(initialCapacity, loadFactor);          accessOrder = false;     }      public LinkedHashMap(int initialCapacity) {          super(initialCapacity);          accessOrder = false;     }      public LinkedHashMap() {          super();          accessOrder = false;     }      // 要使用LRU算法则需将accessOrder设置为true      public LinkedHashMap(int initialCapacity,                           float loadFactor,                           boolean accessOrder) {          super(initialCapacity, loadFactor);          this.accessOrder = accessOrder;     }                  /**---------------------常用操作----------------------------*/      public V get(Object key) {          Node<K,V> e;          if ((e = getNode(hash(key), key)) == null)              return null;          if (accessOrder)              // 如果是按照访问顺序只要调用回调函数              afterNodeAccess(e);          return e.value;     }      // 将当前最近一次访问的节点移动到链表尾部      void afterNodeAccess(Node<K,V> e) { // move node to last          LinkedHashMap.Entry<K,V> last;          if (accessOrder && (last = tail) != e) {              // 当前访问的节点不再最后一个位置              LinkedHashMap.Entry<K,V> p =(LinkedHashMap.Entry<K,V>)e,              // b:当前访问节点的上一个节点,a:当前访问节点的下一个节点             b = p.before, a = p.after;              p.after = null;              if (b == null)                  // p就是头结点                  head = a;              else                  // p节点要移除到最后一个位置的                  b.after = a;                            if (a != null)                  // p不是最后一个元素                  a.before = b;              else                  last = b;                            if (last == null)                  // 第一次访问,                  head = p;              else {                  p.before = last;                  last.after = p;             }              tail = p;              ++modCount;         }     }  } ~~~ &nbsp; ### 使用LinkedHashMap实现LRU缓存 ```java class LRUCache extends LinkedHashMap<Integer, Integer>{ private int capacity; public LRUCache(int capacity) { // 传递true会使用LRU算法 super(capacity, 0.75f, true); this.capacity = capacity; } public int get(int key) { return super.getOrDefault(key, -1); } public void put(int key, int value) { super.put(key, value); } // 重写这个方法,实现超过容量时移除 @Override protected boolean removeEldestEntry(Map.Entry<Integer, Integer> eldest) { // 节点数大于容量时需要移除 return size() > capacity; } } ``` &nbsp; ## 总结 1. HashMap、TreeMap、LinkedHashMap底层都是由红黑树结构的存在。 2. TreeMap可以实现key有序,LinkedHashMap可以实现访问有序和LRU缓存。