java8 lambda表达式是继范型之后java语言最有意思的特性了,理解其中用法、原理可让我们写CRUD时得心应手,撸代码时有飞一般的感觉。本文从lambda表达式的基础用法、流操作、JVM层面与匿名内部类区别、7种遍历性能比较等4大方面讲述。
版本:JDK1.8
应用篇 基础用法 首先看看case用法,如下
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 public class LambdaTester { public static void main (String[] args) { LambdaTester lambdaTester = new LambdaTester(); MathOperation addOp = (int a, int b) -> a + b; MathOperation subOp = (a, b) -> a - b; MathOperation multi = (int a, int b) -> { return a * b; }; MathOperation divOp = (int a, int b) -> a / b; System.out.println("10 + 20 = " + lambdaTester.operate(10 , 20 , addOp)); System.out.println("10 - 20 = " + lambdaTester.operate(10 , 20 , subOp)); System.out.println("10 * 20 = " + lambdaTester.operate(10 , 20 , multi)); System.out.println("10 / 20 = " + lambdaTester.operate(10 , 20 , divOp)); } interface MathOperation { int operation (int a, int b) ; } private int operate (int a, int b, MathOperation mathOperation) { return mathOperation.operation(a, b); } }
out:
1 2 3 4 10 + 20 = 30 10 - 20 = -10 10 * 20 = 200 10 / 20 = 0
上面case展示了常用4种语法:
带有类型声明
不用类型声明,
大括号中间返回值
没有大括号及返回值
转换为lambda表达式 下面展示了可隐式转换为lambda表达式几种情况。
函数式接口 函数式接口可以隐式的转换为lambda表达式,所谓的函数式接口是有且只有一个抽象方法,但是可以有多个非抽象方法的接口。 如上图接口interface MathOperation
就是个函数式接口。
方法引用 上述MathOperation addOp = (int a, int b) -> a + b;
可简写为MathOperation addOp2 = Integer::sum;
现在也有现成的方法可以完成某个能力,比如Sysout.out::println
和Integer::sum
等等,都属于方法引用,也等价于lambda表达式。
从上面的示例可以看出, 要用::
操作符分隔方法名与对象或类名。 主要有 3 种情况:
object::instanceMethod,如 System.out::println
Class::static Method , 如 Integer::sum
Class::instanceMethod,如 String::compareToIgnoreCase
构造器引用 首先看一个示例,把字符串数据转化为Person列表
1 2 3 4 5 6 7 8 9 10 List<String> names = Arrays.asList("java" ,"go" ,"php" ); Stream<Person> stream = names.stream().map(Person::new ); List<Person> people = stream.collect(Collectors.toList()); class Person { String name; Person(String str){ this .name = str; } }
使用方式Person::new是Person构造器的一个引用,构造器引用也可以转换为lambda表达式。
变量作用域 看看lambda表达式内变量作用域情况。
首先看一个case:
1 2 3 4 5 6 7 8 public static void repeatMessage (String text, int num) { new Thread(() -> System.out.println(text)).start(); new Thread(() -> System.out.println(num--)).start(); }
可以看到,lambda是可以捕获外围作用域中的变量的值(必须是最终变量 ),只能引用值不能修改且不能引用在外围变化的变量 。
铁打的规则:
lambda 表达式中捕获的变量必须实际上是最终变量(effectivelyfinal)。 实际上的最终变量是指, 这个变量初始化之后就不会再为它赋新值。
流式用法 使用Java的流库处理集合数据让编码更优雅简洁,并且不会改变原数据,StreamAPI的流式操作的接口java.util.stream.Stream;
流式操作分三个部分:
创建流操作,即可从集合转化创建一个流
中间流操作,即对流进行filter、map等操作
终结流操作,即把流转换为集合数据
流的创建 流创建方式有很多种,以下常用的几种
Stream静态方法创建流
数组转化为流
正则转化为流
Collection接口类和Map接口类支持直接流操作
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 Stream<String> langArr = Stream.of("java" ,"go" ,"php" ,"java" ); Stream.generate(Math::random).limit(10 ).forEach(System.out::println); String[] arr = {"java" ,"go" ,"php" ,"java" }; Arrays.stream(arr).forEach(System.out::println); List<String> names = Arrays.asList("java" , "go" , "php" ); Stream<Person> stream = names.stream().map(Person::new ); Stream<String> words = Pattern.compile("\\PL" ).splitAsStream("hello lambda" ); ArrayList<String> list = new ArrayList<>(Arrays.asList("java" ,"go" ,"php" ,"java" )); list.forEach(System.out::println);
流的中间操作 Stream接口中提供了很多种流操作,常用方法如下
foreach()方法,包含应用于所有元素的所产生的结果
filter()方法,包含当前流中所有满足条件的元素
map()方法,包含将map方法引用应用于当前流中所有元素所产生的结果
flatMap()方法,将原来几个流中元素平摊返回新结果
distinct()方法,将原来元素的顺序提出重复元素后所产生的结果
sorted()方法,原有流中按照顺序排列的元素产生的结果
peek()方法,在获取每个元素时,会将其传递给方法引用
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 String[] str = {"java" , "go" , "php" , "java" }; Arrays.stream(str).filter(x -> x.equals("java" )).forEach(x -> System.out.println(x)); Arrays.stream(str).map(String::toUpperCase).forEach(x -> System.out.println(x)); Stream<List<Integer>> stream1 = Stream.of(Arrays.asList(1 ,2 ), Arrays.asList(3 , 4 , 5 )); stream1.flatMap(list -> list.stream()).forEach(i -> System.out.println(i)); Arrays.stream(str).distinct().forEach(x -> System.out.println(x)); Arrays.stream(str).sorted((s1, s2) -> s2.length()-s1.length()).forEach(x -> System.out.println(x)); Arrays.stream(str).peek(x -> System.out.println(x)).toArray();
流的终结操作 在上面创建和转换流后最终需要转换为程序中使用的值。
collect()方法,将流收集到列表或集中
toArray()方法,流转换成数组
reduce()方法,用于从流中计算某个值的通用机制
1 2 3 4 5 6 7 8 9 10 11 String[] str = {"java" , "go" , "php" , "java" }; List<String> strList = Arrays.stream(str).collect(Collectors.toList()); String[] strArr1 = Arrays.stream(str).toArray(String[]::new ); Arrays.stream(intArr).reduce((x,y)->x+y).orElse(0 );
其中collect()方法中传入Collectors对象可以实现从流转换到任意集合中(其实现了Collection接口类和map接口类)。
原理篇 基础篇了解到lambda表达式可简化匿名内部类操作,那接下来我们从JVM层面看看与内部类有什么关系呢?又有什么区别呢?
推荐一篇较好的文章分析jVM层面的内部类和lambda表达式链接
那接下来写个case看看效果。
匿名内部类实现 创建匿名内部类通过编译、反编译查看其JVM指令实现。
1 2 3 4 5 6 7 8 9 10 public class AnonymousClassTest { public static void main (String[] args) { new Thread(new Runnable() { @Override public void run () { System.out.println("内部匿名类 - 线程" ); } }).start(); } }
javac LambdaTest.java
编译后生俩.class
文件,分别是AnonymousClassTest.class
和AnonymousClassTest$1.class
。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 public class AnonymousClassTest { public AnonymousClassTest () ; Code: 0 : aload_0 1: invokespecial #1 // Method java/lang/Object."<init>":()V 4 : return public static void main (java.lang.String[]) ; Code: 0: new #2 // class java/lang/Thread 3 : dup 4: new #3 // class AnonymousClassTest$1 /** 创建内部匿名类**/ 7 : dup 8: invokespecial #4 // Method AnonymousClassTest$1."<init>":()V 11: invokespecial #5 // Method java/lang/Thread."<init>":(Ljava/lang/Runnable;)V 14: invokevirtual #6 // Method java/lang/Thread.start:()V 17 : return }
可看到创建了一个AnonymousClassTest$1
类,且可看其AnonymousClassTest$1.class
文件。
lambda表达式实现 lambda表达式在JVM指令实现如下
1 2 3 4 5 6 public class LambdaTest { public static void main (String[] args) { new Thread(() -> System.out.println("lambda表达式 - 线程" )).start(); } }
编译后只产生一个.class
文件,反编译查看其实现。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 public class LambdaTest { public LambdaTest () ; Code: 0 : aload_0 1: invokespecial #1 // Method java/lang/Object."<init>":()V 4 : return public static void main (java.lang.String[]) ; Code: 0: new #2 // class java/lang/Thread 3 : dup 4: invokedynamic #3, 0 // InvokeDynamic #0:run:()Ljava/lang/Runnable; /** 使用invokedynamic指令调用 **/ 9: invokespecial #4 // Method java/lang/Thread."<init>":(Ljava/lang/Runnable;)V 12: invokevirtual #5 // Method java/lang/Thread.start:()V 15 : return }
推论,this引用意义 从反编译指令得知lambda没有创建匿名内部类,那么lambda内部使用this引用会不会是直接用外部类的this呢?跟匿名内部类this不一样呢?
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 public class ThisTest { public static void main (String[] args) { new ThisTest().test(); } public void test () { new Thread(() -> System.out.println(this )).start(); new Thread(() -> System.out.println(toString())).start(); new Thread(new Runnable() { @Override public void run () { System.out.println(this ); } }).start(); } public String toString () { return "hello java" ; } }
out:
1 2 3 hello java hello java ThisTest$1 @3 c86690c
可从输出判断lambda内部使用this即使用了外部类的this,跟内部类的this截然不同。
性能篇 自从java8推出了lambda特性深受人们的喜爱(包括我,能用lambda表达式绝不用其它),那lambda跟原来的iterator 和 for-each 循环相比性能如何呢?
根据Alex Zhitnitsky 的测试结果显示:使用JMH(Java Microbenchmarking Harness) 执行基准测试,比较了7种遍历方式性能,提出Java8 Lambda表达式和流操作如何让你的代码变慢 5 倍。
效果如图:
具体测试过程可查看原文(需要梯子),或者查看importnew译文。
原文作者:Alex Zhitnitsky , java界有名的软件测试原文链接 importnew译文链接 原文github源码链接
总结篇 从lambda表达式的基础用法、流操作、JVM层面与匿名内部类区别、7种遍历性能比较都实现了case,基本涵盖了所有的lamnbda表达式应用,剩余其他的一些类支持流操作本文就不再讲述了(像Arrays、Collection和Map内部也支持流操作)。
总体来讲,语法糖虽很甜但一直用不一定一直甜,当遇到性能瓶颈时可考虑一下是否是lambda表达式性能问题。