iis服务器助手广告广告
返回顶部
首页 > 资讯 > 后端开发 > Python >深入理解JDK8中Stream使用
  • 629
分享到

深入理解JDK8中Stream使用

2024-04-02 19:04:59 629人浏览 八月长安

Python 官方文档:入门教程 => 点击学习

摘要

概述 Stream 是 Java8 中处理集合的关键抽象概念,它可以指定你希望对集合进行的操作,可以执行非常复杂的查找、过滤和映射数据等操作。使用Stream api 对集合数据进行

概述

Stream 是 Java8 中处理集合的关键抽象概念,它可以指定你希望对集合进行的操作,可以执行非常复杂的查找、过滤和映射数据等操作。使用Stream api 对集合数据进行操作,就类似于使用 sql 执行的数据库查询。也可以使用 Stream API 来并行执行操作。简而言之,Stream API 提供了一种高效且易于使用的处理数据的方式。

特点:

不是数据结构,不会保存数据。

不会修改原来的数据源,它会将操作后的数据保存到另外一个对象中。(保留意见:毕竟peek方法可以修改流中元素)

惰性求值,流在中间处理过程中,只是对操作进行了记录,并不会立即执行,需要等到执行终止操作的时候才会进行实际的计算。

现在谈及jdk8的新特新,已经说不上新了。本篇介绍的就是StreamLambda,说的Stream可不是JDK中的io,这里的Stream指的是处理集合的抽象概念『像流一样处理集合数据』。

了解Stream前先认识一下Lambda

函数式接口和Lambda

先看一组简单的对比

传统方式使用一个匿名内部类的写法


new Thread(new Runnable() {
    @Override
    public void run() {
        // ...
    }
}).start();

换成Lambda的写法


new Thread(() -> {
    // ...
}).start();

其实上面的写法就是简写了函数式接口匿名实现类

配合Lambda,JDK8引入了一个新的定义叫做:函数式接口(Functional interfaces)

函数式接口

从概念上讲,有且仅有一个需要实现方法的接口称之为函数式接口。

看一个JDK给的一个函数式接口的源码


@FunctionalInterface
public interface Runnable {
    public abstract void run();
}

可以看到接口上面有一个@FunctionalInterface注释,功能大致和@Override类似

不写@Override也能重写父类方法,该方法确实没有覆盖或实现了在超类型中声明的方法时编译器就会报错,主要是为了编译器可以验证识别代码编写的正确性。

同样@FunctionalInterface也是这样,写到一个不是函数式接口的接口上面就会报错,即使不写@FunctionalInterface注释,编译器也会将满足函数式接口定义的任何接口视为函数式接口。

写一个函数式接口加不加@FunctionalInterface注释,下面的接口都是函数式接口


interface MyFunc {
  String show(Integer i);
}

Lambda表达式

Lambda表达式就是为了简写函数式接口

构成

看一下Lambda的构成

  • 括号里面的参数
  • 箭头 ->
  • 然后是身体

它可以是单个表达式或java代码块。

整体表现为 (...参数) -> {代码块}

简写

下面就是函数式接口的实现简写为Lambda的例子

无参 - 无返回


interface MyFunc1 {
    void func();
}

// 空实现
MyFunc1 f11 = () -> { };
// 只有一行语句
MyFunc1 f12 = () -> {
    System.out.println(1);
    System.out.println(2);
};
// 只有一行语句
MyFunc1 f13 = () -> {
    System.out.println(1);
};
// 只有一行语句可以省略 { }
MyFunc1 f14 = () -> System.out.println(1);

有参 - 无返回


interface MyFunc2 {
    void func(String str);
}

// 函数体空实现
MyFunc2 f21 = (str) -> { };
// 单个参数可以省略 () 多个不可以省略
MyFunc2 f22 = str -> System.out.println(str.length());

无参 - 有返回


interface MyFunc3 {
    int func();
}

// 返回值
MyFunc3 f31 = () -> {return 1;};
// 如果只有一个return 语句时可以直接写return 后面的表达式语句
MyFunc3 f32 = () -> 1;

有参 - 有返回


interface MyFunc4 {
    int func(String str);
}

// 这里单个参数简写了{}
MyFunc4 f41 = str -> {
    return str.length();
};
// 这里又简写了return
MyFunc4 f42 = str -> str.length();
// 这里直接使用了方法引用进行了简写 - 在文章后续章节有介绍到
MyFunc4 f43 = String::length;

这里可以总结出来简写规则

上面写的Lambda表达式中参数都没有写参数类型(可以写参数类型的),so

  • 小括号内参数的类型可以省略;
  • 没有参数时小括号不能省略,小括号中有且仅有一个参数时,不能缺省括号
  • 如果大括号内有且仅有一个语句,则无论是否有返回值,都可以省略大括号、return关键字及语句分号(三者省略都需要一起省略)。

看到这里应该认识到了如何用Lambda简写函数式接口,那现在就进一步的认识一下JDK中Stream中对函数式接口的几种大类

常用内置函数式接口

上节说明了Lambda表达式就是为了简写函数式接口,为使用方便,JDK8提供了一些常用的函数式接口。最具代表性的为Supplier、Function、Consumer、Perdicate,这些函数式接口都在java.util.function包下。

这些函数式接口都是泛型类型的,下面的源码都去除了default方法,只保留真正需要实现的方法。

Function接口

这是一个转换的接口。接口有参数、有返回值,传入T类型的数据,经过处理后,返回R类型的数据。『T和R都是泛型类型』可以简单的理解为这是一个加工工厂。


@FunctionalInterface
public interface Function<T, R> {
    R apply(T t);
}

使用实例:定义一个转换函数『将字符串转为数字,再平方』


// 将字符串转为数字,再平方
Function<String, Integer> strConvertToIntAndSquareFun = (str) -> {
    Integer value = Integer.valueOf(str);
    return value * value;
};
Integer result = strConvertToIntAndSquareFun.apply("4");
System.out.println(result); // 16

Supplier接口

这是一个对外供给的接口。此接口无需参数,即可返回结果


@FunctionalInterface
public interface Supplier<T> {
    T get();
}

使用实例:定义一个函数返回“Tom”字符串


// 供给接口,调用一次返回一个 ”tom“ 字符串
Supplier<String> tomFun = () -> "tom";
String tom = tomFun.get();
System.out.println(tom); // tom

Consumer接口

这是一个消费的接口。此接口有参数,但是没有返回值


@FunctionalInterface
public interface Consumer<T> {
    void accept(T t);
}	

使用实例:定义一个函数传入数字,打印一行相应数量的A


// 重复打印
Consumer<Integer> printA = (n)->{
    for (int i = 0; i < n; i++) {
        System.out.print("A");
    }
    System.out.println();
};
printA.accept(5); // AAAAA

Predicate接口

这是一个断言的接口。此接口对输入的参数进行一系列的判断,返回一个Boolean值。


@FunctionalInterface
public interface Predicate<T> {
    boolean test(T t);	
}

使用实例:定义一个函数传入一个字符串,判断是否为A字母开头且Z字母结尾


// 判断是否为`A`字母开头且`Z`字母结尾
Predicate<String> strAStartAndZEnd = (str) -> {
    return str.startsWith("A") && str.endsWith("Z");
};
System.out.println(strAStartAndZEnd.test("AaaaZ")); // true 
System.out.println(strAStartAndZEnd.test("Aaaaa")); // false
System.out.println(strAStartAndZEnd.test("aaaaZ")); // false
System.out.println(strAStartAndZEnd.test("aaaaa")); // false

Supplier接口外Function、Consumer、Perdicate还有其他一堆默认方法可以用,比如Predicate接口包含了多种默认方法,用于处理复杂的判断逻辑(and, or);

上面的使用方式都是正常简单的使用函数式接口,当函数式接口遇见了方法引用才真正发挥他的作用。

方法引用

方法引用的唯一存在的意义就是为了简写Lambda表达式。

方法引用通过方法的名字来指向一个方法,可以使语言的构造更紧凑简洁,减少冗余代码。

比如上面章节使用的


MyFunc4 f43 = String::length; // 这个地方就用到了方法引用

方法引用使用一对冒号 ::

相当于将String类的实例方法length赋给MyFunc4接口


public int length() {
    return value.length;
}

interface MyFunc4 {
    int func(String str);
}

这里可能有点问题:方法 int length()的返回值和int func(String str)相同,但是方法参数不同为什么也能正常赋值给MyFunc4

可以理解为Java实例方法有一个隐藏的参数第一个参数this(类型为当前类)


public class Student {
    public void show() {
        // ...
    }
    public void print(int a) {
        // ...
    }
}

实例方法show()print(int a)相当于


public void show(String this);
public void print(String this, int a);

这样解释的通为什么MyFunc4 f43 = String::length;可以正常赋值。


String::length;
public int length() {
    return value.length;
}

// 相当于
public int length(String str) {
    return str.length();
}
// 这样看length就和函数式接口MyFunc4的传参和返回值就相同了

不只这一种方法引用详细分类如下

方法引用分类

类型 引用写法 Lambda表达式
静态方法引用 ClassName::staticMethod (args) -> ClassName.staticMethod(args)
对象方法引用 ClassName::instanceMethod (instance, args) -> instance.instanceMethod(args)
实例方法引用 instance::instanceMethod (args) -> instance.instanceMethod(args)
构建方法引用 ClassName::new (args) -> new ClassName(args)
上面的方法就属于对象方法引用

记住这个表格,不用刻意去记,使用Stream时会经常遇到

有几种比较特殊的方法引用,一般来说原生类型如int不能做泛型类型,但是int[]可以


IntFunction<int[]> arrFun = int[]::new;
int[] arr = arrFun.apply(10); // 生成一个长度为10的数组

这节结束算是把函数式接口,Lambda表达式,方法引用等概念串起来了。

Optional工具

Optional工具是一个容器对象,最主要的用途就是为了规避 NPE(空指针) 异常。构造方法是私有的,不能通过new来创建容器。是一个不可变对象,具体原理没什么可以介绍的,容器源码整个类没500行,本章节主要介绍使用。

构造方法


private Optional(T value) {
    // 传 null 会报空指针异常
    this.value = Objects.requireNonNull(value);
}

创建Optional的方法

empyt返回一个包含null值的Optional容器


public static<T> Optional<T> empty() {
    @SuppressWarnings("unchecked")
    Optional<T> t = (Optional<T>) EMPTY;
    return t;
}

of返回一个不包含null值的Optional容器,传null值报空指针异常


public static <T> Optional<T> of(T value) {
    return new Optional<>(value);
}

ofNullable返回一个可能包含null值的Optional容器


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

可以使用的Optional的方法

ifPresent方法,参数是一个Consumer,当容器内的值不为null是执行Consumer


Optional<Integer> opt = Optional.of(123);
opt.ifPresent((x) -> {
	System.out.println(opt);
});
// out: 123

get方法,获取容器值,可能返回空

orElse方法,当容器中值为null时,返回orElse方法的入参值


public T orElse(T other) {
    return value != null ? value : other;
}

orElseGet方法,当容器中值为null时,执行入参Supplier并返回值


public T orElseGet(Supplier<? extends T> other) {
    return value != null ? value : other.get();
}

常见用法


// 当param为null时 返回空集合
Optional.ofNullable(param).orElse(Collections.emptyList());
Optional.ofNullable(param).orElseGet(() -> Collections.emptyList());

orElseorElseGet的区别,orElseGet算是一个惰性求值的写法,当容器内的值不为null时Supplier不会执行。

平常工作开发中,也是经常通过 orElse 来规避 NPE 异常。

这方面不是很困难难主要是后续Stream有些方法需要会返回一个Optional一个容器对象。

Stream

Stream可以看作是一个高级版的迭代器。增强了Collection的,极大的简化了对集合的处理。

想要使用Stream首先需要创建一个

创建Stream流的方式


// 方式1,数组转Stream
Arrays.stream(arr);
// 方式2,数组转Stream,看源码of就是方法1的包装
Stream.of(arr);
// 方式3,调用Collection接口的stream()方法
List<String> list = new ArrayList<>();
list.stream();

有了Stream自然就少不了操作流

常用Stream流方法

大致可以把对Stream的操作大致分为两种类型中间操作终端操作

  • 中间操作是一个属于惰式的操作,也就是不会立即执行,每一次调用中间操作只会生成一个标记了新的Stream
  • 终端操作会触发实际计算,当终端操作执行时会把之前所有中间操作以管道的形式顺序执行,Stream是一次性的计算完会失效

操作Stream会大量的使用Lambda表达式,也可以说它就是为函数式编程而生

先提前认识一个终端操作forEach对流中每个元素执行一个操作,实现一个打印的效果


// 打印流中的每一个元素
Stream.of("jerry", "lisa", "moli", "tom", "Demi").forEach(str -> {
    System.out.println(str);
});

forEach的参数是一个Consumer可以用方法引用优化(静态方法引用),优化后的结果为


Stream.of("jerry", "lisa", "moli", "tom", "Demi")
    .forEach(System.out::println);

有这一个终端操作就可以向下介绍大量的中间操作了

中间操作

中间操作filter:过滤元素

fileter方法参数是一个Predicate接口,表达式传入的参数是元素,返回true保留元素,false过滤掉元素

过滤长度小于3的字符串,仅保留长度大于4的字符串


Stream.of("jerry", "lisa", "moli", "tom", "Demi")
    // 过滤
    .filter(str -> str.length() > 3)
    .forEach(System.out::println);

中间操作limit:截断元素

限制集合长度不能超过指定大小


Stream.of("jerry", "lisa", "moli", "tom", "Demi")
    .limit(2)
    .forEach(System.out::println);

中间操作skip:跳过元素(丢弃流的前n元素)


// 丢弃前2个元素
Stream.of("jerry", "lisa", "moli", "tom", "Demi")
    .skip(2)
    .forEach(System.out::println);

中间操作map:转换元素

map传入的函数会被应用到每个元素上将其映射成一个新的元素


// 为每一个元素加上 一个前缀 "name: "
Stream.of("jerry", "lisa", "moli", "tom", "Demi")
    .map(str -> "name: " + str)
    .forEach(System.out::println);

中间操作peek:查看元素

peek方法的存在主要是为了支持调试,方便查看元素流经管道中的某个点时的情况

下面是一个JDK源码中给出的例子


Stream.of("one", "two", "three", "four")
    // 第1次查看
    .peek(e -> System.out.println("第1次 value: " + e))
    // 过滤掉长度小于3的字符串
    .filter(e -> e.length() > 3)
    // 第2次查看
    .peek(e -> System.out.println("第2次 value: " + e))
    // 将流中剩下的字符串转为大写
    .map(String::toUpperCase)
    // 第3次查看
    .peek(e -> System.out.println("第3次 value: " + e))
    // 收集为List
    .collect(Collectors.toList());

mappeek有点相似,不同的是peek接收一个Consumer,而map接收一个Function

当然了你非要采用peek修改数据也没人能限制的了


public class User {
    public String name;

    public User(String name) {
        this.name = name;
    }

    @Override
    public String toString() {
        return "User{" +
            "name='" + name + '\'' +
            '}';
    }
}

Stream.of(new User("tom"), new User("jerry"))
    .peek(e -> {
        e.name = "US:" + e.name;
    })
    .forEach(System.out::println);

中间操作sorted:排序数据


// 排序数据
Stream.of(4, 2, 1, 3)
    // 默认是升序
    .sorted()
    .forEach(System.out::println);

逆序排序


// 排序数据
Stream.of(4, 2, 1, 3)
    // 逆序
    .sorted(Comparator.reverseOrder())
    .forEach(System.out::println

如果是对象如何排序,自定义Comparator,切记不要违反自反性,对称性,传递性原则


public class User {
    public String name;

    public User(String name) {
        this.name = name;
    }

    @Override
    public String toString() {
        return "User{" +
            "name='" + name + '\'' +
            '}';
    }
}

// 名称长的排前面
Stream.of(new User("tom"), new User("jerry"))
    .sorted((e1, e2) -> {
        return e2.name.length() - e1.name.length();
    })
    .forEach(System.out::println);
	

中间操作distinct:去重

注意:必须重写对应泛型的hashCode()和equals()方法


Stream.of(2, 2, 4, 4, 3, 3, 100)
    .distinct()
    .forEach(System.out::println);

中间操作flatMap:平铺流

返回一个流,该流由通过将提供的映射函数(flatMap传入的参数)应用于每个元素而生成的映射流的内容替换此流的每个元素,通俗易懂就是将原来的Stream中的所有元素都展开组成一个新的Stream


List<Integer[]> arrList = new ArrayList<>();
arrList.add(arr1);
arrList.add(arr2);
// 未使用
arrList.stream()
    .forEach(e -> {
        System.out.println(Arrays.toString(e));
    });

	

// 平铺后
arrList.stream()
    .flatMap(arr -> Stream.of(arr))
    .forEach(e -> {
        System.out.println(e);
    });

终端操作max,min,count:统计


// 最大值
Optional<Integer> maxOpt = Stream.of(2, 4, 3, 100)
    .max(Comparator.comparing(e -> e));
System.out.println(maxOpt.get()); // 100

// 最小值
Optional<Integer> minOpt = Stream.of(2, 4, 3, 100)
    .min(Comparator.comparing(Function.identity()));
System.out.println(minOpt.get()); // 2

// 数量
long count = Stream.of("one", "two", "three", "four")
    .count();
System.out.println(count); // 4

上面例子中有一个点需要注意一下Function.identity()相当于 e -> e

看源码就可以看出来


static <T> Function<T, T> identity() {
    return t -> t;
}

终端操作findAny:返回任意一个元素


Optional<String> anyOpt = Stream.of("one", "two", "three", "four")
    .findAny();
System.out.println(anyOpt.orElse(""));
	

终端操作findFirst:返回第一个元素


Optional<String> firstOpt = Stream.of("one", "two", "three", "four")
    .findFirst();

System.out.println(firstOpt.orElse(""));
	

返回的Optional容器在上面介绍过了,一般配置orElse使用,原因就在于findAnyfindFirst可能返回空空容器,调用get可能会抛空指针异常

终端操作allMatch,anyMatch:匹配


// 是否全部为 one 字符串
boolean allIsOne = Stream.of("one", "two", "three", "four")
    .allMatch(str -> Objects.equals("one", str));
System.out.println(allIsOne); // false

allIsOne = Stream.of("one", "one", "one", "one")
    .allMatch(str -> Objects.equals("one", str));
System.out.println(allIsOne); // true

// 是否包含 one 字符串
boolean hasOne = Stream.of("one", "two", "three", "four")
    .anyMatch(str -> Objects.equals("one", str));
System.out.println(hasOne); // true

hasOne = Stream.of("two", "three", "four")
    .anyMatch(str -> Objects.equals("one", str));
System.out.println(hasOne); // false

上面仅仅介绍了一个forEach终端操作,但是业务开发中更多的是对处理的数据进行收集起来,如下面的一个例子将元素收集为一个List集合

终端操作collect:收集元素到集合

collect高级使用方法很复杂,常用的用法使用Collectors工具类

收集成List


List<String> list = Stream.of("one", "two", "three", "four")
    .collect(Collectors.toList());
System.out.println(list);
	

收集成Set『收集后有去除的效果,结果集乱序』


Set<String> set = Stream.of("one", "one", "two", "three", "four")
    .collect(Collectors.toSet());
System.out.println(set);
	

字符串拼接


String str1 = Stream.of("one", "two", "three", "four")
    .collect(Collectors.joining());
System.out.println(str1); // onetwothreefour
String str2 = Stream.of("one", "two", "three", "four")
    .collect(Collectors.joining(", "));
System.out.println(str2); // one, two, three, four

收集成Map


// 使用Lombok插件
@Data
@AllArgsConstructor
public class User {
    public Integer id;
    public String name;
}

Map<Integer, User> map = Stream.of(new User(1, "tom"), new User(2, "jerry"))
    .collect(Collectors.toMap(User::getId, Function.identity(), (k1, k2) -> k1));
System.out.println(map);
	

toMap常用的方法签名


public static <T, K, U>
    Collector<T, ?, Map<K,U>> toMap(Function<? super T, ? extends K> keyMapper,
                                    Function<? super T, ? extends U> valueMapper,
                                    BinaryOperator<U> mergeFunction) {
    return toMap(keyMapper, valueMapper, mergeFunction, HashMap::new);
}

数据分组


@Data
@AllArgsConstructor
class User {
    public Integer id;
    public String name;
}
Map<String, List<User>> map = Stream.of(
    new User(1, "tom"), new User(2, "jerry"),
    new User(3, "moli"), new User(4, "lisa")
).collect(Collectors.groupingBy(u -> {
    if (u.id % 2 == 0) {
        return "奇";
    }
    return "偶";
}));
System.out.println(map);
	

分组后value 是一个集合,groupingBy分组还有一个参数可以指定下级收集器,后续例子中有使用到

Steam例

下面例子用到的基础数据,如有例子特例会在例子中单独补充


List<Student> studentList = new ArrayList<>();
studentList.add(new Student(1, "tom",    19, "男", "软工"));
studentList.add(new Student(2, "lisa",   15, "女", "软工"));
studentList.add(new Student(3, "Ada",    16, "女", "软工"));
studentList.add(new Student(4, "Dora",   14, "女", "计科"));
studentList.add(new Student(5, "Bob",    20, "男", "软工"));
studentList.add(new Student(6, "Farrah", 15, "女", "计科"));
studentList.add(new Student(7, "Helen",  13, "女", "软工"));
studentList.add(new Student(8, "jerry",  12, "男", "计科"));
studentList.add(new Student(9, "Adam",   20, "男", "计科"));

例1:封装一个分页函数



public static <T> List<T> page(Collection<T> list, long pageNo, long pageSize) {
    if (Objects.isNull(list) || list.isEmpty()) {
        return Collections.emptyList();
    }
    return list.stream()
        .skip((pageNo - 1) * pageSize)
        .limit(pageSize)
        .collect(Collectors.toList());
}

List<Student> pageData = page(studentList, 1, 3);
System.out.println(pageData);

例2:获取软工班全部的人员id


List<Integer> idList = studentList.stream()
    .filter(e -> Objects.equals(e.getClassName(), "软工"))
    .map(Student::getId)
    .collect(Collectors.toList());
System.out.println(idList);

例3:收集每个班级中的人员名称列表


Map<String, List<String>> map = studentList.stream()
        .collect(Collectors.groupingBy(
                Student::getClassName,
                Collectors.mapping(Student::getName, Collectors.toList())
        ));
System.out.println(map);

例4:统计每个班级中的人员个数


Map<String, Long> map = studentList.stream()
    .collect(Collectors.groupingBy(
        Student::getClassName,
        Collectors.mapping(Function.identity(), Collectors.counting())
    ));
System.out.println(map);

例5:获取全部女生的名称


List<String> allFemaleNameList = studentList.stream()
    .filter(stu -> Objects.equals("女", stu.getSex()))
    .map(Student::getName)
    .collect(Collectors.toList());
System.out.println(allFemaleNameList);

例6:依照年龄排序


// 年龄升序排序
List<Student> stuList1 = studentList.stream()
    // 升序
    .sorted(Comparator.comparingInt(Student::getAge))
    .collect(Collectors.toList());
System.out.println(stuList1);


// 年龄降序排序
List<Student> stuList2 = studentList.stream()
    // 降序
    .sorted(Comparator.comparingInt(Student::getAge).reversed())
    .collect(Collectors.toList());
System.out.println(stuList2);

例7:分班级依照年龄排序

该例中和例3类似的处理,都使用到了downstream下游 - 收集器


Map<String, List<Student>> map = studentList.stream()
        .collect(
                Collectors.groupingBy(
                        Student::getClassName,
                        Collectors.collectingAndThen(Collectors.toList(), arr -> {
                            return arr.stream()
                                    .sorted(Comparator.comparingInt(Student::getAge))
                                    .collect(Collectors.toList());
                        })
                )
        );

本例中使用到的downstream的方式更为通用,可以实现绝大多数的功能,例3中的方法JDK提供的简写方式

下面是用collectingAndThen的方式实现和例3相同的功能


Map<String, Long> map = studentList.stream()
        .collect(
                Collectors.groupingBy(
                        Student::getClassName,
                        Collectors.collectingAndThen(Collectors.toList(), arr -> {
                            return (long) arr.size();
                        })
                )
        );

例8:将数据转为ID和Name对应的数据结构Map


Map<Integer, String> map = studentList.stream()
    .collect(Collectors.toMap(Student::getId, Student::getName));
System.out.println(map);

情况1

上面代码,在现有的数据下正常运行,当添加多添加一条数据


studentList.add(new Student(9, "Adam - 2", 20, "男", "计科"));

这个时候id为9的数据有两条了,这时候再运行上面的代码就会出现Duplicate key Adam

也就是说调用toMap时,假设其中存在重复的key,如果不做任何处理,会抛异常

解决异常就要引入toMap方法的第3个参数mergeFunction,函数式接口方法签名如下


R apply(T t, U u);

代码修改后如下


Map<Integer, String> map = studentList.stream()
    .collect(Collectors.toMap(Student::getId, Student::getName, (v1, v2) -> {
        System.out.println("value1: " + v1);
        System.out.println("value2: " + v2);
        return v1;
    }));

可以看出来mergeFunction参数v1为原值,v2为新值

日常开发中是必须要考虑第3参数的mergeFunction,一般采用策略如下


// 参数意义: o 为原值(old),n 为新值(new)
studentList.stream()
    // 保留策略
    .collect(Collectors.toMap(Student::getId, Student::getName, (o, n) -> o));


studentList.stream()
    // 覆盖策略
    .collect(Collectors.toMap(Student::getId, Student::getName, (o, n) -> n));

在原有的数据下增加一条特殊数据,这条特殊数据的namenull


studentList.add(new Student(10, null, 20, "男", "计科"));

此时原始代码情况1的代码都会出现空指针异常

解决方式就是toMap的第二参数valueMapper返回值不能为null,下面是解决的方式


Map<Integer, String> map = studentList.stream()
    .collect(Collectors.toMap(
        Student::getId,
        e -> Optional.ofNullable(e.getName()).orElse(""),
        (o, n) -> o
     ));
System.out.println(map);

// 此时没有空指针异常了

还有一种写法(参考写法,不用idea工具编写代码,这种写法没有意义)


public final class Func {

    
    public static <T, R> Function<T, R> defaultValue(@NonNull Function<T, R> func, @NonNull R defaultValue) {
        Objects.requireNonNull(func, "func不能为null");
        Objects.requireNonNull(defaultValue, "defaultValue不能为null");
        return t -> Optional.ofNullable(func.apply(t)).orElse(defaultValue);
    }

}

Map<Integer, String> map = studentList.stream()
    .collect(Collectors.toMap(
        Student::getId,
        Func.defaultValue(Student::getName, null),
        (o, n) -> o
    ));
System.out.println(map);

这样写是为了使用像idea这样的工具时,Func.defaultValue(Student::getName, null)调用第二个参数传null会有一个告警的标识『不关闭idea的检查就会有warning提示』。

综上就是toMap的使用注意点,

key映射的id有不能重复的限制,value映射的name也有不能有null,解决方式也在下面有提及

例9:封装一下关于Stream的工具类

工作中使用Stream最多的操作都是对于集合来的,有时Stream使用就是一个简单的过滤filter或者映射map操作,这样就出现了大量的.collect(Collectors.toMap(..., ..., ...)).collect(Collectors.toList()),有时还要再调用之前检测集合是否为null,下面就是对Stream的单个方法进行封装


public final class CollUtils {

    
    public static <T> List<T> filter(Collection<T> collection, Predicate<T> filter) {
        if (isEmpty(collection)) {
            return Collections.emptyList();
        }
        return collection.stream()
                .filter(filter)
                .collect(Collectors.toList());
    }

    
    public static <T, R> List<R> attrs(Collection<T> collection, Function<T, R> attrFunc) {
        return attrs(collection, attrFunc, true);
    }

    
    public static <T, R> List<R> attrs(Collection<T> collection, Function<T, R> attrFunc, boolean filterEmpty) {
        if (isEmpty(collection)) {
            return Collections.emptyList();
        }
        Stream<R> rStream = collection.stream().map(attrFunc);
        if (!filterEmpty) {
            return rStream.collect(Collectors.toList());
        }
        return rStream.filter(e -> {
            if (Objects.isNull(e)) {
                return false;
            }
            if (e instanceof Collection) {
                return !isEmpty((Collection<?>) e);
            }
            if (e instanceof String) {
                return ((String) e).length() > 0;
            }
            return true;
        }).collect(Collectors.toList());
    }


    
    public static <T, K, V> Map<K, V> toMap(Collection<T> collection,
                                            Function<T, K> keyMapper,
                                            Function<T, V> valueMapper) {
        if (isEmpty(collection)) {
            return Collections.emptyMap();
        }
        return collection.stream()
                .collect(Collectors.toMap(keyMapper, valueMapper, (k1, k2) -> k1));
    }

    
    public static boolean isEmpty(Collection<?> collection) {
        return Objects.isNull(collection) || collection.isEmpty();
    }
}

如果单次使用Stream都在一个函数中可能出现大量的冗余代码,如下


// 获取id集合
List<Integer> idList = studentList.stream()
    .map(Student::getId)
    .collect(Collectors.toList());
// 获取id和name对应的map
Map<Integer, String> map = studentList.stream()
    .collect(Collectors.toMap(Student::getId, Student::getName, (k1, k2) -> k1));
// 过滤出 软工 班级的人员
List<Student> list = studentList.stream()
    .filter(e -> Objects.equals(e.getClassName(), "软工"))
    .collect(Collectors.toList());

使用工具类


// 获取id集合
List<Integer> idList = CollUtils.attrs(studentList, Student::getId);
// 获取id和name对应的map
Map<Integer, String> map = CollUtils.toMap(studentList, Student::getId, Student::getName);
// 过滤出 软工 班级的人员
List<Student> list = CollUtils.filter(studentList, e -> Objects.equals(e.getClassName(), "软工"));

工具类旨在减少单次使用Stream时出现的冗余代码,如toMaptoList,同时也进行了为null判断

总结

本篇介绍了函数式接口LambdaOptional方法引用Stream等一系列知识点

也是工作中经过长时间积累终结下来的,比如例5中每一个操作都换一行,这样不完全是为了格式化好看


List<String> allFemaleNameList = studentList.stream()
    .filter(stu -> Objects.equals("女", stu.getSex()))
    .map(Student::getName)
    .collect(Collectors.toList());
System.out.println(allFemaleNameList);
// 这样写 .filter 和 .map 的函数表达式中报错可以看出来是那一行

如果像下面这样写,报错是就会指示到一行上不能直接看出来是.filter还是.map报的错,并且这样写也显得拥挤


List<String> allFemaleNameList = studentList.stream().filter(stu -> Objects.equals("女", stu.getSex())).map(Student::getName).collect(Collectors.toList());
System.out.println(allFemaleNameList);

Stream的使用远远不止本篇文章介绍到的,比如一些同类的IntStreamLongStreamDoubleStream都是大同小异,只要把Lambda搞熟其他用法都一样

学习Stream流一定要结合场景来,同时也要注意Stream需要规避的一些风险,如toMap的注意点(例8有详细介绍)。

还有一些高级用法downstream下游 - 收集器等(例4,例7)。

以上就是JDK8中Stream使用解析的详细内容,更多关于JDK8中Stream使用的资料请关注编程网其它相关文章!

--结束END--

本文标题: 深入理解JDK8中Stream使用

本文链接: https://www.lsjlt.com/news/127683.html(转载时请注明来源链接)

有问题或投稿请发送至: 邮箱/279061341@qq.com    QQ/279061341

本篇文章演示代码以及资料文档资料下载

下载Word文档到电脑,方便收藏和打印~

下载Word文档
猜你喜欢
  • 深入理解JDK8中Stream使用
    概述 Stream 是 Java8 中处理集合的关键抽象概念,它可以指定你希望对集合进行的操作,可以执行非常复杂的查找、过滤和映射数据等操作。使用Stream API 对集合数据进行...
    99+
    2024-04-02
  • 深入理解 PHP Stream 的工作原理
    PHP Stream 是 PHP 内置的一个重要特性,它提供了灵活的文件读写操作,允许 PHP 脚本以各种方式操作输入和输出数据流。本文将深入探讨 PHP Stream 的工作原理,并...
    99+
    2024-04-02
  • GosyncWaitGroup使用深入理解
    目录基本介绍使用源码分析AddDoneWait注意事项基本介绍 WaitGroup是go用来做任务编排的一个并发原语,它要解决的就是并发 - 等待的问题: 当有一个 goroutin...
    99+
    2024-04-02
  • 【Java系列】深入解析Stream API
    序言 你只管努力,其他交给时间,时间会证明一切。 文章标记颜色说明: 黄色:重要标题红色:用来标记结论绿色:用来标记论点蓝色:用来标记论点 希望这篇文章能让你不仅有一定的收获,而且可以愉快的学习,如果有什么建议,都可以留...
    99+
    2023-09-01
    java 算法 python
  • 深入浅析Node中的Stream(流)
    和上面的示例对比起来,我们发现一个流同时面向生产者和消费者服务的时候我们会选择 Duplex,当只是对数据做一些转换工作的时候我们便会选择使用 Tranform背压问题什么是背压背压问题来源于生产者消费者模式中,消费者处理速度过慢比如说,我...
    99+
    2023-05-14
    前端 Node.js
  • jdk8中怎么使用stream实现对象属性的合并
    这篇“jdk8中怎么使用stream实现对象属性的合并”文章的知识点大部分人都不太理解,所以小编给大家总结了以下内容,内容详细,步骤清晰,具有一定的借鉴价值,希望大家阅读完这篇文章能有所收获,下面我们一起来看看这篇“jdk8中怎么使用str...
    99+
    2023-06-26
  • 深入理解vue的使用
    目录理解vue的核心理念探讨vue的双向绑定原理及实现vue双向绑定原理实现过程理解vue的核心理念 使用vue会让人感到身心愉悦,它同时具备angular和react的优点,轻量级...
    99+
    2024-04-02
  • 深入理解golangchan的使用
    目录前言见真身结构体发送数据接收数据上手定义发送与接收前言 之前在看golang多线程通信的时候, 看到了go 的管道. 当时就觉得这玩意很神奇, 因为之前接触过的不管是php, j...
    99+
    2024-04-02
  • 深入理解PyTorch中的nn.Embedding的使用
    目录一、前置知识1.1 语料库(Corpus)1.2 词元(Token)1.3 词表(Vocabulary)二、nn.Embedding 基础2.1 为什么要 embedding?2...
    99+
    2024-04-02
  • 深入理解numpy中argmax的具体使用
    目录一、基本介绍二、代码实验1、一维数组情况1.1、axis=01.2、axis=12、二维数组情况2.1、axis=02.2、axis=13、三维数组情况3.1、axis=03.2...
    99+
    2024-04-02
  • Spring中Controller应用深入理解
    目录概述1. 添加依赖2. 关于异常总结概述 Controller是Spring接受并处理网页请求的组件,是整个应用的入口,因此学会Controller的常用注解对理解一个应用是重中...
    99+
    2022-12-08
    Spring Controller Spring Controller原理 Spring Controller注解
  • 深入了解Vue3中props的原理与使用
    目录前言介绍原理前提创建组件实例对象初始化Props操作创建proxy对象去获取Propsprops作为参数传入setup将proxy挂载到render上总结前言 props指父组件...
    99+
    2023-05-19
    Vue3 props原理 Vue3 props使用 Vue3 props
  • 深入理解Java中包的定义与使用
    目录包是什么?包的作用导入包中的类自定义包包的访问权限控制包是什么? 在开发过程中,会定义很多类,随着类越写越多,难免会出现类重名而发生覆盖的情况,为了在使用它们的时候不让编译器混淆...
    99+
    2024-04-02
  • NIO深入理解FileChannel使用方法原理
    目录前言FileChannelFileChannel的创建RandomAccessFile中的模式FileChannel操作文件读文件操作写文件操作对文件的更新强制输出到底层存储设备...
    99+
    2023-05-19
    NIO FileChannel NIO FileChannel使用
  • 深入理解php中unset()
    目录概述变化情况情况一:情况二:情况三:概述 unset()经常会被用到,用于销毁指定的变量,但它有自己的行为模式,如果不仔细的话可能会被中文解释给迷惑: 先来看看官方文档的说法: ...
    99+
    2024-04-02
  • 深入理解typescript中的infer关键字的使用
    目录infer案例:加深理解参考infer 这个关键字,整理记录一下,避免后面忘记了。有点难以理解呢。 infer infer 是在 typescript 2.8中新增的关键字。 ...
    99+
    2024-04-02
  • 深入理解C#委托delegate的使用
    目录1.什么是委托2:委托的实现 命名法委托静态方法的实例化委托 实例化方法的委托多播委托 匿名委托什么时候适用委托1.什么是委托 委托就是委托某个方法...
    99+
    2022-11-13
    C#委托delegate C#委托
  • 深入理解java中Arrays.sort()的用法
    在Java中,Arrays.sort()方法是用来对数组进行排序的。它使用了经过优化的快速排序算法,可以对任何类型的数组进行排序。A...
    99+
    2023-08-14
    Java
  • Netty序列化深入理解与使用
    目录序列化与反序列化序列化模式其他对象序列化方式使用json协议实现对象的传输MessagePack编码器在Netty框架中使用序列化与反序列化 序列化:把对象转换成字节的过程,称为...
    99+
    2022-11-13
    Netty 序列化 Netty 序列化协议
  • Vue深入理解插槽slot的使用
    目录一、插槽(slot)是什么二、使用场景三、slot的分类默认插槽具名插槽作用域插槽四、介绍对slot的理解一、插槽(slot)是什么 slot是组件内的一个占位符,该占位符可以在...
    99+
    2022-11-13
    Vue slot Vue slot用法 Vue slot原理
软考高级职称资格查询
编程网,编程工程师的家园,是目前国内优秀的开源技术社区之一,形成了由开源软件库、代码分享、资讯、协作翻译、讨论区和博客等几大频道内容,为IT开发者提供了一个发现、使用、并交流开源技术的平台。
  • 官方手机版

  • 微信公众号

  • 商务合作