Java 8 Lambda 学习笔记

Java 8发布于2014年3月18日,核心特性即,lambda表达式、函数式接口、流API、默认方法和新的Date以及Time API。学习和掌握lambda表达式的最佳方法就是勇于尝试,尽可能多练习lambda表达式例子。鉴于受Java 8发布的影响最大的是Java集合框架(Java Collections framework),所以最好练习流API和lambda表达式,用于对列表(Lists)和集合(Collections)数据进行提取、过滤和排序。

lambda 使用示例

lambda实现多线程:
1
new Thread(()-> System.out.println("Hello World")).start();

相当于:

1
2
3
4
5
new Thread(new Runnable() {
@Override public void run() {
System.out.println("Hello world!");
}
}).start();
按字符串的长度进行排序

不需要实现Comparable接口,使用一个Lambda表达式就可以改变一个函数的行为:

1
2
3
String []datas = new String[] {"peng","zhao","li"};
Arrays.sort(datas,(v1 , v2) -> Integer.compare(v1.length(), v2.length()));
Stream.of(datas).forEach(param -> System.out.println(param));
Swing API编程,事件监听代码
1
2
3
4
5
6
7
8
9
10
11
12
13
// Java 8之前:
JButton show = new JButton("Show");
show.addActionListener(new ActionListener() {
@Override
public void actionPerformed(ActionEvent e) {
System.out.println("Event handling without lambda expression is boring");
}
});

// Java 8方式:
show.addActionListener((e) -> {
System.out.println("Light, Camera, Action !! Lambda expressions Rocks");
});
迭代遍历
1
2
3
4
5
6
7
8
9
10
11
12
13
// Java 8之前:
List features = Arrays.asList("Lambdas", "Default Method", "Stream API", "Date and Time API");
for (String feature : features) {
System.out.println(feature);
}

// Java 8之后:
List features = Arrays.asList("Lambdas", "Default Method", "Stream API", "Date and Time API");
features.forEach(n -> System.out.println(n));

// 使用Java 8的方法引用更方便,方法引用由::双冒号操作符标示,
// 看起来像C++的作用域解析运算符
features.forEach(System.out::println);
  • 如果Lambda表达式不需要参数,可以使用一个空括号表示
    () -> {for (int i = 0; i < 1000; i++) doSomething();};
  • 如果编译器能够推测出Lambda表达式的参数类型,则不需要显示指定,如上述排序示例。
  • Lambda表达式的返回类型,无需指定,编译器会自行推断,说是自行推断
  • Lambda表达式的参数可以使用修饰符及注解,如final、@NonNull等
使用lambda表达式和函数式接口Predicate

Java 8添加了一个包java.util.function,包含很多类,用来支持Java的函数式编程。其中一个便是Predicate,使用java.util.function.Predicate 函数式接口以及lambda表达式,可以向API方法添加逻辑,用更少的代码支持更多的动态行为。
Predicate接口非常适用于做过滤。过滤集合数据的多种常用方法示例:

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
public static void main (String[] args){
List languages = Arrays.asList("Java", "Scala", "C++", "Haskell", "Lisp");

System.out.println("Languages which starts with J :");
filter(languages, (str)->((String)str).startsWith("J"));

System.out.println("Languages which ends with a ");
filter(languages, (str)->((String)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)->((String)str).length() > 4);
}

public static void filter(List names, Predicate condition) {
for(Object name: names) {
if(condition.test(name)) {
System.out.println(name + " ");
}
}
}

输出如下:
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

其中 filter 可继续简化写为:

1
2
3
public static void filter(List names, Predicate condition){
names.stream().filter((name)->(condition.test(name))).forEach(System.out::println);
}

可以看到,Stream API的过滤方法也接受一个Predicate,这意味着可以将我们定制的 filter() 方法替换成写在里面的内联代码,这就是lambda表达式的魔力。另外,Predicate接口也允许进行多重条件的测试。

lambda表达式中加入Predicate

java.util.function.Predicate 允许将两个或更多的 Predicate 合成一个。它提供类似于逻辑操作符AND和OR的方法,名字叫做and()、or()和xor(),用于将传入 filter() 方法的条件合并起来。例如,要得到所有以J开始,长度为四个字母的语言,可以定义两个独立的 Predicate 示例分别表示每一个条件,然后用 Predicate.and() 方法将它们合并起来,如下所示:

1
2
3
4
5
Predicate<String> startsWithJ = (n) -> n.startsWith("J");
Predicate<String> fourLetterLong = (n) -> n.length() == 4;
names.stream()
.filter(startsWithJ.and(fourLetterLong))
.forEach((n) -> System.out.print("nName, which starts with 'J' and four letter long is : " + n));

也可以使用 or() 和 xor() 方法。

Java 8中使用lambda表达式的Map和Reduce示例
1
2
3
4
5
6
7
8
9
// 不使用lambda表达式
List<Integer> costBeforeTax = Arrays.asList(100, 200, 300, 400, 500);
for(Integer cost : costBeforeTax){
double price = cost + .12*cost;
System.out.println(price);
}
// 使用lambda表达式
List<Integer> costBeforeTax = Arrays.asList(100, 200, 300, 400, 500);
costBeforeTax.stream().map((cost) -> cost + .12*cost).forEach(System.out::println);

将 costBeforeTax 列表的每个元素转换成为税后的值。我们将 x -> x*x lambda表达式传到 map() 方法,后者将其应用到流中的每一个元素。然后用 forEach() 将列表元素打印出来。使用流API的收集器类,可以得到所有含税的开销。有 toList() 这样的方法将 map 或任何其他操作的结果合并起来。由于收集器在流上做终端操作,因此之后便不能重用流了。

算总和,使用reduce()

1
2
3
4
5
6
7
8
9
10
11
12
13
// 不使用 lambda 表达式
double total = 0;
List<Integer> costBeforeTax = Arrays.asList(100, 200, 300, 400, 500);
for(Integer cost : costBeforeTax){
double price = cost + .12*cost;
total += price;
}
System.out.println("Total: " + total);

// 使用 lambda 表达式
List<Integer> costBeforeTax = Arrays.asList(100, 200, 300, 400, 500);
double total = costBeforeTax.stream().map((cost) -> cost + .12*cost).reduce((sum, cost)-> sum + cost).get();
System.out.println("Total: " + total);

reduce() 函数可以将所有值合并成一个。Map和Reduce操作是函数式编程的核心操作,因为其功能,reduce 又被称为折叠操作。reduce 并不是一个新的操作,你有可能已经在使用它。SQL中类似 sum()、avg() 或者 count() 的聚集函数,实际上就是 reduce 操作,因为它们接收多个值并返回一个值。流API定义的 reduce() 函数可以接受lambda表达式,并对所有值进行合并。Stream类有类似average()、count()、sum()的内建方法来做 reduce 操作,也有mapToLong()、mapToDouble() 方法来做转换。这并不会限制你,你可以用内建方法,也可以自己定义。在这个Java 8的Map Reduce示例里,我们首先对所有价格应用 12% 的VAT[1],然后用 reduce() 方法计算总和。

通过过滤创建一个String列表
1
2
3
4
// 创建一个字符串列表,每个字符串长度大于2
List<String> strList = Arrays.asList("abc","","bcd","","defg","jk");
List<String> filtered = strList.stream().filter(x -> x.length()> 2).collect(Collectors.toList());
System.out.printf("Original List : %s, \nfiltered list : %s %n", strList, filtered);

输出:
Original List : [abc, , bcd, , defg, jk],
filtered list : [abc, bcd, defg]

过滤是Java开发者在大规模集合上的一个常用操作,而现在使用lambda表达式和流API过滤大规模数据集合是惊人的简单。流提供了一个 filter() 方法,接受一个 Predicate 对象,即可以传入一个lambda表达式作为过滤逻辑。

对列表的每个元素应用函数
1
2
3
4
// 将字符串换成大写并用逗号链接起来
List<String> G7 = Arrays.asList("USA", "Japan", "France", "Germany", "Italy", "U.K.","Canada");
String G7Countries = G7.stream().map(x -> x.toUpperCase()).collect(Collectors.joining(", "));
System.out.println(G7Countries);

输出:
USA, JAPAN, FRANCE, GERMANY, ITALY, U.K., CANADA

通常需要对列表的每个元素使用某个函数,例如逐一乘以某个数、除以某个数或者做其它操作。这些操作都很适合用 map() 方法,可以将转换逻辑以lambda表达式的形式放在 map() 方法里,就可以对集合的各个元素进行转换了。

distinct() 方法来对集合进行去重
1
2
3
4
// 用所有不同的数字创建一个正方形列表
List<Integer> numbers = Arrays.asList(9, 10, 3, 4, 7, 3, 4);
List<Integer> distinct = numbers.stream().map( i -> i*i).distinct().collect(Collectors.toList());
System.out.printf("Original List : %s, Square Without duplicates : %s %n", numbers, distinct);

输出:
Original List : [9, 10, 3, 4, 7, 3, 4], Square Without duplicates : [81, 100, 9, 16, 49]

计算集合元素的最大值、最小值、总和以及平均值
1
2
3
4
5
6
7
//获取数字的个数、最小值、最大值、总和以及平均值
List<Integer> primes = Arrays.asList(2, 3, 5, 7, 11, 13, 17, 19, 23, 29);
IntSummaryStatistics stats = primes.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());

输出:
Highest prime number in List : 29
Lowest prime number in List : 2
Sum of all prime numbers : 129
Average of all prime numbers : 12.9

here is a very useful method called summaryStattics() in stream classes like IntStream, LongStream and DoubleStream. Which returns returns an IntSummaryStatistics, LongSummaryStatistics or DoubleSummaryStatistics describing various summary data about the elements of this stream. In following example, we have used this method to calculate maximum and minimum number in a List. It also has getSum() and getAverage() which can give sum and average of all numbers from List.
IntStream、LongStream 和 DoubleStream 等流的类中,有个非常有用的方法叫做 summaryStatistics() 。可以返回 IntSummaryStatistics、LongSummaryStatistics 或者 DoubleSummaryStatistic s,描述流中元素的各种摘要数据。在本例中,我们用这个方法来计算列表的最大值和最小值。它也有 getSum() 和 getAverage() 方法来获得列表的所有元素的总和及平均值。

lambda表达式变量作用域

Lambda表达式的变量作用域与内部类非常相似,只是条件相对来说,放宽了些以前内部类要想引用外部类的变量。
以前写法,需要将变量声明为final类型的:

1
2
3
4
5
6
7
final String[] datas = new String[] { "peng", "Zhao", "li" };
new Thread(new Runnable() {
@Override
public void run() {
System.out.println(datas);
}
}).start();

Java 8不要求强制的加上final关键字:

1
2
String []datas = new String[] {"peng","Zhao","li"};
new Thread(() -> System.out.println(datas)).start();

但是Java 8中要求这个变量是effectively final:有效只读变量,意思是这个变量可以不加final关键字,但是这个变量必须是只读变量,即一旦定义后,在后面就不能再随意修改。
Java中内部类以及Lambda表达式中也不允许修改外部类中的变量,这是为了避免多线程情况下的race condition。
外部类中定义的变量,Lambda不能再重复定义,同时在Lambda表达式使用的this关键字,指向的是外部类。

函数式接口

函数式接口是Java 8为支持Lambda表达式新发明的,在上面讲述的Lambda Syntax时提到的sort排序方法就是一个样例,在这个排序方法中就使用了一个函数式接口,函数的原型声明如下所示

1
public static <T> void sort(T[] a, Comparator<? super T> c)
  • 函数式接口具有两个主要特征,是一个接口,这个接口具有唯一的一个抽像方法,我们将满足这两个特性的接口称为函数式接口。
  • 函数式接口可以使用@FunctionalInterface进行标注,使用这个标注后,主要有两个优势,编译器知道这是一个函数式接口,符合函数式的要求,另一个就是生成Java Doc时会进行显式标注
  • 异常,如果Lambda表达式会抛出非运行时异常,则函数式接口也需要抛出异常,函数式接口是Lambda表达式的目标类型
  • 函数式接口可以提供多个抽像方法,纳尼!上面不是说只能有一个嘛?是的,在函数式接口中可以提供多个抽像方法,但这些抽像方法限制了范围,只能是Object类型里的已有方法,为什么要这样做呢?此处忽略,大家可以自已研究
  • 函数式接口里面可以定义方法的默认实现,如下所示是Predicate类的代码,不仅可以提供一个default实现,而且可以提供多个default实现
1
2
3
4
default Predicate<T> or(Predicate<? super T> other) {
Objects.requireNonNull(other);
return (t) -> test(t) || other.test(t);
}

Java 8中在接口中增加了默认实现这种函数,其实在很大程序上违背了接口具有抽象这种特征的,增加default实现主要原因是因为考虑兼容及代码的更改成本,例如,在Java 8中向iterator这种接口增加一个方法,那么实现这个接口的所有类都要需实现一遍这个方法,那么Java 8需要更改的类就太多的,因此在Iterator接口里增加一个default实现,那么实现这个接口的所有类就都具有了这种实现,说白了,就是一个模板设计模式吧

方法引用

若需要执行的代码在某些类中已经存在,就没必要再去写Lambda表达式,可以直接使用该方法:

1
Stream.of(datas).forEach(System.out::println);

相当于

1
Stream.of(datas).forEach(param -> {System.out.println(param);});
类的静态方法引用对字符串数组里的元素忽略大小写进行排序
1
2
3
String []datas = new String[] {"peng","Zhao","li"};
Arrays.sort(datas,String::compareToIgnoreCase);
Stream.of(datas).forEach(System.out::println);
方法引用的具体分类
1
2
3
Object:instanceMethod
Class:staticMethod
Class:instanceMethod

上面分类中前两种在Lambda表达式的意义上等同,都是将参数传递给方法,如上示例
System.out::println == x -> System.out.println(x)

最后一种分类Class:instanceMethod,第一个参数是方法执行的目标,如下示例
String::compareToIgnoreCase == (x,y) -> x.compareToIgnoreCase(y)
构造方法引用与方法引用类似,除了一点,就是构造方法引用的方法是new

1
2
Class::new
Class[]::new

示例

1
2
3
4
5
6
// 示例一
String str = "test";
Stream.of(str).map(String::new).peek(System.out::println).findFirst();
// 示例二
String []copyDatas = Stream.of(datas).toArray(String[]::new);
Stream.of(copyDatas).forEach(x -> System.out.println(x));
Lambda表达式 vs 匿名类
  • 一个关键的不同点就是关键字 this。匿名类的 this 关键字指向匿名类,而lambda表达式的 this 关键字指向包围lambda表达式的类。
  • 另一个不同点是二者的编译方式。Java编译器将lambda表达式编译成类的私有方法。使用了Java 7的 invokedynamic 字节码指令来动态绑定这个方法。

要点回顾

  • lambda表达式仅能放入如下代码:预定义使用了 @Functional 注释的函数式接口,自带一个抽象函数的方法,或者SAM(Single Abstract Method 单个抽象方法)类型。这些称为lambda表达式的目标类型,可以用作返回类型,或lambda目标代码的参数。例如,若一个方法接收Runnable、Comparable或者 Callable 接口,都有单个抽象方法,可以传入lambda表达式。类似的,如果一个方法接受声明于 java.util.function 包内的接口,例如 Predicate、Function、Consumer 或 Supplier,那么可以向其传lambda表达式。
  • lambda表达式内可以使用方法引用,仅当该方法不修改lambda表达式提供的参数。本例中的lambda表达式可以换为方法引用,因为这仅是一个参数相同的简单方法调用。

    1
    2
    list.forEach(n -> System.out.println(n)); 
    list.forEach(System.out::println); // 使用方法引用

    然而,若对参数有任何修改,则不能使用方法引用,而需键入完整地lambda表达式,如下所示:

    1
    list.forEach((String s) -> System.out.println("*" + s + "*"));

    (可以省略这里的lambda参数的类型声明,编译器可以从列表的类属性推测出来。)

  • lambda内部可以使用静态、非静态和局部变量,这称为lambda内的变量捕获。
  • lambda表达式在Java中又称为闭包或匿名函数
  • lambda方法在编译器内部被翻译成私有方法,并派发 invokedynamic 字节码指令来进行调用。可以使用JDK中的 javap 工具来反编译class文件。使用 javap -p 或 javap -c -v 命令来看一看lambda表达式生成的字节码。大致应该长这样:

    1
    private static java.lang.Object lambda$0(java.lang.String);
  • lambda表达式有个限制,那就是只能引用 final 或 final 局部变量,这就是说不能在lambda内部修改定义在域外的变量。

    1
    2
    3
    List<Integer> primes = Arrays.asList(new Integer[]{2, 3,5,7});
    int factor = 2;
    primes.forEach(element -> { factor++; });

会有编译错误 Compile time error : “local variables referenced from a lambda expression must be final or effectively final”

只是访问它而不作修改是可以的,如下所示:

1
2
3
List<Integer> primes = Arrays.asList(new Integer[]{2,3,5,7});
int factor = 2;
primes.forEach(element -> { System.out.println(factor*element); });

因此,它看起来更像不可变闭包,类似于Python。


[1]: value added tax<英>增值税;

参考资料:
http://www.cnblogs.com/WJ5888/p/4618465.html
http://www.importnew.com/16436.html
http://javarevisited.blogspot.jp/2014/02/10-example-of-lambda-expressions-in-Java8.html#ixzz3gCMp6Vhc