目录
- 1. lambda表达式介绍及基本语法
- 1.1 为什么要使用lambda?
- 1.2 lambda基本语法
- 1.3 函数式接口
- 1.3.1 默认方法
- 1.3.2 静态方法
- 1.4 lambda表达式和匿名内部类的区别
- 2. lambda表达式使用案例
- 2.1 数据流stream
- 2.1.1 filter过滤
- 2.1.2 map映射
- 2.1.3 排序sorted
- 2.1.4 聚合reduce
- 2.1.5 收集collect
- 2.2 Optional
- 2.2.1 创建Optional
- 2.2.2 访问Optional
- 2.2.3 处理空值
- 2.2.4 转换操作
- 3 方法引用
- 3.1 方法引用类别
- 3.2 一些区别和注意点
1. lambda表达式介绍及基本语法
1.1 为什么要使用lambda?
lambda表达式是Java 8引入的一种新特性,用于简化函数式接口的实现,能够替代函数形参中需要实现的匿名内部类,使得代码更加清晰简洁,此外,在Java集合、数据流中也常常能见到它的身影。
比如若要创建一个线程,需要在new出线程的同时实现参数Runnable
接口,这就是一个实现了Runnable
接口的匿名内部类:
Thread thread = new Thread(new Runnable() {@Overridepublic void run() {System.out.println(" runing ...");}
});
而如果使用lambda表达式,上述代码可以简化成:
Thread thread = new Thread(() -> { System.out.println(" runing... "); });
可以很直观的看到lambda表达式省略了大量非重要代码(样板代码),只保留具体实现内容。
1.2 lambda基本语法
lambda表达式是一种匿名函数,没有名称,但需要提供参数列表、函数主体和返回值,其基本语法为:
// 0. 基本语法,params为0~n个参数,body为函数体,逻辑代码主体
(params) -> {body}
// 1. 无参数,单行body实现代码,body的大括号可省略,参数的小括号不可省略:
() -> System.out.println(" test ...");
// 2. 单参数,多行代码,参数小括号可以省略:
param -> {System.out.println(" test1 ...");System.out.println(" test2 ...");
}
// 3.1 多参数,一行返回值:
(param1, param2) -> { param1 + param2; }
// 3.2 多参数,多行代码需要返回值,必须使用代码块和return关键字
(param1, param2) -> {int ans = param1 + param2;return ans;
}
可以发现,在上述lambda表达式例子中,参数都未声明类型,它怎么知道我传的param1
和param2
是int还是double呢?
因为lambda表达式是对匿名内部类的简化,是实现了函数式接口的实例,而接口方法的参数列表早已规定好,lambda表达式只是补充了方法内容,并不是就在它出现的地方直接执行了,会在调用函数式接口的方法时才会执行lambda表达式body代码。因此当真正执行到lambda表达式时会根据函数式接口方法声明的参数列表(上下文)来推断参数类型,故不需要显示声明。那么,函数式接口具体有哪些要求呢?
1.3 函数式接口
函数式接口是指仅有一个抽象方法的接口,接口中可以包含默认方法或静态方法,函数式接口使得方法可以作为参数传递。java 8后可以使用@FunctionalInterface
注解标记函数式接口,如果在该注解标记的接口中声明多个抽象方法,编译器会报错,因为这样补充的方法体不知道具体实现的是哪个方法。
接口在设计之初仅能有抽象方法,而Java 8之后为了支持接口的演化,适应实际开发需求,引入了默认方法和静态方法。
1.3.1 默认方法
- 为什么引入?接口设计按照开闭原则,对修改关闭,对扩展开放。但是若随着业务发展必须扩展某个接口为其添加新的方法,而它又已经被大量类实现时,所有实现类都会因为缺少对新方法的实现而报错。这时就需要将这个接口新增方法定义成默认实现,如此接口实现类可以保持原样,不受到影响。
- 怎么使用?使用
default
关键字修饰,拥有方法体,用来提供接口的默认实现,实现接口的实例类可以选择覆盖它,也可以不覆盖。 - 是否覆盖?如果实现类没覆盖默认方法,那么它会继承接口的默认方法,可以直接调用;如果已覆盖,那么调用的是实现类的方法。
- 多接口冲突?Java支持多接口实现,如果某个实现类继承多个接口,而这些接口拥有同名默认方法,则必须处理,否则会报错,可以通过以下两种方式解决:
a. 实现类重写默认方法,方法体内提供实现类自己的代码;
b. 若要调用接口默认方法,则必须在重写方法中指定父接口明确调用哪个接口方法。
1.3.2 静态方法
- 为什么引入?如果希望在接口中实现类似工具类的方法,比如一个校验接口,要完成输入文本的字符串内容、日期格式、数据类型等基本校验要求(可能还需要其他针对具体业务的校验),这时会希望这些通用功能可以集成在接口内被直接调用,而业务校验留在实现类中。在java 8之前,由于接口只能定义抽象方法,所以只能通过定义工具校验类并实现通用校验功能,然后在实现类中调用工具类来辅助实现功能:
interface Validator{boolean validate(String text);
}class ValidatorUtil{static boolean checkDate(String text){// 核对日期相关代码...return true;}static boolean checkNum(String text){// 核对数据类型相关代码...return true;}
}
class MyValidator implements Validator{@Overridepublic boolean validate(String text) {if (!ValidatorUtil.checkDate(text)) return false;if (!ValidatorUtil.checkNum(text)) return false;// 具体业务核对代码...return true;}
}
上面这种写法可以实现校验功能,但工具类与接口的联系不紧密,两者呈现独立关系,若中间层再使用一个抽象类来代替工具类,那么实现类只能单继承这个抽象类,丧失了灵活性。因此Java 8引入了接口静态方法,将工具方法和接口定义集中到同一接口中,让工具方法和接口紧密相连,使得接口不仅具有基础的“契约”或“模板”定义,还能成为一个功能模块。
- 怎么使用? 使用
static
关键字修饰,只能通过接口名调用,被static
修饰的方法不能被重写。
为了支持函数式编程,Java提供了一组通用的函数式接口:
- Predicate接口:抽象方法为
boolean test(T t)
,接收单个参数并返回布尔值,stream
的filter
方法参数是Predicate
类型; - Function<T,R>接口:抽象方法为
R apply(T t)
,接收单个参数并返回一个结果,通常用作转换,stream
的map
方法参数是Function
类型; - Consumer接口: 抽象方法为
void accept(T t)
,接收单个参数但不返回输出,可用于日志打印; - Supplier接口:抽象方法为
T get()
,无参但有输出,可用于生成随机数(但这种一般直接写一行代码就行了); - UnaryOperator接口: 抽象方法为
T apply(T t)
,其实就是Function
接口的特殊版; - BinaryOperator、BiPredicate<T,U>、Function<T,U,R>:原接口的双参数版本。
1.4 lambda表达式和匿名内部类的区别
lambda表达式是对匿名内部类实现的简化,本质上仍旧和匿名内部类一样是一个函数式接口的实例,但是它们仍有一些区别:
- 作用域不同
匿名内部类:会新建一个新的作用域,内部可以重新定义变量,可以使用外部的final
定义的变量或有效最终变量(未使用final标记,但在它整个生命周期中只读、并未被重新赋值/改变引用地址的变量,通常集合对象都是有效最终变量);
lambda表达式:与外部共用一个作用域,因此不能定义与外部同样的变量名,但对外部变量的访问和匿名内部类一致。 - 字节码生成方式不同
匿名内部类:编译时会生成一个单独的.class
文件,名称一般是‘OuterClassName\$1.class’
,OuterClassName
是外部主类名,运行时JVM会通过类加载机制加载这个类;
lambda表达式:不会生成额外的类文件,而是通过生成invokedynamic
字节码指令和JVM的动态方法调用机制实现,在运行时动态生成实例。
总体来说,lambda表达式不管是在书写还是运行上都更加轻量级。
2. lambda表达式使用案例
lambda表达式用在函数式接口实例化中,只要定义的接口满足函数式接口的定义即可,也可以使用@FunctionalInterface
注解来规范定义,编译器会帮忙检查,但这个注解并不强制使用。给出一些常用的例子:
2.1 数据流stream
将集合、数组等类型的数据转成stream
之后再使用lambda表达式来进一步处理是开发中的高频场景,stream API常见的用法有过滤filter
、映射map
、排序sorted
、聚合reduced
、收集collect
。
2.1.1 filter过滤
filter
方法用于根据条件筛选元素,下面以filter
为例看看lambda表达式是如何使用的:
//list是Person列表,Person包含name(string)、age(int)、valid(boolean)字段,existNames为name列表
list.stream().filter(item -> existNames.contains(item.getName()));
filter
里可以访问有效最终变量existNames
,因为列表existNames
的引用并未改变,而filter
本身的声明在Steam
接口中:
Stream<T> filter(Predicate<? super T> predicate);
它需要传入一个Predicate
(谓词)类型的函数接口,该接口要求补充能够返回布尔值的表达式,参数为泛型,且和流中元素类型一致(因此Predicate
的test
参数必须是变成stream
之前集合或列表中的元素类型,这一步在流操作中会自动根据上下文信息填充)。Predicate
的定义是:
@FunctionalInterface
public interface Predicate<T> {boolean test(T t);//还有一些默认函数
}
filter
的具体实现在实现了Stream
接口的类ReferencePipeline
中定义,filter
方法中会调用predicate
的test
方法(lambda
中只是实例化,predicate
真实调用在ReferencePipeline
中),因此当我们在使用stream
的filter
方法时,只需要人为填充好谓词类型要求的参数(集合中元素),方法体给出能够返回布尔值的表达式即可(但如果是多行代码,需要显式使用return
返回判断结果)。当代码执行到predicate.test
时,就会自动调用我们在lambda表达式中书写的参数item
和代码块。
还有另外一种方法引用写法:
//list是Person列表,Person包含name(string)、age(int)、valid(boolean)字段
list.stream().filter(Person::getValid)
乍一眼看都不符合lambda表达式规范了,但是这种写法是可以的,只是必须满足两个条件才能使得方法签名兼容:
a. 流中元素和方法引用前的类型必须一致,即Person
必须是list
里的元素类型,这样流元素与Predicate
参数类型才匹配,满足上下文关系;
b. 方法必须是无参方法,并且返回类型是boolean
类型;
这是因为filter
需要一个Predicate
,它的抽象方法test
需要一个参数并返回布尔值,在流操作中,test
需要的参数类型和流元素类型一致,因此它依赖于流元素实例,而Person::getValid
就是一个依赖Person
实例(流中元素)且返回布尔值的方法,因此等价于Predicate<Person>
和item->item.getValid()
。这个方法引用将被应用于流中每个元素,当filter
方法调用test
方法时,它会将每个Person
实例传递getValid
方法。后续再对方法引用做详细说明。
2.1.2 map映射
map
方法用于将流中元素转换为其他元素,使用的函数接口为Function
,T
是流中元素类型,R
是转换后元素类型:
<R> Stream<R> map(Function<? super T, ? extends R> mapper);
实例代码:
//提取person集合的姓名
List<String> names= person.stream().map(Person::getName).collect(Collectors.toList());
上述代码同样使用方法引用将getName
方法绑定到流元素Person
实例上,这样能将接口函数R apply(T t)
填充为:
String apply(Person item){return item.getName();
}
2.1.3 排序sorted
sorted
方法用于对流中元素进行排序,如果不带任何参数,则按照自然顺序(数字按升序、字符串按字母顺序)进行排序,如果是带参数版本的,则是:
Stream<T> sorted(Comparator<? super T> comparator);
需要传递一个Comparator
接口,这个函数式接口包含的抽象方法为compare
,并且提供许多默认方法及静态方法来生成Comparator
实例,支持多个比较器比较(使用thenComparing
方法)。以一个例子来说明这些用法:一个Person
包含年龄age
和姓名name
,现在要对Person
列表按照
- 年龄降序,名字长度升序排序;
- 年龄降序,名字降序排列,那么代码为:
//peple为Person列表
// 1)年龄降序,名字长度升序排序
List<Person> sortedPeople = people.stream().sorted(Comparator.comparingInt(Person::getAge).reversed().thenComparing(Comparator.comparingInt(person -> person.getName().length()))).collect(Collectors.toList());
// 2)年龄降序,名字降序排列
List<Person> sortedPeople = people.stream().sorted(Comparator.comparingInt(person -> -person.getAge()).thenComparing(Comparator.comparing(Person::getName))).collect(Collectors.toList());
上述代码中sorted
需要的Comparator
接口参数都通过Comparator
提供的默认函数生成,当需要使用数字比较大小时,可以使用Comparator.comparingInt
方法:
public static <T> Comparator<T> comparingInt(ToIntFunction<? super T> keyExtractor)
这个方法需要传入一个ToIntFunction
接口:
@FunctionalInterface
public interface ToIntFunction<T> {int applyAsInt(T value);
}
这个接口是Function
接口的变种,将一个任意对象转换为一个整数,在流操作中,它的意义是将流元素T
转换为一个整数,后续再在comparingInt
默认方法中使用整型的compare
方法根据转换值获得对象的大小关系。
当需要按照对象整型属性逆序排序时,可以使用reverse()
方法,或者在lambda表达式中返回值前添加一个负号(上述代码直接写在代码前,因为只有一行代码默认为返回代码),这样能让对象间转换值大小关系反转,实现逆序排序。
当需要使用字符串进行排序时,可使用Comparator.comparing
方法,它的参数是Function
接口,用以将流元素转换为另一个对象类型R
,对象R
必须实现Comparable
接口,上述代码是使用方法引用将流元素转换为String(R=String)
,再利用R
实现Comparable
接口的compare
方法进行对比获得对象间大小关系。
如果要直接实例化Comparator
接口,则代码是:
// 年龄降序,名字降序排列
List<Person> sortedPeople = people.stream().sorted((p1, p2) -> {int pare1 = Integer.compare(p1.getAge(), p2.getAge());return pare1 !=0 ? pare1 : p2.getName().compareTo(p1.getName());}).collect(Collectors.toList());
2.1.4 聚合reduce
reduce
用于对流中元素进行聚合操作,可以将流元素合并为一个单一的值,比如求总和、乘积、最大值、最小值。reduce
拥有重载函数:
//有初始值版本,流中的运算从初始值identity开始
T reduce(T identity, BinaryOperator<T> accumulator);
//无初始值版本
Optional<T> reduce(BinaryOperator<T> accumulator);
当不使用初始值时,reduce
返回Optional
对象,需要使用orElse()
处理流为空的情况同时获得值,另外从上面的函数声明可以看出,在使用reduce
函数时参数和返回值为同样的泛型,因此如果要求对象集合的某个属性聚合值,比如Person
列表的年龄总和,需要先通过map
获取年龄流,再使用reduce
计算总和:
// 有初始值计算,从0开始
int totalAge = people.stream().map(Person::getAge).reduce(0, (a, b) -> a + b);
// 无初始值,orElse获得值,且同时处理流为空无初始值情况,流为空返回50
int totalAge = people.stream().map(Person::getAge).reduce(Integer::sum).orElse(50);
2.1.5 收集collect
collect
在stream
中用作终结操作,用于将流元素收集到集合、数组、哈希表中,使用collect
函数时通常用Collectors
类提供的静态方法来获得它需要的Collector
接口参数,常用方法如下:
// 1) toList(),将流中元素收集到List中
List<String> names = people.stream().map(Person::getName).collect(Collectors.toList());
// 2) toSet(),将流中元素收集到Set中,如果最后是生成引用对象类型的Set,那么需要先对这个对象重写hashCode()和equals()方法
Set<String> namesSet = people.stream().map(Person::getName).collect(Collectors.toSet());
// 3) toMap(),收集到哈希表,需要提供key和value两个函数
Map<String,Integer> namesMap = people.stream().map(Person::getName).collect(Collectors.toMap(Person::getName, Person::getAge));
// 4) joining(),将流中字符串(字符串流)连接成单一字符串
String allName = people.stream().map(people::getName).collect(Collectors.joining(","));
// 5) 根据某个属性将流元素分类
Map<Integer, List<Person>> groups = people.stream().collect(Collectors.groupingBy(Person::getAge));
……
2.2 Optional
Optional
是一个容器对象,使用Optional
能够减少空指针异常的风险,因为它可以处理可能为null
的值。Optional
操作里面也会用到函数式接口。
2.2.1 创建Optional
// 1) Optional.of(T value),传参必须非空,否则抛出空指针错误
// 2) Optional.ofNullable(T value),传参可以为空,为空则返回空的Optional
Optional<String> optional = Optional.ofNullable("test");
// 3) Optional.empty(),创建一个空的Optional
2.2.2 访问Optional
1)isPresent()
:使用isPresent()
方法来检查Optional
对象是否包含值,这个用法等价于用“==null”
:
if(optional.isPresent()){//其他代码}
2)ifPresent()
:更常用的是ifPresent()
:
public void ifPresent(Consumer<? super T> consumer) {if (value != null)consumer.accept(value);
}
ifPresent()
常与ofNullable()
方法一起使用,它需要实例化Consumer
接口,接口参数就是Optional
容器里放的元素,因此Optional.ofNullable(xxx).ifPresent(xxx → xxxx)
的含义是如果xxx
不为空则执行xxxx
操作。
Optional.ofNullable(dto.getTotalFee()).ifPresent(totalFee -> {beforeInfo.fluentPut(InsureFieldEnum.TOTAL_FEE.fieldName, tbshareinsure.getTotalFee());afterInfo.fluentPut(InsureFieldEnum.TOTAL_FEE.fieldName, dto.getTotalFee());
});
3)get()
:若Optional
为空,则抛出空指针异常
2.2.3 处理空值
由于Optional
支持为空,因此它也有配套的空值处理操作来避免异常抛出:
orElse()
,stream.reduce
无初始值版本配合使用
int totalAge = people.stream().map(Person::getAge).reduce(Integer::sum).orElse(50);
T orElseGet(Supplier<? extends T> other)
,使用supplier
接口提供
Optional.ofNullable(null).orElseGet(() -> "defaultValue");
- 如果
Option
为空,也可以自定义抛出异常,使用orElseThrow
方法,需要提供能返回异常类型对象的Supplier
实例
//<X extends Throwable> T orElseThrow(Supplier<? extends X> exceptionSupplier) throws X
Optional.ofNullable(null).orElseThrow(() ->new RuntimeException("ERROR!"));
2.2.4 转换操作
1)map
映射,需要提供Function
接口实例,返回的是Optional
对象,因此如果要获取容器的值最好再配合orElse
使用:
//函数原式为Optional<U> map(Function<? super T, ? extends U> mapper),这里的用法和stream.map类似,person为Person实例
Optional.ofNullable(person).map(Person::getName).orElse(null);
2)fitler
,与stream.filter
用法一样,返回Optional
对象。
3 方法引用
lambda表达式实例化函数式接口其实是通过现场填充方法体来实现的,而方法引用是直接将另一个方法引用到函数式接口,用被引用的方法填充原抽象方法。
方法引用是用来引用一个方法,并不是调用方法,所以不能在方法中指明具体的参数(即使方法带有参数),只能使用方法的名字。如果一定要传递参数,则需要使用lambda表达式语法。
在使用方法引用来实例化函数式接口时,必须注意引用方法的参数签名/返回类型和接口抽象方法的参数签名/返回类型一致,这样才能达到“用引用方法替换接口方法”的目的。
3.1 方法引用类别
- 静态方法引用:引用类的静态方法,在没有实例的情况下调用,语法为:
ClassName::staticMethod
; - 实例方法引用-任意对象:引用某个对象的实例方法,语法为
ClassName::method
,一般用于流中调用某一个类的实例方法,不特指某个实例对象; - 实例方法引用-特定对象:引用某个特定实例的方法,语法为
instance::method
,instance
是一个具体的实例; - 构造方法引用:引用类的构造方法,用于创建对象,语法为
ClassName::New
;
3.2 一些区别和注意点
任意对象和特定对象的实例方法引用区别是什么?——任意对象突出”实例“而非静态,指某个类的一群实例,特定对象突出”特定“,特指某一个实例。
任意对象实例引用多用于流操作,流需要对集合中每一个实例操作方法,比如:
list.stream().filter(Person::getValid)
因为Person::getValid
在上述流操作中(泛型匹配)直接与Person
实例挂钩,因此getValid
参数为空是可以的。
特定对象实例引用需要先生成一个实例,再引用实例方法:
// 实例举例
Class PrintMsg{public void print(String msg){log.info(msg);}
}//实例方法引用
PrintMsg pm = new PrintMsg();
Consumer<String> printer = pm::print;
printer.accept("test");
此处print
方法签名(String
类型的参数,空返回)和Consumer
接口抽象方法accept
的签名(泛型参数-此处为String
,空返回)一致,如果泛型用了其他类,那么print
方法里也要相应类的实例作为参数。
构造方法引用需要注意传递的参数与构造方法的参数匹配,比如如下例子是错误的:
Class Person{private String name;Person(String name){this.name = name;}
}//构造方法引用
Supplier<Person> provider = Person::new;
Person person = provider.get();
因为Supplier
抽象函数get
的参数列表为空,而Person
只提供了有参构造,因此上述代码签名是不匹配,如果要正常运行代码,Person
需要提供一个无参构造。