六、lambda之Stream流式编程

一、 什么是 Stream

Stream 中文称为 “流”,通过将集合转换为这么一种叫做 “流” 的元素序列,通过声明性方式,能够对集合中的每个元素进行一系列并行或串行的流水线操作。

换句话说,你只需要告诉流你的要求,流便会在背后自行根据要求对元素进行处理,而你只需要 “坐享其成”。

二、流操作

在这里插入图片描述
整个流操作就是一条流水线,将元素放在流水线上一个个地进行处理。

其中数据源便是原始集合,然后将如 List 的集合转换为 Stream 类型的流,并对流进行一系列的中间操作,比如过滤保留部分元素、对元素进行排序、类型转换等;最后再进行一个终端操作,可以把 Stream 转换回集合类型,也可以直接对其中的各个元素进行处理,比如打印、比如计算总数、计算最大值等等。

很重要的一点是,很多流操作本身就会返回一个流,所以多个操作可以直接连接起来,我们来看看一条 Stream 操作的代码:
在这里插入图片描述
如果是以前,进行这么一系列操作,你需要做个迭代器或者 foreach 循环,然后遍历,一步步地亲力亲为地去完成这些操作;但是如果使用流,你便可以直接声明式地下指令,流会帮你完成这些操作。

有没有想到什么类似的?是的,就像 SQL 语句一样, select username from user where id = 1,你只要说明:“我需要 id 是 1 (id = 1)的用户(user)的用户名(username )”,那么就可以得到自己想要的数据,而不需要自己亲自去数据库里面循环遍历查找。

三、流与集合

什么时候计算

  • Stream 和集合的其中一个差异在于什么时候进行计算。

一个集合,它会包含当前数据结构中所有的值,你可以随时增删,但是集合里面的元素毫无疑问地都是已经计算好了的。

流则是按需计算,你可以想象一个水龙头,假设你需要一个奇数流,从 1 开始,那么这个水龙头会源源不断地流出你需要的数据,假设你只需要 10 个,那么这个流便会按需生成 10 个奇数,换句话来说,就是在用户要求的时候才会计算值,只要你需要,你便可以打开这个水龙头。

又比方说我们通过搜索引擎进行搜索,搜索出来的条目并不是全部呈现出来的,而且先显示最符合的前 10 条或者前 20 条,只有在点击 “下一页” 的时候,才会再输出新的 10 条。

再比方在线观看电影和你硬盘里面的电影,也是差不多的道理。

外部迭代和内部迭代

  • Stream 和集合的另一个差异在于迭代。

我们可以把集合比作一个工厂的仓库,一开始工厂比较落后,要对货物作什么修改,只能工人亲自走进仓库对货物进行处理,有时候还要将处理后的货物放到一个新的仓库里面。在这个时期,我们需要亲自去做迭代,一个个地找到需要的货物,并进行处理,这叫做外部迭代

后来工厂发展了起来,配备了流水线作业,只要根据需求设计出相应的流水线,然后工人只要把货物放到流水线上,就可以等着接收成果了,而且流水线还可以根据要求直接把货物输送到相应的仓库。这就叫做内部迭代,流水线已经帮你把迭代给完成了,你只需要说要干什么就可以了(即设计出合理的流水线)。

Java 8 引入 Stream 很大程度是因为,流的内部迭代可以自动选择一种合适你硬件的数据表示和并行实现;而以往程序员自己进行 foreach 之类的时候,则需要自己去管理并行等问题。

一次性的流

流和迭代器类似,只能迭代一次。

Stream<String> stream = list.stream().map(Person::getName).sorted().limit(10);         
List<String> newList = stream.collect(Collectors.toList());
List<String> newList2 = stream.collect(Collectors.toList());

上面代码中第三行会报错,因为第二行已经使用过这个流,这个流已经被消费掉了。

四、 一般方法

首先我们先创建一个 Person 泛型的 List

List<Person> list = new ArrayList<>();
list.add(new Person("jack", 20));
list.add(new Person("mike", 25));
list.add(new Person("tom", 30));

Person 类包含年龄和姓名两个成员变量

private String name;
private int age;

4.1、 stream() / parallelStream()

最常用到的方法,将集合转换为流

List list = new ArrayList();
// return Stream<E>
list.stream();

而 parallelStream() 是并行流方法,能够让数据集执行并行操作,后面会更详细地讲解。

4.2、filter(T -> boolean)

保留 boolean 为 true 的元素

//保留年龄为 20 的 person 元素
list = list.stream()
            .filter(person -> person.getAge() == 20)
            .collect(Collectors.toList());

//打印输出 [Person{name='jack', age=20}]

collect(toList()) 可以把流转换为 List 类型,这个以后会讲解。

4.3、distinct()

去除重复元素,这个方法是通过类的 equals 方法来判断两个元素是否相等的

如例子中的 Person 类,需要先定义好 equals 方法,不然类似[Person{name='jack', age=20}, Person{name='jack', age=20}] 这样的情况是不会处理的。

4.4、sorted() / sorted((T, T) -> int)

如果流中的元素的类实现了 Comparable 接口,即有自己的排序规则,那么可以直接调用 sorted() 方法对元素进行排序,如 Stream

反之, 需要调用 sorted((T, T) -> int) 实现 Comparator 接口

根据年龄大小来比较:

list = list.stream()
           .sorted((p1, p2) -> p1.getAge() - p2.getAge())
           .collect(Collectors.toList());

当然这个可以简化为

list = list.stream()
           .sorted(Comparator.comparingInt(Person::getAge))
           .collect(Collectors.toList());
4.4.1、数字排序
	/**
     * 数字排序
     */
    public static void testIntegerSort() {
        List<Integer> list = Arrays.asList(4, 2, 5, 3, 1);
        System.out.println(list);//执行结果:[4, 2, 5, 3, 1]
        //升序
        list.sort((a, b) -> a.compareTo(b.intValue()));
        System.out.println(list);//执行结果:[1, 2, 3, 4, 5]
        //降序
        list.sort((a, b) -> b.compareTo(a.intValue()));
        System.out.println(list);//执行结果:[5, 4, 3, 2, 1]
    }
4.4.2、字符串排序
	/**
     * 字符串排序
     */
    public static void testStringSort() {
        List<String> list = new ArrayList<>();
        list.add("aa");
        list.add("cc");
        list.add("bb");
        list.add("ee");
        list.add("dd");
        System.out.println(list);//执行结果:aa, cc, bb, ee, dd
        //升序
        list.sort((a, b) -> a.compareTo(b.toString()));
        System.out.println(list);//执行结果:[aa, bb, cc, dd, ee]
        //降序
        list.sort((a, b) -> b.compareTo(a.toString()));
        System.out.println(list);//执行结果:[ee, dd, cc, bb, aa]
    }
4.4.2、对象字段排序
	class Person {
        private String name;
        private int age;

        public Person() {
        }

        public Person(String name, Integer age) {
            this.name = name;
            this.age = age;
        }

        public String getName() {
            return name;
        }

        public void setName(String name) {
            this.name = name;
        }

        public int getAge() {
            return age;
        }

        public void setAge(int age) {
            this.age = age;
        }

        @Override
        public String toString() {
            return "Person{" +
                    "name='" + name + '\'' +
                    ", age=" + age +
                    '}';
        }
    }
	/**
     * 对象串排序
     */
    public void testObjectSort() {
        List<Person> list = new ArrayList<>();
        list.add(new Person("三炮", 48));
        list.add(new Person("老王", 35));
        list.add(new Person("小明", 8));
        list.add(new Person("叫兽", 70));
        System.out.println(list); //执行结果:[Person{name='三炮', age=48}, Person{name='老王', age=35}, Person{name='小明', age=8}, Person{name='叫兽', age=70}]
        //按年龄升序
        list.sort((a, b) -> Integer.compare(a.age, b.getAge()));
        System.out.println(list);//执行结果:[Person{name='小明', age=8}, Person{name='老王', age=35}, Person{name='三炮', age=48}, Person{name='叫兽', age=70}]
        //按年龄降序
        list.sort((a, b) -> Integer.compare(b.age, a.getAge()));
        System.out.println(list);//执行结果:[Person{name='叫兽', age=70}, Person{name='三炮', age=48}, Person{name='老王', age=35}, Person{name='小明', age=8}]
        //如果按姓名排序,其实就是按字符串排序一样
    }

4.5、limit(long n)

返回前 n 个元素

list = list.stream()
            .limit(2)
            .collect(Collectors.toList());
//打印输出 [Person{name='jack', age=20}, Person{name='mike', age=25}]

4.6、skip(long n)

去除前 n 个元素

list = list.stream()
            .skip(2)
            .collect(Collectors.toList());

//打印输出 [Person{name='tom', age=30}]

tips:

  • skip(m)用在 limit(n) 前面时,先去除前 m 个元素再返回剩余元素的前 n 个元素。
  • limit(n) 用在 skip(m) 前面时,先返回前 n 个元素再在剩余的 n 个元素中去除 m 个元素。
list = list.stream()
            .limit(2)
            .skip(1)
            .collect(Collectors.toList());

//打印输出 [Person{name='mike', age=25}]

4.7、map(T -> R)

将流中的每一个元素 T 映射为 R(类似类型转换)

List<String> newlist = list.stream().map(Person::getName).collect(Collectors.toList());

newlist 里面的元素为 list 中每一个 Person 对象的 name 变量。

4.8、flatMap(T -> Stream)

将流中的每一个元素 T 映射为一个流,再把每一个流连接成为一个流。

List<String> list = new ArrayList<>();
list.add("aaa bbb ccc");
list.add("ddd eee fff");
list.add("ggg hhh iii");

list = list.stream().map(s -> s.split(" ")).flatMap(Arrays::stream).collect(toList());

上面例子中,我们的目的是把 List 中每个字符串元素以” “分割开,变成一个新的 List。
首先 map 方法分割每个字符串元素,但此时流的类型为 Stream。

4.9、anyMatch(T -> boolean)

流中是否有一个元素匹配给定的 T -> boolean 条件

是否存在一个 person 对象的 age 等于 20:

boolean b = list.stream().anyMatch(person -> person.getAge() == 20);

4.10、 allMatch(T -> boolean)

流中是否所有元素都匹配给定的 T -> boolean 条件

boolean result = list.stream().allMatch(Person::isStudent);

4.11、noneMatch(T -> boolean)

流中是否没有元素匹配给定的 T -> boolean 条件

boolean result = list.stream().noneMatch(Person::isStudent);

4.12、findAny() 和 findFirst()

  • findAny():找到其中一个元素 (使用 stream() 时找到的是第一个元素;使用 parallelStream()并行时找到的是其中一个元素)
  • findFirst():找到第一个元素

值得注意的是,这两个方法返回的是一个 Optional 对象,它是一个容器类,能代表一个值存在或不存在,这个后面会讲到。

4.13、reduce((T, T) -> T) 和 reduce(T, (T, T) -> T)

归约是将集合中的所有元素经过指定运算,折叠成一个元素输出,如:求最值、平均数等,这些操作都是将一个集合的元素折叠成一个元素输出。

在流中,reduce函数能实现归约。
reduce函数接收两个参数:

  • 初始值
  • 进行归约操作的Lambda表达式

用于组合流中的元素,如求和,求积,求最大值等

int age = list.stream().reduce(0, (person1,person2)->person1.getAge()+person2.getAge());
//计算年龄总和:
int sum = list.stream().map(Person::getAge).reduce(0, (a, b) -> a + b);

//与之相同:
int sum = list.stream().map(Person::getAge).reduce(0, Integer::sum);

其中,reduce 第一个参数 0 代表起始值为 0,lambda (a, b) -> a + b 即将两值相加产生一个新值

同样地:

//计算年龄总乘积:
int sum = list.stream().map(Person::getAge).reduce(1, (a, b) -> a * b);

当然也可以

Optional<Integer> sum = list.stream().map(Person::getAge).reduce(Integer::sum);

即不接受任何起始值,但因为没有初始值,需要考虑结果可能不存在的情况,因此返回的是 Optional 类型

4.14、count()

返回流中元素个数,结果为 long 类型。

4.15、collect()

收集方法,我们很常用的是 collect(toList()),当然还有 collect(toSet()) 等,参数是一个收集器接口,这个后面会另外讲。

4.16、forEach()

返回结果为 void,很明显我们可以通过它来干什么了,比方说:

//打印各个元素:
list.stream().forEach(System.out::println);

再比如说 MyBatis 里面访问数据库的 mapper 方法:

//向数据库插入新元素:
list.stream().forEach(PersonMapper::insertPerson);

4.16、unordered()

还有这个比较不起眼的方法,返回一个等效的无序流,当然如果流本身就是无序的话,那可能就会直接返回其本身

五、数值流

前面介绍的如
int sum = list.stream().map(Person::getAge).reduce(0, Integer::sum); 计算元素总和的方法其中暗含了装箱成本,map(Person::getAge) 方法过后流变成了 Stream 类型,而每个 Integer 都要拆箱成一个原始类型再进行 sum 方法求和,这样大大影响了效率。

针对这个问题 Java 8 有良心地引入了数值流 IntStream, DoubleStream, LongStream,这种流中的元素都是原始数据类型,分别是 int,double,long

5.1、流与数值流的转换

流转换为数值流

  • mapToInt(T -> int) : return IntStream
  • mapToDouble(T -> double) : return DoubleStream
  • mapToLong(T -> long) : return LongStream
IntStream intStream = list.stream().mapToInt(Person::getAge);

当然如果是下面这样便会出错

LongStream longStream = list.stream().mapToInt(Person::getAge);

因为 getAge 方法返回的是 int 类型(返回的如果是 Integer,一样可以转换为 IntStream)

数值流转换为流

很简单,就一个 boxed

Stream<Integer> stream = intStream.boxed();

5.2、数值流方法

下面这些方法作用不用多说,看名字就知道:

  • sum()
  • max()
  • min()
  • average() 等…

5.3、数值范围

IntStream 与 LongStream 拥有 range 和 rangeClosed 方法用于数值范围处理。

  • IntStream : rangeClosed(int, int) / range(int, int)
  • LongStream : rangeClosed(long, long) / range(long, long)

这两个方法的区别在于一个是闭区间,一个是半开半闭区间:

  • rangeClosed(1, 100) :[1, 100]
  • range(1, 100) :[1, 100)

我们可以利用 IntStream.rangeClosed(1, 100) 生成 1 到 100 的数值流。

//求 1 到 10 的数值总和:
IntStream intStream = IntStream.rangeClosed(1, 10);
int sum = intStream.sum();

六、 Optional 类

NullPointerException 可以说是每一个 Java 程序员都非常讨厌看到的一个词,针对这个问题, Java 8 引入了一个新的容器类 Optional,可以代表一个值存在或不存在,这样就不用返回容易出问题的 null。之前文章的代码中就经常出现这个类,也是针对这个问题进行的改进。

Optional 类比较常用的几个方法有:

  • isPresent() :值存在时返回 true,反之 flase
  • get() :返回当前值,若值不存在会抛出异常
  • orElse(T) :值存在时返回该值,否则返回 T 的值

Optional 类还有三个特化版本 OptionalInt,OptionalLong,OptionalDouble,刚刚讲到的数值流中的 max 方法返回的类型便是这个

Optional 类其中其实还有很多学问,讲解它说不定也要开一篇文章,这里先讲那么多,先知道基本怎么用就可以。

七、构建流

之前我们得到一个流是通过一个原始数据源转换而来,其实我们还可以直接构建得到流。

7.1、值创建流

  • Stream.of(T…) : Stream.of(“aa”, “bb”) 生成流
//生成一个字符串流
Stream<String> stream = Stream.of("aaa", "bbb", "ccc");
  • Stream.empty() : 生成空流

7.2、数组创建流

根据参数的数组类型创建对应的流:

  • Arrays.stream(T[ ])
  • Arrays.stream(int[ ])
  • Arrays.stream(double[ ])
  • Arrays.stream(long[ ])

值得注意的是,还可以规定只取数组的某部分,用到的是Arrays.stream(T[], int, int)

只取索引第 1 到第 2 位的:
int[] a = {1, 2, 3, 4};
Arrays.stream(a, 1, 3).forEach(System.out :: println);

//打印 2 ,3

7.3、文件生成流

Stream<String> stream = Files.lines(Paths.get("data.txt"));

每个元素是给定文件的其中一行

7.4、函数生成流

两个方法:

  • iterate : 依次对每个新生成的值应用函数
  • generate :接受一个函数,生成一个新的值
Stream.iterate(0, n -> n + 2)
//生成流,首元素为 0,之后依次加 2

Stream.generate(Math :: random)
//生成流,为 0 到 1 的随机双精度数

Stream.generate(() -> 1)
//生成流,元素全为 1

八、collect 收集数据

coollect 方法作为终端操作,接受的是一个 Collector 接口参数,能对数据进行一些收集归总操作

8.1、收集

最常用的方法,把流中所有元素收集到一个 List, Set 或 Collection 中

  • toList
  • toSet
  • toCollection
List newlist = list.stream.collect(toList());

8.2、汇总

(1)counting

用于计算总和:
(推荐第二种)

long l = list.stream().collect(counting());

没错,你应该想到了,下面这样也可以:

long l = list.stream().count();

(2)summingInt ,summingLong ,summingDouble

summing,没错,也是计算总和,不过这里需要一个函数参数

计算 Person 年龄总和:
(推荐第二种)

int sum = list.stream().collect(summingInt(Person::getAge));

当然,这个可以也简化为:

int sum = list.stream().mapToInt(Person::getAge).sum();

除了上面两种,其实还可以:

int sum = list.stream().map(Person::getAge).reduce(Interger::sum).get();

由此可见,函数式编程通常提供了多种方式来完成同一种操作

(3)averagingInt,averagingLong,averagingDouble

看名字就知道,求平均数

Double average = list.stream().collect(averagingInt(Person::getAge));

当然也可以这样写

OptionalDouble average = list.stream().mapToInt(Person::getAge).average();

不过要注意的是,这两种返回的值是不同类型的

(4)summarizingInt,summarizingLong,summarizingDouble

这三个方法比较特殊,比如 summarizingInt 会返回 IntSummaryStatistics 类型

IntSummaryStatistics l = list.stream().collect(summarizingInt(Person::getAge));

IntSummaryStatistics 包含了计算出来的平均值,总数,总和,最值,可以通过下面这些方法获得相应的数据
在这里插入图片描述

8.1、取最值

maxBy,minBy 两个方法,需要一个 Comparator 接口作为参数

Optional<Person> optional = list.stream().collect(maxBy(comparing(Person::getAge)));

我们也可以直接使用 max 方法获得同样的结果

Optional<Person> optional = list.stream().max(comparing(Person::getAge));

8.4、joining 连接字符串

也是一个比较常用的方法,对流里面的字符串元素进行连接,其底层实现用的是专门用于字符串连接的 StringBuilder

String s = list.stream().map(Person::getName).collect(joining());

//结果:jackmiketom
String s = list.stream().map(Person::getName).collect(joining(","));

//结果:jack,mike,tom

joining 还有一个比较特别的重载方法:

String s = list.stream().map(Person::getName).collect(joining(" and ", "Today ", " play games."));

//结果:Today jack and mike and tom play games.

即 Today 放开头,play games. 放结尾,and 在中间连接各个字符串

8.5、groupingBy 分组

groupingBy 用于将数据分组,最终返回一个 Map 类型

Map<Integer, List<Person>> map = list.stream().collect(groupingBy(Person::getAge));

例子中我们按照年龄 age 分组,每一个 Person 对象中年龄相同的归为一组。

Map<String,List<Person>> result = list.stream()
                                    .collect(Collectors.groupingby((person)->{
        if(person.getAge()>60)
            return "老年人";
        else if(person.getAge()>40)
            return "中年人";
        else
            return "青年人";
                                    }));

另外可以看出,Person::getAge 决定 Map 的键(Integer 类型),list 类型决定 Map 的值(List 类型)

多级分组

groupingBy 可以接受一个第二参数实现多级分组:

Map<Integer, Map<T, List<Person>>> map = list.stream().collect(groupingBy(Person::getAge, groupBy(...)));

其中返回的 Map 键为 Integer 类型,值为 Map

按组收集数据

Map<Integer, Integer> map = list.stream().collect(groupingBy(Person::getAge, summingInt(Person::getAge)));

该例子中,我们通过年龄进行分组,然后 summingInt(Person::getAge)) 分别计算每一组的年龄总和(Integer),最终返回一个 Map

groupingBy(Person::getAge)

其实等同于:

groupingBy(Person::getAge, toList())

8.6、partitioningBy 分区

分区与分组的区别在于,分区是按照 true 和 false 来分的,因此partitioningBy 接受的参数的 lambda 也是 T -> boolean

//根据年龄是否小于等于20来分区
Map<Boolean, List<Person>> map = list.stream()
                                     .collect(partitioningBy(p -> p.getAge() <= 20));

//打印输出
{
    false=[Person{name='mike', age=25}, Person{name='tom', age=30}], 
    true=[Person{name='jack', age=20}]
}

同样地 partitioningBy 也可以添加一个收集器作为第二参数,进行类似 groupBy 的多重分区等等操作。

九、并行

我们通过 list.stream() 将 List 类型转换为流类型,我们还可以通过 list.parallelStream() 转换为并行流。

并行流就是把内容分成多个数据块,使用不同的线程分别处理每个数据块的流。这也是流的一大特点,要知道,在 Java 7 之前,并行处理数据集合是非常麻烦的,你得自己去将数据分割开,自己去分配线程,必要时还要确保同步避免竞争。

Stream 让程序员能够比较轻易地实现对数据集合的并行处理,但要注意的是,不是所有情况的适合,有些时候并行甚至比顺序进行效率更低,而有时候因为线程安全问题,还可能导致数据的处理错误,因此并行的性能问题非常值得我们思考。

比方说下面这个例子

 int i = Stream.iterate(1, a -> a + 1).limit(100).parallel().reduce(0, Integer::sum);

我们通过这样一行代码来计算 1 到 100 的所有数的和,我们使用了 parallel 来实现并行。

但实际上是,这样的计算,效率是非常低的,比不使用并行还低!一方面是因为装箱问题,这个前面也提到过,就不再赘述,还有一方面就是 iterate 方法很难把这些数分成多个独立块来并行执行,因此无形之中降低了效率。

流的可分解性

这就说到流的可分解性问题了,使用并行的时候,我们要注意流背后的数据结构是否易于分解。比如众所周知的 ArrayList 和 LinkedList,明显前者在分解方面占优。

我们来看看一些数据源的可分解性情况

数据源可分解性
ArrayList极佳
LinkedList
IntStream.range极佳
Stream.iterate
HashSet
TreeSet

顺序性

除了可分解性,和刚刚提到的装箱问题,还有一点值得注意的是一些操作本身在并行流上的性能就比顺序流要差,比如:limit,findFirst,因为这两个方法会考虑元素的顺序性,而并行本身就是违背顺序性的,也是因为如此 findAny 一般比 findFirst 的效率要高。

团队液体流查看器(叉) 这是原始Team Liquid Streams扩展的分支,因为旧的开发人员没有维护/改进现有的扩展。 请在下面的评论部分或以下主题的线程中提出功能请求:http://www.teamliquid.net/forum/viewmessage.php?topic_id=205002 Team Liquid Streams提供了一种在TeamLiquid上观看与Starcraft相关的流的简便方法.net它会保持最新列表,列出当前正在流媒体的人以及正在观看该流媒体的观众人数。 您还可以收藏要在每次再次开始广播时引起弹出式通知的流光。 您可以按收藏夹,视频流名称,是否推荐,视频流类型,种族和观众数进行排序。 现在,该扩展名还按用户名而不是所有者列出流,与TeamLiquid.net上的外观匹配。此外,现在还有一个选项屏幕,用于手动编辑您的收藏夹和隐藏列。 更新:现在启用了流类型过滤。0.0.2.0版:现在启用了竞赛过滤。0.0.2.5版:添加了一次手动刷新。 刷新是每10分钟一次,此外每10分钟会发生一次自动刷新。 0.0.2.6版:修复了没有种族选择的流不会显示的错误。 0.0.2.7版:修复了导致隐藏列时列无法正确移动的错误。 -由Bone_Idle版本0.0.2.8报告:修复了由于标签更改而导致“杂项”流不再显示的错误。版本0.0.2.9:添加了“语言等级”列。 默认情况下不显示,并显示所有三种评级类型。 0.0.3.0版:已更新以添加新类型(Dota2和LoL)。 选项页面应该可以再次正常工作。 0.0.3.1版:在选择过滤类型时,类型选择下拉列表保存用于节制的过滤器选择。0.0.3.4版:D3更新0.0.4.0版:HotS和WoL更新用户请求的通知警报现在在选项菜单0.0.4.1版中切换: HotS和WoL的更新-> SC2 Chrome版本0.0.5.0的更新清单版本:输入EoL,因为我目前没有时间维护此版本,并且Google的新Chrome req需要大量重写。 支持语言:English
评论 6
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值