【学习笔记】Java核心技术——集合

其他知识点的相关链接:

【学习笔记】Java核心技术——集合

相关知识点

  1. 如何接口和实现分离?

  2. Collection接口的两个基本方法?

  3. Collection接口add方法的特点?

  4. Iterator的四个基本方法

  5. Iterator中next到达集合末尾会出现什么异常

  6. iterator调用next之前为什么要先进行hasNext()操作

  7. “for each”循环可以和什么对象一起工作

  8. foreachRemaining的用法?

  9. Iterator中的next和hasNext方法与什么接口的什么方法的作用是一样的

  10. Java迭代器认为iterator中的next应该在什么位置

  11. 为什么Collection的任何集合均能使用”for each"循环

  12. 元素被访问的顺序取决于什么

  13. Java集合类库中的迭代器和其他迭代器的区别

  14. 集合框架的接口图

  15. 集合中equals方法的作用

  16. 集合框架的类图

  17. ArrayList的特点

  18. LinkedList的特点

  19. 链表与泛型集合的区别?

  20. listIterator提供了哪两个新方法?

  21. remove操作和BACKSPACE的区别

  22. 如何避免并发修改异常

  23. 如何检测并发修改异常问题

  24. 在linkedlist集合中get()获取位置元素的微小变化

  25. 数组列表访问元素的方法

  26. 散列表的实现方法

  27. 如何用散列表查找表中对象的位置

  28. 如何进行再散列

  29. 什么是装填因子

  30. 树集的特点(TreeSet)

  31. 红黑树的特点

  32. 基本映射操作特点

  33. 映射的lambda用法

  34. 映射视图的种类

  35. 弱散列映射(WeakHashMap)的用途和工作原理

  36. LinkedHashSet和LinkedHashMap的特点

  37. 枚举集内部的实现方式

  38. 标识散列映射的用途

  39. 不可修改视图的方法

  40. 被定义了不可修改视图的集合是否不能再进行修改

  41. 视图机制可以用来干什么

  42. 受查视图的作用

  43. Hashtable和HashMap的区别

  44. 使用Enumeration接口对元素遍历的方法?

面试常见问题整理:

ArrayList与LinkedList异同

ArrayList与Vector区别

HashMap的底层实现

HashMap与Hashtable的区别

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

HashMap多线程操作导致死循环问题

HashSet和HashMap的区别

ConcurrentHashMap和Hashtable的区别

ConcurrentHashMap线程安全的具体实现方式/底层具体实现

集合框架底层数据结构总结:

List

Set

Map:

HashMap

LinkedHashMap

HashTable

TreeMap

 

知识点解答

1.如何接口和实现分离?

举例队列,队列接口如下定义

public interface Queue<E>{
    void add(E element);
    E remove();
    int size();
}

实现方式:一种使用循环数组;另一种使用链表

循环数组:

public class CircularArrayQueue<E> implements Queue<E>{
    private int head;
    private int tail;

    CircularArrayQueue(int capacity){...}
    public void add(E element){...}
    public E remove(){...}
    public int size(){...}
    private E[] elements;
}

链表方式:

public class LinkedListQueue<E> implements Queue<E>{
    private Link head;
    private Link tail;

    LinkedListQueue(){...}
    public void add(E element){...}
    public E remove(){...}
    public int size(){...}
}

2.Collection接口的两个基本方法?

public interface Collection<E>{
    boolean add(E element);
    Iterator<E> iterator();
}

3.Collection接口add方法的特点?

add方法用于向集合中添加元素,如果添加元素确实改变了集合就返回true,如果集合没有发生变化就返回false,集合中不允许有重复的对象

4.Iterator的四个基本方法

public interface Iterator<E>{
    E next();
    boolean hasNext();
    void remove();
    default void forEachRemaining(Consumer<? super E> action);
}

5.Iterator中next到达集合末尾会出现什么异常

NoSuchException

6.iterator调用next之前为什么要先进行hasNext()操作

先判断迭代器是否存在元素,否则容易造成异常

7.“for each”循环可以和什么对象一起工作

可以与任何实现Iterable接口的对象一起工作

8.forEachRemaining的用法?

forEacheRemaining方法提供了一个lambda表达式,将对迭代器的每一个元素调用这个lambda表达式,直到再没有元素为止

iterator.forEachRemaining(element-> do something with element);

9.Iterator中的next和hasNext方法与什么接口的什么方法的作用是一样的

与Enumeration接口中的nextElement方法和hasMoreElement方法

10.Java迭代器认为iterator中的next应该在什么位置

位于两个元素之间,当调用next时,迭代器就越过下一个元素,并返回刚刚越过的那个元素的引用

11.为什么Collection的任何集合均能使用”for each"循环

因为Collection接口扩展了Iterable接口。

12.元素被访问的顺序取决于什么

取决于集合实现方式

13.Java集合类库中的迭代器和其他迭代器的区别

其他迭代器根据数组索引建模,可以查看指定位置上的元素。

Java迭代器查找操作与位置变更紧密相连,查找一个元素的唯一方法就是调用next,而在执行查找操作的同事,迭代器的位置随之向前移动

14.集合框架的接口图

15.集合中equals方法的作用

只要两个集包含同样的元素就认为是相等的,而不要求这些元素有同样的顺序

16.集合框架的类图

17.ArrayList的特点

从数组中间位置删除一个元素要付出很大的代价,被删除元素之后的所有元素都要想数组的前端移动

18.LinkedList的特点

链表将每个对象存放在独立的结点中,每个节点还存放着序列中下一个结点的引用

所有链表实际上都是双向链接

19.链表与泛型集合的区别?

链表是一个有序集合,每个对象的位置十分重要。由于迭代器是描述集合中的位置,所以这种依赖于位置的add方法将由迭代器负责。只有对自然有序的集合使用迭代器添加元素才有实际意义。

20.listIterator提供了哪两个新方法?

E previous()
boolean hasPrevious()

21.remove操作和BACKSPACE的区别

  • 调用next之后,remove方法确实与BACKSPACE键一样删除了迭代器的左侧元素
  • 如果调用previous就会将右侧的元素删除掉
  • 不能连续调用两次next
  • add方法值依赖于迭代器的位置,remove方法依赖于迭代器的状态

22.如何避免并发修改异常

  • 可以根据需要给容器附加许多迭代器,但是这些迭代器只能读取列表
  • 再单独附加一个既能读又能写的迭代器

23.如何检测并发修改异常问题

  • 每个迭代器维护一个独立的计数值
  • 在每个迭代器方法的开始处检查自己改写操作的计数值是否与集合的改写操作计数值一致
  • 如果不一致,则抛出ConcurrentModificationException

24.在linkedlist集合中get()获取位置元素的微小变化?

当get()方法获取的索引打印size()/2,则从后往前进行遍历查找

25.数组列表访问元素的方法?

  1. 使用迭代器
  2. 用get和set方法随机访问每个元素(不适合链表)

26.散列表的实现方法

散列码:

  • 由对象的实例域产生的一个整数
  • 具有不同数据域的对象将产生不同的散列码

用链表数组实现

27.如何用散列表查找表中对象的位置

  1. 先计算它的散列码
  2. 将散列码与通道总数取余
  3. 得到的结果就是保存这个元素的桶的索引

28.如何进行再散列

线性探测,二次探查,双重探查

29.什么是装填因子

装填因子a,a=n/m 其中n 为关键字个数,m为表长

30.树集的特点(TreeSet)

  • 是一个有序集合,可以以任意顺序将元素插入到集合中
  • 对集合进行遍历是,每个值将自动地按照排序后的顺序呈现
  • 实现使用的是红黑树
  • 将一个元素添加到树中要比添加到散列表中慢
  • 要使用树集,必须能够比较元素。这些元素必须实现Comparable接口,或者构造集时必须提供一个Comparator

31.红黑树的特点

  1. 每个结点是红色,或是黑色
  2. 根节点是黑色
  3. 叶子结点是黑色
  4. 每一个红色结点,它的两个子结点都是黑色
  5. 对每个结点,从该结点到其他结点所有后代叶子结点的简单路径上,均包含相同数量的黑色结点(确保没有一条路径会比其他路径长出两倍,因而,红黑树是相对接近平衡的二叉树)

左旋转

思路:

情况1:如果y的左结点存在,则将y的左结点设为x的右结点

情况2:如果x的父结点不存在,则将y设为父结点

情况3:如果x是左结点,则将y设为x父结点的左结点;如果x是右结点,则将y设为x父结点的右结点

public void LEFT_ROTATE(TreeNode x){
    TreeNode y = x.getRight();
    x.setRight(y.getLeft());
    if(y.getLeft()!=null){
        y.getLeft().setParent(x);
    }
    y.setParent(x.getParent());
    if(x.getParent() == null){
    //根节点就是y
    }else{
        if(x == x.getParent().getLeft()){
            x.getParent().setLeft(y);
        }else{
            x.getParent().setRight(y);
        }
    }
    y.setLeft(x);
    x.setParent(y);
}

右旋转

思路大体如左旋转一致,具体实现如下:

public void RIGHT_ROTATE(TreeNode x){
    TreeNode y = x.getLeft();
    x.setLeft(y.getRight());
    if(y.getRight() != null){
        y.getRight().setParent(x);
    }
    y.setParent(x.getParent());
    if(x.getParent() == null){
        //y就是根结点
    }else{
        if( x == x.getParent().getLeft()){
            x.getParent().setRight(y);
        }else{
            x.getParent.setLeft(y);
        }
    }
    y.setRight(x);
    x.setParent(y);
}

插入

思路:将红黑树当做一棵二叉查找树,将结点插入,并把结点的颜色设为红色

修正树:

情况一:z的父结点的兄弟节点是红色,将z.p设为黑色,将z.p.p设为红色,z=z.p.p,递归上去

情况二:z的父结点的兄弟节点是黑色,

如果z是左结点

如果在是右结点

具体实现如下所示:

public void RB_INSERT(TreeNode root,int key){
    TreeNode z = new TreeNode(key);
    TreeNode y;
    while(root!=null){
        y = root;
        if(y.getKey() >= z.getKey()){
            root = root.getLeft();
        }else{
            root = root.getRight();
        }
    }
    z.setParent(y);
    if(y == null){
        root = z;
    }else if( y.getKey() >=z.getKey()){
        y.setLeft(z);
    }else{
        y.setRight(z);
    }
    z.setColor(RED);
    RB_INSERT_FIXUP(root,z);
}

public void RB_INSERT_FIXUP(TreeNode root,TreeNode z){
    while(z.getParent().getColor() == RED){
        TreeNode y;
        if(z.getParent() == z.getParent().getParent().getLeft()){
            y = z.getParent().getParent().getRight();
        }else{
            y = z.getParent().getParent().getLeft();
        }
        if(y.getColor() == RED){
            z.getParent().setColor(BLACK);
            z.getParent().getParent().setColor(RED);
            z = z.getParent().getParent();
        }else{
            if(z == z.getParent.getRight()){
                z = z.getParent();
                LEFT_ROTATE(z);
            }
            z.getParent().setColor(BLACK);
            z.getParent().getParent().setColor(Red);
            RIGHT_ROTATE(z.getParent.getParent());
        }
    }
}

删除

思路:

情况一:x的兄弟节点w是红色

将w设为黑色,x.parent设为红色,对x.p做一次左旋而不违反红黑树任何性质

情况二:x的兄弟节点w是黑色,且w的两个子结点为黑色

w.color = Red,x=x.p

情况三:x的兄弟节点w是黑色,且w的左孩子是红色,右孩子是黑色

w.left = Black;w.color = Red;RIGHT_ROTATE(w);

情况四:x的兄弟节点w是黑色,且w的左孩子是黑色,右孩子是红色

LEFT_ROTATE(x.p);将x设置为根

具体实现如下所示:

public void RB_DELETE_FIXUP(TreeNode root,TreeNode x){
    while(x != root && x.getColor() == RED){
        TreeNode w;
        //情况1
        if(x == x.getParent().getLeft()){
            w = x.getParent().getRight();
        }else{
            w = x.getParent().getLeft();
        }
        if(w.getColor() == RED){
            w.setColor(BLACK);
            x.getParent().setColor(RED);
            LEFT_ROTATE(x.getParent());
        }
        if(w.getLedt().getColor() == BLACK && w.getRight().getColor() == BLACK){
            w.setColor(RED);
            x = x.getParent();
        }else if(w.getRight().getColor() == BLACK){
            w.getLeft().setColor(BLACK);
            w.setColor(RED);
            LEFT_ROTATE(w);
            w = x.getParent().getRight();
        }
        w.setColor(x.getParent().getColor());
        x.getParent().setColor(RED);
        w.getRight().setColor(BLACK);
        LEFT_ROATE(x.getParent());
        x = root;
    }
    x.setColor(BLACK);
}

32.基本映射操作特点

键必须唯一的,不能对同一个键存放两个值

33.映射的lambda用法

scores.forEach((k,v)-> System.out.println("key="+k+",valueee="+v));

34.映射视图的种类

键集、值集合、键/值对集

35.弱散列映射(WeakHashMap)的用途和工作原理

  • 解决如果有一个值,对应的键已经不再使用的情况
  • 垃圾回收器跟踪活动的对象,只要映射对象是活动的,其中的所有桶也是活动的
  • 需要有程序负责从长期存活的映射表中删除那些无用的值

WeakHashMap工作原理

  • WeakHashMap使用弱引用保存键
  • WeakReference对象将引用保存到另外一个对象中,即散列键
  • 垃圾回收器发现某个特定的对象已经没有他人引用了,就将其回收
  • 如果某个对象只能有WeakReference引用,垃圾回收器仍然回收塔,但要将引用对象的弱引用放入队列中
  • WeakHashMap将周期性检查队列,以便找出新添加的弱引用
  • 一个弱引用进入队列意味着这个键不再被他人使用,并且已经被收集起来,于是,WeakHashMap将删除对应的条目

36.LinkedHashSet和LinkedHashMap的特点

  • LinkedHashSet和LinkedHashMap类用来记住插入元素项的顺序
  • 当条目插入到表中时,就会并入到双向链表中
  • 链接散列映射将用访问顺序,而不是插入顺序,对映射条目进行迭代
  • 每次调用get或put,受到影响的条目将从当前的位置删除,并放到条目链表的尾部

37.枚举集内部的实现方式

用位序列实现

EnumMap是一个键类型为枚举类型的映射,可以直接且高效地用一个值数组实现

38.标识散列映射(IdentityHashMap)的用途?

键的散列值不是用hashCode函数计算的,而是用System.identityHashCode方法计算的。这是Object.hashCode方法根据对象的内存地址来计算散列码是所使用的方式,在对两个对象进行比较时,IdentityHashMap类使用==,而不使用equals

39.不可修改视图的方法

Collections.unmodifiableCollection
Collections.unmodifiableList
Collections.unmodifiableSet
Collections.unmodifiableSortedSet
Collections.unmodifiableNavigableSet
Collections.unmodifiableMap
Collections.unmodifiableSortedMap
Collections.unmodifiableNavigableMap

40.被定义了不可修改视图的集合是否不能再进行修改

  • 不可修改视图并不是集合本身不可修改。仍然可以通过集合的原始引用对集合进行修改。并且仍然可以让集合的元素调用更改器方法。
  • 由于视图只是包装了接口而不是实际的集合对象,所有只能访问接口中定义的方法

41.视图机制可以用来干什么

可以用来确保常规集合的线程安全,而不是实现线程安全的集合类

42.受查视图的作用

“受查”视图用来对泛型类型发生问题是提供调试支持

受查视图受限于虚拟机可以运行的运行时检查

43.Hashtable和HashMap的区别

Hashtable类与HashMap类的作用一样,它们拥有相同的接口

Hashtable是同步的,如果需要并发访问,则要使用ConcurrentHashMap

44.使用Enumeration接口对元素遍历的方法?

Enumeration<Employee> e = staff.elements();
for(e.hasMoreElements(){
    Employee e = e.nextElement();
}

面试常见问题整理:

ArrayList与LinkedList异同

  1. 是否保证线程安全:ArrayList和LinkedList都是不同步的,也就是不保证线程安全
  2. 底层数据结构:ArrayList底层使用的是Object数组。LinkedList底层使用的是双向链表数据结构。
  3. 插入和删除是否受元素位置的影响:ArrayList采用数组存储,所有插入和删除元素的事件复杂度受元素位置的影响。LinkedList采用链表存储,所有插入,删除元素时间复杂度不受元素位置的影响,都是近似O(1)而数组进行O(n)
  4. 是否支持快速随机访问:LinkedList不支持高效的随机元素访问,而ArrayList支持。快速随机访问就是通过元素的序号快速获取元素对象的方法
  5. 内存空间占用:ArrayList的空间浪费主要体现在list列表的结尾会预留一定的容量空间,而LinkedList的空间花费则体现在他的每一个元素都需要消耗比ArrayList更多的空间

ArrayList与Vector区别

  • Vector类的所有方法都是同步的。可以由两个线程安全地访问一个Vector对象,但是一个线程访问Vector的代码要在同步操作上耗费大量的时间
  • ArrayList不是同步的,所以在不需要保证线程安全时检验使用ArrayList

HashMap的底层实现

 

HashMap与Hashtable的区别

  1. 线程是否安全:HashMap是非线程安全的,HashTable是线程安全的;HashTable内部的方法基本都经过synchronized修饰
  2. 效率:因为消除安全的问题,HashMap要比HashTable效率高一点。另外,HashTable基本被淘汰,不要在代码中使用它
  3. 对Null key和Null value的支持:HashMap中,null可以作为键,这样的键只有一个,可以有一个或多个键对于的值为Null。但是在 HashTable 中 put 进的键值只要有一个 null,直接抛出 NullPointerException
  4. 初始容量大小和每次扩充容量大小的不同:a创建时如果不指定容量初始值,Hashtable默认的初始大小为11,之后每次扩充,容量变为原来的2n+1。HashMap默认的初始化大小为16,之后每次扩充,容量编程原来的二倍。b创建时如果给定了容量初始值,那么Hashtable会直接使用给定的大小,而HashMap会将其扩充为2的幂次方大小。也就是说HashMap总是使用2的幂作为哈希表的大小
  5. 底层数据结构:JDK1.8 以后的 HashMap 在解决哈希冲突时有了较大的变化,当链表长度大于阈值(默认为 8)时,将链表转化为红黑树,以减少搜索时间。Hashtable 没有这样的机制。

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

为了能让 HashMap 存取高效,尽量较少碰撞,也就是要尽量把数据分配均匀。我们上面也讲到了过了,Hash 值的 范围值-2147483648到2147483647,前后加起来大概40亿的映射空间,只要哈希函数映射得比较均匀松散,一般应 用是很难出现碰撞的。但问题是一个40亿长度的数组,内存是放不下的。所以这个散列值是不能直接拿来用的。用之 前还要先做对数组的长度取模运算,得到的余数才能用来要存放的位置也就是对应的数组下标。这个数组下标的计算 方法是“ (n - 1) & hash ”。(n代表数组长度)。这也就解释了 HashMap 的长度为什么是2的幂次方。

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

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

HashMap多线程操作导致死循环问题

在多线程下,进行 put 操作会导致 HashMap 死循环,原因在于 HashMap 的扩容 resize()方法。由于扩容是新建一 个数组,复制原数据到数组。由于数组下标挂有链表,所以需要复制链表,但是多线程操作有可能导致环形链表。复 制链表过程如下:
以下模拟2个线程同时扩容。假设,当前 HashMap 的空间为2(临界值为1),hashcode 分别为 0 和 1,在散列地

址 0 处有元素 A 和 B,这时候要添加元素 C,C 经过 hash 运算,得到散列地址为 1,这时候由于超过了临界值,空 间不够,需要调用 resize 方法进行扩容,那么在多线程条件下,会出现条件竞争,模拟过程如下:

线程一:读取到当前的 HashMap 情况,在准备扩容时,线程二介入

线程二:读取 HashMap,进行扩容

线程一:继续执行

这个过程为,先将 A 复制到新的 hash 表中,然后接着复制 B 到链头(A 的前边:B.next=A),本来 B.next=null, 到此也就结束了(跟线程二一样的过程),但是,由于线程二扩容的原因,将 B.next=A,所以,这里继续复制A,让 A.next=B,由此,环形链表出现:B.next=A; A.next=B

HashSet和HashMap的区别

ConcurrentHashMap和Hashtable的区别

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

  • 底层数据结构: JDK1.7的 ConcurrentHashMap 底层采用分段的数组+链表 实现,JDK1.8 采用的数据结构跟 HashMap1.8的结构一样,数组+链表/红黑二叉树。Hashtable 和 JDK1.8 之前的 HashMap 的底层数据结构类似都是采用 数组+链表 的形式,数组是HashMap 的主体,链表则是主要为了解决哈希冲突而存在的;

  • 实现线程安全的方式(重要): a 在JDK1.7的时候,ConcurrentHashMap(分段锁) 对整个桶数组进行了分割分段(Segment),每一把锁只锁容器其中一部分数据,多线程访问容器里不同数据段的数据,就不会存在锁 竞争,提高并发访问率。 到了 JDK1.8 的时候已经摒弃了Segment的概念,而是直接用 Node 数组+链表+红黑 树的数据结构来实现,并发控制使用 synchronized 和 CAS 来操作。(JDK1.6以后 对 synchronized锁做了很 多优化) 整个看起来就像是优化过且线程安全的 HashMap,虽然在JDK1.8中还能看到 Segment 的数据结构, 但是已经简化了属性,只是为了兼容旧版本;b Hashtable(同一把锁) :使用 synchronized 来保证线程安全, 效率非常低下。当一个线程访问同步方法时,其他线程也访问同步方法,可能会进入阻塞或轮询状态,如使用 put 添加元素,另一个线程不能使用 put 添加元素,也不能使用 get,竞争会越来越激烈效率越低。

ConcurrentHashMap线程安全的具体实现方式/底层具体实现

JDK1.7

ConcurrentHashMap 是由 Segment 数组结构和 HashEntry 数组结构组成。

Segment 实现了 ReentrantLock,所以 Segment 是一种可重入锁,扮演锁的角色。HashEntry 用于存储键值对数据。

static class Segment<K,V> extends ReentrantLock implements Serializable {}

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

JDK1.8

ConcurrentHashMap取消了Segment分段锁,采用CAS和synchronized来保证并发安全。数据结构跟HashMap1.8

的结构类似,数组+链表/红黑二叉树。 synchronized只锁定当前链表或红黑二叉树的首节点,这样只要hash不冲突,就不会产生并发,效率又提升N倍。

集合框架底层数据结构总结:

List

  • Arraylist: Object数组
  • Vector: Object数组
  • LinkedList: 双向链表(JDK1.6之前为循环链表,JDK1.7取消了循环)

Set

  • HashSet(无序,唯一): 基于 HashMap 实现的,底层采用 HashMap 来保存元素

  • LinkedHashSet: LinkedHashSet 继承与 HashSet,并且其内部是通过 LinkedHashMap 来实现的。有点类 似于我们之前说的LinkedHashMap 其内部是基于 Hashmap 实现一样,不过还是有一点点区别的。

  • TreeSet(有序,唯一): 红黑树(自平衡的排序二叉树。)

Map:

HashMap

JDK1.8之前HashMap由数组+链表组成的,数组是HashMap的主体,链表则是主要为了解决哈希 冲突而存在的(“拉链法”解决冲突).JDK1.8以后在解决哈希冲突时有了较大的变化,当链表长度大于阈值(默 认为8)时,将链表转化为红黑树,以减少搜索时间

LinkedHashMap

LinkedHashMap 继承自 HashMap,所以它的底层仍然是基于拉链式散列结构即由数组和 链表或红黑树组成。另外,LinkedHashMap 在上面结构的基础上,增加了一条双向链表,使得上面的结构可以 保持键值对的插入顺序。同时通过对链表进行相应的操作,实现了访问顺序相关逻辑。

HashTable

数组+链表组成的,数组是 HashMap 的主体,链表则是主要为了解决哈希冲突而存在的

TreeMap

红黑树

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值