Lambda表达式、Stream流
JDK8的新特性有:
- Lambda表达式
- 更强的Strean API
- 方法引用/构造器引用
- 函数式接口
- Optional
- 接口的增强
- 新的时间和日期API
- 其它新特性,如注解中的重复注解、类型注解,HashMap底层的变化,JVM中Metaspace取代PermGen空间等
函数式编程:
- Lambda表达式
- 函数式接口
- 方法引用/构造器引用
- 更强的Strean API
- Optional
函数式编程案例
@Test
public void test88() {
List<Banji> banjis = getBanjis();
List<Integer> list = new ArrayList<>();
for (Banji banji : banjis) {
List<Student> students = banji.getStudents();
for (Student student : students) {
if (student.getAge() > 18) {
list.add(student.getAge());
}
}
}
System.out.println(list);
System.out.println("------------------");
List<Integer> list1 = banjis.stream()
//faltmap将students对象遍历成流
.flatMap(banji -> banji.getStudents().stream())
//filter 过滤数据
.filter(student -> student.getAge() > 18)
//提取对象中的属性,将student类转换成integer
.map(student -> student.getAge())
//手机map的返回值变为集合,并返回值
.collect(Collectors.toList());
System.out.println(list1);
}
Lambda表达式
Lambda是JDK8中一个语法糖。 他可以对某些匿名内部类的写法进行简化,它是函数式编程思想的一个重要体现。 让我们不用关注是什么对象,而是更关注我们对数据进行了什么操作。 Lambda表达式作用:简化匿名内部类的代码写法。 核心原则:可推导可省略 如果一些参数的类型可以被推导出来,就可以省略类型,如果方法名可以被推导出来,就可以省略方法名
Lambda的标准格式:
(参数列表) -> {
方法体
}
1.(参数列表):参数列表
2.{}:方法体
3.->:没有实际含义,起到连接的作用
组成Lambda表达式的三要素:形式参数,箭头,代码块
Thread thread = new Thread(new MyRunnable());
thread.start();
new Thread(new MyRunnable()).start();
// 使用匿名内部类方式可以帮我们省去实现类的定义
// 1、定义一个没有名字的类
// 2、这个类实现了Runnable接口
// 3、创建了这个类对象
// 使用匿名内部类的语法是冗余的
new Thread(new Runnable() {
@Override
public void run() {
System.out.println(Thread.currentThread().getName());
}
}, "myThread").start();
// 其实我们最关注的是run方法和里面要执行的代码,为了指定run的方法体,不得不需要Runnable接口的实现类;
// 必须覆盖重写抽象run方法,所以方法名称、方法参数、方法返回值不得不再写一遍,且不能写错;
// Lambda表达式体现的是函数式编程思想,只需要将要执行的代码放到函数中
// (参数列表) -> {
// }
new Thread(() -> {
System.out.println("LambdaDemo.main");
}).start();
//排序的两种方式
@Test
public void test12() {
ArrayList<Student> list = new ArrayList<>();
list.add(new Student(1, "张三", 34));
list.add(new Student(1, "李四", 45));
list.add(new Student(1, "王五", 87));
list.add(new Student(1, "赵六", 22));
Collections.sort(list, new Comparator<Student>() {
@Override
public int compare(Student o1, Student o2) {
return o1.getAge() - o2.getAge();//升序
}
});
Collections.sort(list, (Student o1, Student o2) -> {
return o1.getAge() - o2.getAge();
});
Collections.sort(list, (o1, o2) -> o1.getAge() - o2.getAge());
for (Student student : list) {
System.out.println(student);
}
}
@Data
@AllArgsConstructor
@NoArgsConstructor
@EqualsAndHashCode//用于后期的去重使用
public class Student implements Comparable<Student>{
private Integer id;
private String name;
private Integer age;
@Override
public int compareTo(Student o) {
return this.age - o.age;
}
}
注意:事实上,把Lambda表达式可以看做是匿名内部类的一种简写方式。当然,前提是这个匿名内部类对应的必须是接口,而且接口中必须只有一个函数! Lambda表达式就是直接编写函数的:参数列表、代码体、返回值等信息,用函数来代替完整的匿名内部类!
接口中必须只有一个函数,所以使用Lambda表达式时候就没有必要告诉我方法名字是什么了,不会有歧义,因为就只有一个。原来是匿名内部类形式,new 接口名 也可以省略
函数式接口:@FunctionalInterface
@FunctionalInterface
public interface Runnable {
public abstract void run();
}
@FunctionalInterface
public interface Comparator<T> {
int compare(T o1, T o2);
}
Lambda省略格式:
Lanbda表达式是可推导,可以省略,凡是根据上下文推导出来的内容,都可以省略书写
在Lambda标准格式的基础上,使用省略写法的规则为:
1、小括号内参数的类型可以省略,因为可由编译器推断得出,称为“类型推断”
2、如果小括号内有且仅有一个参数,则小括号可以省略
3、如果大括号内有且仅有一个语句,可以同时省略大括号、return关键字及分号
注意:大括号,return,分号必须一起省略
(int a)->{
return new Student();
}
省略后:
a -> new Student()
new Thread(() -> System.out.println("Lambda执行线程11")).start();
Collections.sort(list, (o1, o2) -> o1.getAge() - o2.getAge());
函数时接口
| 函数式接口 | 抽象方法 | 典型用途 |
|---|---|---|
Consumer<T> |
void accept(T t) |
消费一个值,不返回结果 |
Supplier<T> |
T get() |
提供一个值 |
Function<T, R> |
R apply(T t) |
把 T 转换成 R |
Predicate<T> |
boolean test(T t) |
条件判断 |
UnaryOperator<T> |
T apply(T t) |
T 转换成 T |
BinaryOperator<T> |
T apply(T a, T b) |
两个 T 合成一个 T |
BiFunction<T, U, R> |
R apply(T t, U u) |
两个参数转换成一个结果 |
Stream流
Stream流简介
Java8的Stream使用的是函数式编程模式,如同它的名字一样,它可以被用来对集合或数组进行链状流式的操作。可以更方便的让我们对集合或数组操作,使用上类似于使用SQL 执行的数据库查询。 和之前讲的IO流是不一样的,这里是对集合或数组做操作。
Stream 和Collection 集合的区别:“集合讲的是数据,Stream讲的是计算!”
1.Collection 是一种静态的内存数据结构,而Stream 是有关计算的。
2.Collection是主要面向内存,存储在内存中,Stream主要是面向CPU,通过CPU 实现计算。
注意: 1.Stream 自己不会存储元素。 2.Stream 不会改变源对象,相反,他们会返回一个持有结果的新Stream。 3.Stream 操作是延迟执行的,这意味着他们会等到需要结果的时候才执行,也就是执行到第三步终结操作时候才执行。可以使用CUP并行计算
Stream操作步骤
创建流 : 一个数据源(如:集合、数组),获取一个流 单列集合: 集合对象.stream() 数组:Arrays.stream(数组)或者使用Stream.of来创建 双列集合:转换成单列集合后再创建
中间操作: 一个中间操作链,对数据源的数据进行处理 filter、map、distinct、sorted、limit、skip、flatMap
终结操作 一旦执行终止操作,就执行中间操作链,并产生结果。之后,不会再被使用 forEach、count、max、min、collect、reduce
创建流的方式
1、单列集合: 集合对象.stream() Java8 中的Collection 接口被扩展,提供了两个获取流的方法:
ldefault Stream<E> stream() : 返回一个顺序流
ldefault Stream<E> parallelStream() : 返回一个并行流
List<Student> students = getStudents();
Stream<Student> stream = students.stream();
2、双列集合:转换成单列集合后再创建 MapStream<Map.Entry<String, String>> stream = map.entrySet().stream();
3、数组:Arrays.stream(数组)或者使用Stream.of来创建 Integer[] array = {1, 2, 3, 4};
Stream<Integer> stream = Arrays.stream(array);
Stream<Integer> stream = Stream.of(array);
//1.Collection集合获取流
List<Student> students = getStudents();
Stream<Student> stream1 = students.stream();
//2.Map通过entrySet获取流
Map<String, String> map = new HashMap<>();
Stream<Map.Entry<String, String>> stream2 = map.entrySet().stream();
//3.数组生成流
Integer[] array = {1, 2, 3, 4};
Stream<Integer> stream3 = Arrays.stream(array);
Stream<Integer> stream4 = Stream.of(array);
终止操作
| 操作 | 作用 |
|---|---|
toList() |
收集为不可变 List(Java 16+) |
collect(...) |
使用收集器收集结果 |
forEach |
遍历处理 |
count |
统计数量 |
min / max |
求最小值/最大值 |
reduce |
归约合并 |
anyMatch / allMatch / noneMatch |
条件匹配 |
findFirst / findAny |
查找元素,返回 Optional |
1. count 统计元素数量
long count = banjis.stream()
.flatMap(banji -> banji.getStudents().stream())
.distinct()
.count();
System.out.println(count);
2. max / min 求最大、最小值,返回Optional
Optional<Integer> max = banjis.stream()
.flatMap(banji -> banji.getStudents().stream())
.map(student -> student.getAge())
.max((age1, age2) -> age1 - age2);
Optional<Integer> min = banjis.stream()
.flatMap(banji -> banji.getStudents().stream())
.map(student -> student.getAge())
.min((age1, age2) -> age1 - age2);
System.out.println(max.get());
System.out.println(min.get());
3. anyMatch / findAny
anyMatch():任意一个满足条件返回truefindAny():找到任意一个匹配元素,返回Optional
//是否存在年龄大于18的学生
boolean flag = banjis.stream()
.flatMap(banji -> banji.getStudents().stream())
.anyMatch(student -> student.getAge() > 18);
//获取任意一个大于18岁学生
Optional<Student> optionalStudent = banjis.stream()
.flatMap(banji -> banji.getStudents().stream())
.filter(student -> student.getAge() > 18)
.findAny();
optionalStudent.ifPresent(student -> System.out.println(student));
4. reduce 归约,反复运算得到一个结果(求和、累乘)
//数组求和
int[] array = {1,2,3,4,5,6,7,8,9,10};
int sum11 = Arrays.stream(array)
.reduce(0, (left, right) -> left + right);
//集合求和
List<Integer> list = Arrays.asList(1,2,3,4,5,6,7,8,9,10);
Integer sum2 = list.stream().reduce(0, (a, b) -> a + b);
Integer sum4 = list.stream().reduce(0, Integer::sum);
//所有学生年龄总和,无初始值版本
Optional<Integer> sumOptional2 = banjis.stream()
.flatMap(banji -> banji.getStudents().stream())
.map(student -> student.getAge())
.reduce((a, b) -> a + b);
System.out.println(sumOptional2.get());
5. collect 收集器,把流转回集合
收集为 List / Set
List<String> nameList = banjis.stream()
.flatMap(banji -> banji.getStudents().stream())
.map(student -> student.getName())
.collect(Collectors.toList());
Set<String> nameSet = banjis.stream()
.flatMap(banji -> banji.getStudents().stream())
.map(student -> student.getName())
.collect(Collectors.toSet());
中间操作
| 操作 | 作用 |
|---|---|
filter |
过滤(筛选)出符合指定条件的元素 |
dropWhile |
从开头丢弃满足指定条件的元素,直到条件首次不满足为止,然后保留剩余的全部元素。 |
takeWhile |
从开头取满足条件的元素,遇到第一个不满足的就停止。 |
map |
一对一转换,映射 |
distinct |
去重 |
sorted |
排序 |
limit |
截取前 n 个元素 |
skip |
跳过前 n 个元素 |
peek |
调试观察,但不改变元素。类似于forEach,但其为中间操作。 |
flatMap |
一对多展开 |
1. flatMap 扁平化流(把集合里面的子集合拆开)
场景:List<Banji>每个班级内部有List<Student>,把多层嵌套打平成学生流。
//普通双层for循环
List<Banji> banjis = getBanjis();
List<Integer> list = new ArrayList<>();
for (Banji banji : banjis) {
List<Student> students = banji.getStudents();
for (Student student : students) {
if (student.getAge() > 18) {
list.add(student.getAge());
}
}
}
//Stream flatMap扁平化
List<Integer> list1 = banjis.stream()
.flatMap(banji -> banji.getStudents().stream()) //把班级流 → 学生流
.filter(student -> student.getAge() > 18)
.map(student -> student.getAge())
.collect(Collectors.toList());
2. distinct 去重
需要实体重写
equals()和hashCode()
students.stream()
.distinct()
.forEach(student -> System.out.println(student));
3. filter 过滤
//匿名内部类写法
students.stream()
.distinct()
.filter(new Predicate<Student>() {
@Override
public boolean test(Student student) {
return student.getName().length() > 1;
}
});
//lambda简写
students.stream()
.distinct()
.filter(student -> student.getName().length() > 1)
.forEach(student -> System.out.println(student));
4. map 映射转换(类型转换)
把Student对象提取为name字符串
students.stream()
.distinct()
.map(student -> student.getName())
.forEach(name -> System.out.println(name));
5. sorted 排序
//自然排序,Student需要实现Comparable接口
students.stream().distinct().sorted();
//自定义比较器:按年龄降序
students.stream()
.distinct()
.sorted((o1, o2) -> o2.getAge() - o1.getAge())
.forEach(student -> System.out.println(student));
6. skip / limit 截取
skip(n):跳过前n个元素limit(n):只取前n个元素
//跳过1个
students.stream()
.distinct()
.sorted()
.skip(1)
.forEach(student -> System.out.println(student));
//只取2个
students.stream()
.distinct()
.sorted()
.limit(2)
.forEach(student -> System.out.println(student));
##
收集为 Map
key:班级名称,value:班级学生集合
List<Student>
List<Banji> banjis = getBanjis();
//Map<String, List<Student>> map = banjis.stream()
// .distinct()
// .collect(Collectors.toMap(banji -> banji.getName(), banji -> banji.getStudents()));
//Map<String, List<Student>> map;
banjis.stream()
.distinct()
.collect(Collectors.toMap(new Function<Banji, String>() {
@Override
public String apply(Banji banji) {
return banji.getName();
}
}, new Function<Banji, List<Student>>() {
@Override
public List<Student> apply(Banji banji) {
return banji.getStudents();
}
}));
Map<String, List<Student>> map2 = banjis.stream()
.distinct()
.collect(Collectors.toMap(banji -> banji.getName(), banji -> banji.getStudents()));
- 中间操作:
flatMap、filter、map、distinct、sorted、skip、limit,返回Stream,惰性求值,不真正执行。 - 终止操作:
collect、count、max/min、anyMatch、findAny、reduce、forEach,触发真正执行,流关闭,不可复用。
⚠️流只能消费一次,执行终止操作之后,这个流不能再次使用。
Optional
概述
我们在编写代码的时候出现最多的就是空指针异常。所以在很多情况下我们需要做各种非空的判断。
public class Demo {
@Test
public void test1() {
Student student = getStudent();
if (student != null) {
System.out.println(student.getName());
}
}
private Student getStudent() {
Student student = new Student(1, "zhangsan", 23);
return student;
}
}
创建对象Optional.ofNullable、安全消费值ifPresent
Optional就好像是包装类,可以把我们的具体数据封装Optional对象内部。然后我们去使用Optional中封装好的方法操作封装进去的数据就可以非常优雅的避免空指针异常。
我们一般使用Optional的静态方法ofNullable来把数据封装成一个Optional对象。无论传入的参数是否为null都不会出现问题。
@Test
public void test2() {
Student student = getStudent();
Optional<Student> studentOptional = Optional.ofNullable(student);
//ifPresent 这个方法会判断其内封装的数据是否为空,不为空时才会执行具体的消费代码
//如果返回Optional.ofNullable(null);则不会进行消费
studentOptional.ifPresent(stu -> System.out.println(stu.getName()));
}
实际开发中我们的数据很多是框架给我们封装好的,返回的就是Optional,封装的过程也不需要我们自己操作。
@Test
public void test3() {
Optional<Student> studentOptional = getStudentOptional();
studentOptional.ifPresent(student -> System.out.println(student.getName()));
}
private Optional<Student> getStudentOptional() {
Student student = new Student(1, "zhangsan", 23);
return Optional.ofNullable(student);
}
安全获取值
orElseGet 获取数据并且设置数据为空时的默认值 orElseThrow 获取数据,如果数据不为空就能获取到该数据。如果为空则根据你传入的参数来创建异常抛出。
@Test
public void test4() {
Optional<Student> studentOptional = getStudentOptional();
//Student student1 = studentOptional.get();
//System.out.println(student1.getName());
Student student2 = studentOptional.orElseGet(() -> new Student(2, "lisi", 23));
System.out.println(student2.getName());
try {
Student student3 = studentOptional.orElseThrow(() -> new RuntimeException("student为空"));
System.out.println(student3);
} catch (Throwable throwable) {
throwable.printStackTrace();
}
}
private Optional<Student> getStudentOptional() {
Student student = new Student(1, "zhangsan", 23);
return Optional.ofNullable(null);
}
过滤
我们可以使用filter方法对数据进行过滤。如果原本是有数据的,但是不符合判断,也会变成一个无数据的Optional对象。
@Test
public void test5() {
Optional<Student> studentOptional = getStudentOptional();
studentOptional.filter(student -> student.getAge() > 18)
.ifPresent(student -> System.out.println(student.getName()));
}
isPresent方法进行是否存在数据的判断。
@Test
public void test6() {
Optional<Student> studentOptional = getStudentOptional();
if (studentOptional.isPresent()) {
System.out.println(studentOptional.get().getId());
System.out.println(studentOptional.get().getName());
System.out.println(studentOptional.get().getAge());
}
}
方法引用
概念
当 Lambda 的方法体只有一行,且仅仅是调用一个方法(普通方法/静态方法/构造器),就可以使用方法引用进一步简化代码。
本质:方法引用就是 Lambda 的简化写法。
格式:类名或者对象名::方法名
使用小技巧:写完 Lambda,如果只有一行方法调用,IDEA 会提示,可以快捷键直接转换成方法引用,不用死记格式。
四种方法引用形式
1. 类::静态方法
适用条件:
- Lambda体只有一行,调用类的静态方法
- Lambda抽象方法的所有参数,按顺序传给这个静态方法
// Lambda
.map(age -> String.valueOf(age))
// 方法引用
.map(String::valueOf)
2. 对象::实例方法
适用条件:
- Lambda体只有一行,调用外部对象的成员方法
- Lambda抽象方法的所有参数,按顺序传给该成员方法
PrintStream ps = System.out;
// Lambda
.forEach(s -> ps.println(s));
// 方法引用
.forEach(ps::println);
//案例
StringBuilder stringBuilder = new StringBuilder();
//lambda
.forEach(name -> stringBuilder.append(name));
//方法引用
.forEach(stringBuilder::append);
3. 类::实例方法
特殊!不是拿对象调用,而是写类名调用实例方法。
适用条件: 函数式接口抽象方法第一个参数,作为实例方法的调用者;后面的参数作为实例方法的入参。
示例:
String[] strings = {"apple", "banana", "cherry"};
// Lambda
Arrays.sort(strings, (s1, s2) -> s1.compareToIgnoreCase(s2));
// 方法引用:s1是第一个参数,作为调用者;s2是第二个参数作为方法入参
Arrays.sort(strings, String::compareToIgnoreCase);
// 获取学生姓名
//lambda
.map(student -> student.getName())
//方法引用
.map(Student::getName)
4. 构造器引用 类名::new
适用条件:
- Lambda体只有一行,调用构造方法
- 构造器参数列表 和 函数式接口抽象方法参数列表保持一致,返回值就是该类对象。
格式:类名::new
//Lambda
.map(name -> new StringBuilder(name))
//构造器引用
.map(StringBuilder::new)
完整测试案例
@Test
public void test12() {
List<Banji> banjis = getBanjis();
//原始匿名内部类写法
banjis.stream()
.flatMap(banji -> banji.getStudents().stream())
.map(student -> student.getAge())
.map(new Function<Integer, String>() {
@Override
public String apply(Integer age) {
return String.valueOf(age);
}
})
.forEach(new Consumer<String>() {
@Override
public void accept(String age) {
System.out.println(age);
}
});
//方法引用简化
banjis.stream()
.flatMap(banji -> banji.getStudents().stream())
.map(student -> student.getAge())
.map(String::valueOf) //类::静态方法
.forEach(System.out::println); //对象::实例方法
}
@Test
public void test13() {
StringBuilder stringBuilder = new StringBuilder();
List<Banji> banjis = getBanjis();
//匿名内部类
banjis.stream()
.flatMap(banji -> banji.getStudents().stream())
.map(new Function<Student, String>() {
@Override
public String apply(Student student) {
return student.getName();
}
})
.forEach(new Consumer<String>() {
@Override
public void accept(String name) {
stringBuilder.append(name);
}
});
//方法引用
banjis.stream()
.flatMap(banji -> banji.getStudents().stream())
.map(Student::getName) //类::实例方法
.forEach(stringBuilder::append); //对象::实例方法
}
@Test
public void test54() {
List<Banji> banjis = getBanjis();
//lambda写法
banjis.stream()
.flatMap(banji -> banji.getStudents().stream())
.map(student -> student.getName())
.map(name -> new StringBuilder(name))
.map(sb->sb.append("-java"))
.forEach(str-> System.out.println(str));
//构造器引用简化
banjis.stream()
.flatMap(banji -> banji.getStudents().stream())
.map(Student::getName)
.map(StringBuilder::new) //构造器引用 类::new
.map(sb->sb.append("-java"))
.forEach(System.out::println);
}
四种方法引用总结表格
| 类型 | 写法 | 使用场景 |
|---|---|---|
| 类::静态方法 | String::valueOf |
调用静态方法,参数一一对应 |
| 对象::实例方法 | System.out::println |
外部对象调用成员方法,参数一一对应 |
| 类::实例方法 | Student::getName |
第一个参数作为调用者,剩余参数作为方法实参 |
| 构造器引用 | StringBuilder::new |
调用构造方法,参数列表匹配 |
记忆关键点:
1. Lambda体仅有一行方法调用,才可以转方法引用
2. 参数、返回值必须和函数式接口抽象方法匹配
3. IDEA有提示,优先看提示转换,不用死记硬背
基本数据类型优化
JDK5中引入的自动装箱和自动拆箱让我们在使用对应的包装类时就好像使用基本数据类型一样方便。但频繁的装箱和拆箱存在时间的消耗 所以为了让我们能够对这部分的时间消耗进行优化。Stream还提供了很多专门针对基本数据类型的方法。 例如:mapToInt,mapToLong,mapToDouble,flatMapToInt,flatMapToDouble等。
@Test
public void test55() {
List<Banji> banjis = getBanjis();
banjis.stream()
.flatMap(banji -> banji.getStudents().stream())
.map(student -> student.getAge())
.map(age -> age + 10)
.filter(age -> age > 18)
.map(age -> age + 2)
.forEach(System.out::println);
banjis.stream()
.flatMap(banji -> banji.getStudents().stream())
.mapToInt(student -> student.getAge()) //转换成int,避免后面装箱和拆箱过程
.map(age -> age + 10)
.filter(age -> age > 18)
.map(age -> age + 2)
.forEach(System.out::println);
}
JDK8接口新增的两个方法:
JDK8之前:接口是一个纯粹的抽象类 JDK8之后: 1、默认方法 2、静态方法
interface 接口{
静态常量; // public static final
抽象方法; // public abstract
默认方法;
静态方法;
}
默认方法
接口引入默认方法的背景: 如果给接口新增抽象方法,所有实现类都必须重写这个抽象方法。不利于接口的扩展。 例如,JDK8时在Map接口中增加了forEach方法:
public interface Map<K,V> {
abstract void forEach(BiConsumer<? super K, ? super V> action);
}
Map接口的实现类有非常多:
如果在Map接口中增加一个抽象方法,所有的实现类都需要去实现这个方法,那么工程是时巨大的。 因此,在JDK 8时为接口新增了默认方法:
public interface Map<K,V> {
default void forEach(BiConsumer<? super K, ? super V> action) {
//....
}
}
接口中的默认方法实现类不必重写,可以直接使用,实现类也可以根据需要重写。这样就方便接口的扩展
public interface IDB {
abstract void test1();
default void test2() {
// 这里面可以功能,也可以让子类来重写
}
static void test3() {
System.out.println("IDB.test3");
}
}
public class MySql implements IDB{
@Override
public void test1() {
System.out.println("MySql.test1");
}
}
###静态方法
interface 接口名 {
修饰符 static 返回值类型 方法名() {
代码;
}
}
直接使用接口名调用即可:接口名.静态方法名(); IDB.test3();
接口默认方法和静态方法的区别
1.默认方法通过实例调用,静态方法通过接口名调用。IDB.test3();
2.默认方法可以被继承,实现类可以直接使用接口默认方法,也可以重写接口默认方法。
3.静态方法不能被继承,实现类不能重写接口静态方法,只能使用接口名调用
注意:尽管默认方法有这么多好处,但在实际开发中应该谨慎使用
默认方法和静态方法,如何选择呢? 如果这个方法要被实现类继承或重写,使用默认方法,如果接口中的方法不需要被继承就使用静态方法