函数式编程(一)|函数式编程(一) lambda、FunctionalInterface、Method Reference

由于函数式编程涉及内容较多,因此对函数式编程写一个系列博客,内容从JAVA8的新特性开始阐述,而后阐述函数式编程的写法,最后深入源码讲述函数式编程。
函数式编程是一种编程风格,关于它业界褒贬不一,而主流的编程语言都提供了函数式编程的支持,C++从C++11、java从JAVA8引入lambda表达式都提供了函数式编程的支持,本系列文章以JAVA8为进行函数式编程的介绍。
JAVA8的新特性

  • lamda表达式
  • 接口支持default方法和静态方法
  • 函数式接口
  • 方法引用
  • Stream与Collector
  • Optional
  • 日期API
文章围绕Student类展开,为了简化类接口,设计类为包可见,类的定义如下:
class Student{ String name; int age; int score; public Student(String name, int age, int score) { super(); this.name = name; this.age = age; this.score = score; } @Override public String toString() { return "Student [name=" + name + ", age=" + age + ", score=" + score + "]"; } }

初始数据如下:
List list = Arrays.asList(new Student("wang", 20, 90), new Student("zhao", 30, 80), new Student("li", 25, 99), new Student("sun", 20, 80), new Student("zhou", 30, 70));

lambda表达式
需求是为数据list按照学生的成绩从高到低排序,我们可以使用匿名内部类的方式实现该需求:其中sort的排序规则是Comparator的匿名内部类,如果查看编译出的bin文件可以发现"xxx$1.class",而其正是Comparator匿名子类编译出的class文件。
Collections.sort(list, new Comparator() { @Override public int compare(Student o1, Student o2) { return o2.score - o1.score; }});

使用lambda表达式来简化上述代码:
Collections.sort(list, (o1, o2) -> o2.score - o1.score);

在JAVA8中List接口中引入了sort的默认方法,可以继续简化代码为:
list.sort((o1, o2) -> o2.score - o1.score);

lambda表达式可以大幅的简化代码的编写,关于lambda表达式的写法不是本文的讲述范围。
匿名内部类与lambda表达式的区别
  • 匿名内部类会编译产生class文件,而lambda不会
  • 匿名内部类可以为任意接口创建实例,而lambda只能为函数式接口创建实例(关于函数式接口可见下一小节)
  • 匿名内部类可以调用接口的默认方法,而函数式接口不允许
接口支持default方法和静态方法
JAVA8前interface中的方法只是接口声明,由于JAVA是单继承体系,JAVA8前的接口的职责是约定接口,面向抽象编程,而想增强接口的能力时,需要修改全部实现该接口的类,而JAVA8引入了default方法就可以在保持原有继承体系的情况下增强接口的作用。比如上例中的 List增加了default的sort接口,提供对list的排序默认实现。上例中使用的Comparator的接口中增加了reversed、thenComparing等默认接口在不破坏继承体系的前提下实现了Comparator接口的增强。举例说明,目前需求变化为首先对学生按照成绩排序,成绩一样的按照年龄从小到大排序。
Comparator c = (o1, o2) -> {return o2.score - o1.score; }; list.sort(c.thenComparing((o1,o2) -> o1.age - o2.age));

从上述代码中看到,首先定义一个按照成绩排序的比较器,然后调用Comparator的thenComparing,将按照年龄排序的比较器传入,很方便的实现了上述需求。而这要归功于Comparator接口的默认方法增强。
由于Collections提供的默认排序方式是从小到大排序,而需要从大到小排序时,需要自定义比较器实现,而在JAVA8中可以按照如下方式进行排序
List nums = Arrays.asList(1, 4, 7, 3, 2, 5); Collections.sort(nums, Comparator.reverseOrder());

代码使用Comparator接口的静态方法reverseOrder,这自然是JAVA8的接口的静态方法增强带来的好处。
函数式接口
@Documented @Retention(RetentionPolicy.RUNTIME) @Target(ElementType.TYPE) public @interface FunctionalInterface {}

函数式接口FunctionalInterface是JAVA8引入的类的注解,它表示有且仅有一个抽象方法的接口。
  • 默认的方法由于存在实现不算抽象方法
  • 由于接口的实例都会实现Object的类,因此Object的公用方法不算抽象方法。
    JavaDoc关于FunctionalInterface的定义如下:
    A functional interface has exactly one abstract method. Since default methods have an implementation, they are not abstract. If an interface declares an abstract method overriding one of the public methods of java.lang.Object, that also does not count toward the interface's abstract method count since any implementation of the interface will have an implementation from java.lang.Object or elsewhere.
@FunctionalInterface public interface Comparator { int compare(T o1, T o2); boolean equals(Object obj); default Comparator reversed() { return Collections.reverseOrder(this); } public static > Comparator naturalOrder() { return (Comparator) Comparators.NaturalOrderComparator.INSTANCE; } ... }

JAVA的Comparator为函数式接口(有且只有一个抽象方法),其中compare为抽象的方法,乍看equals方法也是一个抽象方法,由于其属于Object的公有方法,因此不算抽象方法,另外静态方法和默认方法都存在实现,自然也不属于抽象方法。
函数式编程与面向对象编程的一个最大的区别是函数式编程可以传递行为,而函数式接口本身提供的默认方法为编写高阶函数代码提供了极强的便捷性,JAVA8新增的主要函数式接口:
1.Predicate 主要作为过滤器的判断条件,在后面Stream的章节中作为filter\match的参数使用。
public interface Predicate { boolean test(T t); ... }

需求1:打印学生列表中年龄小于25岁的学生
Predicate p1 = s -> s.age < 25; list.stream().filter(p1).forEach(System.out::println);

需求2:打印学生列表中成绩大于等于90的学生
Predicate p2 = s -> s.score >= 90; list.stream().filter(p2).forEach(System.out::println);

自然利用Predicate接口提供的默认方法,非常简单的实现了函数式编程,下面的几个函数的作用也非常简单。
list.stream().filter(p1.or(p2)).forEach(System.out::println); //p1||p2 list.stream().filter(p2.and(p1)).forEach(System.out::println); //p1 && p2 list.stream().filter(p1.negate().or(p2)).forEach(System.out::println); // !p1 || p2 list.stream().filter(p1.negate().and(p2.negate())).forEach(System.out::println); //!p1 && !p2

2.Consumer 主要作为消费者使用,如其接口,传入一个T类型的参数,不返回任何东西
public interface Consumer { void accept(T t); ... }

如Predicate中例子中System.out::println即是一个Consumer的方法引用。
Consumer c1 = s -> System.out.println(s.name); Consumer c2 = s -> System.out.println(s.score >= 80 ? "A" : "B" ); list.forEach(s -> {}); //什么都不干的Consumer list.forEach(c1); //打印学生的名字的Consumer list.forEach(c1.andThen(c2)); //打印学生名字后再打印学生成绩为A or B的Consumer

3.Function 函数,传入T类型,返回R类型
public interface Function { R apply(T t); ... }

需求:判断学生列表中是否存在名字以wang开头的
boolean b = list.stream().map(s -> s.name).anyMatch(s -> s.startsWith("wang"));

其中的s -> s.name即为Function类型的lambda表达式。Function中存在andThen、compose根据语义可以知道上述默认方法的作用。
4.Supplier Supplier是不传入参数返回T类型的参数,其作用主要是作为工厂方法使用
Supplier
public interface Supplier { T get(); }

需求:将学生列表中所有的学生姓名归集到List中
List rst = list.stream().map(s ->s.name). collect(ArrayList::new, List::add, List::addAll);

【函数式编程(一)|函数式编程(一) lambda、FunctionalInterface、Method Reference】上述代码使用了Collector的内容,其中ArrayList::new即为一个Supplier类型。其本身是ArrayList的构造方法引用。
5.其他变种 Java8的函数式接口在java.util.function包中,其他函数式接口大都是以上的变种,要么入参增加为2个比如BiFunction、BiConsumer,要么是为了避免对普通类型(int、long、double)类型的装箱操作引入的特化类型如DoubleConsumer、IntSupplier等。本篇以BiFunction为例简单讲述,其无非是入参是两个,第一个入参为T类型,第2个入参为U类型,返回值为R类型
public interface BiFunction { R apply(T t, U u); ... }

方法引用
JAVA8的方法引用分为四种
  • 类名::静态方法名
  • 类名::实例方法名
  • 对象名::实例方法名
  • 类名::new
类名::静态方法名
List rst = list.stream().map(String::valueOf).collect(Collectors.toList());

valueOf为String的静态方法,String::valueOf为【类名::静态方法名】的方法引用,实现了Student到String的转化。
类名::实例方法名
List strings = Arrays.asList("wang", "zhao", "li", "zhou"); strings.sort(String::compareToIgnoreCase);

compareToIgnoreCase为String的实例方法,String::compareToIgnoreCase为【类名::实例方法名】的方法引用,实现了String的比较器。
对象名::实例方法名
class StudentComp{ int compareByName(Student s1, Student s2) { return s1.name.compareToIgnoreCase(s2.name); } } list.sort(new StudentComp()::compareByName);

new StudentComp()::compareByName为【对象名::实例方法名】的方法引用,实现了Student的按照名字升序排列的比较器。
类名::new
Supplier supplier = StringBuilder::new; System.out.println(supplier.get().append("aaa").append("bbb").toString());

StringBuilder::new为【类名::new】的方法引用,其作用与new StringBuilder()一致。
关于Java8的其他的新特性,特别是Stream与Collector的内容非常多,将在下一篇博客中进行阐述。
WalkeR_ZG

    推荐阅读