谓词(predicate)
在数学上常常用来代表一个类似函数的东西,它接受一个参数值,并返回true或false。你在后面会看到,Java 8允许写Function<Apple,Boolean>——函数,但用Predicate是更标准的方式,效率也会更高一点儿,这避免了把boolean封装在Boolean里面
//传递方法
@Data
@AllArgsConstructor
public class Apple {
private String color;
private Integer weight;
public static boolean isGreenApple(Apple apple) {
return "green".equals(apple.getColor());
}
public static boolean isHeavyApple(Apple apple) {
return apple.getWeight() > 150;
}
}
public class Test1 {
public static void main(String[] args) {
ArrayList<Apple> apples = new ArrayList<>();
Apple aa = new Apple("aa", 20);
Apple bb = new Apple("bb", 30);
Apple green = new Apple("green", 160);
Apple green1 = new Apple("green", 60);
apples.add(aa);
apples.add(bb);
apples.add(green);
apples.add(green1);
List<Apple> apples1 = filterApples(apples, Apple::isGreenApple);
System.out.println(apples1);
List<Apple> apples2 = filterApples(apples, Apple::isHeavyApple);
System.out.println(apples2);
}
public static List<Apple> filterApples(List<Apple> inventory, Predicate<Apple> p) {
ArrayList<Apple> apples = new ArrayList<>();
for (Apple apple : inventory) {
if (p.test(apple)) {
apples.add(apple);
}
}
return apples;
}
}
不需要为只用一次的方法写定义;代码更干净、更清晰,用不着去找自己到底传递了什么代码。
public class Test2 {
public static void main(String[] args) {
ArrayList<Apple> apples = new ArrayList<>();
Apple aa = new Apple("aa", 20);
Apple bb = new Apple("bb", 30);
Apple green = new Apple("green", 160);
Apple green1 = new Apple("green", 60);
apples.add(aa);
apples.add(bb);
apples.add(green);
apples.add(green1);
List<Apple> apples1 = filterApples(apples, (Apple apple) -> "green".equals(apple.getColor()));
System.out.println(apples1);
List<Apple> apples2 = filterApples(apples, (Apple apple) -> apple.getWeight() > 60);
System.out.println(apples2);
}
public static List<Apple> filterApples(List<Apple> inventory, Predicate<Apple> p) {
ArrayList<Apple> apples = new ArrayList<>();
for (Apple apple : inventory) {
if (p.test(apple)) {
apples.add(apple);
}
}
return apples;
}
}
但要是Lambda的长度多于几行(它的行为也不是一目了然)的话,应该用方法引用来指向一个有描述性名称的方法,而不是使用匿名的Lambda。以代码的清晰度为准绳。
Java中的并行与无共享可变状态
Java里面并行很难,而且和synchronized相关的都容易出问题。在Java 8里面,首先,库会负责分块,即把大的流分成几个小的流,以便并行处理。其次,流提供的这个几乎免费的并行,只有在传递给filter之类的库方法的方法不会互动(比方说有可变的共享对象)时才能工作。举个例子,Apple::isGreenApple就是这样。确实,虽然函数式编程中的函数的主要意思是“把函数作为一等值”,不过它也常常隐含着第二层意思,即“执行时在元素之间无互动”。
默认方法
Java 8中加入默认方法主要是为了支持库设计师,让他们能够写出更容易改进的接口。在Java 8之前,List并没有stream或parallelStream方法,它实现的Collection接口也没有,因为当初还没有想到这些方法嘛!可没有这些方法,这些代码就不能编译。换作你自己的接口的话,最简单的解决方案就是让Java 8的设计者把stream方法加入Collection接口,并加入ArrayList类的实现。可要是这样做,对用户来说就是噩梦了。有很多的替代集合框架都用Collection API实现了接口。但给接口加入一个新方法,意味着所有的实体类都必须为其提供一个实现。语言设计者没法控制Collections所有现有的实现,这下你就进退两难了:你如何改变已发布的接口而不破坏已有的实现呢?
Java 8的解决方法就是打破最后一环——接口如今可以包含实现类没有提供实现的方法签名了!那谁来实现它呢?缺失的方法主体随接口提供了(因此就有了默认实现),而不是由实现类提供。
这就给接口设计者提供了一个扩充接口的方式,而不会破坏现有的代码。Java 8在接口声明中使用新的default关键字来表示这一点
例如,在Java8里,你现在可以直接对List调用sort方法。它是用Java 8 List接口中如下所示的默认方法实现的,它会调用Arrays.sort静态方法:
@SuppressWarnings({
"unchecked", "rawtypes"})
default void sort(Comparator<? super E> c) {
Object[] a = this.toArray();
Arrays.sort(a, (Comparator) c);
ListIterator<E> i = this.listIterator();
for (Object e : a) {
i.next();
i.set((E) e);
}
}
这意味着List的任何实体类都不需要显式实现sort,而在以前的Java版本中,除非提供了sort的实现,否则这些实体类在重新编译时都会失败
可能存在的问题
如果在好几个接口里有多个默认实现,是否意味着Java中有了某种形式的多重继承?是的,在某种程度上是这样。Java 8用一些限制来避免出现类似于C++中臭名昭著的菱形继承问题。
菱形继承问题
在C++中允许多继承,D类继承自B、C,而B、C有同一个父类A。那么这个时候调用say方法是否成功?答案是不能,编译器并不能判断这个say来自哪个父类
public class DiamondExtendProblem {
private interface A {
default void test() {
System.out.println("A");
}
}
private interface B1 extends A {
@Override
default void test() {
System.out.println("B1");
}
}
private interface B2 extends A {
@Override
default void test() {
System.out.println("B2");
}
}
private static class C implements B1, B2 {
}
public static void main(String[] args) {
C c = new C();
c.test();//菱形继承问题,Error:(32, 20) java: 类C从类型B1和B2中继承了test()的不相关默认值
}
}
C++的解决办法有两个:一是指定域,使用::
,二是虚继承
目前Java8解决这种冲突一般遵循三个原则:
- 类中的方法优先级最高。类或父类中声明的方法的优先级高于任何声明为默认方法的优先级。
- 如果无法依据第一条进行判断,那么子接口的优先级更高:函数签名相同时,优先选择拥有最具体实现的默认方法的接口,即如果B继承了A,那么B就比A更加具体。
- 最后,如果还是无法判断,继承了多个接口的类必须通过显式调用期望的方法。
//显示覆盖
public class DiamondExtendProblemSolve {
private interface A {
default void test() {
System.out.println("A");
}
}
private interface B1 extends A {
@Override
default void test() {
System.out.println("B1");
}
}
private interface B2 extends A {
@Override
default void test() {
System.out.println("B2");
}
}
private static class C implements B1, B2 {
@Override
public void test() {
B1.super.test();//显示调用B1的test方法
}
}
public static void main(String[] args) {
C c = new C();
c.test();
}
}
Java从函数式编程中引入的两个核心思想
- 将方法和Lambda作为一等值
- 在没有可变共享状态时,函数或方法可以有效、安全地并行执行
一等值和二等值
-
一等值
称作java的一等公民,即java可以操作的值,狭义上来说,是可以作为参数传递给方法的值)
-
二等值
有助于表示值的结构,但在程序执行期间不能传递的结构,通俗意义上来说就是不能作为参数传递的结构
其他好思想
-
在Java 8里有一个Optional类,如果你能一致地使用它的话,就可以帮助你避免出现NullPointer异常。它是一个容器对象,可以包含,也可以不包含一个值。Optional中有方法来明确处理值不存在的情况,这样就可以避免NullPointer异常了。换句话说,它使用类型系统,允许你表明我们知道一个变量可能会没有值。
-
(结构)模式匹配
f(0) = 1 f(n) = n*f(n-1) otherwise
在Java中,你可以在这里写一个if-then-else语句或一个switch语句。其他语言表明,对于更复杂的数据类型,模式匹配可以比if-then-else更简明地表达编程思想。对于这种数据类型,你也可以使用多态和方法重载来替代if-then-else,但对于哪种方式更合适,就语言设计而言仍有一些争论。两者都是有用的工具,你都应该掌握。不幸的是,Java 8对模式匹配的支持并不完全。
为什么Java中的switch语句应该限于原始类型值和Strings呢?函数式语言倾向于允许switch用在更多的数据类型上,包括允许模式匹配(在Scala代码中是通过match操作实现的)。在面向对象设计中,常用的访客模式可以用来遍历一组类(如汽车的不同组件:车轮、发动机、底盘等),并对每个访问的对象执行操作。模式匹配的一个优点是编译器可以报告常见错误,如:“Brakes类属于用来表示Car类的组件的一族类。你忘记了要显式处理它。”
函数式编程、及如何在Java 8中编写函数式风格、Java 8的功能并与Scala进行比较
通过行为参数传递代码
行为参数化就是可以帮助你处理频繁变更的需求的一种软件开发模式
例如,你可以将代码块作为参数传递给另一个方法,稍后再去执行它。这样,这个方法的行为就基于那块代码被参数化了。
DRY(Don’t Repeat Yourself,不要重复自己)软件工程原则
行为参数化
把所有属性结合起来的笨拙尝试,糟糕透了!
public static List<Apple> filterApples(List<Apple> inventory, String color,
int weight, boolean flag) {
List<Apple> result = new ArrayList<Apple>();
for (Apple apple: inventory){
if ( (flag && apple.getColor().equals(color)) ||
(!flag && apple.getWeight() > weight) ){
result.add(apple);
}
}
return result;
}
List<Apple> greenApples = filterApples(inventory, "green", 0, true);
List<Apple> heavyApples = filterApples(inventory, "", 150, false);
我们需要一种比添加很多参数更好的方法来应对变化的需求。让我们后退一步来看看更高层次的抽象。一种可能的解决方案是对你的选择标准建模:你考虑的是苹果,需要根据Apple的某些属性(比如它是绿色的吗?重量超过150克吗?)来返回一个boolean值。我们把它称为谓词(即一个返回boolean值的函数)。让我们定义一个接口来对选择标准建模:
public interface ApplePredicate{
boolean test (Apple apple);
}
现在你就可以用ApplePredicate的多个实现代表不同的选择标准了
public class AppleHeavyWeightPredicate implements ApplePredicate{
public boolean test(Apple apple){
return apple.getWeight() > 150;
}
}
public class AppleGreenColorPredicate implements ApplePredicate{
public boolean test(Apple apple){
return "green".equals(apple.getColor());
}
}
你可以把这些标准看作filter方法的不同行为。你刚做的这些和“策略设计模式”相关,它让你定义一族算法,把它们封装起来(称为“策略”),然后在运行时选择一个算法。在这里,算法族就是ApplePredicate,不同的策略就是AppleHeavyWeightPredicate和AppleGreenColorPredicate。但是,该怎么利用ApplePredicate的不同实现呢?你需要filterApples方法接受ApplePredicate对象,对Apple做条件测试。这就是行为参数化:让方法接受多种行为(或战略)作为参数,并在内部使用,来完成不同的行为。
public static List<Apple> filterApples(List<Apple> inventory,
ApplePredicate p){
List<Apple> result = new ArrayList<>();
for(Apple apple: inventory){
if(p.test(apple)){
result.add(apple);
}
}
return result;
}
List<Apple> redAndHeavyApples = filterApples(inventory, new AppleRedAndHeavyPredicate());
但令人遗憾的是,由于该filterApples方法只能接受对象,所以你必须把代码包裹在ApplePredicate对象里。你的做法就类似于在内联“传递代码”,因为你是通过一个实现了test方法的对象来传递布尔表达式的。通过使用Lambda,你可以直接把表达式"red".equals(apple.getColor()) &&apple.getWeight() > 150传递给filterApples方法,而无需定义多个ApplePredicate类,从而去掉不必要的代码。
行为参数化的好处在于你可以把迭代要筛选的集合的逻辑与对集合中每个元素应用的行为区分开来。这样你可以重复使用同一个方法,给它不同的行为来达到不同的目的。
目前,当要把新的行为传递给filterApples方法的时候,你不得不声明好几个实现ApplePredicate接口的类,然后实例化好几个只会提到一次的ApplePredicate对象。这真是很啰嗦,很费时间!
匿名类和Lambda表达式简化代码
-
匿名类
List<Apple> redApples = filterApples(inventory, new ApplePredicate() { public boolean test(Apple apple){ return "red".equals(apple.getColor()); } });
但匿名类还是不够好。第一,它往往很笨重,因为它占用了很多空间。在只需要传递一段简单的代码时(例如表示选择标准的boolean表达式),你还是要创建一个对象,明确地实现一个方法来定义一个新的行为(例如Predicate中的test方法)
-
Lambda
List<Apple> result = filterApples(inventory, (Apple apple) -> "red".equals(apple.getColor())); public interface Predicate<T>{ boolean test(T t); } public static <T> List<T> filter(List<T> list, Predicate<T> p){ List<T> result = new ArrayList<>(); for(T e: list){ if(p.test(e)){ result.add(e); } } return result; }
Lambda表达式
可以把Lambda表达式理解为简洁地表示可传递的匿名函数的一种方式:它没有名称,但它有参数列表、函数主体、返回类型,可能还有一个可以抛出的异常列表
使用地点
函数式接口
函数式接口就是只定义一个抽象方法的接口。接口现在还可以拥有默认方法(即在类没有对方法进行实现时,其主体为方法提供默认实现的方法)。哪怕有很多默认方法,只要接口只定义了一个抽象方法,它就仍然是一个函数式接口。
用函数式接口可以干什么呢?Lambda表达式允许你直接以内联的形式为函数式接口的抽象方法提供实现,并把整个表达式作为函数式接口的实例(具体说来,是函数式接口一个具体实现的实例)。你用匿名内部类也可以完成同样的事情,只不过比较笨拙:需要提供一个实现,然后再直接内联将它实例化。
函数描述符
函数式接口的抽象方法的签名基本上就是Lambda表达式的签名。我们将这种抽象方法叫作函数描述符。
例如,Runnable接口可以看作一个什么也不接受什么也不返回(void)的函数的签名,因为它只有一个叫作run的抽象方法,这个方法什么也不接受,什么也不返回(void)。在这里使用了一个特殊表示法来描述Lambda和函数式接口的签名,() -> void代表了参数列表为空,且返回void的函数,这正是Runnable接口所代表的。
Lambda表达式是怎么做类型检查的???
现在,只要知道Lambda表达式可以被赋给一个变量,或传递给一个接受函数式接口作为参数的方法就好了,当然这个Lambda表达式的签名要和函数式接口的抽象方法一样
@FunctionInterface
@FunctionalInterface又是怎么回事?
在新的Java API中,会发现函数式接口带有@FunctionalInterface的标注。这个标注用于表示该接口会设计成一个函数式接口。如果你用@FunctionalInterface定义了一个接口,而它却不是函数式接口的话,编译器将返回一个提示原因的错误。例如,错误消息可能是“Multiple non-overriding abstract methods found in interface Foo”,表明存在多个抽象方法。请注意,@FunctionalInterface不是必需的,但对于为此设计的接口而言,使用它是比较好的做法。它就像是@Override标注表示方法被重写了。
函数式接口
在java.util.function包中引入了几个新的函数式接口
-
Predicate
java.util.function.Predicate接口定义了一个名叫test的抽象方法,它接受泛型T对象,并返回一个boolean.如果你去查Predicate接口的Javadoc说明,可能会注意到诸如and和or等其他方法。
@FunctionalInterface public interface Predicate<T>{ boolean test(T t); } public static <T> List<T> filter(List<T> list, Predicate<T> p) { List<T> results = new ArrayList<>(); for(T s: list){ if(p.test(s)){ results.add(s); } } return results; } Predicate<String> nonEmptyStringPredicate = (String s) -> !s.isEmpty(); List<String> nonEmpty = filter(listOfStrings, nonEmptyStringPredicate);
-
Consumer
java.util.function.Consumer定义了一个名叫accept的抽象方法,它接受泛型T的对象,没有返回(void)。你如果需要访问类型T的对象,并对其执行某些操作,就可以使用这个接口。比如,你可以用它来创建一个forEach方法,接受一个Integers的列表,并对其中每个元素执行操作。在下面的代码中,你就可以使用这个forEach方法,并配合Lambda来打印列表中的所有元素。
public class TestConsumer { public static void main(String[] args) { forEach(Arrays.asList(1, 2, 3), System.out::println); } public static <T> void forEach(List<T> list, Consumer<T> consumer) { list.forEach(consumer); } }
-
Function
java.util.function.Function<T, R>接口定义了一个叫作apply的方法,它接受一个泛型T的对象,并返回一个泛型R的对象。如果你需要定义一个Lambda,将输入对象的信息映射到输出,就可以使用这个接口(比如提取苹果的重量,或把字符串映射为它的长度)。在下面的代码中,我们向你展示如何利用它来创建一个map方法,以将一个String列表映射到包含每个String长度的Integer列表。
public class TestFunction { public static void main(String[] args) { List<Integer> result = map(Arrays.asList("12", "3423", "ff543"), String::length); System.out.println(result); } public static <T, R> List<R> map(List<T> list, Function<T, R> function) { List<R> result = new ArrayList<>(); list.forEach(v -> result.add(function.apply(v))); return result; } }
原始类型特化
有些函数式接口专为某些类型而设计
Java类型要么是引用类型(比如Byte、Integer、Object、List),要么是原始类型(比如int、double、byte、char)。但是泛型(比如Consumer中的T)只能绑定到引用类型。这是由泛型内部的实现方式造成的。
在Java里有一个将原始类型转换为对应的引用类型的机制。这个机制叫作装箱(boxing)。相反的操作,也就是将引用类型转换为对应的原始类型,叫作拆箱(unboxing)。
但这在性能方面是要付出代价的。装箱后的值本质上就是把原始类型包裹起来,并保存在堆里。因此,装箱后的值需要更多的内存,并需要额外的内存搜索来获取被包裹的原始值。
Java 8为我们前面所说的函数式接口带来了一个专门的版本,以便在输入和输出都是原始类型时避免自动装箱的操作。
public class TestIntegerPredicate {
public static void main(String[] args) {
IntPredicate evenNumber = (int v) -> v % 2 == 0;
Scanner sc = new Scanner(System.in);
boolean test = evenNumber.test(sc.nextInt());
System.out.println(test);
}
}
一般来说,针对专门的输入参数类型的函数式接口的名称都要加上对应的原始类型前缀,比如DoublePredicate、IntConsumer、LongBinaryOperator、IntFunction等。Function接口还有针对输出参数类型的变种:ToIntFunction、IntToDoubleFunction等。
函数式接口 | 函数描述符 | 原始类型特化 |
---|---|---|
Predicate | T->boolean | IntPredicate,LongPredicate, DoublePredicate |
Consumer | T->void | IntConsumer,LongConsumer, DoubleConsumer |
Function<T,R> | T->R | IntFunction, IntToDoubleFunction, IntToLongFunction, LongFunction, LongToDoubleFunction, LongToIntFunction, DoubleFunction, ToIntFunction, ToDoubleFunction, ToLongFunction |
Supplier | ()->T | BooleanSupplier,IntSupplier, LongSupplier, DoubleSupplier |
UnaryOperator | T->T | IntUnaryOperator, LongUnaryOperator, DoubleUnaryOperator |
BinaryOperator | (T,T)->T | IntBinaryOperator, LongBinaryOperator, DoubleBinaryOperator |
BiPredicate<L,R> | (L,R)->boolean | |
BiConsumer<T,U> | (T,U)->void | ObjIntConsumer, ObjLongConsumer, ObjDoubleConsumer |
BiFunction<T,U,R> | (T,U)->R | ToIntBiFunction<T,U>, ToLongBiFunction<T,U>, ToDoubleBiFunction<T,U> |
使用案例 | Lambda的例子 | 对应的函数式接口 |
---|---|---|
布尔表达式 | (List list)->list.isEmpty() | Predicate<List> |
创建对象 | () -> new Apple(10) | Supplier |
消费一个对象 | (Apple a) -> System.out.println(a.getWeight()) | Consumer |
从一个对象中选择/提取 | (String s) -> s.length() | Function<String, Integer>或ToIntFunction |
合并两个值 | (int a, int b) -> a * b | IntBinaryOperator |
比较两个对象 | (Apple a1, Apple a2) -> a1.getWeight().compareTo(a2.getWeight()) | Comparator或BiFunction<Apple,Apple,Integer>或 ToIntBiFunction<Apple, Apple> |
异常、Lambda,函数式接口
请注意,任何函数式接口都不允许抛出受检异常(checked exception)。如果你需要Lambda表达式来抛出异常,有两种办法:定义一个自己的函数式接口,并声明受检异常,或者把Lambda包在一个try/catch块中。
@FunctionalInterface
public interface BufferedReaderProcessor {
String process(BufferedReader b) throws IOException;
}
BufferedReaderProcessor p = (BufferedReader br) -> br.readLine();
但是你可能是在使用一个接受函数式接口的API,比如Function<T, R>,没有办法自己创建一个。这种情况下,你可以显式捕捉受检异常:
Function<BufferedReader, String> f = (BufferedReader b) -> {
try {
return b.readLine();
}
catch(IOException e) {
throw new RuntimeException(e);
}
};
类型检查、类型推断以及限制
类型检查
Lambda的类型是从使用Lambda的上下文推断出来的。上下文(比如,接受它传递的方法的参数,或接受它的值的局部变量)中Lambda表达式需要的类型称为目标类型。
List<Apple> heavierThan150g = filter(inventory, (Apple a) -> a.getWeight() > 150);
从上面看,使用Lambda的上下文是什么呢?该Lambda是作为filter的参数,所以我们进入filter方法中
filter(List<Apple> inventory,Predicate<Apple> p)
从中我们可以很清晰地看到,上下文就是filter方法中的Predicate p参数,那么目标类型就是Predicate,T绑定到Apple,Predicate是一个函数式接口。
boolean test(Apple apple)
这是Predicate的抽象方法,该方法描述了一个函数描述符,Apple->boolean
最后函数描述符Apple->boolean匹配Lambda的签名,返回一个boolean,因此代码类型检查无误。
类型检查过程可以分解为如下所示。
- 首先,你要找出filter方法的声明。
- 第二,要求它是Predicate(目标类型)对象的第二个正式参数。
- 第三,Predicate是一个函数式接口,定义了一个叫作test的抽象方法。
- 第四,test方法描述了一个函数描述符,它可以接受一个Apple,并返回一个boolean。
- 最后,filter的任何实际参数都必须匹配这个要求。
请注意,如果Lambda表达式抛出一个异常,那么抽象方法所声明的throws语句也必须与之匹配。
同样的lambda,不同的函数式接口
有了目标类型的概念,同一个Lambda表达式就可以与不同的函数式接口联系起来,只要它们的抽象方法签名能够兼容。
Comparator<Apple> c1 = (Apple a1, Apple a2) -> a1.getWeight().compareTo(a2.