【学习笔记】JDK8函数式编程:Lambda表达式、Stream流、Optional对象

函数式编程

1 概述

1.1 作用

  • 提高代码可读性
  • 大数据量下处理集合效率高

1.2 函数式编程思想

1.2.1 概念

面向对象思想主要关注用什么对象完成什么事情。
函数式编程思想就类似于我们数学中的函数,它主要关注的是对数据进行了什么操作。

1.2.2 优点

相比面向对象,有以下优点:

  • 代码简洁,开发快速
  • 接近自然语言,易于理解
  • 易于“并发编程”(并行流)

2 Lambda表达式

2.1 概述

Lambda表达式时JDK8中的一个语法糖。他可以对某些匿名内部类的写法进行简化。它是函数式编程思想的一个重要体现。让我们不用关注是什么对象,而是关注对数据进行了怎样的操作。

注:语法糖(Syntactic sugar),也译为糖衣语法,是由英国计算机科学家彼得·约翰·兰达(Peter J. Landin)发明的一个术语,指计算机语言中添加的某种语法,这种语法对语言的功能并没有影响,但是更方便程序员使用。通常来说使用语法糖能够增加程序的可读性,从而减少程序代码出错的机会。

2.2 核心原则

可推导可省略

2.3 基本格式

(参数列表)->{代码}
例1

我们在创建线程并启动时可以使用匿名内部类的写法:

new Thread(new Runnable() {
            @Override
            public void run() {
                System.out.println("run方法执行...");
            }
        }).start();

可以时用Lambda表达式的格式对其进行修改:

new Thread(()->{
    System.out.println("run方法执行...");
}).start();
例2

在主方法中使用匿名内部类的方式调用函数calculateNum(IntBinaryOperator operator),IntBinaryOperator是一个接口,里面只有一个接口方法。

    public static void main(String[] args) {
        int num = calculateNum(new IntBinaryOperator() {
            @Override
            public int applyAsInt(int left, int right) {
                return left + right;
            }
        });
        System.out.println(num);
    }

    public static int calculateNum(IntBinaryOperator operator) {
        int a = 5;
        int b = 10;
        return operator.applyAsInt(5, 10);
    }

可以改写为Lambda表达式的形式
(int left, int right)为重写方法的参数列表,{return left + right;}为重写方法的方法体

//在IDEA中可以使用Alt+Enter将匿名内部类改写成Lambda表达式
int num = calculateNum((int left, int right)->{
            return left + right;
        });
        System.out.println(num);
例3

在主方法中使用匿名内部类的方式调用函数printNum(IntPredicate predicate),IntPredicate是一个接口,里面只有一个接口方法。

    public static void main(String[] args) {
        printNum(new IntPredicate() {
            @Override
            public boolean test(int value) {
                return value > 5;
            }
        });
    }

    public static void printNum(IntPredicate predicate) {
        int[] arr = {1,2,3,4,5,6,7,8,9,10};
        for(int i : arr) {
            if(predicate.test(i)) {
                System.out.println(i);
            }
        }
    }

改写为Lambda表达式:

        printNum((int value)->{
            return value > 5;
        });
例4

与上面的例子类似

    public static void main(String[] args) {
        Integer strLen = typeConver(new Function<String, Integer>() {
            @Override
            public Integer apply(String s) {
                return s.length();
            }
        });
        System.out.println(strLen);
    }

    public static <R> R typeConver(Function<String, R> function) {
        String str = "1235";
        R result = function.apply(str);
        return result;
    }

改写为Lambda表达式:

        Integer strLen = typeConver((String s)->{
            return s.length();
        });
例5

类似的

    public static void main(String[] args) {
        foreachArr(new IntConsumer() {
            @Override
            public void accept(int value) {
                System.out.print(value + " ");
            }
        });
    }

    
    //例5
    public static void foreachArr(IntConsumer consumer) {
        int[] arr = {1,2,3,4,5,6,7,8,9,10};
        for (int i : arr) {
            consumer.accept(i);
        }

    }

改写为Lambda表达式:

        foreachArr((int value)->{
            System.out.print(value + " ");
        });

2.4 省略规则

根据可推导可省略的核心原则,上面的Lambda表达式可以进一步省略。

  • 参数类型可以省略
  • 方法体只有一句代码时大括号return和唯一一句代码的分号可以省略
  • 方法只有一个参数时小括号可以省略

如上述的例4,可以简写为如下形式:
IDEA快捷键:Alt+Enter

    Integer strLen = typeConver(s -> s.length());

3 Stream流

3.1 概述

Java8的Stream使用的是函数式编程模式,如同它的名字一样,它可以用来对集合或数组进行链状流式的操作,可以更方便的让我们对集合或数组操作。

3.2 案例数据准备

需要在pom.xml中引入lombok坐标

    <dependencies>
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <version>1.18.16</version>
        </dependency>
    </dependencies>

Author类

@Data
@NoArgsConstructor
@AllArgsConstructor
@EqualsAndHashCode
public class Author {
    private Long id;
    private String name;
    private Integer age;
    private String intro;
    private List<Book> books;
}

Book类

@Data
@NoArgsConstructor
@AllArgsConstructor
@EqualsAndHashCode
public class Book {
    private Long id;
    private String name;
    private String category;//"小说,散文"
    private Integer score;
    private String intro;
}

测试类

public class StreamDemo {
    public static void main(String[] args) {
        
    }

    private static List<Author> getAuthors() {
        //数据初始化
        Author author1 = new Author(1L, "罗贯中", 70, "罗贯中,名本,字贯中,号湖海散人,山西并州太原府人", null);
        Author author2 = new Author(2L, "曹雪芹", 48, "曹雪芹,字梦阮,号雪芹,又号芹溪、芹圃,祖籍辽宁辽阳", null);
        Author author3 = new Author(3L, "施耐庵", 75, "施耐庵,字肇瑞,号子安,别号耐庵,生于兴化白驹镇,祖籍苏州", null);
        Author author4 = new Author(3L, "施耐庵", 75, "施耐庵,字肇瑞,号子安,别号耐庵,生于兴化白驹镇,祖籍苏州", null);

        //书籍列表
        List<Book> books1 = new ArrayList<>();
        List<Book> books2 = new ArrayList<>();
        List<Book> books3 = new ArrayList<>();

        books1.add(new Book(1L, "《宋太祖龙虎风云会》", "剧本,小说", 98, "宋太祖龙虎风云会"));
        books1.add(new Book(2L, "《三国演义》", "剧本,小说", 99, "与施耐庵合著"));

        books2.add(new Book(3L, "《红楼梦》", "小说", 97, "四大名著之一"));
        books2.add(new Book(3L, "《红楼梦》", "小说", 97, "四大名著之一"));
        books2.add(new Book(4L, "《题敦诚琵琶行传奇》", "诗词", 96, "散佚,存残句"));

        books3.add(new Book(5L, "《蝶恋花·一别家山音信杳》", "诗词全集,诗集", 96, "一别家山音信杳,百种相思,肠断何时了。"));
        books3.add(new Book(6L, "《水浒传》", "小说", 98, "四大名著之一"));
        books3.add(new Book(6L, "《水浒传》", "小说", 98, "四大名著之一"));

        author1.setBooks(books1);
        author2.setBooks(books2);
        author3.setBooks(books3);
        author4.setBooks(books3);
        List<Author> authors = Arrays.asList(author1, author2, author3, author4);
        return authors;
    }
}

3.3 快速入门

3.3.1 需求

调用getAuthors方法获取到作家的集合,打印所有年龄大于72的作家的名字并去重。

3.3.2 实现

匿名内部类

    public static void main(String[] args) {
        List<Author> authors = getAuthors();
        authors.stream()//把集合转换成流对象
                .distinct()//去重
                .filter(new Predicate<Author>() {
                    @Override
                    public boolean test(Author author) {
                        return author.getAge() > 72;
                    }
                })//过滤
                .forEach(new Consumer<Author>() {
                    @Override
                    public void accept(Author author) {
                        System.out.println(author.getName());
                    }
                });//打印
    }

Lambda表达式

public static void main(String[] args) {
        List<Author> authors = getAuthors();
        authors.stream()//把集合转换成流对象
                .distinct()//去重
                .filter(author -> author.getAge() > 72)//过滤
                .forEach(author -> System.out.println(author.getName()));//打印
    }

3.4 常用操作

注意:对流进行操作包括获取流对象、中间操作、终结操作三部分,缺一不可。

3.4.1 创建流

单列集合:集合对象.stream()

List<Author> authors = getAuthors();
Stream<Author> stream = authors.stream();

数组:Arrays.stream(arr)或者使用stream.of(arr)方法

int[] arr = new int[]{1,2,3,3,4,5};
        //Stream<int[]> arr = Stream.of(arr);
        Arrays.stream(arr)
                .distinct()
                .filter(i -> i >= 3)
                .forEach(i -> System.out.println(i));

双列集合:转换成单列集合后再创建

Map<String, Integer> map = new HashMap<>();
map.put("小成", 20);
map.put("中成", 25);
map.put("大成", 30);
map.entrySet()//转换为单列集合
        .stream()
        .filter(entry -> entry.getValue() > 20)
        .forEach(entry -> System.out.println(entry.getKey()+ " " + entry.getValue()));
3.4.2 中间操作
1.filter

过滤操作

List<Author> authors = getAuthors();
        authors.stream()
                .distinct()
                .filter(author -> author.getName().length() == 3)
                .forEach(author -> System.out.println(author.getName()));
2.map

可以对流中的元素进行计算或进行数据类型的转换

List<Author> authors = getAuthors();
        /*authors.stream().map(new Function<Author, Integer>() {
            @Override
            public Integer apply(Author author) {
                return author.getAge() + 10;
            }
        }).forEach(new Consumer<Integer>() {
            @Override
            public void accept(Integer integer) {
                System.out.println(integer);
            }
        });*/

        //简化成Lambda表达式
        authors.stream()
                .map(author -> author.getAge() + 10)
                .forEach(age -> System.out.println(age));
3.distinct

去除流中的重复元素

//打印所有作者姓名并去重
List<Author> authors = getAuthors();
authors.stream()
        .distinct()
        .forEach(author -> System.out.println(author.getName()));

注意:distinct方法是依赖Object的equals方法来判断是否是同一个对象,也就是地址值相同才认为是同一个对象,实际使用中一般需要重写equals方法,可以使用lombok的注解@EqualsAndHashCode

4.sorted

对流中的元素进行排序

//按照作家的年龄降序排列
List<Author> authors = getAuthors();
authors.stream()
        .sorted((o1, o2) -> o2.getAge() - o1.getAge())
        .forEach(author -> System.out.println(author.getName() + " " + author.getAge()));

注意:使用无参的sorted方法,需要流中的元素实现Comparable接口。

5.limit

设置流的最大长度,超出的部分将被抛弃

//打印年龄最大的两个作家的姓名和年龄
List<Author> authors = getAuthors();
        authors.stream()
                .distinct()
                .sorted((o1, o2) -> o2.getAge() - o1.getAge())
                .limit(2)
                .forEach(author -> System.out.println(author.getName() + " " + author.getAge()));
6.skip

跳过流中的前n个元素,返回剩下的元素

//打印除了年龄最大的作家外的其他作家
List<Author> authors = getAuthors();
        authors.stream()
                .distinct()
                .sorted((o1, o2) -> o2.getAge() - o1.getAge())
                .skip(1)
                .forEach(author -> System.out.println(author.getName() + " " + author.getAge()));

7.flatMap

map可以对流中的元素进行计算或进行数据类型的转换,将计算或转换后的元素再放入流中,只能将一个对象转换成另一个对象。而flatMap可以将一个对象转换成多个对象作为流中的元素。

例1

打印所有作家的所有书籍的名字,并去重

        List<Author> authors = getAuthors();
        authors.stream()
                /*.flatMap(new Function<Author, Stream<Book>>() {
                    @Override
                    public Stream<Book> apply(Author author) {
                        return author.getBooks().stream();
                    }
                })*/
                .flatMap(author -> author.getBooks().stream())
                .distinct()
                .forEach(book -> System.out.println(book.getName()));
例2

打印现有数据的所有分类,要求对分类进行去重,不能出现这种格式:“哲学,爱情”,应该分成两个类

List<Author> authors = getAuthors();
        authors.stream()
                .flatMap(author -> author.getBooks().stream())
                .distinct()//书籍去重
                .flatMap(book -> Arrays.stream(book.getCategory().split(",")))
                .distinct()//分类去重
                .forEach(catagory -> System.out.println(catagory));

结果

D:\program\jdk8\bin\java.exe ...
剧本
小说
诗词
诗词全集
诗集

Process finished with exit code 0
3.4.3 终结操作
1.forEach

对流中的元素进行遍历操作,通过传入的而参数去指定对遍历到的元素进行什么具体操作。

getAuthors().stream()
                .map(author -> author.getName())
                .distinct()
                .forEach(name -> System.out.println(name));
2.count

获取当前流中元素的个数

//打印这些作家所出书籍的数目,注意去重
long count = getAuthors().stream()
                .flatMap(author -> author.getBooks().stream())
                .distinct()
                .count();
        System.out.println(count);
3.max&min

求流中的最大最小值

//获取集合中作家的书籍的最高分和最低分
Optional<Integer> max = getAuthors().stream()
                .flatMap(author -> author.getBooks().stream())
                .map(book -> book.getScore())
                .max((score1, score2) -> score1 - score2);

        Optional<Integer> min = getAuthors().stream()
                .flatMap(author -> author.getBooks().stream())
                .map(book -> book.getScore())
                .min((score1, score2) -> score1 - score2);
        System.out.println(max.get());
        System.out.println(min.get());
4.collect

把当前流转换成一个集合(List,Set,Map)

//获取一个存放所有作者名字的List集合
        List<String> nameList = getAuthors().stream()
                .map(author -> author.getName())
                .collect(Collectors.toList());
        System.out.println(nameList);
//获取一个所有书名的Set集合
        Set<String> bookSet = getAuthors().stream()
                .flatMap(author -> author.getBooks().stream())
                .map(book -> book.getName())
                .collect(Collectors.toSet());
        System.out.println(bookSet);
//获取一个Map集合,key为作者名,value为List<Book>
        getAuthors().stream()
                .distinct()
                .collect(Collectors.toMap(author -> author.getName(), author -> author.getBooks()));
5.查找与匹配的终结方法
anyMatch

判断是否有任意符合条件的元素

//判断是否有年龄在70以上的作家
        boolean match = getAuthors().stream()
                .anyMatch(author -> author.getAge() > 70);
        System.out.println(match);
allMatch

判断全部元素是否符合条件

noneMatch

判断全部元素是否都不符合条件

findAny

获取流中的任意一个元素

//获取任意一个年龄大于70的作者,如果存在就输出他的名字
        Optional<String> name = getAuthors().stream()
                .filter(author -> author.getAge() > 70)
                .map(author -> author.getName())
                .findAny();
        name.ifPresent(name1 -> System.out.println(name1));
findFirst

获取流中的第一个元素

reduce归并

对流中的数据按照指定的计算方式计算出一个结果(缩减操作)

两个参数的重载形式
//使用reduce求所有作者年龄的和
        List<Author> authors = getAuthors();
        Integer sum = authors.stream()
                .distinct()
                .map(author -> author.getAge())
                .reduce(0, (result, age) -> result + age);
        System.out.println(sum);
//使用reduce求所有作者年龄的最大值
        Integer maxAge = getAuthors().stream()
                .distinct()
                .map(author -> author.getAge())
                .reduce(Integer.MIN_VALUE, (result, age) -> age > result ? age : result);
        System.out.println(maxAge);

两个参数的重载形式的reduce源码计算形式

T result = identity;
for (T element : this stream) {
    result = accumulator.apply(result, element)
}
return result;
一个参数的重载形式

使用一个参数的重载形式做上一个例子

//使用reduce求所有作者年龄的最大值
        Optional<Integer> maxAge = getAuthors().stream()
                .distinct()
                .map(author -> author.getAge())
                .reduce((result, age) -> age > result ? age : result);
        maxAge.ifPresent(maxage -> System.out.println(maxage));

一个参数的重载形式的reduce源码计算形式

boolean foundAny = false;
T result = null;
for (T element : this stream) {
    if (!foundAny) {
        foundAny = true;
        result = element;
    }else {
        result = accumulator.apply(result, element);   
    }
}
return foundAny ? Optional.of(result) : Optional.empty();

3.5 注意事项

  • 惰性求值:如果没有终结操作,流的中间操作是不会执行的
  • 流是一次性的:一个流对象经过一个终结操作后,这个流就不能再使用了
  • 不会影响原数据:正常情况下流的操作不会影响原数据

4.Optional

4.1 概述

我们在编写代码的时候经常会出现空指针异常,所以在很多情况下我们要做非空的判断。
例如:

Author author = getAuthor();
if(author != null) {
    System.out.println(author.getName());
}

getAuthor()的实现

private static Author getAuthor() {
        Author author = new Author(1L, "罗贯中", 70, "罗贯中,名本,字贯中,号湖海散人,山西并州太原府人", null);

        return author;
    }

而过多的判断会使得代码臃肿,JDK8引入了Optional可以更优雅地处理空指针异常。

4.2 使用

4.2.1 创建对象

Optional就好像是包装类,可以把我们的具体数据封装在Optional对象内部,然后我们去使用Optional封装好的方法操作封装进去的数据就可以非常优雅地避免空指针异常。

1.使用Opional的静态方法ofNullable(最常使用)
Author author = getAuthor();
Optional<Author> authorOptional = Optional.ofNullable(author);//将对象分装成Optional
2.使用Opional的静态方法of(必须保证封装的对象不为null)
Author author = getAuthor();
Optional<Author> authorOptional = Optional.of(author);
3.使用Opional的静态方法empty(封装null对象)
Optional.empty();
4.2.2 安全使用对象

使用Optional封装对象能有效避免空指针异常

        Author author = getAuthor();
        Optional<Author> authorOptional = Optional.ofNullable(author);
        authorOptional.ifPresent(author1 -> System.out.println(author1.getName()));
4.2.3 获取封装的对象
1.get获取封装的对象(不推荐)

如果Optional封装的对象为null,则会抛出NoSuchElementException

Author author = authorOptional.get();
2.orElseGet安全获取封装的对象(推荐)

如果Optional封装的对象为null,则会返回所设置的默认值() -> new Author()

        Optional<Author> authorOptional = Optional.ofNullable(author);
        Author author = authorOptional.orElseGet(() -> new Author());
3.orElseThrow

如果Optional封装的对象为null,则抛出自定义的异常(在spring等框架中常用来做异常统一管理

Optional<Author> authorOptional = Optional.ofNullable(null);
Author author2 = authorOptional.orElseThrow(() -> new RuntimeException("数据为null"));
4.2.4 filter过滤
        Optional<Author> authorOptional = getAuthorOptional();
        Optional<Author> author2 = authorOptional.filter(author1 -> author1.getAge() > 60);
        author2.ifPresent(author1 -> System.out.println(author1.getName()));
4.2.5 isPresent判断

这种写法比较麻烦,还是推荐ifPresent

        Optional<Author> authorOptional = getAuthorOptional();
        if (authorOptional.isPresent()) {
            System.out.println(authorOptional.get().getName());
        }
4.2.5 map转换

和stream中的map用法一致

        Optional<Author> authorOptional = getAuthorOptional();
        authorOptional.map(author1 -> author1.getIntro()).ifPresent(intro -> System.out.println(intro));

5 函数式接口

5.1 概述

只有一个抽象方法的接口称为函数式接口

5.2 常见函数式接口

  • Consumer 消费接口
@FunctionalInterface
public interface Consumer<T> {

    /**
     * Performs this operation on the given argument.
     *
     * @param t the input argument
     */
    void accept(T t);
  • Function 计算转换接口
@FunctionalInterface
public interface Function<T, R> {

    /**
     * Applies this function to the given argument.
     *
     * @param t the function argument
     * @return the function result
     */
    R apply(T t);
  • Predicate 判断接口
public interface Predicate<T> {

    /**
     * Evaluates this predicate on the given argument.
     *
     * @param t the input argument
     * @return {@code true} if the input argument matches the predicate,
     * otherwise {@code false}
     */
    boolean test(T t);
  • Supplier 生产型接口
@FunctionalInterface
public interface Supplier<T> {

    /**
     * Gets a result.
     *
     * @return a result
     */
    T get();
}

5.3 常用的默认方法

函数式接口除了有一个抽象方法,还可以有很多默认方法。
可能暂时用不到,自定义类似于filter,map这种方法的时候可能会用。

  • and
  • or
  • negate

6 方法引用

JDK8提供的一个语法糖,可以在某些情况下进一步简化Lambda表达式。
在使用Lambda表达式时,如果方法体中只有一个方法的调用的话(包括构造方法),我们可以用方法引用进一步简化代码。

6.1 推荐用法

写出Lambda表达式后,发现方法体中只有一行代码,并且是方法的调用,可以转换成方法引用。

6.2 基本格式

类名或者对象名::方法名

6.3 语法详解(了解)

6.3.1 引用类的静态方法
类名::方法名

使用前提:在重写方法的时候,方法体中只有一行代码,并且这行代码是调用了某个类的静态方法,并且我们把要重写的抽象方法中所有的参数都按照顺序传入了这个静态方法中,这个时候我们就可以引用类的静态方法。
例:

List<Author> authors = getAuthors();
        authors.stream()
                .map(author -> author.getAge())
                .map(String::valueOf)
                .forEach(str -> System.out.println(str.getClass()));
6.3.2 引用对象的实例方法
对象名::方法名

使用前提:在重写方法的时候,方法体中只有一行代码,并且这行代码是调用了某个类的成员方法,并且我们把要重写的抽象方法中所有的参数都按照顺序传入了这个成员方法中,这个时候我们就可以引用类的成员方法。
例:

List<Author> authors = getAuthors();
        ArrayList<String> names = new ArrayList<>();
        authors.stream()
                .map(author -> author.getName())
                .forEach(names::add);
        System.out.println(names);
6.3.3 引用类的实例方法
类名::方法名

使用前提:在重写方法的时候,方法体中只有一行代码,并且这行代码是调用了第一个参数的成员方法,并且我们把要重写的抽象方法中剩余的所有参数都按照顺序传入了这个成员方法中,这个时候我们可以引用类的实例方法。

6.3.4 构造方法引用
类名::new

使用前提:在重写方法的时候,方法体中只有一行代码,并且这行代码是调用了某个类的构造方法,并且我们把要重写的抽象方法中所有的参数都按照顺序传入了这个构造方法中,这个时候我们就可以引用类的构造方法。

7 高级用法

7.1 基本数据类型优化

我们之前用到的很多Stream的方法由于都使用了泛型。所以涉及到的参数和返回值都是引用数据类型。

即使我们操作的是整数小数,但是实际用的都是他们的包装类。JDK5中引入的自动装箱和自动拆箱让我们在使用对应的包装类时就好像使用基本数据类型一样方便。但是你一定要知道装箱和拆箱肯定是要消耗时间的。虽然这个时间消耗很下。但是在大量的数据不断的重复装箱拆箱的时候,你就不能无视这个时间损耗了。

所以为了让我们能够对这部分的时间消耗进行优化。Stream还提供了很多专针对基本数据类型的方法。
例如: mapTolnt,mapToLong,mapToDouble,flatMapTolnt,flatMapToDouble等。

List<Author> authors = getAuthors();
        authors.stream()
                .map(author -> author.getAge())
                .map(age -> age + 10)
                .filter(age -> age > 18)
                .map(age -> age + 1)
                .forEach(System.out::println);

使用mapToInt优化

List<Author> authors = getAuthors();
        authors.stream()
                .mapToInt(author -> author.getAge())
                .map(age -> age + 10)
                .filter(age -> age > 18)
                .map(age -> age + 1)
                .forEach(System.out::println);

7.2 并行流

当流中有大元素时,我们可以使用并行流去提高操作的效率。其实并行流就是把任务分配给多个线程去完全。如果我们自己去用代码实现的话其实会非常的复杂,并且要求你对并发编程有足够的理解和认识。而如果我们使用Stream的话,我们只需要修改一个方法的调用就可以使用并行流来帮我们实现,从而提高效率。

parallel方法可以把串行流转换为并行流。

List<Author> authors = getAuthors();
        authors.stream().parallel()
                .mapToInt(author -> author.getAge())
                .map(age -> age + 10)
                .filter(age -> age > 18)
                .map(age -> age + 1)
                .forEach(System.out::println);

也可以通过parallelStream直接获取并行流对象

List<Author> authors = getAuthors();
        authors.parallelStream()
                .mapToInt(author -> author.getAge())
                .map(age -> age + 10)
                .filter(age -> age > 18)
                .map(age -> age + 1)
                .forEach(System.out::println);

此为本人在B站上学习JAVA函数式编程时所作笔记,学习的视频链接如下:
视频链接

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值