集合-List列表

列表List

List接口在Collection的基础上添加了许多方法,允许在list中间插入和删除元素。

List主要有两种类型的具体实现:

  • ArrayList:底层数组实现,动态调整集合的大小,擅长随机访问,但在ArrayList中插入和删除元素速度较慢。
  • LinkedList:底层链表实现,擅长插入和删除,对于随机访问来说相对较慢。

List

下面以一个例子来介绍List接口中方法的使用:首先定义一个外部类Phone,提供一个静态方法list返回一组Phone集合。

class Phone {
    String name;
    static List<Phone> list() {
        // Exception in thread "main" java.lang.UnsupportedOperationException
        //return Arrays.asList(new HuaWei(), new Nova(), new P40(), new Honor(), new IPhone(), new OnePlus());
        return new ArrayList<>(Arrays.asList(new HuaWei(), new Nova(), new P40(), new Honor(), new IPhone(), new OnePlus()));
    }

    @Override
    public String toString() {
        return name;
    }
}
class HuaWei extends Phone{public HuaWei() {super.name = "HuaWei";}}
class Nova extends Phone{public Nova() {super.name = "Nova";}}
class P40 extends Phone{public P40() {super.name = "P40";}}
class Honor extends Phone{public Honor() {super.name = "Honor";}}
class Honor20 extends Phone{public Honor20() {super.name = "Honor20";}}
class IPhone extends Phone{public IPhone() {super.name = "IPhone";}}
class OnePlus extends Phone{public OnePlus() {super.name = "OnePlus";}}
public class ListTest {

    public static void main(String[] args) {
        List<Phone> phones = Phone.list();
        System.out.println(phones);
		//=================[1]====================
        Honor20 honor20 = new Honor20();
        phones.add(honor20);    // Automatically resizes
        System.out.println("insert honor20->" + phones);
        System.out.println("contains honor20->" + phones.contains(honor20));
        phones.remove(honor20);  // Remove by Object
        phones.remove(1);  // Remove by index
        System.out.println("remove honor20 and object in 1 index->" + phones);
        /*
        输出:
        [HuaWei, Nova, P40, Honor, IPhone, OnePlus]
        insert honor20->[HuaWei, Nova, P40, Honor, IPhone, OnePlus, Honor20]
        contains honor20->true
        remove honor20 and object in 1 index->[HuaWei, P40, Honor, IPhone, OnePlus]
        */
		//=================[2]====================
        Phone p = phones.get(0);
        System.out.println(p + " index:" + phones.indexOf(p));
        HuaWei huaWei = new HuaWei();
        // 因为集合中存有一个HuaWei对象
        // 在没有将新对象huaWei加入到集合中之前,删除这个新对象,查看是否会影响集合中的HuaWei对象
        System.out.println(phones.indexOf(huaWei));
        System.out.println(phones.remove(huaWei));
		// 删除集合中的HuaWei对象
        System.out.println(phones.remove(p));
        System.out.println(phones);
		
        phones.add(0, new HuaWei()); // 在指定索引处插入对象
        System.out.println(phones);
		/*
		输出:
        HuaWei index:0
        -1
        false
        true
        [P40, Honor, IPhone, OnePlus]
        [HuaWei, P40, Honor, IPhone, OnePlus]
		*/
        //=================[3]====================
        List<Phone> sub = phones.subList(1, 4);// 求子集范围[1,4),4是开区间
        System.out.println("subList: " + sub);
        System.out.println("before shuffled containsAll->" + phones.containsAll(sub));
        Collections.shuffle(phones);        // 打乱集合
        System.out.println("shuffled subList: " + sub);
        System.out.println("after shuffled containsAll->" + phones.containsAll(phones)); 		//集合元素的顺序不影响containsAll的结果
		
        ArrayList<Phone> copy = new ArrayList<>(phones);//[3.1]
        sub = Arrays.asList(phones.get(1), phones.get(4));//[3.2]
        System.out.println("copy: " + copy + " sub: " + sub);
        copy.retainAll(sub);    //求交集
        System.out.println("retainAll(求交集之后)的copy: " + copy);
        /*
        输出:
        subList: [P40, Honor, IPhone]
        before shuffled containsAll->true
        shuffled subList: [OnePlus, Honor, HuaWei]
        after shuffled containsAll->true
        copy: [IPhone, OnePlus, Honor, HuaWei, P40] sub: [OnePlus, P40]
        retainAll(求交集之后)的copy: [OnePlus, P40]
        */
        
        //=================[4]====================
        copy = new ArrayList<>(phones);
        copy.removeAll(sub);
        System.out.println(copy);

        copy.set(1, new Honor());   // replace an element
        copy.addAll(2, sub);    // 在指定索引处插入集合

        System.out.println("before clear phones is empty:" + phones.isEmpty());
        phones.clear();
        System.out.println("clear phones->" + phones);
        System.out.println("after clear phones is empty:" + phones.isEmpty());
        phones.addAll(Phone.list());
        Object[] objects = phones.toArray();
        System.out.println(objects[3]);

        Phone[] ph = phones.toArray(new Phone[0]);
        System.out.println(ph[3]);
		/*
		输出:
		[IPhone, Honor, HuaWei]
        before clear phones is empty:false
        clear phones->[]
        after clear phones is empty:true
        Honor
        Honor
		*/
    }
}

[1]:当向List的实现ArrayList集合中插入元素时,能够动态增减大小(自动扩容调整索引),contains方法判断指定的对象是否在集合内,remove是一个重载方法,可以根据对象删除,也可以根据索引删除。

[2]:如果集合中已存在一个HuaWei对象,在没有将新对象HuaWei加入到集合中之前,删除这个新对象,查看是否会影响集合中的HuaWei对象,这是不会影响原集合的,尽管在认知上认为是同一个。contains行为依赖于equals方法。下面会介绍依赖于equals() 的点。

[3]:subList() 方法可以轻松地从更大的列表中创建切片,注意这里不包括边界,当将切片结果传递给原来这个较大的列表的 containsAll() 方法时,很自然地会得到 true。请注意,顺序并不重要,在 sub 上调用直观命名的 Collections.sort()Collections.shuffle() 方法,不会影响 containsAll() 的结果。 subList() 所产生的列表的幕后支持就是原始列表,sub只持有原始列表的部分引用。

retainAll() 方法实际上是一个“集合交集”操作,在本例中,它保留了同时在 copysub 中的所有元素。请再次注意,所产生的结果行为依赖于 equals() 方法。

[3.1]、[3.2]处的代码,展示了集合是**“持有对象引用”**的,集合对象变了,但是集合中数据元素的对象引用并没有发生变化,copy、sub集合里面的对象引用和phone中的对象引用是相同的。

[4]:removeAll() 方法也是基于 equals() 方法运行的。 顾名思义,它会从 List 中删除在参数 List 中的所有元素。可以通过set()方法替换指定索引处的元素值,clear()用于清空集合中的元素(清空了集合中持有的对象引用),isEmpty()判断集合中是否含有对象引用(元素)。对于 List ,有一个重载的 addAll() 方法可以将新列表插入到原始列表的中间位置,而不是仅能用 CollectionaddAll() 方法将其追加到列表的末尾。

toArray() 方法将任意的 Collection 转换为数组。这是一个重载方法,其无参版本返回一个 Object 数组,但是如果将目标类型的数组传递给这个重载版本,那么它会生成一个指定类型的数组(假设它通过了类型检查)。如果参数数组太小而无法容纳 List 中的所有元素(就像本例一样),则 toArray() 会创建一个具有合适尺寸的新数组

依赖于equals方法?持有对象引用?集合常见误区

1、依赖于equals方法

当确定元素是否是属于某个 List ,寻找某个元素的索引,以及通过引用从 List 中删除元素时,都会用到 equals() 方法(根类 Object 的一个方法),如List.indexOf(Object obj)、List.contains(Object obj)、List.containsAll(List lists)、List.remove(Object obj)、List.removeAll(List lists)、List.retainAll(List lists)。上面的HuaWei的例子也可以说明,新生成的HuaWei对象,当调用contains()方法时,怎么知道集合是否包含HuaWei对象,底层就是通过调用对象的equals()判断是否包含,因为类中都没有重写equals()方法,所以默认调用的是父类中的equals()(判断地址),所以当对新实例HuaWei调用indexOf时,就会返回 -1 (表示未找到),或者调用remove就会返回false。如果我们重写了 equals() ,那么结果就会有所不同。

对于其他类, equals() 的定义可能有所不同。例如,如果两个 String 的内容相同,则这两个 String 相等。因此,为了防止出现意外,请务必注意 List 行为会根据 equals() 行为而发生变化。

@Test
public void testList() {

    List<String> strings =  new ArrayList<>(Arrays.asList("Long", "Abc", "Qwe"));
    strings.add("Long");
    System.out.println(Arrays.toString(strings.toArray()));
    System.out.println(strings.remove("Long"));
    //System.out.println(strings.remove(new String("Long"))); 等同于上面
    System.out.println(Arrays.toString(strings.toArray()));
    System.out.println(strings.remove("Long"));
    System.out.println(Arrays.toString(strings.toArray()));
    /*
    输出:
    [Long, Abc, Qwe, Long]
    true
    [Abc, Qwe, Long]
    true
    [Abc, Qwe]
    */
}

从上面结果我们就可以看出,一String类重写了equals方法,所以remove方法看的效果和之前是不一样的;二因为集合中有两个与"Long"相等的数据元素,默认是从第一个开始处理的,不仅仅是对于remove还有contains等等都会处理第一个出现的元素。

2、持有对象引用

public static void main(String[] args) {
    // 持有对象引用思想
    List<Phone> phones = Phone.list();
    System.out.println(phones);
    // copy集合也保存了phones集合中的所持有的对象引用,
    // 注意仅仅是保存了一组地址值在集合中,并不是保存了数据对象。
    // 注意理解 对象引用的概念。
    ArrayList<Phone> copy = new ArrayList<>(phones);
    copy.clear();
    System.out.println("copy->" + copy);
    // copy仅仅是清空了集合中保存的地址值,并没有销毁对象,只是不在持有对象引用
    // phones并没有清空引用值,所以说phone还是保留着手机对象的引用值。
    System.out.println("phones->" + phones);

    // 还是对象引用的概念,这里不再阐述。
    Phone phone = phones.get(0);
    System.out.println(phone.name);
    copy = new ArrayList<>(phones);
    Phone copyPhone = copy.get(0);
    copyPhone.name = "Nokia";
    System.out.println(phone.name);
}

3、集合常见误区?

Phone类中存在一个静态方法

static List<Phone> list() {
        // Exception in thread "main" java.lang.UnsupportedOperationException
        //return Arrays.asList(new HuaWei(), new Nova(), new P40(), new Honor(), new IPhone(), new OnePlus());[1]
        return new ArrayList<>(Arrays.asList(new HuaWei(), new Nova(), new P40(), new Honor(), new IPhone(), new OnePlus()));
    }

当使用[1]构造的集合列表,若之后对该集合列表进行add或者remove操作就会引发java.lang.UnsupportedOperationException,这是为什么呢?来看一下Arrays.asList(T... a)底层源码:

@SafeVarargs
    public static <T> List<T> asList(T... a) {
        return new ArrayList<>(a);
    }

方法返回的ArrayList,ArrayList集合不可以动态扩容吗?这就很奇怪了,当仔细观察,发现ArrayList并不是java.util.ArrayList,而是java.util.Arrays.ArrayList,属于Arrays的一个私有内部类,继承了AbstractList并重写了一些方法,add和remove方法并没有重写,那么默认会调用父类AbstractList的方法,AbstractList抽象类的add和remove的方法体就是抛出异常,所以说这就是为什么对Arrays.asList(T... a)的结果进行写操作时会引发异常。另一方面来说,java.util.Arrays.ArrayList底层是数组来存储值的,由于add和remove这两个方法会尝试修改数组大小,所以会在运行时得到“Unsupported Operation(不支持的操作)”错误:

public void add(int index, E element) {
        throw new UnsupportedOperationException();
}
public E remove(int index) {
    throw new UnsupportedOperationException();
}

LinkedList(链表)

数组和数组列表都有一个重大的缺陷,当从数组的中间位置删除一个元素要付出很大的代价,因为数组中处于被删除元素之后的所有元素都要向数组的前端移动,如果数据量大的话,这是十分耗时的。Java中的链表解决了这个问题,链表将对象存放在独立的结点中,每个结点保留着下一个结点的引用。

LinkedList底层结构就是链表,它实现了基本的List接口,它在List中间执行插入和删除时比ArrayList更高效,但随机访问操作效率不及ArrayList

在Java中,所有链表实际上都是双向链表(doubly linked)—每个结点还存放着指向前驱结点的引用。

LinkedList 还添加了一些方法,使其可以被用作栈、队列或双端队列(deque) 。在这些方法中,有些彼此之间可能只是名称有些差异,或者只存在些许差异,以使得这些名字在特定用法的上下文环境中更加适用(特别是在 Queue 中)。例如:

  • getFirst()element() 是相同的,element()底层就是调用的getFirst(),它们都返回列表的头部(第一个元素)而并不删除它,如果 List 为空,则抛出 NoSuchElementException 异常。 peek() 方法与这两个方法只是稍有差异,它在列表为空时返回 null
  • removeFirst()remove() 也是相同的,它们删除并返回列表的头部元素,并在列表为空时抛出 NoSuchElementException 异常。 poll() 稍有差异,它在列表为空时返回 null
  • addFirst() 在列表的开头插入一个元素。
  • offer()add()addLast() 相同。 它们都在列表的尾部(末尾)添加一个元素。
  • removeLast() 删除并返回列表的最后一个元素。

示例:

public class LinkedListTest {
    public static void main(String[] args) {
        LinkedList<Phone> phones = new LinkedList<>(Phone.list());
        System.out.println(phones);
        // 获取第一个元素,不同点是对empty-list的行为不同
        System.out.println("getFirst:" + phones.getFirst());
        System.out.println("element:" + phones.element());
        System.out.println("peek:" + phones.peek());

        // 删除并返回删除的元素
        System.out.println("phones.remove():" + phones.remove());// 底层通过removeFirst删除
        System.out.println("phones.removeFirst():" + phones.removeFirst());
        System.out.println("phones.poll():" + phones.poll());
        System.out.println(phones);

        // 在列表头插入一个元素
        phones.addFirst(Phone.get());
        System.out.println("After addFirst():" + phones);
        // 在列表尾插入元素 offer add addLast
        phones.offer(Phone.get());
        System.out.println("After offer():" + phones);
        phones.add(Phone.get());
        System.out.println("After add():" + phones);
        phones.addLast(new Honor20());
        System.out.println("After addLast(): " + phones);
        /*
        输出:
        [HuaWei, Nova, P40, Honor, IPhone, OnePlus]
        getFirst:HuaWei
        element:HuaWei
        peek:HuaWei
        phones.remove():HuaWei
        phones.removeFirst():Nova
        phones.poll():P40
        [Honor, IPhone, OnePlus]
        After addFirst():[OnePlus, Honor, IPhone, OnePlus]
        After offer():[OnePlus, Honor, IPhone, OnePlus, IPhone]
        After add():[OnePlus, Honor, IPhone, OnePlus, IPhone, IPhone]
        After addLast(): [OnePlus, Honor, IPhone, OnePlus, IPhone, IPhone, Honor20]
         */
    }
}

Iterator

迭代器是一个对象,它在一个序列中移动并选择该序列中的每个对象,而客户端程序员不知道或不关心该序列的底层结构。另外迭代器通常称为轻量级对象(lightweight object):创建它的代价小。Java的Iterator只能单向移动

Iterator接口源码:

public interface Iterator<E> {
   
    /*检查序列中是否还有元素*/
    boolean hasNext();
    
	/*获得序列中的下一个元素*/
    E next();
    
    /*将迭代器最近返回的那个元素删除*/
    default void remove() {
        throw new UnsupportedOperationException("remove");
    }
    
    default void forEachRemaining(Consumer<? super E> action) {
        Objects.requireNonNull(action);
        while (hasNext())
            action.accept(next());
    }
}

Iterator的简单使用

示例一:

public class IteratorTest {

    public static void main(String[] args) {
        // Iterator遍历元素
        List<Phone> phones = Phone.list();
        Iterator<Phone> it = phones.iterator();
        while (it.hasNext()) {
            Phone p = it.next();
            System.out.print(p + " ");
        }
        System.out.println();

        // for-each增强for循环,Collection接口扩展了Iterable接口,
        // 对于任何实现了Collection接口的类都使用for-each循环
        for (Phone p : phones) {
            System.out.print(p + " ");
        }
        System.out.println();

        // 利用Iterator删除元素
        it = phones.iterator();
        for (int i = 0; i < 3; i++) {
            it.next();
            it.remove();
        }
        System.out.println(phones);
        
        /*
        输出
        HuaWei Nova P40 Honor IPhone OnePlus 
        HuaWei Nova P40 Honor IPhone OnePlus 
        [Honor, IPhone, OnePlus]
         */
    }

}

根据示例一,可得知,有了Iterator,遍历元素时,我们不在关心集合的数量,会由hasNext()next()帮我们处理。Iterator可以删除next()生成的最后一个元素,需要注意,必须在next之后调用remove(),至于为什么,下面会介绍。

示例二:

public class IteratorTestTwo {
	
   public static void display(Iterator<Phone> it) {
        while(it.hasNext()) {
            Phone p = it.next();
            System.out.print(p + " ");
        }
        System.out.println();
    }
    
    // 更通用的方法
    public static void display(Iterable<Phone> iterable) {
        iterable.forEach(System.out::print);
    }

    public static void main(String[] args) {

        List<Phone> phones = Phone.list();
        LinkedList<Phone> phonesLL = new LinkedList<>(phones);
        HashSet<Phone> phonesHS = new HashSet<>(phones);
        // 注意这里需要之前的Phone类实现Comparable接口,因为TreeSet需要比较然后按元素顺序排序
        TreeSet<Phone> phonesTS = new TreeSet<>(phones);
        display(phones.iterator());
        display(phonesLL.iterator());
        display(phonesHS.iterator());
        display(phonesTS.iterator());
        /*
        输出:
        HuaWei Nova P40 Honor IPhone OnePlus
        HuaWei Nova P40 Honor IPhone OnePlus
        P40 OnePlus IPhone HuaWei Honor Nova
        Honor HuaWei IPhone Nova OnePlus P40
         */
        display(phones);	// List间接继承了Iterable接口,对于其他集合序列也一样
    }

}

示例二展示了我们无需知道具体序列的类型,Iterator将遍历序列的操作与该序列的底层结构分离,或者说迭代器统一了对集合的访问方式。另外display是一个重载方法,形参是Iterable类型的,Iterable可以产生Iterator的任何方法,并且它还有一个forEach默认方法。使用它对集合的访问显得更简单,可以直接通过display(phones)就可以访问,因为集合都实现了Collection,而它又扩展了Iterable,间接继承。

Collection类扩展了Iterable接口,而Iterable接口提供了获取一个Iterator对象的方法,所以对于任何集合,都可以获取它的Iterator对象。

ListIterator

ListIterator是一个更强大的Iterator子类型,它只能由各种List类生成。Iterator 只能向前移动,而 ListIterator 可以双向移动。它可以生成迭代器在列表中指向位置的后一个和前一个元素的索引,并且支持修改集合中的元素。可以通过调用集合实现类中的 listIterator() 方法来生成指向 List 开头处的 ListIterator ,还可以通过调用 listIterator(n) 创建一个一开始就指向列表索引号为 n 的元素处的 ListIterator

ListIterator源码

public interface ListIterator<E> extends Iterator<E> {
    // Query Operations

    boolean hasNext();

    E next();

    boolean hasPrevious();

    E previous();

    int nextIndex();

    int previousIndex();

    // Modification Operations

    void remove();
    
    /*set方法用一个新元素取代调用next或previous方法返回的上一个元素*/
    void set(E e);

    void add(E e);
}

示例:

public static void main(String[] args) {
    
    List<Phone> phones = Phone.list();
    ListIterator<Phone> it = phones.listIterator();
    while (it.hasNext()) {
        System.out.println(it.next() + ",nextIndex:" + it.nextIndex() + ",previousIndex:" + it.previousIndex()+";");
    }

    // 从后往前遍历
    System.out.print("reverse traverse->" );
    while (it.hasPrevious()) {
        System.out.print(it.previous() + " ");
    }

    System.out.println();
    System.out.println(phones);
    it = phones.listIterator(3);
    // 获得从索引3处开始的ListIterator对象
    while (it.hasNext()) {
        it.next();
        // get()会随机得到一个Phone对象
        it.set(Phone.get());
    }
    // 在集合尾部添加一个元素
    it.add(Phone.get());
    System.out.println(phones);
    /*
    输出:
    HuaWei,nextIndex:1,previousIndex:0;
    Nova,nextIndex:2,previousIndex:1;
    P40,nextIndex:3,previousIndex:2;
    Honor,nextIndex:4,previousIndex:3;
    IPhone,nextIndex:5,previousIndex:4;
    OnePlus,nextIndex:6,previousIndex:5;
    reverse traverse->OnePlus IPhone Honor P40 Nova HuaWei
    [HuaWei, Nova, P40, Honor, IPhone, OnePlus]
    [HuaWei, Nova, P40, OnePlus, IPhone, P40, Honor]
    */
}

ListIterator是一个接口,ArrayList没有实现却能返回这个接口的对象?

底层发现ArrayList并没有直接实现ListIterator,有点和Arrays类似,也是通过一个私有匿名内部类间接实现ListIterator,所以说就能获得该对象。

看源码!

迭代器注意点

迭代器解析

Java迭代器的查找操作和位置变更是紧密相连的。只能顺序next()或者反序previous()依次遍历。不能像get(index)那样随机访问。

因此,应该讲Java迭代器认为是位于两个元素之间。当调用next或者previous,迭代器就越过下一个元素或者上一个元素,并返回刚刚越过的那个元素的引用。

在这里插入图片描述

Iterator的next方法和remove方法的调用具有相互依赖性。如果调用remove之前没有调用next将是不合法的。否则就会抛出IllegalStateException。对于previous同样道理。

List<String> strings = new ArrayList<>(Arrays.asList("aaa", "bbb", "ccc"));
ListIterator<String> it = strings.listIterator(3);
while (it.hasPrevious()) {
    it.hasPrevious();
    it.remove();
    // 不可再次调用,只消耗刚刚返回的那个元素
    // it.remove();
}

这样做有什么好处,我所理解到的是避免了一定的死循环,比如

List<String> strings = new ArrayList<>(Arrays.asList("aaa", "bbb", "ccc"));
ListIterator<String> it = strings.listIterator();
// [1]没有死循环
while (it.hasNext()) {
    it.next();
    it.add("11");
}
System.out.println(Arrays.toString(strings.toArray()));
// 输出:[aaa, 11, bbb, 11, ccc, 11]
// [2]下面就会出现死循环
while (it.hasPrevious()) {
    it.previous();
    it.add("22");
}
System.out.println(Arrays.toString(strings.toArray()));
// 死循环

[1]如果it指针指向了当前索引而不是当前元素和下一个元素的中间位置,那么上面就会造成死循环。因为遍历的总是插入的前一个元素

[2]为什么会死循环?看源码给予了解答:

/*
 *Inserts the specified element into the list (optional operation).
 * The element is inserted immediately before the element that
 * would be returned by {@link #next}, if any, and after the element
 * that would be returned by {@link #previous}, if any
 */

插入元素在调用next()方法返回的元素之前(如果有的话),或者调用previous()在返回的元素之后插入(如果有的话)。当调用previous()后,然后add()总是会在光标(指针所在位置)之后插入,所以就会导致插入的元素总是在光标之后,从而导致了死循环。下图解释了这一现象:

在这里插入图片描述

多个迭代器修改访问异常

List<String> strings = new ArrayList<>(Arrays.asList("aaa", "bbb", "ccc"));
ListIterator<String> it1 = strings.listIterator();
ListIterator<String> it2 = strings.listIterator();
it1.next();
it1.remove();
it2.next();
// Exception in thread "main" java.util.ConcurrentModificationException

1)如果在某个迭代器修改集合时,另一个迭代器对其进行遍历,一定会出现混乱的状态。如上,如果一个迭代器指向另一个迭代器刚刚删除的元素前,现在这个迭代器就是无效的,并且不应该在使用。否则抛出ConcurrentModificationException

2)在迭代元素的时候不能通过集合的方法删除元素, 否则会抛出ConcurrentModificationException 异常. 但是可以通过Iterator接口中的remove()方法进行删除.

fail-fast&fail-safe

快速失败机制和安全失败机制的区别?

Iterator的安全失败是基于对底层集合做拷贝,因此,它不受源集合上修改的影响。java.util包下面的所有的集合类都是快速失败的,而java.util.concurrent包下面的所有的类都是安全失败的。快速失败的迭代器会抛出ConcurrentModificationException异常,而安全失败的迭代器永远不会抛出这样的异常。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值