♥6000字介绍集合框架下的List,Set,Map以及支持类Collections【Java养成】

Java学习打卡:第三十天

Java养成计划(打卡第30天)


内容管理:最后一点点内容,介绍集合框架下面的几种数据结构
先来看一下总的集合框架,这是UML表示,Map是键值对,Collection是键值

是不是看着非常的直观,在数据结构中,Queue就是队列,Stack就是栈,LinkedList就是链表,ArrayList就是顺序表

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-N7ss5Avg-1634132990423)(C:\Users\OMEY-PC\AppData\Roaming\Typora\typora-user-images\image-20211012220211691.png)]

表(List)List< E>

表是一种有序集合,每一个元素都由一个位置下标。学过数据结构就直到除了普通的顺序表,还有LIFO,FIFO表,这些表结构接下来都会一一提到

表的特点: 表支持面向位置的操作,用户可以将元素插入指定的位置,向AbstractCollection就没有面向位置,用户也可以访问指定位置的元素。与集(set)不同,表允许出现重复的元素。表涉及的接口和类

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-CaQHgWnO-1634132990425)(C:\Users\OMEY-PC\AppData\Roaming\Typora\typora-user-images\image-20211012220633885.png)]

从这张Picture可以看到List继承了Collection,关联的迭代器是ListIterator【都是接口】,之后就是有相应的适配器抽象容器,抽象表,还有一个抽象顺序表AbstractSequentialList继承抽象表,其中链式表实现的就是该抽象顺序表,ArrayList和Vector继承的是普通抽象表,之后有Stack继承该Vector;这所有的表都有迭代器

List接口扩展Collection接口,是所有表的根接口

增加的方法主要是面向位置的一些方法,就可以在指定位置操作

void add(int index,E element); //指定位置插入元素
E get(int index); //指定位置的元素
int indexOf(Object o); //指定元素第一次出现的位置
ListIterator<E> listIterator(int startIndex); //返回当前表的表迭代器,参数指定为第一次调用时next方法的下标
List<E> subList(int fromIndex,int toIndex); //获取子表
····

对于equals方法具体就是对象地址相同,都是表,且表对应位置的元素相同

对于iterator除了返回普通的迭代器之外,还可以返回一个表迭代器,该迭代器支持双向遍历

ListIterator接口扩展Iterator接口

ListIterator< E> listIterator(int startIndex);//确定迭代器最开始游标的位置

//hasnext,next,remove
boolean hasPrevious(); //是否有上一个元素
void add(E,e); //向表中插入一个元素
void set(E e); //替换由next或者previous返回的元素【就是游标位置】add也是
int nextIndex(); //返回下一个元素的位置

AbstractList也是对与该接口的实现,其中也只是size和get方法没有实现,并且set,remove,add都只是简单抛出了例外。

可以扩展该类来实现随机存取的表类,那么还应该覆盖set,remove和add提供具体的实现

AbstractSequentialList是Abstract的抽象子类,适合当作那些元素存储便于顺序访问的表的超类,特点是将listIterator声明为抽象的,而get,set,add,remove则是实现了的,要定义适合顺序访问的表类,可以扩展该抽象类,提供size和listIterator的实现

数组表ArrayList

通过上面的表的接口可以看出ArrayList和LinkedList是实现List接口的具体两个实现类,在内部,数组表采用数组存储表的元素,而链式表则采用双向链表存储表的元素,如果需要使用下标随机访问元素,并且除了末尾之外,不在其他位置插入或者删除元素,应该考虑使用数组表。如果需要在任意位置插入元素,应该考虑链式表

一个绕的疑问: 既然ArrayList和LinkedList都是List的实现类,那么都可以实现插入操作,那为什么还要区分两者呢?

这就像普通接口的实现,虽然接口中的声明是一样的,但是实现的方式可能是不一样的,这里比如add(int index,E element); 就是在某一个位置插入元素,两种实现都可以实现,只是效率不一样。

就是数据结构中的顺序表和链表,它们都是可以实现在任意位置插入删除的;只是顺序表会耗时间一些,仅此而已,所以我们使用数据时就要考虑一下选用哪种数据结构

链式表占用的空间要大一些,存储效率低一些,除了数据之外,还有两个指针(引用),就是双链表的结构,实现方式就是之前的QueueNode那样子。

数组表的构造方法有几种

  • ArrayList(); //创造一个初始容量为10的空数组表
  • ArrayList(Collection<?extends E) c); //创建一个包含一个集合所有元素的数组表,比如之前的ls依据s构造
  • ArrarList(int initalCapacity); //创建一个指定容量的空数组表

【其实这就是顺序表和链表的区别,存储的结构不一样】只是java中封装了,都是数据结构中的基础知识

  • LinlkedList(); //构造一个空链式表
  • LinkedList(Collection<?extends E) c); //构造一个包含c中所有元素的链式表

参数化类型ArrayList< Integer>是List< Integer>的子类,由于泛型一旦指定就不能更改,所以List< X>中x就是Integer,不会是非Integer,所以强制类型转换不会报错,(类和类才存在继承和强制类型转换,而同一个泛型有类型擦除技术,是同样的.class文件,不存在继承之类的问题)

LinkedList也实现了Deque接口

所以LinkedList也提供了一些双向队列的操作(double ended queue)

public interface Deque<E>extends Queue<E>{
    int size();
    void addFirst();
    void addLast();
    E getFirst();
    E getLast();
    E removeFirst(); //删除并返回第一个元素
    E removeLast();
}

这里涉及的数据接口就是队列FIFO表,队尾插入,队首删除,所以调用addLast,和removeFirst就是入队和出队,具体的实现可以看源码,等之后讲解数据结构时,就会具体讲解。

向量Vector和栈Stack

Vector继承AbstractList,Stack又继承Vector ,需要注意的时Vector和Stack是同步的,【加上synchronized修饰】这里和之前生产者和消费者中的Queue一样,防止多线程引起的数据冲突(当时模拟过)

与ArrayList一样,Vector也采用数组存储元素向量的容量可以根据需要任意动态伸缩,且一定不会小于向量的大小构造方法和LinkedList一样是三个

  • Vector();
  • Vector(Collection<?extends E) c);
  • Vector(int initalCapacity);

栈Stack是对Vector的扩展,新增栈的操作方法

public class Stack<E> extends Vector<E>{
	public Stack(); //空栈
    public E push(E item); //栈顶压入元素
    public E pop(); //返回并返回栈顶
    public E peek(); //返回栈顶元素
    boolean isEmpty(); //栈空
    public int search(Object o); //返回指定元素在栈的位置,栈顶为1
}
数据结构Stack经典问题

括号匹配,给出一串表达式,如果不匹配就报错,匹配就不报错

package Luogu;

import java.util.*;

public class ContainerDemo {
	public static boolean match(Character b,Character a)//注意顺序
	{
		if(a == '('&&b == ')')
			return true;
		if(a == '['&&b == ']')
			return true;
		if(a == '{'&&b == '}')
			return true;
		return false;
		
	}
	public static void isMatch(String str)
	{//给的是字符串,但入栈的是字符,使用包裹体
		Stack<Character> stack = new Stack<Character>(); //空栈
		Character c;
		for(int i = 0;i < str.length();i++)//之前没有lenth方法,只能while,这里就直接用方法 lenth是长度,size是容量
		{
			c = str.charAt(i); //获取字符
			switch(c){
				case '(':
				case '[':
				case '{':
					stack.push(c); //入栈
					break;
				case ')':
				case ']':
				case '}':
					if(stack.isEmpty())
					{
						System.out.println("右括号多余");
					    return; //防止重复输出
					}
					if(match(c,stack.peek()))
					{
						stack.pop();//出栈
						break;
					}
					else {
						System.out.println("不匹配");
					    return; //防止重复输出
					}
			}
		}
		if(!stack.isEmpty())
			System.out.println("左括号多余");
		else 
			System.out.println("匹配呢");
	}
	
	public static void main(String[] args) {
		isMatch("{3+5}");
		isMatch("[(3+5)");
		isMatch("[3+5)");
		isMatch("[(3+5)]}");
  }
}//任何问题都是值得重视的
//这里match时注意顺序,还有就是方法体中,逻辑运算符的运用

输出结果为

匹配呢
左括号多余
不匹配
右括号多余

集(set) Set< E>

集就是数学中的集合,集合中不能包含相同元素,所以如果存储数据需要相同数据就只能使用表了,一个集中不能存在时式子el.equals(e2)成立的e1,e2,一个集中最多只能包含一个null元素,因为不重复呢

从上面的总图可以看出,集也是继承的Clloection

集所涉及的主要接口和类

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-sPSOtOvg-1634132990427)(file:///C:\Users\OMEY-PC\Documents\Tencent Files\1589827479\Image\C2C\1D642008C54073A404C1092EA64DC391.jpg)]

Set接口扩展Collection接口,是所有集的根接口,但是并没有扩展增加新的方法,但是对与一些方法做了新的约定

  • add(E e) 方法向集中添加指定元素,若集中已经包含该元素,则不做添加操作,直接返回false
  • equals(Object o) 测试当前集与参数对象o是否相等。若参与对象o也是一个集且其所有元素多存在于当前集、两个集的元素个数相等,则返回true
  • hasCode() 返回集的散列码,一个集的散列码等于所有元素的散列码之和。【这里可以去了解哈希表】

AbstractSet是一个扩展AbstractCollection类并实现Set接口的抽象类。但是**除了对从AbstractCollection类继承而来的equals方法和hashCode方法进行覆盖、提供特定的实现之外,该抽象类没有做更多的工作

散列集和链式散列集

Hash(散列,哈希),HashSet类扩展AbstractSet抽象类,是一个基于哈希表技术实现的Set接口的具体类,散列集的元素是没有顺序的,不能保证其迭代器按照某种次序返回元素,散列集包含null元素

散列集的构造方法为

  • HashSet(); //创建一个空散列集,初始容量为16,装填因子为0.75
  • HahSet(int initalCapacity); //创建一个由指定初始容量的空散列集,装填因子为0.75
  • HashSet(int initalCapacity, floatFactor);// 创建一个指定初始容量和指定装填因子的空散列集
  • HashSet(Collection<?extends E> c); //创建一个指定集合元素的散列集,状态因子为0.75

散列集的迭代器遍历元素的效率与散列集的容量成正比,因此不应该设置太大的初始容量(或太小的装填因子)

链式散列集LinkedList类扩展HashSet类,是一个基于散列表技术和链表技术实现的Set接口的具体类,链式散列集是有顺序的,其迭代器可以按元素插入集合的顺序返回各元素。链式散列集允许包含null元素

链式散列集的构造方法为

  • LinkedHashSet(); //创建一个空链式散列集,初始容量为16,装填因子为0.75
  • LinkedHahSet(int initalCapacity); //创建一个由指定初始容量的空链式散列集,装填因子为0.75
  • LinkedHashSet(int initalCapacity, floatFactor);// 创建一个指定初始容量和指定装填因子的空链式散列集
  • LinkedHashSet(Collection<?extends E> c); //创建一个指定集合元素的链式散列集,状态因子为0.75

关于集合的底层实现到数据结构就会再详细讲解

这里可以先大概回答一下问题

哈希表(散列表)的存储时依靠数组实现的,那既然数组已经够方便了,那为什么还需要哈希表?

那现在比如给一个数组,那要再数组中查询某个数,那就需要使用循环遍历,这样的话效率就比较低了,那么如何降低这时间复杂度呢?比如我要查询11,那我就直到它在那个位置,然后看是否在那个位置

hash函数就建立了数据与存储位置之间的关系,方便存取和插入

  • 装载因子 == 数据个数/数组容量 ;容量越大,迭代器效率低,但是装载因子过大,又会增大hash冲突的概率

这里就以一个例题来看一下Set的操作

使用散列集或者链式散列集,在实验类中定义两个类犯法,方法addElements使用Scanner从文本中抽取单词添加到指定的集,方法print利用print利用迭代器遍历输出指定集的各元素

package Luogu;

import java.util.*;

public class ContainerDemo {
	private static String text = "Do sit down.Do study hard!"+"Let me  try again.Let Tom go there themself";
    public static void addElements(Set<String> set)
    {
    	Scanner scanner = new Scanner(text);//从String中获取数据
    	scanner.useDelimiter("[\\s.! ]\\s*");//将text从!. 空白分隔
    	while(scanner.hasNext())//是否还有token
    	{//加入集合
    		set.add(scanner.next());//和迭代器一样也是光标
    	}
    }
  //利用迭代器遍历并输出
	public static void print(Set<String> set)
	{
		Iterator<String> iterator = set.iterator();
		while(iterator.hasNext())
		{
			System.out.print(iterator.next()+" ");
		}
		System.out.println();
	}
	//操作
	public static void main(String[] args) {
		Set<String> hashset = new HashSet<String>();//上转型
		addElements(hashset);
		print(hashset);//元素只允许出现一次
		Set<String> linkedset = new LinkedHashSet<>();
		addElements(linkedset);
		print(linkedset);//按照添加的顺序添加
		
	}
}

这里的输出结果为

study again go Do down Tom there themself me Let try hard sit
Do sit down study hard Let me try again Tom go there themself

链式hash集是是按照插入顺序输出的,而普通的就是无序的,但是这里两者都是会自动删除重复的元素,就是add的时候执行的

树型集treeSet

这个类继承AbstractSet的扩展,同时实现了SortedSet接口,加入的元素必须是可以相互比较的,树型集根据比较结果维护其元素的顺序,树型集的迭代器按照升序遍历遍历元素

树型集之间的比较以及排序方式有两种

  • 自然顺序:添加到树型集中的元素必须实现Comparable接口,提供compareTo方法,元素之间就是利用该方法排序和比较,很多ApI比如String类和基本类型类都实现了该接口
  • 比较器顺序:给树型集指定一个实现Comparator接口的比较器,利用比较器对象的相关方法完成排序和比较,加入树型集的元素必须可利用该比较器进行比较

Comparator接口中声明了两个方法

public interface Comaparator<T>{
    int compare(T o1,T o2); //比较两个参数对象,返回负整数,零或正整数
    boolean equals(Object obj); //比较参数o是否和当前比较器相等
}

这里具体使用那种构造方法与与构造方法有关

  • TreeSet(); //创建一个空树型集,采用自然顺序排序
  • TreeSet(Comparator<? super E) comparator);//创建一个空属性集,采用指定的比较器排序
  • TreeSet(Collection<?extends E) c); //创建一个包含指定集合元素的树型集,采用自然顺序排序
  • TreeSet(SortedSet< E> s); //创建一个树型集,包含与参数集相同的元素,采用与参数集相同的排序方式

与散列集相比,树型集是可比较和有序的,除了Set的基本方法,还有一些与元素次序有关的方法

Comparator<?super E> comparator();//返回集的比较器,若是自然顺序,则为null
E first(); // 集的第一个元素
E last(); //返回最后一个元素
SortedSet<E>head[tail][sub]Set(E fromElement); //返回一个视图

目前来看,树型集相比散列集就是一个可比较,一个不可比较,如果要求操作次序相关或者需要比较,就采用树型集

下面来举一个例子

使用树型集,首先定义一个比较器类,用来比较两个字符串的大小,比较时忽略大小写,应用程序先后采用自然顺序和比较器顺序创建两个树型集,添加和删除元素使用上一个例子的方法

package Luogu;import java.util.*;public class ContainerDemo implements Comparator<String>{//要使用比较器顺序,那就要实现Comparator类	private static String text = "Do sit down.Do study hard!"+"Let me  try again.Let Tom go there themself";    //覆写比较方法	@Override	public int compare(String o1, String o2) {		return o1.compareToIgnoreCase(o2);//忽略大小写的比较	}	//添加元素	public static void addElements(Set<String> set)    {    	Scanner scanner = new Scanner(text);//从String中获取数据    	scanner.useDelimiter("[\\s.! ]\\s*");//将text从!. 空白分隔    	while(scanner.hasNext())//是否还有token    	{//加入集合    		set.add(scanner.next());//和迭代器一样也是光标    	}    }  //利用迭代器遍历并输出	public static void print(Set<String> set)	{		Iterator<String> iterator = set.iterator();		while(iterator.hasNext())		{			System.out.print(iterator.next()+" ");		}		System.out.println();	}	//操作	public static void main(String[] args) {//		Set<String> hashset = new HashSet<String>();//上转型//		addElements(hashset);//		print(hashset);//元素只允许出现一次//		Set<String> linkedset = new LinkedHashSet<Stirng>();//		addElements(linkedset);//		print(linkedset);//按照添加的顺序添加		Set<String> treeset = new TreeSet<String>(); //自然顺序		addElements(treeset);		print(treeset);		Set<String> comset = new TreeSet<String>(new ContainerDemo());		addElements(comset);		print(comset);			}}//这里为了方便就直接继承了比较器了

输出的结果为

Do Let Tom again down go hard me sit study themself there try
again Do down go hard Let me sit study themself there Tom try

前面的是自然排序,实际上使用的是元素本身的compareTo来比较的,Unicode码;后面使用的是比较器顺序,这里的比较器是要求忽略大小写,所以两者的结果不一样

图(Map)

映射是一种键-值对集合,其中键和值都可以是任意类型的对象,映射不能包含重复的键,每一个键最多只能对应一个值

与表和集将Collection作为根接口不同,映射以Map为根接口,映射涉及的接口和类为

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-GCwOhhiY-1634132990429)(file:///C:\Users\OMEY-PC\Documents\Tencent Files\1589827479\Image\C2C\73A24B4150C6A0D6E78FCD511EA9B760.jpg)]

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-bqrdvABh-1634132990431)(https://www.freesion.com/images/840/730d25e60a75d5c23c4fdb09665668e8.JPEG)]

Map接口和Collection接口是不一样的,因为所传入的类型数量是不一样的

public interface Map<K,V>{    void clear(); //删除所有的键--值对    V get(Object key); //返回指定键对应的值 (由键获值)    V put(K key,V value); //添加一个键-值对    void putAll(Map<?extends K,?extends V> m); //将映射m的所有键--值对添加到当前映射    V remove(Object k); //删除指定键对应的值    int size(); //返回键值对的数目    boolean isEmpty(); //当前映射是否为空    boolean containsKey(Object key); //测试是狗包含具有指定键的键值对    boolean containsValue(Object value); //测试是否包含指定值的键值对    boolean equals(Object o); //测试两个映射是否相等    Set<Map.Entry<K,V>> entrySet(); //返回当前映射的一个集视图【不重复】    Set<K> keySet(); //返回当前映射的一个集视图 【键是不重复的,为Set】    Collection<V> values(); //返回当前值的一个集合视图 【值可能重复,为Collection】}

与集不能有重复的元素一样,映射要求不能有重复的键,相比表可以通过下标访问元素,映射可以通过键访问相应的值(这在数据结果的内部排序中的key是相同的意思),与集和表使用add或者addAll方法添加元素不同,映射通过put方法与putAll方法添加元素

这里添加时,如果与原键值对具有相同的键,那么就用新值替代原值并返回原址,否则添加后返回Null。putAll就是调用的put方法来解决

表和集都可以使用迭代器遍历访问所有元素,映射没有迭代器,但映射可以返回视图返回一个集合视图,从而间接访问。keySet方法返回所有键值的集视图,values方法返回所有值的集合视图,entry方法返回映射中所有键值对的集视图,其中键值对的表示为Map.Entrydui想,Entry是嵌套在Map中的接口,声明的方法有

K getKey(); //返回键值对的键V getValue(); //返回键值对的值V setValule(V value); //设置键值对的值

AbstractMap是Map的一个抽象类,除了entrySet方法,它实现了接口中的其他方法,可扩展该类来定义具体的映射类,如果定义的映射是不可更改的,那就要覆盖put,提供具体的实现,entrySet().iterator()返回的迭代器必须有remove方法

映射的实现类

和Set类似,也有Hash,LinkedHash,Tree三种具体实现类

构造方法

  • HashMap();
  • HahMap(int initialCapacity);
  • HashMap(int initialCapacity,floatFactor); //都是和之前相同的意义,只是变成了映射
  • HashMap(Map<?extends K,? extends V> m);

与散列集一样,散列映射基于散列表技术,键值对没有顺序

链式散列映射的键值对是有序的,其顺序有两种模式:插入顺序 【之前那种】 访问顺序【按照元素最后一次被访问的时间,由早到晚排序】,选择那种排序方式取决于构造方法

  • 前四个和上面一样,都是插入排序
  • LinkedHashMap(int initialCapacity,floatFactor,boolean accessOrder);//true就是访问顺序

与树型集一样,树型映射是按照键值排序的,排序顺序还是自然顺序和比较器顺序,树型映射的构造方法和树型集差不多

  • TreeMap();
  • TreeMap(Comparator<? super E) comparator);
  • TreeMap(Map<? extends L.? extends V> m);
  • TreeMap(SortedMap<K,? extends V> m);

树型映射不仅实现了Map接口,同时也实现了SortedMap接口,声明的也是与次序相关的方法,与树型集是类似的,这里就不再赘述了

所以说,散列,链式散列,树,就是三种不同的结构,散列无需,后两者有序,链式散列是插入顺序,再Map中还可能使访问顺序,树就使自然顺序或者比较器顺序

支持类Collections

这个类和Collection有什么渊源呢,Collections是集合框架的支持类,提供了一组静态方法,便于programmer对表和集和映射进行更为有效的管理

frequency //对象在集合中出现的次数max //最大元素集合中min //最小元素集合中repalceAll  //用新值替换表中所有的旧值shuffle  //随机打乱表中的顺序binarySearch  //使用二分查找法在排好序(自然顺序)的表中查找对象,返回对象的下标sort //使用自然顺序排序表中元素synchronizedList //包装指定表成为同步表synochronizedSet //包装指定集synchronizedMap //包装指定映射synchronizedSortedMap //包装指定有序映射unmodifiableList //包装指定表为只读表(不可修改) ····Set Map SortedMap

树型集和树型映射都支持对元素进行排序,但是表没有相应的实现类,Collections中的sort方法就提供了对表的排序,二分查找可以在表中查找元素。若找到就返回元素下标,没有找到就返回 【-<插入点> -1】

Collection和Map接口的方法都没有要求保障线程安全,所以一般的对象都不是线程安全的,多线程访问时iju可能引起数据混乱,Collections中的synchronizedSet之类的方法就可以包装这些对象(形成同步版本),但是需要注意,如果利用迭代器访问元素,还是要自己完成线程同步

List list = Collections.synchronizedList(new ArrayLiast);//这就是装饰器,就像IO中的过滤流一样,最里面一定使原始的synchronized(list){    Iterator i = list.iterator();    while(i.hasnext())    {//迭代器访问要自己加同步锁,因为只是list为同步的,就是list中的方法        process(i.next);}}

另外还有unmodified方法可以包装成只读版本

下面来演示一下Collections的使用

package Luogu;import java.util.*;public class ContainerDemo{	public static void main(String[] args) {		List<Integer> list = new LinkedList<Integer>();//要随机插入,所以使用链式表		//产生10个0-100之间的随机数插入链式表中		for(int i = 0;i <10;i++)		{			list.add((int)(Math.random()*100));//这里int也可以,不能转化为包装类型		}		System.out.println("未排序的 "+list);		Collections.sort(list); //调用类方法进行排序		System.out.println("排序之后 "+ list);		//无论表中是否有10,都在相应位插入10		int find = Collections.binarySearch(list, 10);//使用二分查找		find = find >= 0?find:-(find+1); //使用三目符,如果找到就		list.add(find, 10);		System.out.println("插入后"+list);		//包装为只读类型		list = Collections.unmodifiableList(list);//注意原来的表没有被覆盖,和String一样,要指针赋值		try {			list.add(10); //自己加例外		}catch(Exception e) {			e.printStackTrace();		}	}}

这里的一种输出结果为

未排序的 [20, 90, 16, 39, 93, 51, 76, 46, 25, 70]
排序之后 [16, 20, 25, 39, 46, 51, 70, 76, 90, 93]
插入后[10, 16, 20, 25, 39, 46, 51, 70, 76, 90, 93]
java.lang.UnsupportedOperationException

Collections类是一个非常适用的类,可以方便对表list进行排序

对于集合的简单介绍就到此结束了,SE也就这样子了,接下来会分享项目的具体实现~

  • 5
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值