简介 Java 8 的 Lambda 表达式提供了强大的函数化的编程能力,将函数作为参数传递进方法中。免去了使用匿名方法的麻烦,这样使可读性更好,表达更清晰。它是推动 Java 8 发布的最重要新特性。Lambda 表达式的简洁让人非常激动,但是如果第一次看到一段复杂的Lambda表达式的代码,会让你非常头疼,对于初学者来说,可能就是一段垃圾代码,因为你并不知道 Lambda 表达式到底在表达什么╮(╯▽╰)╭下面我们就举一些小例子由浅入深的了解下 Lambda 表达式。
基本语法 (parameters) -> expression
或者 (parameters) ->{ statements; }
个人理解,把 Lambda 表达式看成咱们上学的时候学的函数 f(x) = x + 1
会让你更容易理解。
以下是lambda表达式的重要特征:
可选类型声明: 不需要声明参数类型,编译器可以统一识别参数值。可选的参数圆括号: 一个参数无需定义圆括号,但多个参数需要定义圆括号。可选的大括号: 如果主体包含了一个语句,就不需要使用大括号。可选的返回关键字: 如果主体只有一个表达式返回值则编译器会自动返回值,大括号需要指定明表达式返回了一个数值。简单实例 1 2 3 4 public interface Operation { int calc (int a, int b) ; }
1 2 3 4 5 public interface Greeting { void say (String msg) ; }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 public class LambdaTest { public static int calc (int a, int b, Operation operation) { return operation.calc(a, b); } public static void say (String msg, Greeting greeting) { greeting.say(msg); } public static void main (String args[]) { Operation addition = (int a, int b) -> a + b; Operation subtraction = (a, b) -> a - b; Operation multiplication = (int a, int b) -> { return a * b; }; Operation division = (int a, int b) -> a / b; System.out.println("10 + 5 = " + LambdaTest.calc(10 , 5 , addition)); System.out.println("10 - 5 = " + LambdaTest.calc(10 , 5 , subtraction)); System.out.println("10 x 5 = " + LambdaTest.calc(10 , 5 , multiplication)); System.out.println("10 / 5 = " + LambdaTest.calc(10 , 5 , division)); Greeting sayHello = message -> System.out.println("Hello " + message); Greeting sayBye = (message) -> System.out.println("Bye " + message); LambdaTest.say("cylong" , sayHello); LambdaTest.say("cylong" , sayBye); LambdaTest.say("cylong" , message -> System.out.println("Hi " + message)); LambdaTest.say("cylong" , new Greeting () { @Override public void say (String msg) { System.out.println("Hello " + msg); } }); } }
以上代码的输出为:
1 2 3 4 5 6 7 8 10 + 5 = 15 10 - 5 = 5 10 x 5 = 50 10 / 5 = 2 Hello cylong Bye cylong Hello cylong Hello cylong
Java 8 的 Stream API 其实目前用 Lambda 表达式最多的地方就是 Java 8 的新特性——Stream API,借助于 Lambda 表达式,极大的提高编程效率和程序可读性。同时它提供串行和并行两种模式进行汇聚操作,并发模式能够充分利用多核处理器的优势,使用 fork/join 并行方式来拆分任务和加速处理过程。Java 8 中的 Stream 是对集合(Collection)对象功能的增强,Stream 不是集合元素,它不是数据结构并不保存数据,它是有关算法和计算的。在以前的 Java API中,我们更多的是使用for或者Iterator来遍历集合,同时我们可能会对集合里的数据进行过滤,计算等等处理,导致代码量非常的多,还容易出错。而使用 Stream,用户只要给出需要对其包含的元素执行什么操作,比如 “过滤掉长度大于 10 的字符串”、“获取每个字符串的首字母”等,Stream 会隐式地在内部进行遍历,做出相应的数据转换。下面我们就看一些例子,深入了解下 Stream 的使用。
集合迭代 1 2 3 4 5 6 7 8 9 10 11 12 13 List<Integer> numList = Arrays.asList(1 , 2 , 3 , 4 , 5 , 6 , 7 , 8 , 9 , 10 ); for (int n : numList) { System.out.println(n); } numList.forEach(n -> System.out.println(n)); numList.forEach(System.out::println);
上面的例子包含普通的for循环,Lambda表达式遍历的方式,最后一种方式是方法引用,让代码量再次减少,代码更加清晰。
集合过滤 1 2 numList.stream().filter(n -> n > 5 ).forEach(System.out::println);
上面的代码中 filter(n -> n > 5)
就是获得集合中大于5的所有值,filter 的参数是 java.util.function.Predicate
,返回值是一个 Stream。使用 Predicate 可以向API方法添加逻辑,用更少的代码支持更多的动态行为。上面的例子就是使用 Predicate 对集合进行过滤。在 filter() 方法中,我们可以写更多复杂的逻辑来过滤集合元素。甚至可以使用 and()
或者 or()
等合并多个条件,如下面这样。
1 2 3 4 Predicate<Integer> start = n -> n >5 ; Predicate<Integer> end = n -> n <8 ; numList.stream().filter(start.and(end)).forEach(System.out::println);
另外,关于 filter() 方法有个常见误解。在现实生活中,做过滤的时候,通常会丢弃部分,但使用filter()方法则是获得一个新的列表,且其每个元素符合过滤原则。
1 2 3 4 5 List<Integer> newNumList = numList.stream().filter(n -> n > 5 ).collect(Collectors.toList()); newNumList.forEach(System.out::println); System.out.println(); numList.forEach(System.out::print);
输出是:
Stream 的 map 示例 1 2 numList.stream().map(n -> n * n).forEach(System.out::println);
本例介绍最广为人知的函数式编程概念 map。它允许你将对象进行转换。例如在本例中,我们将 n -> n * n
lambda 表达式传到 map() 方法,后者将其应用到流中的每一个元素。然后用 forEach()
将列表元素打印出来。
Stream 的 Reduce 示例 1 2 3 int result = numList.stream().reduce((sum, n) -> sum + n).get();System.out.println(result);
在上个例子中,可以看到map将集合类(例如列表)元素进行转换的。还有一个 reduce() 函数可以将所有值合并成一个。Map和Reduce操作是函数式编程的核心操作,因为其功能,reduce 又被称为折叠操作。另外,reduce 并不是一个新的操作,你有可能已经在使用它。SQL中类似 sum()、avg() 或者 count() 的聚集函数,实际上就是 reduce 操作,因为它们接收多个值并返回一个值。流API定义的 reduce() 函数可以接受lambda表达式,并对所有值进行合并。IntStream这样的类有类似 average()、count()、sum() 的内建方法来做 reduce 操作,也有mapToLong()、mapToDouble() 方法来做转换。这并不会限制你,你可以用内建方法,也可以自己定义。
计算集合元素的最大值、最小值、总和以及平均值 1 2 3 4 5 6 IntSummaryStatistics stats = numList.stream().mapToInt((x) -> x).summaryStatistics();System.out.println("Highest prime number in List : " + stats.getMax()); System.out.println("Lowest prime number in List : " + stats.getMin()); System.out.println("Sum of all prime numbers : " + stats.getSum()); System.out.println("Average of all prime numbers : " + stats.getAverage());
输出:
1 2 3 4 Highest prime number in List : 10 Lowest prime number in List : 1 Sum of all prime numbers : 55 Average of all prime numbers : 5.5
并行流 parallelStream 上文有提到,Stream API 提供串行和并行两种模式进行汇聚操作,并发模式能够充分利用多核处理器的优势。串行流就是上面的 stream,而想要并行操作,就需要使用 parallelSteram。下面举一个例子来看看 stream 和 parallelStream 的区别。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 public class ParallelStreamTest { public static void main (String[] args) { List<Integer> numList = Arrays.asList(1 , 2 , 3 , 4 , 5 ); doFor(numList); doStream(numList); doParallelStream(numList); } private static void doFor (List<Integer> numList) { long start = System.currentTimeMillis(); for (int num : numList) { try { Thread.sleep(1000 ); } catch (InterruptedException e) { e.printStackTrace(); } System.out.print(num); } System.out.println(); long stop = System.currentTimeMillis(); System.out.println("doFor: " + (stop - start)); } private static void doStream (List<Integer> numList) { long start = System.currentTimeMillis(); numList.stream().forEach(num -> { try { Thread.sleep(1000 ); } catch (InterruptedException e) { e.printStackTrace(); } System.out.print(num); }); System.out.println(); long stop = System.currentTimeMillis(); System.out.println("doStream: " + (stop - start)); } private static void doParallelStream (List<Integer> numList) { long start = System.currentTimeMillis(); numList.parallelStream().forEach(num -> { try { Thread.sleep(1000 ); } catch (InterruptedException e) { e.printStackTrace(); } System.out.print(num); }); System.out.println(); long stop = System.currentTimeMillis(); System.out.println("doParallelStream: " + (stop - start)); } }
输出:
1 2 3 4 5 6 12345 doFor: 5003 12345 doStream: 5003 34251 doParallelStream: 1009
代码上 stream 和 parallelStream 语法差异较小,用法基本一样。从执行结果来看,stream 顺序输出,而 parallelStream 无序输出;parallelStream 执行耗时是 stream 的五分之一,stream 和 for 循环用时一样。可以看到在当前测试场景下,parallelStream 获得的相对较好的执行性能,那 parallelStream 背后到底是什么呢? 要深入了解 parallelStream,首先要弄明白 ForkJoin 框架和 ForkJoinPool。ForkJoin 框架是 java 7 中提供的并行执行框架,他的策略是分而治之。说白了,就是把一个大的任务切分成很多小的子任务,子任务执行完毕后,再把结果合并起来。
parallelStream 使用注意点 在开发过程中,经常会遇到遍历一个很大的集合做重复的操作,这时候如果使用串行执行会相当耗时,因此一般会采用多线程来提速。但是 parallelStream 若使用不当,很容易掉进陷阱中。总结以下几点需要注意:
parallelStream 对集合操作是无序的,所以若需要顺序操作,请使用 stream 或者使用 parallelStream().forEachOrdered,后者执行时间就和 stream一样了,并不会提高效率。 parallelStream 速度并不会总是比 stream 快。将上面的例子修改为 Thread.sleep(1)
,输出结果为: 1 2 3 4 5 6 12345 doFor: 10 12345 doStream: 12 32154 doParallelStream: 13
可见并不是并行执行就是性能最好的,要根据具体的应用场景测试分析。这个例子中,每个子任务执行时间较短,而线程切换消耗了大量时间。
paralleStream 是非线程安全的!非线程安全!非线程安全!重要的事情说三遍。下面看一个例子就可以很明显的看到了。 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 public class ThreadSafeTest { private static List<Integer> list1 = new ArrayList <>(); private static List<Integer> list2 = new ArrayList <>(); private static List<Integer> list3 = new ArrayList <>(); private static Lock lock = new ReentrantLock (); public static void main (String[] args) { IntStream.range(0 , 10000 ).forEach(list1::add); IntStream.range(0 , 10000 ).parallel().forEach(list2::add); IntStream.range(0 , 10000 ).forEach(i -> { lock.lock(); try { list3.add(i); }finally { lock.unlock(); } }); System.out.println("串行执行的大小:" + list1.size()); System.out.println("并行执行的大小:" + list2.size()); System.out.println("加锁并行执行的大小:" + list3.size()); } }
输出:
1 2 3 串行执行的大小:10000 并行执行的大小:9592 加锁并行执行的大小:10000
显而易见,stream.parallel.forEach()中执行的操作并非线程安全。如果需要线程安全,可以把集合转换为同步集合,即:Collections.synchronizedList(new ArrayList<>())。也可以像例子中的那样,对操作进行加锁。
总结 Lambda 表达式提供了 Java 的函数化编程能力,取代了匿名内部类。让我们的代码量更少更美观。 Lambda表达式在Java中又称为闭包或匿名函数。 Stream API 提供了强大的集合操作。让我们在开发过程中更关心逻辑,而不是怎么详细的去实现。 stream 是串行的,线程安全的。parallelStream 是并行的,线程不安全的,在使用过程中尤其要注意。 参考资料 Java 8 Lambda 表达式 | 菜鸟教程 Java 8 中的 Streams API 详解 | IBM Developer Java 8 Lambda 表达式10个示例 | ImportNew Java 8 parallelStream 浅析 | 知乎 Java 8 parallelStream 并发安全的思考 | puyangsky 博客园 深入浅出 parallelStream | 梦铃之境的专栏
文章标题:Java 8 的 Lambda 表达式和 Stream API 文章作者:cylong 文章链接:https://0skyu.cn/posts/af72.html 有问题或者建议欢迎在下方评论。欢迎转载、引用,但希望标明出处,感激不尽(●’◡’●)