函数式编程
函数式编程
提示
没有共享的可变数据,以及将方法和函数(即代码)传递给其他方法的能力,这两个要点是函数式编程范式的基石
一、名词解释
1.外部迭代
Collection API:用for-each循环一个个地迭代元素,然后再处理元素。我们把这种数据迭代方法称为外部迭代
2.内部迭代
有了Stream API,你根本用不着操心循环的事情。数据处理完全是在库内部进行的。我们把这种思想叫作内部迭代。
3.方法引用(行为参数化)
比方说,你想要筛选一个目录中的所有隐藏文件。你需要编写一个方法,然后给它一个File
,它就会告诉你文件是不是隐藏的。幸好,File类里面有一个叫作isHidden
的方法。可以把它看作一个函数,接受一个File
,返回一个布尔值。但要用它做筛选,需要把它包在一个FileFilter
对象里,然后传递给File.listFiles
方法,如下所示:
File[] hiddenFiles = new File(".").listFiles(new FileFilter() {
public boolean accept(File file) {
return file.isHidden(); ←---- 筛选隐藏文件
}
});
虽然只有三行,但这三行可真够绕的。我们第一次碰到的时候肯定都说过:“非得这样不可吗?”已经有一个方法isHidden
可用,为什么非得把它包在一个啰唆的FileFilter
类里面再实例化呢?因为在Java 8之前你必须这么做!如今在Java 8里,你可以把代码重写成这样:
File[] hiddenFiles = new File(".").listFiles(File::isHidden);
你已经有了函数isHidden
,因此只需用Java 8的方法引用::
语法(即“把这个方法作为值”)将其传给listFiles
方法。
只要方法中有代码(方法中的可执行部分),那么用方法引用就可以传递代码。
一个例子
假设你有一个Apple
类,它有一个getColor
方法,还有一个变量inventory
保存着一个Apples
列表。你可能想要选出所有的绿苹果(此处使用包含值GREEN
和RED
的Color
枚举类型 ),并返回一个列表。通常用筛选(filter)一词来表达这个概念。在Java 8之前,你可能会写这样一个方法filterGreenApples
:
public static List<Apple> filterGreenApples(List<Apple> inventory){
List<Apple> result = new ArrayList<>(); ←---- result是用来累积结果的List,开始为空,然后一个个加入绿苹果
for (Apple apple: inventory){
if (GREEN.equals(apple.getColor())) { ←---- 加粗显示的代码会仅仅选出绿苹果
result.add(apple);
}
}
return result;
}
但是接下来,有人可能想要选出重的苹果,比如超过150克的苹果,于是你心情沉重地写了下面这个方法,甚至用了复制粘贴:
public static List<Apple> filterHeavyApples(List<Apple> inventory){
List<Apple> result = new ArrayList<>();
for (Apple apple: inventory){
if (apple.getWeight() > 150) { ←---- 这里加粗显示的代码会仅仅选出重的苹果
result.add(apple);
}
}
return result;
}
我们都知道软件工程中复制粘贴的危险——给一个做了更新和修正,却忘了另一个。嘿,这两个方法只有一行不同:if里面加粗的那行条件。如果这两个加粗的方法之间的差异仅仅是接受的重量范围不同,那么你只要把接受的重量上下限作为参数传递给filter
就行了,比如指定(150, 1000)
来选出重的苹果(超过150克),或者指定(0, 80)
来选出轻的苹果(低于80克)。
但是,前面提过了,Java 8会把条件代码作为参数传递进去,这样可以避免filter方法中出现重复的代码。现在你可以写:
public static boolean isGreenApple(Apple apple) {
return GREEN.equals(apple.getColor());
}
public static boolean isHeavyApple(Apple apple) {
return apple.getWeight() > 150;
}
public interface Predicate<T>{ ←---- 写出来是为了清晰(平常只要从java.util.function导入就可以了)
boolean test(T t);
}
static List<Apple> filterApples(List<Apple> inventory,
Predicate<Apple> p) { ←---- 方法作为Predicate参数p传递进去(见附注栏“什么是谓词?”)
List<Apple> result = new ArrayList<>();
for (Apple apple: inventory){
if (p.test(apple)) { ←---- 苹果符合p所代表的条件吗
result.add(apple);
}
}
return result;
}
要用它的话,你可以写:
filterApples(inventory, Apple::isGreenApple);
或者
filterApples(inventory, Apple::isHeavyApple);
4.谓词
前面的代码传递了方法Apple::isGreenApple
(它接受参数Apple
并返回一个boolean
)给filterApples
,后者则希望接受一个Predicate<Apple>
参数。谓词(predicate)在数学上常常用来代表类似于函数的东西,它接受一个参数值,并返回true或false。后面你会看到,Java 8也允许你写Function<Apple,Boolean>
——在学校学过函数却没学过谓词的读者对此可能更熟悉,但用Predicate<Apple>
是更标准的方式,效率也会更高一点儿,这避免了把boolean
封装在Boolean
里面。
5.默认方法
Java 8之前,List<T>
并没有stream或parallelStream方法,它实现的Collection<T>
接口也没有,因为当初还没有想到这些方法嘛!可没有这些方法,这些代码就不能编译。换作你自己的接口的话,最简单的解决方案就是让Java 8的设计者把stream方法加入Collection接口,并加入ArrayList类的实现。
可要是这样做,对用户来说就是噩梦了。有很多集合框架都用Collection API实现了接口。但给接口加入一个新方法,意味着所有的实体类都必须为其提供一个实现。语言设计者没法控制Collection所有现有的实现,这下你就进退两难了:你如何改变已发布的接口而不破坏已有的实现呢?
Java 8的解决方法就是打破最后一环——接口如今可以包含实现类没有提供实现的方法签名了!那谁来实现它呢?缺失的方法主体随接口提供了(因此就有了默认实现),而不是由实现类提供。
这就给接口设计者提供了一种扩充接口的方式,而不会破坏现有的代码。Java 8在接口声明中使用新的default关键字来表示这一点。
例如,在Java 8里,你可以直接对List调用sort方法。它是用Java 8 List接口中如下所示的默认方法
实现的,它会调用Collections.sort静态方法:
default void sort(Comparator<? super E> c) {
Collections.sort(this, c);
}
这意味着List的任何实体类都不需要显式实现sort,而在以前的Java版本中,除非提供了sort的实现,否则这些实体类在重新编译时都会失败。
6.Java中的函数
Java 8中新增了函数,作为值的一种新形式。
想想Java程序可能操作的值吧。首先有原始值,比如42(int类型)和3.14(double类型)。其次,值可以是对象(更严格地说是对象的引用)。获得对象的唯一途径是利用new,这也许是通过工厂方法或库函数实现的;对象引用指向一个类的实例。例子包括"abc"(String类型)、new Integer(1111)(Integer类型),以及new HashMap<Integer,String>(100)的结果——它显式调用了HashMap的构造函数。甚至数组也是对象。那么有什么问题呢?
为了帮助回答这个问题,我们要注意到,编程语言的整个目的就在于操作值,按照历史上编程语言的传统,这些值应被称为一等值(或一等公民)。编程语言中的其他结构也许有助于表示值的结构,但在程序执行期间不能传递,因而是二等值。前面所说的值是Java中的一等值,但其他很多Java概念(比如方法和类等)则是二等值。人们发现,在运行时传递方法能将方法变成一等值。这在编程中非常有用,因此Java 8的设计者把这个功能加入到了Java中。
7.流处理
流
是一系列数据项,一次只生成一项。程序可以从输入流中一个一个读取数据项,然后以同样的方式将数据项写入输出流。一个程序的输出流很可能是另一个程序的输入流。
8.Lambda——匿名函数
除了允许(命名)函数成为一等值外,Java 8还体现了更广义的将函数作为值的思想,包括Lambda5 (或匿名函数)。比如,你现在可以写(int x) -> x + 1
,表示“调用时给定参数x,就返回x+1值的函数”。你可能会想这有什么必要呢?因为你可以在MyMathsUtils
类里面定义一个add1
方法,然后写MyMathsUtils::add1
嘛!确实是可以,但要是你没有方便的方法和类可用,新的Lambda语法更简洁。
3.方法引用(行为参数化)把方法作为值来传递显然很有用,但要是为类似于isHeavyApple
和isGreenApple
这种可能只用一两次的短方法写一堆定义就有点儿烦人了。不过Java 8也解决了这个问题,它引入了一套新记法(匿名函数或Lambda),让你可以写
filterApples(inventory, (Apple a) -> GREEN.equals(a.getColor()) );
或者
filterApples(inventory, (Apple a) -> a.getWeight() > 150 );
甚至
filterApples(inventory, (Apple a) -> a.getWeight() < 80 || RED.equals(a.getColor()) );
9.菱形继承
待完成
10.匿名类
匿名类
和你熟悉的Java局部类(块中定义的类)差不多,但匿名类没有名字。它允许你同时声明并实例化一个类。换句话说,它允许你随用随建。
二、新类
2.1 Optional<T>
三、示例
1.按照重量给inventory中的苹果排序
// Java7
Collections.sort(inventory, new Comparator<Apple>() {
public int compare(Apple a1, Apple a2){
return a1.getWeight().compareTo(a2.getWeight());
}
});
// Java8
inventory.sort(comparing(Apple::getWeight));
2.你需要从一个列表中筛选金额较高的交易,然后按货币分组。
-- Java7
Map<Currency, List<Transaction>> transactionsByCurrencies =
new HashMap<>(); ←---- 建立累积交易分组的Map
for (Transaction transaction : transactions) { ←---- 遍历交易的List
if(transaction.getPrice() > 1000){ ←---- 筛选金额较高的交易
Currency currency = transaction.getCurrency(); ←---- 提取交易货币
List<Transaction> transactionsForCurrency =
transactionsByCurrencies.get(currency);
if (transactionsForCurrency == null) { ←---- 如果这个货币的分组Map是空的,那就建立一个
transactionsForCurrency = new ArrayList<>();
transactionsByCurrencies.put(currency,
transactionsForCurrency);
}
transactionsForCurrency.add(transaction); ←---- 将当前遍历的交易添加到具有同一货币的交易List中
}
}
-- Java8
import static java.util.stream.Collectors.groupingBy;
Map<Currency, List<Transaction>> transactionsByCurrencies =
transactions.stream()
.filter((Transaction t) -> t.getPrice() > 1000) ←---- 筛选金额较高的交易
.collect(groupingBy(Transaction::getCurrency)); ←---- 按货币分组