Java8新特性详细总结

Java8新特性

一、 lambda表达式

  • Lambda 表达式是JDK8 的一个新特性,也被称为闭包,Lambda表达式允许把函数作为一个方法的参数(即行为参数化)传递进方法中。
  • Lambda表达式可以取代大部分的匿名内部类,写出更优雅的 Java 代码,尤其在集合的遍历和集合操作中,可以极大地优化代码结构。
  • Lambda表达式的参数列表的数据类型可以省略不写,因为JVM编译器能够通过上下文推断出数据类型,这就是“类型推断“。
  • lambda表达式可以返回值,就像从方法中返回值一样,添加一个return;

说明

  • lambda 表达式引用的外层局部变量必须是具有final语义的变量:
    • 在域外已被final修饰的局部变量
    • 没有被final修饰,但是必须不可被后面的代码修改(包括lambda内部和外部均不可修改)(即隐性的具有 final 的语义)
  • 在 Lambda 表达式当中不允许声明一个与局部变量同名的参数或者局部变量。

示例

  1. 无参数
()-> System.outprintln("hello word!");
  1. 一个参数
Consumer<string> con =(x)->System.out.println(x);

当只有一个参数时可以省略括号

Consumer<string> con =x->System.out.println(x);
  1. 多个参数
BinaryOperator<Integer> bo = (a,b) -> {
	system.outprintln("函数式接口);
	return a + b; 
};
  1. 指定参数类型

如果编译器无法从lambda匹配的函数式接口抽象方法推断参数类型,则有时可能需要为lambda表达式指定参数类型

(Car car)-> System.out.println("The car is"+ car.getName ());
  1. 只有一条语句时

当Lambda体只有一条语句时,return和大括号可以省略,示例:

BinaryOperator<Integer>bo= (a,b) -> a + b;
  1. 完整示例
//JDK 8之前
new Thread(new Runnable () {
   @Override
    public void run() {
    	system.out.printIn("使用匿名内部类,开线程");
    }
}).start(); 

//JDK 8 使用Lambda表达式
new Thread(() -> System.out.println("使用lambda表达式,开线程")).start();

二、 函数式接口

  • lambda表达式需要函数式接口的支持
  • 对于函数式接口,我们可以理解为只有一个抽象方法的接口,除此之外它和别的接口相比并没有什么特殊的地方。
  • 为了确保函数式接口的正确性,我们可以给这个接口添加@FunctionalInterface注解,这样当其中有超过一个抽象方法时就会报错。
  • 只要一个对象是函数式接口的实例,那么该对象就可以用Lambda表达式来表示
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target (ElementType.TYPE)
public @interface FunctionalInterface {}

常见函数式接口

  • Consumer 表示接受一个输入参数并且不返回结果的操作。其中 T 表示输入参数类型。它有一个名为 accept 的抽象方法,可以使用 Lambda 表达式来实现该方法。例如:

    Consumer<String> printString = s -> System.out.println(s);
    printString.accept("Hello World");
    
  • Function<T, R> 表示接受一个输入参数并返回结果的操作。其中 T 表示输入参数类型,R 表示返回结果类型。它有一个名为 apply 的抽象方法,可以使用 Lambda 表达式来实现该方法。例如:

    Function<Integer, String> convertToString = i -> i.toString();
    String str = convertToString.apply(123);
    System.out.println(str);
    
  • BiFunction<T, U, R> 表示接受两个输入参数并返回结果的操作。其中 T 和 U 分别表示两个输入参数的类型,R 表示返回结果类型。它有一个名为 apply 的抽象方法,可以使用 Lambda 表达式来实现该方法。例如:

    BiFunction<Integer, Integer, Integer> add = (a, b) -> a + b;
    int result = add.apply(1, 2);
    System.out.println(result);
    
  • Supplier 表示提供一个结果的操作。其中 T 表示返回结果的类型。它有一个名为 get 的抽象方法,可以使用 Lambda 表达式来实现该方法。例如:

    Supplier<String> getMessage = () -> "Hello World";
    String msg = getMessage.get();
    System.out.println(msg);
    

三、 方法引用

java中的方法引用(Method Reference)是一种简化Lambda表达式的语法,它可以直接引用已有方法或构造方法,并将其作为Lambda表达式的参数传递。方法引用可以使代码更加简洁易读,提高代码的可维护性。需要注意的是,方法引用只是Lambda表达式的一种语法糖,它并不是新的语言特性。

在Java 8中,方法引用的语法格式为:

方法引用类型::方法名

其中,方法引用类型可以是以下四种:

  1. 静态方法引用:Class::staticMethod
//使用 Integer.parseInt 静态方法引用来将字符串转换为整型
List<String> strList = Arrays.asList("1", "2", "3");
List<Integer> intList = strList.stream()
                               .map(Integer::parseInt)
                               .collect(Collectors.toList());
  1. 实例方法引用:instance::instanceMethod
class MyClass   {
    public static void main(String[] args) {
        // 使用方法引用
        //字符串拼接
        MyClass obj = new MyClass();
        Consumer<String> instanceMethod = obj::instanceMethod;
        instanceMethod.accept("sssss");
        //生成随机数
        Random random = new Random();
        Function<Integer, Integer> integerFunction = random::nextInt;
        Integer randomInt = integerFunction.apply(10);
    }
    // 实例方法
    public void instanceMethod(String name) {
        System.out.println("Hello " + name);
    }
}
  1. 构造方法引用:Class::new
public class Person {
    private String name;
    private int age;

    public Person(String name, int age) {
        this.name = name;
        this.age = age;
    }
}
class MyPerson{
    public static void main(String[] args) {
        BiFunction<String, Integer, Person> stringPersonFunction = Person::new;
        Person xiaolin = stringPersonFunction.apply("xiaolin", 18);
    }
}
  1. 数组构造方法引用(创建数组):Type[]::new
使用场景:在需要大量创建数组来存储数据的情况下,示例:
String[] array = Stream.generate(() -> "Hello")
    .limit(5)
    .toArray(String[]::new);
//---输出---
[Hello, Hello, Hello, Hello, Hello]

四、 Stream流

Java 8 API添加了一个新的抽象称为流Stream,可以让你以一种声明的方式处理数据,Stream流是对集合(Collection)对象功能的增强,与Lambda表达式结合,可以提高编程效率、间接性和程序可读性

两种运行方式

  • 顺序流(Sequential Stream)是普通的单线程流,所有的元素按顺序依次处理。这种方式适合于处理小型数据集,或者需要保持元素顺序的场景。
  • 并行流(Parallel Stream)是多线程流,将数据集分割成多个小块,并且在不同线程中并行处理,然后将结果合并返回。这种方式适合于处理大型数据集或需要加速处理的场景。并行流的处理方式是 Java 自动管理的,我们只需要将顺序流转换为并行流即可。

两种操作方式

  • 中间操作(Intermediate Operations):在流上执行的操作,它们返回一个新的流,并且支持链式调用。中间操作不会立即执行,只有在终止操作被调用时才会触发执行。
  • 终止操作(Terminal Operations):是流的最后一步操作,它们会执行中间操作链上的所有操作,并产生最终的结果

生成流

  1. 集合创建:可以通过 Collection 接口的 stream() 或 parallelStream() 方法创建 Stream 流。比如:
List<String> list = Arrays.asList("apple", "banana", "orange");
Stream<String> stream = list.stream(); // 串行流
Stream<String> parallelStream = list.parallelStream(); // 并行流
  1. Array数组创建:可以通过 Arrays 类的 stream() 方法创建 Stream 流。比如:
int[] arr = {1, 2, 3, 4, 5};
IntStream intStream = Arrays.stream(arr);
注:通过Arrays.stream方法生成流,该方法生成的流是数值流【即IntStream】而不是 Stream,使用数值流可以避免计算过程中拆箱装箱,提高性能。
  1. Stream创建:可以通过 Stream 接口的静态方法 of() 创建 Stream 流。比如:
Stream<String> stream = Stream.of("apple", "banana", "orange");
  1. 函数创建 :可以通过 Stream 接口的静态方法 generate() 或 iterate() 创建 Stream 流。比如:
Stream<Integer> stream = Stream.generate(() -> new Random().nextInt(10)).limit(5);;
Stream<Integer> iterateStream = Stream.iterate(0, n -> n + 2).limit(5);;
注: generate() 或 iterate() 创建的流都是无限流,所以需要用limit(n)进行截断
  1. 通过 BufferedReader 的 lines() 方法创建 Stream 流:可以通过 BufferedReader 的 lines() 方法创建 Stream 流,用于读取文件中的文本内容。比如:
try (BufferedReader br = new BufferedReader(new FileReader("file.txt"))) {
    Stream<String> lines = br.lines();
} catch (IOException e) {
    e.printStackTrace();
}
注:返回的 Stream

对象,每个字符串表示文件中的一行文本

中间操作符

  1. filter:过滤,用于通过设置的条件过滤出元素
List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9, 10);
List<Integer> evenNumbers = numbers.stream()
                                    .filter(n -> n % 2 == 0)
                                    .collect(Collectors.toList());
  1. map:接受一个函数作为参数。这个函数会被应用到每个元素上,并将其映射成一个新的元素(使用映射一词,是因为它和转换类似,但其中的细微差别在于它是“创建一个新版本”而不是去“修改”)
List<String> words = Arrays.asList("apple", "banana", "orange");
List<String> upperCaseWords = words.stream()
                                    .map(String::toUpperCase)
                                    .collect(Collectors.toList());
  1. distinct:去重,返回一个元素各异(根据流所生成元素的hashCode和equals方法实现)的流
List<Integer> numbers = Arrays.asList(1, 2, 3, 2, 1, 4, 5, 3);
List<Integer> distinctNumbers = numbers.stream()
                                            .distinct()
                                            .collect(Collectors.toList());
  1. sorted:sorted()返回一个自然排序后的 Stream,sorted(Comparator comparator):返回一个按指定比较器排序后的 Stream
List<Integer> numbers = Arrays.asList(3, 1, 4, 2, 5);
List<Integer> sortedNumbers = numbers.stream()
                                        .sorted()
                                        .collect(Collectors.toList());
  1. limit:截取流中前n个元素
List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5);
List<Integer> limitedNumbers = numbers.stream()
                                        .limit(3)
                                        .collect(Collectors.toList());
  1. skip:返回一个扔掉了前n个元素的流
List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5);
List<Integer> skippedNumbers = numbers.stream()
                                        .skip(2)
                                        .collect(Collectors.toList());
  1. flatMap:将每个元素映射为一个 Stream 流,然后将所有的 Stream 流合并成一个 Stream 流,它返回的是一个 Stream 流的流(Stream)
List<String> lines = Arrays.asList("hello world", "welcome to java", "stream example");
List<String> words = lines.stream()
    .flatMap(line -> Arrays.stream(line.split(" ")))
    .collect(Collectors.toList());
System.out.println(words);

//----------输出内容----
[hello, world, welcome, to, java, stream, example]

  1. peek:对元素进行遍历处理,在处理 Stream 中的每个元素时查看它们的值,但不会修改元素本身
List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5);
List<Integer> squares = numbers.stream()
                                .peek(System.out::println)
                                .map(x -> x * x)
                                .collect(Collectors.toList());

注:map和peek的区别:

  • map是将流中每个元素进行处理并转换,最终形成一个新的流
  • peek是操作流中的所有元素,完成一定的动作(例如诊断性质的操作),但是不会修改流本身,不会生成新的流

终端操作符

一个流有且只能有一个终端操作,当这个操作执行后,流就被关闭了,无法再被操作,因此一个流只能被遍历一次,若想在遍历需要通过源数据在生成流

  1. forEach:对流中的每个元素进行操作,不返回值。例如:

    List<String> list = Arrays.asList("a", "b", "c");
    list.stream().forEach(System.out::println);
    
  2. count:返回流中元素的个数。例如:

    List<String> list = Arrays.asList("a", "b", "c");
    long count = list.stream().count();
    System.out.println(count);
    
  3. collect:将流中的元素收集到一个集合中。例如:

    List<String> list = Arrays.asList("a", "b", "c");
    List<String> newList = list.stream().collect(Collectors.toList());
    System.out.println(newList);
    
  4. reduce:对流中的元素进行归约操作。例如:

    List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5);
    int sum = numbers.stream().reduce(0, (a, b) -> a + b);
    System.out.println(sum); 
    //上述代码中,reduce()方法将流中的元素归约成一个值,初始值为0,BinaryOperator函数的实现是将两个元素相加
    //---输出---
    15
    
    List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5);
    Optional<Integer> max = numbers.stream().reduce(Integer::max);
    System.out.println(max.get());
    //上述代码中,使用Integer::max方法引用作为BinaryOperator函数,将流中的元素归约成一个最大值,最终得到Optional对象,需要使用get()方法获取结果。
    //---输出---
    5
    
  5. anyMatch:判断流中是否有任意一个元素满足指定的条件。例如:

    List<String> list = Arrays.asList("apple", "banana", "cherry");
    boolean result = list.stream().anyMatch(s -> s.startsWith("a"));
    System.out.println(result);
    //---输出---
    true
    
  6. allMatch:判断流中是否所有元素都满足指定的条件。例如:

    List<Integer> list = Arrays.asList(1, 2, 3, 4, 5);
    boolean result = list.stream().allMatch(i -> i > 0);
    System.out.println(result);
    //---输出---
    true
    
  7. noneMatch:判断流中是否没有任何一个元素满足指定的条件。例如:

    List<String> list = Arrays.asList("apple", "banana", "cherry");
    boolean result = list.stream().noneMatch(s -> s.startsWith("d"));
    System.out.println(result);
    //---输出---
    true
    
  8. findFirst:返回流中的第一个元素。例如:

    List<String> list = Arrays.asList("apple", "banana", "cherry");
    Optional<String> first = list.stream().findFirst();
    System.out.println(first.get());
    //---输出---
    apple
    
  9. findAny:返回流中的任意一个元素。例如:

    List<String> list = Arrays.asList("apple", "banana", "cherry");
    Optional<String> any = list.stream().findAny();
    System.out.println(any.get());
    //---输出---
    apple
    
  10. min()、max():返回流中的最小值和最大值,可以接受一个Comparator作为参数进行比较。例如:

    List<Integer> list = Arrays.asList(1, 2, 3, 4, 5);
    Optional<Integer> min = list.stream().min(Comparator.naturalOrder());
    Optional<Integer> max = list.stream().max(Comparator.naturalOrder());
    
    System.out.println("Min value: " + min.get()); // 输出:Min value: 1
    System.out.println("Max value: " + max.get()); // 输出:Max value: 5
    
  11. sum()方法返回流中所有元素的和。例如:

    List<Integer> list = Arrays.asList(1, 2, 3, 4, 5);
    int sum = list.stream().mapToInt(Integer::intValue).sum();
    
    System.out.println("Sum: " + sum); // 输出:Sum: 15
    
  12. Collect收集

    Collector:结果收集策略的核心接口,具备将指定元素累加存放到结果容器中的能力;并在Collectors工具中提供了Collector接口的实现

    1. toList(): 将Stream中的元素收集到List中,例如:

      List<String> list = Stream.of("apple", "banana", "orange")
                              .filter(s -> s.contains("a"))
                              .collect(Collectors.toList());
      
    2. toSet(): 将Stream中的元素收集到Set中,例如:

      Set<Integer> set = Stream.of(1, 2, 3, 2, 1)
                            .collect(Collectors.toSet());
      
    3. joining(): 将Stream中的元素连接成一个字符串,例如:

      String str = Stream.of("Hello", "World", "Java")
                   .collect(Collectors.joining(", ", "[", "]"));
      //---输出---
      [Hello, World, Java]
      
    4. groupingBy(): 将Stream中的元素按照某个属性分组,例如:

      Map<String, List<Person>> personGroups = Stream.of(
              new Person("John", "Doe", 28),
              new Person("Jane", "Doe", 26),
              new Person("Mary", "Smith", 30),
              new Person("Tom", "Smith", 32))
              .collect(Collectors.groupingBy(Person::getLastName));
      
    5. partitioningBy(): 将Stream中的元素按照某个条件分成两组,例如:

      Map<Boolean, List<Integer>> partitioned = Stream.of(1, 2, 3, 4, 5, 6)
          .collect(Collectors.partitioningBy(i -> i % 2 == 0));
      //---输出---
      {false=[1, 3, 5], true=[2, 4, 6]}
      
    6. maxBy()/minBy(): 返回Stream中最大/最小的元素,例如:

      Optional<Integer> max = Stream.of(1, 2, 3, 4, 5)
                 .collect(Collectors.maxBy(Integer::compareTo));
      Optional<Integer> min = Stream.of(1, 2, 3, 4, 5)
                 .collect(Collectors.minBy(Integer::compareTo));
      
      
    7. counting(): 统计Stream中的元素个数,例如:

      long count = Stream.of(1, 2, 3, 4, 5)
                 .collect(Collectors.counting());
      
    8. toMap():将流中的元素转化为一个 Map,其中流中的每个元素都将作为 key-value 对的一部分插入到 Map 中,例如:

      List<String> words = Arrays.asList("hello", "world", "hello", "java");
      Map<String, Integer> wordCountMap = words.stream()
              .collect(Collectors.toMap(Function.identity(), s -> 1, Integer::sum));
      System.out.println(wordCountMap);
      //---输出---
      {world=1, java=1, hello=2}
      

      解析:上面的示例中,我们使用了 toMap() 方法,将 words 中的元素转化为一个 Map,其中 key 是元素本身,value 是该元素在流中出现的次数。

      • Function.identity() 表示使用元素本身作为 key
      • s -> 1 表示每个元素最初的 value 为 1
      • Integer::sum 表示当出现重复的 key 时,将对应的 value 相加
    9. summingInt()、summingLong()、summingDouble():将元素转换为整数(Long、Double)后进行求和操作。具体来说,该方法返回一个 Collector 对象,可用于将 Stream 中的元素转换为整数(Long、Double)并求和,示例:

      List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5);
      int sum = numbers.stream()
                      .collect(Collectors.summingInt(Integer::intValue));
      System.out.println(sum); // 输出 15
      

五、 CompletableFuture

CompletableFuture是Java 8中新增的一个类,用于支持异步编程。它可以在一个任务执行完成后自动触发下一个任务的执行,从而实现

链式调用。同时,CompletableFuture还提供了丰富的方法来处理任务的结果、异常和取消操作,以及等待任务执行完成的方法。

CompletableFuture和Future都是Java中用于异步执行任务的类,但是它们之间有一些重要的区别:

  1. 链式调用:CompletableFuture支持链式调用,可以在一个任务执行完成后自动触发下一个任务的执行,从而实现异步编程。而Future不支持链式调用,需要手动处理多个任务之间的依赖关系。
  2. 异常处理:CompletableFuture提供了异常处理方法,可以在任务执行过程中捕获异常并进行处理。而Future只能通过try-catch块来捕获异常。
  3. 取消任务:CompletableFuture支持取消任务,可以在任务执行过程中取消任务,或者在任务还没有开始执行时取消任务。而Future只能等待任务执行完成或者超时后取消任务。
  4. 等待任务完成:CompletableFuture提供了多种等待任务完成的方法,可以等待所有任务完成、等待任意一个任务完成或者等待指定时间后超时。而Future只能等待任务执行完成或者超时。
  5. 结果处理:CompletableFuture提供了多种方法来处理任务的结果,可以在任务执行完成后对结果进行转换、合并或者过滤。而Future只能通过get()方法来获取任务的结果。

常用方法(部分):

  • runAsync():创建一个没有返回值的异步任务。
  • supplyAsync():创建一个有返回值的异步任务
  • thenRun()/thenRunAsync():上一个任务执行完成后,执行回调方法,但是前后两个任务没有参数传递,第二个任务也没有返回值。
  • thenAccept()/thenAcceptAsync():上一个任务执行完成后,将上一个任务的返回值作为参数传到thenAccept方法中,thenAccept和thenAcceptAsync没有返回值
  • thenApply()和thenApplyAsync():上一个任务执行完成后,将上一个任务的返回值作为参数传到thenApply方法中,thenApply和thenApplyAsync回调方法是有返回值的
  • exceptionally():某个任务执行异常时,执行的回调方法;并且有抛出异常作为参数,传递到回调方法。参数为异常,有返回值。
  • whenComplete():某个任务执行完成后,执行的回调方法,无返回值;并且whenComplete方法的参数是上个任务的结果。whenComplete接收的参数为两个,第一个是上个任务的结果,另一个是异常。因此即使主任务有异常,依然会执行。
  • handle:和whenComplete差不多,都是表示某个任务执行完成后,执行的回调方法,handle方法的参数是上个任务的结果,但是handle是有返回值的,whenComplete没有返回值。

注意事项

  1. CompletableFuture需要获取返回值,才能获取到异常信息。如果不加 get()/join()方法,看不到异常信息。

  2. CompletableFuture的get()方法是阻塞的。如果使用它来获取异步调用的返回值,需要添加超时时间:

    //反例  
    CompletableFuture.get(); 
    //正例 
    CompletableFuture.get(5, TimeUnit.SECONDS);
    
  3. 默认线程池的注意点:CompletableFuture代码中使用了默认的线程池,处理的线程个数是电脑CPU核数-1。在大量请求过来的时候,处理逻辑复杂的话,响应会很慢。一般建议使用自定义线程池,优化线程池配置参数。

简单示例

public static void main(String[] args) throws Exception {
        long startTime = System.currentTimeMillis();
        System.out.println("主线程:程序开始执行");
        // 创建一个CompletableFuture对象,表示一个异步任务,任务创建后就会开始执行
        CompletableFuture<String> future = CompletableFuture.supplyAsync(() -> {
            try {
                // 模拟一个长时间运行的任务
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            // 返回任务结果
            System.out.println("第一个任务执行完毕");
            return "Hello, world!";
        });
        System.out.println("主线程:我在两个异步任务中间");
        //用上一个异步任务的入参作为返回值执行另一个任务,这个任务会等待上一个任务执行完毕后才会执行
        CompletableFuture<String> async = future.thenApplyAsync(result -> {
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("第二个异步任务完成");
            return result.toUpperCase();
        });
        System.out.println("主线程:进入睡眠模式,已睡着");
        Thread.sleep(3000);
        //在回调之前
        System.out.println("主线程:我睡了3秒,我醒了");
        // 等待异步任务完成
        System.out.println("主线程:第一个任务返回"+future.get());
        //异步回调
        System.out.println("主线程:第二个任务返回"+async.get());
        long endTime = System.currentTimeMillis();
        System.out.println("主线程:程序执行耗时:"+(endTime-startTime));
    }
//---输出---
主线程:程序开始执行
主线程:我在两个异步任务中间
主线程:进入睡眠模式,已睡着
第一个任务执行完毕
第二个异步任务完成
主线程:我睡了3秒,我醒了
主线程:第一个任务返回Hello, world!
主线程:第二个任务返回HELLO, WORLD!
主线程:程序执行耗时:3069

六、 接口默认方法

Java 8允许在接口中定义默认方法,这些方法可以有实现,可以在接口的实现类中直接调用,而无需重新实现。例如:
public interface MyInterface {
    default void myMethod() {
        System.out.println("Default method");
    }
}

public class MyClass implements MyInterface {
    // 不需要实现myMethod()方法
}

MyClass obj = new MyClass();
obj.myMethod(); // 输出:Default method

接口默认方法的要求:

  • 接口默认方法必须用default关键字修饰。
  • 接口默认方法必须有方法体,即必须有方法的实现。
  • 接口默认方法可以被实现类覆盖(实现类重写默认方法可以加@Override也可以不加)

七、 Date/Time API

Java 8引入了新的Date/Time API,用于处理日期和时间。它提供了一组新的日期和时间类,如LocalDate、LocalTime、LocalDateTime等,可以更好地处理日期和时间,并提供了许多方便的方法,如计算日期间隔、格式化日期等。

例如:

LocalDate date = LocalDate.of(2022, Month.MARCH, 14);
LocalTime time = LocalTime.of(12, 30);
LocalDateTime dateTime = LocalDateTime.of(date, time);
System.out.println(dateTime); // 输出:2022-03-14T12:30
  • 0
    点赞
  • 15
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值