Java 8为什么需要Lambda表达式

来源:互联网 发布:windows 采样频率 编辑:程序博客网 时间:2024/06/10 12:45

转自 http://my.oschina.net/feichexia/blog/119805 

函数编程在C#、Python、JavaScript中都得到充分体现。而Java直到最新的Java 8才开始正式支持函数编程,最明显的改进就是对Lamba表达式的支持。正如C#之父Anders Hejlsberg在那篇文章 编程语言大趋势 中所讲,未来的编程语言将逐渐融合各自的特性,而不存在单纯的声明式语言(如之前的Java)或者单纯的函数编程语言。将来声明式编程语言借鉴函数编程思想,函数编程语言融合声明式编程特性...这几乎是一种必然趋势。如下图所示:

                                      图2  影响力较大的三个趋势

                                                影响力较大的三个趋势

    那具体而言我们为什么需要Lambda表达式呢?难道Java的OO和命令式编程(imperative programming)特性不够强大吗?下面让我们来分析下其原因。


1、内部循环和外部循环

     先看一个大家耳熟能详的例子:

1List<Integer> numbers = Arrays.asList(1,2,3,4,5,6);
2 
3for (intnumber : numbers) {
4    System.out.println(number);
5}
    是不是很常见呢?这个叫外部循环(External Iteration)。但是外部循环有什么问题呢?简单来说存在下面三个缺点:
  1. 只能顺序处理List中的元素(process one by one)
  2. 不能充分利用多核CPU
  3. 不利于编译器优化

    而如果利用内部循环,代码写成下面这样:

1List<Integer> numbers = Arrays.asList(1,2,3,4,5,6);
2 
3numbers.forEach((Integer value) -> System.out.println(value));
    这样就能规避上面的三个问题:
  1. 不一定需要顺序处理List中的元素,顺序可以不确定
  2. 可以并行处理,充分利用多核CPU的优势
  3. 有利于JIT编译器对代码进行优化

    类似的C#从4.0版本开始也支持集合元素并行处理,代码如下:

1List<int> nums =newList<int> { 1, 2, 3, 4, 5, 6 };
2Parallel.ForEach(nums, (value) =>
3{
4   Console.WriteLine(value);
5});


2、传递行为,而不仅仅是传值

     如果你使用C#有一段时间的话,那么你很可能已经明白这个标题的意思了。在C#中,经常看到一些函数的参数是Action或者Func类型,比如下面这个:

01public classArticleDac {
02   ...
03   publicArticle GetArticles(Func<IDbSet<Article>, Article> func)  // 这里传递的就是行为
04   {
05      using(var db = xx) {
06         returnfunc(db.Articles);
07      
08   }
09   ...
10}
11// 下面是调用
12int articleId = 119;
13var firstArticle = newArticleDac().GetArticles(
14    articleDbSet =>
15    articleDbSet.AsQueryable().FirstOrDefault(x => x.id == articleId)
16);
     看不懂?没关系。我们先来看一个体现传值局限性的场景吧,上代码:
1List<Integer> numbers = Arrays.asList(1,2,3,4,5,6);
2 
3public intsumAll(List<Integer> numbers) {
4    inttotal =0;
5    for(intnumber : numbers) {
6        total += number;
7    }
8    returntotal;
9}
     sumAll算法很简单,完成的是将List中所有元素相加。某一天如果我们需要增加一个对List中所有偶数求和的方法sumAllEven,如下:
1public intsumAllEven(List<Integer> numbers) {
2    inttotal =0;
3    for(intnumber : numbers) {
4        if(number %2 == 0) {
5            total += number;
6        }
7    }
8    returntotal;
9}
     又有一天,我们需要增加第三个方法:对List中所有大于3的元素求和,那是不是继续加下面的方法呢?
1public intsumAllEven(List<Integer> numbers) {
2    inttotal =0;
3    for(intnumber : numbers) {
4        if(number >3) {
5            total += number;
6        }
7    }
8    returntotal;
9}

     比较这三个方法,我们发现了一个很明显的“代码臭味”—— 代码重复(详情参考《重构》),三个方法的唯一区别在于if判断这一行代码。如果脱离这里的上下文,我们会怎么做呢?我首先会先想到利用策略模式重构代码如下:

01public interfaceStrategy {
02   publicbooleantest(intnum);
03}
04 
05public classSumAllStrategyimplementsStrategy {
06   publicbooleantest(intnum) {
07      returntrue;
08   }
09}
10 
11public classSumAllEvenStrategyimplementsStrategy {
12   publicbooleantest(intnum) {
13      returnnum %2 == 0;
14   }
15}
16 
17public classContextClass {
18   privateStrategy stragegy =null;
19   privatefinalstatic Strategy DEFAULT_STRATEGY =new SumAllStrategy();
20 
21   publicContextClass() {
22      this(null);
23   }
24 
25   publicContextClass(Stragegy stragegy) {
26      if(strategy !=null) {
27         this.strategy = strategy;
28      }
29      else{
30         this.strategy = DEFAULT_STRATEGY;
31      }
32   }
33 
34   publicintsumAll(List<Integer> numbers) {
35      inttotal =0;
36      for(intnumber : numbers) {
37         if(strategy.test(number)) {
38            total += number;
39         }
40      }
41 
42      returntotal;
43   }
44}
45 
46 
47// 调用
48ContextClass context = newContextClass();
49context.sumAll(numbers);

     设计模式在这里发挥了作用,OO特性还是蛮强大的!但这是唯一的解决方案吗(当然不考虑用其他设计模式来解决,因为都是OO范畴!)?当然有,该轮到Java 8 Lambda表达式中的谓词(Predicate)发挥作用了!

01public intsumAll(List<Integer> numbers, Predicate<Integer> p) {
02    inttotal =0;
03    for(intnumber : numbers) {
04        if(p.test(number)) {
05            total += number;
06        }
07    }
08    returntotal;
09}
10 
11sumAll(numbers, n -> true);
12sumAll(numbers, n -> n % 2==0);
13sumAll(numbers, n -> n > 3);
      代码是不是比上面简洁很多了?语义应该也很明确,就不多解释了,如果实在看不懂,请参考我的另外一篇文章: http://www.cnblogs.com/feichexia/archive/2012/11/15/Java8_LambdaExpression.html 从这里也可以看出未引入Lambda表达式之前的Java代码的冗长(Java这点被很多人诟病)。

     当然C#早已经支持这种用法,用C#改写上面的代码如下:

1public intSumAll(IEnumerable<int> numbers, Predicate<int> predicate) {    
2   returnnumbers.Where(i => predicate(i)).Sum();
3}
4 
5SumAll(numbers, n => true);
6SumAll(numbers, n => n % 2 == 0);
7SumAll(numbers, n => n > 3);


3、Consumer与Loan Pattern

     比如我们有一个资源类Resource:

01public classResource {
02 
03    publicResource() {
04        System.out.println("Opening resource");
05    }
06 
07    publicvoidoperate() {
08        System.out.println("Operating on resource");
09    }
10 
11    publicvoiddispose() {
12        System.out.println("Disposing resource");
13    }
14}
      我们必须这样调用:
1Resource resource = newResource();
2try {
3    resource.operate();
4} finally{
5    resource.dispose();
6}

     因为对资源对象resource执行operate方法时可能抛出RuntimeException,所以需要在finally语句块中释放资源,防止可能的内存泄漏。

     但是有一个问题,如果很多地方都要用到这个资源,那么就存在很多段类似这样的代码,这很明显违反了DRY(Don't Repeat It Yourself)原则。而且如果某位程序员由于某些原因忘了用try/finally处理资源,那么很可能导致内存泄漏。那咋办呢?Java 8提供了一个Consumer接口,代码改写为如下:

01public classResource {
02 
03    privateResource() {
04        System.out.println("Opening resource");
05    }
06 
07    publicvoidoperate() {
08        System.out.println("Operating on resource");
09    }
10 
11    publicvoiddispose() {
12        System.out.println("Disposing resource");
13    }
14 
15    publicstaticvoid withResource(Consumer<Resource> consumer) {
16        Resource resource =newResource();
17        try{
18            consumer.accept(resource);
19        } finally {
20            resource.dispose();
21        }
22    }
23}
      调用代码如下:
1Resource.withResource(resource -> resource.operate());
      外部要访问Resource不能通过它的构造函数了(private),只能通过withResource方法了,这样代码清爽多了,而且也完全杜绝了因人为疏忽而导致的潜在内存泄漏。


4、stream+laziness => efficiency

     像之前一样先来一段非常简单的代码:

01List<Integer> numbers = Arrays.asList(1,2,3,4,5,6);
02 
03for (intnumber : numbers) {
04    if(number %2 == 0) {
05        intn2 = number *2;
06        if(n2 >5) {
07            System.out.println(n2);
08            break;
09        }
10    }
11}
      这段代码有什么问题? 没错,可读性非常差。第一步,我们利用《重构》一书中的最基础的提取小函数重构手法来重构代码如下:
01public booleanisEven(intnumber) {
02    returnnumber %2 == 0;
03}
04 
05public intdoubleIt(intnumber) {
06    returnnumber *2;
07}
08 
09public booleanisGreaterThan5(intnumber) {
10    returnnumber >5;
11}
12 
13for (intnumber : numbers) {
14    if(isEven(number)) {
15        intn2 = doubleIt(number);
16        if(isGreaterThan5(n2)) {
17            System.out.println(n2);
18            break;
19        }
20    }
21}
      OK,代码的意图清晰多了,但是可读性仍然欠佳,因为循环内嵌套一个if分支,if分支内又嵌套另外一个分支,于是继续重构代码如下:
01public booleanisEven(intnumber) {
02    returnnumber %2 == 0;
03}
04 
05public intdoubleIt(intnumber) {
06    returnnumber *2;
07}
08 
09public booleanisGreaterThan5(intnumber) {
10    returnnumber >5;
11}
12 
13List<Integer> l1 = newArrayList<Integer>();
14for (intn : numbers) {
15    if(isEven(n)) l1.add(n);
16}
17 
18List<Integer> l2 = newArrayList<Integer>();
19for (intn : l1) {
20    l2.add(doubleIt(n));
21}
22 
23List<Integer> l3 = newArrayList<Integer>();
24for (intn : l2) {
25    if(isGreaterThan5(n)) l3.add(n);
26}
27 
28System.out.println(l3.get(0));
      现在代码够清晰了,这是典型的“流水线”风格代码。但是等等,现在的代码执行会占用更多空间(三个List)和时间,我们来分析下。首先第二版代码的执行流程是这样的:
1isEven: 1
2isEven: 2
3doubleIt: 2
4isGreaterThan5: 2
5isEven: 3
6isEven: 4
7doubleIt: 4
8isGreaterThan5: 4
98

     而我们的第三版代码的执行流程是这样的:

01isEven: 1
02isEven: 2
03isEven: 3
04isEven: 4
05isEven: 5
06isEven: 6
07doubleIt: 2
08doubleIt: 4
09doubleIt: 6
10isGreaterThan5: 2
11isGreaterThan5: 4
12isGreaterThan5: 6
138

     步骤数是13:9,所以有时候重构得到可读性强的代码的同时可能会牺牲一些运行效率(但是一切都得实际衡量之后才能确定)。那么有没有“三全其美”的实现方法呢?即:

  1. 代码可读性强
  2. 代码执行效率不比第一版代码差
  3. 空间消耗小

     Streams come to rescue! Java 8提供了stream方法,我们可以通过对任何集合对象调用stream()方法获得Stream对象,Stream对象有别于Collections的几点如下:

  1. 不存储值:Streams不会存储值,它们从某个数据结构的流水线型操作中获取值(“酒肉穿肠过”
  2. 天生的函数编程特性:对Stream对象操作能得到一个结果,但是不会修改原始数据结构
  3. Laziness-seeking(延迟搜索):Stream的很多操作如filter、map、sort和duplicate removal(去重)可以延迟实现,意思是我们只要检查到满足要求的元素就可以返回
  4. 可选边界:Streams允许Client取足够多的元素直到满足某个条件为止。而Collections不能这么做

     上代码:

1System.out.println(
2    numbers.stream()
3            .filter(Lazy::isEven)
4            .map(Lazy::doubleIt)
5            .filter(Lazy::isGreaterThan5)
6            .findFirst()
7);
      现在的执行流程是:
1isEven: 1
2isEven: 2
3doubleIt: 2
4isGreaterThan5: 4
5isEven: 3
6isEven: 4
7doubleIt: 4
8isGreaterThan5: 8
9IntOptional[8]
      流程基本和第二版代码一致,这归功于Laziness-seeking特性。怎么理解呢?让我来构造下面这个场景:
1Stream流对象要经过下面这种流水线式处理:
2过滤出偶数 => 乘以2 => 过滤出大于5的数 => 取出第一个数
3 
4注意:=> 左边的输出是右边的输入
      而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

原创粉丝点击