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();

// 1.类型声明
MathOperation addOp = (int a, int b) -> a + b;
// 2.不用类型声明
MathOperation subOp = (a, b) -> a - b;
// 3.大括号中间返回值
MathOperation multi = (int a, int b) -> {
return a * b;
};
// 4.没有大括号以及返回值
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));
}

// 函数式接口,可隐式转换为lambda表达式
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种语法:

  1. 带有类型声明
  2. 不用类型声明,
  3. 大括号中间返回值
  4. 没有大括号及返回值

转换为lambda表达式

下面展示了可隐式转换为lambda表达式几种情况。

函数式接口

函数式接口可以隐式的转换为lambda表达式,所谓的函数式接口是有且只有一个抽象方法,但是可以有多个非抽象方法的接口。
如上图接口interface MathOperation就是个函数式接口。

方法引用

上述MathOperation addOp = (int a, int b) -> a + b;可简写为MathOperation addOp2 = Integer::sum;

现在也有现成的方法可以完成某个能力,比如Sysout.out::printlnInteger::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();

// 错误用法
// 会报错从lambda表达式应用的本地变量必须是最终变量
new Thread(() -> System.out.println(num--)).start();
}

可以看到,lambda是可以捕获外围作用域中的变量的值(必须是最终变量),只能引用值不能修改且不能引用在外围变化的变量

铁打的规则:

lambda 表达式中捕获的变量必须实际上是最终变量(effectivelyfinal)。 实际上的最终变量是指, 这个变量初始化之后就不会再为它赋新值。

流式用法

使用Java的流库处理集合数据让编码更优雅简洁,并且不会改变原数据,StreamAPI的流式操作的接口java.util.stream.Stream;

java.util.stream包

流式操作分三个部分:

  • 创建流操作,即可从集合转化创建一个流
  • 中间流操作,即对流进行filter、map等操作
  • 终结流操作,即把流转换为集合数据

流的创建

流创建方式有很多种,以下常用的几种

  1. Stream静态方法创建流
  2. 数组转化为流
  3. 正则转化为流
  4. Collection接口类和Map接口类支持直接流操作
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// 1. Stream静态方法创建流,of() 和 generate() 方法
Stream<String> langArr = Stream.of("java","go","php","java");

Stream.generate(Math::random).limit(10).forEach(System.out::println);

// 2. 数组转化为流
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);

// 3. 正则表达式中转化出流
Stream<String> words = Pattern.compile("\\PL").splitAsStream("hello lambda");

// 4. Collection直接流操作
ArrayList<String> list = new ArrayList<>(Arrays.asList("java","go","php","java"));
list.forEach(System.out::println);

流的中间操作

Stream接口中提供了很多种流操作,常用方法如下

  1. foreach()方法,包含应用于所有元素的所产生的结果
  2. filter()方法,包含当前流中所有满足条件的元素
  3. map()方法,包含将map方法引用应用于当前流中所有元素所产生的结果
  4. flatMap()方法,将原来几个流中元素平摊返回新结果
  5. distinct()方法,将原来元素的顺序提出重复元素后所产生的结果
  6. sorted()方法,原有流中按照顺序排列的元素产生的结果
  7. 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"};

// 1. 2. foreach() 、filter()操作,过滤不是java的字符串
Arrays.stream(str).filter(x -> x.equals("java")).forEach(x -> System.out.println(x));

// 3. map()操作,元素全部转大写
Arrays.stream(str).map(String::toUpperCase).forEach(x -> System.out.println(x));

// 4. flatMap()操作,平摊
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));

// 5. distinct()操作
Arrays.stream(str).distinct().forEach(x -> System.out.println(x));

// 6. sorted()操作
Arrays.stream(str).sorted((s1, s2) -> s2.length()-s1.length()).forEach(x -> System.out.println(x));

// 7. peek()操作
Arrays.stream(str).peek(x -> System.out.println(x)).toArray();

流的终结操作

在上面创建和转换流后最终需要转换为程序中使用的值。

  1. collect()方法,将流收集到列表或集中
  2. toArray()方法,流转换成数组
  3. reduce()方法,用于从流中计算某个值的通用机制
1
2
3
4
5
6
7
8
9
10
11
String[] str = {"java", "go", "php", "java"};

// 1. collect()操作,生成list
List<String> strList = Arrays.stream(str).collect(Collectors.toList());

// 2. toArray()操作,生成字符串数组,可加任意流中间操作
// 无String[]::new 会生成Object[]对象数组
String[] strArr1 = Arrays.stream(str).toArray(String[]::new);

// 3. reduce操作,计算0+v1+v2+...+vn
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.classAnonymousClassTest$1.class

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// 运行反编译 javap -c AnonymousClassTest.class
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(); // 外部类this
new Thread(() -> System.out.println(toString())).start();

new Thread(new Runnable() {
@Override
public void run() {
System.out.println(this); //内部类this
}
}).start();
}

public String toString() {
return "hello java";
}
}

out:

1
2
3
hello java
hello java
ThisTest$1@3c86690c

可从输出判断lambda内部使用this即使用了外部类的this,跟内部类的this截然不同。

性能篇

自从java8推出了lambda特性深受人们的喜爱(包括我,能用lambda表达式绝不用其它),那lambda跟原来的iterator 和 for-each 循环相比性能如何呢?

根据Alex Zhitnitsky 的测试结果显示:使用JMH(Java Microbenchmarking Harness) 执行基准测试,比较了7种遍历方式性能,提出Java8 Lambda表达式和流操作如何让你的代码变慢 5 倍。

效果如图:
7种遍历方式性能比较

具体测试过程可查看原文(需要梯子),或者查看importnew译文。

原文作者:Alex Zhitnitsky , java界有名的软件测试
原文链接
importnew译文链接
原文github源码链接

总结篇

从lambda表达式的基础用法、流操作、JVM层面与匿名内部类区别、7种遍历性能比较都实现了case,基本涵盖了所有的lamnbda表达式应用,剩余其他的一些类支持流操作本文就不再讲述了(像Arrays、Collection和Map内部也支持流操作)。

总体来讲,语法糖虽很甜但一直用不一定一直甜,当遇到性能瓶颈时可考虑一下是否是lambda表达式性能问题。