java 集合整理归纳

1. 概述:

  • List, Set, Map都是接口,前两个继承collenction 接口,Map为独立接口
  • Set 下有HashSet, LinkedHashSet,TreeSet 几个类
  • List 下有ArrayList, Vector,LinkedList 几个类
  • Map 下有Hashtable , LinkedHashMap,HashMap,TreeMap 几个类
  • collection 接口下还有个Queue接口,有PriorityQueue类
  • Queue 队列的实现方式常见有两种: 一种使用循环数组;另一种使用链表

2. connection接口 小结

2.1. List

特点: 有序,可重复

  • ArrayList

    特点: 底层数据结构是数组,查找快,增删慢,效率高 (查询多使用)

    线程安全性: 线程不安全

  • Vector

    特点: 底层数据结构是数组,查询快,增删慢,效率低

    线程安全性: 线程安全

  • LinkedList

    线程安全性: 线程不安全

    特点:

    • 底层数据结是链表,查询慢,增删快,效率高 (增删多使用)
    • LinkedList还实现了Deque接口,可以被当作成双端队列来使用,因此既可以被当成“栈"来使用,也可以当成队列来使用。
    • LinkedList的工作原理: LinkedList调用默认构造函数,创建一个链表。由于维护了一个表头,表尾的Node对象的变量。可以进行后续的添加元素到链表中的操作,以及其他删除,插入等操作。也因此实现了双向队列的功能,即可向表头加入元素,也可以向表尾加入元素
  public boolean add(E e) {
      linkLast(e);
      return true;
  }

调用linkLast(e);方法,默认向表尾节点加入新的元素

   void linkLast(E e) {
        final Node<E> l = last;
        final Node<E> newNode = new Node<>(l, e, null);
        last = newNode;
        if (l == null)
            first = newNode;
        else
            l.next = newNode;
        size++;
        modCount++;
    }

更新表尾节点,建立连接。其他操作类似,维护了整个链表。
如何将“双向链表和索引值联系起来的”?

    public E get(int index) {
         checkElementIndex(index);//检查索引是否有效
         return node(index).item;
     }

调用了node(index)方法返回了一个Node对象,其中node(index)方法具体如下

    Node<E> node(int index) {
         // assert isElementIndex(index);

         if (index < (size >> 1)) {
             Node<E> x = first;
             for (int i = 0; i < index; i++)
                 x = x.next;
             return x;
         } else {
             Node<E> x = last;
             for (int i = size - 1; i > index; i--)
                 x = x.prev;
             return x;
         }
   }

首先会比较“index”和“双向链表长度的1/2”;若前者小,则从链表头开始往后查找,直到index位置;否则,从链表末尾开始先前查找,直到index位置。这就是“双线链表和索引值联系起来”的方法。

2.1.1. ArrayList与Vector区别

相同点:

  • ArrayList和Vector都是基于数组实现的List类,这两个类封装了一个动态的,允许再分配的Object[]数组. ArrayList和Vector对象使用initalCapacity (默认容量10)参数来设置该数组的长度,当向ArrayList和Vector中添加元素超过了该数组的长度时,他们的initalCapacity会自动增加.

  • 自动增加容量思路是: 先调用了一个ensureCapacityInternal()方法,顾名思义:该方法用来确保数组中是否还有足够容量。经过一系列方法(不必关心),最后有个判断:如果剩余容量足够存放这个数据,则进行下一步,如果不够,则需要执行一个重要的方法:

  private void grow(int minCapacity) {
   //......省略部分内容  主要是为了生成大小合适的newCapacity
  //下面这行就是进行了数组扩容
   elementData = Arrays.copyOf(elementData, newCapacity);
  }
  • 此外,ArrayList还提供了两个额外的方法来调整其容量大小
  1. void ensureCapacity(int minCapacity): 如有必要,增加此 ArrayList 实例的容量,以确保它至少能够容纳最小容量参数所指定的元素数。
  2. void trimToSize():将此 ArrayList 实例的容量调整为列表的当前大小。

不同点:

  • ArrayList是线程不安全的,Vector是线程安全的。
  • Vector的性能比ArrayList差。
  • Vector 有Stack子类, Stack与Vector一样,是线程安全的,但是性能较差, 可以使用LinkedList替代Stack
  • ArrayList的遍历方式 有三种:

2.1.2. ArrayList与LinkedList区别

相同点:

  • ArrayList和LinkedList都是List接口的实现类,都可以通过索引来随机访问集合中的元素
  • 二者都是线程不安全的类

不同点:

  • LinkedList还实现了Deque接口,可以被当作成双端队列来使用,因此既可以被当成“栈"来使用,也可以当成队列来使用。
  • ArrayList 是一个数组队列,相当于动态数组。它由数组实现,随机访问效率高,随机插入、随机删除效率低。ArrayList应使用随机访问(即,通过索引序号访问)遍历集合元素。
  • LinkedList的实现机制与ArrayList完全不同。ArrayList内部是以数组的形式来保存集合中的元素的,因此随机访问集合元素时有较好的性能;而LinkedList内部以链表的形式来保存集合中的元素,因此随机访问集合元素时性能较差,但在插入、删除元素时性能比较出色。
  • 对于“单线程环境” 或者 “多线程环境,但List仅仅只会被单个线程操作”,此时应该使用非同步的类(如ArrayList)。对于“多线程环境,且List可能同时被多个线程操作”,此时,应该使用同步的类(如Vector)。

2.1.3. List 常用遍历方法

第一种,通过迭代器遍历 ; 效率最低

	  Integer value = null;
	  Iterator iter = list.iterator();
	  while (iter.hasNext()) {
	    value = (Integer)iter.next();
	  }

第二种,随机访问,通过索引值去遍历; 效率最高


	  Integer value = null;
	   int size = list.size();
	   for (int i=0; i<size; i++) {
	       value = (Integer)list.get(i);        
	   }
 

第三种,for循环遍历

		Integer value = null;
		for (Integer integ:list) {
		    value = integ;
		}

2.2. Set

特点: 无序,唯一

  • HashSet

    特点: 底层数据结构是哈希表,无序,唯一

    保证元素唯一性: hashCode() 和 eques() 两个方法

  • LinkedHashSet

    特点: 底层数据结构是链表和哈希表(FIFO 插入有序,唯一)

    保证元素唯一性: 链表保证元素有序,哈希表保证元素唯一

  • TreeSet

    特点: 底层数据结构是红黑树,有序,唯一

    保证元素唯一性: 根据比较的返回值是否是0来决定

    如何保证元素排序: 通过 自然排序 和 比较器排序

2.2.1 HashSet, TreeSet, LinkedHashSet区别

相同点:

  • 三者都实现Set的数据结构,所有元素都是唯一的, 三者都不是线程安全的,如果要使用线程安全的Set可以使用collections.synchronizedSet()

不同点:

  • TreeSet 主要功能用于排序,HashSet只是通用的存储数据的集合,LinkedHashSet主要功能用于保证有序的集合(先进先出).
  • HashSet插入数据最快,其次是LinkedHashSet,最慢是TreeSet因为内部实现了排序
  • HashSet 和LinkedHashSet 语序存在null数据,但TreeSet插入null数据会报空指针异常

2.3. Map

Map实现类用于保存有映射关系的数据,内部保存的每项数据都是k-v对,Map里的Key不能重复. 接口有三个比较重要的实现类,分别是HashMap,TreeMap和Hashtable

2.3.1. TreeMap

  • TreeMap不是直接实现Map接口,而是继承于AbstractMap,实现了NavigableMap接口,意味着它支持一系列的导航方法。比如返回有序的key集合。

  • TreeMap基于红黑树(Red-Black tree)实现,它包含几个重要的成员变量: root, size, comparator。该映射根据其键的自然顺序进行排序,或者根据创建映射时提供的 comparator 进行排序,具体取决于使用的构造方法。

    • 红黑树排序简介

      root 是红黑数的根节点。它是Entry类型,Entry是红黑数的节点,它包含了红黑数的6个基本组成成分:key(键)、value(值)、left(左孩子)、right(右孩子)、parent(父节点)、color(颜色)。Entry节点根据key进行排序,Entry节点包含的内容为value。
        
      红黑数排序时,根据Entry中的key进行排序;Entry中的key比较大小是根据比较器comparator来进行判断的。size是红黑数中节点的个数。

  • TreeMap的基本操作 containsKey、get、put 和 remove 的时间复杂度是 log(n) 。

  • 另外,TreeMap是非同步的同时也是非线程安全的。 它的iterator 方法返回的迭代器是fail-fastl的。

2.3.2. HashMap

  • HashMap 是一个散列表,它存储的内容是键值对(key-value)映射。的实现不是同步的,这意味着它不是线程安全的。它的key、value都可以为null。此外,HashMap中的映射不是有序的。

  • HashMap 的实例有两个参数影响其性能:“初始容量” 和 “加载因子”。容量 是哈希表中桶的数量,初始容量 只是哈希表在创建时的容量。加载因子 是哈希表在其容量自动增加之前可以达到多满的一种尺度。当哈希表中的条目数超出了加载因子与当前容量的乘积时,则要对该哈希表进行 rehash 操作(即重建内部数据结构),从而哈希表将具有大约两倍的桶数。

  • 通常,默认加载因子是 0.75, 这是在时间和空间成本上寻求一种折衷。加载因子过高虽然减少了空间开销,但同时也增加了查询成本(在大多数 HashMap 类的操作中,包括 get 和 put 操作,都反映了这一点)。在设置初始容量时应该考虑到映射中所需的条目数及其加载因子,以便最大限度地减少 rehash 操作次数。如果初始容量大于最大条目数除以加载因子,则不会发生 rehash 操作。

  • 除了使用迭代器的remove方法外其的其他方式删除,都会抛出ConcurrentModificationException. 原因如下:

    • 使用iterator迭代删除时没有问题的,迭代器用了自己封装的remove方法,最后一步多了一个操作 expectedModCount = modCount,在每一次迭代时都会调用hasNext()方法判断是否有下一个,是允许集合中数据增加和减少的。
   /**
    * 正确操作例子
    **/
  Map<String, Integer> map = new HashMap<>();
  map.put("GoddessY", 1);
  map.put("Joemsu", 2);

  Iterator<Map.Entry<String, Integer>> iterator = map.entrySet().iterator();
  while(iterator.hasNext()){
      Map.Entry<String, Integer> entry=iterator.next();
      String name=entry.getKey();
      if(name.equals("GoddessY")){
         //特别注意:不能使用map.remove(name)  否则会报同样的错误
          iterator.remove();
      }
  }

  //使用迭代器的remove不会抛出ConcurrentModificationException异常,原因如下:
   /**
   *迭代器中remove源码
   ***/
  public final void remove() {
      Node<K,V> p = current;
      if (p == null)
      throw new IllegalStateException();
      if (modCount != expectedModCount)
      throw new ConcurrentModificationException();
      current = null;
      K key = p.key;
      removeNode(hash(key), key, null, false, false);
      //注意这里:对expectedModCount重新进行了赋值。所以下次比较的时候还是相同的
      expectedModCount = modCount; 
      //此处不+1
  }
  • 使用forEach删除时,会报错ConcurrentModificationException,因为在forEach遍历时,是不允许map元素进行删除和增加。forEach遍历的时候,会初始化expectedModCount=modCount,这时候对HashMap进行修改操作,modCount会+1,继续遍历的时候expectedModCount!=modCount,继而抛出java.util.ConcurrentModificationException异常。
   //如果HashMap中modCount和expectedModCount不相等,则会抛出异常
   final Node<K,V> nextNode() {
       Node<K,V>[] t;
       Node<K,V> e = next;
       if (modCount != expectedModCount)
           throw new ConcurrentModificationException();
       if (e == null)
           throw new NoSuchElementException();
       if ((next = (current = e).next) == null && (t = table) != null) {
           do {} while (index < t.length && (next = t[index++]) == null);
       }
       return e;
   }

modCount具体用途是记录该HashMap修改次数,比如在对一个HashMap put或者remove操作时,会对modCount进行++modCount操作

expectedModCount它是HashIterator中的一个变量,在对HashMap迭代的时候,将modCount赋给expectedModCount而HashMap中的entrySet()迭代时候会创建 HashIterator子类对象EntryIterator

  • 解决办法:
1) 通过Iterator修改Hashtable
   while(it.hasNext()) {
      Map.Entry entry = (Map.Entry) it.next();
        it.remove();//增加此操作
  }
2) 根据实际程序,您自己手动给Iterator遍历的那段程序加锁,给修改HashMap的那段程序加锁。
3) 使用“ConcurrentHashMap”替换HashMap,ConcurrentHashMap会自己检查修改操作,对其加锁,也可针对插入操作。
2.3.2.1 HashMap构造函数及源码分析
  • 4个构造函数

          // 默认构造函数。
          HashMap()
    
          // 指定“容量大小”的构造函数
          HashMap(int capacity)
    
          // 指定“容量大小”和“加载因子”的构造函数
          HashMap(int capacity, float loadFactor)
    
          // 包含“子Map”的构造函数
          HashMap(Map<? extends K, ? extends V> map)
    
  • HashMap是通过"拉链法"实现的哈希表。它包括几个重要的成员变量:table, size, threshold, loadFactor, modCount。

    • table是一个Entry[]数组类型,而Entry实际上就是一个单向链表。哈希表的"key-value键值对"都是存储在Entry数组中的。
    • size是HashMap的大小,它是HashMap保存的键值对的数量。
    • threshold是HashMap的阈值,用于判断是否需要调整HashMap的容量。threshold的值=“容量*加载因子”,当HashMap中存储数据的数量达到threshold时,就需要将HashMap的容量加倍。
    • loadFactor就是加载因子。
    • modCount是用来实现fail-fast机制的。
  • HashMap源码摘要

      package java.util;
      import java.io.*;

      public class HashMap<K,V>
          extends AbstractMap<K,V>
          implements Map<K,V>, Cloneable, Serializable
      {

          // 默认的初始容量是16,必须是2的幂。
          static final int DEFAULT_INITIAL_CAPACITY = 16;

          // 最大容量(必须是2的幂且小于2的30次方,传入容量过大将被这个值替换)
          static final int MAXIMUM_CAPACITY = 1 << 30;

          // 默认加载因子
          static final float DEFAULT_LOAD_FACTOR = 0.75f;

          // 存储数据的Entry数组,长度是2的幂。
          // HashMap是采用拉链法实现的,每一个Entry本质上是一个单向链表
          transient Entry[] table;

          // HashMap的大小,它是HashMap保存的键值对的数量
          transient int size;

          // HashMap的阈值,用于判断是否需要调整HashMap的容量(threshold = 容量*加载因子)
          int threshold;

          // 加载因子实际大小
          final float loadFactor;

          // HashMap被改变的次数
          transient volatile int modCount;

          // 指定“容量大小”和“加载因子”的构造函数
          public HashMap(int initialCapacity, float loadFactor) {
              if (initialCapacity < 0)
                  throw new IllegalArgumentException("Illegal initial capacity: " +
                                                    initialCapacity);
              // HashMap的最大容量只能是MAXIMUM_CAPACITY
              if (initialCapacity > MAXIMUM_CAPACITY)
                  initialCapacity = MAXIMUM_CAPACITY;
              if (loadFactor <= 0 || Float.isNaN(loadFactor))
                  throw new IllegalArgumentException("Illegal load factor: " +
                                                    loadFactor);

              // 找出“大于initialCapacity”的最小的2的幂
              int capacity = 1;
              while (capacity < initialCapacity)
                  capacity <<= 1;

              // 设置“加载因子”
              this.loadFactor = loadFactor;
              // 设置“HashMap阈值”,当HashMap中存储数据的数量达到threshold时,
              //就需要将HashMap的容量加倍。
              threshold = (int)(capacity * loadFactor);
              // 创建Entry数组,用来保存数据
              table = new Entry[capacity];
              init();
          }


          // 指定“容量大小”的构造函数
          public HashMap(int initialCapacity) {
              this(initialCapacity, DEFAULT_LOAD_FACTOR);
          }

          // 默认构造函数。
          public HashMap() {
              // 设置“加载因子”
              this.loadFactor = DEFAULT_LOAD_FACTOR;
              // 设置“HashMap阈值”,当HashMap中存储数据的数量达到threshold时,
              //就需要将HashMap的容量加倍。
              threshold = (int)(DEFAULT_INITIAL_CAPACITY * DEFAULT_LOAD_FACTOR);
              // 创建Entry数组,用来保存数据
              table = new Entry[DEFAULT_INITIAL_CAPACITY];
              init();
          }

          // 包含“子Map”的构造函数
          public HashMap(Map<? extends K, ? extends V> m) {
              this(Math.max((int) (m.size() / DEFAULT_LOAD_FACTOR) + 1,
                            DEFAULT_INITIAL_CAPACITY), DEFAULT_LOAD_FACTOR);
              // 将m中的全部元素逐个添加到HashMap中
              putAllForCreate(m);
          }

          static int hash(int h) {
              h ^= (h >>> 20) ^ (h >>> 12);
              return h ^ (h >>> 7) ^ (h >>> 4);
          }

          // 返回索引值
          // h & (length-1)保证返回值的小于length
          static int indexFor(int h, int length) {
              return h & (length-1);
          }

          public int size() {
              return size;
          }

          public boolean isEmpty() {
              return size == 0;
          }

          // 获取key对应的value
          public V get(Object key) {
              if (key == null)
                  return getForNullKey();
              // 获取key的hash值
              int hash = hash(key.hashCode());
              // 在“该hash值对应的链表”上查找“键值等于key”的元素
              for (Entry<K,V> e = table[indexFor(hash, table.length)];
                  e != null;
                  e = e.next) {
                  Object k;
                  if (e.hash == hash && ((k = e.key) == key || key.equals(k)))
                      return e.value;
              }
              return null;
          }

          // 获取“key为null”的元素的值
          // HashMap将“key为null”的元素存储在table[0]位置!
          private V getForNullKey() {
              for (Entry<K,V> e = table[0]; e != null; e = e.next) {
                  if (e.key == null)
                      return e.value;
              }
              return null;
          }

          // HashMap是否包含key
          public boolean containsKey(Object key) {
              return getEntry(key) != null;
          }

          // 返回“键为key”的键值对
          final Entry<K,V> getEntry(Object key) {
              // 获取哈希值
              // HashMap将“key为null”的元素存储在table[0]位置,“key不为null”的则调用hash()计算哈希值
              int hash = (key == null) ? 0 : hash(key.hashCode());
              // 在“该hash值对应的链表”上查找“键值等于key”的元素
              for (Entry<K,V> e = table[indexFor(hash, table.length)];
                  e != null;
                  e = e.next) {
                  Object k;
                  if (e.hash == hash &&
                      ((k = e.key) == key || (key != null && key.equals(k))))
                      return e;
              }
              return null;
          }

          // 将“key-value”添加到HashMap中
          public V put(K key, V value) {
              // 若“key为null”,则将该键值对添加到table[0]中。
              if (key == null)
                  return putForNullKey(value);
              // 若“key不为null”,则计算该key的哈希值,然后将其添加到该哈希值对应的链表中。
              int hash = hash(key.hashCode());
              int i = indexFor(hash, table.length);
              for (Entry<K,V> e = table[i]; e != null; e = e.next) {
                  Object k;
                  // 若“该key”对应的键值对已经存在,则用新的value取代旧的value。然后退出!
                  if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {
                      V oldValue = e.value;
                      e.value = value;
                      e.recordAccess(this);
                      return oldValue;
                  }
              }

              // 若“该key”对应的键值对不存在,则将“key-value”添加到table中
              modCount++;
              addEntry(hash, key, value, i);
              return null;
          }

          // putForNullKey()的作用是将“key为null”键值对添加到table[0]位置
          private V putForNullKey(V value) {
              for (Entry<K,V> e = table[0]; e != null; e = e.next) {
                  if (e.key == null) {
                      V oldValue = e.value;
                      e.value = value;
                      e.recordAccess(this);
                      return oldValue;
                  }
              }
              // 这里的完全不会被执行到!
              modCount++;
              addEntry(0, null, value, 0);
              return null;
          }

          // 创建HashMap对应的“添加方法”,
          // 它和put()不同。putForCreate()是内部方法,它被构造函数等调用,用来创建HashMap
          // 而put()是对外提供的往HashMap中添加元素的方法。
          private void putForCreate(K key, V value) {
              int hash = (key == null) ? 0 : hash(key.hashCode());
              int i = indexFor(hash, table.length);

              // 若该HashMap表中存在“键值等于key”的元素,则替换该元素的value值
              for (Entry<K,V> e = table[i]; e != null; e = e.next) {
                  Object k;
                  if (e.hash == hash &&
                      ((k = e.key) == key || (key != null && key.equals(k)))) {
                      e.value = value;
                      return;
                  }
              }

              // 若该HashMap表中不存在“键值等于key”的元素,则将该key-value添加到HashMap中
              createEntry(hash, key, value, i);
          }

          // 将“m”中的全部元素都添加到HashMap中。
          // 该方法被内部的构造HashMap的方法所调用。
          private void putAllForCreate(Map<? extends K, ? extends V> m) {
              // 利用迭代器将元素逐个添加到HashMap中
              for (Iterator<? extends Map.Entry<? extends K, ? extends V>> i = m.entrySet().iterator(); i.hasNext(); ) 
              {
                  Map.Entry<? extends K, ? extends V> e = i.next();
                  putForCreate(e.getKey(), e.getValue());
              }
          }

          // 重新调整HashMap的大小,newCapacity是调整后的单位
          void resize(int newCapacity) {
              Entry[] oldTable = table;
              int oldCapacity = oldTable.length;
              if (oldCapacity == MAXIMUM_CAPACITY) {
                  threshold = Integer.MAX_VALUE;
                  return;
              }

              // 新建一个HashMap,将“旧HashMap”的全部元素添加到“新HashMap”中,
              // 然后,将“新HashMap”赋值给“旧HashMap”。
              Entry[] newTable = new Entry[newCapacity];
              transfer(newTable);
              table = newTable;
              threshold = (int)(newCapacity * loadFactor);
          }

          // 将HashMap中的全部元素都添加到newTable中
          void transfer(Entry[] newTable) {
              Entry[] src = table;
              int newCapacity = newTable.length;
              for (int j = 0; j < src.length; j++) {
                  Entry<K,V> e = src[j];
                  if (e != null) {
                      src[j] = null;
                      do {
                          Entry<K,V> next = e.next;
                          int i = indexFor(e.hash, newCapacity);
                          e.next = newTable[i];
                          newTable[i] = e;
                          e = next;
                      } while (e != null);
                  }
              }
          }

          // 将"m"的全部元素都添加到HashMap中
          public void putAll(Map<? extends K, ? extends V> m) {
              // 有效性判断
              int numKeysToBeAdded = m.size();
              if (numKeysToBeAdded == 0)
                  return;

              // 计算容量是否足够,
              // 若“当前实际容量 < 需要的容量”,则将容量x2。
              if (numKeysToBeAdded > threshold) {
                  int targetCapacity = (int)(numKeysToBeAdded / loadFactor + 1);
                  if (targetCapacity > MAXIMUM_CAPACITY)
                      targetCapacity = MAXIMUM_CAPACITY;
                  int newCapacity = table.length;
                  while (newCapacity < targetCapacity)
                      newCapacity <<= 1;
                  if (newCapacity > table.length)
                      resize(newCapacity);
              }

              // 通过迭代器,将“m”中的元素逐个添加到HashMap中。
              for (Iterator<? extends Map.Entry<? extends K, ? extends V>> i = m.entrySet().iterator(); i.hasNext(); ) {
                  Map.Entry<? extends K, ? extends V> e = i.next();
                  put(e.getKey(), e.getValue());
              }
          }

          // 删除“键为key”元素
          public V remove(Object key) {
              Entry<K,V> e = removeEntryForKey(key);
              return (e == null ? null : e.value);
          }

          // 删除“键为key”的元素
          final Entry<K,V> removeEntryForKey(Object key) {
              // 获取哈希值。若key为null,则哈希值为0;否则调用hash()进行计算
              int hash = (key == null) ? 0 : hash(key.hashCode());
              int i = indexFor(hash, table.length);
              Entry<K,V> prev = table[i];
              Entry<K,V> e = prev;

              // 删除链表中“键为key”的元素
              // 本质是“删除单向链表中的节点”
              while (e != null) {
                  Entry<K,V> next = e.next;
                  Object k;
                  if (e.hash == hash &&
                      ((k = e.key) == key || (key != null && key.equals(k)))) {
                      modCount++;
                      size--;
                      if (prev == e)
                          table[i] = next;
                      else
                          prev.next = next;
                      e.recordRemoval(this);
                      return e;
                  }
                  prev = e;
                  e = next;
              }

              return e;
          }

          // 删除“键值对”
          final Entry<K,V> removeMapping(Object o) {
              if (!(o instanceof Map.Entry))
                  return null;

              Map.Entry<K,V> entry = (Map.Entry<K,V>) o;
              Object key = entry.getKey();
              int hash = (key == null) ? 0 : hash(key.hashCode());
              int i = indexFor(hash, table.length);
              Entry<K,V> prev = table[i];
              Entry<K,V> e = prev;

              // 删除链表中的“键值对e”
              // 本质是“删除单向链表中的节点”
              while (e != null) {
                  Entry<K,V> next = e.next;
                  if (e.hash == hash && e.equals(entry)) {
                      modCount++;
                      size--;
                      if (prev == e)
                          table[i] = next;
                      else
                          prev.next = next;
                      e.recordRemoval(this);
                      return e;
                  }
                  prev = e;
                  e = next;
              }

              return e;
          }

          // 清空HashMap,将所有的元素设为null
          public void clear() {
              modCount++;
              Entry[] tab = table;
              for (int i = 0; i < tab.length; i++)
                  tab[i] = null;
              size = 0;
          }

          // 是否包含“值为value”的元素
          public boolean containsValue(Object value) {
          // 若“value为null”,则调用containsNullValue()查找
          if (value == null)
                  return containsNullValue();

          // 若“value不为null”,则查找HashMap中是否有值为value的节点。
          Entry[] tab = table;
              for (int i = 0; i < tab.length ; i++)
                  for (Entry e = tab[i] ; e != null ; e = e.next)
                      if (value.equals(e.value))
                          return true;
          return false;
          }

          // 是否包含null值
          private boolean containsNullValue() {
          Entry[] tab = table;
              for (int i = 0; i < tab.length ; i++)
                  for (Entry e = tab[i] ; e != null ; e = e.next)
                      if (e.value == null)
                          return true;
          return false;
          }

          // 克隆一个HashMap,并返回Object对象
          public Object clone() {
              HashMap<K,V> result = null;
              try {
                  result = (HashMap<K,V>)super.clone();
              } catch (CloneNotSupportedException e) {
                  // assert false;
              }
              result.table = new Entry[table.length];
              result.entrySet = null;
              result.modCount = 0;
              result.size = 0;
              result.init();
              // 调用putAllForCreate()将全部元素添加到HashMap中
              result.putAllForCreate(this);

              return result;
          }

          // Entry是单向链表。
          // 它是 “HashMap链式存储法”对应的链表。
          // 它实现了Map.Entry 接口,即实现getKey(), getValue(), setValue(V value), 
          // equals(Object o), hashCode()这些函数
          static class Entry<K,V> implements Map.Entry<K,V> {
              final K key;
              V value;
              // 指向下一个节点
              Entry<K,V> next;
              final int hash;

              // 构造函数。
              // 输入参数包括"哈希值(h)", "键(k)", "值(v)", "下一节点(n)"
              Entry(int h, K k, V v, Entry<K,V> n) {
                  value = v;
                  next = n;
                  key = k;
                  hash = h;
              }

              public final K getKey() {
                  return key;
              }

              public final V getValue() {
                  return value;
              }

              public final V setValue(V newValue) {
                  V oldValue = value;
                  value = newValue;
                  return oldValue;
              }

              // 判断两个Entry是否相等
              // 若两个Entry的“key”和“value”都相等,则返回true。
              // 否则,返回false
              public final boolean equals(Object o) {
                  if (!(o instanceof Map.Entry))
                      return false;
                  Map.Entry e = (Map.Entry)o;
                  Object k1 = getKey();
                  Object k2 = e.getKey();
                  if (k1 == k2 || (k1 != null && k1.equals(k2))) {
                      Object v1 = getValue();
                      Object v2 = e.getValue();
                      if (v1 == v2 || (v1 != null && v1.equals(v2)))
                          return true;
                  }
                  return false;
              }

              // 实现hashCode()
              public final int hashCode() {
                  return (key==null   ? 0 : key.hashCode()) ^
                        (value==null ? 0 : value.hashCode());
              }

              public final String toString() {
                  return getKey() + "=" + getValue();
              }

              // 当向HashMap中添加元素时,绘调用recordAccess()。
              // 这里不做任何处理
              void recordAccess(HashMap<K,V> m) {
              }

              // 当从HashMap中删除元素时,绘调用recordRemoval()。
              // 这里不做任何处理
              void recordRemoval(HashMap<K,V> m) {
              }
          }

          // 新增Entry。将“key-value”插入指定位置,bucketIndex是位置索引。
          void addEntry(int hash, K key, V value, int bucketIndex) {
              // 保存“bucketIndex”位置的值到“e”中
              Entry<K,V> e = table[bucketIndex];
              // 设置“bucketIndex”位置的元素为“新Entry”,
              // 设置“e”为“新Entry的下一个节点”
              table[bucketIndex] = new Entry<K,V>(hash, key, value, e);
              // 若HashMap的实际大小 不小于 “阈值”,则调整HashMap的大小
              if (size++ >= threshold)
                  resize(2 * table.length);
          }

          // 创建Entry。将“key-value”插入指定位置,bucketIndex是位置索引。
          // 它和addEntry的区别是:
          // (01) addEntry()一般用在 新增Entry可能导致“HashMap的实际容量”超过“阈值”的情况下。
          //   例如,我们新建一个HashMap,然后不断通过put()向HashMap中添加元素;
          // put()是通过addEntry()新增Entry的。
          //   在这种情况下,我们不知道何时“HashMap的实际容量”会超过“阈值”;
          //   因此,需要调用addEntry()
          // (02) createEntry() 一般用在 新增Entry不会导致“HashMap的实际容量”超过“阈值”的情况下。
          //   例如,我们调用HashMap“带有Map”的构造函数,它绘将Map的全部元素添加到HashMap中;
          // 但在添加之前,我们已经计算好“HashMap的容量和阈值”。也就是,可以确定“即使将Map中
          // 的全部元素添加到HashMap中,都不会超过HashMap的阈值”。
          //   此时,调用createEntry()即可。
          void createEntry(int hash, K key, V value, int bucketIndex) {
              // 保存“bucketIndex”位置的值到“e”中
              Entry<K,V> e = table[bucketIndex];
              // 设置“bucketIndex”位置的元素为“新Entry”,
              // 设置“e”为“新Entry的下一个节点”
              table[bucketIndex] = new Entry<K,V>(hash, key, value, e);
              size++;
          }

          // HashIterator是HashMap迭代器的抽象出来的父类,实现了公共了函数。
          // 它包含“key迭代器(KeyIterator)”、“Value迭代器(ValueIterator)”和“Entry迭代器(EntryIterator)”3个子类。
          private abstract class HashIterator<E> implements Iterator<E> {
              // 下一个元素
              Entry<K,V> next;
              // expectedModCount用于实现fast-fail机制。
              int expectedModCount;
              // 当前索引
              int index;
              // 当前元素
              Entry<K,V> current;

              HashIterator() {
                  expectedModCount = modCount;
                  if (size > 0) { // advance to first entry
                      Entry[] t = table;
                      // 将next指向table中第一个不为null的元素。
                      // 这里利用了index的初始值为0,从0开始依次向后遍历,直到找到不为null的元素就退出循环。
                      while (index < t.length && (next = t[index++]) == null)
                          ;
                  }
              }

              public final boolean hasNext() {
                  return next != null;
              }

              // 获取下一个元素
              final Entry<K,V> nextEntry() {
                  if (modCount != expectedModCount)
                      throw new ConcurrentModificationException();
                  Entry<K,V> e = next;
                  if (e == null)
                      throw new NoSuchElementException();

                  // 注意!!!
                  // 一个Entry就是一个单向链表
                  // 若该Entry的下一个节点不为空,就将next指向下一个节点;
                  // 否则,将next指向下一个链表(也是下一个Entry)的不为null的节点。
                  if ((next = e.next) == null) {
                      Entry[] t = table;
                      while (index < t.length && (next = t[index++]) == null)
                          ;
                  }
                  current = e;
                  return e;
              }

              // 删除当前元素
              public void remove() {
                  if (current == null)
                      throw new IllegalStateException();
                  if (modCount != expectedModCount)
                      throw new ConcurrentModificationException();
                  Object k = current.key;
                  current = null;
                  HashMap.this.removeEntryForKey(k);
                  expectedModCount = modCount;
              }

          }

          // value的迭代器
          private final class ValueIterator extends HashIterator<V> {
              public V next() {
                  return nextEntry().value;
              }
          }

          // key的迭代器
          private final class KeyIterator extends HashIterator<K> {
              public K next() {
                  return nextEntry().getKey();
              }
          }

          // Entry的迭代器
          private final class EntryIterator extends HashIterator<Map.Entry<K,V>> {
              public Map.Entry<K,V> next() {
                  return nextEntry();
              }
          }

          // 返回一个“key迭代器”
          Iterator<K> newKeyIterator()   {
              return new KeyIterator();
          }
          // 返回一个“value迭代器”
          Iterator<V> newValueIterator()   {
              return new ValueIterator();
          }
          // 返回一个“entry迭代器”
          Iterator<Map.Entry<K,V>> newEntryIterator()   {
              return new EntryIterator();
          }

          // HashMap的Entry对应的集合
          private transient Set<Map.Entry<K,V>> entrySet = null;

          // 返回“key的集合”,实际上返回一个“KeySet对象”
          public Set<K> keySet() {
              Set<K> ks = keySet;
              return (ks != null ? ks : (keySet = new KeySet()));
          }

          // Key对应的集合
          // KeySet继承于AbstractSet,说明该集合中没有重复的Key。
          private final class KeySet extends AbstractSet<K> {
              public Iterator<K> iterator() {
                  return newKeyIterator();
              }
              public int size() {
                  return size;
              }
              public boolean contains(Object o) {
                  return containsKey(o);
              }
              public boolean remove(Object o) {
                  return HashMap.this.removeEntryForKey(o) != null;
              }
              public void clear() {
                  HashMap.this.clear();
              }
          }

          // 返回“value集合”,实际上返回的是一个Values对象
          public Collection<V> values() {
              Collection<V> vs = values;
              return (vs != null ? vs : (values = new Values()));
          }

          // “value集合”
          // Values继承于AbstractCollection,不同于“KeySet继承于AbstractSet”,
          // Values中的元素能够重复。因为不同的key可以指向相同的value。
          private final class Values extends AbstractCollection<V> {
              public Iterator<V> iterator() {
                  return newValueIterator();
              }
              public int size() {
                  return size;
              }
              public boolean contains(Object o) {
                  return containsValue(o);
              }
              public void clear() {
                  HashMap.this.clear();
              }
          }

          // 返回“HashMap的Entry集合”
          public Set<Map.Entry<K,V>> entrySet() {
              return entrySet0();
          }

          // 返回“HashMap的Entry集合”,它实际是返回一个EntrySet对象
          private Set<Map.Entry<K,V>> entrySet0() {
              Set<Map.Entry<K,V>> es = entrySet;
              return es != null ? es : (entrySet = new EntrySet());
          }

          // EntrySet对应的集合
          // EntrySet继承于AbstractSet,说明该集合中没有重复的EntrySet。
          private final class EntrySet extends AbstractSet<Map.Entry<K,V>> {
              public Iterator<Map.Entry<K,V>> iterator() {
                  return newEntryIterator();
              }
              public boolean contains(Object o) {
                  if (!(o instanceof Map.Entry))
                      return false;
                  Map.Entry<K,V> e = (Map.Entry<K,V>) o;
                  Entry<K,V> candidate = getEntry(e.getKey());
                  return candidate != null && candidate.equals(e);
              }
              public boolean remove(Object o) {
                  return removeMapping(o) != null;
              }
              public int size() {
                  return size;
              }
              public void clear() {
                  HashMap.this.clear();
              }
          }

          // java.io.Serializable的写入函数
          // 将HashMap的“总的容量,实际容量,所有的Entry”都写入到输出流中
          private void writeObject(java.io.ObjectOutputStream s)
              throws IOException
          {
              Iterator<Map.Entry<K,V>> i =
                  (size > 0) ? entrySet0().iterator() : null;

              // Write out the threshold, loadfactor, and any hidden stuff
              s.defaultWriteObject();

              // Write out number of buckets
              s.writeInt(table.length);

              // Write out size (number of Mappings)
              s.writeInt(size);

              // Write out keys and values (alternating)
              if (i != null) {
                  while (i.hasNext()) {
                  Map.Entry<K,V> e = i.next();
                  s.writeObject(e.getKey());
                  s.writeObject(e.getValue());
                  }
              }
          }


          private static final long serialVersionUID = 362498820763181265L;

          // java.io.Serializable的读取函数:根据写入方式读出
          // 将HashMap的“总的容量,实际容量,所有的Entry”依次读出
          private void readObject(java.io.ObjectInputStream s)
              throws IOException, ClassNotFoundException
          {
              // Read in the threshold, loadfactor, and any hidden stuff
              s.defaultReadObject();

              // Read in number of buckets and allocate the bucket array;
              int numBuckets = s.readInt();
              table = new Entry[numBuckets];

              init();  // Give subclass a chance to do its thing.

              // Read in size (number of Mappings)
              int size = s.readInt();

              // Read the keys and values, and put the mappings in the HashMap
              for (int i=0; i<size; i++) {
                  K key = (K) s.readObject();
                  V value = (V) s.readObject();
                  putForCreate(key, value);
              }
          }

          // 返回“HashMap总的容量”
          int   capacity()     { return table.length; }
          // 返回“HashMap的加载因子”
          float loadFactor()   { return loadFactor;   }
      }
2.3.2.2 HashMap的“拉链法”相关内容
  • HashMap数据存储数组
    transient Entry[] table;

HashMap中的key-value都是存储在Entry数组中的。

  • 数据节点Entry的数据结构

    Entry 实际上就是一个单向链表。这也是为什么我们说HashMap是通过拉链法解决哈希冲突的。Entry 实现了Map.Entry 接口,即实现getKey(), getValue(), setValue(V value), equals(Object o), hashCode()这些函数。这些都是基本的读取/修改key、value值的函数。

2.3.2.3 HashMap 主要对外接口
  • clear()

    clear() 的作用是清空HashMap。它是通过将所有的元素设为null来实现的。

  • containsKey()

    containsKey() 首先通过getEntry(key)获取key对应的Entry,然后判断该Entry是否为null。

    getEntry() 的作用就是返回“键为key”的键值对

    HashMap将“key为null”的元素都放在table的位置0处,即table[0]中;“key不为null”的放在table的其余位置!

  • containsValue()

    containsValue()分为两步进行处理:第一,若“value为null”,则调用containsNullValue()。第二,若“value不为null”,则查找HashMap中是否有值为value的节点。

    containsNullValue() 的作用判断HashMap中是否包含“值为null”的元素。

    private boolean containsNullValue() {
      Entry[] tab = table;
      for (int i = 0; i < tab.length ; i++)
          for (Entry e = tab[i] ; e != null ; e = e.next)
              if (e.value == null)
                  return true;
        return false;
    }
  • entrySet()、values()、keySet()

    它们3个的原理类似,这里以entrySet()为例来说明。

    entrySet()的作用是返回 HashMap中所有Entry的集合 它是一个集合。而每一个Entry本质上又是一个单向链表

    HashMap是如何通过entrySet()遍历的。entrySet()实际上是通过newEntryIterator()实现的。 下面我们看看它的代码:

  // 返回一个“entry迭代器”
  Iterator<Map.Entry<K,V>> newEntryIterator()   {
      return new EntryIterator();
  }

  // Entry的迭代器
  private final class EntryIterator extends HashIterator<Map.Entry<K,V>> {
      public Map.Entry<K,V> next() {
          return nextEntry();
      }
  }

  // HashIterator是HashMap迭代器的抽象出来的父类,实现了公共了函数。
  // 它包含“key迭代器(KeyIterator)”、“Value迭代器(ValueIterator)”和“Entry迭代器(EntryIterator)”3个子类。
  private abstract class HashIterator<E> implements Iterator<E> {
      // 下一个元素
      Entry<K,V> next;
      // expectedModCount用于实现fast-fail机制。
      int expectedModCount;
      // 当前索引
      int index;
      // 当前元素
      Entry<K,V> current;

      HashIterator() {
          expectedModCount = modCount;
          if (size > 0) { // advance to first entry
              Entry[] t = table;
              // 将next指向table中第一个不为null的元素。
              // 这里利用了index的初始值为0,从0开始依次向后遍历,直到找到不为null的元素就退出循环。
              while (index < t.length && (next = t[index++]) == null)
                  ;
          }
      }

      public final boolean hasNext() {
          return next != null;
      }

      // 获取下一个元素
      final Entry<K,V> nextEntry() {
          if (modCount != expectedModCount)
              throw new ConcurrentModificationException();
          Entry<K,V> e = next;
          if (e == null)
              throw new NoSuchElementException();

          // 注意!!!
          // 一个Entry就是一个单向链表
          // 若该Entry的下一个节点不为空,就将next指向下一个节点;
          // 否则,将next指向下一个链表(也是下一个Entry)的不为null的节点。
          if ((next = e.next) == null) {
              Entry[] t = table;
              while (index < t.length && (next = t[index++]) == null)
                  ;
          }
          current = e;
          return e;
      }

      // 删除当前元素
      public void remove() {
          if (current == null)
              throw new IllegalStateException();
          if (modCount != expectedModCount)
              throw new ConcurrentModificationException();
          Object k = current.key;
          current = null;
          HashMap.this.removeEntryForKey(k);
          expectedModCount = modCount;
      }

  }

当我们通过entrySet()获取到的Iterator的next()方法去遍历HashMap时,实际上调用的是 nextEntry() 。而nextEntry()的实现方式,先遍历Entry(根据Entry在table中的序号,从小到大的遍历);然后对每个Entry(即每个单向链表),逐个遍历。

  • put()

    若要添加到HashMap中的键值对对应的key已经存在HashMap中,则找到该键值对;然后新的value取代旧的value,并退出!

    若要添加到HashMap中的键值对对应的key不在HashMap中,则将其添加到该哈希值对应的链表中,并调用addEntry()。

    addEntry()的代码:

	 void addEntry(int hash, K key, V value, int bucketIndex) {
	     // 保存“bucketIndex”位置的值到“e”中
	     Entry<K,V> e = table[bucketIndex];
	     // 设置“bucketIndex”位置的元素为“新Entry”,
	     // 设置“e”为“新Entry的下一个节点”
	     table[bucketIndex] = new Entry<K,V>(hash, key, value, e);
	     // 若HashMap的实际大小 不小于 “阈值”,则调整HashMap的大小
	     if (size++ >= threshold)
	         resize(2 * table.length);
	 }

addEntry() 的作用是新增Entry。将“key-value”插入指定位置,bucketIndex是位置索引。

说到addEntry(),就不得不说另一个函数createEntry()。createEntry()的代码如下:

	  void createEntry(int hash, K key, V value, int bucketIndex) {
	      // 保存“bucketIndex”位置的值到“e”中
	      Entry<K,V> e = table[bucketIndex];
	      // 设置“bucketIndex”位置的元素为“新Entry”,
	      // 设置“e”为“新Entry的下一个节点”
	      table[bucketIndex] = new Entry<K,V>(hash, key, value, e);
	      size++;
	  }

它们的作用都是将key、value添加到HashMap中。而且,比较addEntry()和createEntry()的代码,我们发现addEntry()多了两句:

      if (size++ >= threshold)
         resize(2 * table.length);

addEntry() 与 createEntry()区别如下

  • (01) addEntry()一般用在 新增Entry可能导致“HashMap的实际容量”超过“阈值” 的情况下。

    例如,我们新建一个HashMap,然后不断通过put()向HashMap中添加元素;put()是通过addEntry()新增Entry的。在这种情况下,我们不知道何时“HashMap的实际容量”会超过“阈值”; 因此,需要调用addEntry()。

  • (02) createEntry() 一般用在 新增Entry不会导致“HashMap的实际容量”超过“阈值” 的情况下。

    例如,我们调用HashMap“带有Map”的构造函数,它绘将Map的全部元素添加到HashMap中;但在添加之前,我们已经计算好“HashMap的容量和阈值”。也就是,可以确定“即使将Map中的全部元素添加到HashMap中,都不会超过HashMap的阈值”。此时,调用createEntry()即可。

2.3.2.4 HashMap 遍历方式
  • 遍历HashMap的键值对

    第一步:根据entrySet()获取HashMap的“键值对”的Set集合。
    第二步:通过Iterator迭代器遍历“第一步”得到的集合。

  • 遍历HashMap的键

    第一步:根据keySet()获取HashMap的“键”的Set集合。
    第二步:通过Iterator迭代器遍历“第一步”得到的集合。

  • 遍历HashMap的值

    第一步:根据value()获取HashMap的“值”的集合。
    第二步:通过Iterator迭代器遍历“第一步”得到的集合。

2.3.2.5 HashMap的hashCode()方法特点

在HashMap类中, 如果HashMap对象的内部对象是空的, 则hashcode 一定是 0,如果非空, 则遍历容器中的全部对象然后取key的hashcode和value的hashcode 按位异或运算,然后把他们依次相加,也就是说, 如果你的两个HashMap的key和value全部相同的话, 那么, 它们的hashcode就是相同的

所以在任何时候不要使用HashMap或一些特殊容器的Hashcode作为key来进行缓存

  • map通常情况下都是hash桶结构,但是当桶太大的时候,会转换成红黑树,可以增加在桶太大情况下访问效率 桶变成红黑树的代码如下
      final void treeifyBin(Node<K,V>[] tab, int hash) {
      int n, index; Node<K,V> e;
      //这里MIN_TREEIFY_CAPACITY派上了用场,及时单个桶数量达到了树化的阈值,总的容量没到,也不会进行树化
      if (tab == null || (n = tab.length) < MIN_TREEIFY_CAPACITY)
          resize();
      else if ((e = tab[index = (n - 1) & hash]) != null) {
          TreeNode<K,V> hd = null, tl = null;
          do {
          // 返回树节点 return new TreeNode<>(p.hash, p.key, p.value, next);
          TreeNode<K,V> p = replacementTreeNode(e, null);
          //为空说明是第一个节点,作为树的根节点
          if (tl == null)
              hd = p;
          //设置树的前后节点
          else {
              p.prev = tl;
              tl.next = p;
          }
          tl = p;
          } while ((e = e.next) != null);
          //对整棵树进行处理,形成红黑树
          if ((tab[index] = hd) != null)
          hd.treeify(tab);
      }
      }
2.3.2.5 HashMap在JDK7,JDK8 区别
  • 数据结构方面: JDK 1.6,1.7 的HashMap 使用 Node数组+链表; JDK1.8 HashMap是使用Node数组+链表+红黑树的数据结构来实现。

    在JDK 1.7中,HashMap处理“碰撞”的时候,都是采用链表来存储,当碰撞的结点很多时,查询时间是O(n),在JDK 1.8中,HashMap处理“碰撞”增加了红黑树这种数据结构,当碰撞结点较少时,采用链表存储,当较大时(>8个),采用红黑树(特点是查询时间是O(logn))存储(有一个阀值控制,大于阀值(8个),将链表存储转换成红黑树存储)

  • 新节点插入到链表是的插入顺序不同
    jdk7插入在头部,jdk8插入在尾部。为什么jdk7要插入在头部呢?因为插入头部效率高,不需要遍历整个链表。jdk8中为什么新来的元素是放在节点的后面?我们知道jdk8链表大于8的时候就要树化,本身就要算这个链表的长度有多大,链表算大小就要一个个去遍历了,遍历到了才能知道数量,也可以直接把数据放到后面了,这样也是很方便,减少了把数据移动到头数组位置这一步,效率也提高了。jdk7中出现的HashMap死循环bug不会有了,因为jdk8这里是只是平移,没有调换顺序。

  • hash算法有所简化
    jdk7默认初始化大小16,加载因子0.75。如果传入了size,会变为大于等于当前值的2的n次方的最小的数。为什么是2次方数?因为indexFor方法的时候,h&(length-1),length是2的次方,那么length-1总是00011111等后面都是1的数,h&它之后,其实就相当于取余,与的效率比取余高,所以用了这种方式达到高效率。

    hash函数里面方法里面,数会经过右移,为什么要右移?因为取余操作都是操作低位,hash碰撞的概率会提高,为了减少hash碰撞,右移就可以将高位也参与运算,减少了hash碰撞。
    jdk7 hash函数

  /**
   * Retrieve object hash code and applies a supplemental hash function to the
   * result hash, which defends against poor quality hash functions.  This is
   * critical because HashMap uses power-of-two length hash tables, that
   * otherwise encounter collisions for hashCodes that do not differ
   * in lower bits. Note: Null keys always map to hash 0, thus index 0.
   */
  final int hash(Object k) {
      int h = hashSeed;
      if (0 != h && k instanceof String) {
          return sun.misc.Hashing.stringHash32((String) k);
      }

      h ^= k.hashCode();

      // This function ensures that hashCodes that differ only by
      // constant multiples at each bit position have a bounded
      // number of collisions (approximately 8 at default load factor).
      h ^= (h >>> 20) ^ (h >>> 12);
      return h ^ (h >>> 7) ^ (h >>> 4);
  }

jdk8 hash函数

/**
   * Computes key.hashCode() and spreads (XORs) higher bits of hash
   * to lower.  Because the table uses power-of-two masking, sets of
   * hashes that vary only in bits above the current mask will
   * always collide. (Among known examples are sets of Float keys
   * holding consecutive whole numbers in small tables.)  So we
   * apply a transform that spreads the impact of higher bits
   * downward. There is a tradeoff between speed, utility, and
   * quality of bit-spreading. Because many common sets of hashes
   * are already reasonably distributed (so don't benefit from
   * spreading), and because we use trees to handle large sets of
   * collisions in bins, we just XOR some shifted bits in the
   * cheapest possible way to reduce systematic lossage, as well as
   * to incorporate impact of the highest bits that would otherwise
   * never be used in index calculations because of table bounds.
   */
  static final int hash(Object key) {
      int h;
      return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
  }

为什么jdk8的hash函数会变简单?jdk8中用的是链表过度到红黑树,效率会提高,所以jdk8提高查询效率的地方由红黑树去实现,没必要像jdk7那样右移那么复杂。

  • HashMap扩容条件不同
    JDK7中HashMap扩容是要同时满足两个条件:

    1. 当前数据存储的数量(即size())大小必须大于等于阈值
    2. 当前加入的数据是否发生了hash冲突

    因为上面这两个条件,所以存在下面两种情况:

    1. 就是hashmap在存值的时候(默认大小为16,负载因子0.75,阈值12),可能达到最后存满16个值的时候,再存入第17个值才会发生扩容现象,因为前16个值,每个值在底层数组中分别占据一个位置,并没有发生hash碰撞。
    2. 当然也有可能存储更多值(超多16个值,最多可以存26个值)都还没有扩容。原理:前11个值全部hash碰撞,存到数组的同一个位置(这时元素个数小于阈值12,不会扩容),后面所有存入的15个值全部分散到数组剩下的15个位置(这时元素个数大于等于阈值,但是每次存入的元素并没有发生hash碰撞,所以不会扩容),前面11+15=26,所以在存入第27个值的时候才同时满足上面两个条件,这时候才会发生扩容现象。

    JDK8中,扩容的条件只有一个,就是当前容量大于阈值(阈值等于当前hashmap最大容量乘以负载因子)

  • 扩容计算新索引方法不一样 详解并发下的HashMap以及JDK8的优化

    JDK7中通过transfer方法将旧数组中的元素复制到新数组,在这个方法中进行了包括释放旧的Entry中的对象引用,该过程中如果需要重新计算hash值就重新计算,然后根据indexfor()方法计算索引值。而索引值的计算方法为: h & (length-1) ,即hashcode计算出的hash值和数组长度进行与运算。
    JDK1.7中rehash的时候,旧链表迁移新链表的时候,如果在新表的数组索引位置相同,则链表元素会倒置,因为他采用的是头插法,先拿出旧链表头元素。

    一般认为,Java的%、/操作比&慢10倍左右,因此采用&运算而不是h % length会提高性能。
    JDK8中扩充HashMap的时候不用重新计算hash,只要要看看原来的hash值新添加的那个bit是1还是0就好了,是0的话索引没变,是1的话索引变成“原索引+“oldCap”。
    这样做既省去了重新计算hash值的时间,而且同时,因为新添加的1bit是0还是1可以认为是随机的,因而resize的过程,均匀的把之前的冲突的节点分散到新的bucket了。

  • 扩容机制有所优化
    在JDK7中,对所有链表进行rehash计算;在JDK8中,实际上也是通过取余找到元素所在的数组的位置,取余的方式在putVal里面:i = (n - 1) & hash。我们假设,在扩容之前,key取余之后留下了n位。扩容之后,容量变为2倍,所以key取余得到的就有n+1位。在这n+1位里面,如果第1位是0,那么扩容前后这个key的位置还是在相同的位置(因为hash相同,并且余数的第1位是0,和之前n位的时候一样,所以余数还是一样,位置就一样了);如果这n+1位的第一位是1,那么就和之前的不同,那么这个key就应该放在之前的位置再加上之前整个数组的长度的位置。

      hiTail.next = null;
      newTab[j + oldCap] = hiHead;

通过上面的操作就减少了移动所有数据带来的消耗。
图解上述步骤可能更清楚一点:
在这里插入图片描述

  • 节点表示不同
    在jdk7中,节点是Entry,在jdk8中节点是Node,其实是一样的。

  • 关于jdk7HashMap的一些细节
    1、jdk7里面addEntry方法扩容的条件size>threshold,还有一个很容易忽略的,就是null!=table[bucketIndex],这个是什么意思?意思是如果当前放进来的值的那个位置也被占用了,才进行扩容,否则还是放到那个空的位置就行了,反正不存在hash冲突。(但是在jdk8里面取消了这个限制,只要达到了threshold,不管原来的位置空不空,还是要扩容)

    2、jdk7 resize方法多线程死循环的bug HashMap 在高并发下引起的死循环 在jdk8的这个bug已经解决了 【JDK 8 用 head 和 tail 来保证链表的顺序和之前一样;JDK 7 rehash 会倒置链表元素),但是还会有数据丢失等弊端(并发本身的问题)】

  • 关于jdk8HashMap的一些细节
    1、jdk8 默认初始化大小16,加载因子0.75。还有一个默认的树化的大小8。非树化大小为6,也就是红黑树的元素小于6的时候,又会变回一个链表。为什么是8和6?
    解释仅供参考:
    HashMap在JDK1.8及以后的版本中引入了红黑树结构,若桶中链表元素个数大于等于8时,链表转换成树结构;若桶中链表元素个数小于等于6时,树结构还原成链表。因为红黑树的平均查找长度是log(n),长度为8的时候,平均查找长度为3,如果继续使用链表,平均查找长度为8/2=4,这才有转换为树的必要。链表长度如果是小于等于6,6/2=3,虽然速度也很快的,但是转化为树结构和生成树的时间并不会太短。
    还有选择6和8,中间有个差值7可以有效防止链表和树频繁转换。假设一下,如果设计成链表个数超过8则链表转换成树结构,链表个数小于8则树结构转换成链表,如果一个HashMap不停的插入、删除元素,链表个数在8左右徘徊,就会频繁的发生树转链表、链表转树,效率会很低。
    2、jdk8的putVal方法:i = (n - 1) & hash这个其实也就是取余操作了。

2.3.3. Hashtable

  • Hashtable 也是一个散列表,它也是通过“拉链法”解决哈希冲突的。
  • 它存储的内容是键值对(key-value)映射。Hashtable 继承于Dictionary,实现了Map、Cloneable、java.io.Serializable接口。
  • Hashtable 的函数都是同步的,这意味着它是线程安全的。它的key、value都不可以为null。此外,Hashtable中的映射不是有序的。
  • Hashtable 的实例有两个参数影响其性能:初始容量 和 加载因子。容量 是哈希表中桶 的数量,初始容量 就是哈希表创建时的容量。注意,哈希表的状态为 open:在发生“哈希冲突”的情况下,单个桶会存储多个条目,这些条目必须按顺序搜索。
  • 通常,默认加载因子是 0.75, 这是在时间和空间成本上寻求一种折衷。加载因子过高虽然减少了空间开销,但同时也增加了查找某个条目的时间(在大多数 Hashtable 操作中,包括 get 和 put 操作,都反映了这一点)。
2.3.3.1 Hashtable的主要对外接口
  • elements()

    elements() 的作用是返回“所有value”的枚举对象

	 public synchronized Enumeration<V> elements() {
	       return this.<V>getEnumeration(VALUES);
	 }
	
	   // 获取Hashtable的枚举类对象
	 private <T> Enumeration<T> getEnumeration(int type) {
	       if (count == 0) {
	           return (Enumeration<T>)emptyEnumerator;
	       } else {
	           return new Enumerator<T>(type, false);
	       }
	 }

(01) 若Hashtable的实际大小为0,则返回“空枚举类”对象emptyEnumerator;
(02) 否则,返回正常的Enumerator的对象。(Enumerator实现了迭代器和枚举两个接口)

emptyEnumerator对象是如何实现的

	  private static Enumeration emptyEnumerator = new EmptyEnumerator();
	
	  // 空枚举类
	  // 当Hashtable的实际大小为0;此时,又要通过Enumeration遍历Hashtable时,返回的是“空枚举类”的对象。
	  private static class EmptyEnumerator implements Enumeration<Object> {
	
	      EmptyEnumerator() {
	      }
	
	      // 空枚举类的hasMoreElements() 始终返回false
	      public boolean hasMoreElements() {
	          return false;
	      }
	
	      // 空枚举类的nextElement() 抛出异常
	      public Object nextElement() {
	          throw new NoSuchElementException("Hashtable Enumerator");
	      }
	  }

Enumerator的作用是提供了“通过elements()遍历Hashtable的接口” 和 “通过entrySet()遍历Hashtable的接口”。因为,它同时实现了 “Enumerator接口”和“Iterator接口”。

	  private class Enumerator<T> implements Enumeration<T>, Iterator<T> {
	      // 指向Hashtable的table
	      Entry[] table = Hashtable.this.table;
	      // Hashtable的总的大小
	      int index = table.length;
	      Entry<K,V> entry = null;
	      Entry<K,V> lastReturned = null;
	      int type;
	
	      // Enumerator是 “迭代器(Iterator)” 还是 “枚举类(Enumeration)”的标志
	      // iterator为true,表示它是迭代器;否则,是枚举类。
	      boolean iterator;
	
	      // 在将Enumerator当作迭代器使用时会用到,用来实现fail-fast机制。
	      protected int expectedModCount = modCount;
	
	      Enumerator(int type, boolean iterator) {
	          this.type = type;
	          this.iterator = iterator;
	      }
	
	      // 从遍历table的数组的末尾向前查找,直到找到不为null的Entry。
	      public boolean hasMoreElements() {
	          Entry<K,V> e = entry;
	          int i = index;
	          Entry[] t = table;
	          /* Use locals for faster loop iteration */
	          while (e == null && i > 0) {
	              e = t[--i];
	          }
	          entry = e;
	          index = i;
	          return e != null;
	      }
	
	      // 获取下一个元素
	      // 注意:从hasMoreElements() 和nextElement() 可以看出“Hashtable的elements()遍历方式”
	      // 首先,从后向前的遍历table数组。table数组的每个节点都是一个单向链表(Entry)。
	      // 然后,依次向后遍历单向链表Entry。
	      public T nextElement() {
	          Entry<K,V> et = entry;
	          int i = index;
	          Entry[] t = table;
	          /* Use locals for faster loop iteration */
	          while (et == null && i > 0) {
	              et = t[--i];
	          }
	          entry = et;
	          index = i;
	          if (et != null) {
	              Entry<K,V> e = lastReturned = entry;
	              entry = e.next;
	              return type == KEYS ? (T)e.key : (type == VALUES ? (T)e.value : (T)e);
	          }
	          throw new NoSuchElementException("Hashtable Enumerator");
	      }
	
	      // 迭代器Iterator的判断是否存在下一个元素
	      // 实际上,它是调用的hasMoreElements()
	      public boolean hasNext() {
	          return hasMoreElements();
	      }
	
	      // 迭代器获取下一个元素
	      // 实际上,它是调用的nextElement()
	      public T next() {
	          if (modCount != expectedModCount)
	              throw new ConcurrentModificationException();
	          return nextElement();
	      }
	
	      // 迭代器的remove()接口。
	      // 首先,它在table数组中找出要删除元素所在的Entry,
	      // 然后,删除单向链表Entry中的元素。
	      public void remove() {
	          if (!iterator)
	              throw new UnsupportedOperationException();
	          if (lastReturned == null)
	              throw new IllegalStateException("Hashtable Enumerator");
	          if (modCount != expectedModCount)
	              throw new ConcurrentModificationException();
	
	          synchronized(Hashtable.this) {
	              Entry[] tab = Hashtable.this.table;
	              int index = (lastReturned.hash & 0x7FFFFFFF) % tab.length;
	
	              for (Entry<K,V> e = tab[index], prev = null; e != null;
	                  prev = e, e = e.next) {
	                  if (e == lastReturned) {
	                      modCount++;
	                      expectedModCount++;
	                      if (prev == null)
	                          tab[index] = e.next;
	                      else
	                          prev.next = e.next;
	                      count--;
	                      lastReturned = null;
	                      return;
	                  }
	              }
	              throw new ConcurrentModificationException();
	          }
	      }
	  }
  • Hashtable实现的Serializable接口

    Hashtable实现java.io.Serializable,分别实现了串行读取、写入功能。

    串行写入函数: 就是将Hashtable的“总的容量,实际容量,所有的Entry” 都写入到输出流中

    串行读取函数: 根据写入方式读出将Hashtable的“总的容量,实际容量,所有的Entry”依次读出

  • 其余对外接口原理和HashMap一样

2.3.4. WeakHashMap

  • WeakHashMap是基于java弱引用实现的HashMap

  • WeakHashMap 继承于AbstractMap,实现了Map接口。WeakHashMap 也是一个散列表,它存储的内容也是键值对(key-value)映射,而且键和值都可以是null。

  • 在 WeakHashMap 中,当某个键不再正常使用时,会被从WeakHashMap中被自动移除。更精确地说,对于一个给定的键,其映射的存在并不阻止垃圾回收器对该键的丢弃,这就使该键成为可终止的,被终止,然后被回收。某个键被终止时,它对应的键值对也就从映射中有效地移除了。

  • 这个“弱键”的原理呢?

    大致上就是,通过WeakReference和ReferenceQueue实现的。 WeakHashMap的key是“弱键”,即是WeakReference类型的;ReferenceQueue是一个队列,它会保存被GC回收的“弱键”。“弱键”如何被自动从WeakHashMap中删除的实现步骤是:

    (01) 新建WeakHashMap,将“键值对”添加到WeakHashMap中。 实际上,WeakHashMap是通过数组table保存Entry(键值对);每一个Entry实际上是一个单向链表,即Entry是键值对链表。
    (02) 当某“弱键”不再被其它对象引用,并被GC回收时。在GC回收该“弱键”时,这个“弱键”也同时会被添加到ReferenceQueue(queue)队列中。
    (03) 当下一次我们需要操作WeakHashMap时,会先同步table和queue。table中保存了全部的键值对,而queue中保存被GC回收的键值对;同步它们,就是删除table中被GC回收的键值对。

  • 和HashMap一样,WeakHashMap是不同步的。可以使用 Collections.synchronizedMap 方法来构造同步的 WeakHashMap。 在结构上,基本和HashMap一致。在Java8中,唯一存储上的不同点就是,当冲突的key变多时,HashMap引入了二叉树(红黑树)进行存储,而WeakHashMap则一直使用链表进行存储。

  • 在WeakHashMap中,就只有key被回收,而value,则是通过expungeStaleEntries赋值为null。 参考介绍在WeakHashMap中 ReferenceQueue (引用队列) 作用.

2.3.5. LinkedHashMap

  • LinkedHashMap 继承自 HashMap,在 HashMap 基础上,通过维护一条双向链表,解决了 HashMap 不能随时保持遍历顺序和插入顺序一致的问题。除此之外,LinkedHashMap 对访问顺序也提供了相关支持。

  • LinkedHashMap 基本数据结构是相同的数组、链表、红黑树

  • LinkedHashMap 内部类 Entry 继承自 HashMap 内部类 Node,并新增了两个引用,分别是 before 和 after。这两个引用的用途不难理解,也就是用于维护双向链表。

  • HashMap 的内部类 TreeNode 不继承它的一个内部类 Node,却继承自 Node 的子类 LinkedHashMap 内部类 Entry 原因如下:

  • 在 HashMap 的设计思路注释中解释了原因: TreeNode 对象的大小约是普通 Node 对象的2倍,我们仅在桶(bin)中包含足够多的节点时再使用。当桶中的节点数量变少时(取决于删除和扩容),TreeNode 会被转成 Node。当用户实现的 hashCode 方法具有良好分布性时,树类型的桶将会很少被使用。

  • 也就是说 TreeNode 使用的并不多,浪费那点空间是可接受的。假如 TreeNode 机制继承自 Node 类,那么它要想具备组成链表的能力,就需要 Node 去继承 LinkedHashMap 的内部类 Entry。这个时候就得不偿失了,浪费很多空间去获取不一定用得到的能力。

  • 链表的建立过程

    1. 链表的建立过程是在插入键值对节点时开始的,初始情况下,让 LinkedHashMap 的 head 和 tail 引用同时指向新节点,链表就算建立起来了。随后不断有新节点插入,通过将新节点接在 tail 引用指向节点的后面,即可实现链表的更新。
    2. LinkedHashMap 本身并没有覆写父类的 put 方法,而是直接使用了父类的实现,但也有不同,不同在于LinkedHashMap实现了afterNodeAccess,afterNodeInsertion方法
  • 链表节点获取

    • 使用get()方法,get进行了重写
	     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;
	     }
	     
	     //唯一需要关注的就是在插入模式中,获取值后又一次进行把当前节点移到链表尾部操作

2.3.5. ConcurrentHashMap

2.3.6. ConcurrentHashMap JDK6、JDK7和JDK8总结

2.3.7. HashMap 与Hashtable 区别

  • HashMap:

    a. 是非线程安全的;
    b. HashMap可以使用null作为key。
    c. 遍历使用的是Iterator迭代器;
    d. HashMap是对Map接口的实现
    e. HashMap的初始容量为16,填充因子默认都是0.75,HashMap扩容时是当前容量翻倍即:capacity*2
    f. 计算hash的方法: HashMap计算hash对key的hashcode进行了二次hash,以获得更好的散列值,然后对table数组长度取摸

static int hash(int h) {
        // This function ensures that hashCodes that differ only by
        // constant multiples at each bit position have a bounded
        // number of collisions (approximately 8 at default load factor).
        h ^= (h >>> 20) ^ (h >>> 12);
        return h ^ (h >>> 7) ^ (h >>> 4);
    }
 
 static int indexFor(int h, int length) {
        return h & (length-1);
    }

g. 底层实现都是数组+链表结构实现
h. 添加、删除、获取元素时都是先计算hash
i. 在插入元素时,可能会扩大数组的容量,在扩大容量时须要重新计算hash,并复制对象到新的数组中;

  • HashTable:

    a. 是线程安全的(Hashtable的实现方法里面都添加了synchronized关键字来确保线程同步,因此相对而言HashMap性能会高一些);
    b. 无论是key还是value都不允许有null值的存在;在HashTable中调用put方法时,如果key为null,直接抛出NullPointerException异常;
    c. 遍历使用的是Enumeration列举;
    d. HashTable实现了Map接口和Dictionary抽象类
    e. Hashtable初始容量为11,填充因子默认都是0.75,Hashtable扩容时是容量翻倍+1即:capacity*2+1
    f. 计算hash的方法: Hashtable计算hash是直接使用key的hashcode对table数组的长度直接进行取模

 int hash = key.hashCode();
 int index = (hash & 0x7FFFFFFF) % tab.length;

g. 底层实现都是数组+链表结构实现

3. Java里面引用分类 WeakHashMap的延伸

  • 此处涉及到JVM虚拟机部分知识,WeakHashMap涉及到一个叫做弱引用的东西,干脆在此处延伸介绍一下

3.1 java中的四种引用方式如下:

  • 强引用: Java默认支持的操作,即使进行了多次的GC回收,即使JVM真的已经不够用了,即使JVM最终不得已抛出了OOM错误,那么该引用继续抢占。
    在多线程状态下,即使产生多个引用对象,空间也不会被回收。每个线程都会出现OOM ,强引用并不是造成OOM的关键性因素

  • 软引用: 当内存空间不足时,可以回收此内存空间。如果充足则不回收,可以用其完成缓存的一些处理操作开发。缓存:保证数据更快的操作(读取),比如网页缓存,图片缓存等. 是不重要的数据。可以作为牺牲来释放空间。

    • 软引用指的是当内存不足的时候才进行GC的空间释放,但是如果要想使用软引用必须单独使用特殊的处理类 java.lang.ref.SoftReference
    • 软引用与强引用相比,最大的特点在于:软引用中保存的内容如果在内存富裕的时候会继续保留,内存不足会作为第一批的丢弃者进行垃圾空间的释放。
  • 弱引用: 不管内存是否紧张,只要一出现GC处理,则立即回收。

    • 弱引用使用“WeakReference”类来完成。 java.lang.ref.WeakReference
    • 一旦执行了GC,那么就需要进行内存空间的释放,在类集里面有一个与弱引用功能相似的Map集合–WeakHashMap
    • WeakHashMap最大的好处是可以用它保存一些共享数据,这些共享数据如果长时间不使用,可以将其清空
  • 虚引用: 和没有引用是一样的。 比如HashMap根据key取得值,设置key值为null和不设置key值的效果是一样的。

    • JDK1.2 之后,用 PhantomReference 类来表示,通过查看这个类的源码,发现它只有一个构造函数和一个 get() 方法,而且它的 get() 方法仅仅是返回一个null,也就是说将永远无法通过虚引用来获取对象,虚引用必须要和 ReferenceQueue 引用队列一起使用。
	     public class PhantomReference<T> extends Reference<T> {
	         /**
	         * Returns this reference object's referent.  Because the referent of a
	         * phantom reference is always inaccessible, this method always returns
	         * <code>null</code>.
	         *
	         * @return  <code>null</code>
	         */
	         public T get() {
	             return null;
	         }
	         public PhantomReference(T referent, ReferenceQueue<? super T> q) {
	             super(referent, q);
	         }
	     }
  • 构造方法中的 ReferenceQueue (引用队列) 使用方法

    引用队列可以与软引用、弱引用以及虚引用一起配合使用,当垃圾回收器准备回收一个对象时,如果发现它还有引用,那么就会在回收对象之前,把这个引用加入到与之关联的引用队列中去。程序可以通过判断引用队列中是否已经加入了引用,来判断被引用的对象是否将要被垃圾回收,这样就可以在对象被回收之前采取一些必要的措施。

    与软引用、弱引用不同,虚引用必须和引用队列一起使用。

3.1.1 在WeakHashMap中 ReferenceQueue (引用队列) 作用

queue是用来存放那些,被jvm清除的entry的引用,因为WeakHashMap使用的是弱引用,所以一旦gc,就会有key键被清除,所以会把entry加入到queue中。在WeakHashMap中加入queue的目的,就是为expungeStaleEntries所用。

	   Entry(Object key, V value,
	       ReferenceQueue<Object> queue,
	       int hash, Entry<K,V> next) {
	       super(key, queue);
	       this.value = value;
	       this.hash  = hash;
	       this.next  = next;
	   }

在构造每一个Entry时,都将它与queue绑定,从而一旦被jvm回收,那么这个Entry就会倍加如到queue中。

expungeStaleEntries方法具体意思

方法里面就仅仅是释放value值。由前面的Entry的构造方法可知, super(key, queue); 传入父类的仅仅是key,所以经过仔细阅读jdk源码开始部分分析后,得出结论,在WeakHashMap中,有jvm回收的,仅仅是Entry的key部分,所以一旦jvm强制回收,那么这些key都会为null,再通过私有的expungeStaleEntries 方法,把value也制null,并且把size–。

       /**
       * 从ReferenceQueue中取出过期的entry,从WeakHashMap找到对应的entry,逐一删除
       * 注意,只会把value置为null。
       */
       private void expungeStaleEntries() {
           for (Object x; (x = queue.poll()) != null; ) {
               //遍历queue
               synchronized (queue) {
                   @SuppressWarnings("unchecked")
                       Entry<K,V> e = (Entry<K,V>) x;
                   int i = indexFor(e.hash, table.length);
                   Entry<K,V> prev = table[i];
                   Entry<K,V> p = prev;
                   while (p != null) {
                       //遍历table[i]所在链表
                       Entry<K,V> next = p.next;
                       if (p == e) {
                           //queue里面有e,那就删了。
                           if (prev == e)
                               //e就是当前的p.next
                               table[i] = next;
                           else
                               prev.next = next;
                           // Must not null out e.next;
                           // stale entries may be in use by a HashIterator
                           //置为null,帮助gc。只制null了value。
                           e.value = null; // Help GC
                           //设置e的value,但是没看到设置e的key。
                           size--;
                           break;
                       }
                       prev = p;
                       p = next;
                   }
               }
           }
       }

上面代码逻辑为,当在table中找到queue中存在元素时,就把value制空,然后size–。
所以在WeakHashMap中,就只有key被回收,而value,则是通过expungeStaleEntries赋值为null。

  • 1
    点赞
  • 10
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值