- 函数式编程具体实践了前面介绍的声明式编程(“你只需要使用不相互影响的表达式,描述想要做什么,由系统来选择如何实现”)和无副作用计算。正如我们前面所讨论的,这两个思想能帮助你更容易地构建和维护系统。
- 对于“什么是函数式编程”这一问题最简化的回答是“它是一种使用函数进行编程的方式”。那什么是函数呢?在函数式编程的上下文中,一个“函数”对应于一个数学函数:它接受零个或多个参数,生成一个或多个结果,并且不会有任何副作用
- 我们的准则是,被称为“函数式”的函数或方法都只能修改本地变量。除此之外,它引用的对象都应该是不可修改的对象。通过这种规定,我们期望所有的字段都为final类型,所有的引用类型字段都指向不可变对象。
- 要被称为函数式,函数或者方法不应该抛出任何异常。关于这一点,有一个极为简单而又极为教条的解释:你不应该抛出异常,因为一旦抛出异常,就意味着结果被终止了;不再像我们之前讨论的黑盒模式那样,由return返回一个恰当的结果值。
- 作为函数式的程序,你的函数或方法调用的库函数如果有副作用,你必须设法隐藏它们的非函数式行为,否则就不能调用这些方法(换句话说,你需要确保它们对数据结构的任何修改对于调用者都是不可见的,你可以通过首次复制,或者捕获任何可能抛出的异常实现这一目的)
- “没有可感知的副作用”(不改变对调用者可见的变量、不进行I/O、不抛出异常)的这些限制都隐含着引用透明性。如果一个函数只要传递同样的参数值,总是返回同样的结果,那这个函数就是引用透明的。String.replace方法就是引用透明的,因为像”raoul”.replace(‘r’, ‘R’)这样的调用总是返回同样的结果(replace方法返回一个新的字符串,用小写的r替换掉所有大写的R),而不是更新它的this对象,所以它可以被看成函数式的。
- 换句话说,函数无论在何处、何时调用,如果使用同样的输入总能持续地得到相同的结果,就具备了函数式的特征。
以构造无副作用方法的思想指导你的程序设计能帮助你编写更具维护性的代码。
为改善可读性和灵活性重构代码
我们一直强调的是使用Lambda表达式来简洁的去编写代码,但是经常在开发中有人会讲,我使用for循环就已经很清晰了,为什么需要用Lambda表达式,其实我认为没必要去非要使用某种标准。如果整个体系都是同一个标准,那就遵循好了。
- 如果你希望将一个既有的方法作为参数传递给另一个方法,那么方法引用无疑是我们推荐的方法,利用这种方式我们能写出非常简洁的代码。
- 改善方法可读性的方法
- 从匿名类到Lambda表达式的转换
- 从Lambda表达式到方法引用的转换
- 从命令式的数据处理切换到Stream
-
使用Lambda重构面向对象的设计模式
-
策略模式
关于策略模式[一个代表某个算法的接口(它是策略模式的接口)],此处不再具体解释,前面已经在设计模式专栏进行讲解。 ```java public interface Strategy { void executor(String s); }
```java
public class StrategyA implements Strategy {
@Override
public void executor(String s) {
System.out.println(s);
}
}
public class StrategyB implements Strategy {
@Override
public void executor(String s) {
System.out.println(s + s);
}
}
public class StrategyTest {
private static final String s = "SSS";
public static void main(String[] args) {
StrategyA strategyA = new StrategyA();
StrategyB strategyB = new StrategyB();
strategyA.executor(s);
strategyB.executor(s);
}
}
使用Lambda进行处理 ```java public class StrategyTest2 { private static final String s = “SSS”; static Strategy strategyA = s -> System.out.println(s); static Strategy strategyB = s -> System.out.println(s + s);
public static void main(String[] args) {
strategyA.executor(s);
strategyB.executor(s);
} }
<a name="GSINw"></a>
## 模板方法
- 假设你需要编写一个简单的在线银行应用。通常,用户需要输入一个用户账户,之后应用才能从银行的数据库中得到用户的详细信息,最终完成一些让用户满意的操作。不同分行的在线银行应用让客户满意的方式可能还略有不同,比如给客户的账户发放红利,或者仅仅是少发送一些推广文件
```java
public abstract class OnlineBanking {
public void processCustomer(int id) {
Customer customerWithId = Database.getCustomerWithId(id);
makeCustomerHappy(customerWithId);
}
protected abstract void makeCustomerHappy(Customer customerWithId);
}
class Database {
public static Customer getCustomerWithId(int id) {
return new Customer();
}
}
class Customer {
}
- 这样每个子类去实现怎么让顾客开心
使用Lambda表达式 ```java public class OnlineBanking {
public void processCustomer(int id, Consumer
makeCustomerHappy) { Customer customerWithId = Database.getCustomerWithId(id);
makeCustomerHappy.accept(customerWithId);
}
}
class Database { public static Customer getCustomerWithId(int id) { return new Customer(); } }
class Customer {
}
```java
public class OnlineBankingTest {
public static void main(String[] args) {
new OnlineBanking().processCustomer2(1, (Customer c) -> {
System.out.println(c);
});
}
}
调试
- Lambda的报错一般不是很好排查,来看一个案例
```java
public class Debugging {
public static void main(String[] args) {
} }List<Point> points = Arrays.asList(new Point(1, 2), null);
points.stream().map(p -> p.getX()).forEach(System.out::println);
```java
Exception in thread "main" java.lang.NullPointerException
at com.ldl.function.debug.Debugging2.lambda$main$0(Debugging2.java:14)
at java.util.stream.ReferencePipeline$3$1.accept(ReferencePipeline.java:193)
at java.util.Spliterators$ArraySpliterator.forEachRemaining(Spliterators.java:948)
at java.util.stream.AbstractPipeline.copyInto(AbstractPipeline.java:482)
at java.util.stream.AbstractPipeline.wrapAndCopyInto(AbstractPipeline.java:472)
at java.util.stream.ForEachOps$ForEachOp.evaluateSequential(ForEachOps.java:150)
at java.util.stream.ForEachOps$ForEachOp$OfRef.evaluateSequential(ForEachOps.java:173)
at java.util.stream.AbstractPipeline.evaluate(AbstractPipeline.java:234)
at java.util.stream.ReferencePipeline.forEach(ReferencePipeline.java:485)
at com.ldl.function.debug.Debugging2.main(Debugging2.java:14)
- 这些表示错误发生在Lambda表达式内部。由于Lambda表达式没有名字,所以编译器只能为它们指定一个名字。这个例子中,它的名字是lambda$main$0,看起来非常不直观。如果你使用了大量的类,其中又包含多个Lambda表达式,这就成了一个非常头痛的问题。
另外一个案例 ```java public class Debugging { public static void main(String[] args) {
List<Integer> list = Arrays.asList(1, 2, 3, 4, 4, 5);
list.stream().map(Debugging::divideByZero).forEach(System.out::println);
}
public static int divideByZero(int n) {
return n / 0;
} }
```java
Exception in thread "main" java.lang.ArithmeticException: / by zero
at com.ldl.function.debug.Debugging.divideByZero(Debugging.java:17)
at java.util.stream.ReferencePipeline$3$1.accept(ReferencePipeline.java:193)
at java.util.Spliterators$ArraySpliterator.forEachRemaining(Spliterators.java:948)
at java.util.stream.AbstractPipeline.copyInto(AbstractPipeline.java:482)
at java.util.stream.AbstractPipeline.wrapAndCopyInto(AbstractPipeline.java:472)
at java.util.stream.ForEachOps$ForEachOp.evaluateSequential(ForEachOps.java:150)
at java.util.stream.ForEachOps$ForEachOp$OfRef.evaluateSequential(ForEachOps.java:173)
at java.util.stream.AbstractPipeline.evaluate(AbstractPipeline.java:234)
at java.util.stream.ReferencePipeline.forEach(ReferencePipeline.java:485)
at com.ldl.function.debug.Debugging.main(Debugging.java:13)
[ERROR] Command execution failed.
- 调试:使用
peek(Consumer<? super T> action)
方法 ```java public class Debugging3 { public static void main(String[] args) {
} }List<Integer> list = Arrays.asList(1, 2, 3, 4, 4, 5);
list.stream()//
.peek(x -> System.out.println("from stream:" + x))//
.map(x -> x + 17)//
.peek(x -> System.out.println("from map:" + x))//
.filter(x -> x % 2 == 0)//
.peek(x -> System.out.println("from filter:" + x))//
.forEach(System.out::println);
- 输出结果
```java
from stream:1
from map:18
from filter:18
18
from stream:2
from map:19
from stream:3
from map:20
from filter:20
20
from stream:4
from map:21
from stream:4
from map:21
from stream:5
from map:22
from filter:22
22
小结
- Lambda表达式能提升代码的可读性和灵活性
- 如果你的代码中使用了匿名类,尽量用Lambda表达式替换它们,但是要注意二者间语义的微妙差别,比如关键字this,以及变量隐藏
- 跟Lambda表达式比起来,方法引用的可读性更好
- 尽量使用Stream API替换迭代式的集合处理
- Lambda表达式有助于避免使用面向对象设计模式时容易出现的僵化的模板代码,典型的比如策略模式、模板方法、观察者模式、责任链模式,以及工厂模式
- 即使采用了Lambda表达式,也同样可以进行单元测试,但是通常你应该关注使用了Lambda表达式的方法的行为
- 尽量将复杂的Lambda表达式抽象到普通方法中
- Lambda表达式会让栈跟踪的分析变得更为复杂
流提供的peek方法在分析Stream流水线时,能将中间变量的值输出到日志中,是非常有用的工具
参考文章
《Java 8 in Action》
- 《Java8函数式编程》