在java8 里面Lambda是最火的主题,不仅仅是因为语法的改变,更重要的是带来了函数式编程的思想,我觉得优秀的程序员,有必要学习一下函数式编程的思想以开阔思路。所以这篇文章聊聊Lambda的应用场景,性能,也会提及下不好的一面。
Java为何需要Lambda
1996年1月,Java 1.0发布了,此后计算机编程领域发生了翻天覆地的变化。商业发展需要更复杂的应用,大多数程序都跑在更强大的装备多核CPU的机器上。带有高效运行期编译器的Java虚拟机(JVM)的出现,使得程序员将精力更多放在编写干净、易于维护的代码上,而不是思考如何将每一个CPU时钟、每一字节内存物尽其用。
多核CPU的出现成了“房间里的大象”,无法忽视却没人愿意正视。算法中引入锁不但容易出错,而且消耗时间。人们开发了java.util.concurrent包和很多第三方类库,试图将并发抽象化,用以帮助程序员写出在多核CPU上运行良好的程序。不幸的是,到目前为止,我们走得还不够远。
那些类库的开发者使用Java时,发现抽象的级别还不够。处理大数据就是个很好的例子,面对大数据,Java还欠缺高效的并行操作。Java 8允许开发者编写复杂的集合处理算法,只需要简单修改一个方法,就能让代码在多核CPU上高效运行。为了编写并行处理这些大数据的类库,需要在语言层面上修改现有的Java:增加lambda表达式。
当然,这样做是有代价的,程序员必须学习如何编写和阅读包含lambda表达式的代码,但是,这不是一桩赔本的买卖。与手写一大段复杂的、线程安全的代码相比,学习一点新语法和一些新习惯容易很多。开发企业级应用时,好的类库和框架极大地降低了开发时间和成本,也扫清了开发易用且高效的类库的障碍。
Lambda的应用场景
下面我将重点放在函数式编程的实用性上,包括那些可以被大多数程序员理解和使用的技术,我们关心的如何写出好代码,而不是符合函数编程风格的代码。
1.1.1 1.使用() -> {} 替代匿名类
现在Runnable线程,Swing,JavaFX的事件监听器代码等,在java 8中你可以使用Lambda表达式替代丑陋的匿名类。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
|
//Before Java 8: new Thread( new Runnable() { @Override public void run() { } }).start(); //Java 8 way: new Thread(() -> System.out.println( "In Java8!" )); // Before Java 8: JButton show = new JButton( "Show" ); show.addActionListener( new ActionListener() { @Override public void actionPerformed(ActionEvent e) { System.out.println( "without lambda expression is boring" ); } }); // Java 8 way: show.addActionListener((e) -> { System.out.println( "Action !! Lambda expressions Rocks" ); }); |
使用内循环替代外循环
外循环:描述怎么干,代码里嵌套2个以上的for循环的都比较难读懂;只能顺序处理List中的元素;
内循环:描述要干什么,而不是怎么干;不一定需要顺序处理List中的元素
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
|
//Prior Java 8 : List features = Arrays.asList( "Lambdas" , "Default Method" , "Stream API" , "Date and Time API" ); for (String feature : features) { System.out.println(feature); } //In Java 8: List features = Arrays.asList( "Lambdas" , "Default Method" , "Stream API" , "Date and Time API" ); features.forEach(n -> System.out.println(n)); // Even better use Method reference feature of Java 8 // method reference is denoted by :: (double colon) operator // looks similar to score resolution operator of C++ features.forEach(System.out::println); Output: Lambdas Default Method Stream API Date and Time API |
支持函数编程
为了支持函数编程,Java 8加入了一个新的包java.util.function,其中有一个接口java.util.function.Predicate是支持Lambda函数编程:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
|
public static void main(args[]){ List languages = Arrays.asList( "Java" , "Scala" , "C++" , "Haskell" , "Lisp" ); System.out.println( "Languages which starts with J :" ); filter(languages, (str)->str.startsWith( "J" )); System.out.println( "Languages which ends with a " ); filter(languages, (str)->str.endsWith( "a" )); System.out.println( "Print all languages :" ); filter(languages, (str)-> true ); System.out.println( "Print no language : " ); filter(languages, (str)-> false ); System.out.println( "Print language whose length greater than 4:" ); filter(languages, (str)->str.length() > 4 ); } public static void filter(List names, Predicate condition) { names.stream().filter((name) -> (condition.test(name))) .forEach((name) -> {System.out.println(name + " " ); }); } |
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
|
Output: Languages which starts with J : Java Languages which ends with a Java Scala Print all languages : Java Scala C++ Haskell Lisp Print no language : Print language whose length greater than 4 : Scala Haskell |
处理数据?用管道的方式更加简洁
Java 8里面新增的Stream API ,让集合中的数据处理起来更加方便,性能更高,可读性更好
假设一个业务场景:对于20元以上的商品,进行9折处理,最后得到这些商品的折后价格。
1
2
3
4
5
|
final BigDecimal totalOfDiscountedPrices = prices.stream() .filter(price -> price.compareTo(BigDecimal.valueOf( 20 )) > 0 ) .map(price -> price.multiply(BigDecimal.valueOf( 0.9 ))) .reduce(BigDecimal.ZERO,BigDecimal::add); System.out.println( "Total of discounted prices: " + totalOfDiscountedPrices); |
想象一下:如果用面向对象处理这些数据,需要多少行?多少次循环?需要声明多少个中间变量?
Lambda的阴暗面
前面都是讲Lambda如何改变Java程序员的思维习惯,但Lambda确实也带来了困惑
JVM可以执行任何语言编写的代码,只要它们能编译成字节码,字节码自身是充分OO的,被设计成接近于Java语言,这意味着Java被编译成的字节码非常容易被重新组装。
但是如果不是Java语言,差距将越来越大,Scala源码和被编译成的字节码之间巨大差距是一个证明,编译器加入了大量合成类 方法和变量,以便让JVM按照语言自身特定语法和流程控制执行。
我们首先看看Java 6/7中的一个传统方法案例:
1
2
3
4
5
6
7
8
9
10
11
12
|
// simple check against empty strings public static int check(String s) { if (s.equals( "" )) { throw new IllegalArgumentException(); } return s.length(); } //map names to lengths List lengths = new ArrayList(); for (String name : Arrays.asList(args)) { lengths.add(check(name)); } |
如果一个空的字符串传入,这段代码将抛出错误,堆栈跟踪如下:
1
2
|
at LmbdaMain.check(LmbdaMain.java: 19 ) at LmbdaMain.main(LmbdaMain.java: 34 ) |
再看看Lambda的例子
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
Stream lengths = names.stream().map(name -> check(name)); at LmbdaMain.check(LmbdaMain.java: 19 ) at LmbdaMain.lambda$ 0 (LmbdaMain.java: 37 ) at LmbdaMain$$Lambda$ 1 / 821270929 .apply(Unknown Source) at java.util.stream.ReferencePipeline$ 3 $ 1 .accept(ReferencePipeline.java: 193 ) at java.util.Spliterators$ArraySpliterator.forEachRemaining(Spliterators.java: 948 ) at java.util.stream.AbstractPipeline.copyInto(AbstractPipeline.java: 512 ) at java.util.stream.AbstractPipeline.wrapAndCopyInto(AbstractPipeline.java: 502 ) at java.util.stream.ReduceOps$ReduceOp.evaluateSequential(ReduceOps.java: 708 ) at java.util.stream.AbstractPipeline.evaluate(AbstractPipeline.java: 234 ) at java.util.stream.LongPipeline.reduce(LongPipeline.java: 438 ) at java.util.stream.LongPipeline.sum(LongPipeline.java: 396 ) at java.util.stream.ReferencePipeline.count(ReferencePipeline.java: 526 ) at LmbdaMain.main(LmbdaMain.java: 39 ) |
这非常类似Scala,出错栈信息太长,我们为代码的精简付出力代价,更精确的代码意味着更复杂的调试。
但这并不影响我们喜欢Lambda!
总结
在Java世界里面,面向对象还是主流思想,对于习惯了面向对象编程的开发者来说,抽象的概念并不陌生。面向对象编程是对数据进行抽象,而函数式编程是对行为进行抽象。现实世界中,数据和行为并存,程序也是如此,因此这两种编程方式我们都得学。
这种新的抽象方式还有其他好处。很多人不总是在编写性能优先的代码,对于这些人来说,函数式编程带来的好处尤为明显。程序员能编写出更容易阅读的代码——这种代码更多地表达了业务逻辑,而不是从机制上如何实现。易读的代码也易于维护、更可靠、更不容易出错。
在写回调函数和事件处理器时,程序员不必再纠缠于匿名内部类的冗繁和可读性,函数式编程让事件处理系统变得更加简单。能将函数方便地传递也让编写惰性代码变得容易,只有在真正需要的时候,才初始化变量的值。
总而言之,Java更趋于完美了。