Java函数式编程及lambda

Java函数式编程及lambda

为什么要使用函数式编程

从jdk1.8开始,java开始支持函数式编程以及lambda表达式,本文简单描述一下函数式编程以及lambda表达式的常见使用方法。函数式编程是一种编程范式,他和命令式编程的区别有几点:

  • 最大区别在于关注点不同:命令式编程关注怎么做,函数式编程中关注做什么

例如,利用java实现找到最小的数,命令式编程的代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
public class MinDemo{
public static void main(String[] args){
int[] nums = {34,45,65,11,-47,23,75};

int min = Integer.MAX_VALUE;
for(int i:nums){
if(i < min){
min = i;
}
}
System.out.println(min);
}
}

这也是我们之前常用的写法,那么如果利用函数式编程的方式是什么样的呢:

1
2
3
4
5
6
7
8
public class MinDemo1{
public static void main(String[] args){
int[] nums = {34,45,65,11,-47,23,75};

int min = IntStream.of(nums).min().getAsInt();
System.out.println(min);
}
}

我们发现,函数式编程的写法我们不需要知道取最小值怎么实现的,那么这样的好处在哪儿呢?试想,当我们的nums里面的数据非常多时,会影响性能,我们使用命令式编程还要自己修改代码,使用线程池,数据拆分等方法自己实现,而函数式编程中只需要在执行时添加一个parallel()就可以并行运行,我们并不需要知道其怎么实现的。即将int min = IntStream.of(nums).min().getAsInt();修改为int min = IntStream.of(nums).parallel().min().getAsInt();。所以说函数式编程和命令式编程的最大区别在于他们关注点不一样,命令式编程关注怎么做,函数式编程中关注做什么。

  • lambda表达式可以将代码变得更加简洁易懂

在jdk8之前创建一个线程:

1
2
3
4
5
6
7
8
9
10
public class ThreadDemo{
public static void main(String[] args){
new Thread(new Runnable(){
@Override
public void run(){
System.out.println("thread run");
}
}).start();
}
}

我们发现代码十分冗余。而使用jdk8的lambda写法可以改为

1
2
3
4
5
public class ThreadDemo1{
public static void main(String[] args){
new Thread(()->System.out.println("thread run")).start();
}
}

使用这种方法只需要一行代码,我们甚至不需要知道Runnable接口,只需要知道这边需要一个方法,并且这种写法十分简洁。

lambda

在之前的new Thread(()->System.out.println("thread run")).start();()->System.out.println("thread run")这种带箭头->的表达式就叫lambda表达式,其实类似于在js中es6标准中的箭头函数。先改写一下代码:

1
2
3
4
5
6
7
8
9
10
public class ThreadDemo2{
public static void main(String[] args){
Runnable target = ()->System.out.println("thread run");
new Thread(target).start();
//上面的写法是没有问题的,也就是()->System.out.println("thread run")返回了一个Runnable接口的实现类
//如果将下面写法的(Runnable)强制类型转换去掉,就会报错,说object不是一个functional interface(函数式接口)
Object target1 = (Runnable)()->System.out.println("thread run");
new Thread((Runnable)target1).start();
}
}

再来看下有参数和返回值的写法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
interface Interface1{
int doubleNum(int i);
}

public class LambdaDemo1{
public static void main(String[] args){
Interface1 i1 = (i)->i * 2;
//当参数个数为1个的时候,括号可以去掉,最常用的写法
Interface1 i2 = i -> i * 2;
//也可以这样写
Interface1 i3 = (int i)->i * 2;
Interface1 i4 = (i)-> {
//方法有多行时可以这样写
return i * 2;
}
}
}

接口新特性

函数接口

lambda表达式并不是对每个接口都可以使用,必须当接口中只有一个要实现的方法时才可以使用lambda表达式,这种接口也叫做functional interface(函数接口),并且新增了注解@FunctionalInterface,同@Override的用法类似,是编译期间的校验,当一个接口上有@FunctionalInterface注解时,在这个接口中添加第二个需要实现的方法的时候就会报错。设计接口时也尽量保证一个接口只做一件事(单一责任制),会比较清晰,也可以使用lambda表达式。

默认方法

在jdk8中,接口中可以添加默认方法,即此方法在接口中具有默认实现。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
@FunctionalInterface
interface Interface1{
int doubleNum(int i);

default int add(int x,int y){
return x + y;
}
}

public class LambdaDemo1{
public static void main(String[] args){
Interface1 i1 = (i)->i * 2;
//当参数个数为1个的时候,括号可以去掉,最常用的写法
Interface1 i2 = i -> i * 2;
//也可以这样写
Interface1 i3 = (int i)->i * 2;
Interface1 i4 = (i)-> {
//方法有多行时可以这样写
return i * 2;
}
}
}

Interface1中有两个方法使用lambda表达式和@FunctionalInterface却没有报错,是因为之前说lambda表达式必须当接口中只有一个要实现的方法时才可以使用,这个接口中虽然有两个方法,但是只有一个需要实现的方法,因此不会影响。

这个特性使得许多老的接口能够新增方法,例如java.util.List接口中,如果在里面新增一个方法时,那么许多实现这个接口的类就都会报错,因为他们都必须实现这个方法,而现在有了默认实现方法,就不会强制要求实现这个接口的类强制实现默认方法,因此在1.8中,java.util.List也确实增加了一些默认实现方法。

函数式接口

先看下面代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
interface IMoneyFormat{
String format(int i);
}

class MyMoney{
private final int money;

public MyMoney(int money){
this.money = money;
}

public void printMoney(IMoneyFormat moneyFormat){
System.out.println("我的存款:"+moneyFormat.format(this.money));
}

}

public class MoneyDemo{
public static void main(String[] args){
MyMoney me = new MyMoney(999999);

me.printMoney(i -> new DecimalFormat("#,###").format(i));
}
}

这个代码清单实现了打印存款,在main方法里使用了lambda表达式,即me.printMoney只需要知道输入的是什么,输出的是什么,并不需要知道这个接口,也完全不需要定义这个接口,现在修改一下代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
class MyMoney{
private final int money;

public MyMoney(int money){
this.money = money;
}

public void printMoney(Function<Integer,String> moneyFormat){
System.out.println("我的存款:"+moneyFormat.apply(this.money));
}

}

public class MoneyDemo{
public static void main(String[] args){
MyMoney me = new MyMoney(999999);

me.printMoney(i -> new DecimalFormat("#,###").format(i));
}
}

把接口的定义去掉了,并且使用了jdk1.8的函数接口Function<T,R>,这个用于输入值和输出值都只有一个的时候,T表示输入类型,在此代码清单中就是Integer,输出就是R类型,在此代码清单中就是String。这个函数接口的apply方法就是调用这个函数,接受的参数时T类型,返回的参数是R类型。可以看出,使用函数接口的好处在于不用定义那么多的接口,还有一个好处,函数接口可以支持链式操作,例如我们要在输出的金额有单位,仅需修改main方法:

1
2
3
4
5
6
7
public class MoneyDemo{
public static void main(String[] args){
MyMoney me = new MyMoney(999999);
Function<Integer,String> moneyFormat = i -> new DecimalFormat("#,###").format(i)
me.printMoney(moneyFormat.andThen(s -> "人民币" + s));//输出为 我的存款:人民币999,999
}
}
jdk自带的函数接口
接口 输入参数 返回类型 说明
Predicate<T> T boolean 一个输入参数,返回boolean值
Consumer<T> T / 一个输入参数,没有返回值
Function<T,R> T R 一个输入参数,一个返回值
Supplier<T> / T 没有输入参数,没有返回值
UnaryOperator<T> T T 一个输入参数,一个返回值(输入值和返回值类型相同)
BiFunction<T,U,R> (T,U) R 两个输入参数,一个返回值
BinaryOperator<T> (T,T) T 两个输入参数,一个返回值(两个参数和一个返回值类型均相同)

当我们使用这些函数,参数类型是Java的常用类型的时候,还提供了特定的实现,例如IntPredicate可以替代Predicate<Integer>等等。

方法引用

先看下面代码清单:

1
2
3
4
5
6
public class MethodRefrenceDemo{
public static void main(String[] args){
Consumer<String> consumer = i - > System.out.println(i);
consumer.accept("aaa");
}
}

当函数执行体里面只有一个函数调用且参数就是箭头左边的参数一致的话,可以使用方法引用的方式来调用这个方法。修改成下面的代码:

1
2
3
4
5
6
public class MethodRefrenceDemo{
public static void main(String[] args){
Consumer<String> consumer = System.out::println(i);
consumer.accept("aaa");
}
}

方法引用还有其他方式:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
class Dog{

private String name = "中华田园犬";

public static void bark(Dog dog){
System.out.println(dog + "在叫");
}

public void eat(){
System.out.println(dog + "在吃狗粮");
}

public String toString(){
return this.name;
}

}

public class MethodRefrenceDemo2{
public static void main(String[] args){
//静态方法的引用 类名::方法名
Consumer<Dog> consumer = Dog::bark;
Dog dog = new Dog();
consumer.accept(dog);
//非静态方法的引用 对象实例::方法名
Consumer<Dog> consumer1 = dog::eat;
consumer1.accept(dog);
//构造函数的引用 类名::new
Supplier<Dog> supplier = Dog::new;
Dog dog1 = supplier.get();//创建了新的dog对象
}
}

变量引用

lambda表达式中引用外部变量必须是final类型的,因为lambda表达式的方法是不知道什么时候会运行的。

1
2
3
4
5
6
7
public class VarDemo{
public static void main(String[] args){
String str = "111";
Consumer<String> consumer = s -> System.out.println(str+s);
consumer.accept("222");
}
}

这段代码是没有问题的,但是要注意,此时的str变量已经是final类型了。当我们没有明确指定str为final类型,但是在lambda表达式中使用到了,就会默认此变量为final。如果我们现在更改str的值就会报错:

1
2
3
4
5
6
7
8
public class VarDemo{
public static void main(String[] args){
String str = "111";
str = "000";//会报错
Consumer<String> consumer = s -> System.out.println(str+s);
consumer.accept("222");
}
}

级联表达式和柯里化

级联表达式就是多个箭头的lambda表达式,例如:x->y->x+y;,

1
2
3
4
5
6
7
public class CurryDemo{
public static void main(String[] args){
//x->y->x+y 假设类型都是Integer,实现了x+y的效果
Function<Integer,Function<Integer,Integer>> function = x->y->x+y;
System.out.println(function.apply(2).apply(3));//输出5
}
}

柯里化就是把多个参数的函数转换为只有一个参数的函数,他的返回值也是一个函数,柯里化目的是把函数标准化,以提高函数的灵活性。

# Java
Your browser is out-of-date!

Update your browser to view this website correctly. Update my browser now

×