《Java核心技术卷Ⅰ》P242

概述

为什么要学函数式编程?

  • 易于使用并发编程,大数据量下,集合处理效率高:可以使用并行流,自动使用多线程方式处理。
  • 代码可读性高
  • 消灭嵌套地狱
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// 查询未成年作家评分在70分以上的书籍,由于流的影响所以作家和书籍可能会重复出现,所以要去重
List<Author> authors = new ArrayList<>();
List<Book> bookList = new ArrayList<>();
Set<Book> uniqueBookValues = new HashSet<>();
Set<Author> uniqueAuthorValues = new HashSet<>();

for (Author author : authors) {
if (uniqueAuthorValues.add(author)) {
if (author.getAge() < 18) {
List<Book> books = author.getBooks();
for (Book book : books) {
if (book.getScore() > 70) {
if (uniqueBookValues.add(book)) {
bookList.add(book);
}
}
}
}
}
}

System.out.println(bookList);

但如果改用函数式编程,代码会变得非常简单:

1
2
3
4
5
6
7
8
9
10
11
List<Author> authors = new ArrayList<>();

List<Book> collect = authors.stream()
.distinct()
.filter(author -> author.getAge() < 18)
.map(author -> author.getBooks())
.flatMap(Collection::stream)
.filter(book -> book.getScore() > 70)
.distinct()
.collect(Collectors.toList());
System.out.println(collect);

函数式编程思想

面向对象思想需要关注用什么对象完成什么事情。而函数式编程思想就类似于我们数学中的函数。它主要关注的是对数据进行了什么操 作。

优点:

  • 代码简洁,开发快速
  • 接近自然语言,易于理解
  • 易于"并发编程"

Lambda表达式

Lambda是JDK8中一个语法糖。它可以对某些匿名内部类的写法进行简化。它是函数式编程思想的一个重要体现,让我们不用关注是什 么对象,而是更关注我们对数据进行了什么操作。

核心原则:

可推导可省略。(如果一些参数的类型可以被推导出来,那么就可以省略它的类型;如果它的方法名可以被推导出来,就可以省略方法名)

格式:

(参数列表)->{代码}

例题

例一:

我们在创建线程并启动时可以使用匿名内部类的写法:

1
2
3
4
5
6
new Thread(new Runnable(){
@Override
public void run(){
System.out.println("你知道吗 我比你想象的 更想在你身边");
}
}).start();

可以使用Lambda的格式对其进行修改(因为Runnable中只有一个抽象方法需要重写,就能够推出),修改后如下:

1
2
3
4
new Thread(()->{
System.out.println("你知道吗 我比你想象的 更想在你身边");
}).start();
// new Thread(() -> System.out.println("你知道吗 我比你想象的 更想在你身边")).start();

例二: 现有方法定义如下,其中IntBinaryOperator是一个接口,先使用匿名内部类的写法调用该方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public static int calculateNum(IntBinaryOperator operator){
int a = 10;
int b = 20;
return operator.applyAsInt(a,b);
}

public static void main(string[] args){
int num = calculateNum(new IntBinaryOperator() {
@Override
public int applyAsInt(int left, int right) {
return left + right;
}
});
System.out.println(num);
}

小技巧

写完匿名内部类之后,将鼠标移动到该类上,按下alt + enter,如果出现了如图所示的情况,表明可以简化成lambda表达式。

(或者看这个类名是不是淡灰色)

Lambda写法:

1
2
3
4
5
6
7
8
public static void main(string[] args){
int num = calculateNum((int left, int right) -> {
return left + right;
});
// 后面会学到,还可以可以简化成下列形式
// int num = calculateNum((left, right) -> left + right);
System.out.println(num);
}

例三:

现有方法定义如下,其中IntPredicate是一个接口,先使用匿名内部类的写法调用该方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public static void printNum(IntPredicate predicate){
int[] arr = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
for (int i : arr) {
if(predicate.test(i)){
System.out.println(i);
}
}
}

public static void main(Strings[] args) {
printNum(new IntPredicate() {
@Override
public boolean test(int value) {
return value % 2 == 0;
}
})
}

Lambda写法:

1
2
3
4
5
6
public static void main(Strings[] args) {
printNum((int value)->{
return value % 2 == 0;
});
// printNum(value -> value % 2 == 0);
}

例四:

现有方法定义如下,其中Function是一个接口。先使用匿名内部类的写法调用该方法。

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
public static <R> R typeConver(Function<String, R> function) {
String str = "1235";
R result = function.apply(str);
return result;
}


public static void main(Strings[] args) {
Integer reslut = typeConver(new Function<String, Integer>() {
@Override
public Integer apply(String s) {
return Integer.valueOf(s);
}
});
System.out.println("reslut = " + reslut);

// 修改一下
String s = typeConver(new Function<String, String>() {
@Override
public String apply(String s) {
return s + "torch";
}
});
System.out.println(s);
}

Lambda写法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public static void main(Strings[] args) {
Integer reslut = typeConver((String s) -> {
return Integer.valueof(s);
});
// Integer reslut = typeConver(s -> Integer.valueOf(s));
System.out.println("reslut = " + reslut);

String s = typeConver((String s1) -> {
return s1 + "torch";
});
// String s = typeConver(s1 -> s1 + "torch");
// 注意使用lambda表达式的时候,里面的变量不能定义成与外面的变量名相同,但是匿名内部类的时候可以
System.out.println(s);
}

注:不管方法多复杂,牢记关注的是参数类型和方法体,() -> {}, 把参数类型填到()中,把方法体填到{}即可。

例五:

现有方法定义如下,其中IntConsumer是一个接口,先使用匿名内部类的写法调用该方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public static void foreachArr(IntConsumer consumer){
int[] arr = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
for(int i : arr) {
consumer.accept(i);
}
}

public static void main(Strings[] args) {
foreachArr(new IntConsumer() {
@Override
public void accept(int value) {
System.out.println(value);
}
});
}

Lambda写法:

1
2
3
4
5
6
public static void main(Strings[] args) {
foreachArr((int value) -> {
System.out.println(value);
});
// foreachArr(value -> System.out.println(value));
}

总结

可以不用一开始直接写lambda表达式,先写出匿名内部类的格式,然后优化成lambda表达式。(优化方法:只关注参数和方法体,参数填入(),方法体填入{},格式:(参数列表)->{方法体}

省略规则

  • 参数类型可以省略
  • 方法体只有一句代码时,大括号、return、这句代码的分号可以省略
  • 方法只有一个参数时小括号可以省略

省略原因是编译器能推导出。

Stream流

Java8的Stream使用的是函数式编程模式,如同它的名字一样,它可以被用来对集合或数组进行链状流式的操作。可以更方便的让我们 对集合或数组操作。(之前学习的InputStreamOutputStream是IO流,是读写数据用的)

例子:

1
2
3
4
5
6
// 打印所有年龄小于18的作家的名字,并且要注意去重
List<Author> authors = getAuthors();
authors.stream() // 把集合转换为流
.distinct() // 先去重
.filter(author -> author.getAge() < 18) // 筛选年龄小于18的
.forEach(author -> System.out.println(author.getName())); // 遍历打印名字

在idea中对Stream流进行调试

1.现在Stream流处打个断点。

2.点击debug,然后点击debug栏目的三个小点->点击Trace Current Stream Chain

3.出现如图界面,进入后得稍等会,等数据加载出来。点击上面的选项即可查看Stream流的走向。

常用操作

创建流

单列集合:集合对象.stream()

1
2
List<Author> authors = getAuthors();
Stream<Author> stream = authors.stream();

数组:Arrays.stream(数组)或者使用Stream.of来创建

1
2
3
Integer[] arr = {1, 2, 3, 4, 5};
Stream<Integer> stream = Arrays.stream(arr);
Stream<Integer> stream2 = Stream.of(arr);

双列集合:转换成单列集合后再创建

1
2
3
4
5
6
Map<String, Integer> map = new HashMap<>();
map.put("蜡笔小新", 19);
map.put("黑子", 17);
map.put("日向翔阳", 16);

Stream<Map.Entry<String, Integer>> stream = map.entrySet().stream();

中间操作

filter

可以对流中的元素进行条件过滤,符合过滤条件的才能继续留在流中。 例如: 打印所有姓名长度大于1的作家的姓名

1
2
3
4
List<Author> authors = getAuthors();
authors.stream()
.filter(author -> author.getName().length() > 1)
.forEach(author -> System.out.println(author.getName()));

map

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
// 示例1:字符串转大写
List<String> names = Arrays.asList("Alice", "Bob", "Charlie");
List<String> upperNames = names.stream()
.map(s -> s.toUpperCase())
// collect方法:将流中的元素 聚合 为特定类型的容器(如集合、字符串或自定义对象)
.collect(Collectors.toList()); // [ALICE, BOB, CHARLIE]

// 示例2:提取对象属性
class Person {
private String name;
public String getName() { return name; }
}

List<Person> people = ...;
List<String> names = people.stream()
.map(person -> person.getName())
.collect(Collectors.toList());

// 示例3:字符串转整数
List<String> numbers = Arrays.asList("1", "2", "3");
List<Integer> ints = numbers.stream()
.map(s -> Integer.parseInt(s))
.collect(Collectors.toList()); // [1, 2, 3]

// 示例4:值计算
authors.stream()
.map(author -> author.getAge())
.map(age -> age + 10)
.forEach(age -> System.out.println(age));

distinct

可以去除流中的重复元素

例如:

1
2
3
4
5
// 打印所有作家的姓名,并且要求其中不能有重复元素
List<Author> authors = getAuthors();
authors.stream()
.distinct()
.forEach(author -> System.out.println(author.getName()));

注意:distinct()方法是以来Object的equals方法来判断是否是相同对象的,所以需要注意重写equals方法(因为Object的equals方法是比较的是地址引用是否相同,如果要根据对象的内容或者其他来比较,就需要重写equals方法)。

sorted

空参

如果调用空参的sorted方法,会将流中的元素转换为Comparable接口类型,再进行排序,所以要使用sorted()空参方法,需要流中的元素实现Comparable接口。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// Author类
@Data
@NoArgsConstructor
@AllArgsConstructor
@EqualsAndHashCode
public class Author implements Comparable<Author> {
private Long id;
private String name;
private Integer age;
private String intro;
private List<Book> books;

@Override
public int compareTo(Author o) {
return o.getAge() - this.getAge();
}
}
1
2
3
4
5
6
7
8
9
// 测试类中的方法
public static void test01() {
List<Author> authors = getAuthors();
authors.stream()
.distinct()
.sorted()
.forEach(author -> System.out.println(author.getAge()));

}
有参

先写出匿名内部类的形式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public static void test01() {
List<Author> authors = getAuthors();
authors.stream()
.distinct()
.sorted(new Comparator<Author>() {
@Override
public int compare(Author o1, Author o2) {
return o1.getAge() - o2.getAge();
}
})
.forEach(new Consumer<Author>() {
@Override
public void accept(Author author) {
System.out.println(author.getAge());
}
});
}

再转换成lambda表达式

1
2
3
4
5
6
7
public static void test01() {
List<Author> authors = getAuthors();
authors.stream()
.distinct()
.sorted((o1, o2) -> o1.getAge() - o2.getAge())
.forEach(author -> System.out.println(author.getAge()));
}

limit

可以设置流的最大长度,超出的部分将被抛弃。

例如:

1
2
3
4
5
6
7
8
9
public static void test02() {
// 对流中的元素按照年龄进行降序排序,并且要求不能有重复元素,然后打印其中年龄最大的两个作家的姓名。
List<Author> authors = getAuthors();
authors.stream()
.distinct()
.sorted((o1, o2) -> o2.getAge() - o1.getAge())
.limit(2)
.forEach(author -> System.out.println(author.getName()));
}

skip

跳过流中的前n个元素,返回剩下的元素。

例如:

1
2
3
4
5
6
7
8
9
public static void test03() {
// 打印除了年龄最大的作家外的其他作家,要求不能有重复元素,并且按照年龄降序排序。
List<Author> authors = getAuthors();
authors.stream()
.distinct()
.sorted((o1, o2) -> o2.getAge() - o1.getAge())
.skip(1)
.forEach(author -> System.out.println(author.getName()));
}

flatmap

  • map:一对一转换,每个元素生成一个结果。
  • flatMap:一对多转换,每个元素生成一个流,最终合并为单一流。

flatMap是把每个元素转换成一个流,然后把所有流合并成一个流。比如,如果有一个列表的列表,用flatMap可以把它们展平成一个流。而map只是每个元素一对一地转换,不会展开。

那如果流的元素是更复杂的对象,比如订单,每个订单有多个商品,这时候想获取所有商品的流,这时候应该用flatMap,而不是map。比如,orders.stream().flatMap(order -> order.getItems().stream()); 这样就把每个订单的商品列表转成流,然后合并成一个流。

例一:

1
2
3
4
5
6
7
8
9
public static void test04() {
// 打印所有书籍的名字,要求对重复的元素进行去重
val authors = getAuthors();
authors.stream()
.flatMap((Function<Author, Stream<Book>>) author -> author.getBooks().stream())
.distinct()
.forEach(book -> System.out.println(book.getName()));

}

例二:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
public static void test05() {
// 打印现有数据的所有分类,要求对分类进行去重,不能出现这种格式:哲学,爱情
List<Author> authors = getAuthors();
authors.stream()
.flatMap(new Function<Author, Stream<Book>>() {
@Override
public Stream<Book> apply(Author author) {
return author.getBooks().stream();
}
})
// 对书籍进行去重
.distinct()
// 数组转换成流对象:Arrays.stream(), 或Stream.of()
.flatMap(new Function<Book, Stream<String>>() {
@Override
public Stream<String> apply(Book book) {
return Stream.of(book.getCategory().split(","));
}
})
// 对分类进行去重
.distinct()
.forEach(category -> System.out.println(category));

}

lambda:

1
2
3
4
5
6
7
8
9
10
11
12
13
public static void test05() {
// 打印现有数据的所有分类,要求对分类进行去重,不能出现这种格式:哲学,爱情
List<Author> authors = getAuthors();
authors.stream()
.flatMap(author -> author.getBooks().stream())
// 对书籍进行去重
.distinct()
// 数组转换成流对象:Arrays.stream(), 或Stream.of()
.flatMap(book -> Stream.of(book.getCategory().split(",")))
// 对分类进行去重
.distinct()
.forEach(category -> System.out.println(category));
}

终结操作

终结操作是流处理过程中的最后一个操作,它会触发流的遍历和处理,并返回一个最终的结果,例如集合、列表、数组等。在Java中,Stream流的终结操作通常会返回一个集合对象或者一个值。

通过终结操作,可以对流进行遍历和处理,并得到最终的结果。在使用终结操作时,需要注意流只能被消费一次,也就是说一旦对流进行了终结操作,就不能再使用该流进行其他操作了。因此,在使用终结操作前,需要确保已经完成了所有需要的中间操作。

forEach

对流中的元素进行遍历操作,通过传入的参数去指定对遍历到的元素进行什么具体的操作。

例子:

1
2
3
4
5
6
7
8
public static void test06() {
// 打印所有作家的名字
List<Author> authors = getAuthors();
authors.stream()
.map( author -> author.getName())
.distinct()
.forEach(name -> System.out.println(name));
}

count

可以用来获取当前流中元素的个数(count()不用传参)

例子:

1
2
3
4
5
6
7
8
9
public static void test07() {
// 打印这些作家的所出版的书籍的数目,注意删除重复元素
List<Author> authors = getAuthors();
long count = authors.stream()
.flatMap(author -> author.getBooks().stream())
.distinct()
.count();
System.out.println("count = " + count);
}

max&min

可以用来求流中的最值

返回值是Optional<>类型

例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public static void test08() {
// 分别获取这些作家的所出书籍的最高分和最低分并打印
List<Author> authors = getAuthors();
Optional<Integer> max = authors.stream()
.flatMap(author -> author.getBooks().stream())
.map(book -> book.getScore())
.max((score1, score2) -> score1 - score2);

// 上面的流经过终结操作之后,被消费完了,要再创建一个流
Optional<Integer> min = authors.stream()
.flatMap(author -> author.getBooks().stream())
.map(book -> book.getScore())
.min((score1, score2) -> score1 - score2);

System.out.println(max.get());
System.out.println(min.get());
}

collect

把当前流转换成一个集合

例子:

使用collect(),一般不写匿名内部类,因为太多参数了,直接使用Collectors,这是collect的一个工具类,通过.出想要的方法。

1
2
3
4
5
6
7
8
9
public static void test09() {
// 获取一个存放所有作者名字的List集合
List<Author> authors = getAuthors();
List<String> nameList = authors.stream()
.map(author -> author.getName())
// 使用collect(),一般不写匿名内部类,因为太多参数了,直接使用Collectors,这是collect的一个工具类,通过.出想要的方法。
.collect(Collectors.toList());
System.out.println(nameList);
}

例子:

1
2
3
4
5
6
7
8
9
public static void test010() {
// 获取一个所有书名的Set集合
List<Author> authors = getAuthors();
Set<String> books = authors.stream()
.flatMap(author -> author.getBooks().stream())
.map(book -> book.getName())
.collect(Collectors.toSet());
System.out.println(books);
}

例子:

匿名内部类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public static void test011() {
// 获取一个Map集合,map的key为作者名,value为List<Book>
List<Author> authors = getAuthors();
Map<String, List<Book>> map = authors.stream()
.distinct()
.collect(Collectors.toMap(new Function<Author, String>() {
@Override
public String apply(Author author) {
return author.getName();
}
}, new Function<Author, List<Book>>() {
@Override
public List<Book> apply(Author author) {
return author.getBooks();
}
}));
System.out.println(map);
}

lambda:

1
2
3
4
5
6
7
8
public static void test011() {
// 获取一个Map集合,map的key为作者名,value为List<Book>
List<Author> authors = getAuthors();
Map<String, List<Book>> map = authors.stream()
.distinct()
.collect(Collectors.toMap(author -> author.getName(), author -> author.getBooks()));
System.out.println(map);
}

anyMatch

可以用来判断是否有任意符合匹配条件的元素,结果为boolean类型

例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public static void test012() {
// 判断是否有年龄在29以上的作家
List<Author> authors = getAuthors();
boolean flag = authors.stream()
// lambda表达式
// .anyMatch(author -> author.getAge() > 29);
.anyMatch(new Predicate<Author>() {
@Override
public boolean test(Author author) {
return author.getAge() > 29;
}
});
System.out.println(flag);
}

allMatch

可以用来判断是否都符合匹配条件,结果为boolean类型,如果都符合结果为true,否则结果为false。

例子:

1
2
3
4
5
6
7
public static void test013() {
// 判断是否所有的作家都是成年人
List<Author> authors = getAuthors();
boolean flag = authors.stream()
.allMatch(author -> author.getAge() >= 18);
System.out.println(flag);
}

noneMatch

可以判断流中的元素是否都不符合匹配条件,如果都不符合结果为true,否则结果为false。

例子:

1
2
3
4
5
6
7
8
public static void test014() {
// 判断作家是否都没有超过100岁的
// 都没有超过结果为true(是),都超过了结果为false(否)
List<Author> authors = getAuthors();
boolean flag = authors.stream()
.noneMatch(author -> author.getAge() > 100);
System.out.println(flag);
}

findAny

获取流中的任意一个元素,该方法没有办法保证获取的一定是流中的第一个元素。

例子:

1
2
3
4
5
6
7
8
9
10
11
public static void test015() {
// 获取任意一个大于18的作家,如果存在就输出他的名字
List<Author> authors = getAuthors();
Optional<Author> any = authors.stream()
.filter(author -> author.getAge() > 10)
.findAny();

// ifPresent()是Optional类型的一个方法,用于判断值存不存在,如果存在则执行括号里面的方法
// 括号里面的参数类型是Consumer,可以用lambda表达式
any.ifPresent(author -> System.out.println(author.getName()));
}

findFirst

获取流中的第一个元素

一般用findFirst比较多,因为findAny是获取任意一个元素

例子:

解法1:

1
2
3
4
5
6
7
8
public static void test016() {
// 获取一个年龄最小的作家,并输出他的姓名
List<Author> authors = getAuthors();
Optional<Author> first = authors.stream()
.sorted((o1, o2) -> o1.getAge() - o2.getAge())
.findFirst();
first.ifPresent(author -> System.out.println(author.getName()));
}

解法2:

1
2
3
4
5
6
7
public static void test016() {
// 获取一个年龄最小的作家,并输出他的姓名
List<Author> authors = getAuthors();
Optional<Author> min = authors.stream()
.min(((o1, o2) -> o1.getAge() - o2.getAge()));
min.ifPresent(author -> System.out.println(author.getName()));
}

reduce

对流中的数据按照你指定的计算方式计算出一个结果。(缩减操作) reduce的作用是把stream中的元素给组合起来,我们可以传入一个初始值,它会按照我们的计算方式依次拿流中的元素和初始化值进行计算,计算结果再和后面的元素计算。 reduce两个参数的重载形式内部的计算方式如下:

1
2
3
4
T result = identity;
for (T element : this stream)
result accumulator.apply(result,element);
return result;

其中identity就是我们可以通过方法参数传入的初始值,accumulator的apply具体进行什么计算也是我们通过方法参数来确定的。

在Java Stream API中,reduce方法是一个非常重要的终端操作,用于将流中的元素归约到一个单一的结果。根据不同的需求,reduce方法有三种重载形式,分别接受1个参数、2个参数和3个参数。以下是对这三种参数形式的详细解释:

1. 一个参数的reduce方法
1
Optional<T> reduce(BinaryOperator<T> accumulator);
  • 功能:此方法接受一个BinaryOperator作为参数,用于定义如何累积流中的元素。

  • 特点:

    • 返回类型是Optional<T>,因为流可能为空,结果可能是null
    • 如果流为空,则返回Optional.empty()
  • 工作原理:

    • 部分源码解析:

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      boolean foundAny = false; 
      T result = null;
      for (T element : this stream) {
      if (!foundAny) {
      foundAny = true;
      result = element; // 初始化值设置成流中的第一个元素
      }
      else
      result = accumulator. apply(result, element);
      }
      return foundAny ? Optional. of(result) : Optional. empty();
    • 使用提供的BinaryOperator对流中的元素进行累积操作。

    • 初始值为流的第一个元素,后续元素依次与当前结果进行运算。

    • 示例代码:

1
2
3
List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5);
Optional<Integer> sum = numbers.stream().reduce((a, b) -> a + b);
sum.ifPresent(System.out::println); // 输出15
  • 适用场景:适用于需要对流中的元素进行简单累积操作的场景,例如求和、连接字符串等。
2. 两个参数的reduce方法
1
T reduce(T identity, BinaryOperator<T> accumulator);
  • 功能:此方法接受一个初始值(identity)和一个累积器(accumulator)。
  • 特点:
    • identity是累积操作的初始值,必须是函数式接口BinaryOperator的一个固定点,即满足identity.apply(identity, t) == t
    • 返回类型是T,表示最终结果与流中的元素类型一致。
    • 如果流为空,则返回提供的初始值。
  • 工作原理:
    • 初始值作为累积操作的起点。
    • 流中的每个元素依次与当前累积结果进行运算。
    • 示例代码:
1
2
List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5);
int sum = numbers.stream().reduce(0, (a, b) -> a + b); // 输出15
  • 适用场景:适用于需要指定初始值的累积操作,例如求和、累乘等。
3. 三个参数的reduce方法
1
<U> U reduce(U identity, BiFunction<U, ? super T, U> accumulator, BinaryOperator<U> combiner);
  • 功能:此方法接受一个初始值(identity)、一个累加器(accumulator)和一个合并器(combiner)。
  • 特点:
    • identity是累积操作的初始值。
    • accumulator用于对单个流元素进行累积操作。
    • combiner用于合并多个线程的累积结果(仅在并行流中生效)。
    • 返回类型是泛型U,表示最终结果可以与流中的元素类型不同。
  • 工作原理:
    • 在并行流中,多个线程会分别计算各自的累积结果。
    • 使用combiner将这些局部结果合并为一个全局结果。
    • 示例代码:
1
2
List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5);
int sum = numbers.stream().parallel().reduce(0, (a, b) -> a + b, (a, b) -> a + b); // 输出15
  • 适用场景:适用于需要并行处理的复杂累积操作,例如在并行流中合并多个线程的结果

例子:

两个参数的

1
2
3
4
5
6
7
8
public static void test017() {
// 使用reduce求所有作者的年龄和
List<Author> authors = getAuthors();
Integer sum = authors.stream()
.map(author -> author.getAge())
.reduce(0, (Integer result, Integer element) -> result + element);
System.out.println("sum = " + sum);
}

之前的min/max方法其实底层还是用的是reduce方法的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public static void test018() {
// 使用reduce求所有作者中年龄的最大值
List<Author> authors = getAuthors();
Integer max = authors.stream()
.map(author -> author.getAge())
// lambda
// .reduce(Integer.MIN_VALUE, (result, element) -> result > element ? result : element);
.reduce(Integer.MIN_VALUE, new BinaryOperator<Integer>() {
@Override
public Integer apply(Integer result, Integer element) {
return result > element ? result : element;
}
});
System.out.println(max);
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public static void test019() {
// 使用reduce求所有作者中年龄的最小值
List<Author> authors = getAuthors();
Integer min = authors.stream()
.map(author -> author.getAge())
// lambda
// .reduce(Integer.MAX_VALUE, (result, element) -> result < element ? result : element);
.reduce(Integer.MAX_VALUE, new BinaryOperator<Integer>() {
@Override
public Integer apply(Integer result, Integer element) {
return result < element ? result : element;
}
});
System.out.println(min);
}

一个参数的

1
2
3
4
5
6
7
8
public static void test020() {
// 使用reduce求所欲作者中年龄的最小值
List<Author> authors = getAuthors();
Optional<Integer> minOptional = authors.stream()
.map(author -> author.getAge())
.reduce((result, element) -> result < element ? result : element);
minOptional.ifPresent(age -> System.out.println(age));
}

注意事项

  • 惰性求值(如果没有终结操作,没有中间操作是不会得到执行的)
  • 流是一次性的(一旦一个流对象经过一个终结操作后。这个流就不能再被使用)
  • 不会影响原数据(我们在流中可以多数据做很多处理。但是正常情况下是不会影响原来集合中的元素的,除非引用了原来元素的地址值,进行修改内容,比如author -> author.setAge(10);。这往往也是我们期望的)

Optional

我们在编写代码的时候出现最多的就是空指针异常,所以在很多情况下我们需要做各种非空的判断。 例如:

1
2
3
4
Author author = getAuthor();
if(author!=nul1){
System.out.println(author.getName());
}

尤其是对象中的属性还是一个对象的情况下,这种判断会更多。 而过多的判断语句会让我们的代码显得臃种不堪。 所以在DK8中引入了Optional,养成使用Optional的习惯后你可以写出更优雅的代码来避免空指针异常。 并且在很多函数式编程相关的API中也都用到了Optional,如果不会使用Optionalt也会对函数式编程的学习造成影响。

使用

创建对象

Optional 是 Java 中用于优雅处理空指针异常的工具类,其核心思想是通过将数据封装在 Optional 对象内部,借助其内置方法安全地操作可能为空的数据。

ofNullable(常用)

我们一般使用Optional的静态方法ofNullable来把数据封装成一个Optional对象,无论传入的参数是否为null都不会出现问题。

源码:

1
2
3
public static <T> Optional<T> ofNullable(T value) {
return value == null ? empty() : of(value);
}

例子:

1
2
Author author = getAuthor();
Optional<Author> authorOptional = Optional.ofNullable(author);

你可能会觉得还要加一行代码来封装数据比较麻烦。但是如果改造下getAuthor方法,让其的返回值就是封装好的Optional的话,我们再使用时就会方便很多。而且在实际开发中我们的数据很多是从数据库获取的,Mybatis/从3.5版本可以也已经支持Optional了。我们可以直接把dao方法的返回值类型定义成Optional类型,MyBastis会自己把数据封装成Optionals对象返回,封装的过程也不需要我们自己操作。

of(不常用)

如果你确定一个对象不是空的则可以使用Optional的静态方法of来把数据封装成Optional对象。

1
2
Author author = new Author();
Optional<Author> authorOptional = Optional.of(author);

但是一定要注意,如果使用of的时候传入的参数必须不为null。(传入null会出现空指针异常)

empty(不常用)

如果一个方法的返回值类型是Optional类型,而如果我们经判断发现某次计算得到的返回值为null,这个时候就需要把null封装成Optional对象返回,可以使用Optional的静态方法empty来进行封装。

1
Optional.empty();

安全消费值

我们获取到一个Optional对象后肯定需要对其中的数据进行使用,这时候我们可以使用其ifPresent方法来消费其中的值。这个方法会判断其内封装的数据是否为空,不为空时才会执行具体的消费代码,这样使用起来就更加安全。

例如,以下写法就优雅的避免了空指针异常。

1
2
Optional<Author> authoroptional = Optional.ofNullable(getAuthor());
authoroptional.ifPresent(author -> System.out.println(author.getName()));

获取值

如果我们想获取值自己进行处理可以使用get方法获取,但是不推荐。因为当Optionall内部的数据为空的时候会出现异常。

安全获取值

如果我们期望安全的获取值。我们不推荐使用get方法,而是使用Optional提供的以下方法。

  • orElseGet 获取数据并且设置数据为空时的默认值。如果数据不为空就能获取到该数据,如果为空则根据你传入的参数来创建对象作为默认值返回。

    1
    2
    Optional<Author> authorOptional = Optional.ofNullable(getAuthor());
    Author author = authorOptional.orElseGet(() -> new Author(2L, "蒙多", 33, "一个从菜刀中明悟哲理的祖安人", null));
  • orElseThrow

    获取数据,如果数据不为空就能获取到该数据,如果为空则根据你传入的参数来创建异常抛出。

    1
    2
    3
    4
    5
    6
    7
    Optional<Author> authorOptional = Optional.ofNUllable(getAuthor());
    try {
    Author author = authorOptional.orElseThrow((Supplier<Throwable>) () -> new RuntimeException("author为空"));
    System.out.println(author.getName());
    } catch (Throwable throwable) {
    throwable.printStackTrace();
    }

过滤

我们可以使用filter方法对数据进行过滤,如果原本是有数据的,但不符合判断,也会变成一个无数据的Optional对象。

1
2
Optional<Author> authorOptional = Optional.ofNUllable(getAuthor());
authorOptional.filter(author -> author.getAge() > 100).ifPresent(author ->System.out.println(author.getName()));

判断

我们可以使用isPresent方法进行是否存在数据的判断,如果为空返回值为false,如果不为空,返回值为true。但是这种方式并不能体现Optional的好处,更推荐ifPresent方法。

1
2
3
4
Optional<Author> authorOptional = Optional.ofNullable(getAuthor());
if (authorOptional.isPresent()){
System.out.println(authorOptional.get().getName());
}

数据转换

Optionali还提供了map可以让我们的对数据进行转换,并且转换得到的数据也还是被Optionalt包装好的,保证了我们的使用安全。

例如我们想获取作家的书籍集合。

1
2
3
4
5
6
7
8
Optional<Author> authorOptional = Optional.ofNullable(getAuthor());
Optional<List<Book>> books = authorOptional.map(author -> author.getBooks());
books.ifPresent(new Consumer<List<Book>>() {
@Override
public void accept(List<Book> books) {
books.forEach(book -> System.out.println(book.getName()));
}
});

函数式接口

概述

只有一个抽象方法的接口称之为函数接口。

JDK的函数式接口都加上了@FunctionalInterface注解进行标识,但是无论是否加上该注解只要接口中只有一个抽象方法,都是函数式接口。

常见函数式接口

  • Consumer消费接口

    接受一个参数 T 并执行操作,无返回值。

    1
    2
    3
    4
    5
    @FunctionalInterface
    public interface Consumer<T> {
    void accept(T t); // 核心方法:消费参数
    default Consumer<T> andThen(Consumer<? super T> after) { ... } // 链式调用
    }
  • Functional计算转换接口

    表示接受一个输入参数 T 并返回结果 R 的函数。

    1
    2
    3
    4
    5
    6
    7
    8
    @FunctionalInterface
    public interface Function<T, R> {
    R apply(T t); // 核心方法:执行转换操作
    // 组合方法
    default <V> Function<V, R> compose(Function<? super V, ? extends T> before) { ... }
    default <V> Function<T, V> andThen(Function<? super R, ? extends V> after) { ... }
    static <T> Function<T, T> identity() { return t -> t; } // 返回输入本身
    }
  • Predicate判断接口

    接受参数 T 并返回布尔值。

    1
    2
    3
    4
    5
    6
    7
    @FunctionalInterface
    public interface Predicate<T> {
    boolean test(T t); // 核心方法:条件判断
    default Predicate<T> and(Predicate<? super T> other) { ... } // 逻辑与
    default Predicate<T> negate() { ... } // 逻辑非
    static <T> Predicate<T> isEqual(Object targetRef) { ... } // 对象相等判断
    }
  • Supplier生产型接口

    无输入参数,返回一个结果 T

    1
    2
    3
    4
    @FunctionalInterface
    public interface Supplier<T> {
    T get(); // 核心方法:提供数据
    }

在IDEA中查看函数式接口,首先随便定义一个函数式接口(如Consumer),点进去源码,然后点击下面按钮即可。

常用的默认方法

  • and

​ 在使用Predicate接口时候可能需要进行判断条件的拼接。而and方法相当于是使用&&来拼接两个判断条件。例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// 打印作家中年龄大于17并且姓名的长度大于1的作家。
List<Author> authors = getAuthors();
Stream<Author> authorStream = authors.stream();
authorstream.filter(new Predicate<Author>() {
@Override
public boolean test(Author author){
return author.getAge() > 17;
}.and(new Predicate<Author>() {
@Override
public boolean test(Author author){
return author.getName().length() > 1;
}
})).forEach(author -> System.out.println(author));

/* 以下这样写也可以
authors.stream()
.filter(author -> author.getAge() > 17 && author.getName().length() > 1)
.forEach(author -> System.out.println(author));
*/
  • or 在使用Predicate接口时候可能需要进行判断条件的拼接。而or方法相当于是使用||来拼接两个判断条件。

  • negate

    Predicate接口中的方法,negate方法相当于是在判断前面加了个!表示取反。

    1
    2
    3
    4
    5
    6
    7
    8
    public static void test022() {
    // 打印作家中年龄不大于17的作家
    List<Author> authors = getAuthors();
    authors.stream()
    .filter(((Predicate<Author>)author -> author.getAge() > 17).negate())
    // .filter((author -> !(author.getAge() > 17)))
    .forEach(author -> System.out.println(author));
    }

Predicate中,and方法和在lambda中使用&&的区别?(其他也是,or和||,在lambda表达式中写&&更简洁,为什么还要and方法呢)

  • && 只能在 同一个 lambda 表达式内部 硬编码组合条件

  • 无法复用独立条件:每个条件被强制绑定在同一个 lambda 中,无法单独传递或复用已有 Predicate 对象。

  • 动态组合条件:and 方法允许在运行时 动态组合多个独立的条件,无需预先知道所有条件:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    Predicate<String> baseCondition = s -> s.startsWith("A");

    // 动态添加条件
    if (someFlag) {
    baseCondition = baseCondition.and(s -> s.contains("B"));
    }
    if (anotherFlag) {
    baseCondition = baseCondition.and(s -> s.endsWith("C"));
    }

    // 最终组合条件:A && B && C(根据标志位动态生成)
  • 链式调用与代码复用:可以利用已有的 Predicate 对象,通过链式调用构建复杂逻辑:

    1
    2
    3
    4
    5
    6
    7
    8
    Predicate<Integer> isEven = n -> n % 2 == 0;
    Predicate<Integer> isPositive = n -> n > 0;

    // 复用已有条件构建新条件
    Predicate<Integer> isEvenAndPositive = isEven.and(isPositive);

    // 进一步扩展
    Predicate<Integer> isEvenPositiveAndLessThan100 = isEvenAndPositive.and(n -> n < 100);

方法引用

在使用lambda时,如果方法体中只有一个方法的调用(包括构造方法),可以使用方法引用进一步简化代码。

我们在使用lambda时不需要考虑什么时候使用方法引用,用哪种方法引用,方法引用的格式是什么。只需要在写完lambda方法之后,如果发现方法体只有一行代码,并且是方法的调用时,使用快捷键尝试是否能够转换方法引用即可。

语法

引用类的静态方法

简记:==类名.静态方法(参数) -> 类名::静态方法==

格式:类名::方法名

使用前提:如果我们在重写方法的时候,方法体中只有一个方法,并且这行代码是调用了某个类的静态方法,并且我们把要重写的抽象方法中所有的参数,这个时候我们就可以引用类的静态方法。

例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public static void test023() {
List<Author> authors = getAuthors();
Stream<Author> authorStream = authors.stream();
authorStream.map(new Function<Author, Integer>() {
@Override
public Integer apply(Author author) {
return author.getAge();
}
})
// 以上这个匿名内部类不能转换成这种引用类的静态方法的方法引用(可以转换成其他的方法引用),因为不满足要重写的抽象方法中所有的参数都按顺序传入了这个静态方法中
// .map(Author::getAge)
.map(new Function<Integer, String>() {
@Override
public String apply(Integer age) {
return String.valueOf(age);
}
});
// .map(String::valueOf);
}

引用对象的实例方法

简记:==对象.方法(对象) -> 对象::方法==

与引用类的实例方法的区别:==第一个对象不是参数,此处括号的对象是参数==

格式:对象名::方法名

使用前提:如果我们在重写方法的时候,方法体中只有一个方法,并且这行代码是调用了某个对象的成员方法,并且我们把要重写的抽象方法中所有的参数都按照顺序传入这个成员方法中,这个时候我们就可以引用对象的实例方法。

例:

1
2
3
4
5
6
7
8
9
List<Author> authors = getAuthors();
Stream<Author> authorStream = authors.stream();
StringBuilder sb = new StringBuilder();
authorStream.map(author -> author.getName())
// 不符合引用类的静态方法和引用对象的实例方法,但是符合引用类的实例方法,所以可以转换成以下这种格式,
// .map(Author::getName())
.forEach(name -> sb.append(name));
// 符合引用对象的实例方法,可以转换成以下这种格式
// .forEach(sb::append)

引用类的实例方法

简记:==对象.方法() -> 类名::方法==

与引用对象的实例方法的区别:==第一个对象是第一个参数==

格式:类名::方法名

使用前提:如果我们在重写方法的时候,方法体中只有一个方法,并且这行代码是调用了第一个参数的成员方法,并且我们把要重写的抽象方法中剩余的所有的参数都按照顺序传入了这个成员方法中,这个时候我们就可以引用类的实例方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public class MethodDemo {
interface UseString {
String use(String str, int start, int length);
}

public static String subAuthorName(String str, UseString useString) {
int start = 0;
int length = 1;
return useString.use(str, start, length);
}

public static void main(String[] args) {
subAuthorName("torch", new UseString() {
@Override
public String use(String str, int start, int length) {
// 只调用了第一个参数
return str.substring(start, length);
}
});
// 可以转换成以下
// subAuthorName("torch", String::substring);
}
}

构造器引用

如果方法体中的一行代码是构造器的话就可以使用构造器引用。

格式:类名::new

使用前提:如果我们在重写方法的时候,方法体中只有一个方法,并且这行代码是调用了某个类的构造方法,并且我们要把重写的抽象方法中的所有参数都按照顺序传入了这个构造方法中,这个时候我们就可以引用构造器。

例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public static void test025() {
List<Author> authors = getAuthors();
authors.stream()
.map(author -> author.getName())
// 可以转换成
// .map(Author::getName)
.map(name -> new StringBuilder(name))
// 符合构造器引用,可以转换成
// .map(StringBuilder::new)
.map(sb -> sb.append("torch").toString())
// 不符合只有一个方法的条件,链式调用,两个方法,append和toString,所以不能转换成方法引用
.forEach(str -> System.out.println(str));
// 可以转换成
// .forEach(System.out::println);
}

高级用法

基本数据类型优化

我们之前用到的很多Stream的方法由于都使用了泛型,所以涉及到的参数和返回值都是引用数据类型。

即使我们操作的是整数小数,但是实际用的都是他们的包装类。JDK5中引入的自动装箱和自动拆箱让我们在使用对应的包装类时就好像使用基本数据类型一样方便,但是装箱和拆箱是要消耗时间的,如果流中有大量的数据,那么消耗的时间非常多。

所以为了让我们能够对这部分的时间消耗进行优化。Stream还提供了很多专门针对基本数据类型的方法。 例如:mapToInt,mapToLong,mapToDouble,flatMapToInt,flatMapToDouble等。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public static void test26(){
List<Author>authors = getAuthors();
authors.stream()
.map (author -> author.getAge())
.map(age -> age + 10)
.filter(age -> age > 18)
.map(age -> age + 2)
.forEach(System.out::println);
// 用mapToInt转换,变成了IntStream,减少了大量装箱拆箱的时间,可以改成以下形式,
/*
authors.stream()
.mapToInt(author -> author.getAge())
.map(age -> age + 10)
.filter(age -> age > 18)
.map(age -> age + 2)
.forEach(System.out::println);
*/
}

并行流

​ 当流中有大量元素时,我们可以使用并行流去提高操作的效率。其实并行流就是把任务分配给多个线程去完成。如果我们自己去用代码实现的话其实会非常的复杂,并且要求你对并发编程有足够的理解和认识,而如果我们使用Stream的话,我们只需要修改一个方法的调用就可以使用并行流来帮我们实现,从而提高效率。

串行:

1
2
3
4
5
6
7
8
public static void test027() {
Stream<Integer> stream = Stream.of(1,2,3,4,5,6,7,8,9,10);
Integer sum = stream.peek(num -> System.out.println(num + ": " + Thread.currentThread().getName()))
.filter (num -> num > 5)
.reduce((result,element) -> result + element)
.get();
System.out.println (sum);
}

输出结果:

并行:

1
2
3
4
5
6
7
8
9
10
11
public static void test027() {
Stream<Integer> stream = Stream.of(1,2,3,4,5,6,7,8,9,10);
// .parallel()开启并行流,会有多个线程来执行
// 或者直接stream.parallelStream()得到一个并行流对象
Integer sum = stream.parallel()
.peek(num -> System.out.println(num + ": " + Thread.currentThread().getName()))
.filter (num -> num > 5)
.reduce((result,element) -> result + element)
.get();
System.out.println (sum);
}

输出结果: