Java 8

函数式接口

https://www.cnblogs.com/heimianshusheng/p/5672641.html
http://lucida.me/blog/java-8-lambdas-insideout-language-features
https://www.cnblogs.com/jianwei-dai/p/5819479.html

函数式接口,简单的说就是指仅含有一个抽象方法的接口,以 @Functionalnterface 标注,注意,这里的抽象方法指的是该接口自己特有的抽象方法,而不包含它从其上级继承过来的抽象方法。
java.lang.Comparable,之前被称为 SAM 类型,即单抽象方法(Single Abstract Method)。我们并不需要声明一个接口是函数式接口,编译器会根据接口的结构自行判断,但是也可以用
@FunctionalInterface 来显式指定一个接口是函数式接口,加上这个注解之后,编译器就会验证该接口是否满足函数式接口的要求

Java8 的接口还可以定义一个 default 的方法,default 方法使用 default 关键字修饰,它是对象方法,需要使用对象来进行访问。

JAVA8接口中的default、static方法使用注意事项

Java8 中常用的全新的接口

Java7 中已经存在的函数式接口:

  • java.lang.Runnable
  • java.util.concurrent.Callable
  • java.security.PrivilegedAction
  • java.util.Comparator
  • java.io.FileFilter
  • java.beans.PropertyChangeListener

除此之外,Java8 中增加了一个新的包:java.util.function,它里面包含了常用的函数式接口,例如:

  • Predicate:接收 T 并返回 boolean
  • Consumer:接收 T,不返回值
  • Supplier:提供 T 对象(例如工厂),不接收值
  • Function<T, R>:接收 T,返回 R
  • UnaryOperator:接收 T 对象,返回 T
  • BinaryOperator:接收两个 T,返回 T

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

Predicate 接口

Predicate 接口只有一个参数,返回 boolean 类型。该接口包含多种默认方法来将 Predicate 组合成其他复杂的逻辑(比如:与,或,非):

代码如下:

1
2
3
4
5
6
7
8
9
10
Predicate<String> predicate = (s) -> s.length() > 0;

predicate.test("foo"); // true
predicate.negate().test("foo"); // false

Predicate<Boolean> nonNull = Objects::nonNull;
Predicate<Boolean> isNull = Objects::isNull;

Predicate<String> isEmpty = String::isEmpty;
Predicate<String> isNotEmpty = isEmpty.negate();

Consumer 接口

java.lang.Iterable 接口中有一个默认的方法实现:

1
2
3
4
5
6
default void forEach(Consumer<? super T> action) {
Objects.requireNonNull(action);
for (T t : this) {
action.accept(t);
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
List<User> users = userRepository.findAll();
users.stream.forEach(user -> {
System.out.println(user.username);
});
```

在 Java8 `java.util.Optional` 类中的 ifPresent 方法实现:
```
public void ifPresent(Consumer<? super T> consumer) {
if (value != null)
consumer.accept(value);
}
```

#### Function 接口
Function 接口有一个参数并且返回一个结果,并附带了一些可以和其他函数组合的默认方法(compose, andThen):

代码如下:
```
Function<String, Integer> toInteger = Integer::valueOf;
Function<String, String> backToString = toInteger.andThen(String::valueOf);
backToString.apply("123"); // "123"

Supplier 接口

Supplier 接口返回一个任意范型的值,和 Function 接口不同的是该接口没有任何参数

代码如下:

1
2
Supplier<Person> personSupplier = Person::new;
personSupplier.get(); // new Person

Comparator 接口

Comparator 是老 Java 中的经典接口, Java8 在此之上添加了多种默认方法:

代码如下:

1
2
3
4
5
6
7
Comparator<Person> comparator = (p1, p2) -> p1.firstName.compareTo(p2.firstName);

Person p1 = new Person("John", "Doe");
Person p2 = new Person("Alice", "Wonderland");

comparator.compare(p1, p2); // > 0
comparator.reversed().compare(p1, p2); // < 0

Lambda 表达式

1
2
3
4
5
6
7
8
9
基本语法:
(parameters) -> expression

(parameters) -> { statements; }
```

可见λ表达式有三部分组成:参数列表,箭头(->),以及一个表达式或语句块。

下面这个例子里的λ表达式没有参数,也没有返回值(相当于一个方法接受 0 个参数,返回 void,其实就是 Runnable 里 run 方法的一个实现):

() -> { System.out.println(“Hello Lambda!”); }

1
2
3
如果只有一个参数且可以被 Java 推断出类型,那么参数列表的括号也可以省略:
```
c -> {return c.size(); }

Target Typing(目标类型)

需要注意的是,函数式接口的名称并不是 lambda 表达式的一部分。那么问题来了,对于给定的 lambda 表达式,它的类型是什么?答案是:它的类型是由其上下文推导而来。例如,下面代码中的 lambda 表达式类型是ActionListener:

1
ActionListener l = (ActionEvent e) -> ui.dazzle(e.getModifiers());

这就意味着同样的 lambda 表达式在不同上下文里可以拥有不同的类型:

1
2
3
Callable<String> c = () -> "done";

PrivilegedAction<String> a = () -> "done";

第一个 lambda 表达式 () -> “done” 是Callable的实例,而第二个 lambda 表达式则是 PrivilegedAction 的实例。

编译器负责推导 lambda 表达式的类型。它利用 lambda 表达式所在上下文所期待的类型进行推导,这个被期待的类型被称为目标类型。lambda 表达式只能出现在目标类型为函数式接口的上下文中。
当然,lambda 表达式对目标类型也是有要求的。编译器会检查 lambda 表达式的类型和目标类型的方法签名(method signature)是否一致。当且仅当下面所有条件均满足时,lambda 表达式才可以被赋给目标类型 T:

  • T 是一个函数式接口
  • lambda 表达式的参数和 T 的方法参数在数量和类型上一一对应
  • lambda 表达式的返回值和 T 的方法返回值相兼容(Compatible)
  • lambda 表达式内所抛出的异常和 T 的方法 throws 类型相兼容

由于目标类型(函数式接口)已经 “知道” lambda 表达式的形式参数(Formal parameter)类型,所以我们没有必要把已知类型再重复一遍。也就是说,lambda 表达式的参数类型可以从目标类型中得出:

1
Comparator<String> c = (s1, s2) -> s1.compareToIgnoreCase(s2);

在上面的例子里,编译器可以推导出 s1 和 s2 的类型是 String。此外,当 lambda 的参数只有一个而且它的类型可以被推导得知时,该参数列表外面的括号可以被省略:

1
2
3
FileFilter java = f -> f.getName().endsWith(".java");

button.addActionListener(e -> ui.dazzle(e.getModifiers()));

Contexts for Target Typing(目标类型的上下文)

之前我们提到 lambda 表达式智能出现在拥有目标类型的上下文中。下面给出了这些带有目标类型的上下文:

  • 变量声明
  • 赋值
  • 返回语句
  • 数组初始化器
  • 方法和构造方法的参数
  • lambda 表达式函数体
  • 条件表达式(? :)
  • 转型(Cast)表达式

在前三个上下文(变量声明、赋值和返回语句)里,目标类型即是被赋值或被返回的类型:

1
2
3
4
5
6
7
8
Comparator<String> c;
c = (String s1, String s2) -> s1.compareToIgnoreCase(s2);

public Runnable toDoLater() {
return () -> {
System.out.println("later");
}
}

数组初始化器和赋值类似,只是这里的 “变量” 变成了数组元素,而类型是从数组类型中推导得知:

1
2
3
filterFiles(new FileFilter[] {
f -> f.exists(), f -> f.canRead(), f -> f.getName().startsWith("q")
});

方法参数的类型推导要相对复杂些:目标类型的确认会涉及到其它两个语言特性:重载解析(Overload resolution)和参数类型推导(Type argument inference)。

1
2
3
4
5
6
7
8
9
10
Thread oldSchool = new Thread(new Runnable () {
@Override
public void run() {
System.out.println("This is from an anonymous class.");
}
} );

Thread gaoDuanDaQiShangDangCi = new Thread(() -> {
System.out.println("This is from an anonymous method (lambda exp).");
} );

第二个线程里的λ表达式,你并不需要显式地把它转成一个 Runnable,因为 Java 能根据上下文自动推断出来:一个 Thread 的构造函数接受一个 Runnable 参数,而传入的λ表达式正好符合其 run() 函数,所以 Java 编译器推断它为 Runnable。

重载解析会为一个给定的方法调用(method invocation)寻找最合适的方法声明(method declaration)。由于不同的声明具有不同的签名,当 lambda 表达式作为方法参数时,重载解析就会影响到 lambda 表达式的目标类型。编译器会通过它所得之的信息来做出决定。如果 lambda 表达式具有显式类型(参数类型被显式指定),编译器就可以直接 使用 lambda 表达式的返回类型;如果 lambda 表达式具有隐式类型(参数类型被推导而知),重载解析则会忽略 lambda 表达式函数体而只依赖 lambda 表达式参数的数量。

如果在解析方法声明时存在二义性(ambiguous),我们就需要利用转型(cast)或显式 lambda 表达式来提供更多的类型信息。如果 lambda 表达式的返回类型依赖于其参数的类型,那么 lambda 表达式函数体有可能可以给编译器提供额外的信息,以便其推导参数类型。

1
2
List<Person> ps = ...
Stream<String> names = ps.stream().map(p -> p.getName());

在上面的代码中,ps 的类型是 List,所以 ps.stream() 的返回类型是 Stream。map() 方法接收一个类型为 Function<T, R> 的函数式接口,这里 T 的类型即是 Stream 元素的类型,也就是 Person,而 R 的类型未知。由于在重载解析之后 lambda 表达式的目标类型仍然未知,我们就需要推导R的类型:通过对 lambda 表达式函数体进行类型检查,我们发现函数体返回 String,因此R的类型是 String,因而 map() 返回 Stream。绝大多数情况下编译器都能解析出正确的类型,但如果碰到无法解析的情况,我们则需要:

  • 使用显式 lambda 表达式(为参数 p 提供显式类型)以提供额外的类型信息
  • 把 lambda 表达式转型为 Function<Person, String>
  • 为泛型参数 R 提供一个实际类型。(. map(p -> p.getName()))

lambda 表达式本身也可以为它自己的函数体提供目标类型,也就是说 lambda 表达式可以通过外部目标类型推导出其内部的返回类型,这意味着我们可以方便的编写一个返回函数的函数:

1
Supplier<Runnable> c = () -> () -> { System.out.println("hi"); };

类似的,条件表达式可以把目标类型 “分发” 给其子表达式:

1
Callable<Integer> c = flag ? (() -> 23) : (() -> 42);

最后,转型表达式(Cast expression)可以显式提供 lambda 表达式的类型,这个特性在无法确认目标类型时非常有用:

1
2
// Object o = () -> { System.out.println("hi"); }; 这段代码是非法的
Object o = (Runnable) () -> { System.out.println("hi"); };

除此之外,当重载的方法都拥有函数式接口时,转型可以帮助解决重载解析时出现的二义性。

Method Reference(方法引用)

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

方法引用语法如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
静态方法引用:ClassName :: methodName  
实例上的实例方法引用:instanceReference :: methodName
超类上的实例方法引用:super :: methodName
类型上的实例方法引用:ClassName :: methodName
```
前两种方式类似,等同于把 lambda 表达式的参数直接当成 instanceMethod | staticMethod 的参数来调用。
比如 `System.out :: println` 等同于 `x -> System.out.println(x)`,`Math :: max` 等同于 `(x, y) -> Math.max(x,y)`。

最后一种方式,等同于把 lambda 表达式的第一个参数当成 instanceMethod 的目标对象,其他剩余参数当成该方法的参数。比如 `String :: toLowerCase` 等同于 `x -> x.toLowerCase()`。

### Construct Reference(构造器引用)
构造器引用语法如下:
```
构造方法引用:Class :: new
数组构造方法引用:TypeName[] :: new

把 lambda 表达式的参数当成 ClassName 构造器的参数 。例如 BigDecimal :: new 等同于 x -> new BigDecimal(x)

Stream

https://www.ibm.com/developerworks/cn/java/j-lo-java8streamapi/
http://www.importnew.com/20331.html
http://docs.oracle.com/javase/tutorial/java/javaOO/lambdaexpressions.html

Java8 里面对 Stream 的定义:

A sequence of elements supporting sequential and parallel aggregate operations.

我们来解读一下上面的那句话:

  1. Stream 是元素的集合,这点让 Stream 看起来用些类似 Iterator
  2. 可以支持顺序和并行的对原 Stream 进行汇聚的操作

可以把 Stream 当成一个高级版本的 Iterator。
原始版本的 Iterator,用户只能一个一个的遍历元素并对其执行某些操作;
高级版本的 Iterator(Stream),用户只要给出需要对其包含的元素执行什么操作,比如 “过滤掉长度大于 10 的字符串”、“获取每个字符串的首字母” 等,具体这些操作如何应用到每个元素上,就交给 Stream 了。

创建 Stream

  1. 通过 Stream 接口的静态工厂方法(Java8 中的一个新特性: 接口可以带静态方法)
  2. 通过 Collection 接口的默认方法(Java8 中的一个新特性: 就是接口中的一个带有实现的方法)

使用 Stream 静态方法来创建 Stream

  1. of 方法:有两个 overload 方法,一个接受变长参数,一个接口单一值
    1
    2
    3
    4
    5
    Stream<Integer> integerStream = Stream.of(1, 2, 3, 5);
    Stream<String> stringStream = Stream.of("taobao");
    ```

    2. generator 方法:生成一个无限长度的 Stream,其元素的生成是通过给定的 Supplier(这个接口可以看成一个对象的工厂,每次调用返回一个给定类型的对象)

Stream.generate(new Supplier() {
@Override
public Double get() {
return Math.random();
}
});

Stream.generate(() -> Math.random());

Stream.generate(Math::random);

1
2
3
4
5
三条语句的作用都是一样的,只是使用了 lambda 表达式和方法引用的语法来简化代码。每条语句其实都是生成一个无限长度的 Stream,其中值是随机的。这个无限长度 Stream 是懒加载,一般这种无限长度的 Stream 都会配合 Stream 的 limit() 方法来用。

3. iterate 方法:也是生成无限长度的 Stream,和 generator 不同的是,其元素的生成是重复对给定的种子值 (seed) 调用用户指定函数来生成的。其中包含的元素可以认为是:seed,f(seed),f(f(seed)) 无限循环
```
Stream.iterate(1, item -> item + 1).limit(10).forEach(System.out::println);

这段代码就是先获取一个无限长度的正整数集合的 Stream,然后取出前 10 个打印。千万记住使用 limit 方法,不然会无限打印下去。

通过 Collection 子类获取 Stream

Collection 接口有一个 stream 方法,所以其所有子类都都可以获取对应的 Stream 对象。

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 interface Collection<E> extends Iterable<E> {
//其他方法省略
default Stream<E> stream() {
return StreamSupport.stream(spliterator(), false);
}
}
```

### 转换 Stream
转换 Stream 其实就是把一个 Stream 通过某些行为转换成一个新的 Stream。Stream 接口中定义了几个常用的转换方法:

示例:
```
# 抽取对象中所有的 id 的集合
List<Long> userIds = users.stream().map(user::getId).collect(Collectors.toList());
```

### Stream 的使用

#### Optional 类
Optional 不是函数接口,这是个用来防止 NullPointerException 异常的辅助类型,这是下一届中将要用到的重要概念,现在先简单的看看这个接口能干什么:

Optional 被定义为一个简单的容器,其值可能是 null 或者不是 null 。在 Java8 之前一般某个函数应该返回非空对象但是偶尔却可能返回了 null,而在 Java8 中,
不推荐你返回 null 而是返回 Optional。

#### Stream 接口

`java.util.stream.Stream` 表示能应用在一组元素上一次执行的操作序列。Stream 操作分为中间操作或者最终操作两种,最终操作返回一特定类型的计算结果,
而中间操作返回 Stream 本身,这样你就可以将多个操作依次串起来。Stream 的创建需要指定一个数据源,比如 `java.util.Collection` 的子类,List 或者 Set, Map 不支持。Stream 的操作可以串行执行或者并行执行。

首先看看 Stream 是怎么用,首先创建实例代码的用到的数据 List:

代码如下:
```
List<String> stringCollection = new ArrayList<>();
stringCollection.add("ddd2");
stringCollection.add("aaa2");
stringCollection.add("bbb1");
stringCollection.add("aaa1");
stringCollection.add("bbb3");
stringCollection.add("ccc");
stringCollection.add("bbb2");
stringCollection.add("ddd1");

Filter 过滤

过滤通过一个 Predicate 接口来过滤并只保留符合条件的元素,该操作属于中间操作,所以我们可以在过滤后的结果来应用其他 Stream 操作(比如 forEach)。
forEach 需要一个函数来对过滤后的元素依次执行。forEach 是一个最终操作,所以我们不能在 forEach 之后来执行其他 Stream 操作。

代码如下:

1
2
3
4
5
stringCollection
.stream()
.filter((s) -> s.startsWith("a"))
.forEach(System.out::println);
// "aaa2", "aaa1"

Sort 排序

排序是一个中间操作,返回的是排序好后的 Stream。如果你不指定一个自定义的 Comparator 则会使用默认排序。

代码如下:

1
2
3
4
5
6
7
8
stringCollection
.stream()
.sorted()
.filter((s) -> s.startsWith("a"))
.forEach(System.out::println);


// "aaa1", "aaa2"

需要注意的是,排序只创建了一个排列好后的 Stream,而不会影响原有的数据源,排序之后原数据 stringCollection 是不会被修改的:

代码如下:

1
2
System.out.println(stringCollection);
// ddd2, aaa2, bbb1, aaa1, bbb3, ccc, bbb2, ddd1

其实这也是函数式编程的一个好处:不会改变对象状态,每次都会创建一个新对象。

Map 映射

中间操作 map 会将元素根据指定的 Function 接口来依次将元素转成另外的对象,你也可以通过 map 来将对象转换成其他类型,
map 返回的 Stream 类型是根据 map 传递进去的函数的返回值决定的。下面的示例展示了将字符串转换为大写字符串:

代码如下:

1
2
3
4
5
6
7
8
stringCollection
.stream()
.map(String::toUpperCase)
.sorted((a, b) -> b.compareTo(a))
.forEach(System.out::println);


// "DDD2", "DDD1", "CCC", "BBB3", "BBB2", "AAA2", "AAA1"

Collectors

java8 快速实现List转map 、分组、过滤等操作

Match 匹配

Stream 提供了多种匹配操作,允许检测指定的 Predicate 是否匹配整个 Stream。所有的匹配操作都是最终操作,并返回一个 boolean 类型的值。

代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
boolean anyStartsWithA = 
stringCollection
.stream()
.anyMatch((s) -> s.startsWith("a"));


System.out.println(anyStartsWithA); // true

boolean allStartsWithA =
stringCollection
.stream()
.allMatch((s) -> s.startsWith("a"));

System.out.println(allStartsWithA); // false

boolean noneStartsWithZ =
stringCollection
.stream()
.noneMatch((s) -> s.startsWith("z"));

System.out.println(noneStartsWithZ); // true

Count 计数

计数是一个最终操作,返回 Stream 中元素的个数,返回值类型是 long。

代码如下:

1
2
3
4
5
6
7
long startsWithB = 
stringCollection
.stream()
.filter((s) -> s.startsWith("b"))
.count();

System.out.println(startsWithB); // 3

findFirst 返回集合的第一个对象

findAny 返回流计算中第一个计算完成的集合中的任何一个对象

在串行流中,findAny 和 findFirst 返回的都是第一个对象;而在并行流中,findAny 返回的是最快处理完的那个线程的数据。所以说,在并行操作中,对数据没有顺序上的要求,那么 findAny 的效率会比 findFirst 要快。

Java8 新特性 - CompletableFuture

分享到:
Disqus 加载中...

如果长时间无法加载,请针对 disq.us | disquscdn.com | disqus.com 启用代理