3、Consumer与Loan Pattern
比如我们有一个资源类Resource:
01 | public class Resource { |
04 | System.out.println( "Opening resource" ); |
07 | public void operate() { |
08 | System.out.println( "Operating on resource" ); |
11 | public void dispose() { |
12 | System.out.println( "Disposing resource" ); |
我们必须这样调用:
1 | Resource resource = new Resource(); |
因为对资源对象resource执行operate方法时可能抛出RuntimeException,所以需要在finally语句块中释放资源,防止可能的内存泄漏。
但是有一个问题,如果很多地方都要用到这个资源,那么就存在很多段类似这样的代码,这很明显违反了DRY(Don't Repeat It Yourself)原则。而且如果某位程序员由于某些原因忘了用try/finally处理资源,那么很可能导致内存泄漏。那咋办呢?Java 8提供了一个Consumer接口,代码改写为如下:
01 | public class Resource { |
04 | System.out.println( "Opening resource" ); |
07 | public void operate() { |
08 | System.out.println( "Operating on resource" ); |
11 | public void dispose() { |
12 | System.out.println( "Disposing resource" ); |
15 | public static void withResource(Consumer<Resource> consumer) { |
16 | Resource resource = new Resource(); |
18 | consumer.accept(resource); |
调用代码如下:
1 | Resource.withResource(resource -> resource.operate()); |
外部要访问Resource不能通过它的构造函数了(private),只能通过withResource方法了,这样代码清爽多了,而且也完全杜绝了因人为疏忽而导致的潜在内存泄漏。
4、stream+laziness => efficiency
像之前一样先来一段非常简单的代码:
01 | List<Integer> numbers = Arrays.asList( 1 , 2 , 3 , 4 , 5 , 6 ); |
03 | for ( int number : numbers) { |
04 | if (number % 2 == 0 ) { |
07 | System.out.println(n2); |
这段代码有什么问题? 没错,可读性非常差。第一步,我们利用《重构》一书中的最基础的提取小函数重构手法来重构代码如下:
01 | public boolean isEven( int number) { |
02 | return number % 2 == 0 ; |
05 | public int doubleIt( int number) { |
09 | public boolean isGreaterThan5( int number) { |
13 | for ( int number : numbers) { |
15 | int n2 = doubleIt(number); |
16 | if (isGreaterThan5(n2)) { |
17 | System.out.println(n2); |
OK,代码的意图清晰多了,但是可读性仍然欠佳,因为循环内嵌套一个if分支,if分支内又嵌套另外一个分支,于是继续重构代码如下:
01 | public boolean isEven( int number) { |
02 | return number % 2 == 0 ; |
05 | public int doubleIt( int number) { |
09 | public boolean isGreaterThan5( int number) { |
13 | List<Integer> l1 = new ArrayList<Integer>(); |
14 | for ( int n : numbers) { |
15 | if (isEven(n)) l1.add(n); |
18 | List<Integer> l2 = new ArrayList<Integer>(); |
23 | List<Integer> l3 = new ArrayList<Integer>(); |
25 | if (isGreaterThan5(n)) l3.add(n); |
28 | System.out.println(l3.get( 0 )); |
现在代码够清晰了,这是典型的“流水线”风格代码。但是等等,现在的代码执行会占用更多空间(三个List)和时间,我们来分析下。首先第二版代码的执行流程是这样的:
而我们的第三版代码的执行流程是这样的:
步骤数是13:9,所以有时候重构得到可读性强的代码可能会牺牲一些运行效率(但是一切都得实际衡量之后才能确定)。那么有没有“三全其美”的实现方法呢?即:
- 代码可读性强
- 代码执行效率不比第一版代码差
- 空间消耗小
Streams come to rescue! Java 8提供了stream方法,我们可以通过对任何集合对象调用stream()方法获得Stream对象,Stream对象有别于Collections的几点如下:
- 不存储值:Streams不会存储值,它们从某个数据结构的流水线型操作中获取值(“酒肉穿肠过”)
- 天生的函数编程特性:对Stream对象操作能得到一个结果,但是不会修改原始数据结构
- Laziness-seeking(延迟搜索):Stream的很多操作如filter、map、sort和duplicate removal(去重)可以延迟实现,意思是我们只要检查到满足要求的元素就可以返回
- 可选边界:Streams允许Client取足够多的元素直到满足某个条件为止。而Collections不能这么做
上代码:
5 | .filter(Lazy::isGreaterThan5) |
现在的执行流程是:
流程基本和第二版代码一致,这归功于Laziness-seeking特性。怎么理解呢?让我来构造下面这个场景:
2 | 过滤出偶数 => 乘以2 => 过滤出大于5的数 => 取出第一个数 |
而Laziness-seeking意味着
我们在每一步只要一找到满足条件的数字,马上传递给下一步去处理并且暂停当前步骤。比如先判断1是否偶数,显然不是;继续判断2是否偶数,是偶数;好,暂停过滤偶数操作,将2传递给下一步乘以2,得到4;4继续传递给第三步,4不满足大于5,所以折回第一步;判断3是否偶数,不是;判断4是否偶数,是偶数;4传递给第二步,乘以2得到8;8传递给第三步,8大于5;所以传递给最后一步,直接取出得到IntOptional[8]。
IntOptional[8]只是简单包装了下返回的结果,这样有什么好处呢?如果你接触过Null Object Pattern的话就知道了,这样可以避免无谓的null检测。
本文完,希望对大家有所帮助,O(∩_∩)O
参考自:
http://java.dzone.com/articles/why-we-need-lambda-expressions
http://java.dzone.com/articles/why-we-need-lambda-expressions-0 |