深入理解JDK8中Stream使用

概述

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);
/*
输出:
jerry
lisa
moli
Demi
*/

中间操作limit:截断元素

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

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

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

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

中间操作map:转换元素

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

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

中间操作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());

/*
输出:
第1次 value: one
第1次 value: two
第1次 value: three
第2次 value: three
第3次 value: THREE
第1次 value: four
第2次 value: four
第3次 value: FOUR
*/

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);
/*
输出:
User{name='US:tom'}
User{name='US:jerry'}
*/

中间操作sorted:排序数据

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

逆序排序

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

如果是对象如何排序,自定义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);
/*
输出:
User{name='US:jerry'}
User{name='US:tom'}
*/	

中间操作distinct:去重

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

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

中间操作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));
    });

/*
输出:
[1, 2]
[3, 4]
*/	

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

/*
输出:
1
2
3
4
*/

终端操作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(""));
/*
输出:
one
*/	

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

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

System.out.println(firstOpt.orElse(""));
/*
输出:
one
*/	

返回的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);
/*
输出:
[one, two, three, four]
*/	

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

Set<String> set = Stream.of("one", "one", "two", "three", "four")
    .collect(Collectors.toSet());
System.out.println(set);
/*
输出:
[four, one, two, three]
*/

字符串拼接

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);
/*
输出:
{
    1=User(id=1, name=tom),
    2=User(id=2, name=jerry)
}
*/

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);
}
/*
keyMapper:Key 的映射函数
valueMapper:Value 的映射函数
mergeFunction:当 Key 冲突时,调用的合并方法
*/

数据分组

@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);
/*
输出:
{
    偶=[User(id=1, name=tom), User(id=3, name=moli)],
    奇=[User(id=2, name=jerry), User(id=4, name=lisa)]
}
*/	

分组后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:封装一个分页函数

/**
* 分页方法
*
* @param list     要分页的数据
* @param pageNo   当前页
* @param pageSize 页大小
*/
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);
/*
输出:
[
  Student(id=1, name=tom, age=19, sex=男, className=软工),
  Student(id=2, name=lisa, age=15, sex=女, className=软工),
  Student(id=3, name=Ada, age=16, sex=女, className=软工)
]
*/

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

List<Integer> idList = studentList.stream()
    .filter(e -> Objects.equals(e.getClassName(), "软工"))
    .map(Student::getId)
    .collect(Collectors.toList());
System.out.println(idList);
/*
输出:
[1, 2, 3, 5, 7]
*/

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

Map<String, List<String>> map = studentList.stream()
        .collect(Collectors.groupingBy(
                Student::getClassName,
                Collectors.mapping(Student::getName, Collectors.toList())
        ));
System.out.println(map);
/*
输出:
{
  计科=[Dora, Farrah, jerry, Adam],
  软工=[tom, lisa, Ada, Bob, Helen]
}
*/

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

Map<String, Long> map = studentList.stream()
    .collect(Collectors.groupingBy(
        Student::getClassName,
        Collectors.mapping(Function.identity(), Collectors.counting())
    ));
System.out.println(map);
/*
输出:
{
  计科=4,
  软工=5
}
*/

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

List<String> allFemaleNameList = studentList.stream()
    .filter(stu -> Objects.equals("女", stu.getSex()))
    .map(Student::getName)
    .collect(Collectors.toList());
System.out.println(allFemaleNameList);
/*
输出:
[lisa, Ada, Dora, Farrah, Helen]
*/

例6:依照年龄排序

// 年龄升序排序
List<Student> stuList1 = studentList.stream()
    // 升序
    .sorted(Comparator.comparingInt(Student::getAge))
    .collect(Collectors.toList());
System.out.println(stuList1);
/*
输出:
[
Student(id=8, name=jerry, age=12, sex=男, className=计科),
Student(id=7, name=Helen, age=13, sex=女, className=软工),
Student(id=4, name=Dora, age=14, sex=女, className=计科),
Student(id=2, name=lisa, age=15, sex=女, className=软工),
Student(id=6, name=Farrah, age=15, sex=女, className=计科),
Student(id=3, name=Ada, age=16, sex=女, className=软工),
Student(id=1, name=tom, age=19, sex=男, className=软工),
Student(id=5, name=Bob, age=20, sex=男, className=软工),
Student(id=9, name=Adam, age=20, sex=男, className=计科)
]
*/

// 年龄降序排序
List<Student> stuList2 = studentList.stream()
    // 降序
    .sorted(Comparator.comparingInt(Student::getAge).reversed())
    .collect(Collectors.toList());
System.out.println(stuList2);
/*
输出:
[
Student(id=5, name=Bob, age=20, sex=男, className=软工),
Student(id=9, name=Adam, age=20, sex=男, className=计科),
Student(id=1, name=tom, age=19, sex=男, className=软工),
Student(id=3, name=Ada, age=16, sex=女, className=软工),
Student(id=2, name=lisa, age=15, sex=女, className=软工),
Student(id=6, name=Farrah, age=15, sex=女, className=计科),
Student(id=4, name=Dora, age=14, sex=女, className=计科),
Student(id=7, name=Helen, age=13, sex=女, className=软工),
Student(id=8, name=jerry, age=12, sex=男, className=计科)
]
*/

例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());
                        })
                )
        );
/*
输出:
{
  计科 =[
    Student(id = 8, name = jerry, age = 12, sex = 男, className = 计科),
    Student(id = 4, name = Dora, age = 14, sex = 女, className = 计科),
    Student(id = 6, name = Farrah, age = 15, sex = 女, className = 计科),
    Student(id = 9, name = Adam, age = 20, sex = 男, className = 计科)
  ],
  软工 =[
    Student(id = 7, name = Helen, age = 13, sex = 女, className = 软工),
    Student(id = 2, name = lisa, age = 15, sex = 女, className = 软工),
    Student(id = 3, name = Ada, age = 16, sex = 女, className = 软工),
    Student(id = 1, name = tom, age = 19, sex = 男, className = 软工),
    Student(id = 5, name = Bob, age = 20, sex = 男, className = 软工)
  ]
}
*/

本例中使用到的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();
                        })
                )
        );
/*
输出:
{
  计科=4,
  软工=5
}
*/

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

Map<Integer, String> map = studentList.stream()
    .collect(Collectors.toMap(Student::getId, Student::getName));
System.out.println(map);
/*
输出:
{
  1=tom,
  2=lisa,
  3=Ada,
  4=Dora,
  5=Bob,
  6=Farrah,
  7=Helen,
  8=jerry,
  9=Adam
}
*/

情况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;
    }));
/*
输出:
value1: Adam
value2: Adam - 2
{1=tom, 2=lisa, 3=Ada, 4=Dora, 5=Bob, 6=Farrah, 7=Helen, 8=jerry, 9=Adam}
*/

可以看出来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);
/*
输出:
{1=tom, 2=lisa, 3=Ada, 4=Dora, 5=Bob, 6=Farrah, 7=Helen, 8=jerry, 9=Adam, 10=}
*/
// 此时没有空指针异常了

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

public final class Func {

    /**
     * 当 func 执行结果为 null 时, 返回 defaultValue
     *
     * @param func         转换函数
     * @param defaultValue 默认值
     * @return
     */
    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 {

    /**
     * 过滤数据集合
     *
     * @param collection 数据集合
     * @param filter     过滤函数
     * @return
     */
    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());
    }

    /**
     * 获取指定集合中的某个属性
     *
     * @param collection 数据集合
     * @param attrFunc   属性映射函数
     * @return
     */
    public static <T, R> List<R> attrs(Collection<T> collection, Function<T, R> attrFunc) {
        return attrs(collection, attrFunc, true);
    }

    /**
     * 获取指定集合中的某个属性
     *
     * @param collection  数据集合
     * @param attrFunc    属性映射函数
     * @param filterEmpty 是否过滤空值 包括("", null, [])
     * @return
     */
    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());
    }

    /**
     * 转换为map, 有重复key时, 使用第一个值
     *
     * @param collection  数据集合
     * @param keyMapper   key映射函数
     * @param valueMapper value映射函数
     * @return
     */
    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));
    }

    /**
     * 判读集合为空
     *
     * @param collection 数据集合
     * @return
     */
    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使用的资料请关注我们其它相关文章!

(0)

相关推荐

  • 深入解析Jdk8中Stream流的使用让你脱离for循环

    学习要求: 知道一点儿函数式接口和Lambda表达式的基础知识,有利于更好的学习. 1.先体验一下Stream的好处 需求:给你一个ArrayList用来保存学生的成绩,让你打印出其中大于60的成绩. public static void main(String[] args) { ArrayList<Integer> arrList = new ArrayList<>(); for (int i = 0; i < 100; i++) { arrList.add((int)

  • JDK8通过Stream 对List,Map操作和互转的实现

    1.Map数据转换为自定义对象的List,例如把map的key,value分别对应Person对象两个属性: List<Person> list = map.entrySet().stream().sorted(Comparator.comparing(e -> e.getKey())) .map(e -> new Person(e.getKey(), e.getValue())).collect(Collectors.toList()); List<Person> l

  • Java如何使用Optional与Stream取代if判空逻辑(JDK8以上)

    通过本文你可以用非常简短的代码替代业务逻辑中的判null校验,并且很容易的在出现空指针的时候进行打日志或其他操作. 注:如果对Java8新特性中的lambda表达式与Stream不熟悉的可以去补一下基础,了解概念. 首先下面代码中的List放入了很多Person对象,其中有的对象是null的,如果不加校验调用Person的getXXX()方法肯定会报空指针错误,一般我们采取的方案就是加上if判断: public class DemoUtils { public static void main(

  • 深入理解JDK8中Stream使用

    概述 Stream 是 Java8 中处理集合的关键抽象概念,它可以指定你希望对集合进行的操作,可以执行非常复杂的查找.过滤和映射数据等操作.使用Stream API 对集合数据进行操作,就类似于使用 SQL 执行的数据库查询.也可以使用 Stream API 来并行执行操作.简而言之,Stream API 提供了一种高效且易于使用的处理数据的方式. 特点: 不是数据结构,不会保存数据. 不会修改原来的数据源,它会将操作后的数据保存到另外一个对象中.(保留意见:毕竟peek方法可以修改流中元素)

  • 一文详解Java中Stream流的使用

    目录 简介 操作1:创建流 操作2:中间操作 筛选(过滤).去重 映射 排序 消费 操作3:终止操作 匹配.最值.个数 收集 规约 简介 说明 本文用实例介绍stream的使用. JDK8新增了Stream(流操作) 处理集合的数据,可执行查找.过滤和映射数据等操作. 使用Stream API 对集合数据进行操作,就类似于使用 SQL 执行的数据库查询.可以使用 Stream API 来并行执行操作. 简而言之,Stream API 提供了一种高效且易于使用的处理数据的方式. 特点 不是数据结构

  • JDK8中的HashMap初始化和扩容机制详解

    一.HashMap初始化方法 HashMap() 不带参数,默认初始化大小为16,加载因子为0.75: HashMap(int initialCapacity) 指定初始化大小: HashMap(int initialCapacity, float loadFactor) 指定初始化大小和加载因子大小: HashMap(Map<? extends K,? extends V> m) 用现有的一个map来构造HashMap. 二.分析初始化过程 1.初始化代码测试用例 Map<String

  • jdk8使用stream实现两个list集合合并成一个(对象属性的合并)

    目录 一.前言 二.示例 示例1:java8 合并两个 list<map> 示例2:java8 合并两个 list<T> 示例3:java8 合并两个 list<T>,集合个数前者小于后者,要后者 示例4:java8 合并两个 list<T>,集合个数前者大于后者,要后者 java使用stream实现list中对象属性的合并:根据两个List中的某个相同字段合并成一条List,包含两个List中的字段 一.前言 为什么要用Lambda表达式和Stream流做

  • Java中Stream流中map和forEach的区别详解

    目录 什么是 stream 流 Map forEach 使用场景 不是很难的知识,但是今天犯错了,记录一下 什么是 stream 流 我们在使用集合或数组对元素进行操作时往往会遇到这种情况:通过对不同类型的存储元素,按照特定条件进行查找.排序.等操作时往往会写一大段代码,而且更要命的是,不同类型的数据,操作的方法也不一样,比如一个存储 Student 实体类和一个只存储 String 类型的集合俩者的操作步骤肯定大不一样且无法通用,而 stream API 就解决了这些问题,对数据操作时进行了统

  • Java8中Stream的详细使用方法大全

    目录 一.概述 1.使用流的好处 2.流是什么? 二.分类 三.Stream的创建 1.通过 java.util.Collection.stream() 方法用集合创建流 2.使用 java.util.Arrays.stream(T[]array)方法用数组创建流 3.使用 Stream的静态方法:of().iterate().generate() 四.Stream API简介 1.遍历/匹配(foreach/find/match) 2.按条件匹配filter 3.聚合max.min.count

  • Java中stream.map和stream.forEach的区别

    目录 什么是 stream 流 stream.map 和 stream.forEach 的区别 网上很多关于讲解这俩个区别的文章,但大多数要么不明不白,要么太复杂难理解.所以自己通俗的讲一下,毕竟不会太深奥,只是个人理解 (评论区指出了错误改了一下). 什么是 stream 流 我们在使用集合或数组对元素进行操作时往往会遇到这种情况:通过对不同类型的存储元素,按照特定条件进行查找.排序.等操作时往往会写一大段代码,而且更要命的是,不同类型的数据,操作的方法也不一样,比如一个存储 Student

  • Java 8中 Stream小知识小技巧方法梳理

    目录 前言 只能遍历的一次 Stream 那么为什么流只能遍历一次呢? 流操作 中间操作 终端操作 前言 上篇只是简单的动手操作操作了流(stream),那 stream 到底是什么呢? 官方的简短定义:“从支持数据处理操作的源生成的元素序列” 分成三部分: 元素序列:你可以简单将它类比于一样,不过集合说的是数据的集合,而 stream 重点在于表达计算.如我们之前说到的 filter.map.sorted.limit等等 源:昨天我提到,如果了解过 Liunx 管道命令的朋友们,会知道,Liu

  • 深入理解python中函数传递参数是值传递还是引用传递

    目前网络上大部分博客的结论都是这样的: Python不允许程序员选择采用传值还是传 引用.Python参数传递采用的肯定是"传对象引用"的方式.实际上,这种方式相当于传值和传引用的一种综合.如果函数收到的是一个可变对象(比如字典 或者列表)的引用,就能修改对象的原始值--相当于通过"传引用"来传递对象.如果函数收到的是一个不可变对象(比如数字.字符或者元组)的引用,就不能 直接修改原始对象--相当于通过"传值"来传递对象. 你可以在很多讨论该问题

随机推荐