Java 8为什么需要Lambda表达式

http://developer.51cto.com/art/201304/387681.htm

函数编程在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检测。

参考自:

http://java.dzone.com/articles/why-we-need-lambda-expressions

http://java.dzone.com/articles/why-we-need-lambda-expressions-0

原文链接:http://my.oschina.net/feichexia/blog/119805

【编辑推荐】

  1. Java8和Scala中的Lambda表达式
  2. Java8 和 Scala 中的高阶函数
  3. 从根本上改变我们开发Java程序的方式:Lambda
  4. 探索Java语言与JVM中的Lambda表达式
  5. 和Lambdas的第一次亲密接触

 

----------------------------------------------

Java8是Java最新的版本,它带给我们以"Project Lambda"为核心的一些新特性。在这篇文章中,就让我们一起领略一下Java8的魅力吧。

接口默认方法

Java8对接口做了一些改善,它支持在接口中定义默认方法。在过去,java类库的接口中添加方法基本上是不可能的,在接口中添加方法意味着破坏了实现了这个接口的代码。但是现在,我们只要能够提供一个正确默认方法的实现, 维护者就可以在接口中添加方法。

默认方法可以使用default关键字来定义。定义一个默认方法后,实现接口的所有类都可以直接使用这一方法。

 

public interface MathService {
    default Double abs(Double x) {
        return x > 0 ? x : -x;
    }

    public Double sqrt();
}

在MathService实现默认的方法abs。再在MathServiceImpl类中实现MathService接口。

 

public class MathServiceImpl implements MathService {

    @Override
    public Double pow(Double a, int times) {
        if (0 == a || 1 == a) {
            return a;
        }
        Double result = a;
        if (times > 0) {
            for (int i = 1; i < times; i++) {
                result *= result;
            }
            return result;
        }
        else if (0 == times) {
            return 1d;
        }
        else {
            return 1/pow(a, -times);
        }
    }
}

实例化MathServiceImpl类后即可调用接口的默认方法。

 

public static void main(String[] args) {
    MathService mathService = new MathServiceImpl();
    mathService.abs(-5d);
}

提示

需要注意的是:接口不能提供对Object类的任何方法的默认实现,包括equals,hashCode和toString方法。这是因为所有的类都是Object类的子孙类,而且如果一个类已经实现一个方法,它会优于默认方法使用。所以在实现这个接口前,该类就有了equals、hashCode和toString方法,所以接口实现这些默认方法也就无效了。

函数式接口

Java8的另一个概念是函数式接口。如果一个接口定义了唯一一个抽象方法的话,这个接口就成为了函数式接口。抽象方法可以使用abstract关键字来定义。如:

 

public abstract void doSomething();

与此同时,Java引入了一个新的标注:@FunctionalInterface。我们可以将标注写在接口前,表明这个接口是函数式接口。

Lambda表达式

Lambda表达式是Java8的核心,以至于它被叫做Lambda Project。一个函数式接口最有价值的属性就是能够用Lambda表达式来实例化。

什么是Lambda表达式

我们可以通过几个例子来学习Lambda表达式:

比如我们需要排列一组存放在List容器中的对象,过去我们需要这样实现:

 

List<SomeObject> words = new ArrayList<>();
        words.add(new SomeObject("test5"));
        words.add(new SomeObject("some0"));
        words.add(new SomeObject("another3"));

        Collections.sort(words, new Comparator<SomeObject>() {
            @Override
            public int compare(SomeObject o1, SomeObject o2) {
                return o1.getSomething().compareTo(o2.getSomething());
            }
        });

需要使用匿名的方式覆盖compare方法来进行排序。但是引入了Lambda表达式后,只需这样:

 

Collections.sort(words, (o1, o2) -> o1.getSomething().compareTo(o2.getSomething()));

是不是简洁了很多。其中(o1, o2) -> o1.getSomething().compareTo(o2.getSomething())是Lambda表达式,左侧的括号里的内容是输入列表,右边是返回值。每一个表达式都对应了一个类型,而通常来说会是接口类型。我们可以把Lambda表达式当做任何一个函数式接口类型。

使用Lambda表达式

Lambda表达式代表一个函数式接口,那么抽象方法不同的情况下该如何使用Lambda表达式:

  1. 抽象方法有多个参数,有返回值

 

(x1, x2) -> x1 + x2;
  1. 抽象方法只有一个参数,有返回值

 

x1 -> x1 + x1;
  1. 抽象方法无输入,有返回值

 

() -> 5;
  1. 抽象方法无返回值(x1, x2) -> { System.out.print(x1 + x2); }

方法与构造方法的引用

Java8允许使用::关键字来传递方法和构造函数的引用。它可以简化一些Lambda表达式的书写:

类的静态方法:x -> String.valueOf(x) 等价于 String::valueOf类的非静态方法:x -> x.toString() 等价于 Object::toString构造函数: () -> new HashMap() 等价于 HashMap::new

提示

在这里同样支持重载,如果一个类有多个构造函数如HashMap,那么HashMap::new就能够指向它的构造方法中任何一个。编译器根据在使用的函数式接口决定使用哪个方法。

将Lambda表达式赋值给函数式接口变量

因为Lambda表达式表示了一个函数式接口,我们也就可以将之赋值给一个变量。如:

 

Comparator<SomeObject> someObjectComparator = (o1, o2) -> o1.getSomething().compareTo(o2.getSomething());

作用域

在lambda表达式中访问外层作用域和匿名对象中的方式很相似。

 

final int plus = 1;
Comparator<SomeObject> someObjectComparator = (o1, o2) -> (o1.getSomething() + plus).compareTo(o2.getSomething());

它可以直接访问外部标记为final的变量,但是事实上不声明为fianl也可以进行访问,这一点是与匿名对象不同的:

 

int plus = 1;
Comparator<SomeObject> someObjectComparator = (o1, o2) -> (o1.getSomething() + plus).compareTo(o2.getSomething());

但是如果plus变量在后面的代码中被修改就会无法编译,所以实际上在Lambda表达式中使用的外部变量还是具有fianl语义的,这一点使我们需要注意的。

https://www.tianmaying.com/tutorial/java_lambda

猜你喜欢

转载自nethub2.iteye.com/blog/2334753