JDK8 新特性

一、接口的默认方法实现

在 JDK8 中,接口允许有默认的实现方法,这些默认的实现方法需要使用 default 关键字修饰即可,同时其实现类可直接调用这些方法;代码示例如下:

1、自定义接口

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public interface TestInterface {

// 接口方法
public void test();

// 接口默认方法1
default public void defaultMethod1(){
System.out.println("Hello World!");
}

// 接口默认方法2
default public void defaultMethod2(){
System.out.println("World Hello!");
}
}

2、实现类及测试

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class TestInterfaceImpl implements TestInterface {
@Override
public void test() {
System.out.println("Test 方法实现");
}

public static void main(String[] args) {
TestInterfaceImpl testInterface = new TestInterfaceImpl();
testInterface.test();

// default 修饰的默认方法可直接调用
testInterface.defaultMethod1();
testInterface.defaultMethod2();
}
}

3、默认方法的继承

同普通的类一样,接口的默认方法也可以被继承;而一旦被继承就会出现继承的顺序即优先级问题,接口的默认方法继承遵续以下几个原则:

  • 1、当某一个类实现了某一接口后,那么该类便继承了该接口的默认方法。
  • 2、当某一个类实现某一接口,同时又继承另一个类,而另一个类中有一方法和接口中的默认方法相同,那么最终以类中的方法为准,即 类中的方法优先级高于接口中的方法。
  • 3、当某一类实现了两个接口,而两个接口中又有同方法签名的默认方法时,必须显示在该类中覆盖两个接口中的同方法签名的方法,否则编译不通过。
  • 4、当某一类实现了两个接口,而其中一个接口的父接口,与另一个接口中的默认方法方法签名一致时,同样需要显示覆盖,否则编译报错。
  • 5、当某一类实现了两个接口,而其中一个接口的父接口,与另一个接口中的默认方法方法签名一致,同时该接口又覆盖了父类的默认方法,此时也必须显式覆盖该方法,否则编译报错。

3.1、第2中情况测试(1省略)

有一默认方法的接口

1
2
3
4
5
6
7
public interface TestIntefaceDefaultMethod {

// 接口中的默认 test 方法
default public void test(){
System.out.println("TestIntefaceDefaultMethod");
}
}

有一普通方法的父类

1
2
3
4
5
6
7
public class TestInterfaceDefaultMethodClazz {

// 类中的 test 方法
public void test(){
System.out.println("TestInterfaceDefaultMethodClazz");
}
}

测试类,实现并继承上述接口和类

1
2
3
4
5
6
7
8
9
// 继承一个类和一个接口
public class Test extends TestInterfaceDefaultMethodClazz implements TestIntefaceDefaultMethod{

public static void main(String[] args) {
// 打印 TestInterfaceDefaultMethodClazz
// 说明调用的是类中的 test 方法
new Test().test();
}
}

3.2、第3中情况测试

第一个有默认方法的接口

1
2
3
4
5
6
public interface TestIntefaceDefaultMethod1 {

default public void test(){
System.out.println("TestIntefaceDefaultMethod1");
}
}

第二个有默认方法的接口

1
2
3
4
5
6
public interface TestIntefaceDefaultMethod2 {

default public void test(){
System.out.println("TestIntefaceDefaultMethod2");
}
}

同时实现两个接口的测试类

1
2
3
4
5
6
7
8
9
10
11
12
13
public class TestExtendsInteface implements TestIntefaceDefaultMethod1,TestIntefaceDefaultMethod2{

// 必须显式覆盖 否则编译不通过
@Override
public void test() {
// 在这里显式的调用要使用 哪个实现接口的方法
TestIntefaceDefaultMethod1.super.test();
}

public static void main(String[] args) {
new TestExtendsInteface().test();
}
}

3.3、第4种情况测试

TestIntefaceDefaultMethod1、TestIntefaceDefaultMethod2 接口与上面相同,新增一个 TestIntefaceDefaultMethod3 接口,该接口继承自 TestIntefaceDefaultMethod1,并且是一个空的接口

1
2
3
public interface TestIntefaceDefaultMethod3 extends TestIntefaceDefaultMethod1{

}

测试类同时实现 2、3两个接口,其中3接口继承1接口,并且为空接口

1
2
3
4
5
6
7
8
9
10
11
12
13
public class TestExtendsInteface implements TestIntefaceDefaultMethod3,TestIntefaceDefaultMethod2{

// 同样必须显式覆盖 否则编译报错
@Override
public void test() {
// 在这里显式的调用要使用 哪个实现接口的方法
TestIntefaceDefaultMethod3.super.test();
}

public static void main(String[] args) {
new TestExtendsInteface().test();
}
}

3.4、第5种情况

TestIntefaceDefaultMethod3 覆盖了 TestIntefaceDefaultMethod1 的默认方法

1
2
3
4
5
6
7
public interface TestIntefaceDefaultMethod3 extends TestIntefaceDefaultMethod1{

@Override
default public void test(){
System.out.println("TestIntefaceDefaultMethod3");
}
}

测试类

1
2
3
4
5
6
7
8
9
10
11
12
public class TestExtendsInteface implements TestIntefaceDefaultMethod3,TestIntefaceDefaultMethod2{

// 同样必须显式覆盖
@Override
public void test() {
System.out.println("TestExtendsInteface");
}

public static void main(String[] args) {
new TestExtendsInteface().test();
}
}

3.5、总结

单接口实现情况下,默认方法可以直接用,多接口实现情况下一旦出现同方法签名的默认方法,那么必须显式覆盖,否则编译不通过。

二、接口的静态方法

JDK 8 中的接口允许使用静态的方法实现,如下所示:

静态方法接口

1
2
3
4
5
6
public interface TestStaticInterface {
// 静态方法
public static void testStaticMethod(){
System.out.println("JDK8 Static Method!");
}
}

测试类

1
2
3
4
5
6
7
8
9
public class TestStaticInterfaceImpl {

public static void main(String[] args) {

// 直接调用
TestStaticInterface.testStaticMethod();

}
}

三、Lambda 表达式

Lambda 表达式是 JDK8 新增的重要特性,Lambda 使 Java 具有了类似函数式编程的风格,其实 Lambda 表达式也是一个 “语法糖”,其实质也是由编译器根据表达式推断最终生成原始语法的字节码方式。

1、基本语法

Lambda 基本语法以 (argument) -> (body) 表示,以下为具体的一些例子:

1
2
3
4
5
6
7
8
// 不需要参数,返回值为5
() -> 5
// 接受一个 String 型的参数,返回值为 字符串+Hello World
(String str) -> str+" Hello World"
// 接受两个 int 型的参数,返回两数之和
(int a,int b) -> a+b
// 接受一个 String 型的参数,并将其打印到控制台,不反悔任何参数(void)
(String str) -> System.out.println(str)

2、Lambda 表达式结构

  • 一个 Lambda 可以有零个或者多个参数;
  • 参数类型可以声明,也可以由上下文推断,如 (int a)(a) 效果一致;
  • 所有参数包含在圆括号内,参数之间用分号分隔,如 (int a,int b)(a,b);
  • 空圆括号代表参数集为空,如 () -> 2
  • 当只有一个参数,且类型可被推倒时,圆括号可省略,如 a -> a+1
  • Lambda 表达式主体可包含一条或多条语句;
  • 如果 Lamdba 表达式主体部分只有一条语句,那么花括号可以省略,同时匿名函数返回类型与该语句返回类型一致,如 (int a,int b) -> return a+b
  • 如果 Lambda 表达式主体部分含有多条语句,则必须使用花括号,此时匿名函数返回类型与该代码块返回一致,没有返回则为空。

3、Lambda 表达式原理(函数式接口)

在 Java 中有两种描述层面的特殊接口:

Marker(标记) 接口:这种接口内部不定义任何待实现的抽象方法,就像他的名字那样,只用作标记一个类,从而使其能够通过 instanceof 关键字检查。

Function(函数) 接口:与标记接口不同的是,这种接口内部内有一个待实现的抽象方法,此种接口的作用是提供函数式编程需要,例如为 Lambda 表达式提供便利。

其实每个 Lambda 表达式都隐式的赋值给了函数式接口,也就是说每个 Lambda 表达式其实都是函数式接口的内部抽象方法实现;从而我们便可以通过 Lambda 表达式来完成对函数式接口的简化操作,例如下面的 Runnable 接口:

1
Runnable runnable = () -> System.out.println("Hello World");

当不指明 Lambda 表达式是哪个函数式接口的实现时,编译器还可以自动根据构造器将 Lambda 赋值给对应的函数式接口,如下:

1
2
3
4
5
6
public class TestLambda {

public static void main(String[] args) {
new Thread(() -> System.out.println("Hello World!")).start();
}
}

以上代码,由于 Thread 类存在一个接受 Runnable 接口的构造器,而 Runnable 又是函数式接口,所以编译器自动推断将 Lambda 表达式隐式的赋值给了 Runnable 接口。

注意:根据本人测试,当某个类有两个有参构造器,且两个有参构造器入参全部为函数式接口,同时该函数式接口返回值相同时,Lambda 表达式必须强制转换为其中一种类型,因为编译器无法推断出 Lambda 表达式到底是实现的那种函数式接口,测试代码如下:

两个函数式接口:Interface1、Interface2

1
2
3
public interface Interface1 {
public void sayHello();
}
1
2
3
public interface Interface2 {
public void sayHello();
}

对应的使用类:TestLambdaInterface

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public class TestLambdaInterface {

// 两个有参构造器,参数都为函数式接口
public TestLambdaInterface (Interface1 interface1){
interface1.sayHello();
}

public TestLambdaInterface (Interface2 interface2){
interface2.sayHello();
}

public static void main(String[] args) {

// 直接这么干是无法通过编译的
// new TestLambdaInterface(() -> System.out.println("Hello World!"));

// 必须强制转换为某一具体接口类型
new TestLambdaInterface((Interface1)() -> System.out.println("Hello World!"));
}
}

4、FunctionalInterface 注解

JDK 8 新增了 FunctionalInterface 注解,该注解只能作用在接口上,用于标注该接口是一个函数式接口;从而显式的告诉 Lambda 表达式将其赋值到此接口,这个注解标注了函数式接口,同时也限定了该接口内只能有一个抽象方法,否则将无法编译通过,示例如下:

标注 @FunctionalInterface 注解的函数式接口

1
2
3
4
@FunctionalInterface
public interface TestLambdaAnnotation {
public void sayHello();
}

测试使用类

1
2
3
4
5
6
7
8
9
10
public class TestLambdaAnnotationImpl {

public TestLambdaAnnotationImpl(TestLambdaAnnotation lambdaAnnotation){

}

public static void main(String[] args) {
new TestLambdaAnnotationImpl(() -> System.out.println("Hello World"));
}
}

5、Lambda 表达式作用域

Lambda 表达式和原来的匿名内部类相似,但在作用域等方面还有些细微差别:

5.1、局部变量

局部变量不必使用 final 关键字修饰,即可在 Lambda 表达式中直接使用,但是此后的该变量不可改变,相当于隐式 final,如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class TestLocalVariables {

public static void main(String[] args) {

// 不必声明为 final
int a = 10;

// 直接使用
new Thread(() -> System.out.println(a)).start();

// 不可改变该变量,否则编译报错
// a = 20;
}
}

5.2、对象属性与静态字段

同匿名对象一样,Lambda 表达式对于对象的属性和静态属性既可读又可写,如下:

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 class TestField {

// 普通属性
String field = "Hello World";

// 静态属性
static int a = 10;

public void testScope() {


new Thread(() -> {

a =20;
System.out.println(a);

field = "Test Scope";
System.out.println(field);

}).start();
}

public static void main(String[] args) {
new TestField().testScope();
}
}

5.3、接口的默认方法

在普通的接口中,JDK8后新增的 default 关键字可实现让接口有其默认实现方法,外部接口对象不必实现,且可直接调用,然而在 Lambda 表达式中,不支持直接调用这些默认方法,如下:

1
2
3
4
5
6
7
8
9
10
11
public class TestDefaultMethod {

public static void main(String[] args) {

LambdaInterface lambdaInterface = () -> System.out.println("Hello");

// 以下代码无法通过编译
// LambdaInterface lambdaInterface = () -> testDeafaultMethod();

}
}

5.4、this 关键字

在内部类中,this 关键字指向当前内部类对象自己,而在 Lambda 表达式中,this 关键字指向的是 Lambda 表达式外部的类对象。

6、Lambda 例子

以下为一个利用 Lambda 表达式简写 forEach 的例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public class TestLambdaForEach {

public static void main(String[] args) {

// 首先创建一个待排序的 list
String str [] = {"a","b","c","d","e"};
List<String> list = Arrays.asList(str);

// 原始的 forEach 遍历
for (String strTmp:list) {
System.out.println(strTmp);
}

// Lambda 表达式遍历
list.forEach((String strTmp) -> System.out.println(strTmp));

}
}

除了 forEach 外还可以简写 Thread,如下:

1
2
3
4
5
public class TestLambdaThread {
public static void main(String[] args) {
new Thread(() -> System.out.println("Lambda Thread Test!")).start();
}
}

四、方法引用

Lambda 表达式提供了一种匿名方法实现,并允许我么以函数接口的形式使用它;对于已有的方法,JDK8 提供了 方法引用(Method references) 来实现同样的特性;方法引用并不需要提供方法体,而只需使用已有的方法名即可直接引用已存在的方法。

1、方法引用替代 Lambda 表达式

以下为一个使用方法引用来替代 Lambda 表达式的例子:

POJO

1
2
3
4
5
public class Persion {

private String name;
//省略 SET GET
}

一个函数接口

1
2
3
4
5
6
@FunctionalInterface
public interface TestInterface {

public String test(Persion persion);

}

测试类

1
2
3
4
5
6
7
8
9
10
11
12
13
public class TestMethodReferences {

public static void main(String[] args) {

// 使用 Lambda 表达式
TestInterface testInterface = (persion) -> persion.getName();

// 使用方法引用
TestInterface testInterface1 = Persion::getName;

}

}

2、方法引用的种类

JDK 8中方法引用大致分为以下6种:

  • 静态方法引用 ClassName::methodName
  • 实例上的实例方法引用 instanceReference::methodName
  • 超类上的实例方法引用 super::methodName
  • 类型上的实例方法引用 ClassName::methodName
  • 构造方法引用 Class::new
  • 数组构造方法引用 TypeName[]::new

3、方法引用与 Lambda 表达式

无论是方法引用还是 Lambda 表达式,其最终原理和所要实现的就是当某一个类中,或者接口中的某一方法,其入参为一个接口类型时,使用方法引用或者 Lambda 表达式可以快速而简洁的实现这个接口,而不必繁琐的创建一个这个接口的对象或者直接实现。

参考文章:

五、Stream API

JDK 8 带来了强大的流式 API,用于简化集合操作;流式(Stream) API 配合 Lambda 表达式和方法引用 “可变身超级英雄”。

1、Strean API 简介

新增的 Stream API 主要围绕 java.util.stream 包下的 Stream 类展开,Stream API 并非其语义上的 “流”,它不同于任何 I/O 流,实质上 Stream 可以看做是一个加强版 Iterator,不同的是 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
public class Test1 {

public static void main(String[] args) {

// 首先创建一个待处理的 List
List<Integer> list = new ArrayList<>();
list.add(1);
list.add(2);
list.add(3);
list.add(4);

// 传统的过滤掉元素

// 定义一个 ArrayList 用于存放结果
List resultList1 = new ArrayList<>();

// 循环遍历
for (int tmp:list) {
if (tmp<4){
resultList1.add(tmp);
}
}

System.out.println(resultList1);


// Stream API + Lambda 过滤

// 定义结果 List
List resultList2 = new ArrayList<>();

// Stream+Lambda 操作
list.stream().filter((tmp) -> tmp <4).forEach((result)-> resultList2.add(result));

System.out.println(resultList2);
}
}

2、Stream 语法

Stream 的大体语法如下:

hexo_java8_stream1

如上图所示,红色部分主要是创建一个 Stream,可以使从集合中创建或者选择其它方式,绿色部分代表对这个流的处理操作,可能是一个也可能是多个,比如连续 filter,每次操作后都可能返回处理后的流对象以便下次重复操作;最后的蓝色部分代表最终的 “汇聚” 动作,实质上中间部分对流做的各种动作不会立即执行,因为流不可逆性,无法重复操作,所以只有到最后的汇聚动作时才会一次执行到底。≈

3、创建 Stream

创建一个 Stream 可以通过两种方式,一种是通过其静态工厂方法创建,另一种通过 Collection 接口默认方法创建;

3.1、静态工厂创建 Stream

Stream 类中提供了三个静态工厂方法 Stream.of()Stream.generate()Stream.iterate() 来创建 Stream,其中 Stream.of() 有两个重载,一个接受固定长度的参数,一个接受可变长度的参数.

Stream.of() 创建 Stream

1
2
3
4
5
// 固定长度参数
Stream<String> stream1 = Stream.of("aa");

// 可变长度参数
Stream<Integer> stream2 = Stream.of(1,2,3,4,5);

Stream.generate() 创建 Stream

Stream.generate() 方法用于生成一个无限长度的 Stream,其接受一个 Supplier 接口作为入参,该接口可看作是 Stream 内部元素的对象工厂,每次调用必须返回一个对象实例,测试如下:

1
Stream stream3 = Stream.generate(() -> new Object());

Stream.iterate() 创建 Stream

该方法同样用于创建一个无限长度的 Stream,不同的是 iterate() 方法接受两个参数,第一个参数可看作是一个种子,第二个参数可看作是一个方法,每次都会使用种子重复的执行方法,如下:

1
2
// 此代码将一直执行
Stream.iterate(1,(i) -> i++).forEach((i)-> System.out.println(i));

3.2、Collection 创建 Stream

Collection 集合类的父接口提供了一个默认的方法 ConllectionInstance.stream() 来实现创建 Stream,所以直接使用或使用任意的 Collection 实现类即可创建 Stream,样例代码如下:

1
2
// 使用 ArrayList 创建
Stream stream = new ArrayList().stream();

4、转换Stream

对 Stream 每次操作后其实都会对其进行转换,从而返回一个新的 Stream,Stream 中定义了几个常用的转换方法如下:

4.1、distinct 方法

顾名思义,此方法将对 Stream 中的内容进行去重操作,注意,此操作依赖于 元素的 equals 方法,所以在对象去重时请重写 equals 和 hashcode 方法。此操作后新生成的 Stream 没有重复的元素,以下为测试代码:

1
2
3
4
5
6
7
8
9
10
11
// 创建一个 存放元素的 ArrayList
List<String> list = new ArrayList<>();

list.add("a");
list.add("a");
list.add("b");

// 去重后的 list
List list1 = list.stream().distinct().collect(Collectors.toList());

System.out.println(list1);

4.2、filter 方法

同样,从名字可以看出这是个过滤操作,通过 filter 方法可以过滤流,并返回一个新的 Stream,其中 filter 方法入参接受一个 Predicate 函数接口,可使用 Lambda 实现该函数接口,该接口主要作用是需要我们实现一个过滤条件;测试如下:

1
2
3
4
5
6
7
8
9
10
11
//创建一个元素 List
List<Integer> list = new ArrayList<>();

list.add(1);
list.add(2);
list.add(3);

// 创建 Stream 并进行过滤
List<Integer> list1 = list.stream().filter((i) -> i>1).collect(Collectors.toList());

System.out.println(list1);

4.3、map 方法

该方法从名字上看似乎与 Map 有关,实质上该方法主要作用是将 Stream 中的数据按照某种给定的转换策略进行转换,比如将 Stream 中的 字符串全部转化为 Integer 类型,map 方法同样接受一个函数接口,该接口需要我们实现为具体的转换策略,测试代码如下:

1
2
3
4
5
6
7
8
9
10
11
// 创建一个待转换的 List
List<String> list = new ArrayList<>();

list.add("1");
list.add("2");
list.add("3");

// 获取 Stream 并转换
List<Integer> list1 = list.stream().map((str) -> Integer.parseInt(str)).collect(Collectors.toList());

System.out.println(list1);

map 方法有其精简版本,如 mapToInt,mapToLong和mapToDouble,从名字可以知道其作用。

4.4、flatMap 方法

flatMap 方法 和 map 方法类似,同样用于结果转换,但不同的是 flatMap 可以处理返回值为 Stream 的处理,如下面的样例:

使用 map 的处理情况

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
public class TestFlatMap {

public static void main(String[] args) {

// 创建元素集合
List<String> list = new ArrayList<>();

list.add("123");
list.add("456");
list.add("789");

// 创建流并转换 此处将返回一个套嵌流
Stream<Stream<Character>> stream = list.stream().map(TestFlatMap::toCharterStream);

// 获取这个 套嵌流中所有内容
stream.forEach((streamTmp) -> streamTmp.forEach((c) -> System.out.println(c)));

}


// 字符串分解 并返回 Charcater 的 Stream
public static Stream<Character> toCharterStream(String s){

// 缓存结果
List<Character> list = new ArrayList<>();

for (char c: s.toCharArray()) {
// 将每一个字符串分解为 char 放入缓存结果中
list.add(Character.valueOf(c));
}

// 返回 一个 Character 的 Stream
return list.stream();
}
}

从上面可以看出,当转换方法返回另一个流即套嵌流 Stream<Stream> 时,我们想获取 Stream 中所有内容极其困难和复杂,而 flatMap 方法很好的解决了这个问题:

flatmap 实现

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
public class TestFlatMap1 {


public static void main(String[] args) {

// 创建元素集合
List<String> list = new ArrayList<>();

list.add("123");
list.add("456");
list.add("789");


// 注意 此处套嵌流自动被合并
Stream<Character> stream = list.stream().flatMap(TestFlatMap1::toCharterStream);

stream.forEach((c) -> System.out.println(c));

}


// 字符串分解 并返回 Charcater 的 Stream
public static Stream<Character> toCharterStream(String s) {

// 缓存结果
List<Character> list = new ArrayList<>();

for (char c : s.toCharArray()) {
// 将每一个字符串分解为 char 放入缓存结果中
list.add(Character.valueOf(c));
}

// 返回 一个 Character 的 Stream
return list.stream();
}
}

总结:map 方法在实现的转换方法返回 Stream 时,会形成套嵌 Stream<Stream>,当要遍历时极其困难,而 flatMap 方法则会自动合并套嵌流。

4.5、peek 方法

peek 方法用于对 Stream 内元素进行包装,即在外面包装一层我们自定义的方法,当 Stream 内元素被消费(处理/转化)时,则触发该方法执行,如下测试例子:

1
2
3
4
5
6
7
8
9
10
11
public class TestPeek {

public static void main(String[] args) {

// peek 包装时自定义了一个 输出方法,当每次 map 方法对元素转换(消费)时,都会触发 该方法执行
Stream.of("1","2","3")
.peek((s) -> System.out.println("元素被消费: "+s))
.map((s) -> Integer.parseInt(s))
.collect(Collectors.toList());
}
}

4.6、limit 方法

顾名思义,此方法将对 Stream 内元素进行截断操作,返回的 Stream 中包含截断后的元素,注意此方法向后截断,即语义为 “保留前面的 N 个元素”;测试如下:

1
2
3
4
5
// 截断操作
List<Integer> list = Stream.of(1,2,3,4,5).limit(3).collect(Collectors.toList());

// 打印 [1, 2, 3]
System.out.println(list);

4.7、skip 方法

skip 方法与 limit 正好相反,语义为:丢弃前 N 个元素;测试如下:

1
2
// 打印 3,4,5
Stream.of(1,2,3,4,5).skip(2).forEach((i)-> System.out.println(i));

5、汇聚(折叠)

汇聚也成折叠,当对一个流进行各种操作后(filter、map等),实际上该流并不会立即被处理,因为流只能流向性的一次走完,只有在最后进行汇聚(折叠)动作时,该流才会一直被转换/处理完成并最终返回一个新的流。

汇聚一般分为两种:

可变汇聚:把输入的元素累积到一个可变容器中,比如 Collection或者 StringBuilder。
其他汇聚:其他汇聚即除了可变汇聚之外的都称作其他汇聚,一般这些汇聚都是反复操作结果流,并通过前一次的结果流当做下一次的入参,最终形成一个结果,如 reduce,count,allMatch等。

5.1、可变汇聚

可变汇聚对应的只有一个方法 Stream.collect(),该方法将流中处理后的元素汇聚到一个 容器中,比如 Collection注意,Stream API 中对流处理操作并不会改变原有容器,比图 List.stream() 返回的流无论怎么处理,原来的 List 都不会受影响。

5.1.1、默认的 collect 方法

查看 collect 方法如下:

1
2
3
<R> R collect(Supplier<R> supplier,
BiConsumer<R, ? super T> accumulator,
BiConsumer<R, R> combiner);

Supplier supplier是一个工厂函数,用来生成一个新的容器;BiConsumer accumulator也是一个函数,用来把Stream中的元素添加到结果容器中;BiConsumer combiner还是一个函数,用来把中间状态的多个结果容器合并成为一个(并发的时候会用到),下面举个栗子:

1
2
3
4
5
6
List result = Stream.of(1,2,3,4).filter((i) -> i>1).
collect(() -> new ArrayList(),
(list,i) -> list.add(i),
(listAll,list) -> listAll.addAll(list));

System.out.println(result);

第一个参数用于生成一个容器(ArrayList),第二个参数用于将 Stream 中的元素加入到前面生成的容器(list)中,第三个参数在并发时,用于将

5.1.2、collect 简化版本

clooect 还有一个 override 版本,如下:

1
<R, A> R collect(Collector<? super T, A, R> collector);

从上面可以看出,该方法接受一个 Collector 接口,JDK 8 为我们提供了快速创建该接口的工具类 java.util.stream.Collectors,其中调用该工具类的方法可以快速创建 Collector 接口的实力,典型的方法有 Collectors.toList()Collectors.toSet()Collectors.toCollection() 等等,具体可自行查看源码,从名字也很容易看出其用途,如下是一个汇聚到 List 的代码示例:

1
List result = Stream.of(1,2,3,4).filter((i) -> i>1).collect(Collectors.toList());

5.2、其他汇聚

5.2.1、reduce 方法

reduce 方法用于迭代累加操作,该方法有三种 override 版本,第一种结构如下:

1
Optional<T> reduce(BinaryOperator<T> accumulator);

同样该方法接受一个 BinaryOperator 接口对象,BinaryOperator 接口对象用于实现 “累加动作”,即以何种方式累加;该接口接受两个参数,第一个参数为上次操作结果的临时变量,第二个参数为 Stream 中的元素,测试代码如下:

1
2
3
4
5
6
7
8
// int 累加
int test1 = Stream.of(1, 2, 3, 4).filter((i) -> i > 1).reduce((result, filed) -> result + filed).get();

// String 累加
String test2 = Stream.of("He","llo"," ","Wo","rld").reduce((result,filed) -> result+filed).get();

System.out.println(test1);
System.out.println(test2);

该方法的第二种结构如下:

1
T reduce(T identity, BinaryOperator<T> accumulator);

注意,该版本返回值直接就是一个具体的数据类型,这个数据类型可以根据传入的初始值 identity 进行推断出来。

此时第一个参数为一个初始值,后面的累加操作基于这个初始值,如果 Stream 为空那么将直接返回初始值,第二个参数同样为累加算法,测试代码如下:

1
2
3
4
// 带有初始值的累加
String test3 = Stream.of("llo"," ","Wo","rld").reduce("He",(result,filed) -> result+filed);

System.out.println(test3);
5.2.2、count 方法

count 方法用于统计元素个数,测试代码如下:

1
2
3
// 注意返回值为long 类型
long count = Stream.of(1,2,3,4,5).count();
System.out.println(count);
5.2.3、其他方法
  • allMatch 方法用于检测结果中是否都满足某一条件,测试如下:
1
2
boolean flag = Stream.of(1,2,3,4,5).allMatch((i) -> i>0);
System.out.println(flag);

其他方法基本一致,代码就不写了

  • anyMatch:Stream中是否存在任何一个元素满足匹配条件
  • findFirst:返回Stream中的第一个元素,如果Stream为空,返回空Optional
  • noneMatch:是不是Stream中的所有元素都不满足给定的匹配条件
  • max和min:使用给定的比较器(Operator),返回Stream中的最大/最小值。

Stream API 参考自 Java8初体验(二)Stream语法详解


JDK8 新特性
https://mritd.com/2016/04/11/jdk8-new-features/
作者
Kovacs
发布于
2016年4月11日
许可协议