Lambda表达式

介绍Lambda表示

最初我们学习线程时, 是以下面方法创建并开启线程的

1
2
3
4
5
6
7
8
9
10
11
12
13
class MyThread implements Runnable {
@Override
public void run() {
System.out.println("hello world");
}
}

@Test
public void test() {
MyThread myThread = new MyThread();
Thread thread = new Thread(myThread);
thread.start();
}

后来我们学会了匿名内部类, 代码精简了很多, 但还是很冗余

1
2
3
4
5
6
7
8
9
@Test
public void test1() {
new Thread(new Runnable() {
@Override
public void run() {
System.out.println("hello world");
}
}).start();
}

现在我们用标准格式的Lambda表达式来实现

1
2
3
4
5
6
@Test
public void test3() {
new Thread(() -> {
System.out.println("hello, world");
}).start();
}

对上面的例子可以分析出几点内容:

  1. Thread 类需要 Runnable 接口作为参数,其中的抽象 run 方法是用来指定线程任务内容的核心

  2. 为了指定 run 的方法体,不得不需要 Runnable 接口的实现类

  3. 为了省去定义一个 Runnable 实现类的麻烦,不得不使用匿名内部类

  4. 必须覆盖重写抽象 run 方法,所以方法名称、方法参数、方法返回值不得不再写一遍,且不能写错

  5. 而实际上,似乎只有方法体才是关键所在。

  6. Lambda表达式简化了匿名内部类的使用,语法更加简单。

Lambda的标准格式

Lambda省去面向对象的条条框框,Lambda的标准格式格式由3个部分组成

1
2
3
(参数类型 参数名称) -> {
代码体;
}

格式说明:

  • (参数类型 参数名称) :参数列表

  • {代码体;} :方法体

  • -> :箭头,分隔参数列表和方法体

Lambda与匿名内部类的对比

从匿名内部类改成Lambda表达式可以总结为

  1. 去掉new和接口名().

  2. 去掉方法名, 只剩下 (参数类型 参数1, 参数类型 参数2)

  3. 最后加个 -> { 方法体 }

    一开始不熟悉可以先把匿名表达式写出来, 再改成Lambda表达式.

无参数无返回值

匿名内部类

1
2
3
4
5
6
7
8
9
10
@Test
public void test4() {
Runnable r1 = new Runnable() {
@Override
public void run() {
System.out.println("我是匿名内部类");
}
};
r1.run();
}

Lambda

1
2
3
4
5
6
7
@Test
public void test5() {
Runnable r2 = () -> {
System.out.println("我是Lambda");
};
r2.run();
}

有参数有返回值

例一

我们先新建一个Person类, 有name, age, height三个属性, 再创建一个有参构造器, 最后在重写toString方法.

匿名内部类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
@Test
public void test5() {
ArrayList<Person> persons = new ArrayList<>();
persons.add(new Person("刘德华", 58, 174));
persons.add(new Person("张学友", 58, 176));
persons.add(new Person("刘德华", 54, 171));
persons.add(new Person("黎明", 53, 178));
Collections.sort(persons, new Comparator<Person>() {
@Override
public int compare(Person o1, Person o2) {
return o1.getAge() - o2.getAge(); //按年龄由低到高
}
});
for (Person person : persons) {
System.out.println(person);
}
}

Lambda表达式

1
2
3
Collections.sort(persons, (Person o1, Person o2) -> {
return o2.getAge() - o1.getAge();
});

例二

匿名内部类

1
2
3
4
5
6
7
8
9
10
@Test
public void test6() {
List<Integer> list = Arrays.asList(11, 22, 33, 44);
list.forEach(new Consumer<Integer>() {
@Override
public void accept(Integer integer) {
System.out.println(integer);
}
});
}

Lambda表达式

1
2
3
4
List<Integer> list = Arrays.asList(11, 22, 33, 44);
list.forEach((Integer integer) -> {
System.out.println(integer);
});

Lambda的省略格式

在Lambda标准格式的基础上,使用省略写法的规则为:

  1. 小括号内参数的类型可以省略

  2. 如果小括号内有且仅有一个参数,则小括号可以省略

  3. 如果大括号内有且仅有一个语句,可以同时省略大括号、return关键字及语句分号

标准格式

1
2
3
(int a) -> {
return new Person();
}

省略格式

1
a -> new Person()

无参数无返回值

无参数, ()不能省略, 可以省略{}

1
2
3
4
5
6
7
8
9
//标准格式
Runnable r2 = () -> {
System.out.println("我是Lambda标准格式");
};
r2.run();

//省略格式
Runnable r3 = () ->System.out.println("我是Lambda省略格式");
r3.run();

一个参数一条语句

一个参数, ()可以省略, 只有方法体只有一条语句, 可以省略{}

1
2
3
4
5
6
7
List<Integer> list = Arrays.asList(11, 22, 33, 44);
//标准格式
list.forEach((Integer integer) -> {
System.out.println(integer);
});
//省略格式
list.forEach(integer -> System.out.println(integer));

多个参数一条return

多个参数, ()不能省, 方法体只有一条 return 语句, 可以省略 {}return

1
2
3
4
5
6
//标准格式
Collections.sort(persons, (Person o1, Person o2) -> {
return o2.getAge() - o1.getAge();
});
//省略格式
Collections.sort(persons, (o1, o2) -> o1.getAge() - o2.getAge());

Lambda的前提条件

Lambda的语法非常简洁,但是Lambda表达式不是随便使用的,使用时有几个条件要特别注意:

  1. 方法的参数或局部变量类型必须为接口才能使用Lambda

  2. 接口中有且仅有 一个 抽象方法

我们可以先新建一个接口Swimming, 里面目前只有一个方法.

1
2
3
public interface Swimming {
void goSwimming();
}

接着我们定义一个参数列表是Swimming接口的函数test7

1
2
3
public static void test7(Swimming swimming) {
swimming.goSwimming();
}

然后在main函数中调用test7

1
2
3
public static void main(String[] args) {
test7(() -> System.out.println("我去游泳了"));
}

运行结果如下

此时我们在Swimming接口中加入一个抽象方法run

1
2
3
4
public interface Swimming {
void goSwimming();
void goRunning();
}

main函数马上爆红

函数式接口

函数式接口在Java中是指:有且仅有一个抽象方法的接口。

函数式接口,即适用于函数式编程场景的接口。而Java中的函数式编程体现就是Lambda,所以函数式接口就是可以适用于Lambda使用的接口。只有确保接口中有且仅有一个抽象方法,Java中的Lambda才能顺利地进行推导。

FunctionalInterface注解

与 @Override 注解的作用类似,Java 8中专门为函数式接口引入了一个新的注解: @FunctionalInterface 。

该注解可用于一个接口的定义上:

1
2
3
4
@FunctionalInterface
public interface Operator {
void myMethod();
}

一旦使用该注解来定义接口,编译器将会强制检查该接口是否确实有且仅有一个抽象方法,否则将会报错。不过,即使不使用该注解,只要满足函数式接口的定义,这仍然是一个函数式接口,使用起来都一样

了解Lambda的实现原理

我们现在已经会使用Lambda表达式了。那么Lambda到底是如何实现的,现在我们就来探究Lambda表达式的底层实现原理。

准备工作

定义一个函数式接口

1
2
3
4
@FunctionalInterface
interface Swimmable {
public abstract void swimming();
}

定义函数, 参数是接口

1
2
3
4
5
public class Demo04LambdaImpl {
public static void goSwimming(Swimmable swimmable) {
swimmable.swimming();
}
}

反编译匿名内部类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class Demo04LambdaImpl {
public static void main(String[] args) {
goSwimming(new Swimmable() {
@Override
public void swimming() {
System.out.println("使用匿名内部类实现游泳");
}
});
}

public static void goSwimming(Swimmable swimmable) {
swimmable.swimming();
}
}

我们可以看到匿名内部类会在编译后产生一个类: Demo04LambdaImpl$1.class

使用XJad反编译这个类,得到如下代码

1
2
3
4
5
6
7
8
9
10
11
12
package com.itheima.demo01lambda;
import java.io.PrintStream;
// Referenced classes of package com.itheima.demo01lambda:
// Swimmable, Demo04LambdaImpl
static class Demo04LambdaImpl$1 implements Swimmable {
public void swimming()
{
System.out.println("使用匿名内部类实现游泳");
}
Demo04LambdaImpl$1() {
}
}

反编译Lambda

Lambda和匿名内部类对比

  1. 所需的类型不一样

    ​ 匿名内部类,需要的类型可以是类,抽象类,接口

    ​ Lambda表达式,需要的类型必须是接口

  2. 抽象方法的数量不一样

    ​ 匿名内部类所需的接口中抽象方法的数量随意

    ​ Lambda表达式所需的接口只能有一个抽象方法

  3. 实现原理不同

    ​ 匿名内部类是在编译后会形成class

    ​ Lambda表达式是在程序运行的时候动态生成class

JDK 8接口新增的两个方法

JDK 8接口增强介绍

JDK 8以前的接口

1
2
3
4
interface 接口名 {
静态常量;
抽象方法;
}

JDK 8对接口的增强,接口还可以有默认方法静态方法

JDK 8的接口

1
2
3
4
5
6
interface 接口名 {    
静态常量;
抽象方法;
默认方法;
静态方法;
}

接口默认方法

接口引入默认方法的背景

在JDK 8以前接口中只能有抽象方法。存在以下问题:

如果给接口新增抽象方法,所有实现类都必须重写这个抽象方法。不利于接口的扩展.

例如,JDK 8 时在Map接口中增加了forEach方法, 如果所有的实现类都需要去实现这个方法,那么工程量时巨大的。

因此,在JDK 8时为接口新增了默认方法,效果如下

1
2
3
4
5
6
public interface Map<K, V> {
...
default void forEach(BiConsumer<? super K, ? super V> action) {
...
}
}

接口中的默认方法实现类不必重写,可以直接使用,实现类也可以根据需要重写。这样就方便接口的扩展。

接口默认方法的定义格式

1
2
3
4
5
interface 接口名 {    
修饰符 default 返回值类型 方法名() {
代码;
}
}

接口默认方法的使用

定义一个接口, 其中test1是抽象方法, test2是默认方法, test1实现类必须实现, test2可以重写也可以直接使用

1
2
3
4
5
6
interface AA {
public abstract void test1();
public default void test02() {
System.out.println("AA test02");
}
}

定义两个实现类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
//只实现test1, 不重写test2
class BB implements AA {
@Override
public void test1() {
System.out.println("BB test1");
}
}

//test1, test2都重写
class CC implements AA {
@Override
public void test1() {
System.out.println("CC test1");
}
//实现类重写接口默认方法
@Override
public void test02() {
System.out.println("CC实现类重写接口默认方法");
}
}

调用两个实现类

1
2
3
4
5
6
7
// 实现类直接调用接口默认方法
BB b = new BB();
b.test02();

// 调用实现类重写接口默认方法
CC c = new CC();
c.test02();

接口静态方法

为了方便接口扩展,JDK 8为接口新增了静态方法。静态方法不能重写!!!

接口静态方法的定义格式

1
2
3
4
5
interface 接口名 {
修饰符 static 返回值类型 方法名() {
代码;
}
}

xxxxxxxxxx /**​ * 关掉tcp连接​ */​xnet_err_t xtcp_close(xtcp_t *tcp) {​    tcp_free(tcp);​    return XNET_ERR_OK;​}c

直接使用接口名调用即可:接口名.静态方法名()

不能重写!!!!

定义一个有静态方法的接口

1
2
3
4
5
interface AAA {
public static void test01() {
System.out.println("AAA 接口的静态方法");
}
}

调用

1
2
3
4
public static void main(String[] args) {
// 直接使用接口名调用即可:接口名.静态方法名();
AAA.test01();
}

接口默认方法和静态方法的区别

  1. 默认方法通过实例调用,静态方法通过接口名调用。

  2. 默认方法可以被继承,实现类可以直接使用接口默认方法,也可以重写接口默认方法。

  3. 静态方法不能被继承,实现类不能重写接口静态方法,只能使用接口名调用

如何选择呢?如果这个方法需要被实现类继承或重写,使用默认方法,如果接口中的方法不需要被继承就使用静态方

常用内置函数式接口

内置函数式接口来由来

我们知道使用Lambda表达式的前提是需要有函数式接口。而Lambda使用时不关心接口名,抽象方法名,只关心抽象方法的参数列表和返回值类型。因此为了让我们使用Lambda方便,JDK提供了大量常用的函数式接口。

常用内置函数式接口介绍

它们主要在 java.util.function 包中。下面是最常用的几个接口。

Supplier接口

Supplier意味着”供给” , 对应的Lambda表达式需要“对外提供”一个符合泛型类型的对象数据。

供给型接口,通过Supplier接口中的get方法可以得到一个值,无参有返回的接口。

1
2
3
4
@FunctionalInterface
public interface Supplier<T> {
public abstract T get();
}

使用Lambda表达式返回数组元素最大值

使用 Supplier 接口作为方法参数类型,通过Lambda表达式求出 int 数组中的最大值,接口的泛型使用java.lang.Integer类。

1
2
3
4
5
6
7
8
9
10
11
12
13
public void printMax(Supplier<Integer> supplier) {
Integer integer = supplier.get();
System.out.println(integer);
}

@Test
public void test8() {
printMax(() -> {
int[] arr = {5, 7, 4, 10};
Arrays.sort(arr);
return arr[arr.length - 1]; //返回排序后的数组的最后一个元素, 即最大值
});
}

最后输出:10.

分析下过程:

  1. printMax方法需要传入一个Supplier<Integer>类型的参数, 并且调用了接口中的get方法得到了一个参数并将其打印出来.
  2. test8方法调用了printMax方法, 在括号中用Lambda表达式重写了接口的get方法, 重写的结果是返回一个数组中的最大值.
  3. 相当于integer通过重写的方法得到了数组里的最大值.
  4. 接下来执行打印语句, 将最大值打印出来.

Consumer接口

1
2
3
4
@FunctionalInterface
public interface Consumer<T> {
public abstract void accept(T t);
}

Consumer接口是消费一个数据,其数据类型由泛型参数决定。

消费型接口,通过接口中的accept方法可以消费一个值,有参无返回的接口。

使用Lambda表达式将一个字符串转成大写和小写的字符串

Consumer消费型接口,可以拿到accept方法参数传递过来的数据进行处理, 有参无返回的接口(所以是accept有参数,)。基本使用如

1
2
3
4
5
6
7
public void conversion(Consumer<String> consumer) {
consumer.accept("HELLO WORLD");
}
@Test
public void test9() {
conversion(s -> System.out.println(s.toLowerCase()));
}

默认方法:andThen

如果一个方法的参数和返回值全都是 Consumer 类型,那么就可以实现效果:消费一个数据的时候,首先做一个操作,然后再做一个操作,实现组合。而这个方法就是 Consumer 接口中的default方法 andThen 。下面是JDK的源代码:

1
2
3
4
default Consumer<T> andThen(Consumer<? super T> after) {
Objects.requireNonNull(after);
return (T t) -> { accept(t); after.accept(t); };
}

备注: java.util.Objects 的 requireNonNull 静态方法将会在参数为null时主动抛出NullPointerException 异常。这省去了重复编写if语句和抛出空指针异常的麻烦。

要想实现组合,需要两个或多个Lambda表达式即可,而 andThen 的语义正是“一步接一步”操作。例如两个步骤组合的情况

1
2
3
4
5
6
7
8
9
10
11
12
public void conversion(Consumer<String> c1, Consumer<String> c2) {
String s = "HELLO WORLD";
//c1.accept(s);
//c2.accept(s);
//等于
c1.andThen(c2).accept(s);
}
@Test
public void test9() {
conversion(s -> System.out.println(s.toLowerCase()), s -> System.out.println(s.toUpperCase())
);
}

运行结果将会首先打印完全大写的HELLO,然后打印完全小写的hello。当然,通过链式写法可以实现更多步骤的组合。

Function接口

Function接口用来根据一个T类型的数据得到一个R类型的数据,前者称为前置条件,后者称为后置条件。有参数有返回值.

1
2
3
4
@FunctionalInterface
public interface Function<T, R> {
public abstract R apply(T t);
}

使用Lambda表达式将字符串转成数字

将 String 类型转换为 Integer 类型

1
2
3
4
5
6
7
8
public void stringToInt(Function<String, Integer> function) {
Integer apply = function.apply("1234");
System.out.println(apply + "+" + apply.getClass());
}
@Test
public void Test10() {
stringToInt(s -> Integer.parseInt(s));
}

输出结果是: 1234+class java.lang.Integer

默认方法:andThen

Function 接口中有一个默认的 andThen 方法,用来进行组合操作。JDK源代码如下

1
2
3
4
default <V> Function<V, R> compose(Function<? super V, ? extends T> before) {
Objects.requireNonNull(before);
return (V v) -> apply(before.apply(v));
}

该方法同样用于“先做什么,再做什么”的场景,和 Consumer 中的 andThen 差不多

例如:将字符串解析成数字, 将上一步得到的数字乘以10

1
2
3
4
5
6
7
8
9
public void handler(Function<String, Integer> fun1, Function<Integer, Integer> fun2) {
Integer result = fun1.andThen(fun2).apply("66");
System.out.println(result);
}

@Test
public void Test11() {
handler(s -> Integer.parseInt(s), s -> 10 * s);
}

输出: 660

Predicate接口

有时候我们需要对某种类型的数据进行判断,从而得到一个boolean值结果。这时可以使用java.util.function.Predicate<T> 接口。

1
2
3
4
@FunctionalInterface
public interface Predicate<T> {
public abstract boolean test(T t);
}

使用Lambda判断一个人名如果超过3个字就认为是很长的名字

1
2
3
4
5
6
7
8
9
10
public void judge(Predicate<String> predicate, String name) {
boolean result = predicate.test(name);
if(result) {
System.out.println("这是一个很长的名字");
}
}
@Test
public void test12() {
judge(s -> s.length() > 3, "迪丽冷巴");
}

默认方法:and

既然是条件判断,就会存在与、或、非三种常见的逻辑关系。其中将两个 Predicate 条件使用“与”逻辑连接起来实

现“并且”的效果时,可以使用default方法 and 。其JDK源码为

1
2
3
4
default Predicate<T> and(Predicate<? super T> other) {
Objects.requireNonNull(other);
return (t) -> test(t) && other.test(t);
}

使用Lambda表达式判断一个字符串中即包含W也包含H

1
2
3
4
5
6
7
8
9
10
public void wh(Predicate<String> p1, Predicate<String> p2) {
boolean result = p1.and(p2).test("HelloWorld");
if(result) {
System.out.println("该字符串包含W和H");
}
}
@Test
public void test13() {
wh(s -> s.contains("W"), s -> s.contains("H"));
}

默认方法:or

and 的“与”类似,默认方法 or 实现逻辑关系中的“”。JDK源码为

1
2
3
4
default Predicate<T> or(Predicate<? super T> other) {
Objects.requireNonNull(other);
return (t) -> test(t) || other.test(t);
}

使用Lambda表达式判断一个字符串中包含W或者包含H

代码只需要将and修改为or即可,其他都不变

1
2
3
4
5
6
7
8
9
10
public void wh(Predicate<String> p1, Predicate<String> p2) {
boolean result = p1.or(p2).test("HelloWorld");
if(result) {
System.out.println("该字符串包含W或H");
}
}
@Test
public void test13() {
wh(s -> s.contains("W"), s -> s.contains("H"));
}

默认方法:negate

negate方法是“非”(取反), JDK源代码为

1
2
3
default Predicate<T> negate() {
return (t) -> !test(t);
}

使用Lambda表达式判断一个字符串中不包含X

1
2
3
4
5
6
7
8
9
10
public void judge(Predicate<String> predicate) {
boolean result = predicate.negate().test("HelloWorld");
if(result) {
System.out.println("字符串里不包含X");
}
}
@Test
public void test12() {
judge(s -> s.contains("X"));
}

方法引用

方法引用简化Lambda

如果我们在Lambda中所指定的功能,已经有其他方法存在相同方案,那是否还有必要再写重复逻辑?可以直接“引用”过去就好了

下面举例了使用Lambda表达式和方法引用来对一个数组求和.

lambda表达式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public class Demo11MethodRefIntro {
public static void getMax(int[] arr) {
int sum = 0;
for (int n : arr) {
sum += n;
}
System.out.println(sum);
}

private static void printMax(Consumer<int[]> consumer, int[] arr) {
consumer.accept(arr);
}

public static void main(String[] args) {
int[] arr = {10, 20, 30, 40, 50};
printMax(a -> getMax(a), arr);
}

}

如果我们在Lambda中所指定的功能,已经有其他方法存在相同方案,那是否还有必要再写重复逻辑?可以直接“引用”过去就好了

方法引用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public class Demo11MethodRefIntro {
public static void getMax(int[] arr) {
int sum = 0;
for (int n : arr) {
sum += n;
}
System.out.println(sum);
}

private static void printMax(Consumer<int[]> consumer, int[] arr) {
consumer.accept(arr);
}

public static void main(String[] args) {
int[] arr = {10, 20, 30, 40, 50};
printMax(Demo11MethodRefIntro::getMax, arr);
}

}

请注意其中的双冒号::写法,这被称为“方法引用”,是一种新的语法.

方法引用的格式

符号表示 : ::

符号说明 : 双冒号为方法引用运算符,而它所在的表达式被称为方法引用

应用场景 : 如果Lambda所要实现的方案 , 已经有其他方法存在相同方案,那么则可以使用方法引用。

常见引用方式

方法引用在JDK 8中使用方式相当灵活,有以下几种形式:

  1. instanceName::methodName 对象::方法名

  2. ClassName::staticMethodName 类名::静态方法

  3. ClassName::methodName 类名::普通方法

  4. ClassName::new 类名::new 调用的构造器

  5. TypeName[]::new String[]::new 调用数组的构造器

对象名::引用成员方法

这是最常见的一种用法,与上例相同。如果一个类中已经存在了一个成员方法,则可以通过对象名引用成员方法

1
2
3
4
5
6
7
8
9
10
11
12
13
@Test
public void test01() {
Date now = new Date();
//Lambda表达式
Supplier<Long> supp = () -> {
return now.getTime();
};
System.out.println(supp.get());

//方法引用: 对象名::引用成员方法
Supplier<Long> supp2 = now::getTime;
System.out.println(supp2.get());
}

方法引用的注意事项

  1. 被引用的方法,参数要和接口中抽象方法的参数一样

  2. 当接口抽象方法有返回值时,被引用的方法也必须有返回值

类名::引用静态方法

由于在 java.lang.System 类中已经存在了静态方法currentTimeMillis,所以当我们需要通过Lambda来调用该

方法时,可以使用方法引用 .

1
2
3
4
5
6
7
8
9
10
11
12
@Test
public void test02() {
//Lambda表达式
Supplier<Long> supp = () -> {
return System.currentTimeMillis();
};
System.out.println(supp.get());

//方法引用: 类名::静态方法
Supplier<Long> supp2 = System::currentTimeMillis;
System.out.println(supp2.get());
}

类名::引用实例方法

Java面向对象中,类名只能调用静态方法,类名引用实例方法是有前提的,实际上是拿第一个参数作为方法的调用者。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
@Test
public void test03() {
//Lambda表达式
Function<String, Integer> f1 = (s) -> {
return s.length();
};
System.out.println(f1.apply("abc"));

//方法引用: 类名::引用实例方法
Function<String, Integer> f2 = String::length;
System.out.println(f2.apply("abc"));

//方法引用: 类名::引用实例方法
BiFunction<String, Integer, String> bif = String::substring;
String hello = bif.apply("hello", 2);
System.out.println("hello -> " + hello);
}

类名::new引用构造器

由于构造器的名称与类名完全一样。所以构造器引用使用 类名称::new 的格式表示。

首先定义一个简单得Person类

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
44
45
46
47
48
49
50
51
52
53
public class Person {
private String name;
private int age;
private int height;

public Person() {
}

public Person(String name, int age) {
this.name = name;
this.age = age;
}

public Person(String name, int age, int height) {
this.name = name;
this.age = age;
this.height = height;
}

public String getName() {
return name;
}

public void setName(String name) {
this.name = name;
}

public int getAge() {
return age;
}

public void setAge(int age) {
this.age = age;
}

public int getHeight() {
return height;
}

public void setHeight(int height) {
this.height = height;
}

@Override
public String toString() {
return "Person{" +
"name='" + name + '\'' +
", age=" + age +
", height=" + height +
'}';
}
}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
@Test
public void test04() {
//Lambda表达式
Supplier<Person> sup = () -> {
return new Person();
};
System.out.println(sup.get());

//方法引用 类名::new
Supplier<Person> sup2 = Person::new;
System.out.println(sup2.get());
BiFunction<String, Integer, Person> fun2 = Person::new;
System.out.println(fun2.apply("张三", 18));
}

数组::new引用数组构造器

数组也是 Object 的子类对象,所以同样具有构造器,只是语法稍有不同。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
@Test
public void test05() {
//Lambda表达式
Function<Integer, String[]> fun = (len) -> {
return new String[len];
};
String[] arr1 = fun.apply(10);
System.out.println(arr1 + ", " + arr1.length);

//方法引用: 类型[]::new
Function<Integer, String[]> fun2 = String[]::new;
String[] arr2 = fun.apply(5);
System.out.println(arr2 + ", " + arr2.length);
}

总结

方法引用是对Lambda表达式符合特定情况下的一种缩写,它使得我们的Lambda表达式更加的精简,也可以理解为

Lambda表达式的缩写形式 , 不过要注意的是方法引用只能”引用”已经存在的方法!

Stream流

Stream流式思想概述

注意:Stream和IO流(InputStream/OutputStream)没有任何关系,请暂时忘记对传统IO流的固有印象!

Stream流式思想类似于工厂车间的“生产流水线”,Stream流不是一种数据结构,不保存数据,而是对数据进行加工处理。Stream可以看作是流水线上的一个工序。在流水线上,通过多个工序让一个原材料加工成一个商品。

image-20221226135848883

image-20221226135922355

Stream API能让我们快速完成许多复杂的操作,如筛选、切片、映射、查找、去除重复,统计,匹配和归约。

获取Stream流的两种方式

java.util.stream.Stream 是JDK 8新加入的流接口。

获取一个流非常简单,有以下几种常用的方式:

  1. 所有的 Collection 集合都可以通过 stream 默认方法获取流;
  2. Stream 接口的静态方法 of 可以获取数组对应的流。

根据Collection获取流

首先, java.util.Collection 接口中加入了default方法 stream 用来获取流,所以其所有实现类均可获取流。

1
2
3
public interface Collection {
default Stream<E> stream();
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import java.util.*;
import java.util.stream.Stream;
public class Demo04GetStream {
public static void main(String[] args) {
// 集合获取流
// Collection接口中的方法: default Stream<E> stream() 获取流
List<String> list = new ArrayList<>();

Stream<String> stream1 = list.stream();
Set<String> set = new HashSet<>();

Stream<String> stream2 = set.stream();
Vector<String> vector = new Vector<>();

Stream<String> stream3 = vector.stream();
}
}

java.util.Map 接口不是 Collection 的子接口,所以获取对应的流需要分key、value或entry等情况:

1
2
3
4
5
6
7
8
9
10
11
12
13
import java.util.HashMap;
import java.util.Map;
import java.util.stream.Stream;
public class Demo05GetStream {
public static void main(String[] args) {
// Map获取流
Map<String, String> map = new HashMap<>();

Stream<String> keyStream = map.keySet().stream();
Stream<String> valueStream = map.values().stream();
Stream<Map.Entry<String, String>> entryStream = map.entrySet().stream();
}
}

Stream中的静态方法of获取流

由于数组对象不可能添加默认方法,所以 Stream 接口中提供了静态方法 of ,使用很简单:

!!!注意:基本数据类型的数组不行

1
2
3
4
5
6
7
8
9
10
11
12
13
14
import java.util.stream.Stream;
public class Demo06GetStream {
public static void main(String[] args) {
// Stream中的静态方法: static Stream of(T... values)
Stream<String> stream6 = Stream.of("aa", "bb", "cc");
String[] arr = {"aa", "bb", "cc"};
Stream<String> stream7 = Stream.of(arr);
Integer[] arr2 = {11, 22, 33};
Stream<Integer> stream8 = Stream.of(arr2);
// 注意:基本数据类型的数组不行
int[] arr3 = {11, 22, 33};
Stream<int[]> stream9 = Stream.of(arr3);
}
}

备注: of 方法的参数其实是一个可变参数,所以支持数组。

Stream常用API

Stream流模型的操作很丰富,这里介绍一些常用的API。这些方法可以被分成两种:

image-20221226141212208

  • 终结方法:返回值类型不再是 Stream 类型的方法,不再支持链式调用。本小节中,终结方法包括 count 和

forEach 方法。

  • 非终结方法:返回值类型仍然是 Stream 类型的方法,支持链式调用。(除了终结方法外,其余方法均为非终结

方法。)

备注:本小节之外的更多方法,请自行参考API文档。

Stream注意事项(重要)

  1. Stream只能操作一次

  2. Stream方法返回的是新的流

  3. Stream不调用终结方法,中间的操作不会执行

Stream流的forEach方法

forEach 用来遍历流中的数据

1
void forEach(Consumer<? super T> action);

该方法接收一个 Consumer 接口函数,会将每一个流元素交给该函数进行处理。例如:

1
2
3
4
5
6
7
8
9
10
11
@Test
public void testForEach() {
List<String> one = new ArrayList<>();
Collections.addAll(one, "迪丽热巴", "宋远桥", "苏星河", "老子", "庄子", "孙子");
/*one.stream().forEach((String s) -> {
System.out.println(s);
});*/
// 简写
// one.stream().forEach(s -> System.out.println(s));
one.stream().forEach(System.out::println);
}

Stream流的count方法

Stream流提供 count 方法来统计其中的元素个数

1
long count();

该方法返回一个long值代表元素个数。基本使用:

1
2
3
4
5
6
@Test
public void testCount() {
List<String> one = new ArrayList<>();
Collections.addAll(one, "迪丽热巴", "宋远桥", "苏星河", "老子", "庄子", "孙子");
System.out.println(one.stream().count());
}

Stream流的filter方法

filter用于过滤数据,返回符合过滤条件的数据, 可以通过 filter 方法将一个流转换成另一个子集流。方法声明:

1
Stream<T> filter(Predicate<? super T> predicate);

该接口接收一个 Predicate 函数式接口参数(可以是一个Lambda或方法引用)作为筛选条件。

Stream流中的filter方法基本使用的代码如:

1
2
3
4
5
6
@Test
public void testFilter() {
List<String> one = new ArrayList<>();
Collections.addAll(one, "迪丽热巴", "宋远桥", "苏星河", "老子", "庄子", "孙子");
one.stream().filter(s -> s.length() == 2).forEach(System.out::println); //打印出长度为2的名字
}

在这里通过Lambda表达式来指定了筛选的条件:姓名长度为2个字。

Stream流的limit方法

limit 方法可以对流进行截取,只取用前n个。方法:

1
Stream<T> limit(long maxSize);

参数是一个long型,如果集合当前长度大于参数则进行截取。否则不进行操作。基本使用:

1
2
3
4
5
6
@Test
public void testLimit() {
List<String> one = new ArrayList<>();
Collections.addAll(one, "迪丽热巴", "宋远桥", "苏星河", "老子", "庄子", "孙子");
one.stream().limit(3).forEach(System.out::println);
}

Stream流的skip方法

如果希望跳过前几个元素,可以使用 skip 方法获取一个截取之后的新流:

1
Stream<T> skip(long n);

如果流的当前长度大于n,则跳过前n个;否则将会得到一个长度为0的空流。基本使用:

1
2
3
4
5
6
@Test
public void testSkip() {
List<String> one = new ArrayList<>();
Collections.addAll(one, "迪丽热巴", "宋远桥", "苏星河", "老子", "庄子", "孙子");
one.stream().skip(2).forEach(System.out::println);
}

Stream流的map方法

如果需要将流中的元素映射到另一个流中,可以使用 map 方法。方法签名:

1
<R> Stream<R> map(Function<? super T, ? extends R> mapper);

该接口需要一个 Function 函数式接口参数,可以将当前流中的T类型数据转换为另一种R类型的流。

Stream流中的 map 方法基本使用的代码如:

1
2
3
4
5
6
@Test
public void testMap() {
Stream<String> original = Stream.of("11", "22", "33");
Stream<Integer> result = original.map(Integer::parseInt);
result.forEach(s -> System.out.println(s.getClass())); //结果为class java.lang.Integer
}

这段代码中, map 方法的参数通过方法引用,将字符串类型转换成为了int类型(并自动装箱为 Integer 类对象)。

Stream流的sorted方法

如果需要将数据排序,可以使用 sorted 方法。方法签名:

1
2
Stream<T> sorted();
Stream<T> sorted(Comparator<? super T> comparator);

Stream流中的 sorted 方法基本使用的代码如:

1
2
3
4
5
6
7
8
9
@Test
public void testSorted() {
// sorted(): 根据元素的自然顺序排序
// sorted(Comparator<? super T> comparator): 根据比较器指定的规则排序
Stream.of(33, 22, 11, 55)
.sorted() //11, 22, 33, 55
.sorted((o1, o2) -> o2 - o1) //55, 33, 22, 11
.forEach(System.out::println);
}

这段代码中, sorted 方法根据元素的自然顺序排序,也可以指定比较器排序。

Stream流的distinct方法

如果需要去除重复数据(保留第一次出现的元素, 去除后面重复出现的元素),可以使用 distinct 方法。方法签名:

1
Stream<T> distinct();

Stream流中的 distinct 方法基本使用的代码如:

1
2
3
4
5
6
@Test
public void testDistinct() {
Stream.of(22, 33, 22, 11, 33)
.distinct()
.forEach(System.out::println); //22, 33, 11
}

如果是自定义类型如何是否也能去除重复的数据呢?

1
2
3
4
5
6
7
8
9
10
@Test
public void testDistinct2() {
Stream.of(
new Person("刘德华", 58),
new Person("张学友", 56),
new Person("张学友", 56),
new Person("黎明", 52))
.distinct()
.forEach(System.out::println);
}

image-20221226150351035

结论: 自定义类型是根据对象的hashCodeequals来去除重复元素的。

Stream流的match方法

如果需要判断数据是否匹配指定的条件,可以使用 Match 相关方法。方法签名:

1
2
3
boolean allMatch(Predicate<? super T> predicate);     // allMatch: 元素是否全部满足条件
boolean anyMatch(Predicate<? super T> predicate); // anyMatch: 元素是否任意有一个满足条件
boolean noneMatch(Predicate<? super T> predicate); // noneMatch: 元素是否全部不满足条件

Stream流中的 Match 相关方法基本使用的代码如:

1
2
3
4
5
6
7
8
@Test
public void testMatch() {
boolean b = Stream.of(5, 3, 6, 1)
// .allMatch(e -> e > 0); // allMatch: 元素是否全部满足条件
// .anyMatch(e -> e > 5); // anyMatch: 元素是否任意有一个满足条件
.noneMatch(e -> e < 0); // noneMatch: 元素是否全部不满足条件
System.out.println("b = " + b);
}

Stream流的find方法

如果需要找到某些数据,可以使用 find 相关方法。方法签名:

1
2
Optional<T> findFirst();  //从流中获取一个元素(一般情况下,是获取的开头的元素)
Optional<T> findAny(); //从流中获取一个元素(一般情况下,是获取的开头的元素)

这两个方法,绝大部分情况下,是完全相同的,但是在多线程的环境下,findAnyfindFirst返回的结果
可能不一样。

Stream流中的 find 相关方法基本使用的代码如:

1
2
3
4
5
6
7
@Test
public void testFind() {
Optional<Integer> first = Stream.of(5, 3, 6, 1).findFirst();
System.out.println("first = " + first.get());
Optional<Integer> any = Stream.of(5, 3, 6, 1).findAny();
System.out.println("any = " + any.get());
}

Stream流的max和min方法

如果需要获取最大和最小值,可以使用 maxmin 方法。方法签名:

1
2
Optional<T> max(Comparator<? super T> comparator);
Optional<T> min(Comparator<? super T> comparator);

Stream流中的 maxmin 相关方法基本使用的代码如:

1
2
3
4
5
6
7
@Test
public void testMax_Min() {
Optional<Integer> max = Stream.of(5, 3, 6, 1).max((o1, o2) -> o1 - o2);
System.out.println("first = " + max.get()); //6
Optional<Integer> min = Stream.of(5, 3, 6, 1).min((o1, o2) -> o1 - o2);
System.out.println("any = " + min.get()); //1
}

Stream流的reduce方法

Reduce 原意:减少,缩小.

根据指定的计算模型将Stream中的值计算得到一个最终结果, reduce 操作可以实现从Stream中生成一个值,其生成的值不是随意的,而是根据指定的计算模型。比如,之前提到countminmax方法,因为常用而被纳入标准库中。事实上,这些方法都是reduce操作。

所以如果需要将所有数据归纳得到一个数据,可以使用 reduce 方法。方法签名:

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

Stream流中的 reduce 相关方法基本使用的代码如:

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
@Test
public void testReduce() {
// reduce:
// 第一次将默认值(也就是第一个参数0)赋值给x, 取出第一个元素赋值给y,进行相加操作
// 第二次,将第一次的结果赋值给x, 取出二个元素赋值给y,进行相加操作
// 第三次,将第二次的结果赋值给x, 取出三个元素赋值给y,进行相加操作
// 第四次,将第三次的结果赋值给x, 取出四个元素赋值给y,进行相加操作
int reduce = Stream.of(4, 5, 3, 9)
.reduce(0, (a, b) -> {
System.out.println("a = " + a + ", b = " + b);
return a + b;
});
System.out.println("reduce = " + reduce);

//求和
//reduce2用Lambda表达式, reduce3用方法引用
int reduce2 = Stream.of(4, 5, 3, 9)
.reduce(0, (x, y) -> Integer.sum(x, y));

int reduce3 = Stream.of(4, 5, 3, 9).reduce(0, Integer::sum);
System.out.println(reduce2 + "+" + reduce3);

//求最大值
int max = Stream.of(4, 5, 3, 9)
.reduce(0, (x, y) -> x > y ? x : y);
System.out.println("max = " + max);
}

Stream流的map和reduce组合使用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
@Test
public void testMapReduce() {
int reduceAge = Stream.of(
new Person("刘德华", 58),
new Person("张学友", 56),
new Person("郭富城", 54),
new Person("黎明", 52))
.map(Person::getAge) //将Person元素映射成年龄传到另一个流中.
//.reduce(0, Integer::sum); // 求出所有年龄的总和
.reduce(0, (x, y) -> x > y ? x : y); //求出年龄最大值
System.out.println(reduceAge);

//统计数字2出现的次数
int count = Stream.of(1, 2, 2, 1, 3, 2)
.map(i -> {
if (i == 2) {
return 1;
} else {
return 0;
}
})
.reduce(0, Integer::sum);
System.out.println("count = " + count);
}

Stream流的mapToInt

如果需要将Stream中的Integer类型数据转成int类型,可以使用 mapToInt 方法。方法签名:

1
IntStream mapToInt(ToIntFunction<? super T> mapper);

Stream流中的 mapToInt 相关方法基本使用的代码如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
@Test
public void test1() {
// Integer占用的内存比int多,在Stream流操作中会自动装箱和拆箱
Stream<Integer> stream = Arrays.stream(new Integer[]{1, 2, 3, 4, 5});
// 把大于3的和打印出来
// Integer result = stream
// .filter(i -> i.intValue() > 3)
// .reduce(0, Integer::sum);
// System.out.println(result);
// 先将流中的Integer数据转成int,后续都是操作int类型
IntStream intStream = stream.mapToInt(Integer::intValue);
int reduce = intStream
.filter(i -> i > 3)
.reduce(0, Integer::sum);
System.out.println(reduce);
// 将IntStream转化为Stream<Integer>
IntStream intStream1 = IntStream.rangeClosed(1, 10);
Stream<Integer> boxed = intStream1.boxed();
boxed.forEach(s -> System.out.println(s.getClass() + ", " + s));
}

Stream流的concat方法

如果有两个流,希望合并成为一个流,那么可以使用 Stream 接口的静态方法 concat

1
static <T> Stream<T> concat(Stream<? extends T> a, Stream<? extends T> b)

备注:这是一个静态方法,与 java.lang.String 当中的 concat 方法是不同的。

该方法的基本使用代码如:

1
2
3
4
5
6
7
@Test
public void testContact() {
Stream<String> streamA = Stream.of("张三");
Stream<String> streamB = Stream.of("李四");
Stream<String> result = Stream.concat(streamA, streamB);
result.forEach(System.out::println);
}

Stream综合案例

现在有两个 ArrayList 集合存储队伍当中的多个成员姓名,要求使用Stream流依次进行以下若干操作步骤:

  1. 第一个队伍只要名字为3个字的成员姓名;

  2. 第一个队伍筛选之后只要前3个人;

  3. 第二个队伍只要姓张的成员姓名;

  4. 第二个队伍筛选之后不要前2个人;

  5. 将两个队伍合并为一个队伍;

  6. 根据姓名创建 Person 对象;

  7. 打印整个队伍的Person对象信息。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
@Test
public void test() {
List<String> one = Arrays.asList("迪丽热巴", "宋远桥", "苏星河", "老子", "庄子", "孙子", "洪七公");
List<String> two = Arrays.asList("古力娜扎", "张无忌", "张三丰", "赵丽颖", "张二狗", "张天爱","张三");

//1. 第一个队伍只要名字为3个字的成员姓名;
//2. 第一个队伍筛选之后只要前3个人;
//3. 第二个队伍只要姓张的成员姓名;
//4. 第二个队伍筛选之后不要前2个人;
//5. 将两个队伍合并为一个队伍;
//6. 根据姓名创建 Person 对象;
//7. 打印整个队伍的Person对象信息。
Stream<String> oneStream = one.stream().filter(s -> s.length() == 3).limit(3);
Stream<String> twoStream = two.stream().filter(s -> s.startsWith("张")).skip(2);
concat(oneStream, twoStream).map(Person::new).forEach(System.out::println);
}

收集Stream流中的结果

对流操作完成之后,如果需要将流的结果保存到数组或集合中,可以收集流中的数据.

Stream流中的结果到集合中

Stream流提供 collect 方法,其参数需要一个 java.util.stream.Collector<T,A, R> 接口对象来指定收集到哪种集合中。java.util.stream.Collectors 类提供一些方法,可以作为 Collector接口的实例:

public static <T> Collector<T, ?, List<T>> toList() :转换为 List 集合。

public static <T> Collector<T, ?, Set<T>> toSet() :转换为 Set 集合。

下面是这两个方法的基本使用代码:

1
2
3
4
5
6
// 将流中数据收集到集合中
Stream<String> stream = Stream.of("aa", "bb", "cc");
List<String> list = stream.collect(Collectors.toList());
Set<String> set = stream.collect(Collectors.toSet());
ArrayList<String> arrayList = stream.collect(Collectors.toCollection(ArrayList::new));
HashSet<String> hashSet = stream.collect(Collectors.toCollection(HashSet::new));

!!!!注意: 上面的代码不能一起运行, 一起运行会报错, 因为stream一旦被收集到集合中后就不再是流了

Stream流中的结果到数组中

Stream提供 toArray 方法来将结果放到一个数组中,返回值类型是Object[]的:

1
Object[] toArray();

其使用场景如:

1
2
3
4
5
6
7
8
9
10
11
12
@Test
public void testStreamToArray() {
Stream<String> stream = Stream.of("aa", "bb", "cc");
// Object[] objects = stream.toArray();
// for (Object obj : objects) {
// System.out.println();
// }
String[] strings = stream.toArray(String[]::new);
for (String str : strings) {
System.out.println(str);
}
}

对流中数据进行聚合计算

Collectors.maxBy/Collectors.minBy/Collectors.counting/Collectors.summingInt/Collectors.averagingInt

当我们使用Stream流处理数据后,可以像数据库的聚合函数一样对某个字段进行操作。比如获取最大值,获取最小值,求总和,平均值,统计数量。

定义一个Person流

1
2
3
4
5
Stream<Person> studentStream = Stream.of(
new Person("赵丽颖", 58, 95),
new Person("杨颖", 56, 88),
new Person("迪丽热巴", 56, 99),
new Person("柳岩", 52, 77));

求年龄最大/最小的一个Person

1
2
3
4
5
6
7
8
// 获取最大值
Optional<Person> collect = studentStream.collect(Collectors.maxBy((o1, o2) ->
o1.getAge() - o2.getAge()));
// 获取最小值
// Optional<Person> collect = studentStream.collect(Collectors.minBy((o1, o2) ->
//o1.getSocre() - o2.getSocre()));

System.out.println(collect.get());

求年龄总和

1
2
3
// 求总和
int sumAge = studentStream.collect(Collectors.summingInt(s -> s.getAge()));
System.out.println("sumAge = " + sumAge);

求平均年龄

1
2
3
// 平均值
double avgAge = studentStream.collect(Collectors.averagingInt(s -> s.getAge()));
System.out.println("avgAge = " + avgAge);

求总人数

1
2
3
// 统计数量
Long count = studentStream.collect(Collectors.counting());
System.out.println("count = " + count);

对流中数据进行分组

Collectors.groupingBy

当我们使用Stream流处理数据后,可以根据某个属性将数据分组:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@Test
public void testGroup() {
Stream<Person> studentStream = Stream.of(
new Person("赵丽颖", 52, 95),
new Person("杨颖", 56, 88),
new Person("迪丽热巴", 56, 55),
new Person("柳岩", 52, 33));
// 将年龄大于55的分为一组,小于55分成另一组
//Map<K, V>, 第一个参数K是组别的key, 以返回值作为key. 第二个参数V是value
//以该例子为例: 大于55岁的为一组, 返回值为"成年"作为第一组的key, value就是年龄大于55的Person, 另一组类似
Map<String, List<Person>> map = studentStream
.collect(Collectors.groupingBy(s -> s.getAge() > 55 ? "成年" : "未成年"));
map.forEach((k, v) -> {
System.out.println(k + ": " + v);
});

运行结果:

1
2
未成年: [Person{name='赵丽颖', age=52, height=95}, Person{name='柳岩', age=52, height=33}]
成年: [Person{name='杨颖', age=56, height=88}, Person{name='迪丽热巴', age=56, height=55}]

对流中数据进行多级分组

Collectors.groupingBy

Stream流也可以多级分组, 只要返回的结果有多少个就会分成多少个以结果为key的Map集合.

1
2
3
4
5
6
7
8
9
10
11
12
13
Map<String, List<Person>> map = studentStream
.collect(Collectors.groupingBy(s -> {
if(s.getAge() > 50 && s.getAge() <53) {
return "50-53";
} else if(s.getAge() > 53 && s.getAge() <55) {
return "54-55";
} else {
return "56-60";
}
}));
map.forEach((k, v) -> {
System.out.println(k + ": " + v);
});

运行结果:

1
2
3
50-53: [Person{name='赵丽颖', age=52, height=95}]
56-60: [Person{name='杨颖', age=56, height=88}, Person{name='迪丽热巴', age=56, height=55}]
54-55: [Person{name='柳岩', age=54, height=33}]

对流中数据进行分区

Collectors.partitioningBy 会根据值是否为true,把集合分割为两个列表,一个true列表,一个false列表。

1
2
3
4
5
// partitioningBy会根据值是否为true,把集合分割为两个列表,一个true列表,一个false列表。
Map<Boolean, List<Person>> map = studentStream.collect(Collectors.partitioningBy(s -> s.getAge() > 55));
map.forEach((k, v) -> {
System.out.println(k + " == " + v);
});

运行结果

1
2
false == [Person{name='赵丽颖', age=52, height=95}, Person{name='柳岩', age=54, height=33}]
true == [Person{name='杨颖', age=56, height=88}, Person{name='迪丽热巴', age=56, height=55}]

对流中数据进行拼接

Collectors.joining 会根据指定的连接符,将所有元素连接成一个字符串。

1
2
3
4
5
String collect = studentStream
.map(Person::getName)
//第一个参数是插入元素之间间隔字符串, 第二个参数是前缀, 第三个参数是后缀
.collect(Collectors.joining(">_<", "^_^", "^v^")); //第一个参数是插入元素之间间隔字符串,
System.out.println(collect);

输出结果

1
^_^赵丽颖>_<杨颖>_<迪丽热巴>_<柳岩^v^

并行的Stream流

串行的Stream流

目前我们使用的Stream流是串行的,就是在一个线程上执行。

1
2
3
4
@Test
public void test0Serial() {
Stream.of(4, 5, 3, 9, 1, 2, 6).forEach(s -> System.out.println(Thread.currentThread() + ", s = " + s));
}

运行结果

1
2
3
4
5
6
7
Thread[main,5,main], s = 4
Thread[main,5,main], s = 5
Thread[main,5,main], s = 3
Thread[main,5,main], s = 9
Thread[main,5,main], s = 1
Thread[main,5,main], s = 2
Thread[main,5,main], s = 6

并行的Stream流

parallelStream其实就是一个并行执行的流。它通过默认的ForkJoinPool,可能提高多线程任务的速度。

获取并行Stream流的两种方式

直接获取并行的流
1
2
3
ArrayList<Integer> list = new ArrayList<>();
// 直接获取并行的流
Stream<Integer> stream = list.parallelStream();
将串行流转成并行流
1
2
3
ArrayList<Integer> list = new ArrayList<>();
// 将串行流转成并行流
Stream<Integer> stream = list.stream().parallel();

并行操作代码:

1
2
3
4
5
6
@Test
public void test0Parallel() {
Stream.of(4, 5, 3, 9, 1, 2, 6)
.parallel() // 将流转成并发流,Stream处理的时候将才去
.forEach(s -> System.out.println(Thread.currentThread() + ", s = " + s));
}

输出结果

1
2
3
4
5
6
7
Thread[main,5,main], s = 1
Thread[main,5,main], s = 9
Thread[main,5,main], s = 4
Thread[ForkJoinPool.commonPool-worker-3,5,main], s = 2
Thread[ForkJoinPool.commonPool-worker-3,5,main], s = 3
Thread[ForkJoinPool.commonPool-worker-2,5,main], s = 6
Thread[ForkJoinPool.commonPool-worker-1,5,main], s = 5

并行和串行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
public class Demo06 {
private static long times = 50000000000L;
private long start;
@Before
public void init() {
start = System.currentTimeMillis();
}
@After
public void destory() {
long end = System.currentTimeMillis();
System.out.println("消耗时间: " + (end - start));
}

// 测试效率,parallelStream 120
@Test
public void parallelStream() {
System.out.println("serialStream");
LongStream.rangeClosed(0, times)
.parallel()
.reduce(0, Long::sum);
}

// 测试效率,普通Stream 342
@Test
public void serialStream() {
System.out.println("serialStream");
LongStream.rangeClosed(0, times)
.reduce(0, Long::sum);
}
// 测试效率,正常for循环 421
@Test
public void forAdd() {
System.out.println("forAdd");
long result = 0L;
for (long i = 1L; i < times; i++) {
result += i;
}
}
}

我们可以看到parallelStream的效率是最高的。

Stream并行处理的过程会分而治之,也就是将一个大任务切分成多个小任务,这表示每个任务都是一个操作。

parallelStream线程安全问题

我们运行下面代码

1
2
3
4
5
6
@Test
public void parallelStreamNotice() {
ArrayList<Integer> list = new ArrayList<>(1000);
IntStream.rangeClosed(1, 1000).parallel().forEach(list::add);
System.out.println(list.size());
}

得到结果为: 913

我们明明是往集合中添加1000个元素,而实际上只有913个元素。

解决方法: 加锁、使用线程安全的集合或者调用Stream的 toArray() / collect() 操作就是满足线程安全的了。

Optional类

Optional是一个没有子类的工具类,Optional是一个可以为null的容器对象。它的作用主要就是为了解决避免Null检查,防止NullPointerException。

Optional的基本使用

Optional类的创建方式

1
2
3
Optional.of(T t) : 创建一个 Optional 实例
Optional.empty() : 创建一个空的 Optional 实例
Optional.ofNullable(T t):若 t 不为 null,创建 Optional 实例,否则创建空实例
1
2
3
4
5
6
7
Optional<String> userNameO = Optional.of("凤姐");
Optional<String> userNameO = Optional.of(null);

Optional<String> userNameO = Optional.ofNullable(null);
Optional<String> userNameO = Optional.ofNullable("凤姐");

Optional<String> userNameO = Optional.empty();

Optional类的常用方法

isPresent

isPresent() : 判断是否包含值,包含值返回true,不包含值返回false

1
2
3
4
5
6
7
if (userNameO.isPresent()) {
// get() : 如果Optional有值则将其返回,否则抛出NoSuchElementException。
String userName = userNameO.get();
System.out.println("用户名为:" + userName);
} else {
System.out.println("用户名不存在");
}
orElse

orElse(T t) : 如果调用对象包含值,返回该值,否则返回参数t

1
2
3
4
5
6
7
@Test
public void test03() {
Optional<String> userName = Optional.of("凤姐");
//Optional<String> userName = Optional.empty();
String name = userName.orElse("如花");
System.out.println(name);
}
其他方法
1
2
3
get() : 如果Optional有值则将其返回,否则抛出NoSuchElementException
orElseGet(Supplier s) :如果调用对象包含值,返回该值,否则返回 s 获取的值
map(Function f): 如果有值对其处理,并返回处理后的Optional,否则返回 Optional.empty()

Optional的高级使用

1
2
3
4
5
6
7
8
9
10
11
12
@Test
public void test05() {
Person u = new Person("hello world", 18);
//Person u = new Person(null, 18);
Optional<Person> uO = Optional.of(u);
System.out.println(getUpperCaseUserName(uO));
}
public String getUpperCaseUserName(Optional<Person> uO) {
return uO.map(u -> u.getName())
.map(name -> name.toUpperCase())
.orElse("null");
}

运行结果: 如果Person的name为空的话, 打印null; Person的name有值的话, 会将小写转换成大写.

不再需要在转换前进行if(name != null)的判断了.

总结

Optional是一个可以为null的容器对象。orElse,ifPresent,ifPresentOrElse,map等方法避免对null的判断,写出更加优雅的代码。

新的日期和时间API

旧版日期时间 API 存在的问题

  1. 设计很差: 在java.util和java.sql的包中都有日期类,java.util.Date同时包含日期和时间,而java.sql.Date仅包含日期。此外用于格式化和解析的类在java.text包中定义。

  2. 非线程安全:java.util.Date 是非线程安全的,所有的日期类都是可变的,这是Java日期类最大的问题之一。

  3. 时区处理麻烦:日期类并不提供国际化,没有时区支持,因此Java引入了java.util.Calendar和java.util.TimeZone类,但他们同样存在上述所有的问题。

新日期时间API介绍

JDK 8中增加了一套全新的日期时间API,这套API设计合理,是线程安全的。新的日期及时间API位于 java.time 包中,下面是一些关键类。

LocalDate(年月日)

LocalDate :表示日期,包含年月日,格式为 2019-10-16

1
2
3
4
5
6
7
8
9
10
11
12
13
// 创建指定日期
LocalDate fj = LocalDate.of(1985, 9, 23);
System.out.println("fj = " + fj); // 1985-09-23

// 得到当前日期
LocalDate nowDate = LocalDate.now();
System.out.println("nowDate = " + nowDate); // 2019-10-16

// 获取日期信息
System.out.println("年: " + nowDate.getYear());
System.out.println("月: " + nowDate.getMonthValue());
System.out.println("日: " + nowDate.getDayOfMonth());
System.out.println("星期: " + nowDate.getDayOfWeek());

LocalTime(时分秒)

LocalTime :表示时间,包含时分秒,格式为 16:38:54.158549300

1
2
3
4
5
6
7
8
9
10
11
12
13
// 得到指定的时间
LocalTime time = LocalTime.of(12,15, 28, 129_900_000);
System.out.println("time = " + time);

// 得到当前时间
LocalTime nowTime = LocalTime.now();
System.out.println("nowTime = " + nowTime);

// 获取时间信息
System.out.println("小时: " + nowTime.getHour());
System.out.println("分钟: " + nowTime.getMinute());
System.out.println("秒: " + nowTime.getSecond());
System.out.println("纳秒: " + nowTime.getNano());

LocalDateTime(年月日时分秒)

LocalDateTime :表示日期时间,包含年月日,时分秒,格式为 2018-09-06T15:33:56.750

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 创建指定日期
LocalDateTime fj = LocalDateTime.of(1985, 9, 23, 9, 10, 20);
System.out.println("fj = " + fj); // 1985-09-23T09:10:20

// 得到当前日期时间
LocalDateTime now = LocalDateTime.now();
System.out.println("now = " + now); // 2019-10-16T16:42:24.497896800
System.out.println(now.getYear());
System.out.println(now.getMonthValue());
System.out.println(now.getDayOfMonth());
System.out.println(now.getHour());
System.out.println(now.getMinute());
System.out.println(now.getSecond());
System.out.println(now.getNano());

其他API

DateTimeFormatter :日期时间格式化类。

Instant:时间戳,表示一个特定的时间瞬间。

Duration:用于计算2个时间(LocalTime,时分秒)的距离

Period:用于计算2个日期(LocalDate,年月日)的距离

ZonedDateTime :包含时区的时间

Java中使用的历法是ISO 8601日历系统,它是世界民用历法,也就是我们所说的公历。平年有365天,闰年是366天。此外Java 8还提供了4套其他历法,分别是:

ThaiBuddhistDate:泰国佛教历

MinguoDate:中华民国历

JapaneseDate:日本历

HijrahDate:伊斯兰历

日期时间的比较

1
2
3
4
5
6
7
8
@Test
public void test06() {
// 在JDK8中,LocalDate类中使用isBefore()、isAfter()、equals()方法来比较两个日期,可直接进行比较。
LocalDate now = LocalDate.now();
LocalDate date = LocalDate.of(2018, 8, 8);
System.out.println(now.isBefore(date)); // false
System.out.println(now.isAfter(date)); // true
}

JDK 8的时间格式化与解析

通过 java.time.format.DateTimeFormatter 类可以进行日期时间解析与格式化。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
@Test
public void test04() {
// 得到当前日期时间
LocalDateTime now = LocalDateTime.now();
DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");

// 将日期时间格式化为字符串
String format = now.format(formatter);
System.out.println("format = " + format);

// 将字符串解析为日期时间
LocalDateTime parse = LocalDateTime.parse("1985-09-23 10:12:22", formatter);
System.out.println("parse = " + parse);
}

JDK 8的Instant类

Instant 时间戳/时间线,内部保存了从1970年1月1日 00:00:00以来的秒和纳秒。

1
2
3
4
5
6
7
8
9
10
11
12
13
@Test
public void test07() {
Instant now = Instant.now();
System.out.println("当前时间戳 = " + now); //当前时间戳 = 2022-12-26T17:12:59.270Z

// 获取从1970年1月1日 00:00:00的秒
System.out.println(now.getNano()); //270000000
System.out.println(now.getEpochSecond()); //1672074779
System.out.println(now.toEpochMilli()); //1672074779270
System.out.println(System.currentTimeMillis()); //1672074779276
Instant instant = Instant.ofEpochSecond(5);
System.out.println(instant); //1970-01-01T00:00:05Z
}

JDK 8的计算日期时间差类

Duration/Period类: 计算日期时间差。

  1. Duration:用于计算2个时间(LocalTime,时分秒)的距离

  2. Period:用于计算2个日期(LocalDate,年月日)的距离

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
@Test
public void test08() {
// Duration计算时间的距离
LocalTime now = LocalTime.now();
LocalTime time = LocalTime.of(14, 15, 20);
Duration duration = Duration.between(time, now);
System.out.println("相差的天数:" + duration.toDays());
System.out.println("相差的小时数:" + duration.toHours());
System.out.println("相差的分钟数:" + duration.toMinutes());
System.out.println("相差的秒数:" + duration.toSeconds());

// Period计算日期的距离
LocalDate nowDate = LocalDate.now();
LocalDate date = LocalDate.of(1998, 8, 8);
// 让后面的时间减去前面的时间
Period period = Period.between(date, nowDate);
System.out.println("相差的年:" + period.getYears());
System.out.println("相差的月:" + period.getMonths());
System.out.println("相差的天:" + period.getDays());
}

JDK 8的时间校正器

有时我们可能需要获取例如:将日期调整到“下一个月的第一天”等操作。可以通过时间校正器来进行。

  • TemporalAdjuster : 时间校正器。

  • TemporalAdjusters : 该类通过静态方法提供了大量的常用TemporalAdjuster的实现。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// TemporalAdjuster类:自定义调整时间
@Test
public void test09() {
LocalDateTime now = LocalDateTime.now();
// 得到下一个月的第一天
TemporalAdjuster firsWeekDayOfNextMonth = temporal -> {
LocalDateTime dateTime = (LocalDateTime) temporal;
LocalDateTime nextMonth = dateTime.plusMonths(1).withDayOfMonth(1);
System.out.println("nextMonth = " + nextMonth);
return nextMonth;
};

LocalDateTime nextMonth = now.with(firsWeekDayOfNextMonth);
System.out.println("nextMonth = " + nextMonth);
}

JDK 8设置日期时间的时区

Java8 中加入了对时区的支持,LocalDate、LocalTime、LocalDateTime是不带时区的,带时区的日期时间类分别为:ZonedDate、ZonedTime、ZonedDateTime。

其中每个时区都对应着 ID,ID的格式为 “区域/城市” 。例如 :Asia/Shanghai 等。

ZoneId:该类中包含了所有的时区信息。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// 设置日期时间的时区
@Test
public void test10() {
// 1.获取所有的时区ID
// ZoneId.getAvailableZoneIds().forEach(System.out::println);
// 不带时间,获取计算机的当前时间
LocalDateTime now = LocalDateTime.now(); // 中国使用的东八区的时区.比标准时间早8个小时
System.out.println("now = " + now);

// 2.操作带时区的类
// now(Clock.systemUTC()): 创建世界标准时间
ZonedDateTime bz = ZonedDateTime.now(Clock.systemUTC());
System.out.println("bz = " + bz);
// now(): 使用计算机的默认的时区,创建日期时间
ZonedDateTime now1 = ZonedDateTime.now();
System.out.println("now1 = " + now1); // 2019-10-
19T16:19:44.007153500+08:00[Asia/Shanghai]
// 使用指定的时区创建日期时间
ZonedDateTime now2 = ZonedDateTime.now(ZoneId.of("America/Vancouver"));
System.out.println("now2 = " + now2); // 2019-10-19T01:21:44.248794200-
07:00[America/Vancouver]
}