新姿势学习之Java8---Lambda Expressions And Stream

摘要: 去年逛 Github 准备秋招项目时,就发现 Android 有的项目使用的语法似乎从来没见过,类似于 (parameters)->expression 还有ObjectReference::methodName 这种类 C++ 语法形式,查找资料之后,知道这个是 Java8 的新特性:Lambda,所以就记下了 Lambda 这个词,后来忙于秋招春招实习,一直也没有取好好学习,昨天在项目当中又看到了这个熟悉的表达式,一脸懵逼,所以准备找几篇 Java8 教程,好好学习一下”新”姿势.





分别总结 java8 中涉及到的一些名词,可能没什么条理,都是有助于理解 java8 中的新特性,特别是 LambdaStream 相关的知识,至少我是怎么认为的,哈哈~。

## 为什么需要 Java8

原因众多,其中最主要的原因是: 可以让多线程并行处理 Colloection 的代码变得容易编写.
商业发展需要复杂的应用,更过的应用都跑在多核的 CPU 上,既然是多核,就需要保证它的并行操作,所以之前 java 中推出了 java.util.concurrent 包来解决并行的问题,但是在大数据的处理上,这些类库的层抽象级别还不够,缺乏高效的并行操作,我们需要编写复杂的集合处理算法,用于处理大数据问题,这种算法已经很难在工具层面来解决了,所以只能上升到语言层面:增加 Lambda 表达式,


## Streams API

标题太广泛,需要一篇文章来总结

## 函数式编程

标题范围太广,需要一篇文章来总结


## 函数式接口

简单来说,函数式接口是只包含一个方法的接口。比如 Java 标准库中的 java.lang.Runnablejava.util.Comparator 都是典型的函数式接口。java 8 提供 @FunctionalInterface 作为注解,这个注解是非必须的,只要接口符合函数式接口的标准(即只包含一个方法的接口),虚拟机会自动判断,但最好在接口上使用注解 @FunctionalInterface 进行声明,以免团队的其他人员错误地往接口中添加新的方法。这里使用一个例子来说明:

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
//定义一个函数式接口
FunctionalInterface
public interface WorkerInterface {
public void doSomeWork();
}
public class WorkerInterfaceTest {
public static void execute(WorkerInterface worker) {
worker.doSomeWork();
}
public static void main(String [] args) {
//invoke doSomeWork using Annonymous class
execute(new WorkerInterface() {
@Override
public void doSomeWork() {
System.out.println("Worker invoked using Anonymous class");
}
});
//invoke doSomeWork using Lambda expression
execute( () -> System.out.println("Worker invoked using Lambda expression") );
}
}


## Lambda 语法 ##

语法的定义比较简单:

1. 一个括号内,用逗号分隔的形式参数,这些个形参是函数式接口里的方法的参数

2. 一个箭头符号 ->
  1. 方法体,可以是表达式或者代码块,是函数式接口里面的方法的具体实现.如果是代码块,就必须要用 {} 包裹起来,且需要一个 return 返回值.但是如果函数式接口里面的方法本身的返回类型就是 void ,那么代码块是不需要用 {} 包裹,也不需要返回值的.
    总结起来,就是它的形式类似于:
1
2
(parameters) -> expression or (parameters) -> { statements; }

4.方法引用.其实是 Lambda 表达式的一个简化写法,所引用的方法其实是 Lambda 表达式的方法体实现,语法也很简单,左边是容器(可以是类名,实例名),中间是 "::",右边是相应的方法名。如下所示:

1
2
ObjectReference::methodName
  • 如果是静态方法,则是 ClassName::methodName。如 Object ::equals

  • 如果是实例方法,则是 Instance::methodName。如 Object obj=new Object();obj::equals;

  • 如果是构造函数 , 则是 ClassName::new

  • 如果是接口方法 , 则是 InterfaceName::methodName.如 List::add , List::addAll

Java 中的 Lambda 无法单独出现,它需要一个函数式接口来盛放, Lambda 表达式方法体其实就是函数接口的实现.

Lambda 应用场景

个人觉得,学习 Lambda 最好的方法,就是通过对比的方式去学,将实现某一功能使用 Lambda 表达式和不使用 Lambda 两套代码进行对比,这样记忆更加深刻,这一小节中也涉及到部分操作符的使用总结。

用 Lambda 表达式实现 Runnable

1
2
3
4
5
6
7
8
9
10
11
//Before java8
new Thread(new Runnable() {
@Override
public void run() {
System.out.println("This is before java8");
}
}).start();
//In java8 way
new Thread(() -> System.out.println("Java8 coming!")).start();

使用 Lambda 表达式进行事件处理

1
2
3
4
5
6
7
8
9
10
11
12
13
//Before Java8
mButton.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
System.out.println("Button clicked!");
}
});
//In java8 way
mButton.setOnClickListener((v)-> {
System.out.println("Button clicked!");
});
  • 这种方式其实跟上面对 Runnable 的操作是一样的,都是将匿名内部类使用 Lambda 来替换,唯一不同的就是,上面 Runnable 的例子里,方法是没有参数的,但是这个例子中, onClick(View view) 回调方法是有一个参数的.
  • 此处的 Lambda 表达式用用的是 (v) ,而不是回调函数本身的 view ,是因为在 Lambda 表达式中的参数是形参,不恰当的例子:形参随便你写什么都行.
  • 上面例子中,方法体的 {} 是可以不用的,这里是为了和上面保证格式一致,方便对比.

用 Lambda 表达式对集合进行迭代

1
2
3
4
5
6
7
8
9
10
11
List<String> features = Arrays.asList("Lambdas", "Default Method", "Stream API", "Date and Time API");
//Before java8
for (String feature : features) {
System.out.println(feature);
}
//In java8 way
features.forEach((String feature) -> System.out.println(feature));
//or
features.forEach(System.out::println);
  • 10 行使用的是方法引用,但是要注意:方法引用不能修改 Lambda 表达式提供的参数.

使用 Lambda 的 filter 操作

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
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<String> names, Predicate<String> condition) {
for(String name: names) {
if(condition.test(name)) {
System.out.println(name + " ");
}
}
}
//另一种写法
public static void filter(List<String> names, Predicate<String> condition) {
names.stream().filter((name) -> (condition.test(name)))
.forEach((name) -> System.out.println(name + " "));
}
//另另一种写法
public static void filter(List<String> names, Predicate<String> condition) {
names.forEach((String str) -> {
if (condition.test(str)) {
System.out.println(str + "");
}
});
}

关于 Predicate,笔者的一些理解:

  1. 可以看到下面的 filter 方法,它接受一个类型为 Predicate 的参数,Predicate 本身是「谓语」的意思(也有翻译做「断言」的,不过我个人觉得,「谓语」更好理解,原因如下)比如:“我打你”,那么这个”打”就是谓语,很明显这是一个谓语动词,其实这个”打”是有一个返回的「结果」的,只是在语言本身的语法当中是没有关注的,比如,打到没有?打疼没有?打死没有? true or false ?
  2. Predicate的描述是这样的: Represents a predicate (boolean-valued function) of one argument. ,代表一个参数的”谓语”,这个谓语是有返回值的,返回值的类型要是 boolean 的.
  3. boolean test(T t) 方法: Evaluates this predicate on the given argument ,用于返回这个参数的谓语的结果.拿上面的例子来说,调用 test(T t) 之后,如果返回的是 false ,那么表示「我没有打到你或者我没有打疼你或者我没打死你」,返回 true 表示,「我打到你了或者我打疼你了或者我打死你了」.
  4. 对应到上面代码第 5 行, Argument 「参数」指的是 languages 集合中的元素,predicate 「谓语」指的是 (str)->str.startWith("J"),很明显,这个谓语是有返回值的,true 表示 languages 中的当前遍历的元素是 J 开头的,反之则不是.

再一次感叹老外命名的巧夺天工!

在 Lambda 表达式中加入 Predicate

上面的例子一次只使用了一个 Predicate ,可以通过逻辑操作符,将两个或者多个 Predicate 的逻辑运算结果作为一个 Predicate.

1
2
3
4
5
6
Predicate<String> lengthFilter = (String str) -> str.length() == 4;
Predicate<String> startFilter = (String str) -> str.startsWith("J");
languages.stream()
.filter(lengthFilter.and(startFilter))
.forEach(language -> System.out.printLn(language));
  • 第四行,的 filter 函数接收一个 Predicate 类型的参数,这个参数是由 lengthFilterstartFilter 的结果两经过与操作组成的,
  • 同理 or() 是或操作, or() 是异或操作.

使用 Lambda 的 map 操作

需求:给出税前的列表,返回税后列表,税12%

1
2
3
4
5
6
7
8
9
10
11
12
13
//Before java8
List<Integer> costBeforeTax = Arrays.asList(100, 200, 300, 400, 500);
for (Integer beforeTax : costBeforeTax) {
double costAfterTax = beforeTax + 0.12 * beforeTax;
System.out.println(costAfterTax);
}
//In java8 way
List<Integer> costBeforeTaxL= Arrays.asList(100,200,300,400,500);
costBeforeTaxL.stream().map(cost -> cost + cost * 0.12).forEach(System.out::println);

这里用到了 map 操作符,它的作用就是:Input Strean 的每一个元素转换成 Output Stream 的另一个元素,这是一个 1:1 的映射.

  • map 的定义:<R> Stream<R> map(Function<? super T, ? extends R> mapper); 官方给的注解是这么说的: Returns a stream consisting of the results of applying the given function to the elements of this stream.翻译成中文 : 返回由 将给定函数 Function 应用于此流的元素的结果 组成的流,那就来看看 Function 是什么咯.

  • @FunctionalInterface public interface Function<T, R>{...} ,注解是 : Represents a function that accepts one argument and produces a result. 这个接口代表一个函数,这个函数接收一个参数(Input Stream),并且会产生一个结果( Output Stream ).

  • 在看上面的例子, map 操作返回的就是将 cost+cost*0.12( Function ) 给应用到 costBeforeTaxl (Input Stream)的每一个元素( cost )之后的结果所组成的流( Output Stream ).

真是拗口…

使用 Lambda 的 reduce 操作

需求:给出税后总和

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
//Before java8
List<Integer> costBeforeTax = Arrays.asList(100, 200, 300, 400, 500);
double total = 0;
for (Integer cost : costBeforeTax) {
double price = cost + .12 * cost;
total = total + price;
}
System.out.println("Total : " + total);
//In java8 way
List<Integer> costBeforeTaxL = Arrays.asList(100, 200, 300, 400, 500);
double bill = costBeforeTaxL.stream().map((cost) -> cost + 0.12 * cost).reduce((sum, cost) -> sum + cost).get();
//或者可以用 reduce 两个参数的方法,两个参数的形式,返回的就不是 Optional<T> 对象了, 而直接是 T 对象
double bill = costBeforeTaxL.stream().map((cost) -> cost + 0.12 * cost).reduce(0d,(sum, cost) -> sum + cost)
System.out.println("Total : " + bill);
  • 先用 map 操作符求出税后的金额,然后在用 reduce 求和.
  • reduce 有两个重载方法,一个有起始参数,也就种子参数,一个是没有起始参数的.
  1. 一个参数定义: Optional<T> reduce(BinaryOperator<T> accumulator); 这个方法的主要作用是把 Stream 元素组合起来。这种方式是没有起始值的,直接依照运算规则(BinaryOperator),和前面 Stream 的第一个、第二个、第 n 个元素组合,返回的是 Optional.
  2. 两个参数定义: T reduce(T identity, BinaryOperator<T> accumulator); 其中 T identity 表示的是起始值.上面的例子传入的值是 0d ,所以输出和一个参数形式的输出结果一样,都是 1680,假设此处传入的起始值是 100 ,那么输出的结果就是 1780.accumulator:计算的「组合器」,其方法签名为 apply(T t,U u),在该 reduce 方法中第一个参数 t为上次函数计算的返回值,第二个参数 uStream 中的元素,这个函数把这两个值计算 apply,得到的「组合」会被赋值给下次执行这个方法的第一个参数。
  3. 也就是说,有起始值的 reduce 返回的是具体的对象,没有起始值返回的是 optianal 对象,因为它可能没有返回的对象,会产生 NOE 异常。
  • Optional 可以简单的理解为一个容器,可能含有某值,也可能不含,使用这个类的目的是为了尽可能的避免 NullPointerException,若含有,则调用 get() 方法之后,就返回这个值,否则抛 NoSuchElementException.在更复杂的 if (xx != null) 的情况中,使用 Optional 代码的可读性更好,而且它提供的是编译时检查,能极大的降低 NPE 这种 Runtime Exception 对程序的影响,或者迫使程序员更早的在编码阶段处理空值问题,而不是留到运行时再发现和调试。
  • 上面的例子,BinaryOperator 指的就是 (sum, cost) -> sum + costrecuxe 函数返回的就是个 Optional 对象,然后我们通过 get() 方法拿到 Optional 中含有的值。

使用 Lambda 的 Collect 操作

1
2
3
List<Integer> originalList= Arrays.asList(1,2,3,4);
List<Integer> afterFilter = Stream.of(1,2,3,4).filter(n -> n > 2).collect(Collectors.toList());
System.out.printf("Original List : %s, afterFilter list : %s %n", originalList, afterFilter);

输出:

1
Original List : [1, 2, 3, 4], afterFilter list : [3, 4]

  • collect 操作用处收集结果,当处理完一个流之后,想看一下处理后的结果,而不是将它们聚合起来,那么就可以用到 Collect 操作符。
  • 首先是对流里面的每一个元素进行 filter 操作,谓语 x.length()>2 这个表达式返回值为 true 的元素,然后对这些符合标准的元素组成的流进行 collect 操作.
  • collect 操作也有两个重载的方法:
  1. <R> R collect(Supplier<R> supplier, BiConsumer<R,? super T> accumulator, BiConsumer<R,R> combiner)
    • supplier 一个能够创造目标类型实例的方法
    • accumulator 一个将元素添加到目标中的方法
    • combiner 一个将中间状态的结果整合到一起的方法注意上面三个参数都是方法.
  2. <R,A> R collect(Collector<? super T,A,R> collector)
    • collector 可以看到,它就是上面 supplier,accumulator,combiner 的聚合体.
  • 将上述例子用三个参数的方法改写:

    1
    2
    3
    4
    5
    Stream<Integer> integerStream = Stream.of(1, 2, 3, 4);
    ArrayList<Integer> result = integerStream.collect(() -> new ArrayList<Integer>(), (list, item) -> list.add(item), (aList, bList) -> aList.addAll(bList));
    //此处也可以使用方法引用
    //ArrayList<Integer> result=integerStream.collect(ArrayList::new,List::add,List::addAll);
    System.out.printf("Original List : %s, afterFilter list : %s %n", integerStream, result);
  • Collectors :看这个类的名字就类似于 Arrays,Executors 这两工具类,点进源码一看,这确实也是一个工具类,它里面都是静态的工厂方法,用于产生 Collector 类型的参数,toListtoSet 就是其中最常见的两个。而通过 Collectors 的静态工厂方法产生的 Collector 的类型由其输入类型和输出类型决定。以 toList() 为例,它的输入类型为 T ,输出类型为 List<T>。对应到上面的例子中,输入类型为 Integer ,所以输出的类型为 List<Integer>

使用 Lambda 的 distinct 操作

1
2
3
4
5
6
7
public static void main(String[] args) {
Stream.of(1, 2, 3, 4, 5, 6, 2, 3, 4, 2, 4, 5)
.map(x -> x * x)
.distinct()
.collect(Collectors.toList())
.forEach(n->System.out.print(n+" "));
}

输出结果为:

1
1 4 9 16 25 36
  • 通过上面的例子可以看出来,distince 操作就是去重。

使用 summaryStatistics 获取几个统计值

1
2
3
4
5
6
7
8
9
10
11
IntSummaryStatistics intSummaryStatistics = Stream
.of(1, 4, 3, 56, 7, 89, 5, 4, 6, 8, 4, 345, 76, 8)
.mapToInt(value -> value)
.summaryStatistics();
System.out.println("max value is:"+ intSummaryStatistics.getMax());
System.out.println("min value is:"+ intSummaryStatistics.getMin());
System.out.println("the average is:"+ intSummaryStatistics.getAverage());
System.out.println("the value is:"+ intSummaryStatistics.getSum());
System.out.println("the count is:"+ intSummaryStatistics.getCount());
  • 此方法用于返回流当中各种在摘要数据,包括最大值,最小值,平均值,和,元素个数。
  • summaryStatistics 方法只有 IntStream、LongStream 和 DoubleStream有。

Lambda 表达式 VS 匿名类

  • 从上面举的例子里可以看到,Lambda 表达式用于提到匿名内部类,这两者有一个关键不同之处就是 this.
  • 匿名类的 this 指向匿名类,而 Lambda 表达式的 this 指向包围 Lambda 表达式的类。
  • 还有一点不同就是编译方式。Java 编译器将 Lambda 表达式是编译成类的私有方法的。

总结

  1. Lambda 表达式仅能放入如下代码:预定义使用了 @FunctionalInterface 注释的函数式接口,自带一个抽象函数的方法,或者SAMSingle Abstract Method 单个抽象方法)类型。这些称为 Lambda 表达式的目标类型,可以用作返回类型,或 Lambda 目标代码的参数。例如,若一个方法接收 RunnableComparable 或者 Callable 接口,都有单个抽象方法,可以传入 Lambda 表达式。类似的,如果一个方法接受声明于 java.util.function 包内的接口,例如 Predicate、Function、Consumer 或 Supplier,那么可以向其传 Lambda 表达式。

  2. 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 参数的类型声明,编译器可以从列表的类属性推测出来。

  1. Lambda 内部可以使用静态、非静态和局部变量,这称为 Lambda 内的变量捕获。

  2. Lambda 表达式在 Java 中又称为闭包或匿名函数。

  3. Lambda 方法在编译器内部被翻译成私有方法,并派发 invokedynamic 字节码指令来进行调用。可以使用 JDK 中的 javap 工具来反编译 class 文件。使用 javap -pjavap -c -v 命令来看一看 Lambda 表达式生成的字节码。大致应该长这样:

1
private static java.lang.Object Lambda $0(java.lang.String);
  1. Lambda 表达式有个限制,那就是只能引用 final 或 final 局部变量,这就是说不能在 Lambda 内部修改定义在域外的变量。
    1
    2
    3
    4
    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); });

输出:

1
2
3
4
4
6
10
14

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

  1. 刚开始使用 Lambda 表达式的时候,会感觉特别困惑,有时不能理解这一个箭头一个参数代表的是什么,这种写法引用的是哪个接口的回调方法,这个回调方法的实现是什么,但是会出现这种情况,就是因为 Lambda 将原来需要由客户定义的一些流程给封装了,现在用户只要告诉它「应该怎么做」,具体的操作过程,不需要我们用户来实施,所以这就是最开始使用起来,感觉疑惑的原因吧,这也是函数式编程所带来的影响。

引用列表

共82.3k字
0%
.gt-container a{border-bottom: none;}