Java8 函数式编程

简述函数式编程


函数式编程是一种编程方式,符合一定的范式(Paradigm)

特性:

  • 函数是一等公民,可以直接传递。输入和输出都可以是一个函数。
  • 输入到输出的计算过程,是一个表达式,因此每个函数要有返回值。
  • 不影响外部,没有副作用。一个函数可以看成一个沙盒,不会修改外部变量。
  • 不改变内部状态。不去修改变量,只返回新值。因此只取决于输入。同输入必定同输出。
  • 惰性计算。不是绑定时立即计算,而是需要时再计算。无穷数列处理,消耗一个再取下一个。

好处:
1.简洁:代码行数减少。
2.易读:函数可链式组合,接近自然语言。
3.独立无状态:容易测试和debug.
4.并发:不修改就不需要加锁,天生适合并发。

Java8的函数式编程支持


  • 经典的应用场景:链表排序
    要自定义排序方式需要自己实现Comparator。虽然可以用匿名类免去了显式定义一个类的麻烦,但是依然有点繁琐。
    可以发现我们实现一个类就是为了一个类中的方法,这个方法接受两个字符串并返回一个整型值。说到底真正关心的只有这个方法而已。
1
2
3
4
5
6
7
List<String> names = Arrays.asList("foo","bar","baz");
Collections.sort(names,new Comparator<String>() {
@Override
public int compare(String a,String b) {
return b.compareTo(a);
}
});

Java8的新特性可以用lambda表达式进行代码简化
符号->的左边是输入,可以看作传参,右边是操作,可以看作函数体

1
Collections.sort(names,(String a,String b) -> {return b.compareTo(a);});

输入类型可以进行推断,单行的简单逻辑可以省略函数体,进一步简化为

1
Collections.sort(names,(a,b) -> b.compareTo(a));

实现机制:函数式接口


查看Collections.sort的实现,发现并没有任何改变,就是传入了一个Comparator接口的实现而已

1
2
3
public static <T> void sort(List<T> list, Comparator<? super T> c) {
list.sort(c);
}

真正的玄机在Comparator这个接口,已经经过了改造
就compare方法本身而言没什么改变,但是整个接口多了一个标注FunctionalInterface,成为了函数式接口
函数式接口要求只有一个要实现的方法,简单的说就是只有一个方法的接口,compare就是这个方法。
然而Comparator接口还有其他方法存在。equals方法构成了对Object类的重载,这种不算。
此外还新增了default方法,这类方法可以具有方法体,类似抽象类中的非抽象方法,实现类可以直接使用。

1
2
3
4
5
6
7
8
9
@FunctionalInterface
public interface Comparator<T> {
int compare(T o1, T o2);
boolean equals(Object obj);
default Comparator<T> reversed() {
return Collections.reverseOrder(this);
}
//...
}

成为函数式接口的好处是可以接受表达式实现,看上去像变量一样进行赋值
这就解释了为何sort函数没改变就可以支持新语法,因为它拿到的依然是Comparator接口的实现。

1
2
Comparator<String> comparator = (a,b) -> b.compareTo(a);
Collections.sort(names,comparator);

sort方法实际上是策略模式,可以通过Comparator来定制比较行为。 lambda表达式简化了策略模式的实现。

方法引用


函数式编程中函数是一等公民,因此也可以像变量一样引用方法,然而Java并不能直接引用一个方法,因此解决方案是把方法包装成实现类

方法引用的关键是java.util.function包下的一组函数式接口
根据函数的特征使用不同函数式接口

  • Function:单输入单输出
    apply方法即为调用函数的方法
1
2
3
4
5
@FunctionalInterface
public interface Function<T, R> {
R apply(T t);
//...
}
  • BiFunction:双输入单出输出
1
2
3
@FunctionalInterface
public interface BiFunction<T, U, R> {
R apply(T t, U u);
  • Consumer:只有单输入
1
2
3
4
5
@FunctionalInterface
public interface Consumer<T> {
void accept(T t);
//...
}
  • Supplier:只有单输出
1
2
3
4
@FunctionalInterface
public interface Supplier<T> {
T get();
}
  • Runnable:输入输出都没有
1
2
3
4
@FunctionalInterface
public interface Runnable {
public abstract void run();
}

方法引用的符号是双冒号::

  • 输入作为静态方法传值
    形式:x -> Class.method(x)
1
2
Function<Integer,String> function = String::valueOf;
System.out.println(function.apply(123).getClass().getName());
  • 输入作为已知实例方法的传值
    形式: x -> obj.method(x)
1
2
Consumer<Object> consumer = System.out::println;
consumer.accept("foo");
  • 调用输入自身方法
    形式:x -> x.method()
1
2
Function<String,Integer> function = String::length;
System.out.println(function.apply("foo"));
  • 构造单一对象
1
2
BiFunction<String,Locale,SimpleDateFormat> constructor = SimpleDateFormat::new;
SimpleDateFormat sdf = constructor.apply("yyyyMMdd", Locale.CHINA);
  • 构造数组对象
    参数为数组大小
1
2
3
Function<Integer,String[]> function = String[]::new;
String[] fiveStrings = function.apply(5);
System.out.println(Arrays.toString(fiveStrings));

异常


因为函数式接口方法没有声明抛出异常,这种情况下表达式不能抛出需检查的异常
要么表达式内部try-catch处理掉,要么定义新的函数式方法允许异常抛出

1
2
3
List<Boolean> created = Arrays.asList("foo.txt","bar.txt").stream()
.map(p -> new File(p).createNewFile()) //IOException, compile error
.collect(Collectors.toList());

内部处理掉

1
2
3
4
5
6
7
8
9
10
11
12
public boolean createFile(String name) throws IOException {
return new File(name).createNewFile();
}
List<Boolean> created = Arrays.asList("foo.txt","bar.txt").stream()
.map(p -> {
try{
return createFile(p);
}catch(IOException e){
return false;
}
})
.collect(Collectors.toList());

或者提供重新抛出非检查异常的版本

1
2
3
4
5
6
7
8
9
10
public boolean createFile(String name) {
try{
return new File(name).createNewFile();
}catch(UncheckedIOException e){
throw new RuntimeException(e);
}
}
List<Boolean> created = Arrays.asList("foo.txt","bar.txt").stream()
.map(p -> createFile(p))
.collect(Collectors.toList());

结合上述两种方法,建立新的函数式接口,允许抛出检查异常,并提供通用的转换逻辑

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@FunctionalInterface
public interface FunctionWithException<T, R, E extends Exception> {

R apply(T t) throws E;
}

static <T, R, E extends Exception> Function<T, R> wrapper(FunctionWithException<T, R, E> fe) {
return arg -> {
try {
return fe.apply(arg);
} catch (Exception e) {
throw new RuntimeException(e);
}
};
}

事实 final


按道理匿名内类访问外部变量,都需要声明final
Java8 新加了Effectively final功能,如果变量只赋值了一次,可以不显式声明final,由编译器自动加上
lambda表达式可以看成是匿名内部类的实现,也遵从此规则

1
2
3
4
int x = 10;
Arrays.asList(10, 20, 30).stream()
.filter(y -> y > x) //Effectively Final
.forEach(System.out::println);

虽然在lambda表达式之后修改,但是修改造成了编译器没加final,报错

1
2
3
4
5
int x = 10;
Arrays.asList(10, 20, 30).stream()
.filter(y -> y > x) //Error
.forEach(System.out::println);
x = 20;