Java的Stream流

154 阅读7分钟

Stream流操作

Java 8 Stream | 菜鸟教程 (runoob.com)

参考 吃透JAVA的Stream流操作,多年实践总结 - 掘金 (juejin.cn)

Stream(流) 是一个来自数据源的元素队列,元素是特定类型的对象,形成一个队列。 Java中的Stream并不会存储元素,而是按需计算。数据源:流的来源,可以是集合,数组 等。(可参考使用Stream - 廖雪峰的官方网站 (liaoxuefeng.com)

Stream流操作是通过对把元素转换成Stream,然后通过中间操作,最终得到想要的结果,一般用在对集合操作中。

Stream流操作分为三步骤:

  • 创建Stream流
  • Stream中间处理
  • 终止Steam

创建Stream流

  1. 集合获取流

通过Collection接口中的Stream方法生成。

List<String> strings = Arrays.asList("abc", "", "bc", "efg", "abcd","", "jkl");
//  创建出一个新的stream串行流对象
Stream<String> stream = strings.stream();
//  创建出一个可并行执行的stream流对象
Stream<String> stream = strings.parallelStream();
  1. 数组获取流

如果使用的不是集合而是数组,由于数组对象不可能添加默认方法,所以 Stream 接口中提供了静态方法of 。

String[] array = { "张无忌", "张翠山", "张三丰", "张一元" };
Stream<String> stream = Stream.of(array);
  1. Map获取流

java.util.Map 接口不是 Collection 的子接口,且其K-V数据结构不符合流元素的单一特征,所以获取对应的流需要分key、value或entry等情况:

Stream<String> keyStream = map.keySet().stream();
Stream<String> valueStream = map.values().stream();
Stream<Map.Entry<String, String>> entryStream = map.entrySet().stream();
  1. 文件获取流

通过Files.line方法得到一个流,并且得到的每个流是给定文件中的一行

Stream<String> lines = Files.lines(Paths.get("data.txt"), Charset.defaultCharset())
  1. 通过值生成
Stream<Integer> stream = Stream.of(1, 2, 3, 4, 5);

Stream中间处理

API说明
limit()获取数据的条数
sorted()对stream的数据进行排序
peek()对stream流中的每个元素进行逐个遍历处理,返回处理后的stream流
concat()将两个流的数据合并起来为1个新的流,返回新的stream流
filter()筛选,按照筛选条件过滤符合要求的元素, 返回新的stream流
skip(long n)跳过前 n 条数据,必须 n >= 0;
map()将流中的元素转换成另一种元素
flatMap()将流中的元素转换成另一种或多种元素
allMatch();allMatch();noneMatch()元素匹配
foreach()
max()
min()
empty()
toArray()
anyMatch()
findFirst()
findAny()
collect();count();

limit()

获取前 n 条数据,接收的参数是数据条数。

List<String> strings = Arrays.asList("abc", "vv", "bc", "efg", "abcd","", "jkl");
Stream<String> stream = strings.stream();
stream.limit(2).forEach(System.out::println);

输出:

abc
vv

sorted()

有两个重载,一个无参数,另外一个有个 Comparator类型的参数。

无参类型的按照自然顺序进行排序。

public static void main(String[] args) {
        List<String> strings = Arrays.asList("abc", "vv", "bc", "efg", "abcd", "jkl");
        Stream<String> stream = strings.stream();
        stream.sorted().forEach(System.out::println);
}

有参数的需要自定义排序规则,例如按第二位的数字大小排序

    public static void main(String[] args) {
        List<String> strings = Arrays.asList("a1c", "v2", "b3", "e4", "a5cd", "j6l");
        Stream<String> stream = strings.stream();
        stream.sorted((x,y)->Integer.parseInt(x.substring(1,2))>Integer.parseInt(y.substring(1,2))?1:-1).forEach(e->System.out.println(e));
    }

peek()

对stream流中的每个元素进行逐个遍历处理

    public static void main(String[] args) {
        List<String> strings = Arrays.asList("a1c", "v2", "b3", "e4", "a5cd", "j6l");
        Stream<String> stream = strings.stream();
        System.out.println("===========================");
        stream.peek(System.out::println).count();
        System.out.println("===========================");
    }

上面代码的结果如下:

===========================
a1c
v2
b3
e4
a5cd
j6l
===========================

peek()是一个 stream流中间处理过程,如果没有结束操作的话,是不会执行的。例如上面的方法,把count()去掉的话,什么也不会输出。

concat()

将两个流合并成一个流

public static void main(String[] args) {
        Stream<String> stringa = Stream.of("a","b","c");
        Stream<String> stringb = Stream.of("d","e","f");
        Stream<String> stream = Stream.concat(stringa, stringb);
        
        System.out.println("===========================");
        stream.forEach(System.out::println);
        System.out.println("===========================");
}

filter

筛选,筛选出符合条件的流数据。

public static void main(String[] args) {
    List<String> strings = Arrays.asList("a1c", "v2", "b3", "e4", "a5cd", "j6l");
    
    System.out.println("===========================");
    strings.stream().filter(i -> i.length() > 2).forEach(System.out::println);
    System.out.println("===========================");
}

skip(long n)

跳过前n条数据,n必须大于等于0,否则抛出异常;

public static void main(String[] args) {
    List<String> strings = Arrays.asList("a1c", "v2", "b3", "e4", "a5cd", "j6l");
    strings.stream().skip(2).forEach(System.out::println);
}
​
//输出:
b3
e4
a5cd
j6l

map

通过原始数据元素,映射出新的类型。

private static void map(){
    List<User> users = getUserData();
    Stream<User> stream = users.stream();
    List<UserDto> userDtos = stream.map(user -> dao2Dto(user)).collect(Collectors.toList());
}
​
private static UserDto dao2Dto(User user){
    UserDto dto = new UserDto();
    BeanUtils.copyProperties(user, dto);
    //其他额外处理
    return dto;
}
map和peek的区别
  • 源码区别

    map

    /**
     * Returns a stream consisting of the results of applying the given
     * function to the elements of this stream.
     *
     * <p>This is an <a href="package-summary.html#StreamOps">intermediate
     * operation</a>.
     *
     * @param <R> The element type of the new stream
     * @param mapper a <a href="package-summary.html#NonInterference">non-interfering</a>,
     *               <a href="package-summary.html#Statelessness">stateless</a>
     *               function to apply to each element
     * @return the new stream
     */
    <R> Stream<R> map(Function<? super T, ? extends R> mapper);

peek

    /**
     * Returns a stream consisting of the elements of this stream, additionally
     * performing the provided action on each element as elements are consumed
     * from the resulting stream.
     *
     * <p>This is an <a href="package-summary.html#StreamOps">intermediate
     * operation</a>.
     *
     * <p>For parallel stream pipelines, the action may be called at
     * whatever time and in whatever thread the element is made available by the
     * upstream operation.  If the action modifies shared state,
     * it is responsible for providing the required synchronization.
     *
     * @apiNote This method exists mainly to support debugging, where you want
     * to see the elements as they flow past a certain point in a pipeline:
     * <pre>{@code
     *     Stream.of("one", "two", "three", "four")
     *         .filter(e -> e.length() > 3)
     *         .peek(e -> System.out.println("Filtered value: " + e))
     *         .map(String::toUpperCase)
     *         .peek(e -> System.out.println("Mapped value: " + e))
     *         .collect(Collectors.toList());
     * }</pre>
     *
     * @param action a <a href="package-summary.html#NonInterference">
     *                 non-interfering</a> action to perform on the elements as
     *                 they are consumed from the stream
     * @return the new stream
     */
    Stream<T> peek(Consumer<? super T> action);
  • peek接受的参数是Consumer,Consumer的注释如下,可以看到是不返回结果的。

Represents an operation that accepts a single input argument and returns no result. Unlike most other functional interfaces, Consumer is expected to operate via side-effects.

map接受的参数是Function,Function是可以返回参数的。

  • 使用

    peek接收一个没有返回值的λ表达式,可以做一些输出,外部处理等。map接收一个有返回值的λ表达式,之后Stream的泛型类型将转换为map参数λ表达式返回的类型

flatMap

这是用在一些比较特别的场景下,当你的 Stream 是以下这几种结构的时候,需要用到 flatMap方法,用于将原有二维结构扁平化。

  1. Stream<String[]>
  2. Stream<Set<String>>
  3. Stream<List<String>>

以上这三类结构,通过 flatMap方法,可以将结果转化为 Stream<String>这种形式,方便之后的其他操作。

比如下面这个方法,将List<List<User>>扁平处理,然后再使用 map或其他方法进行操作。

    List<UserDto> userDtos = stream.flatMap(subUserList->subUserList.stream()).map(user -> dao2Dto(user)).collect(Collectors.toList());
map和flatmap的区别
  • map 必须是一对一的,即每个元素都只能转换为1个新的元素
  • flatMap 可以是一对多的,即每个元素都可以转换为1个或者多个新的元素

allMatch()、allMatch()、noneMatch()

  • allMatch匹配所有

    List<Integer> integerList = Arrays.asList(1, 2, 3, 4, 5);
    if (integerList.stream().allMatch(i -> i > 3)) {
        System.out.println("值都大于3");
    }
    
  • anyMatch匹配其中一个

    List<Integer> integerList = Arrays.asList(1, 2, 3, 4, 5);
    if (integerList.stream().anyMatch(i -> i > 3)) {
        System.out.println("存在大于3的值");
    }//存在大于3的值则打印
    

    等同于

    for (Integer i : integerList) {
        if (i > 3) {
            System.out.println("存在大于3的值");
            break;
        }
    }
    
  • noneMatch全部不匹配

    List<Integer> integerList = Arrays.asList(1, 2, 3, 4, 5);
    if (integerList.stream().noneMatch(i -> i > 3)) {
        System.out.println("值都小于3");
    }
    

终止Steam

API说明
foreach()遍历流中的元素,也可以直接对list使用
max()获取元素中的最大值
min()获取元素中的最小值
toArray()获取数组
anyMatch()返回boolean,判断是否匹配
findFirst()查找第一个元素,返回Optional类型
findAny()随机查找一个元素,返回Optional类型
collect()通过collection()获取返回的List或者Map这样的数据结构
count()计算流的数量

foreach()

List<Integer> integerList = Arrays.asList(1, 2, 3, 4, 5);
integerList.stream().forEach(System.out::println);
// 或者直接变量列表也可以
integerList.forEach(System.out::println)

max()、min()

    public static void main(String[] args) {
        List<Integer> integers = Arrays.asList(1, 2, 3, 4);
        System.out.println(integers.stream().max(Integer::compare).get());
    }

toArray()

collection是返回列表、map 等,toArray是返回数组,有两个重载,一个空参数,返回的是 Object[]

另一个接收一个 IntFunction<R>类型参数。

@FunctionalInterface
public interface IntFunction<R> {
​
    /**
     * Applies this function to the given argument.
     *
     * @param value the function argument
     * @return the function result
     */
    R apply(int value);
}

比如像下面这样使用,参数是 User[]::new也就是new 一个 User 数组,长度为最后的 Stream 长度。

private static void toArray() {
    List<User> users = getUserData();
    Stream<User> stream = users.stream();
    User[] userArray = stream.filter(user -> user.getGender().equals(0) && user.getAge() > 50).toArray(User[]::new);
}
​
​
​

生成集合

public void testCollectStopOptions() {
    List<Dept> ids = Arrays.asList(new Dept(17), new Dept(22), new Dept(23));
// collect成list
    List<Dept> collectList = ids.stream().filter(dept -> dept.getId() > 20).collect(Collectors.toList());
    System.out.println("collectList:" + collectList);
// collect成Set
    Set<Dept> collectSet = ids.stream().filter(dept -> dept.getId() > 20).collect(Collectors.toSet());
    System.out.println("collectSet:" + collectSet);
// collect成HashMap,key为id,value为Dept对象
    Map<Integer, Dept> collectMap = ids.stream().filter(dept -> dept.getId() > 20).collect(Collectors.toMap(Dept::getId, dept -> dept));
    System.out.println("collectMap:" + collectMap);
}

生成字符串

生成拼接字符串,相比于普通方法,代码量变少,不用考虑最后的逗号

​
public void testCollectJoinStrings() {
    List<String> ids = Arrays.asList("205", "10", "308", "49", "627", "193", "111", "193");
    String joinResult = ids.stream().collect(Collectors.joining(","));
    System.out.println("拼接后:" + joinResult);
}
​

Stream优势

代码更简洁、偏声明式的编码风格,更容易体现出代码的逻辑意图

逻辑间解耦,一个stream中间处理逻辑,无需关注上游与下游的内容,只需要按约定实现自身逻辑即可

函数式接口,延迟执行的特性,中间管道操作不管有多少步骤都不会立即执行,只有遇到终止操作的时候才会开始执行,可以避免一些中间不必要的操作消耗