文章目录
函数式编程思想:
面向对象思想需要关注用什么对象完成什么事情,儿函数式编程思想类似于我们数学中的函数。它关注的是对数据进行了什么操作。
优点:
- 代码简介,开发快速
- 接近自然语言,易于理解
- 易于"并发编程"
Stream流前置知识
一、Lambda表达式
概述
Lambda是JDK8中的一个语法糖。他可以对某些匿名内部类的写法进行简化,他是函数式编程思想的一个重要体现。让我们不用关注是什么对象,而是更关注我们对数据进行了什么操作。
核心原则:可推倒可省略
不关注类名,方法名。只关注方法体!
基本格式
(参数类型 参数名称) ->{
方法体;
}
例一:多线程中创建线程的写法
new Thread(new Runnable(){
@Override
public void run(){
System.out.println("我是新建线程1的输出");
}
}).start();
Lambda写法:
new Thread(()->{
System.out.println("我是新建线程2的输出")
}).start();
例二:
现有方法定义如下,其中IntBinaryOperation
是一个接口,先试用匿名内部类的写法调用该方法,再用Lambda
写法。
public class Lambda01 {
public static void main(String[] args) {
System.out.println(calculateNum(new IntBinaryOperator() {
@Override
public int applyAsInt(int left, int right) {
return left + right;
}
}));
}
/**
* 操作两个数
* @param binaryOperator
* @return applyAsInt
*/
public static int calculateNum(IntBinaryOperator binaryOperator){
int a = 10;
int b = 20;
return binaryOperator.applyAsInt(a,b);
}
}
Lambda写法:
public class Lambda01 {
public static void main(String[] args) {
System.out.println(calculateNum((int left,int right)->{
return left+right;
}));
}
/**
* 操作两个数
* @param binaryOperator
* @return applyAsInt
*/
public static int calculateNum(IntBinaryOperator binaryOperator){
int a = 10;
int b = 20;
return binaryOperator.applyAsInt(a,b);
}
}
Lambda省略规则
- 参数类型可以省略
- 方法体中如果只有一句代码,
{}
,return
和唯一一句代码的;
可以省略 - 方法只有一个参数时
()
可以省略
Lambda总结
1.使用前提:
Lambda表达式语法是非常简洁的,但是Lambda
表达式不是随便使用的,使用时有几个条件要特别注意:
- 方法的参数或局部变量类型必须是接口才能使用Lambda
- 接口中有且仅有一个抽象方法(@FunctionalIneterface)
2.与匿名内部类的对比
-
所需类型不同
匿名内部类的类型可以是:类,抽象类,接口。
Lambda表达式需要的类型必须是
接口
。 -
抽象发发数量不同
匿名内部类所需的接口中的抽象方法的数量是随意的。
Lambda表达式所需的接口中只能有
一个抽象方法
-
实现原理不同
匿名内部类是在编译后形成.class
Lambda表达式是在程序运行的时候
动态生成.class
二、方法引用 | ::
在使用Lambda时,如果方法体中只有一个方法的调用的话(包括构造方法),我们可以用发发引用进一步简化代码
类名或者对象名 :: 方法名
推荐用法:
如果lambda方法体中只有一行代码,并且是方法的调用。
java8方法引用有四种形式:
- 静态方法引用 : ClassName :: staticMethodName
- 构造器引用 : ClassName :: new
- 类的任意对象的实例方法引用: ClassName :: instanceMethodName
- 特定对象的实例方法引用 : object :: instanceMethodName
三、Optional
概述
本质上,Optional
是一个包含有可选值的包装类,这意味着 Optional 类既可以含有对象也可以为空。我们要知道,Optional
是 Java 实现函数式编程的强劲一步,并且帮助在范式中实现。但是 Optional
的意义显然不止于此。我们知道,任何访问对象方法或属性的调用都可能导致 NullPointerException
,在这里,我举个简单的例子来说明一下:
我们在编写代码的时候经常会出现空指针异常,为了避免空指针异常我们需要做各种非空的判断:
Author author = getAuthor();
if(author != null){
System.out.println(author.getName());
}
尤其是对象中的属性还是一个对象的情况下。这种判断会更多。而过多的判断会让我们的代码显得很臃肿。
Optional实际上是个容器:它可以保存类型T的值,或者仅仅保存null。Optional提供很多有用的方法,这样我们就不用显式进行空值检测。
使用
Optional的构造函数
Optional 的三种构造方式:Optional.of(obj)
, Optional.ofNullable(obj)
和明确的 Optional.empty()
Optional.of(obj):它要求传入的 obj
不能是 null
值的, 否则直接报NullPointerException
异常。
Optional.ofNullable(obj):它以一种智能的
,宽容的方式来构造一个 Optional
实例。来者不拒,传 null 进到就得到 Optional.empty()
,非 null 就调用 Optional.of(obj).
Optional.empty():返回一个空的 Optional 对象。
Optional的常用函数
isPresent
:如果值存在返回true,否则返回false。
ifPresent
:如果Optional实例有值则为其调用consumer,否则不做处理
get
:如果Optional有值则将其返回,否则抛出NoSuchElementException。因此也不经常用。
orElse
:如果有值则将其返回,否则返回指定的其它值。
orElseGet
:orElseGet与orElse方法类似,区别在于得到的默认值。orElse方法将传入的字符串作为默认值,orElseGet方法可以接受Supplier接口的实现用来生成默认值
orElseThrow
:如果有值则将其返回,否则抛出supplier接口创建的异常。
filter
:如果有值并且满足断言条件返回包含该值的Optional,否则返回空Optional。
map
:如果有值,则对其执行调用mapping函数得到返回值。如果返回值不为null,则创建包含mapping返回值的Optional作为map方法返回值,否则返回空Optional。
flatMap
:如果有值,为其执行mapping函数返回Optional类型返回值,否则返回空Optional。
Optional 应该怎样用
在使用 Optional
的时候需要考虑一些事情,以决定什么时候怎样使用它。重要的一点是 Optional
不是 Serializable
。因此,它不应该用作类的字段。如果你需要序列化的对象包含 Optional 值,Jackson 库支持把 Optional 当作普通对象。也就是说,Jackson 会把空对象看作 null,而有值的对象则把其值看作对应域的值。这个功能在 jackson-modules-java8
项目中。Optional 主要用作返回类型
,在获取到这个类型的实例后,如果它有值,你可以取得这个值,否则可以进行一些替代行为。Optional 类可以将其与流或其它返回 Optional 的方法结合,以构建流畅的API
。我们来看一个示例,我们不使用Optional写代码是这样的
public String getName(User user){
if(user == null){
return "Unknown";
}else return user.name();
}
接着我们来改造一下上面的代码,使用Optional来改造,我们先来举一个Optional滥用,没有达到流畅的链式API,反而复杂的例子,如下
public String getName(User user){
Optional<User> u = Optional.ofNullable(user);
if(!u.isPresent()){
return "Unknown";
}else return u.get().name();
}
这样改写非但不简洁,而且其操作还是和第一段代码一样。无非就是用isPresent
方法来替代原先user==null。这样的改写并不是Optional正确的用法,我们再来改写一次。
public String getName(User user){
return Optional.ofNullable(user)
.map(u -> u.name)
.orElse("Unknown");
}
这样才是正确使用Optional的姿势。那么按照这种思路,我们可以安心的进行链式调用,而不是一层层判断了。当然,我们还可以通过getter方式,对代码进行进一步缩减(前提是User要有getter方法哦),如下
String result = Optional.ofNullable(user)
.flatMap(User::getAddress)
.flatMap(Address::getCountry)
.map(Country::getIsocode)
.orElse("default");
四、常用的函数式接口
JDK8 之前:
interface{
静态常量;
抽象方法;
}
JDK8 之后:
interface{
静态常量;
抽象方法;
默认方法;
静态方法;
}
默认方法通过实例调用,静态方法可以通过接口名调用;
默认方法可以被继承,实现类可以直接调用接口的默认方法,也可以重写接口默认方法;
静态方法不能被继承,实现类不能重写接口的静态方法,只能使用接口名调用。
函数式接口
有且仅有一个
抽象方法
,就是函数式接口。在java.util.function包中。一般函数式接口中还包含默认方法,通常用语配合函数式接口使用。
@FunctionalIneterface
所修饰的接口只能定义一个抽象方法。—>函数式接口。
Supplier
有参无返回值的接口,用来生产数据的。
这是一个函数式接口,其函数式方法是get() 。
自:
1.8
类型形参:
<T> – 此供应商提供的结果类型
@FunctionalInterface
public interface Supplier<T> {
/**
* Gets a result.
*
* @return a result
*/
T get();
}
对应的Lambda表达式:
public class SupplierTest {
/**
* 输出 max:66
* @param args
*/
public static void main(String[] args) {
fun1(() ->{
int arr[] = {22,33,11,44,55,66,12};
//计算出数组中的最大值
Arrays.sort(arr);
return arr[arr.length-1];
});
}
/**
* 完成数据的处理,例如找最大值
*/
private static void fun1(Supplier<Integer> supplier){
Integer max = supplier.get();
System.out.println("max"+max);
}
}
Consumer
有参无返回值的接口,用来消费数据,使用的时候需要指定一个泛型来定义参数类型
@FunctionalInterface
public interface Consumer<T> {
void accept(T t);
//如果一个方法的参数和返回值全部是Consumer类型就可以实现连续操作:
//消费数据之前 先做一个操作,再做一个操作,在消费: consumer1.andThen(consumer2).accept(参数);
default Consumer<T> andThen(Consumer<? super T> after) {
Objects.requireNonNull(after);
return (T t) -> { accept(t); after.accept(t); };
}
}
Lambda使用:
public class ConsumerTest {
public static void main(String[] args) {
test(msg->{
//转小写
msg = msg.toLowerCase();
System.out.println(msg);
});
}
public static void test(Consumer<String> consumer){
consumer.accept("HELLO,WORLD!");
}
}
Function
有参有返回值的接口,根据一个类型的数据得到另一个类型的数据,前者成为前置条件,后者成为后置条件。
首先我们已经知道了Function是一个泛型类,其中定义了两个泛型参数T和R,在Function中,T代表输入参数,R代表返回的结果。
Function 就是一个函数,其作用类似于数学中函数的定义 ,(x,y)跟<T,R>的作用几乎一致。
R=function(T)
所以Function中没有具体的操作,具体的操作需要我们去为它指定,因此apply具体返回的结果取决于传入的lambda表达式。
R apply(T t);
/**表示接受一个参数并产生结果的函数。
这是一个功能接口,其功能方法是apply(Object) 。
自:
1.8
类型形参:
<T> – 函数输入的类型
<R> - 函数结果的类型*/
@FunctionalInterface
public interface Function<T, R> {
/**返回一个组合函数,该函数首先将before函数应用于其输入,然后将此函数应用于结果。 如果对任一函数的评估引发异常,则将其转发给组合函数的调用者。
形参:
before – 在应用此函数之前要应用的函数
类型形参:
<V> – before函数和组合函数的输入类型
返回值:
首先应用before函数然后应用此函数的组合函数
*/
R apply(T t);
}
lambda用法:
public class FunctionTest {
public static void main(String[] args) {
// 6
fun(arr->{
return arr+1;
});
}
//数组转字符串
public static void fun(Function<Integer, Integer> fun1){
System.out.println(fun1.apply(5));
}
}
Predicate
Predicate是个断言式接口其参数是<T,boolean>,也就是给一个参数T,返回boolean类型的结果。跟Function一样,Predicate的具体实现也是根据传入的lambda表达式来决定的。
@FunctionalInterface
public interface Predicate<T> {
boolean test(T t);
}
基本使用:
import java.util.function.Predicate;
//字符串很长吗:true
public class Demo01Predicate {
public static void main(String[] args) {
method(s -> s.length() > 5);
}
private static void method(Predicate<String> predicate) {
boolean veryLong = predicate.test("HelloWorld");
System.out.println("字符串很长吗:" + veryLong);
}
}
Stream流
为什么学习Stream流?
- 工作需要
- 大数量下处理集合效率更高
- 代码可读性高
- 消灭嵌套地狱
概述
Java8的Stream使用的是函数式编程模式,如同它的名字一样,它可以被用来对集合或数组进行链状流式操作。(类似于工厂中传送带)。可以更方便的让我们对集合或数组操作。
Stream产生的背景:
随着项目复杂度越来越高,数据源越来越多,有些数据无法通过SQL语句来查询,需要后期的组合筛选等,这就需要更高效的操作手段,如果使用遍历不但写法复杂,程序也会变得很复杂,Stream的出现大大增加了工作效率和程序的美观和效率。
2.使用案例
package stream.apple;
import java.util.ArrayList;
import java.util.List;
import java.util.stream.Collectors;
/**
* @ClassName: Stream01
* @Description:
* @author: 结构化思维wz
* @date: 2021/12/7 21:34
*/
public class Stream01 {
private static List<Apple> appleStore = new ArrayList<>();
static {
appleStore.add(new Apple(1,Color.RED,400,"重庆"));
appleStore.add(new Apple(2,Color.GREEN,300,"河北"));
appleStore.add(new Apple(3,Color.RED,500,"湖南"));
appleStore.add(new Apple(4,Color.YELLOW,400,"天津"));
appleStore.add(new Apple(5,Color.RED,500,"北京"));
}
/**
* 统计指定颜色的苹果
*/
public static void main(String[] args) {
List<Apple> redApple = new ArrayList<>();
for (Apple apple : appleStore){
if (apple.getColor().equals(Color.RED)){
redApple.add(apple);
}
}
redApple.forEach(System.out::println);
List<Apple> list = appleStore.stream()
.filter(apple -> apple.getColor().equals(Color.RED) && apple.getWeight()>400)
.collect(Collectors.toList());
list.forEach(System.out::println);
}
}
//控制台输出
Apple(id=1, color=RED, weight=400, origin=重庆)
Apple(id=3, color=RED, weight=500, origin=湖南)
Apple(id=5, color=RED, weight=500, origin=北京)
Apple(id=3, color=RED, weight=500, origin=湖南)
Apple(id=5, color=RED, weight=500, origin=北京)
通过上面的例子:可以看见流的方便之处,不仅仅是代码简洁,如果过滤的信息更多if就会更多,代码很不美观。使用Stream流只需要:filter(apple ->apple.getColor.equals("RED")&&apple.getWeight()>300)
。还可以进行 统计,分组等等…操作!
Stream流的两个基础特征:
Pipelining:
中间操作都会返回流对象本身。这样多个操作可以串联成一个管道,如同流式风格。这样做可以对操作进行优化,比如延迟执行和短路。内部迭代:
以前集合遍历都是通过显示的在集合外部进行迭代,Stream提供了内部迭代的方式,流可以直接调用遍历方法。
当时用一个流的时候,通常包括三个基本步骤:
- 获取一个数据源
- 数据转换
- 执行操作获取想要的结果
每次转换原有的Stream对象不改变,返回一个新的Stream对象(可以有多次转换),这就允许对其操作可以像链条一样排列,变成一个管道。
Stream流的创建
顺序流和并行流:
java.util.stream.Stream<T>
是Java8新加入的最常用的流接口。
获取一个流的方式非常简单,有以下几种方式:
-
所有的
Collection
集合都可以通过Stream默认的方法获取流。(注意Map如何创建流)List<String> list = Arrays.asList("a","b","c"); //创建一个顺序流 Stream<String> stream = list.stream(); //创建一个并行流 Stream<String> parallelStream = list.parallelStream();
Map如何创建Stream:Map有key,value还有表示key,value整体的Entry。
//创建一个Map: Map<String, String> someMap = new HashMap<>(); //获取Map的entrySet: Set<Map.Entry<String, String>> entries = someMap.entrySet(); //获取map的key: Set<String> keySet = someMap.keySet(); //获取map的value: Collection<String> values = someMap.values(); //除了Map没有stream,其他两个都有stream方法: Stream<Map.Entry<String, String>> entriesStream = entries.stream(); Stream<String> valuesStream = values.stream(); Stream<String> keysStream = keySet.stream();
-
java.util.Arrays.stream(T[] array)
通过数组创建流。int[] array = {1,3,4,5,6,7}; //通过数组创建一个流 IntStream stream = Arrays.stream(array);
-
使用
Stream
的静态方法。//创建Stream最简单的方式是直接用Stream.of()静态方法,传入可变参数即创建了一个能输出确定元素的Stream: Stream<String> stream = Stream.of("A", "B", "C", "D"); // forEach()方法相当于内部循环调用, // 可传入符合Consumer接口的void accept(T t)的方法引用: stream.forEach(System.out::println);
这种方式基本上没有实质性的用途。
-
基于
Supplier
创建
Stream
还可以通过Stream.generate()
方法,它需要传入一个Supplier
对象:Stream<String> s = Stream.generate(Supplier<String> sp);
基于
Supplier
创建的Stream
会不断调用Supplier.get()
方法来不断产生下一个元素,这种Stream
保存的不是元素,而是算法,它可以用来表示无限序列。例如,我们编写一个能不断生成自然数的
Supplier
,它的代码非常简单,每次调用get()
方法,就生成下一个自然数:public class StreamDemo { public static void main(String[] args) { Stream<Integer> nature = Stream.generate(new NatualSupplier()); //注意要给定一个范围再输出,不然会死循环 nature.limit(10).forEach(System.out::println); } } class NatualSupplier implements Supplier<Integer> { int n = 0; @Override public Integer get() { n++; return n; } }
上述代码我们用一个
Supplier<Integer>
模拟了一个无限序列(当然受int
范围限制不是真的无限大)。如果用List
表示,即便在int
范围内,也会占用巨大的内存,而Stream
几乎不占用空间,因为每个元素都是实时计算出来的,用的时候再算。 -
其他方法
创建
Stream
的第三种方法是通过一些API提供的接口,直接获得Stream
。例如,
Files
类的lines()
方法可以把一个文件变成一个Stream
,每个元素代表文件的一行内容:此方法对于按行遍历文本文件十分有用。try (Stream<String> lines = Files.lines(Paths.get("/path/to/file.txt"))) { ... }
另外,正则表达式的
Pattern
对象有一个splitAsStream()
方法,可以直接把一个长字符串分割成Stream
序列而不是数组:Pattern p = Pattern.compile("\\s+"); Stream<String> s = p.splitAsStream("The quick brown fox jumps over the lazy dog"); s.forEach(System.out::println);
-
基本类型
因为Java的范型不支持基本类型,所以我们无法用
Stream<int>
这样的类型,会发生编译错误。为了保存int
,只能使用Stream<Integer>
,但这样会产生频繁的装箱、拆箱操作。为了提高效率,Java标准库提供了IntStream
、LongStream
和DoubleStream
这三种使用基本类型的Stream
,它们的使用方法和范型Stream
没有大的区别,设计这三个Stream
的目的是提高运行效率。
使用Stream流
流模型的操作很丰富,这里介绍一些常用的API:
- 延迟方法:返回值类型仍然是
Stream接口自身类型
的方法,因此支持链式调用。 - 终结方法:返回值不再是Stream接口自身的方法,因此不支持链式调用。例如
count
和forEach
方法。
先总结一下Stream
提供的常用操作有:
(延迟)转换操作:map()
,filter()
,sorted()
,distinct()
;
(延迟)合并操作:concat()
,flatMap()
;
(终结)聚合操作:reduce()
,collect()
,count()
,max()
,min()
,sum()
,average()
;
(终结)其他操作:allMatch()
, anyMatch()
, forEach()
。
常用转换操作
map()
map()
方法用于将一个Stream
的每个元素映射成另一个元素并转换成一个新的Stream
;通过若干步map
转换,可以写出逻辑简单、清晰的代码。可以将一种元素类型转换成另一种元素类型。
Stream.map()
是Stream
最常用的一个转换方法,它把一个Stream
转换为另一个Stream
。
所谓map
操作,就是把一种操作运算,映射到一个序列的每一个元素上。例如:对x
计算它的平方,可以使用函数f(x) = x * x
。我们把这个函数映射到一个序列1,2,3,4,5上,就得到了另一个序列1,4,9,16,25:
f(x) = x * x
│
│
┌───┬───┬───┬───┼───┬───┬───┬───┐
│ │ │ │ │ │ │ │ │
▼ ▼ ▼ ▼ ▼ ▼ ▼ ▼ ▼
[ 1 2 3 4 5 6 7 8 9 ]
│ │ │ │ │ │ │ │ │
│ │ │ │ │ │ │ │ │
▼ ▼ ▼ ▼ ▼ ▼ ▼ ▼ ▼
[ 1 4 9 16 25 36 49 64 81 ]
map()
方法接收的对象是Function
接口对象,利用map()
,不但能完成数学计算,对于字符串操作,以及任何Java对象都是非常有用的。例如:
public class Main {
public static void main(String[] args) {
//Arrays.asList返回可变的list,而List.of返回的是不可变的list(Java9新语法)
List.of(" Apple ", " pear ", " ORANGE", " BaNaNa ")
.stream()
.map(String::trim) // 去空格
.map(String::toLowerCase) // 变小写
.forEach(System.out::println); // 打印
}
}
filter()
使用
filter()
方法可以对一个Stream
的每个元素进行测试,通过测试的元素被过滤后生成一个新的Stream
。
Stream.filter()
是Stream
的另一个常用转换方法。
所谓filter()
操作,就是对一个Stream
的所有元素一一进行测试,不满足条件的就被“滤掉”了,剩下的满足条件的元素就构成了一个新的Stream
。
例如,我们对1,2,3,4,5这个Stream
调用filter()
,传入的测试函数f(x) = x % 2 != 0
用来判断元素是否是奇数,这样就过滤掉偶数,只剩下奇数,因此我们得到了另一个序列1,3,5:
f(x) = x % 2 != 0
│
│
┌───┬───┬───┬───┼───┬───┬───┬───┐
│ │ │ │ │ │ │ │ │
▼ ▼ ▼ ▼ ▼ ▼ ▼ ▼ ▼
[ 1 2 3 4 5 6 7 8 9 ]
│ X │ X │ X │ X │
│ │ │ │ │
▼ ▼ ▼ ▼ ▼
[ 1 3 5 7 9 ]
public static void main(String[] args) {
IntStream.of(1,2,3,4,5,6,7,8,9).filter(x -> x%2!=0).forEach(System.out::println);
}
filter()
方法接收的对象是Predicate
接口对象,filter()
除了常用于数值外,也可应用于任何Java对象。例如从苹果中过滤红苹果:
public class Stream2 {
private static List<Apple> appleStore = new ArrayList<>();
static {
appleStore.add(new Apple(1,Color.RED,400,"重庆"));
appleStore.add(new Apple(2,Color.GREEN,300,"河北"));
appleStore.add(new Apple(3,Color.RED,500,"湖南"));
appleStore.add(new Apple(4,Color.YELLOW,400,"天津"));
appleStore.add(new Apple(5,Color.RED,500,"北京"));
}
public static void main(String[] args) {
appleStore.stream().filter(apple -> apple.getColor().equals(Color.RED)).forEach(System.out::println);
}
}
排序sorted()
对Stream
的元素进行排序十分简单,只需调用sorted()
方法:
public class Main {
public static void main(String[] args) {
List<String> list = List.of("Orange", "apple", "Banana")
.stream()
.sorted()
.collect(Collectors.toList());
System.out.println(list);
}
}
//[Banana, Orange, apple]
此方法要求Stream
的每个元素必须实现Comparable
接口。如果要自定义排序,传入指定的Comparator
即可:
List<String> list = List.of("Orange", "apple", "Banana")
.stream()
.sorted(String::compareToIgnoreCase)
.collect(Collectors.toList());
注意sorted()
只是一个转换操作,它会返回一个新的Stream
。
去重distinct()
对一个Stream
的元素进行去重,没必要先转换为Set
,可以直接用distinct()
:
List.of("A", "B", "A", "C", "B", "D")
.stream()
.distinct()
.collect(Collectors.toList()); // [A, B, C, D]
截取
截取操作常用于把一个无限的Stream
转换成有限的Stream
,skip()
用于跳过当前Stream
的前N个元素,limit()
用于截取当前Stream
最多前N个元素:
List.of("A", "B", "C", "D", "E", "F")
.stream()
.skip(2) // 跳过A, B
.limit(3) // 截取C, D, E
.collect(Collectors.toList()); // [C, D, E]
截取操作也是一个转换操作,将返回新的Stream
。
合并concat()
将两个Stream
合并为一个Stream
可以使用Stream
的静态方法concat()
:
Stream<String> s1 = List.of("A", "B", "C").stream();
Stream<String> s2 = List.of("D", "E").stream();
// 合并:
Stream<String> s = Stream.concat(s1, s2);
System.out.println(s.collect(Collectors.toList())); // [A, B, C, D, E]
flatMap
如果Stream
的元素是集合:
Stream<List<Integer>> s = Stream.of(
Arrays.asList(1, 2, 3),
Arrays.asList(4, 5, 6),
Arrays.asList(7, 8, 9));
而我们希望把上述Stream
转换为Stream<Integer>
,就可以使用flatMap()
:
Stream<Integer> i = s.flatMap(list -> list.stream());
因此,所谓flatMap()
,是指把Stream
的每个元素(这里是List
)映射为Stream
,然后合并成一个新的Stream
:
┌─────────────┬─────────────┬─────────────┐
│┌───┬───┬───┐│┌───┬───┬───┐│┌───┬───┬───┐│
││ 1 │ 2 │ 3 │││ 4 │ 5 │ 6 │││ 7 │ 8 │ 9 ││
│└───┴───┴───┘│└───┴───┴───┘│└───┴───┴───┘│
└─────────────┴─────────────┴─────────────┘
│
│flatMap(List -> Stream)
│
│
▼
┌───┬───┬───┬───┬───┬───┬───┬───┬───┐
│ 1 │ 2 │ 3 │ 4 │ 5 │ 6 │ 7 │ 8 │ 9 │
└───┴───┴───┴───┴───┴───┴───┴───┴───┘
常用聚合操作
reduce()
reduce()
方法将一个Stream
的每个元素依次作用于BinaryOperator
,并将结果合并。reduce()
是聚合方法,聚合方法会立刻对Stream
进行计算。
Stream.reduce()
则是Stream
的一个聚合方法,它可以把一个Stream
的所有元素按照聚合函数聚合成一个结果。
public static void main(String[] args) {
int sum = Stream.of(1, 2, 3, 4, 5, 6, 7, 8, 9).reduce(0, (acc, n) -> acc + n);
System.out.println(sum); // 45
}
reduce()
方法传入的对象是BinaryOperator
接口,reduce()
操作首先初始化结果为指定值(这里是0),紧接着,reduce()
对每个元素依次调用(acc, n) -> acc + n
,其中,acc
是上次计算的结果。
如果去掉初始值,我们会得到一个Optional<Integer>
:
Optional<Integer> opt = stream.reduce((acc, n) -> acc + n);
if (opt.isPresent()) {
System.out.println(opt.get());
}
这是因为Stream
的元素有可能是0个,这样就没法调用reduce()
的聚合函数了,因此返回Optional
对象,需要进一步判断结果是否存在。
利用reduce()
,我们还可以把求和改为求乘积:
public static void main(String[] args) {
//注意:计算求积时,初始值必须设置为1。
int s = Stream.of(1, 2, 3, 4, 5, 6, 7, 8, 9).reduce(1, (acc, n) -> acc * n);
System.out.println(s); // 362880
}
**灵活的运用reduce()
也可以对Java对象进行操作。**下面的代码演示了如何将配置文件的每一行配置通过map()
和reduce()
操作聚合成一个Map<String, String>
:
public class Main {
public static void main(String[] args) {
// 按行读取配置文件:
List<String> props = List.of("profile=native", "debug=true", "logging=warn", "interval=500");
Map<String, String> map = props.stream()
// 把k=v转换为Map[k]=v:
.map(kv -> {
String[] ss = kv.split("\\=", 2);
return Map.of(ss[0], ss[1]);
})
// 把所有Map聚合到一个Map:
.reduce(new HashMap<String, String>(), (m, kv) -> {
m.putAll(kv);
return m;
});
// 打印结果:
map.forEach((k, v) -> {
System.out.println(k + " = " + v);
});
}
}
其他聚合方法
1️⃣除了reduce()
和collect()
外,Stream
还有一些常用的聚合方法:
count()
:用于返回元素个数;max(Comparator<? super T> cp)
:找出最大元素;min(Comparator<? super T> cp)
:找出最小元素。
2️⃣针对IntStream
、LongStream
和DoubleStream
,还额外提供了以下聚合方法:
sum()
:对所有元素求和;average()
:对所有元素求平均数。
3️⃣还有一些方法,用来测试Stream
的元素是否满足以下条件:
boolean allMatch(Predicate<? super T>)
:测试是否所有元素均满足测试条件;boolean anyMatch(Predicate<? super T>)
:测试是否至少有一个元素满足测试条件。
forEach()
最后一个常用的方法是forEach()
,它可以循环处理Stream
的每个元素,我们经常传入System.out::println
来打印Stream
的元素:
Stream<String> s = ...
s.forEach(str -> {
System.out.println("Hello, " + str);
});
输出集合collect()
Stream
可以输出为集合:
Stream
通过collect()
方法可以方便地输出为List
、Set
、Map
,还可以分组输出。
因为流不存储数据,那么在流中的数据完成处理后,需要将流中的数据重新归集到新的集合里。toList
、toSet
和toMap
比较常用,另外还有toCollection
、toConcurrentMap
等复杂一些的用法。
输出为List
reduce()
只是一种聚合操作,如果我们希望把Stream
的元素保存到集合,例如List
,因为List
的元素是确定的Java对象,因此,把Stream
变为List
不是一个转换操作,而是一个聚合操作,它会强制Stream
输出每个元素。
下面的代码演示了如何将一组String
先过滤掉空字符串,然后把非空字符串保存到List
中:
import java.util.*;
import java.util.stream.*;
public class Main {
public static void main(String[] args) {
Stream<String> stream = Stream.of("Apple", "", null, "Pear", " ", "Orange");
List<String> list = stream.filter(s -> s != null && !s.isBlank()).collect(Collectors.toList());
System.out.println(list);
}
}
把Stream
的每个元素收集到List
的方法是调用collect()
并传入Collectors.toList()
对象,它实际上是一个Collector
实例,通过类似reduce()
的操作,把每个元素添加到一个收集器中(实际上是ArrayList
)。
类似的,collect(Collectors.toSet())
可以把Stream
的每个元素收集到Set
中。
输出为数组
把Stream的元素输出为数组和输出为List类似,我们只需要调用toArray()
方法,并传入数组的“构造方法”:
List<String> list = List.of("Apple", "Banana", "Orange");
String[] array = list.stream().toArray(String[]::new);
注意到传入的“构造方法”是String[]::new
,它的签名实际上是IntFunction<String[]>
定义的String[] apply(int)
,即传入int
参数,获得String[]
数组的返回值。
输出为Map
如果我们要把Stream的元素收集到Map中,就稍微麻烦一点。因为对于每个元素,添加到Map时需要key和value,因此,我们要指定两个映射函数,分别把元素映射为key和value:
import java.util.*;
import java.util.stream.*;
public class Main {
public static void main(String[] args) {
Stream<String> stream = Stream.of("APPL:Apple", "MSFT:Microsoft");
Map<String, String> map = stream
.collect(Collectors.toMap(
// 把元素s映射为key:
s -> s.substring(0, s.indexOf(':')),
// 把元素s映射为value:
s -> s.substring(s.indexOf(':') + 1)));
System.out.println(map);
}
}
分组输出groupingBy()
Stream
还有一个强大的分组功能,可以按组输出。我们看下面的例子:
import java.util.*;
import java.util.stream.*;
public static void main(String[] args) {
//substring() 方法返回字符串的子字符串。
List<String> list = List.of("Apple", "Banana", "Blackberry", "Coconut", "Avocado", "Cherry", "Apricots");
Map<String, List<String>> groups = list.stream()
.collect(Collectors.groupingBy(s -> s.substring(0, 1), Collectors.toList()));
System.out.println(groups);
}
}
分组输出使用Collectors.groupingBy()
,它需要提供两个函数:一个是分组的key,这里使用s -> s.substring(0, 1)
,表示只要首字母相同的String
分到一组,第二个是分组的value,这里直接使用Collectors.toList()
,表示输出为List
,上述代码运行结果如下:
{
A=[Apple, Avocado, Apricots],
B=[Banana, Blackberry],
C=[Coconut, Cherry]
}
可见,结果一共有3组,按"A"
,"B"
,"C"
分组,每一组都是一个List
。
假设有这样一个Student
类,包含学生姓名、班级和成绩:
class Student {
int gradeId; // 年级
int classId; // 班级
String name; // 名字
int score; // 分数
}
如果我们有一个Stream<Student>
,利用分组输出,可以非常简单地按年级或班级把Student
归类。
Map<Integer, List<Student>> groups = list.stream()
.collect(Collectors.groupingBy(s -> s.classId, Collectors.toList()));
System.out.println(groups);
文件按行读取
Files类的lines()方法可以把一个文件变成一个Stream,每个元素代表文件的一行内容:
try (Stream<String> lines = Files.lines(Paths.get("/path/to/file.txt"))) {
...
}
此方法对于按行遍历文本文件十分有用。
本文参考:<廖雪峰老师的网站>
如果看完觉得写的还不错,给个赞再走吧!主页还有更多肝文!