转载记录java8 stream用法

文章目录
Stream基础概念

  1. 分类
  2. 常用方法
    2.1 forEach
    2.2 filter
    2.3 distinct
    2.4 Collectors - (Collector工具库)
    2.4.1 Collectors.toList()方法将Stream转化为List对象
    2.4.2 Collectors.toSet()方法将Stream转化为Set对象
    2.4.3 Collectors.toMap()方法将Stream转化为Map对象
    2.4.4 统计
    2.4.5 分组
    2.4.6 joining(连接)
    2.4.7 reducing (规约)
    2.5 map、flatMap
    2.7 limit
    2.8 skip
    2.9 sorted
    2.10 统计结果收集器
    2.11 流的终止操作
  3. 创建流的两种方式
    Stream基础概念
    Stream流是 Java8 API 新增的一个处理集合的关键抽象概念,是一个来自数据源的元素队列并支持聚合操作。以指定你希望对集合进行的操作,可以执行非常复杂的查找、过滤和映射数据等操作。使用Stream API 对集合数据进行操作,就类似于使用 SQL 执行的数据库查询。也可以使用 Stream API 来并行执行操作。简而言之,Stream API 提供了一种高效且易于使用的处理数据的方式。

相关名词 描述
元素 对象形成的一个队列。 Java中的Stream并不会存储元素,而是按需计算。
数据源 是流Stream的来源。 可以是集合、数组、I/O channel、 产生器generator 等
聚合操作 类似SQL语句一样的操作,比如filter, map, reduce, find, match, sorted等。
内部迭代 中间操作都会返回流对象本身。 这样多个操作可以串联成一个管道, 如同流式风格(fluent style)。 这样做可以对操作进行优化, 比如延迟执行(laziness)和短路( short-circuiting)。
Pipelining 以前对集合遍历都是通过Iterator或者For-Each的方式, 显式的在集合外部进行迭代, 这叫做外部迭代。 Stream提供了内部迭代的方式, 通过访问者模式(Visitor)实现。
Stream的特点:

Stream不是什么数据结构,它不会保存数据,只是将操作的数据结果保存到另一个对象中。
Stream是惰性求值的(延迟执行),在中间处理的过程中,只对操作进行记录,并不会立即执行,只有等到执行终止操作的时候才会进行实际的计算,这时候中间操作才会执行。
可以把Stream当成一个高级版本的Iterator来使用。(原始版本的Iterator,只能一个个的遍历操作)

  1. 分类
    Stream操作分类 作用描述 常用方法
    无状态 中间操作, 该操作不受之前元素的影响 unordered、filter、map、mapToInt、mapToLong、mapToDouble、flatMap、flatMapToInt、flatMapToLongg、flatMapToDouble、peek
    有状态 中间操作,该操作只有拿到所有元素之后才能继续执行 distinct、sorted、limit、skip
    非短路操作 结束操作 , 必须处理所有元素才能得到最终结果 forEach、forEachOrdered、toArray、collect、max、min、count、reduce
    短路操作 结束操作 , 遇到某些符合条件的元素就可以得到最终结果 anyMatch、allMatch、noneMatch、findFirsh、findAny
  2. 常用方法
    初始化一个String类型的List集合

List stringList = Arrays.asList(“a”,“b”, “”, “”, “c”, “”, “a”, “d”,“e”,“a”);
System.out.println(“字符串集合:” + stringList);
1
2
运行结果:

字符串集合:[a, b, , , c, , a, d, e, a]

创建JavaBean对象User,初始化一个User列表

@Data
class User{
String name;
String description;
}
List userList = new ArrayList<>(
Arrays.asList(
new User(“strive”, “努力”),
new User(“fighter”, “奋斗”),
new User(“lucky”, “幸运”),
new User(“lucky”, “幸运222”)
)
);
1
2
3
4
5
6
7
8
9
10
11
12
13
运行结果:

[User{name=‘strive’, description=‘努力’}, User{name=‘fighter’, description=‘奋斗’}, User{name=‘lucky’, description=‘幸运’}, User{name=‘lucky’, description=‘幸运222’}]

2.1 forEach
forEach方法用于迭代遍历每个数据

//迭代遍历输出
stringList.forEach(str ->{
System.out.print(str + " ");
});
1
2
3
4
运行结果:

a b c a d e a

2.2 filter
使用 filter 按照设置的条件过滤元素,得到满足条件的元素。
在这里插入图片描述

//获取stringList中非空字符串的集合
List collect = stringList.stream().filter(str -> !str.isEmpty())
.collect(Collectors.toList());
System.out.println(“非空字符串集合:” + collect);
1
2
3
4
运行结果:

非空字符串集合:[a, b, c, a, d, e, a]

count() 方法,用来统计数量

long emptyStrNum = stringList.stream().filter(str -> str.isEmpty()).count();
System.out.println("空字符串数量 = " + emptyStrNum);
1
2
运行结果:

空字符串数量 = 3

2.3 distinct
distinct() 方法用于去重
在这里插入图片描述

//获取stringList中非空字符串的集合 去重后 转化为list集合
List collect3 = stringList.stream().filter(str -> !str.isEmpty()).distinct()
.collect(Collectors.toList());
System.out.println(“去重后字符串集合:” + collect3);

//提取出userList对象中的属性name并去重
List nameList = userList.stream().map(User::getName).distinct().collect(Collectors.toList());
System.out.println("nameList = " + nameList);
1
2
3
4
5
6
7
8
9
运行结果:

去重后字符串集合:[a, b, c, d, e]

nameList = [strive, fighter, lucky]

2.4 Collectors - (Collector工具库)
Collectors 类中实现了很多的规约操作(可用于返回列表或字符串)

最常用的是将流转换为 集合或聚合元素对象

2.4.1 Collectors.toList()方法将Stream转化为List对象
//查找非空、去重后通过 Collectors.toList() 转化为List列表
List strList = stringList.stream().filter(str -> !str.isEmpty())
.distinct().collect(Collectors.toList());
System.out.println("strList = " + strList);
1
2
3
4
运行结果:

strList = [a, b, c, d, e]

2.4.2 Collectors.toSet()方法将Stream转化为Set对象
//通过 Collectors.toSet() 方法转化为set列表
//set集合,不去重也输出相同的结果(set中不会有重复的元素)
Set strSet = stringList.stream().filter(str -> !str.isEmpty())
.collect(Collectors.toSet());
System.out.println("strSet = " + strList);
1
2
3
4
5
运行结果:

strSet = [a, b, c, d, e]

2.4.3 Collectors.toMap()方法将Stream转化为Map对象
Collector<T, ?, M> toMap(Function<? super T, ? extends K> keyMapper, Function<? super T, ? extends U> valueMapper,BinaryOperator mergeFunction,Supplier mapSupplier)
1
参数1: keyMapper 用来生成key值的。

参数2: valueMapper 用来生成value值的。

参数3: mergeFunction 用在key值冲突的情况下使用(可省略),如果新元素产生的key在Map中已经出现过了,第三个参数就会定义解决的办法。

参数4:mapSupplier 默认返回的map类型为hashMap,可以按自己的需要自己返回不同的map实现。( 可省略 )

主要举个3个参数的案例来说明:

Collectors.toMap(keyMapper, valueMapper, mergeFunction)
1
该toMap()方法有三个参数.

比如 collect Collectors.toMap(User::getName, i -> i, (v1, v2) -> v1)

第一个参数 User::getName 表示选择User对象的的getName方法获取的值作为map的key值;

第二个参数 i -> i 表示选择将原来的对象作为map的value值(这里的i只是对遍历对象取的别名)

第三个参数 (v1, v2) -> v1,当出现key值相同时(也就是如果v1与v2的key值相同),选择前面的 也就是v1 作为那个key所对应的值,**就是出现相同key时,谁覆盖谁的问题 **

案例:

创建一个Javabean对象User

@Data
class User{
String name;
String description;
}
1
2
3
4
5
A. 使用Collectors.toMap(keyMapper, valueMapper)两个参数来将List转化为Map

//初始化一个User的List列表
List list = new ArrayList<>(
Arrays.asList(
new User(“strive”, “努力”),
new User(“fighter”, “奋斗”),
new User(“lucky”, “幸运”),

    )

);
//Collectors.toMap()方法,将List转化为Map集合
//Map<String, String> map = list.stream().collect(Collectors.toMap(item -> item.getName(), item -> item.getDescription()));
//简写为下面这种方式
Map<String, User> map = list.stream().collect(Collectors.toMap(User::getName, i -> i));
System.out.println(map);
1
2
3
4
5
6
7
8
9
10
11
12
13
14
运行结果:

{lucky=User{name=‘lucky’, description=‘幸运’}, strive=User{name=‘strive’, description=‘努力’}, fighter=User{name=‘fighter’, description=‘奋斗’}}

B. 如果出现key相同,但是没有设置第三个参数对其进行处理,就会报错 IllegalStateException:

	//初始化一个User的List列表,里面有两个lucky
    List<User> list = new ArrayList<>(
            Arrays.asList(
                    new User("strive", "努力"),
                    new User("fighter", "奋斗"),
                    new User("lucky",  "幸运"),
                    new User("lucky",  "幸运222")
            )
    );	
    Map<String, User> map = list.stream().collect(Collectors.toMap(User::getName, i -> i));
    System.out.println(map);

1
2
3
4
5
6
7
8
9
10
11
运行结果:

报错:java.lang.IllegalStateException: Duplicate key User{name=‘lucky’, description=‘幸运’}

因为按照User的name转换有两个相同的key值lucky,没有选择处理方式报错:IllegalStateException

C. 设置第三个参数,解决出现相同key时,谁覆盖谁的问题

    List<User> list = new ArrayList<>(
            Arrays.asList(
                    new User("strive", "努力"),
                    new User("fighter", "奋斗"),
                    new User("lucky",  "幸运"),
                    new User("lucky",  "幸运222")
            )
    );
	//设置当key相同的时候,保留前面的 【如果为 (v1,v2)->v2 就是用新的覆盖旧的】
    Map<String, User> map = list.stream().collect(Collectors.toMap(User::getName, i -> i, (v1,v2) -> v1));
    System.out.println(map);

1
2
3
4
5
6
7
8
9
10
11
运行结果:

{lucky=User{name=‘lucky’, description=‘幸运’}, strive=User{name=‘strive’, description=‘努力’}, fighter=User{name=‘fighter’, description=‘奋斗’}}

2.4.4 统计
count获取集合数量

List stringList = Arrays.asList(“a”,“b”, “”, “”, “c”, “”, “a”, “d”,“e”,“a”);
//获取集合StringList的元素数量
//Collectors.counting()
stringList.stream().collect(Collectors.counting());
//简写为count()
stringList.stream().count();
//等价于集合的size方法
(long) stringList.size();
1
2
3
4
5
6
7
8
运行结果:

三种结果最终的效果相同

10

10

10

求平均值:averagingInt、averagingLong、averagingDouble
求最大/最小值:maxBy、minBy
统计求和:summingInt、summingLong、summingDouble
统计所有(包括计数、求和、最小、最大、平均):summarizingInt、summarizingLong、summarizingDouble

	List<Integer> intList = Arrays.asList(2, 3, 10, 6, 8, 5, 2, 9);

    // maxBy 求最大值  --  等价于max
    Optional<Integer> maxBy = intList.stream().collect(Collectors.maxBy(Integer::compareTo));
    // minBy 求最大值  --  等价于min
    Optional<Integer> minBy = intList.stream().collect(Collectors.minBy(Integer::compareTo));
    // averagingInt 求平均值
    Double averagingInt = intList.stream().collect(Collectors.averagingInt(i -> i));
    // 求和summingInt(需要求和的参数) -- 等价于 mapToInt
    Integer summingInt = intList.stream().collect(Collectors.summingInt(i -> i));
    //summarizingInt 统计数目、求和、最小值、平均值、最大值
    IntSummaryStatistics summarizingInt = intList.stream().collect(Collectors.summarizingInt(i -> i));

    System.out.println("maxBy = " + maxBy);
    System.out.println("minBy = " + minBy);
    System.out.println("averagingInt = " + averagingInt);
    System.out.println("summingInt = " + summingInt);
    System.out.println("summarizingInt = " + summarizingInt);

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
运行结果:

maxBy = Optional[10]
minBy = Optional[2]
averagingInt = 5.625
summingInt = 45
summarizingInt = IntSummaryStatistics{count=8, sum=45, min=2, average=5.625000, max=10}

2.4.5 分组
partitioningBy(分区):按照条件分为两个Map<Boolean, List>,一个是满足条件的Map和一个不满足条件的Map。
groupingBy(分组):类似于分区,但是是将集合按照条件分为多个Map,可以对进行分组之后的结果再分组

List intList = Arrays.asList(2, 3, 10, 6, 8, 5, 2, 9);
//按 >5 分为两个区间
Map<Boolean, List> partitioningBy = intList.stream().collect(Collectors.partitioningBy(i -> i > 5));
//按 >5 分为两组
Map<Boolean, List> groupingBy = intList.stream().collect(Collectors.groupingBy(i -> i > 5));
//先按 >5 分为两组,然后再在前面分组满足条件的基础上对(满足条件的集合)再对 >8 进行分组
Map<Boolean, Map<Boolean, List>> groupingBy2 = intList.stream().collect(Collectors.groupingBy(i -> i > 5, Collectors.groupingBy(i -> i > 8)));
System.out.println("分区partitioningBy = " + partitioningBy);
System.out.println("分组groupingBy = " + groupingBy);
System.out.println("两次分组groupingBy2 = " + groupingBy2);

1
2
3
4
5
6
7
8
9
10
11
运行结果:

分区partitioningBy = {false=[2, 3, 5, 2], true=[10, 6, 8, 9]}
分组groupingBy = {false=[2, 3, 5, 2], true=[10, 6, 8, 9]}
两次分组groupingBy2 = {false={false=[2, 3, 5, 2]}, true={false=[6, 8], true=[10, 9]}}

2.4.6 joining(连接)
joining(条件):可以将stream中的元素用特定的连接符(没有的话,则直接连接)连接成一个新的字符串。

    List<String> stringList2 = Arrays.asList("a","b", " ", " ", "c", " ", "a", "d","e","a");
    String joining = stringList2.stream().collect(Collectors.joining("-"));
    System.out.println("joining = " + joining);

1
2
3
运行结果:

joining = a-b- - -c- -a-d-e-a

2.4.7 reducing (规约)
reducing(U identity, Function<? super T, ? extends U> mapper, BinaryOperator op)
1
reducing:Collectors类提供的reducing方法,类似于stream本身的reduce方法,但是增加了对自定义归约的支持。
参数 BinaryOperator: 这是一个函数式接口,是给两个相同类型的量,返回一个跟这两个量相同类型的一个结果,伪表达式为 (T,T) -> T。默认给了两个实现 maxBy 和 minBy ,根据比较器来比较大小并分别返回最大值或者最小值。当然你也可以灵活定制。然后 reducing 就很好理解了,元素两两之间进行比较根据策略淘汰一个,随着轮次的进行元素个数就是 reduce 的。

    List<Integer> intList2 = Arrays.asList(1, 2, 3);
    Integer reducing = intList2.stream().collect(Collectors.reducing(100, i -> i, (v1, v2) -> (v1 + v2 - 1)));
    System.out.println("reducing = " + reducing);

1
2
3
运行结果:

reducing = 103

Stream提供的reduce方法也有类似的作用

    // stream的reduce  --  T reduce(T identity, BinaryOperator<T> accumulator);
    //参1:(identity):求出结果之后再加该值
    Optional<Integer> reduce = Optional.ofNullable(intList2.stream().reduce(100, Integer::sum));
    System.out.println("reduce = " + reduce.get());

1
2
3
4
运行结果:

reduce = 106

Optional reduce = Optional.ofNullable(intList2.stream().reduce(100, Integer::sum, (v1, v2) -> (v1 + v2 - 1)));
System.out.println("reduce = " + reduce.get());
1
2
2.5 map、flatMap
map方法用于 映射每个元素到对应的结果,该函数会被应用到每个元素上,并将其映射成一个新的元素。
在这里插入图片描述

flatMap方法用于 接收一个函数作为参数,将流中的每个值都换成另一个流,然后把所有流连接成一个流。
在这里插入图片描述

//使用 map 输出stringList中元素对应的两倍并去重
List doubleStr = stringList.stream().map(str -> str += str).distinct()
.collect(Collectors.toList());
System.out.print(doubleStr + " ");
1
2
3
4
运行结果:

[aa, bb, , cc, dd, ee]

//通过map获取userList列表中的User对象的name属性组成一个list列表
List nameList = list.stream().map(user -> user.getName()).distinct()
.collect(Collectors.toList());
System.out.println(nameList);
1
2
3
4
运行结果:

[strive, fighter, lucky]

//flagMap转换为流后再转化为list列表输出
List s = stringList.stream().flatMap(s -> {
//将每个元素按照分隔符,转换成一个stream
String[] split = s.split(",");
return Arrays.stream(split);
}).collect(Collectors.toList());
System.out.println(s);
1
2
3
4
5
6
7
运行结果:

[a, b, , , c, , a, d, e, a]

2.6 peek

peek 方法(消费),类似于map,能得到流中的每一个元素。但map接收的是一个Function表达式,有返回值;而peek接收的是Consumer表达式,是没有返回值的。
在这里插入图片描述

//peek消费,去重输出
stringList.stream().peek(i -> i += i).distinct().forEach(i-> System.out.print(i + " "));
1
2
输出结果:

a b c d e

2.7 limit
limit(n) 方法用于获取指定数量 (n个) 的流
在这里插入图片描述

Random random = new Random();
// 通过 limit 输出3个随机数
random.ints().limit(3).forEach(r ->{
System.out.print(r + " ");
});
1
2
3
4
5
运行结果:

1677393552 -608502510 2060923188

// 三个参数:random.ints(生成数量,最小值,最大值)
//生成10个1-100之间的随机数,再通过limit取出5个输出
random.ints(10,1,100).limit(5).forEach(r ->{
System.out.print(r + " ");
});
1
2
3
4
5
运行结果:

7 85 73 60 49

2.8 skip
skip(n) 方法用于跳过 (n个) 元素,配合 limit(n) 可实现分页
在这里插入图片描述

	List<Integer> numbers = Arrays.asList(0, 9 ,6 , 5, 6, 3, 2, 1, 12, 5, 8, 3, 9, 3);
	//过滤得到集合中>1的元素,然后去重,跳过前2个,然后取出5个
    List<Integer> collect = numbers.stream().filter(i -> i > 1)
            .distinct()
            .skip(2)
            .limit(5).collect(Collectors.toList());
    System.out.println(collect);

1
2
3
4
5
6
7
运行结果:

[5, 3, 2, 12, 8]

2.9 sorted
sorted() 方法用于对流进行排序,这是自然排序,流中元素需实现Comparable接口。

sorted(Comparator com) : 带参数是定制排序,使用自定义的Comparator排序器进行排序

Random random = new Random();
//两个参数:random.ints(最小值,最大值)
//随机生成0-1000的随机数,通过limit取8个,然后排序输出
random.ints(0,1000)
.limit(8).sorted().forEach(i ->{
System.out.print(i + " ");
});
1
2
3
4
5
6
7
运行结果:

86 192 232 552 560 776 928 929

2.10 统计结果收集器
比如:getCount、getMax、getMin、getSum、getAverage等等用于统计结果的收集器,主要用于int、double、long等基本类型上

//定义一个int类型的集合
List numbers = Arrays.asList(6, 2, 1, 2, 5, 8, 3, 9);
//通过mapToInt转化
IntSummaryStatistics intStatus = numbers.stream().mapToInt((i) -> i).summaryStatistics();

System.out.println(“列表中元素数量:” + intStatus.getCount());
System.out.println("列表中最大数 : " + intStatus.getMax());
System.out.println("列表中最小数 : " + intStatus.getMin());
System.out.println("所有数之和 : " + intStatus.getSum());
System.out.println("平均值 : " + intStatus.getAverage());
1
2
3
4
5
6
7
8
9
10
运行结果:

列表中元素数量:8
列表中最大数 : 9
列表中最小数 : 1
所有数之和 : 36
平均值 : 4.5

2.11 流的终止操作
方法名称 描述
count 返回流中元素的总个数
max 返回流中元素最大值
min 返回流中元素最小值
findFirst 返回流中第一个元素
findAny 返回流中第一个元素(随机)
allMatch 接收一个 Predicate 函数,当流中每个元素都符合该断言时才返回true,否则返回false
noneMatch 接收一个 Predicate 函数,当流中每个元素都不符合该断言时才返回true,否则返回false
anyMatch 接收一个 Predicate 函数,只要流中有一个元素满足该断言则返回true,否则返回false
List intList = Arrays.asList(2, 3, 10, 6, 8, 5, 2, 9);

    long count = intList.stream().count();// 等价于intList.size()
    Integer max = intList.stream().max(Integer::compareTo).get();
    Integer min = intList.stream().min(Integer::compareTo).get();
    Integer findFirst = intList.stream().findFirst().get();
    Integer findAny = intList.stream().findAny().get();
    boolean allMatch = intList.stream().allMatch(i -> i > 1);
    boolean noneMatch = intList.stream().noneMatch(i -> i > 1);
    boolean anyMatch = intList.stream().anyMatch(i -> i > 5);

1
2
3
4
5
6
7
8
9
10
运行结果:

count = 8
max = 10
min = 2
findFirst = 2
findAny = 2
allMatch = true
noneMatch = false
anyMatch = true

图片来自:https://ifeve.com/stream/

  1. 创建流的两种方式
    创建流的方式 描述
    stream 为集合创建串行流
    parallelStream 为集合创建并行流
    parallelStream里面的执行是异步的,并且使用的线程池是 ForkJoinPool.common,可以通过设置 -Djava.util.concurrent.ForkJoinPool.common.parallelism = N 来调整线程池的大小。可能提高你的多线程任务的速度。

     List<Integer> intList = Arrays.asList(1, 2, 3, 4, 5, 6);
     System.out.print("stream串行:");
     intList.stream().forEach(System.out::print);
     System.out.print("\nparallelStream并行:");
     intList.parallelStream().forEach(System.out::print);
    

1
2
3
4
5
三次运行结果:

第一次运行:

stream串行:123456
parallelStream并行:456321

第二次运行:

stream串行:123456
parallelStream并行:451623

第三次运行:

stream串行:123456
parallelStream并行:465321

可以发现,Stream每次运行都是相同的结果,且是顺序输出的,但是parallelStream每次运行结果可能不同,顺序也是错乱的。

打印执行线程信息,查看是否是并行的

    List<Integer> intList = Arrays.asList(1, 2, 3, 4, 5, 6);
    intList.parallelStream().forEach(i -> System.out.println("线程 = " + Thread.currentThread().getName() +" : 输出 = " + i));

1
2
运行结果:

线程 = main : 输出 = 4
线程 = ForkJoinPool.commonPool-worker-2 : 输出 = 1
线程 = ForkJoinPool.commonPool-worker-2 : 输出 = 3
线程 = ForkJoinPool.commonPool-worker-2 : 输出 = 5
线程 = main : 输出 = 6
线程 = ForkJoinPool.commonPool-worker-9 : 输出 = 2

所以 parallelStream 是利用多线程并行执行的,通过 parallelStream 可以很大程度简化我们使用并发操作。

使用 parallelStream 是平行处理的,所以顺序每次都不一定一致,如果想要顺序是按照原来Stream的数据一样顺序输出,可以通过 forEachOrdered 方法实现。

    List<Integer> intList = Arrays.asList(1, 2, 3, 4, 5, 6);
    System.out.println("parallelStream并行使用forEachOrdered顺序输出:");
    intList.parallelStream().forEachOrdered(System.out::print);
    System.out.println();
    intList.parallelStream().forEachOrdered(i ->   System.out.println("线程 = " + Thread.currentThread().getName() +" : 输出 = " + i));

1
2
3
4
5
运行结果:

parallelStream并行使用forEachOrdered顺序输出:123456
线程 = ForkJoinPool.commonPool-worker-6 : 输出 = 1
线程 = ForkJoinPool.commonPool-worker-6 : 输出 = 2
线程 = ForkJoinPool.commonPool-worker-6 : 输出 = 3
线程 = ForkJoinPool.commonPool-worker-6 : 输出 = 4
线程 = ForkJoinPool.commonPool-worker-6 : 输出 = 5
线程 = ForkJoinPool.commonPool-worker-6 : 输出 = 6

所以调用 forEachOrdered 方法顺序执行的话,就不是多线程并行处理了,是一个线程进行处理。

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值