Java函数式编程

面向对象思想需要关注用什么对象完成什么事情,而函数式编程思想就类似于我们数学中的函数,它主要关注的是对数据进行了什么操作。

函数式编程的优势

  • 易于使用并发编程,大数据量下,集合处理效率高:可以使用并行流,自动使用多线程方式处理。
  • 代码可读性高
  • 消灭嵌套地狱

例如,要查询未成年作家的评分在70分以上的作家的书籍,由于洋流影响所以作家和书籍可能出现重复,需要进行去重。

如果按照原来的写法,需要这么写:

List<Author> authors = new ArrayList<>();
List<Book> bookList = new ArrayList<>();
Set<Book> uniqueBookValues = new HashSet<>();
Set<Author> uniqueAuthorValues = new HashSet<>();

for (Author author : authors) {
   if (uniqueAuthorValues.add(author)) {
       if (author.getAge() < 18) {
           List<Book> books = author.getBooks();
            for (Book book : books) {
                 if (book.getScore() > 70) {
                    if (uniqueBookValues.add(book)) {
                         bookList.add(book);
                     }
                 }
            }
       }
    }
}

System.out.println(bookList);

但如果改用函数式编程,代码会变得非常简单:

List<Author> authors = new ArrayList<>();

List<Book> collect = authors.stream()
       .distinct()
       .filter(author -> author.getAge() < 18)
       .map(author -> author.getBooks())
       .flatMap(Collection::stream)
       .filter(book -> book.getScore() > 70)
       .distinct()
       .collect(Collectors.*toList*());
System.out.println(collect);

01 Lambda表达式

概述

Lambda是IDK8中一个语法糖,他可以对 某些匿名内部类(这类匿名内部类是一个接口,并且接口中只有一个抽象方法需要被重写) 的写法进行简化。

它是函数式编程思想的一个重要体现,让我们不用关注是什么对象,而是更关注我们对数据进行了什么操作,即只关注匿名内部类中抽象方法的 **参数(数据)**及 函数体(操作)

核心原则:可推导可省略。参数类型如果可以推导出来,就可以省略。

基本格式

(参数列表) -> {代码}

使用案例

例一

在创建线程并启动时,可以使用匿名内部类的写法:

new Thread(new Runnable() {
   @Override
   public void run() {
     System.out.println("传统方式创建线程");
   }
}).start();

因为使用到了匿名内部类 Runnable 接口,并且该接口中只有 run() 一个抽象方法,所以可以使用Lambda的格式对其进行修改,如下所示:

// () -> {...} 中 () 相当于 run() 方法的参数列表, {...} 相当于 run() 方法的函数体
new Thread(() -> {
  System.*out*.println("Lambda方式创建线程");
}).start();

例二

例如,有如下一个方法:

public static int calculateNum(IntBinaryOperator op) {
   int a = 10;
   int b = 20;
   return op.applyAsInt(a, b);
}

该方法接收一个 IntBinaryOperator 类型的参数,并调用该参数中的 applyAsInt() 方法。IntBinaryOperator 是 JDK 内置的一个接口,接口源码如下:

@FunctionalInterface
public interface IntBinaryOperator {
   int applyAsInt(int left, int right);
}

可以看到该接口只有一个抽象方法,因此在调用 calculateNum() 方法时,传入自定义的 IntBinaryOperator 实现时,就可以使用 lambda 表达式来实现。

calculateNum((a, b) -> {
   return a + b;
});

甚至可以进一步简化:

calculateNum((a, b) -> a + b);

Tips 在 IDEA 中,可以通过 Alt + Enter 快捷键来进行匿名内部类和Lambda表达式格式之间的转换。 在匿名内部类格式下,使用快捷键可以将其转为Lambda表达式; 在Lambda格式下,使用快捷键可以将其转为匿名内部类格式;

例三

现有方法定义如下:

public static void printNum(IntPredicate p) {
   int[] a = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
   for (int i : a) {
       if (p.test(i)) {
           System.out.println(i);
       }
   }
}

其中 IntPredicate 是一个接口,接口源码如下:

@FunctionalInterface

public interface IntPredicate {
   boolean test(int value);
    
   default IntPredicate and(IntPredicate other) {
       Objects.requireNonNull(other);
       return (value) -> test(value) && other.test(value);
   }
    
   default IntPredicate negate() {
       return (value) -> !test(value);
   }

   default IntPredicate or(IntPredicate other) {
       Objects.requireNonNull(other);
       return (value) -> test(value) || other.test(value);
   }
}

先使用匿名内部类的方式调用该方法:

printNum(new IntPredicate() {
   @Override
   public boolean test(int value) {
       return value % 2 == 0;
   }
});

该接口中虽然不止一个方法,但是只有一个 test() 抽象方法必须被重写,其他方法是默认方法不需要被重写,因此,也可以使用 Lambda 表达式实现:

printNum((int value) -> {
   return value % 2 == 0;
});

// 进一步简写
printNum(value -> value % 2 == 0)

省略规则

在例二和例三的代码中,给出了两种 Lambda 表达式,一种是单纯省略方法名和接口名,保留完整参数列表和方法体;第二种是省略参数类型,以及return 关键字等。

省略的规则如下:

  1. 参数类型可以省略;
  2. 方法只有一个参数时,参数列表的括号可以省略;
  3. 方法体只有一句代码时,大括号、return关键字和唯一一句代码的分号可以省略;