Lamdba

1、为什么使用Lamdba?

Lamdba是一个匿名函数,我们可以把Lamdba表达式理解为一段可以传递的代码,这样我们就能够写出更简洁更灵活的代码,也提升了Java的表达能力

2、Lamdba的实际应用场景

假设现有需求如下:查询出公司中年龄大于35的员工信息

给定的信息如下:

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

private String name; // 姓名
private int age;// 年龄
private double salary;// 薪资

public Employee(String name, int age, double salary) {
this.name = name;
this.age = age;
this.salary = salary;
}
// 省略getter&setter方法
}
// 现有如下员工信息
Employee emp1 = new Employee("张三", 33, 3333.33);
Employee emp2 = new Employee("李四", 44, 4444.44);
Employee emp3 = new Employee("王五", 55, 5555.55);
Employee emp4 = new Employee("赵六", 66, 6666.66);
Employee emp5 = new Employee("田七", 77, 7777.77);
List<Employee> emps = Arrays.asList(emp1, emp2, emp3, emp4, emp5);

在没有学习Lamdba之前,我们可以定义方法来实现这个需求

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// 过滤出年龄大于35岁的员工信息
public List<Employee> filterEmployee(List<Employee> emps) {
List<Employee> results = new ArrayList<>();
for (Employee emp : emps) {
if (emp.getAge() > 35) {
results.add(emp);
}
}
return results;
}
---------------------------------------------------------------------------
@Test
public void test01() {
List<Employee> list = filterEmployee(emps);
for (Employee emp : list) {
System.out.println(emp);
}
}
============控制台信息============
name=李四, age=44, salary=4444.44
name=王五, age=55, salary=5555.55
name=赵六, age=66, salary=6666.66
name=田七, age=77, salary=7777.77

如果这个时候添加了一个新需求,需要查询工资低于5000的员工信息

1
2
3
4
5
6
7
8
9
10
11
12
13
public List<Employee> filterEmployee2(List<Employee> emps) {
List<Employee> results = new ArrayList<>();
for (Employee emp : emps) {
// 我们只需要修改为emp.getSalary() < 5000即可
if (emp.getSalary() < 5000) {
results.add(emp);
}
}
return results;
}
============控制台信息============
name=张三, age=33, salary=3333.33
name=李四, age=44, salary=4444.44

这个时候问题就来了,在我们编写的这段代码中,真正不一样的地方只有一行,但是我们却写了一个新方法来实现现有的需求变动,代码的复用性与扩展性都很差。如果将来有更多的需求,那么我们需要重写很多方法来满足需求的变动,这个时候我们可以考虑使用设计模式来进行修改

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 interface MyPredicate<T> {
// 传入一个对象 校验通过返回true 否则返回false
public boolean test(T t);
}
---------------------------------------------------------------------------
// 实现类实现具体的校验逻辑 校验年龄大于35的员工
public class FilterEmployeeByAge implements MyPredicate<Employee> {
@Override
public boolean test(Employee emp) {
return emp.getAge() > 35;
}
}
---------------------------------------------------------------------------
// 实现类实现具体的校验逻辑 校验薪资小于5000的员工
public class FilterEmployeeBySalary implements MyPredicate<Employee> {
@Override
public boolean test(Employee emp) {
return emp.getSalary() < 5000;
}
}
---------------------------------------------------------------------------
// 可以将之前的两个方法 统一替换成一个方法
public List<Employee> filterEmployee(List<Employee> emps, MyPredicate<Employee> mp) {
List<Employee> results = new ArrayList<>();
for (Employee emp : emps) {
// 当该接口校验通过 才将结果插入到集合中
if (mp.test(emp)) {
results.add(emp);
}
}
return results;
}
---------------------------------------------------------------------------
// 通过传入接口的不同实现类 来实现不同的逻辑校验
List<Employee> listByAge = filterEmployee(emps, new FilterEmployeeByAge());
List<Employee> listBySalary = filterEmployee(emps, new FilterEmployeeBySalary());

通过设计模式的改进,代码确实可扩展性比之前好了许多,但是又引发了新的问题。我们每次都要为了实现新的逻辑编写一个实现类文件,例如:FilterEmployeeBySalary和FilterEmployeeByAge,这个时候我们很自然的想到可以使用匿名内部类来解决这个问题,使我们的项目结构看起来更整洁

1
2
3
4
5
6
7
// 可以直接通过匿名内部类实现对年龄的校验
List<Employee> listByAge = filterEmployee(emps, new MyPredicate<Employee>() {
@Override
public boolean test(Employee emp) {
return emp.getAge() > 35;
}
});

通过观察匿名内部类我们发现,真正的校验逻辑其实只有emp.getAge() > 35这一句,有没有办法再次进行优化呢?那么我们终于可以引入今天的主角Lamdba啦

1
2
// 通过Lamdba进行优化
List<Employee> listByAge = filterEmployee(emps, e -> e.getAge() > 35);

通过上面的这个例子,我们已经感受到了Lamdba给我们带来的方便,那么下面我们就开始正式学习Lamdba的语法吧~

3、Lamdba基本语法

Java8中引入了一个新的操作符 **”->”**,该操作符被称为箭头操作符

该操作符将Lamdba表达式分成了两部分

左侧:Lamdba表达式的参数列表

右侧:Lamdba表达式中需要执行的功能

常见的语法格式有如下几种:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// 语法格式一:无参数 无返回值
Runnable run = () -> System.out.println("Hello Lamdba");
run.run();

// 语法格式二:有一个参数 无返回值(只有一个参数 左侧小括号可以不写)
Consumer<String> con = (s) -> System.out.println(s);
Consumer<String> con = s -> System.out.println(s);
con.accept("Hello Lamdba");

// 语法格式三:有两个或以上参数 有返回值 且有多条语句
Comparator<Integer> com = (x, y) -> {
System.out.println("Hello Lamdba");
return Integer.compare(x, y);
};

// 语法格式四:如果只有一条语句 大括号与return可以不写
Comparator<Integer> com = (x, y) -> Integer.compare(x, y);

// 语法格式五:可推导即可省略 例如参数类型
Comparator<Integer> com = (Integer x, Integer y) -> Integer.compare(x, y);

4、使用的条件

Lamdba表达式需要函数式接口的支持

函数式接口:接口中只有一个抽象方法的接口,称为函数式接口

通常我们可以使用注解@FunctionalInterface来进行检验

1
2
3
4
5
// 该注解可以检验当前接口是否符合函数式接口
@FunctionalInterface
public interface MyPredicate<T> {
boolean test(T t);
}

5、内置核心函数式接口

接口名 接口描述 内置方法
Consumer 消费型接口 void accept(T t)
Supplier 供给型接口 T get()
Function<T,R> 函数型接口 R apply(T t)
Predicate 断言型接口 boolean test(T t)

下面我们通过例子来演示一下

需求:传入任意字符串进行打印

分析:符合需要传入一个参数 且无需返回值的情况

选择:Consumer接口

1
2
3
4
5
6
7
8
@Test
public void test() {
// Lamdba实现
Consumer<String> con = s -> System.out.println(s);
// 方法引用实现
Consumer<String> con = System.out::println;
con.accept("Hello Lamdba");
}

需求:获取当前的日期

分析:符合无需传入参数 且需要返回一个值

选择:Supplier接口

1
2
3
4
5
6
7
8
@Test
public void test() {
// Lamdba实现
Supplier<Date> supplier = () -> new Date();
// 方法引用实现
Supplier<Date> supplier = Date::new;
Date today = supplier.get();
}

需求:获取任意日期的字符串表格式

分析:需要传入一个Date类型 返回一个String类型

选择:Function接口

1
2
3
4
5
6
7
8
@Test
public void test() {
// Lamdba实现
Function<Date, String> con = d -> new SimpleDateFormat("yyyyMM").format(d);
// 方法引用实现
Function<Date, String> con = new SimpleDateFormat("yyyyMM")::format;
String dateStr = con.apply(new Date());
}

需求:判断一个字符串是否包含Lamdba

分析:传入一个值 返回一个boolean

选择:Predicate接口

1
2
3
4
5
6
7
8
9
@Test
public void test() {
// Lamdba实现
Predicate<String> pre = s -> s.contains("Lamdba");
boolean test = pre.test("Hello Lamdba");
// 方法引用实现
Predicate<String> pre = "Hello Lamdba"::contains;
boolean test = pre.test("Lamdba");
}

6、方法引用与构造器

若Lamdba体中的内容有方法已经实现了,我们可以使用方法引用,也可以理解为方法引用是Lamdba的另一种表现形式

主要有三种语法格式:

对象 : : 实例方法名

类名 : : 静态方法名

类名 : : 实例方法名

注意:

  1. Lamdba体中调用方法的参数列表与返回值类型,要与函数式接口中的类型一致
  2. 若Lamdba参数列表中的第一参数是实例方法的调用者,而第二个参数是实例方法的参数时,可以使用ClassName :: method

具体例子可看上方两种实现方式

构造器引用

格式:ClassName : : new

注意:需要调用的构造器的参数列表要与函数式接口中抽象方法的列表保持一致

构造参数 函数式接口参数
空参构造 Supplier刚好符合不需要参数,返回一个值
一个参数构造 Function刚好符合传入一个参数,返回一个值
两个参数构造 BiFunction刚好符合传入两个参数,返回一个值

Stream API

1、Stream的三个操作步骤

  1. 创建Stream
  2. 中间操作
  3. 终止操作

创建Stream的四种方式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
@Test
public void test() {
// 1、可以通过Collection系列集合提供的stream()或paralleStream()
List<String> list = new ArrayList<>();
Stream<String> stream1 = list.stream();

// 2、通过Arrays中的静态方法stream()获取数组流
Employee[] emps = new Employee[10];
Stream<Employee> stream2 = Arrays.stream(emps);

// 3、通过Stream类中的静态方法of()
Stream<String> stream3 = Stream.of("aa", "bb", "cc");

// 4、创建无限流
// 第一种:迭代Stream.iterate(seed, f);
// seed是开始数字 f是一个函数式接口 传入T返回T
Stream<Integer> stream4 = Stream.iterate(0, (x) -> x + 2);
stream4.limit(10).forEach(System.out::println);

// 第二种:生成Stream.generate(s);
// s是Supplier函数式接口 没有参数 返回T
Stream.generate(() -> Math.random()).limit(5).forEach(System.out::println);
}

2、筛选与切片

filter——接收Lamdba,从流中排除某些元素

1
2
3
4
5
// 筛选出流中等于bb的元素
Stream.of("aa", "bb", "cc").filter(x -> x.equals("bb"))
.forEach(System.out::println);
============控制台信息============
bb

limit——截断流,使其元素不超过给定数量

1
2
3
4
5
// 筛选流中的前两个元素
Stream.of("aa", "bb", "cc").limit(2).forEach(System.out::println);
============控制台信息============
aa
bb

skip(n)——跳过元素,返回一个扔掉了前n个元素的流。如果流中元素不足n个,则返回一个空流,与limit(n)互补

1
2
3
4
5
6
7
8
9
// 去掉前两个符合条件的元素
Stream.of("aa", "bb", "cc").skip(2).forEach(System.out::println);
============控制台信息============
cc

// 如果元素不够 则返回空元素
Stream.of("aa", "bb", "cc").skip(3).forEach(System.out::println);
============控制台信息============
此处为空的

distinct——筛选,通过流所生成元素的hashCode和equals方法去除重复元素

1
2
3
4
5
6
7
8
9
// 去除流中的重复元素
// 注意:如果是对象集合 那么需要重写hashCode和equals方法
Stream.of("aa", "bb", "bb", "cc", "cc")
.distinct()
.forEach(System.out::println);
============控制台信息============
aa
bb
cc

下面我们综合使用以下上面学到的五种

1
2
3
4
5
6
7
8
9
10
// 筛选出没有重复的 包含b 除去第一个符合的元素后的两个元素
Stream.of("ab", "bc", "bc", "cd", "de", "abcde")
.distinct()
.filter(x -> x.contains("b"))
.skip(1)
.limit(2)
.forEach(System.out::println);
============控制台信息============
bc
abcde

map——映射,将元素转换成其他形式或提取信息。接收一个函数作为参数,该函数会被应用到每个元素上,并将其映射成一个新的元素

注意:使用map后返回的是Stream流,T为函数的返回值

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 假设现有如下集合
Employee emp1 = new Employee("张三", 33, 3333.33);
Employee emp2 = new Employee("李四", 44, 4444.44);
Employee emp3 = new Employee("王五", 55, 5555.55);
Employee emp4 = new Employee("赵六", 66, 6666.66);
Employee emp5 = new Employee("田七", 77, 7777.77);
List<Employee> emps = Arrays.asList(emp1, emp2, emp3, emp4, emp5);
// 获取所有员工的名字并打印出来
emps.stream().map(Employee::getName).forEach(System.out::println);
============控制台信息============
张三
李四
王五
赵六
田七

flatMap——映射,接收一个函数作为参数,将流中的每个值都换成另一个流,然后把所有流连接成一个流,简言之,flatMap中的函数返回值需要是一个流

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// 初始化数据
List<String> list = Arrays.asList("aaa", "bbb");
// 将集合变成一个流 调用flatMap 传入的参数返回值也要是一个流
list.stream().flatMap(s -> {
// 新建一个集合 用于保存每个字符串拆开的字符
List<Character> rList = new ArrayList<>();
// 遍历每一个字符串生成的字符数组
for (Character c : s.toCharArray()) {
rList.add(c);
}
// 将结果集返回流
return rList.stream();
// 将结果的字符集合打印出来
}).forEach(System.out::println);
============控制台信息============
a
a
a
b
b
b

sorted——自然排序(Comparable)

1
2
3
4
5
6
7
8
9
10
// 初始化数据
List<String> list = Arrays.asList("ccc", "aaa", "bbb", "ddd", "eee");
// 按照自然规则进行排序 这里需要String类实现Comparable接口
list.stream().sorted().forEach(System.out::println);
============控制台信息============
aaa
bbb
ccc
ddd
eee

sorted(Comparator com)——定制排序(Comparator)

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
// 假设现有如下集合
Employee emp1 = new Employee("张三", 33, 777);
Employee emp2 = new Employee("李四", 33, 666);
Employee emp3 = new Employee("王五", 55, 555);
Employee emp4 = new Employee("赵六", 55, 444);
Employee emp5 = new Employee("田七", 55, 333);
List<Employee> emps = Arrays.asList(emp1, emp2, emp3, emp4, emp5);
// 先按照年龄升序排序 年龄相同再按照工资升序排序
emps.stream().sorted((s1, s2) -> {
// 如果年龄相同
if (s1.getAge() == s2.getAge()) {
// 比较工资差值并返回
return Double.compare(s1.getSalary(), s2.getSalary());
} else {
// 否则返回年龄差值
return s1.getAge() - s2.getAge();
}
// 打印结果集
}).forEach(System.out::println);
============控制台信息============
Employee [name=李四, age=33, salary=666.0]
Employee [name=张三, age=33, salary=777.0]
Employee [name=田七, age=55, salary=333.0]
Employee [name=赵六, age=55, salary=444.0]
Employee [name=王五, age=55, salary=555.0]

allMatch——检查是否匹配所有元素

1
2
3
4
5
List<Integer> list = Arrays.asList(111, 222, 333, 444, 555);
boolean flag = list.stream().allMatch(s -> s > 200);
System.out.println("是否都大于200:" + flag);
============控制台信息============
是否都大于200false

anyMatch——检查是否至少匹配一个元素

1
2
3
4
5
List<Integer> list = Arrays.asList(111, 222, 333, 444, 555);
boolean flag = list.stream().anyMatch(s -> s > 200);
System.out.println("是否有值大于200:" + flag);
============控制台信息============
是否有值大于200true

noneMatch——检查是否没有匹配所有元素

1
2
3
4
5
List<Integer> list = Arrays.asList(111, 222, 333, 444, 555);
boolean flag = list.stream().noneMatch(s -> s > 200);
System.out.println("是否没有值大于200:" + flag);
============控制台信息============
是否没有值大于200false

findFirst——返回第一个元素

1
2
3
4
5
List<Integer> list = Arrays.asList(111, 222, 333, 444, 555);
Optional<Integer> first = list.stream().findFirst();
System.out.println("元素中的第一个值是:"+first.get());
============控制台信息============
元素中的第一个值是:111

findAny——返回当前流中的任意元素

1
2
3
4
5
6
7
8
9
List<Integer> list = Arrays.asList(111, 222, 333, 444, 555);
// 需要注意的是 这里如果使用stream 那么永远都会取第一个
// 因为stream是串行流 第一个是符合任意元素的 所以就返回了
// Optional<Integer> any = list.stream().findAny();
// 我们建议使用parallelStream并行流 这样结果就不会是唯一的
Optional<Integer> any = list.parallelStream().findAny();
System.out.println("元素中任意元素是:"+any.get());
============控制台信息============
元素中任意元素是:333

count——返回流中元素的总个数

1
2
3
4
5
List<Integer> list = Arrays.asList(111, 222, 333, 444, 555);
long count = list.stream().count();
System.out.println(count);
============控制台信息============
5

max——返回流中元素的最大值

1
2
3
4
5
List<Integer> list = Arrays.asList(111, 222, 555, 444, 333);
Optional<Integer> max = list.stream().max(Integer::compareTo);
System.out.println(max.get());
============控制台信息============
555

min——返回流中元素的最小值

1
2
3
4
5
List<Integer> list = Arrays.asList(111, 222, 555, 444, 333);
Optional<Integer> min = list.stream().min(Integer::compareTo);
System.out.println(min.get());
============控制台信息============
111

reduce(T identity,BinaryOperator) / reduce(BinaryOperator)——可以将流中的元素反复结合起来,得到一个值

注意:如果第一个参数未指定起始值 那么返回值类型就为Optional 否则返回元素类型

1
2
3
4
5
6
7
List<Integer> list = Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9, 10);
// reduce第一个参数代表一个初始值 即第一个x的值为0 然后从集合中取出第一个值作为y的值
// 之后将算出的数字作为新一轮的x的值 将集合中下一个元素的值作为y的值
Integer sum = list.stream().reduce(0, (x, y) -> x + y);
System.out.println(sum);
============控制台信息============
55

collect——将流转换为其他形式,接收一个Collector接口的实现,用于给Stream中元素做汇总方法

收集数据到List:Collectors.toList()

1
2
3
4
5
6
7
8
9
10
11
12
13
// 初始化数据
Employee emp1 = new Employee("张三", 33, 3333.33);
Employee emp2 = new Employee("李四", 44, 4444.44);
Employee emp3 = new Employee("王五", 55, 5555.55);
Employee emp4 = new Employee("赵六", 66, 6666.66);
Employee emp5 = new Employee("田七", 77, 7777.77);
List<Employee> emps = Arrays.asList(emp1, emp2, emp3, emp4, emp5);
// 将所有员工的姓名收集到一个List集合中
// Collectors可以快速的创建一个收集器(Collector)实例
List<String> names = emps.stream().map(Employee::getName).collect(Collectors.toList());
System.out.println(names);
============控制台信息============
[张三, 李四, 王五, 赵六, 田七]

收集数据到Set:Collectors.toSet()

1
2
3
4
5
6
7
8
9
10
11
12
// 初始化数据
Employee emp1 = new Employee("张三", 33, 3333.33);
Employee emp2 = new Employee("张三", 44, 4444.44);
Employee emp3 = new Employee("王五", 55, 5555.55);
Employee emp4 = new Employee("王五", 66, 6666.66);
Employee emp5 = new Employee("田七", 77, 7777.77);
List<Employee> emps = Arrays.asList(emp1, emp2, emp3, emp4, emp5);
// 将所有员工的姓名收集到一个Set集合中
Set<String> names = emps.stream().map(Employee::getName).collect(Collectors.toSet());
System.out.println(names);
============控制台信息============
[张三, 王五, 田七]

收集到HashSet等特殊集合中:Collectors.toCollection(HashSet::new)

1
2
3
4
5
6
7
8
9
10
11
12
// 初始化数据
Employee emp1 = new Employee("张三", 33, 3333.33);
Employee emp2 = new Employee("张三", 44, 4444.44);
Employee emp3 = new Employee("王五", 55, 5555.55);
Employee emp4 = new Employee("王五", 66, 6666.66);
Employee emp5 = new Employee("田七", 77, 7777.77);
List<Employee> emps = Arrays.asList(emp1, emp2, emp3, emp4, emp5);
// 将所有员工的姓名收集到一个HashSet集合中
HashSet<String> names = emps.stream().map(Employee::getName).collect(Collectors.toCollection(HashSet::new));
System.out.println(names);
============控制台信息============
[张三, 王五, 田七]

收集总数:Collectors.counting()

1
2
3
4
5
6
7
8
9
10
11
// 初始化数据
Employee emp1 = new Employee("张三", 33, 3333.33);
Employee emp2 = new Employee("张三", 44, 4444.44);
Employee emp3 = new Employee("王五", 55, 5555.55);
Employee emp4 = new Employee("王五", 66, 6666.66);
Employee emp5 = new Employee("田七", 77, 7777.77);
List<Employee> emps = Arrays.asList(emp1, emp2, emp3, emp4, emp5);
Long count = emps.stream().collect(Collectors.counting());
System.out.println(count);
============控制台信息============
5

计算工资的平均值:Collectors.averagingDouble()Collectors.averagingInt()Collectors.averagingLong(),示例中用Double进行演示

1
2
3
4
5
6
7
8
9
10
11
12
// 初始化数据
Employee emp1 = new Employee("张三", 33, 3333.33);
Employee emp2 = new Employee("张三", 44, 4444.44);
Employee emp3 = new Employee("王五", 55, 5555.55);
Employee emp4 = new Employee("王五", 66, 6666.66);
Employee emp5 = new Employee("田七", 77, 7777.77);
List<Employee> emps = Arrays.asList(emp1, emp2, emp3, emp4, emp5);
// 计算除所有员工工资的平均值
Double avg = emps.stream().collect(Collectors.averagingDouble(Employee::getSalary));
System.out.println(avg);
============控制台信息============
5555.55

计算工资的总和:Collectors.summingDouble()Collectors.summingInt()Collectors.summingLong(),示例中用Double进行演示

1
2
3
4
5
6
7
8
9
10
11
12
// 初始化数据
Employee emp1 = new Employee("张三", 33, 3333.33);
Employee emp2 = new Employee("张三", 44, 4444.44);
Employee emp3 = new Employee("王五", 55, 5555.55);
Employee emp4 = new Employee("王五", 66, 6666.66);
Employee emp5 = new Employee("田七", 77, 7777.77);
List<Employee> emps = Arrays.asList(emp1, emp2, emp3, emp4, emp5);
// 计算除所有员工工资的总和
Double sum = emps.stream().collect(Collectors.summingDouble(Employee::getSalary));
System.out.println(sum);
============控制台信息============
27777.75

获取工资最大/最小的员工信息:Collectors.maxBy()/Collectors.minBy()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// 初始化数据
Employee emp1 = new Employee("张三", 33, 3333.33);
Employee emp2 = new Employee("张三", 44, 4444.44);
Employee emp3 = new Employee("王五", 55, 5555.55);
Employee emp4 = new Employee("王五", 66, 6666.66);
Employee emp5 = new Employee("田七", 77, 7777.77);
List<Employee> emps = Arrays.asList(emp1, emp2, emp3, emp4, emp5);
// 获取工资最大/最小的员工信息
// 因可能存在空值 所以返回值是Optional
Optional<Employee> max = emps.stream().collect(Collectors.maxBy((e1, e2) -> Double.compare(e1.getSalary(), e2.getSalary())));
Optional<Employee> min = emps.stream().collect(Collectors.minBy((e1, e2) -> Double.compare(e1.getSalary(), e2.getSalary())));
System.out.println(max.get());
System.out.println(min.get());
============控制台信息============
Employee [name=田七, age=77, salary=7777.77]
Employee [name=张三, age=33, salary=3333.33]

根据名字进行分组:Collectors.groupingBy()

1
2
3
4
5
6
7
8
9
10
11
12
// 初始化数据
Employee emp1 = new Employee("张三", 33, 3333.33);
Employee emp2 = new Employee("张三", 44, 4444.44);
Employee emp3 = new Employee("王五", 55, 5555.55);
Employee emp4 = new Employee("王五", 66, 6666.66);
Employee emp5 = new Employee("田七", 77, 7777.77);
List<Employee> emps = Arrays.asList(emp1, emp2, emp3, emp4, emp5);
// 根据员工名字进行分组 返回map中key是名字 value是满足该key的员工集合
Map<String, List<Employee>> group = emps.stream().collect(Collectors.groupingBy(Employee::getName));
System.out.println(group);
============控制台信息============
{张三=[Employee [name=张三, age=33, salary=3333.33], Employee [name=张三, age=44, salary=4444.44]], 王五=[Employee [name=王五, age=55, salary=5555.55], Employee [name=王五, age=66, salary=6666.66]], 田七=[Employee [name=田七, age=77, salary=7777.77]]}

根据条件进行分区:Collectors.partitioningBy()

1
2
3
4
5
6
7
8
9
10
11
12
13
// 初始化数据
Employee emp1 = new Employee("张三", 33, 3333.33);
Employee emp2 = new Employee("张三", 44, 4444.44);
Employee emp3 = new Employee("王五", 55, 5555.55);
Employee emp4 = new Employee("王五", 66, 6666.66);
Employee emp5 = new Employee("田七", 77, 7777.77);
List<Employee> emps = Arrays.asList(emp1, emp2, emp3, emp4, emp5);
// 将所有工资大于7000分为true组 其余分为false组
// 参数为断言型接口
Map<Boolean, List<Employee>> map = emps.stream().collect(Collectors.partitioningBy(e->e.getSalary()>7000));
System.out.println(map);
============控制台信息============
{false=[Employee [name=张三, age=33, salary=3333.33], Employee [name=张三, age=44, salary=4444.44], Employee [name=王五, age=55, salary=5555.55], Employee [name=王五, age=66, salary=6666.66]], true=[Employee [name=田七, age=77, salary=7777.77]]}

按照指定分隔符拼接字符串:Collectors.joining()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// 初始化数据
Employee emp1 = new Employee("张三", 33, 3333.33);
Employee emp2 = new Employee("张三", 44, 4444.44);
Employee emp3 = new Employee("王五", 55, 5555.55);
Employee emp4 = new Employee("王五", 66, 6666.66);
Employee emp5 = new Employee("田七", 77, 7777.77);
List<Employee> emps = Arrays.asList(emp1, emp2, emp3, emp4, emp5);
// 将所有员工的名字拼接到一起
String s1 = emps.stream().map(Employee::getName).collect(Collectors.joining());
// 将所有员工的名字拼接到一起 并且用逗号进行分隔
String s2 = emps.stream().map(Employee::getName).collect(Collectors.joining(","));
// 将所有员工的名字拼接到一起 用逗号分隔 并且添加开始跟结束的字符
String s3 = emps.stream().map(Employee::getName).collect(Collectors.joining(",", "开始[", "]结束 "));
System.out.println(s1);
System.out.println(s2);
System.out.println(s3);
============控制台信息============
张三张三王五王五田七
张三,张三,王五,王五,田七
开始[张三,张三,王五,王五,田七]结束

3、Stream练习题

1、给定一个数字列表,如何返回一个由每个数的平方构成的列表呢?

例如:给定【1,2,3,4,5】,应该返回【1,4,9,16,25】

1
2
3
4
5
List<Integer> nums = Arrays.asList(1, 2, 3, 4, 5);
List<Integer> collect = nums.stream().map(e -> e * e).collect(Collectors.toList());
System.out.println(collect);
============控制台信息============
[1, 4, 9, 16, 25]

2、怎样用map和reduce方法数一数流中由多少个Employee呢?

1
2
3
4
5
6
7
8
9
10
Employee emp1 = new Employee("张三", 33, 3333.33);
Employee emp2 = new Employee("李四", 44, 4444.44);
Employee emp3 = new Employee("王五", 55, 5555.55);
Employee emp4 = new Employee("赵六", 66, 6666.66);
Employee emp5 = new Employee("田七", 77, 7777.77);
List<Employee> emps = Arrays.asList(emp1, emp2, emp3, emp4, emp5);
Integer sum = emps.stream().map(e -> 1).reduce(0, Integer::sum);
System.out.println(sum);
============控制台信息============
5

并行流与顺序流

什么是ForkJoin

Fork/Join框架:就是在必要的情况下,将一个大任务,进行拆分(fork)成若干个小任务(拆到不可再拆时),再将一个个的小任务运算的结果进行join汇总

Fork/Join与传统线程池的区别

采用工作窃取模式(work-stealing):

当执行新的任务时,它可以将其拆分分成更小的任务执行,并将小任务加到线程队列中,然后再从一个随机线程的队列中偷一个并把它放在自己的队列中

相对于一般的线程池实现,fork/join框架的优势体现在对其中包含的任务的处理方式上,在一般的线程池中,如果一个线程正在执行的任务由于某些原因无法继续运行,那么该线程会处于等待状态,而在fork/join框架实现中,如果某个子问题由于等待另外一个子问题的完成而无法继续运行,那么处理该子问题的线程会主动寻找其他尚未运行的子问题来执行,这种方式减少了线程的等待时间,提高了性能

计算0~10000000000L的总和

传统方式使用Fork/Join计算,需要编写一个类继承RecursiveTask<T>RecursiveAction,两种的区别是前者有返回值,后者无返回值

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
import java.util.concurrent.RecursiveTask;
public class ForkJoinCalculate extends RecursiveTask<Long> {
private static final long serialVersionUID = 964117657719938939L;
// 定义开始数字
private long start;
// 定义结束数字
private long end;
// 定义拆分最小临界值
private static final long THRESHOLD = 10000;

public ForkJoinCalculate(long start, long end) {
this.start = start;
this.end = end;
}

@Override
protected Long compute() {
// 计算当前差值是否达到拆分最小临界值
long length = end - start;
// 如果超过10000 就进行拆分
if (length > THRESHOLD) {
// 计算开始和结束的中间值
long middle = (start + end) / 2;
// 构建一个小于中间值的实例
ForkJoinCalculate left = new ForkJoinCalculate(start, middle);
// 执行临界值左侧任务拆分
left.fork();
// 构建一个大于中间值的实例
ForkJoinCalculate right = new ForkJoinCalculate(middle + 1, end);
// 执行临界值右侧任务拆分
right.fork();
// 将左右两侧的值进行合并
return left.join() + right.join();
} else {
// 定义一个计数器计算总和
long sum = 0;
// 计算开始到结束的数字累加
for (long i = start; i < end; i++) {
sum += i;
}
// 返回累加值
return sum;
}
}
}

之后我们编写测试类进行调用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 定义开始时间
Instant start = Instant.now();
// 创建池
ForkJoinPool pool = new ForkJoinPool();
// 创建任务
ForkJoinTask<Long> task = new ForkJoinCalculate(0, 10000000000L);
// 执行任务
Long sum = pool.invoke(task);
// 定义结束时间
Instant end = Instant.now();
System.out.println("sum:" + sum);
System.out.println("耗费时间:" + Duration.between(start, end).toMillis());
============控制台信息============
sum:-5345475101129947011
耗费时间:2054

需要注意的是,ForkJoin拆分任务也是需要耗费时间的,所以一般只建议在大数据量下使用该方式进行调用,且具体的执行速度也受限于具体的拆分逻辑代码

同时我们也不难发现,这种方式编写起来较为复杂,使用起来不是太方便

那么JAVA8中我们如何简化ForkJoin的调用呢?

1
2
3
4
5
6
7
8
9
10
Instant start = Instant.now();
// 创建一个并行流使用parallel()
// 创建一个顺序流使用sequential()
long sum = LongStream.range(0, 10000000000L).parallel().reduce(0, Long::sum);
Instant end = Instant.now();
System.out.println("sum:" + sum);
System.out.println("耗费时间:" + Duration.between(start, end).toMillis());
============控制台信息============
sum:-5340232226128654848
耗费时间:1343