【codejava】第八版:第十三章 集合[002] [20180104]

本文深入探讨Java集合框架的核心组件,包括List、Set、Map等主要接口的实现与应用场景,如ArrayList、LinkedList、HashSet、TreeSet、HashMap和TreeMap等。通过实例讲解各集合的特点与使用方法,帮助读者掌握高效编程技巧。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

表13-1 Java库中的具体集合
——————————————————————————————————————————————————
        集合类型                                                                                                        描述
——————————————————————————————————————————————————
        ArrayList                                                                           一种可以动态增加或者缩短的索引序列
        LinkedList                                                                        一种可以再任何位置进行高效地插入和删除操作的有序序列
        ArrayDeque                                                                     一种用循环数组实现的双端队列
        HashSet                                                                            一种没有重复元素的无序集合
        TreeSet                                                                            一种有序集
        EnumSet                                                                          一种包含枚举类型值得集
        LinkedHashSet                                                                一种可以记住元素插入次序的集
        PriotityQueue                                                                 一种允许高效删除最小元素的集合
        HashMap                                                                        一种存储键/值关联的数据结构
        TreeMap                                                                          一种键值有序裴烈的映射表
        EnumMap                                                                        一种键值属于枚举类型的映射表
        LinkedHashMap                                                              一种可以记住键值添加次序的映射表
        WeakHashMap                                                                一种其值无用武之地后可以被垃圾回收器回收的映射表
        IdentityHashMap                                                            一种用==,而不是equals比较键值的映射表                                        
——————————————————————————————————————————————————

13.2.1 链表
    本书中,有很多示例已经使用了数组以及动态的ArrayList类,然而,数组和数组列表都有一个重要的缺陷,这就是从数组的中间位置删除一个元素要付出很大的代价,其原因是数组中处于被删除元素后的所有元素都要向数组的前端移动,在数组中间插入一个元素也是如此。链表很好的解决了这个问题,尽管数组在连续的存储位置上存放对象引用,但链表却将每个对象存放在独立的结点中,每个结点还存放着序列中下一个结点的引用。在Java程序设计语言中,所有链表实际上都是双向链接的(double linked)——即每个结点还存着指向前驱结点的引用。
    从链表中删除一个元素是很轻松得操作,只需要对被删除元素附近的结点更新一下即可。但是链表和泛型集合之间有一个重要的区别,链表是个有序集合(ordered collection)。每个对象的位置十分重要。LinkedList.add方法将对象添加到链表的尾部,但是常常需要将元素添加到链表的中间,由于迭代器是描述集合中位置的,所以这种依赖于位置的add方法将由迭代器负责。只有对自然有序集合使用迭代器添加元素才更有实际意义。例如Set类型,其中的集合完全无序,因此在Iterator接口中就没有add方法,相反的集合类库提供了子接口ListIterator,其中包含add方法:
public interface ListIterator<E> extends Iterator<E> {
    void add(E element);
    .......
}
与Collection.add不同,这个方法不返回boolean类型的值,他假定添加操作总会改变链表,另外ListIterator接口有两个方法,可以用来反向遍历链表.
 boolean hasPrevious();
 E previous();
与next反之,previous方法返回越过的对象.
LinkedList类的listIterator方法返回一个实现了ListIterator接口的迭代器对象。
    
    当用一个刚刚由Iterator方法返回,并且指向链表表头的迭代器调用add操作时,新添加的元素将变成列表的新表头。当迭代器越过链表的最后一个元素时(即hasNext返回false),添加的元素将变成列表的新表尾。如果链表有n个元素,那么就有n+1个位置可以插入新元素。这些位置与迭代器的n+1个可能的位置相对应。例如链表中包含3个元素,A,B,C,就有4个位置(标记为|)可以插入新元素
|ABC        A|BC        AB|C        ABC|

    √注释:在用“光标”类比时要格外小心,remove操作与BACKSPACE键的工作方式不太一样,在调用next之后,remove方法确实与BASESPACE键一样删除了迭代器左侧的元素,但是调用previous就会将光标右侧的元素删除,并且不能再同一行中调用两次remove。
    add方法只依赖于迭代器的位置,而remove方法依赖于迭代器的状态(理解这句话的意思)。

    在Java类库中,还提供了许多在理论上存在一定争议的方法。链表不支持快速地随机访问。如果要查看链表中的第n个元素,就必须从头开始,越过n-1个元素。尽管如此,LinkedList类还是提供了一个用来访问某个特定元素的get方法。get方法做了微小的优化:如果索引大于size()/2就从列表尾端开始搜索元素.
 /**
     * Returns the (non-null) Node at the specified element 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;
        }
    }




    List<String> linkedList = new LinkedList<String>();
    String index = linkedList.get(3);//这个方法效率不高,不建议使用
    没有捷径可走,鉴于这个元素,在程序需要采用整数索引访问元素时,程序员通常不选用链表。我们建议避免使用以整数索引表示链表中位置的所有方法,如果需要对集合进行随机访问,就使用数组或ArrayList,而不要使用链表.
例13-1  使用的就是链表,他简单的创建了两个链表,将它们合并在一起,然后从第二个链表中间每隔一个元素删除一个元素。
public class LinkedListTest {
    public static void main(String[] args) {
        List<String> a = new LinkedList<String>();
        a.add("Amy");
        a.add("Carl");
        a.add("Erica");
        
        List<String> b = new LinkedList<String>();
        b.add("Bob");
        b.add("Doug");
        b.add("Frances");
        b.add("Gloria");
        
        ListIterator<String> aIter = a.listIterator();
        Iterator<String> bIter = b.iterator();
        
        while(bIter.hasNext()){
            if(aIter.hasNext()){
                aIter.next();
            }
            aIter.add(bIter.next());
        }
        System.out.println(a);
        
        bIter = b.iterator();
        while(bIter.hasNext()){
            bIter.next();
            if(bIter.hasNext()){
                bIter.next();
                bIter.remove();
            }
        }
        System.out.println(b);
        
        a.removeAll(b);
        System.out.println(a);
    }
}

13.2.2 数组列表
    上一节中,介绍了List接口和实现了这个接口的LinkedList类,List接口用于描述一个有序集合,并且集合中每个元素的位置十分重要。有两种访问元素的协议,一种是迭代器,另一种是用get和set方法随机地访问每个元素。后者不适用与LinkedList,但却对数组列表很有用。集合类库提供了一种大家熟悉的ArrayList类,这个接口也实现了List接口,他封装了一个动态再分配的对象数组。

 √注释:对一个经验丰富的Java程序员来说,在需要动态数组的时候,有可能会使用Vector类,为什么要用ArrayList取代Vector呢?原因很简单,Vector类的所有方法都是同步的,可以由两个线程安全地访问一个Vector对象。但是代码要在同步操作上耗费掉大量的时间

13.2.3 散列集
    链表和数组可以按照人们的意愿排列元素的次序,但是,如果想要查看某个指定的元素,                表13-2由HashCode函数导出的散列码(全屏模式)
    却又忘记了它的位置,就需要访问所有元素,知道找到它为止。如果不在意元素顺序的话    ——————————————————————————
,可以由有几种能够快速查找元素的数据结构。起缺点是无法控制元素出现的次序,它将按                                                                散列码
 照有利于其操作目的的原则组织数据。有一种总所周知的数据结构,可以快速地查找所需要    ——————————————————————————
的对象,这就是散列表(hash table).散列表为每个对象计算一个整数,成为散列码(hash code)        "Lee"                                            76268 
.散列码是由对象的实例域产生的一个整数,更准确地说,具有不同数据域的对象将产生不同           “lee”                                           107020
的散列码。表13-2列出了几个散列码的示例,他们是由String类的hashCode方法产生的。             “eel”                                          100300  
                                                                                                                                                 ——————————————————————————        

    如果自定义类,就要负责实现这个类的hashCode方法(如果不实现有什么问题,理解一下)。有关hashCode方法的详细内容请参见第五章。注意,自己实现的hashCode方法应该与equlas方法兼容,即如果a.equlas(b)为true,a与b必有具有相同的散列码。
    现在,最重要的问题是散列码要能够快速地计算出来,并且这个计算只与要散列的对象状态有关,与散列表中的其他对象无关。在java中,变列表用链表数组实现。每个列表被称为桶(bucket).要想查到表中对象的位置,就要先计算它的散列码,然后与桶的总数取余,所得到的结果就是保存这个元素的桶的索引,例如:如果某个对象的散列码为76268,并且有128个桶,对象应该保存在108号桶中(76268%128=108)。或许会很幸运,在这个桶中没有其他元素,此时 将元素直接插入到桶中就可以了。当然,有时候会遇到桶被占满的情况,这也是不可避免的。这种现象被称为散列冲突(hash collision)。这时,需要用新对象与桶中的所有对象进行比较,查看这个对象是否存在。如果散列码是合理且随机分配的,桶的数目也足够大,需要比较的次数就会很少
    如果想更多地控制散列表的运行性能,就要指定一个初始的桶数。桶数是指用于收集具有相同散列值的桶的数目。如果要插入到散列表中的元素太多,就会增加冲突的可能性,降低运算性能。
    如果大致知道最终会有多少个元素要插入到散列表中,就可以设置桶数,通常将桶数设置为预计元素个数的75%~150%。当然,并不是总能够知道需要存储多少个元素的,也有可能最初的估计过低,如果散列表太满,就需要在散列(rehashed)。 如果要对散列表再散列,就需要创建一个桶数更多的表,并将所有元素插入到这个新表中,然后再丢弃原来的表。装填因子(load factor)决定何时对散列表进行再散列。例如,如果装填因子为0.75(默认值),而表中超过75%的位置已经填入元素,这个表就会用双倍的桶数自动地进行再散列。对于大多数应用程序来说,装填因子为75%是比较合理的。

    散列表可以用于实现几个重要的数据结构,其中最简单的是Set类型,set是没有重复元素的元素集合。set的add方法首先在集中查找要添加的对象,如果不存在,就将这个元素添加进去。
    java集合类提供了一个HashSet类,他实现了基于散列表的集,可以用add方法添加元素。contains方法已经被重新定义,用来快速地查看是否某个元素已经出现在集中。它只在某个桶中查找元素,而不必查看集合中的所有元素。
    散列集迭代器将依次访问所有的桶,由于散列将元素分散在表的各个位置上,所以访问它们的顺序几乎是随机的。只有不关心集合元素的顺序时才应该使用HashSet。
package corejava.volume1.collection;
import java.util.HashSet;
import java.util.Iterator;
import java.util.Scanner;
import java.util.Set;
public class SetTest {
    public static void main(String[] args) {
        Set<String> words = new HashSet<String>();
        long totalTime = 0;
        
        Scanner in = new Scanner(System.in);
        while(in.hasNext()){
            String word = in.next();
            long callTime = System.currentTimeMillis();
            words.add(word);
            callTime = System.currentTimeMillis() - callTime;
            totalTime += callTime;
        }
        
        Iterator<String> iter = words.iterator();
        for(int i=0; i<=20; i++){
            System.out.println(iter.next());
        }
        System.out.println(". . .");
        System.out.println(words.size()+"distinct words."+totalTime+"millisecond");
    }
}

输入这行命令 记得把alice.txt放在与java源文件同一个目录下(理解一下为什么输出的是字符串而不是字符)
C:\>java SetTest < alice.txt


13.2.4 树集
    TreeSet类与散列集十分类似,不过,它比散列集有所改进。树集是一个有序集合(order collection)。可以以任意顺序将元素插入到集合中,在对集合进行遍历时,每个值将自动按照排序后的顺序呈现。
        Set<String> set = new TreeSet<String>();
        set.add("niculars");
        set.add("billy");
        set.add("sam");
        set.add("james");
        for(String name : set){
            System.out.println(name);
        }
打印结果
billy
james
niculars
sam
   TreeSet每次将一个元素添加到树中时,都被放置在正确的排序位置上。因此,迭代器总是以排好序的顺序访问每个元素。
    将一个元素添加到树中要比添加到散列表中慢,但是,与将元素添加到数组或链表的正确位置上相比还是快很多的。如果树中包含n个元素,查找新元素的正确位置平均需要log2n次比较。例如,如果一棵树包含了1000个元素,添加一个新元素大概需要比较10次。

13.2.5 对象的比较
    TreeSet如何知道希望元素怎样排列呢?在默认情况时,树集假定插入的元素实现了Comparable接口,这个接口定义了一个方法:
public interface Comparable<T> {
    public int compareTo(T o);
}
    有些标准的Java平台类实现了Comparable接口,例如String类。这个类的compareTo方法依据字典序对字符串进行排序
    如果要插入自定义的对象,就必须通过实现Comparable接口自定义排列顺序。在Obj类中,没有提供任何compareTo接口的默认实现。
×警告:只有整数在一个足够小的范围内,才可以使用这个技巧。如果x是一个较大的正整数,y是一个较大的负整数,x-y就有可能会溢出。

    然而,使用Comparable接口定义排列排序显然有其局限性,对于一个给定的类,只能够实现这个接口一次。如果在一个集合中需要按照部件编号排序,在另一个集合中却要按照描述信息排序,怎么办呢?另外,如果需要对一个类的对象进行排序,而这个类没有实现Comparable接口,又该怎么办呢?在这种情况下可以通过将Comparator对象传递给TreeSet的构造器来告诉树集使用不用的比较方法。Comparator接口声明了一个带有两个显式参数的compare方法:
package corejava.volume1.collection;
import java.util.Comparator;
import java.util.SortedSet;
import java.util.TreeSet;
public class TreeSetTest {
 
    public static void main(String[] args) {
        SortedSet<Item> parts = new TreeSet<Item>();
        parts.add(new Item("Jimmy"2345));
        parts.add(new Item("Toaster"1234));
        parts.add(new Item("Widget"4562));
        parts.add(new Item("Modem"9912));
        parts.add(new Item("Lee"1988));
        System.out.println(parts);
        
        SortedSet<Item> sortByDescription = new TreeSet<Item>(new Comparator<Item>() {
             @Override
            public int compare(Item a, Item b) {
                 return a.getDescription().compareTo(b.getDescription());
            }
         });
        sortByDescription.addAll(parts);
        System.out.println(sortByDescription);
    }
}
class Item implements Comparable<Item>{
    
    
    public Item() {
        super();
     }
    
    public Item(String description, int partNumber) {
         this.description = description;
        this.partNumber = partNumber;
    }
    public String getDescription() {
        return description;
    }
    public int getPartNumber() {
        return partNumber;
    }
    public void setDescription(String description) {
        this.description = description;
    }
    public void setPartNumber(int partNumber) {
        this.partNumber = partNumber;
    }
    
    @Override
    public String toString() {
        return "Item [description=" + description + ", partNumber="
                + partNumber + "]";
    }
    @Override
    public int hashCode() {
        final int prime = 31;
        int result = 1;
        result = prime * result
                + ((description == null) ? 0 : description.hashCode());
        result = prime * result + partNumber;
        return result;
    }
    @Override
    public boolean equals(Object obj) {
        if (this == obj)
            return true;
        if (obj == null)
            return false;
        if (getClass() != obj.getClass())
            return false;
        Item other = (Item) obj;
        return description.equals(other.description) 
                && partNumber != other.partNumber ;
    }
    @Override
    public int compareTo(Item other) {
         return partNumber - other.partNumber;
    }
    private String description;
    private int partNumber;
}
√注释:实际上,Comparator<T>接口声明了两个方法,compare和equals。当然,每一个类都有一个equals方法:因此,为这个接口声明再添加一个equals方法似乎没有太大好处,API文档解释说,不需要覆盖equals方法,但这样做可能会在某些情况下提高性能。例如,如果从另一个集合添加元素,这个由使用相同比较器的另外一个集添加元素,TreeSet类中的addAll方法的效率会更高。
√注释:从java6起,TreeSet类实现了NavigableSet接口,这个接口增加了几个便于定位元素以及反向遍历的方法。

13.2.6 队列与双端队列
    前面已经讨论过,队列可以让人们有效地在尾部添加一个元素,在头部删除一个元素。有两个端头的队列,叫双端队列,可以让人们有效地在头部和尾部同时添加或者删除元素。不支持在队列中间添加元素。在javaSE6中引入了Deque接口,并由ArrayDeque和LinkedList类实现,这两个类都提供了双端队列,而且在必要时可以增加队列的长度。在14章将会看到有限队列和有限双端队列。

13.2.7优先级队列
    优先级队列(priority queue)中的元素可以按照任意的顺序插入,却总是按照排序的顺序进行检索。也就是说,无论何时调用remove方法,总会获得当前优先级队列中最小的元素。然而,优先级队列并没有对所有的元素进行排序。如果用迭代的方式处理这些元素,并不需要对他们进行排序。优先级队列使用了一个优雅且高效的数据结构,称为堆(heap)。堆是一个可以自我调整的二叉树,对数执行添加和删除操作,可以让最小的元素移动到跟,而不必花费时间对元素进行排序。与TreeSet一样,一个优先级队列既可以保存实现了Comparable接口的类对象,也可以保存于构造器中提供比较器的对象。
    使用优先级队列的典型示例是任务调度。每一个任务有一个优先级,任务以随机顺序添加在队列中。每当启动一个新的任务时,都将优先级最高的任务从队列中删除(由于习惯将1设为“最高”优先级,所以会将最小的元素删除)。下面这个例子的迭代并不是按照元素的排列顺序访问的。而删除却总是删除剩余元素中优先级最小的那个元素
public class PriorityQueueTest {
    public static void main(String[] args) {
        PriorityQueue<GregorianCalendar> pq = new PriorityQueue<GregorianCalendar>();
        pq.add(new GregorianCalendar(1906, GregorianCalendar.DECEMBER, 9) );
        pq.add(new GregorianCalendar(1815, GregorianCalendar.DECEMBER, 10) );
        pq.add(new GregorianCalendar(1903, GregorianCalendar.DECEMBER, 3) );
        pq.add(new GregorianCalendar(1910, GregorianCalendar.JUNE, 21) );
        pq.add(new GregorianCalendar(1896, GregorianCalendar.FEBRUARY, 27) );
        System.out.println("iterator thr elements...");
        
        for(GregorianCalendar date : pq){
            System.out.println(date.get(Calendar.YEAR));
        }
        System.out.println("remove a elements...");
        while(!pq.isEmpty()){
            System.out.println(pq.remove().get(Calendar.YEAR));
        }
    }
 }

13.2.8 映射表
        集是一个集合,它可以快速地查找现有的元素,但是,要查看一个元素,需要有要查找元素的精确副本。这不是一种非常通用的查找方式,通常,我们知道某些键的信息,并想要查找与之对应的元素。映射表(map)数据结构就是为此设计的。它用来存放键值对。如果提供了键,就能通过它来找到值。
        java类库为映射表提供了两个通用的实现:HashMapTreeMap。他们都实现了Map接口。
        散列映射表对键进行散列,树映射表用键的整体顺序对元素进行排序,并将其组织成搜索树。散列或比较函数只能作用于键。与键相关联的值不能进行散列或者比较
        键必须是唯一的。不能对同一个键存放两个值,如果对同一个键两次调用put方法,第一个值会被第二个值替换掉。实际上,put将返回用这个键参数存储的上一个值
        System.out.println(map.put("key""val"));
        System.out.println(map.remove("key"));

        集合框架并没有将映射表本身视为一个集合(其它的数据结构框架则将映射表视为对(pair)的集合,或者视为用键作为索引的值得集合)。然而,可以获得映射表的视图,这是一组实现了Collection接口对象,或者他的子接口的视图。有三个视图,他们分别是:键集,值集合(注意,不是集【理解一下意思】),和键/值对集。键与键/值对形成了一个集,这是因为在映射表中一个键只能有一个副本。下列方法将返回这3个视图(条目集的元素时静态内部类Map.Entry的对象)
public class MapTest {
    public static void main(String[] args) {
        Map<String, Employee> staff = new HashMap<String, Employee>();
        staff.put("144-25-5464"new Employee("Amy Lee"));
        staff.put("567-24-2546"new Employee("Harry Hacker"));
        staff.put("157-62-7935"new Employee("Gary Cooper"));
        staff.put("456-62-5227"new Employee("Francesca Cruz"));
        
        System.out.println(staff);
        for(Map.Entry<String, Employee> entry : staff.entrySet()){
            System.out.println("key=" + entry.getKey() + ", val =" + entry.getValue());
        }
    }
}
class Employee{
     
    public Employee() {
     }
    public Employee(String name) {
         this.name = name;
    }
    
    
    @Override
    public String toString() {
        return "employee [name=" + name + ", salary=" + salary + "]";
    }
    
    @Override
    public int hashCode() {
        final int prime = 31;
        int result = 1;
        result = prime * result + ((name == null) ? 0 : name.hashCode());
        result = prime * result + (int) (salary ^ (salary >>> 32));
        return result;
    }
    @Override
    public boolean equals(Object obj) {
        if (this == obj)
            return true;
        if (obj == null)
            return false;
        if (getClass() != obj.getClass())
            return false;
        Employee other = (Employee) obj;
        if (name == null) {
            if (other.name != null)
                return false;
        } else if (!name.equals(other.name))
            return false;
        if (salary != other.salary)
            return false;
        return true;
    }
    public String getName() {
        return name;
    }
    public long getSalary() {
        return salary;
    }
    public void setName(String name) {
        this.name = name;
    }
    public void setSalary(long salary) {
        this.salary = salary;
    }
    private String name;
    private long salary;
}

    13.2.9 专用集与映射表类
        在集合类库中,有几个专用的映射表类,本节对他们做一下简要地介绍
        1.弱散列映射表
        设计WeakHashMap类是为了解决一个有趣的问题。如果有一个值,对应的键已经不再使用了,将会出现什么情况呢?假定对某个键的最后一次引用已经消亡,不再有任何途径引用这个值的对象了。但是由于在程序中的任何部分没有再出现这个键,所以这个键值对无法从映射表中删除。为什么垃圾回收器不能删除它呢?难道删除无用的对象不是垃圾回收器的工作吗?遗憾的是,事情没有这样简单。垃圾回收器跟踪活动的对象。只要映射表对象是活动的,其中的所有桶也是活动的,它们不能被回收。因此,需要由程序负责从长期存活的映射表中删除那些无用的值。或者使用WeakHashMap使用弱引用(weak references)保存键。WeakHashMap对象将引用保存到另外一个对象中,在这里,就是散列表键。对于这种类型的对象,垃圾回收器用一种特有的方式进行处理。通常,如果垃圾回收器发现某个特定的对象已经没有他人引用了,就将其回收。然而,如果某个对象只能由WeakReference引用,垃圾回收器仍然回收它,但要将引用这个对象的弱引用放入队列中。WeakHashMap将周期性地检查队列,以便找出新添加的弱引用。一个弱引用进入队列意味着这个键不再被他人使用,并且已经被收集起来。于是,WeakHashMap将删除对应的条目。
        2.链接散列集和链接映射表
        javaSE1.4增加了两个类:LinkedHashSet和LinkedHashMap,用来记住插入元素项的顺序。这样就可以避免在散列表中的项从表面上看是随机排列的。当条目插入到表中时,就会并入到双向链表中,下面代码输出会按照插入的顺序输出
        Map<String, Employee> staff = new LinkedHashMap<String, Employee>();
        staff.put("144-25-5464"new Employee("Amy Lee"));
        staff.put("567-24-2546"new Employee("Harry Hacker"));
        staff.put("157-62-7935"new Employee("Gary Cooper"));
        staff.put("456-62-5227"new Employee("Francesca Cruz"));
        
        //System.out.println(staff);
        for(Map.Entry<String, Employee> entry : staff.entrySet()){
            System.out.println("key=" + entry.getKey() + ", val =" + entry.getValue());
        }

框架(framework)是一个类的集,它奠定了创建搞基功能的基础。框架包含很多超类,这些超类拥有非常有用的功能,策略和机制。框架使用者创建的子类可以扩展超类的功能,而不必重新创建这些基本的机制。例如,Swing就是一种用户界面的机制。
        java集合类库构成了集合类的框架。它为集合的实现定义了大量的接口和抽象类,并且对其中的某些机制给予了描述,例如迭代协议。正如前面几节所做的那样,可以使用这些集合类,而不必了解框架。但是如果想要实现用于多种集合类型的泛型算法,或者是想要增加新的集合类型,了解一些框架的知识是很有帮助的。
        集合有两个基本的接口:Collection和Map。


评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值