Java8 之 Lambda表达式

Java8 之 Lambda

Java 8 提出了新的语法概念:Lambda,本篇介绍Lambda究竟是什么,以及如何使用。

解耦

代码间的相互依赖,产生了耦合,耦合度高,会使代码的维护变得复杂,容易出错。很多设计模式一直致力于解决由于耦合带来的问题。比如工厂模式,会将对象的创建和使用分离,使用对象的程序,不再硬编码“new”出一个对象来,这使得只要更改工厂,就会统一修改使用者获得对象,修改变得简单,也更不容易出错。Spring通过配置,来统一加载对象,装配对象的属性,更进一步降低了对象间的耦合。
另外的耦合出现在方法内部。当方法中一部分代码是固定的,而另外一部分代码是变化的,往往会为了变化的代码,写出多个函数。例如,对文件的读取:

public void readFile(String filePath){
    File file = new File(filePath);
    BufferedReader bufferedReader = null;
    try {
        bufferedReader = new BufferedReader(new FileReader(file));
        String line = null;
        while((line = bufferedReader.readLine())!=null){
            System.out.println(line);
        }
    } catch (FileNotFoundException e) {
        e.printStackTrace();
    }catch (IOException e){
        e.printStackTrace();
    }finally {
        try {
            bufferedReader.close();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

这是很普通的一段代码,一个函数,输入字符串的文件名,函数会读取文件的每一行,将内容打印到控制台。
但是,如果程序需要另外一个函数,同样读取一个文件,得到每一行的数据,判断是否包含给定的字符串,或单词,如包含才打印。这要怎么操作,再写一个函数?叫做checkFileLineContainStr?这明显是不可取的,类似的需求会有很多,比如读到每一行,为每行增加一个序号,再打印出来...。
如果每个需求都实现一个方法,代码中会有很多类似的方法,这些方法大部分代码相似,打开文件,装载到BufferReader,循环读取直到得到null,抛出同样的异常,最后关闭BufferReader。这时,一个新的需求出现了,需要在捕获异常时,将异常记录到日志文件,供程序故障时排查问题。实现了多个方法的程序员,只好每个函数都改一遍,如果漏改了,还可能出现其他问题。
这就是方法中的代码耦合造成的问题。通常,会把这种重复很多的相同的代码叫做模板,方法中不同的代码才是真正要方法的行为。比如执行一个SQL,要加载驱动、连接数据库、执行SQL、关闭数据库等。通过JDBC执行一条SQL语句需要大段的代码。而真正的行为可能只是其中的一条执行SQL的语句。
解决这个问题的办法,就是创建接口或抽象类,提供统一的行为方法

public interface FileProcessor{
    public void process(String line);
}

实现类完成具体的方法

public class PrintProcessor{
    public void process(String line){
        System.out.println(line);
    }
}

修改之前的函数,变为下面的函数

public void readFile(String filePath,FileProcess fileProcessor){
    File file = new File(filePath);
    BufferedReader bufferedReader = null;
    try {
        bufferedReader = new BufferedReader(new FileReader(file));
        String line = null;
        while((line = bufferedReader.readLine())!=null){
            fileProcessor.process(line);
        }
    } catch (FileNotFoundException e) {
        e.printStackTrace();
    }catch (IOException e){
        e.printStackTrace();
    }finally {
        try {
            bufferedReader.close();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

这样,我们通过接口,将需要执行的具体行为独立出来,可以将具体的行为作为接口的实现传递给模板。上面的例子里具体的处理就是文件处理类FileProcessor,不同的实现类实现具体的方法,然后把实现类的实例传递给readFile函数,就可以根据传入的对象中的process方法实现不同的功能了。
更进一步,还可以使用匿名类,而不是定义一个实现类,来给出具体的行为。这样只要在调用的时候这样来写。

readFile("/path/fileName",new FileProcess(){
   public void process(String line){
        System.out.println(line);
    }
} 
});

使用匿名类可以使代码更加简洁,同时也使得匿名类中的方法实现不可重用,如果在多处需要使用这个方法,每次都要重写,就不如写一个实现类来的划算了。

函数接口和Lambda

java 8 的Lambda就是在上面的那种接口式的编程风格基础上,给出的更加简洁的方案。
首先,要了解一下函数式接口的概念。所谓函数式接口,就是只有一个抽象方法的接口。说白了就是这个接口没有别的作用,只是一个函数的壳子。就像上面的FileProcessor,只有一个process方法,那么这就是一个函数式接口。
如果要使用一个函数式接口,有几种方法呢?
方法一。定义一个实现类,实现接口定义的方法。调用方法传递实现类的实例。

public interface A{
    public void methodA();
}

public class AImpl implements A{
    public void methodA(){
        System.out.println("这是通过实现类实现A接口的调用");
    }
}

调用方法如下

public void useA(A a){
    a.methodA();
}

使用时,可以这样

useA(new AImpl());

这是最原始的使用方法。
方法二。使用匿名类,不用定义实现类。

userA(new A(){
    public void methodA(){
        System.out.println("这是通过匿名类实现的A接口调用");
    }
})

这样看起来更加简洁,但是仍然存在调用的方法定义等代码。
方法三。使用Lambda表达式

useA(()->System.out.println("这是通过Lambda实现A接口调用"));

是不是更加简洁了,只要一行就搞定!
但是,这个表达式是什么意思呢?从这个调用中,可以确定一点,这个Lambda最终还是转换为A接口的一个实例,并且给出了具体实现。这样才能满足函数useA的调用逻辑,useA这个函数是需要传入接口A的具体实现类的实例的,传入Lambda必定就是这样的实现类的实例。
Lambda表达式用“->”分为两个部分,"->"前为参数表的描述;"->"后面是具体功能实现。

Lambda表达式,更像是一个函数,它实际是对函数式接口中的抽象方法的实现。在使用过程中,不必去关心对接口的实现类的代码(定义一个类或匿名类实现一个接口,代码总是很相似的)。
这样看来,Lambda其实就很简单了,只不过是函数的另外一种定义形式,看起来更加简洁而已。下面看看Lambda的几种写法。
1、指定参数类型

(String a,String b)->a+b

表名Lambda实现了一个函数,将传入的两个字符串连接,返回连接后的字符串。注意这里的返回没有使用return。
2、省略参数类型

(a,b)->a+b

与上面的功能一致,但是不指定参数类型。这是因为程序在运行的时候是可以自动判断参数类型的。
3、多行功能实现
前面的Lambda都是实现了相对简单的功能,只有一行代码,但实际情况,大多要通过若干行代码完成一个功能,这样的Lambda需要用{}来框定功能实现部分

(a,b)->{
    a += ",";
    b += "。";
    return a+b;
}

这里在第一个字符串参数后加了一个逗号,在第二个字符串参数后加了一个句号,然后再把两个字符串连接起来返回。整个实现部分跟普通的java实现完全一致,前面的Lambda不能在语句后加";",而大括号"{}"框起来的部分就是java的代码,所以还是要加上";",否则编译会报错误。
Lambda本身是函数式接口的实现,所以可以直接把Lambda赋值给接口。

public interface B{
    public String methodB(String a,String b);
}

这个接口中的方法要求传入两个字符串参数,返回一个字符串参数,这与之前定义的Lambda一致,就可以这样赋值

B = (a,b)->a+b;

或者

B = (a,b)->{
    a += ",";
    b += "。";
    return a+b;
}

这就完成了对接口B的不同实现。

java 8 提供的函数式接口

为了方便使用,Java 8提供了很多定义好的函数式接口,开发人员可以直接使用,而不必自己写单独的接口。这些接口采用泛型,定义参数和返回值,因此具有很强的通用性。这些接口包括:
1 BiConsumer<T,U>
代表了一个接受两个输入参数的操作,并且不返回任何结果

2 BiFunction<T,U,R>
代表了一个接受两个输入参数的方法,并且返回一个结果

3 BinaryOperator
代表了一个作用于于两个同类型操作符的操作,并且返回了操作符同类型的结果

4 BiPredicate<T,U>
代表了一个两个参数的boolean值方法

5 BooleanSupplier
代表了boolean值结果的提供方

6 Consumer
代表了接受一个输入参数并且无返回的操作

7 DoubleBinaryOperator
代表了作用于两个double值操作符的操作,并且返回了一个double值的结果。

8 DoubleConsumer
代表一个接受double值参数的操作,并且不返回结果。

9 DoubleFunction
代表接受一个double值参数的方法,并且返回结果

10 DoublePredicate
代表一个拥有double值参数的boolean值方法

11 DoubleSupplier
代表一个double值结构的提供方

12 DoubleToIntFunction
接受一个double类型输入,返回一个int类型结果。

13 DoubleToLongFunction
接受一个double类型输入,返回一个long类型结果

14 DoubleUnaryOperator
接受一个参数同为类型double,返回值类型也为double 。

15 Function<T,R>
接受一个输入参数,返回一个结果。

16 IntBinaryOperator
接受两个参数同为类型int,返回值类型也为int 。

17 IntConsumer
接受一个int类型的输入参数,无返回值 。

18 IntFunction
接受一个int类型输入参数,返回一个结果 。

19 IntPredicate
:接受一个int输入参数,返回一个布尔值的结果。

20 IntSupplier
无参数,返回一个int类型结果。

21 IntToDoubleFunction
接受一个int类型输入,返回一个double类型结果 。

22 IntToLongFunction
接受一个int类型输入,返回一个long类型结果。

23 IntUnaryOperator
接受一个参数同为类型int,返回值类型也为int 。

24 LongBinaryOperator
接受两个参数同为类型long,返回值类型也为long。

25 LongConsumer
接受一个long类型的输入参数,无返回值。

26 LongFunction
接受一个long类型输入参数,返回一个结果。

27 LongPredicate
R接受一个long输入参数,返回一个布尔值类型结果。

28 LongSupplier
无参数,返回一个结果long类型的值。

29 LongToDoubleFunction
接受一个long类型输入,返回一个double类型结果。

30 LongToIntFunction
接受一个long类型输入,返回一个int类型结果。

31 LongUnaryOperator
接受一个参数同为类型long,返回值类型也为long。

32 ObjDoubleConsumer
接受一个object类型和一个double类型的输入参数,无返回值。

33 ObjIntConsumer
接受一个object类型和一个int类型的输入参数,无返回值。

34 ObjLongConsumer
接受一个object类型和一个long类型的输入参数,无返回值。

35 Predicate
接受一个输入参数,返回一个布尔值结果。

36 Supplier
无参数,返回一个结果。

37 ToDoubleBiFunction<T,U>
接受两个输入参数,返回一个double类型结果

38 ToDoubleFunction
接受一个输入参数,返回一个double类型结果

39 ToIntBiFunction<T,U>
接受两个输入参数,返回一个int类型结果。

40 ToIntFunction
接受一个输入参数,返回一个int类型结果。

41 ToLongBiFunction<T,U>
接受两个输入参数,返回一个long类型结果。

42 ToLongFunction
接受一个输入参数,返回一个long类型结果。

43 UnaryOperator
接受一个参数为类型T,返回值类型也为T。
接口看似很多,但实际只分为几个类型。

谓词 Predicate

谓词提供的是判断的功能,传入参数,返回boolean类型,作用就是对传入的参数执行某种判断,返回判断的结果。接口包括:

接口 输入 输出
Predicate 输入一个参数 返回boolean
BiPredicate 输入两个参数 返回boolean
IntPredicate 输入一个int 返回boolean

函数 Function

函数通常是一种转换,将输入转为输出

接口 输入 输出
Fuction<T,R> 输入T类型对象 返回R类型对象
BiFunction<T,U,R> 输入T类型和U类型两个对象 返回R类型对象
DoubleFunction 输入一个double的原始数据类型 返回R类型对象
DoubleToIntFunction 输入一个double的原始数据类型 返回int变量
DoubleToLongFunction 输入一个double的原始数据类型 返回long
IntFunction 输入一个int的原始数据类型 返回R类型对象
IntToDoubleFunction 输入一个int的原始数据类型 返回double变量
IntToLongFunction 输入一个int变量 返回long变量
LongFunction 输入long变量 返回R类型对象
LongToDoubleFunction 输入long变量 返回double变量
LongToIntFunction 输入long变量 返回int变量
ToDoubleBiFunction<T,U> 输入T类型和U类型两个对象 返回double变量
ToDoubleFunction 输入T类型对象 返回double变量
ToIntBiFunction<T,U> 输入T类型和U类型两个对象 返回int变量
ToIntFunction 输入T类型对象 返回int变量
ToLongBiFunction<T,U> 输入T类型和U类型两个对象 返回long变量
ToLongFunction 输入T类型对象 返回long变量

Consumer

消费者是只传入参数,不返回的接口

接口 输入 输出
Consumer 输入T类型对象 没有输出
BiConsumer<T,U> 输入T类型和U类型两个对象 没有输出
DoubleConsumer 输入double变量 没有输出
IntConsumer 输入一个int变量 没有输出
LongConsumer 输入long变量 没有输出
ObjDoubleConsumer 输入一个T类型对象和一个double变量 没有输出
ObjIntConsumer 输入一个T类型对象和一个int变量 没有输出
ObjLongConsumer 输入一个T类型对象和一个long变量 没有输出

UnaryOperator

一元操作器,输入和输出的数据类型一致

接口 输入 输出
UnaryOperator 输入T类型对象 返回T类型对象
BinaryOperator 输入两个T类型对象 返回T类型对象
DoubleBinaryOperator 输入两个double变量 返回double变量
DoubleUnaryOperator 输入一个double变量 返回一个double变量
IntBinaryOperator 输入两个int变量 返回int变量
IntUnaryOperator 输入一个int变量 返回一个int变量
LongBinaryOperator 输入两个long变量 返回long变量
LongUnaryOperator 输入一个long变量 返回一个long变量

Supplier

生产者,没有输入,但是输出一个对象或数据

Supplier 没有输入 返回T类型对象
BooleanSupplier 没有输入 返回boolean变量
DoubleSupplier 没有输入 返回double变量
IntSupplier 没有输入 返回int变量
LongSupplier 没有输入 返回long变量
所有的函数式接口可以分为上面的五类,其中很多都是为了减少拆箱、装箱成本,而提供的原始数据类型版本。
posted @ 2020-04-24 14:42  潜龙腾渊  阅读(231)  评论(0)    收藏  举报