[转帖]Java 8为什么需要Lambda表达式_Android, Python及开发编程讨论区_Weblogic技术|Tuxedo技术|中间件技术|Oracle论坛|JAVA论坛|Linux/Unix技术|hadoop论坛_联动北方技术论坛  
网站首页 | 关于我们 | 服务中心 | 经验交流 | 公司荣誉 | 成功案例 | 合作伙伴 | 联系我们 |
联动北方-国内领先的云技术服务提供商
»  游客             当前位置:  论坛首页 »  自由讨论区 »  Android, Python及开发编程讨论区 »
总帖数
2
每页帖数
101/1页1
返回列表
0
发起投票  发起投票 发新帖子
查看: 2904 | 回复: 1   主题: [转帖]Java 8为什么需要Lambda表达式        上一篇   下一篇 
    本主题由 huang.wang 于 2018-9-10 10:42:00 移动
huang.wang
注册用户
等级:中将
经验:17623
发帖:407
精华:1
注册:1970-1-1
状态:离线
发送短消息息给huang.wang 加好友    发送短消息息给huang.wang 发消息
发表于: IP:您无权察看 2018-9-10 10:22:57 | [全部帖] [楼主帖] 楼主


本文转自 CSDN博客


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

image.png

影响力较大的三个趋势

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

1、内部循环和外部循环

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


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


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

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

2.不能充分利用多核CPU

3.不利于编译器优化

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


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


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

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

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

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

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


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


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

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


public class ArticleDac {  
   ...  
   public Article GetArticles(Func<IDbSet<Article>, Article> func)   // 这里传递的就是行为  
   {  
      using(var db = xx) {  
         return func(db.Articles);  
      }    
   }  
   ...  
}  
// 下面是调用  
int articleId = 119;  
var firstArticle = new ArticleDac().GetArticles(  
    articleDbSet =>  
    articleDbSet.AsQueryable().FirstOrDefault(x => x.id == articleId)  
);


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


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


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


public int sumAllEven(List<Integer> numbers) {  
    int total = 0;  
    for (int number : numbers) {  
        if (number % 2 == 0) {  
            total += number;  
        }  
    }  
    return total;  
}


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


public int sumAllEven(List<Integer> numbers) {  
    int total = 0;  
    for (int number : numbers) {  
        if (number > 3) {  
            total += number;  
        }  
    }  
    return total;  
}


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


public interface Strategy {  
   public boolean test(int num);  
}  
 
public class SumAllStrategy implements Strategy {  
   public boolean test(int num) {  
      return true;  
   }  
}  
 
public class SumAllEvenStrategy implements Strategy {  
   public boolean test(int num) {  
      return num % 2 == 0;  
   }  
}  
 
public class ContextClass {  
   private Strategy stragegy = null;  
   private final static Strategy DEFAULT_STRATEGY = new SumAllStrategy();  
 
   public ContextClass() {  
      this(null);  
   }  
 
   public ContextClass(Stragegy stragegy) {  
      if(strategy != null) {  
         this.strategy = strategy;   
      }  
      else {  
         this.strategy = DEFAULT_STRATEGY;  
      }  
   }  
 
   public int sumAll(List<Integer> numbers) {  
      int total = 0;  
      for (int number : numbers) {  
         if (strategy.test(number)) {  
            total += number;  
         }  
      }  
 
      return total;  
   }  
}  
 
 
// 调用  
ContextClass context = new ContextClass();  
context.sumAll(numbers);


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


public int sumAll(List<Integer> numbers, Predicate<Integer> p) {  
    int total = 0;  
    for (int number : numbers) {  
        if (p.test(number)) {  
            total += number;  
        }  
    }  
    return total;  
}  
 
sumAll(numbers, n -> true);  
sumAll(numbers, n -> n % 2 == 0);  
sumAll(numbers, n -> n > 3);


代码是不是比上面简洁很多了?语义应该也很明确,就不多解释了。从这里也可以看出未引入Lambda表达式之前的Java代码的冗长(Java这点被很多人诟病)。

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


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


3、Consumer与Loan Pattern

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


public class Resource {  
 
    public Resource() {  
        System.out.println("Opening resource");  
    }  
 
    public void operate() {  
        System.out.println("Operating on resource");  
    }  
 
    public void dispose() {  
        System.out.println("Disposing resource");  
    }  
}


我们必须这样调用:


Resource resource = new Resource();  
try {  
    resource.operate();  
} finally {  
    resource.dispose();  
}


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

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


public class Resource {  
 
    private Resource() {  
        System.out.println("Opening resource");  
    }  
 
    public void operate() {  
        System.out.println("Operating on resource");  
    }  
 
    public void dispose() {  
        System.out.println("Disposing resource");  
    }  
 
    public static void withResource(Consumer<Resource> consumer) {  
        Resource resource = new Resource();  
        try {  
            consumer.accept(resource);  
        } finally {  
            resource.dispose();  
        }  
    }  
}


调用代码如下:


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


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


4、stream+laziness => efficiency

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


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


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


public boolean isEven(int number) {  
    return number % 2 == 0;  
}  
 
public int doubleIt(int number) {  
    return number * 2;  
}  
 
public boolean isGreaterThan5(int number) {  
    return number > 5;  
}  
 
for (int number : numbers) {  
    if (isEven(number)) {  
        int n2 = doubleIt(number);  
        if (isGreaterThan5(n2)) {  
            System.out.println(n2);  
            break;  
        }  
    }  
}


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


public boolean isEven(int number) {  
    return number % 2 == 0;  
}  
 
public int doubleIt(int number) {  
    return number * 2;  
}  
 
public boolean isGreaterThan5(int number) {  
    return number > 5;  
}  
 
List<Integer> l1 = new ArrayList<Integer>();  
for (int n : numbers) {  
    if (isEven(n)) l1.add(n);  
}  
 
List<Integer> l2 = new ArrayList<Integer>();  
for (int n : l1) {  
    l2.add(doubleIt(n));  
}  
 
List<Integer> l3 = new ArrayList<Integer>();  
for (int n : l2) {  
    if (isGreaterThan5(n)) l3.add(n);  
}  
 
System.out.println(l3.get(0));


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

isEven: 1  

isEven: 2  

doubleIt: 2  

isGreaterThan5: 2  

isEven: 3  

isEven: 4  

doubleIt: 4  

isGreaterThan5: 4  

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

isEven: 1 

isEven: 2 

isEven: 3 

isEven: 4 

isEven: 5 

isEven: 6 

doubleIt: 2 

doubleIt: 4 

doubleIt: 6 

isGreaterThan5: 2 

isGreaterThan5: 4 

isGreaterThan5: 6 

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

上代码:


System.out.println(  
    numbers.stream()  
            .filter(Lazy::isEven)  
            .map(Lazy::doubleIt)  
            .filter(Lazy::isGreaterThan5)  
            .findFirst()  
);


现在的执行流程是:

isEven: 1 

isEven: 2 

doubleIt: 2 

isGreaterThan5: 4 

isEven: 3 

isEven: 4 

doubleIt: 4 

isGreaterThan5: 8 

IntOptional[8] 

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

Stream流对象要经过下面这种流水线式处理:  

过滤出偶数 => 乘以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检测。

该贴由huang.wang转至本版2018-9-10 10:42:00


我超级酷,但是如果你回复我的话我可以不酷那么一小会儿。


——来自logo.png


赞(0)    操作        顶端 
koei123
注册用户
等级:大校
经验:4196
发帖:16
精华:0
注册:2011-7-21
状态:离线
发送短消息息给koei123 加好友    发送短消息息给koei123 发消息
发表于: IP:您无权察看 2018-11-5 10:14:29 | [全部帖] [楼主帖] 2  楼


不觉得是个MUST,当时引入只是为了随大流而已~~



赞(0)    操作        顶端 
总帖数
2
每页帖数
101/1页1
返回列表
发新帖子
请输入验证码: 点击刷新验证码
您需要登录后才可以回帖 登录 | 注册
技术讨论