java基础--21(java8新特性--Lambda,函数式接口,流)

1.java8重要的新特性

    lambda表达式

    函数式接口

    接口中可以定义非抽象方法

    Stream

   

1.Lambda表达式

 Lambda表达式(也称为闭包)是Java 8中最大和最令人期待的语言改变。它允许我们将函数当成参数传递给某个方法,或者把代码本身当作数据处理:函数式开发者非常熟悉这些概念。

很多JVM平台上的语言(Groovy、Scala等)从诞生之日就支持Lambda表达式,但是Java开发者没有选择,只能使用匿名内部类代替Lambda表达式。

  之前的代码是这样的:

Runnable r = new Runnable() {
    @Override
    public void run() {
        System.out.println("do something.");      
    }
}

 现在的代码:

Runnable multiLine = () -> {// 3
    System.out.println("Hello ");
    System.out.println("World");
};

 系统的学习Lambda表达式的语法:

expression = (variable) -> action

     1.variable: 这是一个变量,一个占位符。像x,y,z,可以是多个变量;

     2.action: 这里我称它为action, 这是我们实现的代码逻辑部分,它可以是一行代码也可以是一个代码片段。

    lambda表达式可以包含多个参数,例如:

int sum = (x, y) -> x + y;

   lambda表达式可以转换为函数式接口,比如比较器Comparator就是一个函数式接口,既可以继承他实现接口,也可以使用拉姆达表达式转换,BiFunction<T,U,R>也是函数式接口,

ArrayList中的removeIf也接收一个lambda表达式,

lambda表达式也可以转换为以一个方法引用,System.out::println;等价于 (x)-> System.out.println(x),::分割对象名类名和方法名。this,super也可以。

构造器引用,只是将方法名字换为new,比如Person::new.

lambda表达式可以捕获外部的变量,但是这个变量必须是最终的。

 

现在如果我们要自己编写一个方法处理Lambda表达式,那么这个方法参数必定有一个是函数式的接口,比如可能式Runnable,Suppiler<T>,Predicate<T>,BiFuniction<T>等等,他们的功能各不相同,也可以自己写函数式接口。

 

 

2.Predicate函数

         

     首先说一下他的注解,可以看出Predicate上加上了注解@FunctionalInterface表明他是一个函数式接口,比如常见的还有一个  函数式接口Runnable

        

    那么加上这个@FunctionalInterface注解有什么特点:

  1. 该注解只能标记在"有且仅有一个抽象方法"的接口上。
  2.  JDK8接口中的静态方法和默认方法(static,default ),都不算是抽象方法。
  3. 接口默认继承java.lang.Object,所以如果接口显示声明覆盖了Object中方法,那么也不算抽象方法(equals).
  4. 该注解不是必须的,如果一个接口符合"函数式接口"定义,那么加不加该注解都没有影响。加上该注解能够更好地让编译器进行检查。如果编写的不是函数式接口,但是加上了@FunctionInterface,那么编译器会报错。

   注解介绍完了以后就是Predicate函数的一个重要方法test(),他的作用如下

  1. 评估参数里面的表达式(说白了就是验证传进来的参数符不符合规则,后面有例子)
  2. 它的返回值是一个boolean类型(这点需要注意一下)。

   实现一个函数式接口:判断一个人是否成年,在此之前我们的做法一般是编写一个 判断是否是成人的方法,是无法将 判断 共用的。而在本例只,你要做的是将 行为 (判断是否是成人,或者是判断是否大于30岁) 传递进去,函数式接口告诉你结果是什么。

    创建一个函数式接口:

package com.wx.test1;

@FunctionalInterface
public interface Predicate<T> {
    boolean test(T t);
}

    写一个判断方法,将条件作为参数。

package com.wx.test1;


public class Test1 {
    public static void main(String[] args) {
        boolean isAdult = doPredicate(5, x -> x >= 18);
        System.out.println(isAdult);
    }

    public static boolean doPredicate(int age, Predicate<Integer> predicate) {
        return predicate.test(age);
    }

}

  其实就是把条件条件抽取出来作为参数传了进去,所以这个条件是可变的参数,随时可以改,好处就在这里。

3.java Stream

    什么是Stream?  参考博客:https://mp.weixin.qq.com/s/8x16XgHZvqksJfMIi0um1g

     这种风格将要处理的元素集合看作一种流, 流在管道中传输, 并且可以在管道的节点上进行处理, 比如筛选, 排序,聚合等。元素流在管道中经过中间操作(intermediate operation)的处理,最后由最终操作(terminal operation)得到前面处理的结果。Stream(流)是一个来自数据源的元素队列并支持聚合操作。

  • 数据源 流的来源。 可以是集合,数组,I/O channel, 产生器generator 等。
  • 聚合操作 类似SQL语句一样的操作, 比如filter, map, reduce, find, match, sorted等。

和以前的Collection操作不同, Stream操作还有两个基础的特征:

  • Pipelining: 中间操作都会返回流对象本身。 这样多个操作可以串联成一个管道, 如同流式风格(fluent style)。 这样做可以对操作进行优化, 比如延迟执行(laziness)和短路( short-circuiting)。
  • 内部迭代: 以前对集合遍历都是通过Iterator或者For-Each的方式, 显式的在集合外部进行迭代, 这叫做外部迭代。 Stream提供了内部迭代的方式, 通过访问者模式(Visitor)实现

  在 Java 8 中, 集合接口有两个方法来生成流:

  • stream() − 为集合创建串行流。

  • parallelStream() − 为集合创建并行流。

   Stream的用法

    流的创建->流的处理->流的收集->流的分区分组->流的下游收集->约简操作

   首先需要了解的是流的创建方式:

        list.stream();
        Arrays.stream()
        Stream.of()
        Stream.iterate() 
        Stream.generate()
        Stream.empty()

 

看个案例:计算1+2+3.+..100的值

 Integer integer1 = Stream
                .iterate(1, integer -> integer + 1)
                .limit(100)
                .reduce((integer, integer2) -> integer + integer2)
                .get();

  其次需要掌握他的一些核心API,  

   1.Stream 提供了新的方法 'forEach' 来迭代流中的每个数据。以下代码片段使用 forEach 输出了10个随机数:

 Random random = new Random();
 random.ints().limit(10).forEach(System.out::println);

   

 2.map 方法用于映射每个元素到对应的结果,

   flatMap:ok如果对流中的每一个元素操作返回的又是一个流,那么可以使用flatMap来摊平这个流。

     现在有这样的一个需求,就是要把集合中的元素都变成他的平方然后遍历输出。

List<Integer> list = Arrays.asList(2, 5, 4, 9, 6, 4, 5, 8, 4);
        list.stream()
                .map(i -> i*i)
                .forEach(System.out::println);

   如果要返回这个集合:

List<Integer> list = Arrays.asList(2, 5, 4, 9, 6, 4, 5, 8, 4);
        List<Integer> collect = list.stream()
                .map(i -> i * i)
                .collect(Collectors.toList());

 3.filter方法用于条件过滤,判断集合中空字符串的数量

List<String> stringList = Arrays.asList("wef", "fwef", "", "vdsv45", "434", " ", "435ghn");
        long count = stringList.stream()
                .filter(string -> (string.isEmpty())).count();
        System.out.println(count);

 4.limit 方法返回一个新的流,对于裁剪无限流的尺寸非常有用,用于获取指定数量的流。 以下代码片段使用 limit 方法打印出 10 条数据:

 skip(n)方法正好相反,他会丢掉前面的n个元素。这个在将文本分割为单词的时候会显得有用,因为第一个空格可以被跳过。

Random random = new Random();
        random.ints().limit(10).forEach(System.out::println);

上面两个方法是抽取子流的方法,当然流可以拼接。使用Stream类的静态concat()方法,这里需要注意,第一个流不能是无限的流。

  distinct方法是对流的去重。

5.sorted 方法用于对流进行排序。以下代码片段使用 sorted 方法对输出的 10 个随机数进行排序:

  总的来说排序可以接收两个参数,一个是Comparable类型的java bean ,一个是Compartor。

 Random random = new Random();
        random.ints().limit(10).sorted().forEach(System.out::println);

Stream.of("fsd", "sda", "s", "rewrwe").sorted(Comparator.comparing(String::length).reversed());

 

 6.并行(parallel)程序

List<String> strings = Arrays.asList("abc", "", "bc", "efg", "abcd","", "jkl");
// 获取空字符串的数量
int count = strings.parallelStream().filter(string -> string.isEmpty()).count();

7.peek 流的操作都是中间的过程,如果结果不正确,那么如何调试呢?就是用peek,它产生一个新流,元素和原来的元素相同。

上面都是流的中间操作,接下来是流的终结操作。

有count,求和的reduce, 还有匹配的anyMatch,allMatch,还有firstFind,Min,Max等等,他们有的返回的是Optional对象。

有关Optional的博客:https://blog.csdn.net/weixin_37650458/article/details/98522607

收集结果

通常会有这几种操作,转换为数组,转化为集合,转化为字符串,求对象属性的和,求对象属性的最大值等等:

        Object[] objects = Stream.iterate(0, e -> e + 1).limit(10).toArray();
        Integer[] array = Stream.iterate(0, e -> e + 1).limit(10).toArray(Integer[]::new);
        List<Integer> list2 = Stream.iterate(0, e -> e + 1).limit(10).collect(Collectors.toList());
        TreeSet<Integer> collect2 = Stream.iterate(0, e -> e + 1).limit(10).collect(Collectors.toCollection(TreeSet::new));
        String s1 = list.stream().collect(Collectors.joining(","));
        IntSummaryStatistics summaryStatistics = list.stream().collect(Collectors.summarizingInt(String::length));
        summaryStatistics.getAverage();
        summaryStatistics.getCount();
        summaryStatistics.getMax();

 收集到映射表中

群组和分区

groupingBy()将具有相同特性的值群聚成组。

Map<String, List<Locale>> collect4 = Stream.of(Locale.getAvailableLocales()).collect(Collectors.groupingBy(Locale::getCountry));

 下游收集器

    上面groupingBy()将流分组得到一个Value为List类型,下游收集器可以对这个List进行操作提供的方法有counting(),summing(),maxBy(),mapping(),

Map<String, Long> collect5 = Stream.of(Locale.getAvailableLocales())
                .collect(Collectors.groupingBy(Locale::getCountry, Collectors.counting()));

mapping()用法:

   Map<String, Set<String>> collect6 = Stream.of(Locale.getAvailableLocales())
                .collect(Collectors.groupingBy(Locale::getCountry, Collectors.mapping(Locale::getLanguage, Collectors.toSet())));

约简操作

  reduce

 

4.练习

    首先需要理解Lambda跟Stream配合可以做什么,不可以做什么,首先是操作一个数据集合的,数组也叫数据集合,那么可以做的操作就是对数据进行过滤,筛选,映射等等。

 

package com.wx1.test3;

import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
import java.util.function.Predicate;
import java.util.stream.Collectors;
import java.util.stream.Stream;

/**
 * User: Mr.Wang
 * Date: 2019/10/23
 */
public class Test1 {
    public static void main(String[] args) {
        //1.初识
        new Thread(new Runnable() {
            @Override
            public void run() {
                System.out.println("传统方式");
            }
        }).start();

        new Thread(() -> {
            System.out.println("java8");
        }).start();

        //2.练习 Java开发者经常使用匿名类的另一个地方是为 Collections.sort() 定制 Comparator。

        //3.使用Lambda表达式对列表进行迭代
        List<String> list = Arrays.asList("apple", "orange", "banana");
        list.stream().forEach(e -> {
            System.out.println(e.toString());
        });

        //4.使用 java.util.function.Predicate 函数式接口以及lambda表达式,可以向API方法添加逻辑,
        // 用更少的代码支持更多的动态行为。这个函数接口可以把条件传过去,也就是说将条件抽象出来为一个
        // Lambda表达式,作为参数往函数中传,牛批了。
        List<String> filter = filter(list, (e) -> true);
        List<String> list1 = filter(list, (str) -> String.valueOf(str).startsWith("a"));
        filter.stream().forEach((e) -> {
            System.out.println(e);
        });

        //5.如何在lambda表达式中加入Predicate 上面的例子只是帮助我们理解Predicate,
        //真实的用法可能会是下面这个样子,在Stream中做多个条件的筛选。
        Predicate<String> condition1 = (e) -> String.valueOf(e).contains("a");
        Predicate<String> condition2 = (e) -> String.valueOf(e).startsWith("b");

        Predicate<String> and = condition1.and(condition2);
        List<String> collect = list.stream().filter(condition1.and(condition2)).collect(Collectors.toList());

        //6.Java 8中使用lambda表达式的Map和Reduce示例
        //map,它允许你将对象进行转换,允许拿到对象的属性
        list.stream().map((e) -> e + "map").collect(Collectors.toList());
        list.stream().map(String::length).collect(Collectors.toList());

        //7. reduce() 函数可以将所有值合并成一个。Map和Reduce操作是函数式编程的核心操作,
        // 因为其功能,reduce 又被称为折叠操作。另外,reduce 并不是一个新的操作,
        // 你有可能已经在使用它。SQL中类似 sum()、avg() 或者 count() 的聚集函数,实际上就是 reduce 操作,
        // 因为它们接收多个值并返回一个值
        List<Integer> costBeforeTax = Arrays.asList(100, 200, 300, 400, 500);
        Optional<Double> reduce = costBeforeTax.stream().map(e -> e + e * 0.2).reduce((sum, cost) -> sum + cost);
        Double reduce1 = costBeforeTax.stream().map(e -> e + e * 0.2).reduce(8.0, (integer, aDouble) -> (integer + aDouble));

        Double aDouble = reduce.get();

        //8.对列表的每个元素应用函数
        //逐一乘以某个数、除以某个数或者做其它操作。这些操作都很适合用 map() 方法,
        //可以将转换逻辑以lambda表达式的形式放在 map() 方法里,就可以对集合的各个元素进行转换了
        //key要是原来的数,value是映射的数
        Map<Boolean, List<Integer>> collect1 = costBeforeTax.stream().map((e) -> e + 20)
                .collect(Collectors.groupingBy((e) -> Integer.valueOf(e.toString()) >= 200 && Integer.valueOf(e.toString()) <= 400));


    }

    private static List<String> filter(List<String> list, Predicate predicate) {
        List<String> re = new ArrayList<>();
        list.stream().forEach(e -> {
            if (predicate.test(e)) {
                re.add(e);
            }
        });
        return re;
    }
}

   

    假设需要从一个字符串列表中选出以数字开头的字符串并输出,Java 7之前需要这样写:

List<String> list = Arrays.asList("1one", "two", "three", "4four");
for(String str : list){
    if(Character.isDigit(str.charAt(0))){
        System.out.println(str);
    }
}

  而Java 8就可以这样写:

List<String> list = Arrays.asList("1one", "two", "three", "4four");
list.stream()// 1.得到容器的Steam
    .filter(str -> Character.isDigit(str.charAt(0)))// 2.选出以数字开头的字符串
    .forEach(str -> System.out.println(str));// 3.输出字符串

上述代码首先1. 调用List.stream()方法得到容器的Stream,2. 然后调用filter()方法过滤出以数字开头的字符串,3. 最后调用forEach()方法输出结果。

使用Stream有两个明显的好处:

  1. 减少了模板代码,只用Lambda表达式指明所需操作,代码语义更加明确、便于阅读。
  2. 将外部迭代改成了Stream的内部迭代,方便了JVM本身对迭代过程做优化(比如可以并行迭代)。

 假设需要从一个字符串列表中,选出所有不以数字开头的字符串,将其转换成大写形式,并把结果放到新的集合当中。Java 8书写的代码如下:

List<String> list = Arrays.asList("1one", "two", "three", "4four");
Set<String> newList =
        list.stream()// 1.得到容器的Stream
        .filter(str -> !Character.isDigit(str.charAt(0)))// 2.选出不以数字开头的字符串
        .map(String::toUpperCase)// 3.转换成大写形式
        .collect(Collectors.toSet());// 4.生成结果集

上述代码首先1. 调用List.stream()方法得到容器的Stream,2. 然后调用filter()方法选出不以数字开头的字符串,3. 之后调用map()方法将字符串转换成大写形式,4. 最后调用collect()方法将结果转换成Set。这个例子还向我们展示了方法引用method references,代码中标号3处)以及收集器Collector,代码中标号4处)的用法,这里不再展开说明。

通过这个例子我们看到了Stream链式操作,即多个操作可以连成一串。不用担心这会导致对容器的多次迭代,因为不是每个Stream的操作都会立即执行。Stream的操作分成两类,一类是中间操作(intermediate operations),另一类是结束操作(terminal operation),只有结束操作才会导致真正的代码执行,中间操作只会做一些标记,表示需要对Stream进行某种操作。这意味着可以在Stream上通过关联多种操作,但最终只需要一次迭代。如果你熟悉Spark RDD,对此应该并不陌生。

5.Stream中自定义排序

    比如在一个集合中,集合装的是学生的对象,需要按照学生的对象的属性年龄来进行升序或者降序排序。

    使用stream().sorted()进行排序,需要该类实现 Comparable 接口,写一个比较的方法

package com.wx.test1;

import java.time.LocalDate;
import java.util.List;

public class StudentInfo implements Comparable<StudentInfo> {
    //名称
    private String name;
    //性别 true男 false女
    private Boolean gender;
    //年龄
    private Integer age;
    //身高
    private Double height;
    //出生日期
    private LocalDate birthday;

    public StudentInfo(String name, Boolean gender, Integer age, Double height, LocalDate birthday) {
        this.name = name;
        this.gender = gender;
        this.age = age;
        this.height = height;
        this.birthday = birthday;
    }

    @Override
    public int compareTo(StudentInfo ob) {
        return this.age.compareTo(ob.getAge());
    }

    public String getName() {
        return name;
    }

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

    public Boolean getGender() {
        return gender;
    }

    public void setGender(Boolean gender) {
        this.gender = gender;
    }

    public Integer getAge() {
        return age;
    }

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

    public Double getHeight() {
        return height;
    }

    public void setHeight(Double height) {
        this.height = height;
    }

    public LocalDate getBirthday() {
        return birthday;
    }

    public void setBirthday(LocalDate birthday) {
        this.birthday = birthday;
    }
}

  添加数据,按照年龄升序排列:

package com.wx.test1;

import java.time.LocalDate;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.List;
import java.util.stream.Collectors;

public class Test3 {
    public static void main(String[] args) {
        //测试数据,请不要纠结数据的严谨性
        List<StudentInfo> studentList = new ArrayList<>();
        studentList.add(new StudentInfo("李小明",true,18,1.76,LocalDate.of(2001,3,23)));
        studentList.add(new StudentInfo("张小丽",false,18,1.61,LocalDate.of(2001,6,3)));
        studentList.add(new StudentInfo("王大朋",true,19,1.82,LocalDate.of(2000,3,11)));
        studentList.add(new StudentInfo("陈小跑",false,17,1.67,LocalDate.of(2002,10,18)));

        /**按年龄升序排序*/
        studentList.stream()
                .sorted(Comparator.comparing(StudentInfo::getAge)).collect(Collectors.toList()).forEach(studentinfon->System.out.println(studentinfon.getAge()));



    }
}

  测试结果:
 

如果我想输出整个对象的信息呢?,重写toString方法:

  public String toString() {
        String info = String.format("%s\t\t%s\t\t%s\t\t\t%s\t\t%s", this.name, this.gender.toString(), this.age.toString(), this.height.toString(), birthday.toString());
        return info;
    }

 

添加reversed(),按年龄降序排序:

studentList.stream()
                .sorted(Comparator.comparing(StudentInfo::getAge).reversed())
                .collect(Collectors.toList())
                .forEach(studentinfon->System.out.println(studentinfon.toString()));

使用年龄进行降序排序,年龄相同再使用身高升序排序

 studentList.stream()
                .sorted(Comparator.comparing(StudentInfo::getAge).reversed().thenComparing(StudentInfo::getHeight))
                .collect(Collectors.toList())
                .forEach(studentinfon->System.out.println(studentinfon.toString()));

 

 

6.Stream中的分组使用

 例子:

 随机生成50个小于100的整数,放入List中,将List中的数据除以10,以结果的整数值作为key放入Map中,得到 如{1=>[11,10,12],2=>[21,24,23]}的Map,再将Map中key对应的数组进行排序,得到如{1=> [10,11,12],2=>[21,23,24]}。排序不能使用List.sort() 方法。必须自己写排序方法。

要求:

使用工厂模式,创建一个接口类和两个实现类(2分)

创建一个工厂,生成基于给定信息的实体类的对象。(1分)

在main 函数中,通过上面的工厂获取到唯一的类。(1分)

两个实现类分别使用java 8 的Stream 和其他方式(4分)

实现功能 (4) 

分析:按照要求需要创建一个接口,这个接口是专门处理数据类的接口,两个实现类分别完成如下工作,第一个实现类完成数据的映射和分组,第二个实现类完成数据的自定义排序。

ok,首先我们完成一个基本的数据处理接口

package com.wx.test;

import java.util.Map;

public interface BaseInterface<T> {
    Map doData(T collection);
}

 实现这个接口,第一个类完成数据的映射,这里这是数据,如果是对象可以根据对象的属性来分组和排序,使用::就可以取到对象的属性了。

package com.wx.test;

import java.util.*;
import java.util.stream.Collectors;

public class MappingDate implements BaseInterface<List> {
    @Override
    public Map doData(List collection) {
        List<Integer> list = (List) collection;
        Map<Integer, List<Integer>> collect1 = list.stream()
                .collect(Collectors.groupingBy(o -> (Integer) (o / 10)));
        //但是要求需要将List<Integer>转化为数组
        Map<Integer, Integer[]> collect2 = new HashMap<Integer, Integer[]>();
        for (Integer integer : collect1.keySet()) {
            List<Integer> list1 = collect1.get(integer);
            Integer[] array = list1.stream().toArray(Integer[]::new);
            collect2.put(integer, array);
        }
        return collect2;
    }
}

 实现第二个类完成对数据的排序

package com.wx.test;

import java.util.Map;

public class SortDate implements BaseInterface<Map>{
    @Override
    public Map doData(Map collection) {
        //要求使用Stream,那又得转换为List,所以这里就不用了,直接对数组进行排序
        Map<Integer, Integer[]> collection1 = (Map<Integer, Integer[]>) collection;
        for (Integer integer : collection1.keySet()) {
            collection1.put(integer, sort(collection1.get(integer)));
        }
        return collection1;
    }

    //排序方法,希尔排序,是插入排序的一种
    private static Integer[] sort(Integer[] array) {
        int len = array.length;
        int temp;
        //设置增量,增量是数据长度的一半依次减一减到一
        for (int k = len / 2; k > 0; k--) {
            //将依据增量分割的序列进行对比
            for (int i = k; i < len; i++) {
                if (array[i - k] > array[i]) {
                    //交换数据
                    temp = array[i - k];
                    array[i - k] = array[i];
                    array[i] = temp;
                }
            }
        }
        return array;
    }
}

 实现一个工厂类:

package com.wx.test;

public class FactoryMethod {
    public <T extends BaseInterface> T getProuect(Class<T> c) {
        BaseInterface product = null;
        try {
            product = (T) Class.forName(c.getName()).newInstance();
        } catch (Exception e) {
            e.printStackTrace();
        }
        return (T) product;
    }
}

 好了,我们再来写main函数:

package com.wx.test;

import java.util.*;

public class TestOne {
    
    //接口类是对数据的处理接口
    public static void main(String[] args) {
        List<Integer> list = new ArrayList<Integer>();
        Random random = new Random();
        for (int i = 0; i < 50; i++) {
            //int num = random.nextInt(50) + 50;//生成范围为50到100的数
            int num = random.nextInt(100);
            list.add(num);
        }
        FactoryMethod factoryMethod=new FactoryMethod();
        MappingDate mapdata = factoryMethod.getProuect(MappingDate.class);
        //数据的映射,转化为Map<Integer, Integer[]>
        Map<Integer, Integer[]> map = mapdata.doData(list);
        //自定义排序
        SortDate sortDate = factoryMethod.getProuect(SortDate.class);
        Map<Integer, Integer[]> map1 = sortDate.doData(map);
        for (Integer integer : map1.keySet()) {
            System.out.println(integer);
           /* Arrays.asList(map1.get(integer)).stream()
                    .forEach(System.out::print);*/
            Integer[] integers = map1.get(integer);
            for (Integer integer1 : integers) {
                System.out.print(integer1+",");
            }
            System.out.println("");
        }
    }
}

 结果:

 

 

 

 

参考博客:

   

https://www.runoob.com/java/java8-streams.html

https://blog.csdn.net/zjy15203167987/article/details/88246482

https://www.cnblogs.com/kexianting/p/8588987.html

https://www.cnblogs.com/xisuo/p/9705944.html

https://www.cnblogs.com/CarpenterLee/p/5936664.html

 

    

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

时空恋旅人

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值