java8 实战读书笔记:Lambda 表达式语法与函数式编程接口
(2) () -> “Raoul”
(3) () -> {return “Mario”;}
(4) (Integer i) -> return “Alan” + i;
(5) (String s) -> {“IronMan”;}
正解:
(1) 正确。如果使用匿名类(接口名统一使用 IDemoLambda)表示如下:
new IDemoLambda() {
public void test() {
}
}
(2)正确。如果使用匿名类(接口名统一使用 IDemoLambda)表示如下:
new IDemoLambda() {
public String test() {
return "Raoul"; // 如果直接接一个值,表示返回该值
}
}
(3)正确。如果使用匿名类(接口名统一使用 IDemoLambda)表示如下:
new IDemoLambda() {
public String test() {
return "Mario";
}
}
(4)错误。因为 return 是流程控制语句,表示返回,不是一个表达式,故不符合 lambda 语法,正确的表示方法应该是 (Integer i) ->{ return “Alan” + i;}。如果使用匿名类(接口名统一使用 IDemoLambda)表示如下:
new IDemoLambda() {
public String test(Integer i) {
return "Alan" + i;
}
}
(5)错误。因为"IronMan"是一个表达式,并不是一个语句,故不能使用{}修饰,应修改为 (String s) -> “IronMan”。如果使用匿名类(接口名统一使用 IDemoLambda)表示如下:
new IDemoLambda() {
public String test(String s) {
return "IronMan";
}
}
2、初步接触函数式接口
在 java8 中,一个接口如果只定义了一个抽象方法,那这个接口就可以称为函数式接口,就可以使用 lambda 表达式来简化程序代码。Lambda 表达式可以直接赋值给变量,也可以直接作为参数传递给函数,示例如下:
public static void startThread(Runnable a) {
(new Thread(a)).start();
}
public static void main(String[] args) {
// lambda 表达式可以直接赋值给变量,也可以直接以参数的形式传递给方法、
Runnable a = () -> {
System.out.println("Hello World,Lambda...");
};
// JDK8 之前使用匿名类来实现
Runnable b = new Runnable() {
@Override
public void run() {
System.out.println("Hello World,Lambda...");
}
};
startThread(a);
startThread(() -> {
System.out.println("Hello World,Lambda...");
});
}
那能将(int a) -> {System.out.println(“Hello World, Lambda…”);}表达式赋值给 Runnable a 变量吗?答案是不能,因为该表达式不符合函数式编程接口(Runnable)唯一抽象方法的函数签名列表。
Runnable 的函数式签名列表为 public abstract void run();
温馨提示:如果我们有留意 JDK8 的 Runnable 接口的定义,你会发现给接口相对 JDK8 之前的版本多了一个注解:@FunctionalInterface,该注解是一个标识注解,
用来标识这个接口是一个函数式接口。如果我们人为在一个不满足函数式定义的接口上增加 @FunctionalInterface,则会在编译时提示错误。
3、 Lambda 表达式实战思考
例如有如下代码:
/**
处理文件:当前需求是处理文件的第一行数据
@return
@throws IOException
*/
public static String processFile() throws IOException {
try(BufferedReader br = new BufferedReader(new FileReader("data.txt"))) {
return br.readLine();
}
}
当前需求为处理文件的第一行数据,那问题来了,如果需求变化需要返回文件的第一行和第二行数据,那该如何进行改造呢?
在理想的情况下,需要重用执行设置和关闭流的代码,并告诉 processFile()方法对文件执行不同的操作,换句话说就是要实现对 processFile 的行为进行参数化。
Step·1:行为参数化
要读取文件的头两行,用 Lambda 语法如何实现呢?思考一下,下面这条语句是否可以实现?
(BufferedReader bf) -> br.readLine() + br.readLine()
答案是当然可以,接下来就要思考,定义一个什么样的方法,能接收上面这个参数。
Step2:使用函数式接口来传递行为
要使用(bufferedReader bf) -> br.readLine() + br.readLine(),则需要定义一个接受参数为 BufferedReader,并返回 String 类型的函数式接口。
定义如下:
@FunctionalInterface
public interface BufferedReaderProcessor {
public String process(BufferedReader b) throws IoException;
}
那把 processFile 方法改造成如下代码:
/**
处理文件:当前需求是处理文件的第一行数据
@return
@throws IOException
*/
public static String processFile(BufferedReaderProcess brp) throws IOException {
try(BufferedReader br = new BufferedReader(new FileReader("data.txt"))) {
return brp.process(br);
}
}
Step3:使用 lambda 表达式作为参数进行传递
将行为参数化后,并对方法进行改造,使方法接受一个函数式编程接口后,就可以将 Lambda 表达式直接传递给方法,例如:
processFile( (BufferedReader br) -> br.readLine() );
processFile( (BufferedReader bf) -> br.readLine() + br.readLine());
4、Java8 中自定义函数式接口
从上面的讲解中我们已然能够得知,要能够将 Lambda 表达式当成方法参数进行参数行为化的一个前提条件是首先要在方法列表中使用一个函数式接口,例如上例中的 BufferReaderProcess,那如果每次使用 Labmbda 表达式之前都要定义各自的函数式编程接口,那也够麻烦的,那有没有一种方式,或定义一种通用的函数式编程接口呢?答案是肯定的,Java8 的设计者,利用泛型,定义了一整套函数式编程接口,下面将介绍 java8 中常用的函数式编程接口。
4.1 Predicate
所谓函数式编程接口就是只能定义一个抽象方法,Predicate 函数接口中定义的抽象方法为 boolean test(T t),对应的函数式行为为接收一类对象 t,返回 boolean 类型,其可用的 lambda 表达式为(T t) -> boolean 类型的表达式,例如(Sample a) -> a.isEmpty()。
该接口通常的应用场景为过滤。例如,要定义一个方法,从集合中进行刷选,具体的刷选逻辑(行为)由参数进行指定,那我们可以定义这样一个刷选的方法:
public static <T> List<T> filter(List<T> list, Predicate<T> p) {
List<T> results = new ArrayList<>();
for(T s: list){
if(p.test(s)){
results.add(s);
}
}
return results;
}
上述函数,我们可以这样进行调用:
Predicate<String> behaviorFilter = (String s) -> !s.isEmpty(); // lambda 表达式赋值给一个变量
filter(behaviorFilter);
其它 add 等方法,将在下文介绍(复合 lambda 表达式)。
另外,为了避免 java 基本类型与包装类型的装箱与拆箱带来的性能损耗,JDK8 的设计者们提供了如下函数式编程接口:IntPredicate、LongPredicate、DoublePredicate。我们选择 LongPredicate 看一下其函数接口的声明:
boolean test(long value);
4.2 Consumer
该函数式编程接口适合对对象进行处理,但没有返回值,对应的函数描述符:T -> void
举例如下:
public static <T> void forEach(List<T> list, Consumer<T> c) {
for(T t : list) {
c.accept(t);
}
}
其调用示例如下:
forEach( Arrays.asList(1,2,3,4,5), (Integer i) -> System.out.println(i) );
另外,为了避免 java 基本类型与包装类型的装箱与拆箱带来的性能损耗,JDK8 的设计者们提供了如下函数式编程接口:IntConsumer、LongConsumer、DoubleConsumer。
4.3 Function<T,R>
其适合的场景是,接收一个泛型 T 的对象,返回一个泛型为 R 的对象,其对应的函数描述符: T -> R。
示例如下:
public static <T,R> List<R> map(List<T> list, Function<T,R> f) {
List<R> result = new ArrayList<>();
for(T t : list) {
result.add( f.apply(t) );
}
return result;
}
List<Integer> l = map(Arrays.asList("lambdas", "in", "action"), (String s) -> s.length );
另外,为了避免 java 基本类型与包装类型的装箱与拆箱带来的性能损耗,JDK8 的设计者们提供了如下函数式编程接口:IntFunction< R>、LongFunction< R>、DoubleFunction< R>、IntToDoubleFunction、IntToLongFunction、LongToIntFunction、LongToDoubleFunction、ToIntFunction< T>、ToDoubleFunction< T>、ToLongFunction< T>。
4.4 Supplier< T>
函数描述符:() -> T。适合创建对象的场景,例如 () -> new Object();
另外,为了避免 java 基本类型与包装类型的装箱与拆箱带来的性能损耗,JDK8 的设计者们提供了如下函数式编程接口:BooleanSupplier、IntSupplier、LongSupplier、DoubleSupplier。
4.5 UnaryOperator< T >
一元运算符函数式接口,接收一个泛型 T 的对象,同样返回一个泛型 T 的对象。
示例如下:
public static <T> List<T> map(List<T> list, UnaryOperator<T> f) {
List<R> result = new ArrayList<>();
for(T t : list) {
result.add( f.apply(t) );
}
return result;
}
map( list, (int i) -> i ++ );
另外,为了避免 java 基本类型与包装类型的装箱与拆箱带来的性能损耗,JDK8 的设计者们提供了如下函数式编程接口:IntUnaryOperator、LongUnaryOperator、DoubleUnaryOperator。
4.6 BiPredicate<T,U>
接收两个参数,返回 boolean 类型。其对应的函数描述符:(T,U) -> boolean。
4.7 BiConsumer<T,U>
与 Consume 函数式接口类似,只是该接口接收两个参数,对应的函数描述符(T,U) -> void。
另外,为了避免 java 基本类型与包装类型的装箱与拆箱带来的性能损耗,JDK8 的设计者们提供了如下函数式编程接口:ObjIntConsumer、ObjLongConsumer、ObjDoubleConsumer。
4.8 BiFunction<T,U,R>
与 Function 函数式接口类似,其对应的函数描述符:(T,U) -> R。
另外,为了避免 java 基本类型与包装类型的装箱与拆箱带来的性能损耗,JDK8 的设计者们提供了如下函数式编程接口:ToIntBiFunction(T,U)、ToLongBiFunction(T,U)、ToDoubleBiFunction(T,U)。
4.9 BinaryOperator< T >
二维运算符,接收两个 T 类型的对象,返回一个 T 类型的对象。
另外,为了避免 java 基本类型与包装类型的装箱与拆箱带来的性能损耗,JDK8 的设计者们提供了如下函数式编程接口:IntBinaryOperator、LongBinaryOperator、DoubleBinaryOperator。
上述就是 JDK8 定义在 java.util.function 中的函数式编程接口。重点关注的是其定义的函数式编程接口,其复合操作相关的 API 将在下文中详细介绍。
5、类型检查、类型推断以及限制
5.1 类型检查
java8 是如何检查传入的 Lambda 表示式是否符合约定的类型呢?
例如
public static <T> List<T> filter(List<T> list, Predicate<T> p) {
List<T> results = new ArrayList<>();
for(T s: list){
if(p.test(s)){
results.add(s);
}
}
return results;
}
List<Apple> heavierThan150g = filter(inventory, (Apple a) -> a.getWeight() > 150);
评论