文章目录
- 1、为什么使用Lambda表达式?
- 2、Lambda表达式的使用
- 3、函数式(functional)接口
- 4、方法引用与构造器引用
-
- 5、Stream API
- Stream 概述
- Stream的创建
- Stream的中间操作
-
- Stream的终止操作
-
- 6、Optional类
- Java8思维导图
文章 | 链接 |
---|
Java语法 | https://blog.csdn.net/weixin_45606067/article/details/107049186 |
一维数组与二维数组、内存解析 | https://blog.csdn.net/weixin_45606067/article/details/107049178 |
面向对象(1/3)类和对象 | https://blog.csdn.net/weixin_45606067/article/details/108234276 |
面向对象(2/3)封装性、继承性、多态性 | https://blog.csdn.net/weixin_45606067/article/details/108234328 |
面向对象(3/3)抽象类、接口、内部类、代码块 | https://blog.csdn.net/weixin_45606067/article/details/108258152 |
异常处理 | 待更新 |
多线程(1/2) | https://blog.csdn.net/weixin_45606067/article/details/107067785 |
多线程(2/2) | https://blog.csdn.net/weixin_45606067/article/details/107067857 |
常用类 | https://blog.csdn.net/weixin_45606067/article/details/108283203 |
枚举与注解 | 待更新 |
集合(1/5)Collection、Iterator、增强for | https://blog.csdn.net/weixin_45606067/article/details/107046876 |
集合(2/5)List、ArrayList、LinkedList、Vector的底层源码 | https://blog.csdn.net/weixin_45606067/article/details/107069742 |
集合(3/5)set、HashSet、LinkedHashSet、TreeSet的底层源码 | |
集合(4/5)Map、HashMap底层原理分析 | https://blog.csdn.net/weixin_45606067/article/details/107042949 |
集合(5/5)LinkHashMap、TreeMap、Properties、Collections工具类 | https://blog.csdn.net/weixin_45606067/article/details/107069691 |
泛型与File | https://blog.csdn.net/weixin_45606067/article/details/107124099 |
IO流与网络编程 | https://blog.csdn.net/weixin_45606067/article/details/107143670 |
反射机制 | 待更新 |
Java8新特性 | https://blog.csdn.net/weixin_45606067/article/details/107280823 |
Java9/10/11新特性 | 待更新 |
1、为什么使用Lambda表达式?
Lambda是一个匿名函数,我们可以把Lambda表达式理解为是一段可以传递的代码(将代码像数据一样进行传递)可以写出更简洁、更灵活的代码。作为一种更紧凑的代码风格,使Java的语言表达能力得到了提升。
2、Lambda表达式的使用
1.举例: (o1,o2) -> Integer.compare(o1,o2);
2.格式:
- ->:lambda操作符 或 箭头操作符。
- ->左边:lambda形参列表 (其实就是接口中的抽象方法的形参列表)。
- ->右边:lambda体 (其实就是重写的抽象方法的方法体)。
3.Lambda表达式的使用:(分为6种情况介绍)
总结:
->左边:lambda形参列表的参数类型可以省略(类型推断);如果lambda形参列表只有一个参数,其一对()也可以省略。
->右边:lambda体应该使用一对{}包裹;如果lambda体只有一条执行语句(可能是return语句),省略这一对{}和return关键字。
4.Lambda表达式的本质:作为函数式接口的实例。
5.如果一个接口中,只声明了一个抽象方法,则此接口就称为函数式接口。
我们可以在一个接口上使用 @FunctionalInterface
注解,这样做可以检查它是否是一个函数式接口。
@FunctionalInterface
public interface MyInterface {
void method1();
}
6.所以以前用匿名实现类表示的现在都可以用Lambda表达式来写。
代码实现:
@Test
public void test1(){
Runnable r1 = new Runnable() {
@Override
public void run() {
System.out.println("我爱北京天安门");
}
};
r1.run();
System.out.println("***********************");
Runnable r2 = () -> {
System.out.println("我爱北京故宫");
};
r2.run();
}
@Test
public void test2(){
Consumer<String> con = new Consumer<String>() {
@Override
public void accept(String s) {
System.out.println(s);
}
};
con.accept("谎言和誓言的区别是什么?");
System.out.println("*******************");
Consumer<String> con1 = (String s) -> {
System.out.println(s);
};
con1.accept("一个是听得人当真了,一个是说的人当真了");
}
@Test
public void test3(){
Consumer<String> con1 = (String s) -> {
System.out.println(s);
};
con1.accept("一个是听得人当真了,一个是说的人当真了");
System.out.println("*******************");
Consumer<String> con2 = (s) -> {
System.out.println(s);
};
con2.accept("一个是听得人当真了,一个是说的人当真了");
System.out.println("*******************");
Consumer<String> con3 = s -> System.out.println(s);
con3.accept("一个是听得人当真了,一个是说的人当真了");
}
@Test
public void test4(){
Consumer<String> con1 = (s) -> {
System.out.println(s);
};
con1.accept("一个是听得人当真了,一个是说的人当真了");
System.out.println("*******************");
Consumer<String> con2 = s -> {
System.out.println(s);
};
con2.accept("一个是听得人当真了,一个是说的人当真了");
}
@Test
public void test5(){
Comparator<Integer> com1 = new Comparator<Integer>() {
@Override
public int compare(Integer o1, Integer o2) {
System.out.println(o1);
System.out.println(o2);
return o1.compareTo(o2);
}
};
System.out.println(com1.compare(12,21));
System.out.println("*****************************");
Comparator<Integer> com2 = (o1,o2) -> {
System.out.println(o1);
System.out.println(o2);
return o1.compareTo(o2);
};
System.out.println(com2.compare(12,6));
}
@Test
public void test6(){
Comparator<Integer> com1 = (o1,o2) -> {
return o1.compareTo(o2);
};
System.out.println(com1.compare(12,6));
System.out.println("*****************************");
Comparator<Integer> com2 = (o1, o2) -> o1.compareTo(o2);
System.out.println(com2.compare(12,21));
}
3、函数式(functional)接口
1.定义:只包含一个抽象方法的接口,称为 函数式接口。
2.在java.util.function包下定义了Java 8 的丰富的函数式接口。
3.Java内置的四大核心函数式接口
Consumer<T>消费性接口 void accept(T t)
Supplier<T>供给型接口 T get()
Function<T,R>函数型接口 R apply(T t)
Predicate<T>断定型接口 boolean test(T t)
@Test
public void test1(){
happyTime(500, new Consumer<Double>() {
@Override
public void accept(Double aDouble) {
System.out.println("学习太累了,去天上人间买了瓶矿泉水,价格为:" + aDouble);
}
});
System.out.println("********************");
happyTime(400,money -> System.out.println("学习太累了,去天上人间喝了口水,价格为:" + money));
}
public void happyTime(double money, Consumer<Double> con){
con.accept(money);
}
@Test
public void test2(){
List<String> list = Arrays.asList("北京","南京","天津","东京","西京","普京");
List<String> filterStrs = filterString(list, new Predicate<String>() {
@Override
public boolean test(String s) {
return s.contains("京");
}
});
System.out.println(filterStrs);
System.out.println("*********************");
List<String> filterStrs1 = filterString(list, s -> s.contains("京"));
System.out.println(filterStrs1);
}
public List<String> filterString(List<String> list, Predicate<String> pre){
ArrayList<String> filterList = new ArrayList<>();
for(String s : list){
if(pre.test(s)){
filterList.add(s);
}
}
return filterList;
}
其他接口
4、方法引用与构造器引用
方法引用的使用
-
使用情境:当要传递给Lambda体的操作,已经有实现的方法了,可以使用方法引用。
-
方法引用:本质上就是Lambda表达式,而Lambda表达式作为函数式接口的实例。
所以方法引用,也是函数式接口的实例。
-
使用格式: 类(或对象) :: 方法名
-
具体分为如下的三种情况:
- 情况1 对象 :: 非静态方法
- 情况2 类 :: 静态方法
- 情况3 类 :: 非静态方法
- 方法引用使用的要求:要求接口中的抽象方法的形参列表和返回值类型与方法引用的方法的形参列表和返回值类型相同!(针对于情况1和情况2)
@Test
public void test1() {
Consumer<String> con1 = str -> System.out.println(str);
con1.accept("北京");
System.out.println("*******************");
PrintStream ps = System.out;
Consumer<String> con2 = ps::println;
con2.accept("beijing");
}
@Test
public void test2() {
Employee emp = new Employee(1001,"Tom",23,5600);
Supplier<String> sup1 = () -> emp.getName();
System.out.println(sup1.get());
System.out.println("*******************");
Supplier<String> sup2 = emp::getName;
System.out.println(sup2.get());
}
@Test
public void test3() {
Comparator<Integer> com1 = (t1,t2) -> Integer.compare(t1,t2);
System.out.println(com1.compare(12,21));
System.out.println("*******************");
Comparator<Integer> com2 = Integer::compare;
System.out.println(com2.compare(12,3));
}
@Test
public void test4() {
Function<Double,Long> func = new Function<Double, Long>() {
@Override
public Long apply(Double d) {
return Math.round(d);
}
};
System.out.println("*******************");
Function<Double,Long> func1 = d -> Math.round(d);
System.out.println(func1.apply(12.3));
System.out.println("*******************");
Function<Double,Long> func2 = Math::round;
System.out.println(func2.apply(12.6));
}
@Test
public void test5() {
Comparator<String> com1 = (s1,s2) -> s1.compareTo(s2);
System.out.println(com1.compare("abc","abd"));
System.out.println("*******************");
Comparator<String> com2 = String :: compareTo;
System.out.println(com2.compare("abd","abm"));
}
@Test
public void test6() {
BiPredicate<String,String> pre1 = (s1,s2) -> s1.equals(s2);
System.out.println(pre1.test("abc","abc"));
System.out.println("*******************");
BiPredicate<String,String> pre2 = String :: equals;
System.out.println(pre2.test("abc","abd"));
}
@Test
public void test7() {
Employee employee = new Employee(1001, "Jerry", 23, 6000);
Function<Employee,String> func1 = e -> e.getName();
System.out.println(func1.apply(employee));
System.out.println("*******************");
Function<Employee,String> func2 = Employee::getName;
System.out.println(func2.apply(employee));
}
构造器引用的使用
-
构造器引用
和方法引用类似,函数式接口的抽象方法的形参列表和构造器的形参列表一致。
抽象方法的返回值类型即为构造器所属的类的类型。
-
数组引用
大家可以把数组看做是一个特殊的类,则写法与构造器引用一致。
@Test
public void test1(){
Supplier<Employee> sup = new Supplier<Employee>() {
@Override
public Employee get() {
return new Employee();
}
};
System.out.println("*******************");
Supplier<Employee> sup1 = () -> new Employee();
System.out.println(sup1.get());
System.out.println("*******************");
Supplier<Employee> sup2 = Employee :: new;
System.out.println(sup2.get());
}
@Test
public void test2(){
Function<Integer,Employee> func1 = id -> new Employee(id);
Employee employee = func1.apply(1001);
System.out.println(employee);
System.out.println("*******************");
Function<Integer,Employee> func2 = Employee :: new;
Employee employee1 = func2.apply(1002);
System.out.println(employee1);
}
@Test
public void test3(){
BiFunction<Integer,String,Employee> func1 = (id,name) -> new Employee(id,name);
System.out.println(func1.apply(1001,"Tom"));
System.out.println("*******************");
BiFunction<Integer,String,Employee> func2 = Employee :: new;
System.out.println(func2.apply(1002,"Tom"));
}
@Test
public void test4(){
Function<Integer,String[]> func1 = length -> new String[length];
String[] arr1 = func1.apply(5);
System.out.println(Arrays.toString(arr1));
System.out.println("*******************");
Function<Integer,String[]> func2 = String[] :: new;
String[] arr2 = func2.apply(10);
System.out.println(Arrays.toString(arr2));
}
5、Stream API
Stream 概述
Java 8 是一个非常成功的版本,这个版本新增的Stream,配合同版本出现的 Lambda
,给我们操作集合(Collection)提供了极大的便利。
那么什么是 Stream
?
Stream将要处理的元素集合看作一种流,在流的过程中,借助Stream API对流中的元素进行操作,比如:筛选、排序、聚合等。
Stream
可以由数组或集合创建,对流的操作分为两种:
- 中间操作,每次返回一个新的流,可以有多个。
- 终端操作,每个流只能进行一次终端操作,终端操作结束后流无法再次使用。终端操作会产生一个新的集合或值。
另外,Stream有几个特性:
- stream不存储数据,而是按照特定的规则对数据进行计算,一般会输出结果。
- stream不会改变数据源,通常情况下会产生一个新的集合或一个值。
- stream具有延迟执行特性,只有调用终端操作时,中间操作才会执行。
Stream的创建
- 通过Collection 系列集合提供的
stream()
或 parallelStream()
List<String> list = Arrays.asList("a", "b", "c");
Stream<String> stream = list.stream();
Stream<String> parallelStream = list.parallelStream();
- 通过Arrays中的静态方法
stream()
获取数组流
int[] array={1,3,5,6,8};
IntStream stream = Arrays.stream(array);
- 通过
Stream
的静态方法:of()
、iterate()
、generate()
Stream<String> stream2 = Stream.of("aaa", "bbb", "ccc");
Stream<Integer> stream3 = Stream.iterate(0, (x) -> x + 2);
stream3.limit(10).forEach(System.out::println);
Stream<Double> stream4 = Stream.generate(() -> Math.random());
stream4.limit(5).forEach(System.out::println);
stream
和parallelStream
的简单区分: stream是顺序流,由主线程按顺序对流执行操作,而parallelStream是并行流,内部以多线程并行执行的方式对流进行操作,但前提是流中的数据处理没有顺序要求。例如筛选集合中的奇数,两者的处理不同之处:
Stream的中间操作
1. 筛选与切片
- filter:接收Lambda,从流中排除掉某些元素
- limit:截断流,使其元素不超过给定数量
- skip(n):跳过元素,返回一个扔掉了前n个元素的流。若流中元素不足n个,则返回一个空流。与limit(n)互补。
- distinct:筛选,通过流所生成元素的hashCode() 和 equals() 去除重复元素
List<Employee> employees = Arrays.asList(
new Employee("张三",28,9999.99),
new Employee("李四",18,8888.99),
new Employee("王五",38,7777.99),
new Employee("孙刘",50,5555.99),
new Employee("田七",8,3333.99),
new Employee("李四",8,8888.99)
);
@Test
public void test1() {
Stream<Employee> stream = employees.stream()
.filter((e) -> e.getAge() > 35);
stream.forEach(System.out::println);
}
@Test
public void test2() {
Iterator<Employee> iterator = employees.iterator();
while(iterator.hasNext()) {
System.out.println(iterator.next());
}
}
@Test
public void test3() {
employees.stream()
.filter((e) -> e.getSalary() > 5000)
.limit(2)
.forEach(System.out::println);
}
@Test
public void test4() {
employees.stream()
.filter((e) -> e.getSalary() > 5000)
.skip(2)
.forEach(System.out::println);
}
@Test
public void test5() {
employees.stream()
.filter((e) -> e.getSalary() > 5000)
.distinct()
.forEach(System.out::println);
}
2. 映射
- map:接收Lambda,将元素转换成其他形式或提取信息。接收一个函数作为参数,该函数会被应用到每个元素上,并将其映射成一个新的元素。
- flatMap:接收一个函数作为参数,将流中的每个值都转换成另一个流,然后把所有流连接成一个流。
List<Employee> employees = Arrays.asList(
new Employee("张三",28,9999.99),
new Employee("李四",18,8888.99),
new Employee("王五",38,7777.99),
new Employee("孙刘",50,5555.99),
new Employee("田七",8,3333.99),
new Employee("李四",8,8888.99)
);
@Test
public void test6() {
List<String> list = Arrays.asList("aaa", "bbb", "ccc", "ddd");
list.stream()
.map((str) -> str.toUpperCase())
.forEach(System.out::println);
System.out.println("--------------");
employees.stream()
.map(Employee::getName)
.forEach(System.out::println);
System.out.println("--------------");
Stream<Stream<Character>> stream = list.stream()
.map(TestStreamAPI2::filterCharacher);
stream.forEach((sm) -> {
sm.forEach(System.out::println);
});
System.out.println("--------------");
Stream<Character> stream1 = list.stream()
.flatMap(TestStreamAPI2::filterCharacher);
stream1.forEach(System.out::println);
}
public static Stream<Character> filterCharacher(String str) {
List<Character> list = new ArrayList<>();
for (Character ch : str.toCharArray()) {
list.add(ch);
}
return list.stream();
}
3. 排序
- sorted():自然排序
- sorted(Comparator com):定制排序
@Test
public void test7() {
List<String> list = Arrays.asList("ccc","aaa", "ddd", "eee", "bbb");
list.stream()
.sorted()
.forEach(System.out::println);
System.out.println("------------");
employees.stream()
.sorted((e1, e2) -> {
if (e1.getAge().equals(e2.getAge())) {
return e1.getName().compareTo(e2.getName());
} else {
return e1.getAge().compareTo(e2.getAge());
}
}).forEach(System.out::println);
}
Stream的终止操作
1. 匹配与查找
- allMatch:检查是否匹配所有元素
- anyMatch:检查是否至少匹配一个元素
- noneMatch:检查是否没有匹配所有元素
- findFirst:返回第一个元素
- findAny:返回当前流中的任意元素
- count:返回流中元素的总个数
- max:返回流中最大值
- min:返回流中最小值
@Test
public void test1() {
boolean b1 = employees.stream()
.allMatch((e) -> e.getStatus().equals(Status.BUSY));
System.out.println(b1);
System.out.println("--------");
boolean b2 = employees.stream()
.anyMatch((e) -> e.getStatus().equals(Status.BUSY));
System.out.println(b2);
System.out.println("--------");
boolean b3 = employees.stream()
.noneMatch((e) -> e.getStatus().equals(Status.BUSY));
System.out.println(b3);
System.out.println("--------");
Optional<Employee> op = employees.stream()
.sorted((e1, e2) -> Double.compare(e1.getSalary(), e2.getSalary()))
.findFirst();
System.out.println(op.get());
System.out.println("--------");
Optional<Employee> op2 = employees.parallelStream()
.filter((e) -> e.getStatus().equals(Status.FREE))
.findAny();
System.out.println(op2.get());
}
@Test
public void test2() {
long count = employees.stream()
.count();
System.out.println(count);
Optional<Employee> max = employees.stream()
.max((e1, e2) -> Double.compare(e1.getSalary(), e2.getSalary()));
System.out.println(max);
Optional<Double> min = employees.stream()
.map(Employee::getSalary)
.min(Double::compare);
System.out.println(min);
}
2. 归约
- reduce(T identity,BinaryOperator) / reduce(BinarOperator):可以将流中元素反复结合起来,得到一个值
@Test
public void test3(){
List<Integer> list = Arrays.asList(1,2,3,4,5,6,7,8,9,10);
Integer sum = list.stream().reduce(0, Integer::sum);
System.out.println(sum);
Optional<Double> op = employees.stream()
.map(Employee::getSalary)
.reduce(Double::sum);
System.out.println(op);
}
3. 收集
- collect(Collector c)——将流转换为其他形式。接收一个 Collector接口的实现,用于给Stream中元素做汇总的方法
@Test
public void test4() {
List<String> list = employees.stream()
.map(Employee::getName)
.collect(Collectors.toList());
list.forEach(System.out::println);
System.out.println("------------");
Set<String> set = employees.stream()
.map(Employee::getName)
.collect(Collectors.toSet());
set.forEach(System.out::println);
System.out.println("------------");
HashSet<String> hs = employees.stream()
.map(Employee::getName)
.collect(Collectors.toCollection(HashSet::new));
hs.forEach(System.out::println);
}
- 计数:
counting
- 平均值:
averagingInt
、averagingLong
、averagingDouble
- 最值:
maxBy
、minBy
- 求和:
summingInt
、summingLong
、summingDouble
- 统计以上所有:
summarizingInt
、summarizingLong
、summarizingDouble
@Test
public void test5() {
Long count = employees.stream()
.collect(Collectors.counting());
System.out.println(count);
Double avg = employees.stream()
.collect(Collectors.averagingDouble(Employee::getSalary));
System.out.println(avg);
DoubleSummaryStatistics sum = employees.stream()
.collect(Collectors.summarizingDouble(Employee::getSalary));
System.out.println(sum);
Optional<Employee> max = employees.stream()
.collect(Collectors.maxBy((e1, e2) -> Double.compare(e1.getSalary(), e2.getSalary())));
System.out.println(max);
Optional<Double> min = employees.stream()
.map(Employee::getSalary)
.collect(Collectors.minBy(Double::compare));
System.out.println(min);
}
- 分组:将集合分为多个Map
- 分区:将stream按条件分为两个Map
@Test
public void test6() {
Map<Status, List<Employee>> map = employees.stream()
.collect(Collectors.groupingBy(Employee::getStatus));
System.out.println(map);
}
@Test
public void test7() {
Map<Status, Map<String, List<Employee>>> map = employees.stream()
.collect(Collectors.groupingBy(Employee::getStatus, Collectors.groupingBy((e) -> {
if (e.getAge() <= 35) {
return "青年";
} else if (e.getAge() <= 50) {
return "中年";
} else {
return "老年";
}
})));
System.out.println(map);
}
@Test
public void test8() {
Map<Boolean, List<Employee>> map = employees.stream()
.collect(Collectors.partitioningBy((e) -> e.getSalary() > 8000));
System.out.println(map);
}
- 连接(joining):可以将stream中的元素用特定的连接符(没有的话,则直接连接)连接成一个字符串
@Test
public void test10() {
String str = employees.stream()
.map(Employee::getName)
.collect(Collectors.joining(","));
System.out.println(str);
}
@Test
public void test9() {
DoubleSummaryStatistics dss = employees.stream()
.collect(Collectors.summarizingDouble(Employee::getSalary));
System.out.println(dss.getAverage());
System.out.println(dss.getCount());
System.out.println(dss.getMin());
}
以上应用到的 Employee
实体类
public class Employee {
private String name;
private Integer age;
private double salary;
private Status status;
public enum Status{
FREE,
BUSY,
VOCATION;
}
}
6、Optional类
Optional 容器类的常用方法:
- Optional.of(T t) : 创建一个 Optional 实例,t必须非空;
- Optional.empty() : 创建一个空的 Optional 实例
- Optional.ofNullable(T t):若 t 不为null,创建Optional 实例,否则创建空实例
- isPresent():判断是否包含值
- orElse(T t):判断是否包含值
- orElseGet(Supplier s):如果调用对象包含值,返回值,否则返回s获取的值map(Function f):如果有值对其处理,并返回处理后的
- Optional,否则返回Optional.empty()
- flatMap(Function mapper):与map类似,要求返回值必须是Optional
public class OptionalTest {
@Test
public void test1(){
Girl girl = new Girl();
Optional<Girl> optionalGirl = Optional.of(girl);
}
@Test
public void test2(){
Girl girl = new Girl();
Optional<Girl> optionalGirl = Optional.ofNullable(girl);
System.out.println(optionalGirl);
Girl girl1 = optionalGirl.orElse(new Girl("赵丽颖"));
System.out.println(girl1);
}
public String getGirlName(Boy boy){
return boy.getGirl().getName();
}
@Test
public void test3(){
Boy boy = new Boy();
boy = null;
String girlName = getGirlName(boy);
System.out.println(girlName);
}
public String getGirlName1(Boy boy){
if(boy != null){
Girl girl = boy.getGirl();
if(girl != null){
return girl.getName();
}
}
return null;
}
@Test
public void test4(){
Boy boy = new Boy();
boy = null;
String girlName = getGirlName1(boy);
System.out.println(girlName);
}
public String getGirlName2(Boy boy){
Optional<Boy> boyOptional = Optional.ofNullable(boy);
Boy boy1 = boyOptional.orElse(new Boy(new Girl("迪丽热巴")));
Girl girl = boy1.getGirl();
Optional<Girl> girlOptional = Optional.ofNullable(girl);
Girl girl1 = girlOptional.orElse(new Girl("古力娜扎"));
return girl1.getName();
}
@Test
public void test5(){
Boy boy = null;
boy = new Boy();
boy = new Boy(new Girl("苍老师"));
String girlName = getGirlName2(boy);
System.out.println(girlName);
}
}
public class Boy {
private Girl girl;
@Override
public String toString() {
return "Boy{" +
"girl=" + girl +
'}';
}
public Girl getGirl() {
return girl;
}
public void setGirl(Girl girl) {
this.girl = girl;
}
public Boy() {
}
public Boy(Girl girl) {
this.girl = girl;
}
}
public class Girl {
private String name;
@Override
public String toString() {
return "Girl{" +
"name='" + name + '\'' +
'}';
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public Girl() {
}
public Girl(String name) {
this.name = name;
}
}
Java8思维导图
如果有收获!!! 希望老铁们来个三连,点赞、收藏、转发
创作不易,别忘点个赞,可以让更多的人看到这篇文章,顺便鼓励我写出更好的博客
本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)