了解Collection的实现类(List,Set,Map)

提示:以下是本篇文章正文内容,下面案例可供参考

一、Collection集合图

在这里插入图片描述

1. List

1. ArrayList

1. ArrayList 底层实现

add

    public boolean add(E e) {
        //判断是否需要扩容   size :数组中已经存在的元素的个数
        ensureCapacityInternal(size + 1);  // Increments modCount!!
        elementData[size++] = e;
        return true;
    }


    private void ensureCapacityInternal(int minCapacity) {
        ensureExplicitCapacity(calculateCapacity(elementData, minCapacity));
    }

	private static int calculateCapacity(Object[] elementData, int minCapacity) {
        if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
            return Math.max(DEFAULT_CAPACITY, minCapacity);
        }
        return minCapacity;
    }

    private void ensureExplicitCapacity(int minCapacity) {
    	//修改次数+1
        modCount++;

        // overflow-conscious code
        //元素的个数(含当前添加)减去数组的长度大于0   说明容量不够,需要扩容
        if (minCapacity - elementData.length > 0)
            //数组扩容
            grow(minCapacity);
    }
  1. 当往ArrayList 添加元素时,会根据数组中元素个数+1去判断容量是否充足;
  2. 当数组中的元素个数减去数组长度大于0时(minCapacity - elementData.length > 0),数组会进行扩容,如果是第一次添加元素默认初始容量为10的数组,如果不是第一次添加则会扩容为原数组的1.5倍;
    扩容的同时需要将原来数组中的数据复制到新数组里,保持元素存储空间连续
  3. 当数组容量充足,则直接添加到数组索引为 元素总个数+1的位置(elementData[size++] = e;);

get

    public E get(int index) {
	    //对比索引是否越界
        rangeCheck(index);
		//返回当前索引的元素
        return elementData(index);
    }

    E elementData(int index) {
        return (E) elementData[index];
    }
  1. 当往ArrayList 获取元素时,会根据数组中元素个数与获取的索引对比,看是否索引越界;
  2. 索引越界则直接抛异常,没有越界则获取数组中当前索引的元素;

remove

 public E remove(int index) {
 		//对比索引是否越界
        rangeCheck(index);
		//修改次数+1
        modCount++;
        //获取需要删除的元素
        E oldValue = elementData(index);
		//计算需要移动的元素个数
        int numMoved = size - index - 1;
        //移动的元素个数大于0 
        if (numMoved > 0)
        //进行数组移动 将要删除元素后面的元素向前覆盖 
            System.arraycopy(elementData, index+1, elementData, index,
                             numMoved);
         // clear to let GC do its work     翻译:等待GC回收                 
        elementData[--size] = null;

        return oldValue;
    }
  1. 当删除ArrayList 获取元素时,会根据数组中元素个数与删除的索引对比,看是否索引越界;
  2. 索引越界则直接抛异常,没有越界则获取数组中当前索引的元素;
  3. 计算删除后需要配移动的元素,移动后面的数组,调整元素下标;

group

    private void grow(int minCapacity) {
        // overflow-conscious code
        int oldCapacity = elementData.length;
        //右移运算  进行1.5倍扩容
        int newCapacity = oldCapacity + (oldCapacity >> 1);
        //新容量减去最小容量小于0    使用最小容量
        if (newCapacity - minCapacity < 0)
            newCapacity = minCapacity;
            //最小容量将去默认最大值大于0
        if (newCapacity - MAX_ARRAY_SIZE > 0)
            newCapacity = hugeCapacity(minCapacity);
        // minCapacity is usually close to size, so this is a win:
        //数组移动  将原来数组中的数据复制到新数组里,保持元素存储空间连续
        elementData = Arrays.copyOf(elementData, newCapacity);
    }

	
	    private static int hugeCapacity(int minCapacity) {
        if (minCapacity < 0) // overflow
            throw new OutOfMemoryError();
        return (minCapacity > MAX_ARRAY_SIZE) ?
            Integer.MAX_VALUE :
            MAX_ARRAY_SIZE;
    }
2. ArrayList的特点
  1. ArrayList存储是有序的,元素存储空间连续,每个元素都有下标;
  2. ArrayList存储数据可重复,可以存储null;
  3. ArrayList基于索引的随机检索效率高,增删慢,末尾添加,删除元素效率高;
  4. ArrayList非线程安全;
3. 总结
  1. ArrayList底层是基于Object数组,默认初始容量为10,容量随着数据的添加而自动增长的。
  2. 当调用add()方法时,会先判断数组容量是否充足,如果容量不充足则会调用grow()扩容机制,将容量扩大到原来的1.5倍,若容量充足,则直接添加。
  3. ArrayList扩容不是无限增长的,在扩容后会判断新数组的容量是否大于存入的数据,如果新数组的长度还不够,则会将数据的长度赋值给新数组的长度,最后判断新数组的长度不允许超过默认最大限制,否则溢出。
  4. ArrayList在扩容的同时需要将原来数组中的数据复制到新数组里,保持元素存储空间连续。
  5. ArrayList随机向某个位置插入,则会让该索引后的元素下标+1,remove()方法则会让当前元素后的下标-1,并把最后一位的值置空,方便GC。
4. 思考
  1. 为什么扩容因子是1.5倍?不是2,或2.5?
    答:因为节省内存,避免内存浪费,且位移运算在内存效率高,还可以减少扩容次数。

2. Vecto

1. Vecto底层实现

add

 public synchronized boolean add(E e) {
        modCount++;
        //判断是否需要扩容 elementCount:数组中已经存在的元素的个数
        ensureCapacityHelper(elementCount + 1);
        //将添加的元素放在最后面
        elementData[elementCount++] = e;
        return true;
    }

 
    private void ensureCapacityHelper(int minCapacity) {
        // overflow-conscious code
        //元素的个数(含当前添加)减去数组的长度大于0   说明容量不够,需要扩容
        if (minCapacity - elementData.length > 0)
        	//数组扩容
            grow(minCapacity);
    }
  1. 当往Vecto添加元素时,会根据数组中元素个数+1去判断容量是否充足;
  2. 当数组中的元素个数减去数组长度大于0时(minCapacity - elementData.length > 0),数组会进行扩容;
  3. 当数组容量充足,则直接添加到数组索引为 元素总个数+1的位置(elementData[elementCount++] = e);

get

    public synchronized E get(int index) {
    	 //对比索引是否越界
        if (index >= elementCount)
            throw new ArrayIndexOutOfBoundsException(index);

        return elementData(index);
    }

  1. 当往Vecto 获取元素时,会根据数组中元素个数与获取的索引对比,看是否索引越界;
  2. 索引越界则直接抛异常,没有越界则获取数组中当前索引的元素;

remove

 public synchronized E remove(int index) {
        modCount++;
         //对比索引是否越界
        if (index >= elementCount)
            throw new ArrayIndexOutOfBoundsException(index);
        E oldValue = elementData(index);
		//计算需要移动的个数   大于0则需要移动
        int numMoved = elementCount - index - 1;
        if (numMoved > 0)
            //进行数组移动 将要删除元素后面的元素向前覆盖 
            System.arraycopy(elementData, index+1, elementData, index,
                             numMoved);
        elementData[--elementCount] = null; // Let gc do its work

        return oldValue;
    }
  1. 当删除ArrayList 获取元素时,会根据数组中元素个数与删除的索引对比,看是否索引越界;
  2. 索引越界则直接抛异常,没有越界则获取数组中当前索引的元素;
  3. 计算删除后需要配移动的元素,移动后面的数组,调整元素下标;

grow

    private void grow(int minCapacity) {
        // overflow-conscious code
        int oldCapacity = elementData.length;
        //当增量大于0时,则Vecto数组扩大到原来数组长度+增量  否则扩大2倍
        int newCapacity = oldCapacity + ((capacityIncrement > 0) ?
                                         capacityIncrement : oldCapacity);
        if (newCapacity - minCapacity < 0)
            newCapacity = minCapacity;
        if (newCapacity - MAX_ARRAY_SIZE > 0)
            newCapacity = hugeCapacity(minCapacity);
        elementData = Arrays.copyOf(elementData, newCapacity);
    }

    private static int hugeCapacity(int minCapacity) {
        if (minCapacity < 0) // overflow
            throw new OutOfMemoryError();
        return (minCapacity > MAX_ARRAY_SIZE) ?
            Integer.MAX_VALUE :
            MAX_ARRAY_SIZE;
    }
2. Vecto的特点
  1. Vecto存储是有序的,元素存储空间连续,每个元素都有下标;
  2. Vecto新删慢,查询快,线程安全
3. 总结

与ArrayList的底层实现一样,只不过需要注意两个地方:

  1. Vecto扩容和增量有关,当Vecto容量不够,需扩容时,会先判断增量,如果增量大于0时,则Vecto扩大到原来数组长度+增量,否则Vecto扩大2倍;
  2. Vecto的方法中使用了synchronized进行同步,所以他是线程安全的;

现在基本都不使用Vecto了,如果想获取一个线程安全的集合,可以使用Collections.synchronizedList()来获取

3. 了解链表

1. 单向链表

在这里插入图片描述
在这里插入图片描述

2. 双向链表

在这里插入图片描述
在这里插入图片描述

4. LinkedList

双向链表的二分查找

  1. LinkedList的底层是基于双向链表来实现的,允许存储包括null在内的所有元素,;
  2. 双向链表的内存是不连续的,没有长度的概念,无需初始化,无需扩容,存储大小受机器内存限制;
  3. 双向链表由三部分构成:上一个指针,本节点存储的元素,下一个指针
    a. 上一个指针:指向上一个数据
    b. 本节点存储的元素
    c. 下一个指针:指向下一个元素
    d. first 是双向链表的头节点,前一个节点是 null;
    e. last 是双向链表的尾节点,后一个节点是 null;
    f. 当链表中没有数据时,first 和 last 是同一个节点,前后指向都是 null;
    g. 当链表中只有一个数据时,first 和 last 是同一个节点,前后指向都是 null;
1. LinkedList底层实现

add

    public boolean add(E e) {
        linkLast(e);
        return true;
    }

    void linkLast(E e) {
    	//获取双向链表的last
        final Node<E> l = last;
        //将要插入的元素封装为新的Node  上一个指针指向last   下一个指针为null
        final Node<E> newNode = new Node<>(l, e, null);
        //重置last   将新的节点设置为last
        last = newNode;
        //判断之前的last是否为null
        if (l == null)
           //为空:说明第一次插入,新的Node同时为设置为first 节点
            first = newNode;
        else
           //非首次添加,之前的last的下一个指针指向新的Node
            l.next = newNode;
        size++;
        modCount++;
    }
  1. 当向LinkedList添加元素时,会先获取双向链表中的last节点(就是最后一个节点);
  2. 如果last节点为null,说明首次添加,将当前元素设置为first 和 last节点,前后指针指向都是 null;
  3. 如果last节点不为null,本次新添加的元素的上一个指针last节点,下一个指针为null,而last节点的下一个指针指向本次新添加的node;
  4. 重置last节点,由新添加的node为last节点;

get

    public E get(int index) {
        // 判断索引是否越界
        checkElementIndex(index);
        return node(index).item;
    }

    private void checkElementIndex(int index) {
        if (!isElementIndex(index))
            throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
    }

    Node<E> node(int index) {
        // assert isElementIndex(index);
		//通过右移运算(折半查找) 判断当前索引是否小于size/2
        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;
        }
    }
  1. 当向LinkedList获取元素时,会先判断索引是否越界,是:则抛出异常;
  2. 通过右移运算符判断当前索引是否小于 数组元素的一般,是:从头部开始查找,否:从尾部开始查找;

remove

    public E remove(int index) {
        checkElementIndex(index);
        return unlink(node(index));
    }

    Node<E> node(int index) {
        // assert isElementIndex(index);
		//通过右移运算(折半查找) 判断当前索引是否小于size/2
        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;
        }
    }

	E unlink(Node<E> x) {
	        // assert x != null;
	        //获取要删除的元素
	        final E element = x.item;
	        //获取要删除的元素指向的下一个元素
	        final Node<E> next = x.next;
	        //获取要删除的元素指向的上一个元素
	        final Node<E> prev = x.prev;
			
	        if (prev == null) {
	            //说明删除的是first节点 ,重置first节点
	            first = next;
	        } else {
	        	//上一个元素的下指针重新关联到下一个元素
	            prev.next = next;
	            //要删除的元素断开与上一个元素的关联
	            x.prev = null;
	        }
	
	        if (next == null) {
	           //说明删除的是last节点 ,重置last节点
	            last = prev;
	        } else {
	            //下一个元素的上指针重新关联到上一个元素
	            next.prev = prev;
	            //要删除的元素断开与下一个元素的关联
	            x.next = null;
	        }
	
	        x.item = null;
	        size--;
	        modCount++;
	        return element;
	    }
  1. 当向LinkedList删除元素时,会先判断索引是否越界,是:则抛出异常;
  2. 获取要删除元素的上下元素;
  3. 判断上元素是否为空,为空说明删除的是头部节点,重置头部节点,由下元素替代;
  4. 不为空,则上元素的下指针徐指向下元素,同时删除元素的上指针置为null;
  5. 判断下元素是否为空,为空说明删除的是头部节点,重置头部节点,由下元素替代;
  6. 不为空,则下元素的上指针徐指向上元素,同时删除元素的下指针置为null;
  7. 删除元素,同时元素的数量-1,修改次数+1;
2. LinkedList的特点
  1. LinkedList底层是基于基于双向链表来实现的,链表的内存不是连续的,没办法利用cpu缓存预读数据,占用内存低;
  2. 链表没有长度的概念,无需初始化,不需要扩容,存储大小和机器内存有关;
  3. LinkedList允许存储包括null在内的所有元素,靠引用来关联前后的数据;
  4. LinkedList 采用双向链表,头尾新增跟删除快,不需要额外的开销去移动、拷贝元素,中间增删慢,主要查询耗时。随机访问速度慢;
  5. LinkedList非线程安全;

2. Set

HashSet是基于哈希表存储数据,TreeSet是基于红黑树存储数据。

1. HashSet

1. HashSet的底层实现
  1. HashSet 中的集合元素实际上由 HashMap 的 key 来保存
  2. 当往HashSet 中的集合添加自定义的类对象时,一定要重写equals方法和 hashCode方法,来保证存入对象的唯一性

add

    public boolean add(E e) {
	    /**
	     * 实际上底层是调用的HashMap的put方法
	     * 集合元素实际上由 HashMap 的 key 来保存,而 HashMap 的 value 则存储了一个 PRESENT
	     */
        return map.put(e, PRESENT)==null;
    }
	/**
	 * 下面的源码请到HashMap观看
	 * 	在此不再赘述
	 */
    public V put(K key, V value) {
        return putVal(hash(key), key, value, false, true);
    }
  1. HashSet基于哈希表(实际上是HashMap)来实现的,当往HashSet里面添加元素的时候,会先判断数组是否为空;
  2. 数组为空则调用resize()方法,初始化一个长度16,扩容因子为0.75的HashMap数组,扩容为2的幂次方,计算key的hash值放入对应数组内,而 HashMap 的 value 则存储了一个 PRESENT对象,此对象是一个静态的 Object 对象,被static final修饰;
  3. 数组不为空,计算key的hash值得到数组的下标,判断下标数组位置上是否存在数据,不存在则直接插入数据;
  4. 下标数组位置上存在数据,在链表上对比两个对象的Hash值 (Hash值相同不一定是同一个对象所以还要调用equals) 和KEY是否相同,是:则覆盖;
  5. 不是同一个对象,判读是否为红黑树的树节点(有可能已经转化成红黑树了),是:直接添加,
  6. 既不是相同对象,也不是树节点,则添加到链表的尾部,判断链表的长度是否大于8,并且判断数组的长度是否达到64;
  7. 如果链表长度为8,数组长度达到64,那么链表就会进化成红黑树。如果链表为8,数组长度小于64会进行resize()扩容成2倍桶数量,同时拆分链表至不同的桶,减少单个链表的大小。
  8. 最后判断hashmap的size是否达到阈值,是:进行扩容resize()。

remove

   /**
	* 实际上底层是调用的HashMap的remove方法
	* 集合元素实际上由 HashMap 的 key 来保存,而 HashMap 的 value 则存储了一个 PRESENT
	*/
    public boolean remove(Object o) {
        return map.remove(o)==PRESENT;
    }
    /**
	 * 下面的源码请到HashMap观看
	 * 	在此不再赘述
	 */
    public V remove(Object key) {
        Node<K,V> e;
        return (e = removeNode(hash(key), key, null, false, true)) == null ?
            null : e.value;
    }
  1. 当HashSet调用remove方法时,会调用HashMap的remove方法,计算key的hash值得到数组的下标,判断下标数组位置上是否存在数据,不存在则返回null;
  2. 存在数据,则调用hashCode()和equals()与第一个元素进行Hash值和KEY的对比,如果相同则直接删除第一个元素;
  3. 如果不相同则判断是否属于树节点,是: 直接删除,且会判断红黑树的节点数量,如果节点数量小于6个,则会退化成链表;
  4. 既不是第一个元素对象,也不是树节点,则调用hashCode()和equals()与链表的每一个元素进行Hash值和KEY的对比,如果相同则删除,如果不同返回为null;
  5. 删除后size-1(键-值映射的数量),链表重新关联,下一个Node替换当前被删除的Node;
2. HashSet的特点
  1. HashSet不能保证元素的顺序,因为HashMap是无序的;
  2. 允许存入null元素,但不允许元素重复,线程是不安全的;
  3. 增删速度快,查询速度慢;
  4. 没有get 方法,可以通过iterator()方法遍历元素;
3. 思考

HashSet为什么没有get方法

  1. HashSet是基于是HashMap(数组+链表+红黑树)来实现的,不能保证插入的顺序,且无法通过索引直接获取元素,因此get方法是没必要的,可以通过iterator()方法遍历元素;

为什么不能通过HashMap的get方法来实现HashSet的get方法

  1. HashMap是通过key来寻找存储的值(value),而HashSet借助HashMap存储的值都是PRESENT对象,如果通过HashMap的get方法实现,那么就失去了 HashMap通过 key 来寻找value的意义

HashSet在存入对象时为什么要重写 hashCode方法和equals方法

重写 hashCode方法和equals方法

  1. 在HashSet添加对象的时候,会先判断hashCode是否一致,如果一致则再调用equals方法,判断内容是否一致。
  2. 如果自定义对象未重写 hashCode方法,则会使用父类Object的hashCode,Object的hashCode每次new一个对象就会有不同的hash值,如果不重写,即使相同的对象也能被添加。
  3. 即使重写了hashCode方法,当存储对象时也会有几率返回相同的hash值,也就是哈希碰撞,此时就需要通过equals方法来判断对象内容是否相同,相同:则不添加;不相同:则添加到相同索引下的链表。

为何使用红黑树?为何不直接用红黑树?

  1. 防止链表长度超长时影响性能,所以使用红黑树。
  2. 树化是一种偶然情况,是用来防止攻击的。正常情况下在负载因子为0.75.链表长度为8出现的概率是极低的。
  3. 链表长度设置为8,就是为了降低树化的机率。
  4. 链表的查询效率为O(1),红黑树的查询效率为O(log2 N),而且红黑树的TreeNode比链表Node更占空间。

链表何时会树化,红黑树何时会退化成链表?

  1. 链表长度超过阈值(8)且数组长度大于64,满足以上链表会进化成红黑树。
  2. 数组扩容时拆分红黑树的元素个数小于等于6,则会退化成链表。
  3. 删除树节点时,若root 、root.left、root.right、root.let.let有一个为null也会退化成链表。

多线程下对Map进行put造成数据错乱?

在这里插入图片描述
Map1.7 扩容为什么会造成死链

因为JDK1.7 Map使用头插法,在多线程下扩容时容易造成死链。例:链表中有a,b两个元素,其中a的下一个元素是b,当线程T1,T2同时对数组进行扩容时,假设T2先执行,因为头插法扩容后的顺序为b,a,此时b的下一个元素时a。由于扩容不会对元素进行更改,此时b指向a,同时a又指向b,当T1线程对数组进行扩容时就会造成死链。

在这里插入图片描述
在这里插入图片描述

在这里插入图片描述

2. TreeSet

1. TreeSet的底层实现
  1. TreeSet底层是基于TreeMap来实现的,所以底层结构也是红黑树,不需要重写hashCode()和equals()方法,
  2. TreeSet 中的集合元素实际上由 TreeMap 的 key 来保存,而 TreeMap 的 value 则存储了一个 PRESENT,此对象是一个静态的 Object 对象,被static final修饰;

add

    public boolean add(E e) {
        //调用TreeMap的put方法    下面的源码请到TreeMap观看
        return m.put(e, PRESENT)==null;
    }
  1. 当往TreeSet里面添加元素的时候,实际上是调用的TreeMap的put()方法,会先判断根节点是否为空;
  2. 为空:则创建根节点,将元素存入根节点;
  3. 不为空:根节点为初始节点进行检索;
  4. 循环使用比较器对比key,如果没有传入的比较器则使用默认比较器,对比当前节点的key和新增节点的key,直到检索出合适的节点为止;
    a. 比较器返回=0,则两个key值相等,则新值覆盖旧值,并返回新值;
    b. 比较器返回>0,则新增节点值较大,则以当前节点的右子节点作为新的当前节点;
    c. 比较器返回<0,则新增节点值较小,则以当前节点的左子节点作为新的当前节点;
  5. 将新增节点与找到的节点进行比对,如果新增节点较大,则添加为右子节点,反之添加为左子节点。
  6. 调用 fixAfterInsertion(e)方法,通过变色,左旋右旋,修复红黑树,维持平衡,且TreeMap数量+1;

remove

    public boolean remove(Object o) {
       //调用TreeMap的remove方法    下面的源码请到TreeMap观看
        return m.remove(o)==PRESENT;
    }
  1. 当往TreeSet里面删除元素的时候,实际上是调用的TreeMap的remove()方法,当往TreeMap里面删除元素的时候,会先获取要删除的节点;
  2. 判断比较器是否为空,为空则用默认的比较器,循环使用比较器对比key,对比当前节点的key和获取节点的key,直到检索出合适的节点为止;
    a. 比较器返回=0,则两个key值相等,返回当前节点;
    b. 比较器返回>0,则值对于当前节点较大,返回节点右边的元素;
    c. 比较器返回<0,则值对于当前节点较小,返回节点左边的元素;
  3. 判断获取的节点是否为null?是:直接返回null;
  4. 不是节点,调用deleteEntry§方法,则根据红黑树的方法进行删除;
  5. 调用fixAfterDeletion(replacement)方法,通过变色,左旋右旋,修复红黑树,维持平衡,且TreeMap数量-1;
2. TreeSet的特点
  1. TreeSet保证元素的唯一性,且元素有序,不支持储存null元素,因为TreeMap的key是有序的;
  2. TreeSet中的元素支持2种排序方式:自然排序 或者 根据创建TreeSet 时提供的 Comparator 进行排序。这取决于使用的构造方法。
  3. TreeSet线程是不同步的,多个线程不能共享数据;

二、Map集合图

在这里插入图片描述

1. HashMap

1. HashMap的底层实现

  1. HashMap是基于哈希表(数组+链表)来实现的,JDK1.8引入了红黑树;
  2. HashMap存储方式是Key-Value的,JDK1.7底层存储数据的数组使用的是Entry[ ],JDK1.8加入了红黑树,所以改为Node[ ];
  3. Node节点是一个单向的链表结构,可以连接下一个Node节点,用来解决Hash冲突;
    在这里插入图片描述

PUT方法

    public V put(K key, V value) {
        return putVal(hash(key), key, value, false, true);
    }
    
    /**
     * 根据key口算hash值
     */
    static final int hash(Object key) {
        int h;
        return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
    }

	final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
	                   boolean evict) {
	        Node<K,V>[] tab; Node<K,V> p; int n, i;
	        //如果数组为null或数组长度为0 
	        if ((tab = table) == null || (n = tab.length) == 0)
	       		//则初始化一个数组
	            n = (tab = resize()).length;
	        //根据hash值和数组长度计算出索引位置且该位置为null
	        if ((p = tab[i = (n - 1) & hash]) == null)
	            //直接插入数据
	            tab[i] = newNode(hash, key, value, null);
	        else {
	            Node<K,V> e; K k;
	            //如果key的hash值相同且key也相同  说明存入重复
	            if (p.hash == hash &&
	                ((k = p.key) == key || (key != null && key.equals(k))))
	                //直接覆盖
	                e = p;
	            //判断该节点是否为树节点
	            else if (p instanceof TreeNode)
	            	//为树节点 直接存入红黑树
	                e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
	            else {
	               //循环链表
	                for (int binCount = 0; ; ++binCount) {
	                	//循环到链表最后一个元素,则表示插入元素与链表存在的元素没有相同的
	                    if ((e = p.next) == null) {
	                       // 插入元素生成新的Node并与上一个Node关联
	                        p.next = newNode(hash, key, value, null);
	                        //判断链表的长度是否大于8
	                        if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
	                            //树化 转为红黑树 
	                            //treeifyBin方法里判断数组的长度是否达到64,
	                            //如果没有达到进行扩容,不会树化,达到才会树化)  
	                            treeifyBin(tab, hash);
	                        break;
	                    }
	                    //遍历链表,对比链表上的key与插入的key是否一致且hash是否相等 
	                    if (e.hash == hash &&
	                        ((k = e.key) == key || (key != null && key.equals(k))))
	                        break;
	                    p = e;
	                }
	            }
	            if (e != null) { // existing mapping for key
	                V oldValue = e.value;
	                if (!onlyIfAbsent || oldValue == null)
	                    e.value = value;
	               //将元素添加到双向链表的尾部
	                afterNodeAccess(e);
	                return oldValue;
	            }
	        }
	        ++modCount;
	        //判断是否需要扩容
	        if (++size > threshold)
	            resize();
	        // 这他喵的是干嘛用的啊????   
	        afterNodeInsertion(evict);
	        return null;
	    }
    
  1. 当往HashMap里面添加元素的时候,会先判断数组是否为空;
  2. 数组为空则调用resize()方法,初始化一个长度16,扩容因子为0.75的数组,扩容为2的幂次方,计算key的hash值放入对应数组内;
  3. 数组不为空,计算key的hash值得到数组的下标,判断下标数组位置上是否存在数据,不存在则直接插入数据;
  4. 下标数组位置上存在数据,在链表上对比两个对象的Hash值 (Hash值相同不一定是同一个对象所以还要调用equals) 和KEY是否相同,是:则覆盖;
  5. 不是同一个对象,判读是否为红黑树的树节点(有可能已经转化成红黑树了),是:直接添加,
  6. 既不是相同对象,也不是树节点,则添加到链表的尾部,判断链表的长度是否大于8,并且判断数组的长度是否达到64;
  7. 如果链表长度为8,数组长度达到64,那么链表就会进化成红黑树。如果链表为8,数组长度小于64会进行resize()扩容成2倍桶数量,同时拆分链表至不同的桶,减少单个链表的大小。
  8. 最后判断hashmap的size是否达到阈值,是:进行扩容resize()。

GET方法

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


    final Node<K,V> getNode(int hash, Object key) {
        Node<K,V>[] tab; Node<K,V> first, e; int n; K k;
        //数组不为空且长度大于0且计算出的索引位置有值
        if ((tab = table) != null && (n = tab.length) > 0 &&
            (first = tab[(n - 1) & hash]) != null) {
            //与第一个first节点的hash值相等且key也一致
            if (first.hash == hash && // always check first node
                ((k = first.key) == key || (key != null && key.equals(k))))
                //返回first节点
                return first;
             //下一个节点不为null
            if ((e = first.next) != null) {
                //判断first节点是否属于树节点
                if (first instanceof TreeNode)
                    //直接从树节点中获取
                    return ((TreeNode<K,V>)first).getTreeNode(hash, key);
                do {
                    //循环遍历链表与链表的每一个元素进行Hash值和KEY的对比
                    if (e.hash == hash &&
                        ((k = e.key) == key || (key != null && key.equals(k))))
                        //相同则直接返回元素
                        return e;
                } while ((e = e.next) != null);
            }
        }
        return null;
    }

  1. 当调用get方法时,计算key的hash值得到数组的下标,判断下标数组位置上是否存在数据,不存在则返回null;
  2. 存在数据,则调用hashCode()和equals()与第一个元素进行Hash值和KEY的对比,如果相同则直接返回第一个元素;
  3. 如果不相同则判断是否属于树节点,是: 直接返回;
  4. 既不是第一个元素对象,也不是树节点,则调用hashCode()和equals()与链表的每一个元素进行Hash值和KEY的对比,如果相同则直接返回元素,如果不同返回为null;

REMOVE方法

    public V remove(Object key) {
        Node<K,V> e;
        return (e = removeNode(hash(key), key, null, false, true)) == null ?
            null : e.value;
    }
    
    
    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;
        //数组不为空且长度大于0且计算出的索引位置有值
        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;
             //与第一个节点的hash值相等且key也一致
            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 {
                      //循环遍历链表与链表的每一个元素进行Hash值和KEY的对比
                        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)
                    //从3树中删除
                    ((TreeNode<K,V>)node).removeTreeNode(this, tab, movable);
                //删除为头节点
                else if (node == p)
                    //由链表的下一个节点变为头节点
                    tab[index] = node.next;
                else
                	//被删除节点的上一个节点和下一个节点关联
                    p.next = node.next;
                ++modCount;
                --size;
                //被删除的节点 解除链表关联
                afterNodeRemoval(node);
                return node;
            }
        }
        return null;
    }

  1. 当调用remove方法时,计算key的hash值得到数组的下标,判断下标数组位置上是否存在数据,不存在则返回null;
  2. 存在数据,则调用hashCode()和equals()与第一个元素进行Hash值和KEY的对比,如果相同则直接删除第一个元素;
  3. 如果不相同则判断是否属于树节点,是: 直接删除,且会判断红黑树的节点数量,如果节点数量小于6个,则会退化成链表;
  4. 既不是第一个元素对象,也不是树节点,则调用hashCode()和equals()与链表的每一个元素进行Hash值和KEY的对比,如果相同则删除,如果不同返回为null;
  5. 删除后size-1(键-值映射的数量),链表重新关联,下一个Node替换当前被删除的Node;

RESIZE扩容方法

    final Node<K,V>[] resize() {
        Node<K,V>[] oldTab = 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;
            }
            //旧数组的长度通过左移运算扩大2倍,赋值给新数组的长度且小于最大默认值且旧数组的长度大于初始化的值
            else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
                     oldCap >= DEFAULT_INITIAL_CAPACITY)
                // 旧数组扩容阈值通过左移运算扩大2倍赋值给新数组扩容阈值
                newThr = oldThr << 1; // double threshold
        }
        else if (oldThr > 0) // initial capacity was placed in threshold 翻译:初始容量设为阈值
           //旧数组扩容阈值赋值给新数组的长度
            newCap = oldThr;
        else { // zero initial threshold signifies using defaults 翻译:初始阈值为零表示使用默认值
            newCap = DEFAULT_INITIAL_CAPACITY;
            newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
        }
        //新数组扩容阈值为0
        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;
        if (oldTab != null) {
            for (int j = 0; j < oldCap; ++j) {
                Node<K,V> e;
                //如果旧数组碎银位置元素部位null
                if ((e = oldTab[j]) != null) {
                    oldTab[j] = null;
                    //旧数组的下一个元素为null
                    if (e.next == null)
                       //当前位置只有一个数据,并不是链表或树,重新计算索引放入新数组
                        newTab[e.hash & (newCap - 1)] = e;
                    //旧数组的下一个元素不为null  判断是否为树节点
                    else if (e instanceof TreeNode)
                        //为树节点   进行树节点的操作
                        ((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
                    else { // preserve order
                        //当前索引位置为链表,将链表分为高低链表存储
                        Node<K,V> loHead = null, loTail = null;
                        Node<K,V> hiHead = null, hiTail = null;
                        Node<K,V> next;
                        do {
                            next = e.next;
                            //(e.hash & oldCap) == 0为低位链表
                            if ((e.hash & oldCap) == 0) {
                                if (loTail == null)
                                    loHead = e;
                                else
                                    loTail.next = e;
                                loTail = e;
                            }
                            else {
                                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;
    }
  1. 如果数组为空,则初始化默认长度16的数组,初始化阈值(扩容因子*数组长度);
  2. 如果数组不为空,且数组长度大于最大容量,则将数组的阈值设置为最大值(threshold = Integer.MAX_VALUE;) 返回数组;
  3. 如果数组不为空,则将数组的阈值*2设置给新数组阈值,进行数组扩容需要重新计算扩容后每个元素在数组中的位置;
  4. 判断原数组当前的索引是否有值,如果为空则继续下个索引的判断;
  5. 如果不为空,则对元素判断元素是否指向下一个链接,如果不是则放入数组中;
  6. 如果指向下一个链接,则判断元素是否为树节点,如果是,则进行树的操作;
  7. 如果不是树节点,则遍历链表, 如果((e.hash & oldCap) == 0) 则是低位链表,否则为高位链表;
  8. 并将元素放入链表,低位链表newTab[j] = loHead ,高位链表newTab[j+oldCap] = hiHead ;

2. HashMap的特点

  1. HashMap以接受null的键值(key)和值(value),但为null的key只能有一个,且元素是无序的;
  2. HashMap底层使用哈希表结合数组结构和链表结构的优点,增删改查都很快;
  3. HashMap是非同步(synchronized)的,线程不安全,多个线程是不能共享HashMap的;

3. 思考

HashMap 数组的长度为什么是2的幂次方?

  1. 扩容时重新计算索引效率更高:在进行扩容是会进行判断 hash值按位与运算旧数组长租是否 == 0,如果等于0,则把元素留在原来位置 ,否则新位置是等于旧位置的下标+旧数组长度
  2. 计算索引时效率更高,如果是 2 的 n 次幂可以使用位与运算代替取模

在取余操作中如果除数是2的幂次则等价于与(&)其除数减一的操作
hash&(length-1) = hash%length

HashMap高低位链表怎么来的?
数组扩容之后,需要重新计算索引,原链表的索引会重新计算到新数组。但是由于hashMap的数组长度是 2的n次方,每次扩容使数组长度 :newlength = 2* oldlength;并且计算索引方法是:hash & length ;;这些条件决定了 扩容后原数组中所有元素的下标只有2种值:不变,原下标 + oldlength ;
利用这种性质,可以将每个索引对应的链表分裂成2个链表也就是所谓的高低位链表;
例:

在取余操作中如果除数是2的幂次则等价于与(&)其除数减一的操作
hash&(length-1) = hash%length

原数组长度为16,假设添加元素的hash值为1689,根据hash & length(1689%16 或 1689 & 15)计算后等到索引为9;
先将数组扩容到32,原数组索引为9的元素,需要重新计算索引,根据hash & length(1689%32 或 1689 & 31)得到索引为25(原下标 + 原数组长度), 根据hash & old_length(1689 & 16 != 0)此时该元素会被分配到高位链表;
再次将数组扩容到64,原数组索引为25的元素,需要重新计算索引,根据hash & length(1689%64 或 1689 & 63)得到索引为25,索引没变,根据hash & old_length(1689 & 32==0)此时该元素就会被分配到低位链表;

为何使用红黑树?为何不直接用红黑树?

  1. 防止链表长度超长时影响性能,所以使用红黑树。
  2. 树化是一种偶然情况,是用来防止攻击的。正常情况下在负载因子为0.75.链表长度为8出现的概率是极低的。
  3. 链表长度设置为8,就是为了降低树化的机率。
  4. 链表的查询效率为O(1),红黑树的查询效率为O(log2 N),而且红黑树的TreeNode比链表Node更占空间。

链表何时会树化,红黑树何时会退化成链表?

  1. 链表长度超过阈值(8)且数组长度大于64,满足以上链表会进化成红黑树。
  2. 数组扩容时拆分红黑树的元素个数小于等于6,则会退化成链表。
  3. 删除树节点时,若root 、root.left、root.right、root.let.let有一个为null也会退化成链表。

多线程下对Map进行put造成数据错乱?

在这里插入图片描述

Map1.7 扩容为什么会造成死链
在jdk1.7的hashmap中在数组进行扩容的时候,因为链表是头插法,在进行数据迁移的过程中,有可能导致死循环,例:链表中有a,b两个元素,其中a的下一个元素是b,当线程T1,T2同时对数组进行扩容时,假设T2先执行,因为头插法扩容后的顺序为b,a,此时b的下一个元素时a。由于扩容不会对元素进行更改,此时b指向a,同时a又指向b,当由线程对链表进行访问时就会造成死链。
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

2. TreeMap

1. TreeMap的底层实现

TreeMap基于红黑树(Red-Black tree)实现,增删方法按照i红黑树来的;

PUT方法

public V put(K key, V value) {
        Entry<K,V> t = root;
        //根节点为null说明首次添加
        if (t == null) {
            compare(key, key); // type (and possibly null) check 翻译:类型(可能为null)检查
			//创捷根节点
            root = new Entry<>(key, value, null);
            //设置集合元素为1
            size = 1;
            modCount++;
            return null;
        }
        int cmp;
        Entry<K,V> parent;
        // split comparator and comparable paths
        //比较器
        Comparator<? super K> cpr = comparator;
        if (cpr != null) {
            do {
              //此处parent不是根节点, 是本次节点的父节点
                parent = t;
                //使用比较器比较key
                cmp = cpr.compare(key, t.key);
                //小于0
                if (cmp < 0)
                    //存在节点的左边
                    t = t.left;
                //大于0
                else if (cmp > 0)
                    //存在节点的右边
                    t = t.right;
                else
                   //当前节点  直接覆盖
                    return t.setValue(value);
            } while (t != null);
        }
        else {
           //key为null  抛出异常
            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);
        //根据比较器返回的值是否小于0,来判断父节点是左或右关联
        if (cmp < 0)
            parent.left = e;
        else
            parent.right = e;
        //插入后修复平衡红黑树
        fixAfterInsertion(e);
        size++;
        modCount++;
        return null;
    }
  1. 当往TreeMap里面添加元素的时候,会先判断根节点是否为空;
  2. 为空:则创建根节点,将元素存入根节点;
  3. 不为空:根节点为初始节点进行检索;
  4. 循环使用比较器对比key,如果没有传入的比较器则使用默认比较器,对比当前节点的key和新增节点的key,直到检索出合适的节点为止;
    a. 比较器返回=0,则两个key值相等,则新值覆盖旧值,并返回新值;
    b. 比较器返回>0,则新增节点值较大,则以当前节点的右子节点作为新的当前节点;
    c. 比较器返回<0,则新增节点值较小,则以当前节点的左子节点作为新的当前节点;
  5. 将新增节点与找到的节点进行比对,如果新增节点较大,则添加为右子节点,反之添加为左子节点。
  6. 调用 fixAfterInsertion(e)方法,通过变色,左旋右旋,修复红黑树,维持平衡,且TreeMap数量+1;

GET方法

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

	
    final Entry<K,V> getEntry(Object key) {
        // Offload comparator-based version for sake of performance
        if (comparator != null)
            return getEntryUsingComparator(key);
        //key为null  抛出异常
        if (key == null)
            throw new NullPointerException();
        @SuppressWarnings("unchecked")
            Comparable<? super K> k = (Comparable<? super K>) key;
        Entry<K,V> p = root;
        while (p != null) {
            //使用比较器比较key
            int cmp = k.compareTo(p.key);
             //小于0
            if (cmp < 0)
                //存在节点的左边
                p = p.left;
              //大于0
            else if (cmp > 0)
            	//存在节点的右边
                p = p.right;
            else
               //返回当前节点
                return p;
        }
        return null;
    }


    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;
                else
                    return p;
            }
        }
        return null;
    }

  1. 当往TreeMap里面获取元素的时候,会先判断比较器是否为空,为空则用默认的比较器;
  2. 循环使用比较器对比key,对比当前节点的key和获取节点的key,直到检索出合适的节点为止;
    a. 比较器返回=0,则两个key值相等,返回当前节点;
    b. 比较器返回>0,则值对于当前节点较大,返回节点右边的元素;
    c. 比较器返回<0,则值对于当前节点较小,返回节点左边的元素;

remove方法

建议先了解一下红黑树的添加与删除

    public V remove(Object key) {
        Entry<K,V> p = getEntry(key);
        if (p == null)
            return null;

        V oldValue = p.value;
        deleteEntry(p);
        return oldValue;
    }

	
    final Entry<K,V> getEntry(Object key) {
        // Offload comparator-based version for sake of performance
        if (comparator != null)
            return getEntryUsingComparator(key);
        //key为null  抛出异常
        if (key == null)
            throw new NullPointerException();
        @SuppressWarnings("unchecked")
            Comparable<? super K> k = (Comparable<? super K>) key;
        Entry<K,V> p = root;
        while (p != null) {
            //使用比较器比较key
            int cmp = k.compareTo(p.key);
             //小于0
            if (cmp < 0)
                //存在节点的左边
                p = p.left;
              //大于0
            else if (cmp > 0)
            	//存在节点的右边
                p = p.right;
            else
               //返回当前节点
                return p;
        }
        return null;
    }


	private void deleteEntry(Entry<K,V> p) {
	        modCount++;
	        size--;
	
	        // If strictly internal, copy successor's element to p and then make p
	        // point to successor.
	        if (p.left != null && p.right != null) {
	           // 获取后继结点
	            Entry<K,V> s = successor(p);
	            //将删除节点与后继节点的值对换
	            p.key = s.key;
	            p.value = s.value;
	            p = s;
	        } // p has 2 children
	
	        // Start fixup at replacement node, if it exists.翻译:如果替换节点存在,则在替换节点启动修复
	        Entry<K,V> replacement = (p.left != null ? p.left : p.right);
			//替换节点不为null
	        if (replacement != null) {
	            // Link replacement to parent
	            replacement.parent = p.parent;
	            if (p.parent == null)
	                root = replacement;
	            else if (p == p.parent.left)
	                p.parent.left  = replacement;
	            else
	                p.parent.right = replacement;
	
	            // Null out links so they are OK to use by fixAfterDeletion.
	            p.left = p.right = p.parent = null;
	
	            // Fix replacement
	            if (p.color == BLACK)
	                fixAfterDeletion(replacement);
	        } else if (p.parent == null) { // return if we are the only node.
	            root = null;
	        } else { //  No children. Use self as phantom replacement and unlink.
	            if (p.color == BLACK)
	                fixAfterDeletion(p);
	
	            if (p.parent != null) {
	                if (p == p.parent.left)
	                    p.parent.left = null;
	                else if (p == p.parent.right)
	                    p.parent.right = null;
	                p.parent = null;
	            }
	        }
	    }



    static <K,V> TreeMap.Entry<K,V> successor(Entry<K,V> t) {
        if (t == null)
            return null;
        else if (t.right != null) {
            //获取删除节点的右子节点
            Entry<K,V> p = t.right;
            //获取右子节点的最左边的节点
            while (p.left != null)
                p = p.left;
            return p;
        } else {
            Entry<K,V> p = t.parent;
            Entry<K,V> ch = t;
            while (p != null && ch == p.right) {
                ch = p;
                p = p.parent;
            }
            return p;
        }
    }
  1. 当往TreeMap里面删除元素的时候,会先获取要删除的节点;
  2. 判断比较器是否为空,为空则用默认的比较器,循环使用比较器对比key,对比当前节点的key和获取节点的key,直到检索出合适的节点为止;
    a. 比较器返回=0,则两个key值相等,返回当前节点;
    b. 比较器返回>0,则值对于当前节点较大,返回节点右边的元素;
    c. 比较器返回<0,则值对于当前节点较小,返回节点左边的元素;
  3. 判断获取的节点是否为null?是:直接返回null;
  4. 不是节点,调用deleteEntry§方法,则根据红黑树的方法进行删除;
  5. 调用fixAfterDeletion(replacement)方法,通过变色,左旋右旋,修复红黑树,维持平衡,且TreeMap数量-1;

2. TreeMap的特点

  1. TreeMap基于红黑树实现,增删改查的平均和最差时间复杂度均为O(logn),最大特点时Key有序。
  2. TreeMap指定Comparator比较器,key可以为null,如果使用默认比较器key不允许为null。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值