Java 8为什么需要Lambda表达式

来源:互联网 发布:剑灵人族体型数据 编辑:程序博客网 时间:2024/05/19 09:50

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

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

影响力较大的三个趋势

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

1、内部循环和外部循环

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

  1. List<Integer> numbers = Arrays.asList(123456);  
  2.  
  3. for (int number : numbers) {  
  4.     System.out.println(number);  

是不是很常见呢?这个叫外部循环(External Iteration)。但是外部循环有什么问题呢?简单来说存在下面三个缺点:

1.只能顺序处理List中的元素(process one by one)

2.不能充分利用多核CPU

3.不利于编译器优化

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

  1. List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5, 6);  
  2.  
  3. numbers.forEach((Integer value) -> System.out.println(value)); 

这样就能规避上面的三个问题:

1.不一定需要顺序处理List中的元素,顺序可以不确定

2.可以并行处理,充分利用多核CPU的优势

3.有利于JIT编译器对代码进行优化

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

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

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

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

  1. public class ArticleDac {  
  2.    ...  
  3.    public Article GetArticles(Func<IDbSet<Article>, Article> func)   // 这里传递的就是行为  
  4.    {  
  5.       using(var db = xx) {  
  6.          return func(db.Articles);  
  7.       }    
  8.    }  
  9.    ...  
  10. }  
  11. // 下面是调用  
  12. int articleId = 119;  
  13. var firstArticle = new ArticleDac().GetArticles(  
  14.     articleDbSet =>  
  15.     articleDbSet.AsQueryable().FirstOrDefault(x => x.id == articleId)  
  16. ); 

看不懂?没关系。我们先来看一个体现传值局限性的场景吧,上代码:

  1. List<Integer> numbers = Arrays.asList(123456);  
  2.  
  3. public int sumAll(List<Integer> numbers) {  
  4.     int total = 0;  
  5.     for (int number : numbers) {  
  6.         total += number;  
  7.     }  
  8.     return total;  

sumAll算法很简单,完成的是将List中所有元素相加。某一天如果我们需要增加一个对List中所有偶数求和的方法sumAllEven,如下:

  1. public int sumAllEven(List<Integer> numbers) {  
  2.     int total = 0;  
  3.     for (int number : numbers) {  
  4.         if (number % 2 == 0) {  
  5.             total += number;  
  6.         }  
  7.     }  
  8.     return total;  

又有一天,我们需要增加第三个方法:对List中所有大于3的元素求和,那是不是继续加下面的方法呢? 

  1. public int sumAllEven(List<Integer> numbers) {  
  2.     int total = 0;  
  3.     for (int number : numbers) {  
  4.         if (number > 3) {  
  5.             total += number;  
  6.         }  
  7.     }  
  8.     return total;  

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

  1. public interface Strategy {  
  2.    public boolean test(int num);  
  3. }  
  4.  
  5. public class SumAllStrategy implements Strategy {  
  6.    public boolean test(int num) {  
  7.       return true;  
  8.    }  
  9. }  
  10.  
  11. public class SumAllEvenStrategy implements Strategy {  
  12.    public boolean test(int num) {  
  13.       return num % 2 == 0;  
  14.    }  
  15. }  
  16.  
  17. public class ContextClass {  
  18.    private Strategy stragegy = null;  
  19.    private final static Strategy DEFAULT_STRATEGY = new SumAllStrategy();  
  20.  
  21.    public ContextClass() {  
  22.       this(null);  
  23.    }  
  24.  
  25.    public ContextClass(Stragegy stragegy) {  
  26.       if(strategy != null) {  
  27.          this.strategy = strategy;   
  28.       }  
  29.       else {  
  30.          this.strategy = DEFAULT_STRATEGY;  
  31.       }  
  32.    }  
  33.  
  34.    public int sumAll(List<Integer> numbers) {  
  35.       int total = 0;  
  36.       for (int number : numbers) {  
  37.          if (strategy.test(number)) {  
  38.             total += number;  
  39.          }  
  40.       }  
  41.  
  42.       return total;  
  43.    }  
  44. }  
  45.  
  46.  
  47. // 调用  
  48. ContextClass context = new ContextClass();  
  49. context.sumAll(numbers); 

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

  1. public int sumAll(List<Integer> numbers, Predicate<Integer> p) {  
  2.     int total = 0;  
  3.     for (int number : numbers) {  
  4.         if (p.test(number)) {  
  5.             total += number;  
  6.         }  
  7.     }  
  8.     return total;  
  9. }  
  10.  
  11. sumAll(numbers, n -> true);  
  12. sumAll(numbers, n -> n % 2 == 0);  
  13. sumAll(numbers, n -> n > 3); 

代码是不是比上面简洁很多了?语义应该也很明确,就不多解释了,如果实在看不懂,请参考我的另外一篇文章:http://www.cnblogs.com/feichexia/archive/2012/11/15/Java8_LambdaExpression.html 从这里也可以看出未引入Lambda表达式之前的Java代码的冗长(Java这点被很多人诟病)。

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

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

3、Consumer与Loan Pattern

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

  1. public class Resource {  
  2.  
  3.     public Resource() {  
  4.         System.out.println("Opening resource");  
  5.     }  
  6.  
  7.     public void operate() {  
  8.         System.out.println("Operating on resource");  
  9.     }  
  10.  
  11.     public void dispose() {  
  12.         System.out.println("Disposing resource");  
  13.     }  

我们必须这样调用:

  1. Resource resource = new Resource();  
  2. try {  
  3.     resource.operate();  
  4. finally {  
  5.     resource.dispose();  

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

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

  1. public class Resource {  
  2.  
  3.     private Resource() {  
  4.         System.out.println("Opening resource");  
  5.     }  
  6.  
  7.     public void operate() {  
  8.         System.out.println("Operating on resource");  
  9.     }  
  10.  
  11.     public void dispose() {  
  12.         System.out.println("Disposing resource");  
  13.     }  
  14.  
  15.     public static void withResource(Consumer<Resource> consumer) {  
  16.         Resource resource = new Resource();  
  17.         try {  
  18.             consumer.accept(resource);  
  19.         } finally {  
  20.             resource.dispose();  
  21.         }  
  22.     }  

调用代码如下:

  1. Resource.withResource(resource -> resource.operate()); 

外部要访问Resource不能通过它的构造函数了(private),只能通过withResource方法了,这样代码清爽多了,而且也完全杜绝了因人为疏忽而导致的潜在内存泄漏。

4、stream+laziness => efficiency

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

  1. List<Integer> numbers = Arrays.asList(123456);  
  2.  
  3. for (int number : numbers) {  
  4.     if (number % 2 == 0) {  
  5.         int n2 = number * 2;  
  6.         if (n2 > 5) {  
  7.             System.out.println(n2);  
  8.             break;  
  9.         }  
  10.     }  

这段代码有什么问题? 没错,可读性非常差。第一步,我们利用《重构》一书中的最基础的提取小函数重构手法来重构代码如下:

  1. public boolean isEven(int number) {  
  2.     return number % 2 == 0;  
  3. }  
  4.  
  5. public int doubleIt(int number) {  
  6.     return number * 2;  
  7. }  
  8.  
  9. public boolean isGreaterThan5(int number) {  
  10.     return number > 5;  
  11. }  
  12.  
  13. for (int number : numbers) {  
  14.     if (isEven(number)) {  
  15.         int n2 = doubleIt(number);  
  16.         if (isGreaterThan5(n2)) {  
  17.             System.out.println(n2);  
  18.             break;  
  19.         }  
  20.     }  

OK,代码的意图清晰多了,但是可读性仍然欠佳,因为循环内嵌套一个if分支,if分支内又嵌套另外一个分支,于是继续重构代码如下:

  1. public boolean isEven(int number) {  
  2.     return number % 2 == 0;  
  3. }  
  4.  
  5. public int doubleIt(int number) {  
  6.     return number * 2;  
  7. }  
  8.  
  9. public boolean isGreaterThan5(int number) {  
  10.     return number > 5;  
  11. }  
  12.  
  13. List<Integer> l1 = new ArrayList<Integer>();  
  14. for (int n : numbers) {  
  15.     if (isEven(n)) l1.add(n);  
  16. }  
  17.  
  18. List<Integer> l2 = new ArrayList<Integer>();  
  19. for (int n : l1) {  
  20.     l2.add(doubleIt(n));  
  21. }  
  22.  
  23. List<Integer> l3 = new ArrayList<Integer>();  
  24. for (int n : l2) {  
  25.     if (isGreaterThan5(n)) l3.add(n);  
  26. }  
  27.  
  28. System.out.println(l3.get(0)); 

现在代码够清晰了,这是典型的“流水线”风格代码。但是等等,现在的代码执行会占用更多空间(三个List)和时间,我们来分析下。首先第二版代码的执行流程是这样的:

  1. isEven: 1  
  2. isEven: 2  
  3. doubleIt: 2  
  4. isGreaterThan5: 2  
  5. isEven: 3  
  6. isEven: 4  
  7. doubleIt: 4  
  8. isGreaterThan5: 4  

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

  1. isEven: 1 
  2. isEven: 2 
  3. isEven: 3 
  4. isEven: 4 
  5. isEven: 5 
  6. isEven: 6 
  7. doubleIt: 2 
  8. doubleIt: 4 
  9. doubleIt: 6 
  10. isGreaterThan5: 2 
  11. isGreaterThan5: 4 
  12. isGreaterThan5: 6 
  13. 8 

步骤数是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不能这么做

上代码:

  1. System.out.println(  
  2.     numbers.stream()  
  3.             .filter(Lazy::isEven)  
  4.             .map(Lazy::doubleIt)  
  5.             .filter(Lazy::isGreaterThan5)  
  6.             .findFirst()  
  7. ); 

现在的执行流程是:

  1. isEven: 1 
  2. isEven: 2 
  3. doubleIt: 2 
  4. isGreaterThan5: 4 
  5. isEven: 3 
  6. isEven: 4 
  7. doubleIt: 4 
  8. isGreaterThan5: 8 
  9. IntOptional[8

流程基本和第二版代码一致,这归功于Laziness-seeking特性。怎么理解呢?让我来构造下面这个场景:

  1. Stream流对象要经过下面这种流水线式处理:  
  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检测。


0 0
原创粉丝点击
热门问题 老师的惩罚 人脸识别 我在镇武司摸鱼那些年 重生之率土为王 我在大康的咸鱼生活 盘龙之生命进化 天生仙种 凡人之先天五行 春回大明朝 姑娘不必设防,我是瞎子 孕吐伤了胃疼怎么办 孕期吐的胃疼怎么办 买的巧克力化了怎么办 跑步后脸上出盐怎么办 头发被剪的很短怎么办 孩子做事情拖拉不专注怎么办 新热水壶有味道怎么办 新买电热壶有味怎么办 两个月狗耳朵臭怎么办 狗狗牙齿变黄怎么办 人用了狗沐浴露怎么办 狗狗吞食牙膏吐怎么办? 狗狗吞食了牙膏怎么办 大猪拉稀不吃食怎么办 猪不发烧不吃食怎么办 天天吃自热米饭怎么办 喝了加热包水怎么办啊 蛋挞没有盒子装怎么办 塑料饭盒盖子被吸住了怎么办 火腿淹的有臭味怎么办 微波炉热饭盖子打不开怎么办 夏天带饭容易馊怎么办? 保温饭盒里有气打不开怎么办 保温饭盒摔了一下打不开怎么办 饭盒跟盖子盖一起打不开怎么办 玻璃杯子盖被水吸住打不开怎么办 电饭煲热剩饭没加水怎么办 微波炉碗盖子吸住了怎么办 微波炉转饭盖子吸住了怎么办 玻璃碗放进微波炉打不开怎么办 乐扣微波炉加热后打不开怎么办 美的微波炉盖子打不开怎么办 美的微波炉门都打不开了怎么办 饭煮好了有异味怎么办 一正常吃饭就胖怎么办 高铁盒饭没15的怎么办 上火车前票丢了怎么办 减肥期吃了汉堡怎么办 寿司店鳗鱼有刺怎么办 吃泡面胃难受该怎么办 吃上火的东西脸上长痘痘怎么办