【Java】Stream流常用操作以及Optional入门

简介: 【Java】Stream流常用操作以及Optional入门

Stream流中的方法

Stream提供了大量的方法进行聚集操作,这些方法既可以是“中间的”,也可以是“末端的”。

中间方法:中间操作允许流保持打开状态,并允许直接调用后续方法。上面程序中的map()方法就是中间方法。中间方法的返回值是另外一个流。

末端方法:末端方法是对流的最终操作。当对某个Stream执行末端方法后,该流将会被“消耗”且不再可用。上面程序中的sum()、count()、average()等方法都是末端方法。

除此之外,关于流的方法还有如下两个特征:

有状态的方法:这种方法会给流增加一些新的属性,比如元素的唯一性、元素的最大数量、保证元素以排序的方式被处理等。有状态的方法往往需要更大的性能开销。

短路方法:短路方法可以尽早结束对流的操作,不必检查所有的元素。

下面简单介绍一下Stream常用的中间方法:

filter(Predicate predicate):过滤Stream中所有不符合predicate的元素。

mapToXxx(ToXxxFunction mapper):使用ToXxxFunction对流中的元素执行一对一的转换,该方法返回的新流中包含了ToXxxFunction转换生成的所有元素。

peek(Consumer action):依次对每个元素执行一些操作,该方法返回的流与原有流包含相同的元素。该方法主要用于调试。

distinct():该方法用于排序流中所有重复的元素(判断元素重复的标准是使用equals()比较返回true)。这是一个有状态的方法。

sorted():该方法用于保证流中的元素在后续的访问中处于有序状态。这是一个有状态的方法。

limit(long maxSize):该方法用于保证对该流的后续访问中最大允许访问的元素个数。这是一个有状态的、短路方法。

下面简单介绍一下Stream常用的末端方法:

forEach(Consumer action):遍历流中所有元素,对每个元素执行action。

toArray():将流中所有元素转换为一个数组。

reduce():该方法有三个重载的版本,都用于通过某种操作来合并流中的元素。

min():返回流中所有元素的最小值。

max():返回流中所有元素的最大值。

count():返回流中所有元素的数量。

anyMatch(Predicate predicate):判断流中是否至少包含一个元素符合Predicate条件。

noneMatch(Predicate predicate):判断流中是否所有元素都不符合Predicate条件。

findFirst():返回流中的第一个元素。

findAny():返回流中的任意一个元素。

除此之外,Java 8允许使用流式API来操作集合,Collection接口提供了一个stream()默认方法,该方法可返回该集合对应的流,接下来即可通过流式API来操作集合元素。由于Stream可以对集合元素进行整体的聚集操作,因此Stream极大地丰富了集合的功能。

Java 8新增了Stream、IntStream、LongStream、DoubleStream等流式API,这些API代表多个支持串行和并行聚集操作的元素。上面4个接口中,Stream是一个通用的流接口,而IntStream、LongStream、DoubleStream则代表元素类型为int、long、double的流。

Java 8还为上面每个流式API提供了对应的Builder,例如Stream.Builder、IntStream.Builder、LongStream.Builder、DoubleStream.Builder,开发者可以通过这些Builder来创建对应的流。

独立使用Stream的步骤如下:

使用Stream或XxxStream的builder()类方法创建该Stream对应的Builder。

重复调用Builder的add()方法向该流中添加多个元素。

调用Builder的build()方法获取对应的Stream。

调用Stream的聚集方法。

在上面4个步骤中,第4步可以根据具体需求来调用不同的方法,Stream提供了大量的聚集方法供用户调用,具体可参考Stream或XxxStream的API文档。对于大部分聚集方法而言,每个Stream只能执行一次。

特别注意:

  • 流的使用是遵循以下的原则的。
  • 惰性求值(如果没有终结操作,没有中间操作是不会得到执行的)
    流是一次性的(一旦一个流对象经过一个终结操作后。这个流就不能再被使用)
  • 不会影响原数据(我们在流中可以多数据做很多处理。但是正常情况下是不会影响原来集合中的元素的。这往往也是我们期望的)

串行Stream

Stream 接口

java.util.Stream 表示能应用在一组元素上一次执行的操作序列。Stream 操作分为中间操作或者最终操作两种,最终操作返回一特定类型的计算结果,而中间操作返回Stream本身,这样你就可以将多个操作依次串起来。Stream 的创建需要指定一个数据源,比如 java.util.Collection的子类,List或者Set,Map不支持。Stream的操作可以串行执行或者并行执行。首先看看Stream是怎么用,首先创建实例代码的用到的数据List:

代码如下:

List stringCollection = new ArrayList<>();
stringCollection.add("ddd2");
stringCollection.add("aaa2");
stringCollection.add("bbb1");
stringCollection.add("aaa1");
stringCollection.add("bbb3");
stringCollection.add("ccc");
stringCollection.add("bbb2");
stringCollection.add("ddd1");

Java 8扩展了集合类,可以通过 Collection.stream() 或者 Collection.parallelStream() 来创建一个Stream。下面几节将详细解释常用的Stream操作:

Filter 过滤

过滤通过一个predicate接口来过滤并只保留符合条件的元素,该操作属于中间操作,所以我们可以在过滤后的结果来应用其他Stream操作(比如forEach)。forEach需要一个函数来对过滤后的元素依次执行。forEach是一个最终操作,所以我们不能在forEach之后来执行其他Stream操作。

代码如下:

stringCollection
 .stream()
 .filter((s) -> s.startsWith("a"))
 .forEach(System.out::println);
// "aaa2", "aaa1"

Sort 排序

排序是一个中间操作,返回的是排序好后的Stream。如果你不指定一个自定义的Comparator则会使用默认排序。

代码如下:

stringCollection
 .stream()
 .sorted()
 .filter((s) -> s.startsWith("a"))
 .forEach(System.out::println);
// "aaa1", "aaa2"

需要注意的是,排序只创建了一个排列好后的Stream,而不会影响原有的数据源,排序之后原数据stringCollection是不会被修改的:

代码如下:

System.out.println(stringCollection);
// ddd2, aaa2, bbb1, aaa1, bbb3, ccc, bbb2, ddd1

Map 映射

中间操作map会将元素根据指定的Function接口来依次将元素转成另外的对象,下面的示例展示了将字符串转换为大写字符串。你也可以通过map来将对象转换成其他类型,map返回的Stream类型是根据你map传递进去的函数的返回值决定的。

代码如下:

stringCollection
 .stream()
 .map(String::toUpperCase)
 .sorted((a, b) -> b.compareTo(a))
 .forEach(System.out::println);
// "DDD2", "DDD1", "CCC", "BBB3", "BBB2", "AAA2", "AAA1"

flatMap映射

map只能把一个对象转换成另一个对象来作为流中的元素,而flatMap可以把一个对象转换成多个对象作为流中的元素。

这里的意思是,如果我们的一个类中的一个对象数据我们取出来,他是一个List类型,那么如果我们使用的是map,那么我们再次放入到流中的就是这个List类型,也许(当然,需要看业务场景),这并不方便我们对这个List类型中的数据进行处理操作,所以,有没有一种方法,可以我们得到一个集合之后,我们得到的流是这个集合中的所有数据呢?从而变为对集合中所有数据的操作?

有,就是flatMap,我们假设我们的业务需求是需要取出所有Author类中的books属性,books属性是一个List集合,假设我们需要取出所有的List集合并且去重后得到book的name,那么使用map和使用flatMap的差距就很明显了。

package com.stream;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.stream.Collectors;
public class Main {
    public static void main(String[] args) {
        List<Author> authors = getAuthors();
        //会发现其实输出的是一个又一个的List集合
        List<Book> books = new ArrayList<>();
        authors.stream().map(x->{
            return x.getBooks();
        }).forEach(x->{
            for (Book book : x) {
            books.add(book);
            }
        });
        //System.out.println(books);
        books.stream().distinct().forEach(x->{
            System.out.println(x.getName());
        });
        System.out.println("----------------");
        authors.stream().flatMap(x->{
            return x.getBooks().stream();
        }).distinct().forEach(x-> System.out.println(x.getName()));
        System.out.println("----------------");
        //得到书的分类,并且书的分类不止一种可能
   authors.stream()
                .flatMap(x->{
            return x.getBooks().stream();
        }).distinct()
                .flatMap(
                        book->Arrays.stream(book.getCategory().split(",")))
                        .distinct().forEach(System.out::println);
    }
    private static List<Author> getAuthors() {
        //数据初始化
        Author author = new Author(1L, "蒙多", 33, "一个从菜刀中明悟哲理的祖安人", null);
        Author author2 = new Author(2L, "亚拉索", 15, "狂风也追逐不上他的思考速度", null);
        Author author3 = new Author(3L, "易", 14, "是这个世界在限制他的思维", null);
        Author author4 = new Author(3L, "易", 14, "是这个世界在限制他的思维", null);
        //书籍列表
        List<Book> books1 = new ArrayList<>();
        List<Book> books2 = new ArrayList<>();
        List<Book> books3 = new ArrayList<>();
        books1.add(new Book(1L, "刀的两侧是光明与黑暗", "哲学,爱情", 88, "用一把刀划分了爱恨"));
        books1.add(new Book(2L, "一个人不能死在同一把刀下", "个人成长,爱情", 99, "讲述如何从失败中明悟真理"));
        books2.add(new Book(3L, "那风吹不到的地方", "哲学", 85, "带你用思维去领略世界的尽头"));
        books2.add(new Book(3L, "那风吹不到的地方", "哲学", 85, "带你用思维去领略世界的尽头"));
        books2.add(new Book(4L, "吹或不吹", "爱情,个人传记", 56, "一个哲学家的恋爱观注定很难把他所在的时代理解"));
        books3.add(new Book(5L, "你的剑就是我的剑", "爱情", 56, "无法想象一个武者能对他的伴侣这么的宽容"));
        books3.add(new Book(6L, "风与剑", "个人传记", 100, "两个哲学家灵魂和肉体的碰撞会激起怎么样的火花呢?"));
        books3.add(new Book(6L, "风T剑", "个人传记", 100, "两个哲学家灵魂和肉体的碰撞会激起怎么样的火花呢?"));
        author.setBooks(books1);
        author2.setBooks(books2);
        author3.setBooks(books3);
        author4.setBooks(books3);
        List<Author> authorList = new ArrayList<>(Arrays.asList(author, author2, author3, author4));
        return authorList;
    }
}
@Data
@NoArgsConstructor
@AllArgsConstructor
@EqualsAndHashCode
public class Author {
    private Long id;
    private String name;
    private Integer age;
    private String intro;
    private List<Book> books;
}
@Data
@NoArgsConstructor
@AllArgsConstructor
@EqualsAndHashCode
public class Book {
    private Long id;
    private String name;
    private String category;
    private Integer score;
    private String intro;
}

Match 匹配

Stream提供了多种匹配操作,允许检测指定的Predicate是否匹配整个Stream。所有的匹配操作都是最终操作,并返回一个boolean类型的值。

代码如下:

boolean anyStartsWithA = 
 stringCollection
 .stream()
 .anyMatch((s) -> s.startsWith("a"));
System.out.println(anyStartsWithA); // true
boolean allStartsWithA = 
 stringCollection
 .stream()
 .allMatch((s) -> s.startsWith("a"));
System.out.println(allStartsWithA); // false
boolean noneStartsWithZ = 
 stringCollection
 .stream()
 .noneMatch((s) -> s.startsWith("z"));
System.out.println(noneStartsWithZ); // true

Count 计数

计数是一个最终操作,返回Stream中元素的个数,返回值类型是long。

代码如下:

long startsWithB = 
 stringCollection
 .stream()
 .filter((s) -> s.startsWith("b"))
 .count();
System.out.println(startsWithB); // 3

Reduce 规约

这是一个最终操作,允许通过指定的函数来将stream中的多个元素规约为一个元素,规越后的结果是通过Optional接口表示的:

reduce的作用是把stream中的元素组合起来,我们可以传入一个初始值,他会按照我们的计算方式依次拿流中的元素和在初始值的基础上进行计算,计算结果在和后面的元素计算。

代码如下:

Optional reduced =
 stringCollection
 .stream()
 .sorted()
 .reduce((s1, s2) -> s1 + "#" + s2);
reduced.ifPresent(System.out::println);
// "aaa1#aaa2#bbb1#bbb2#bbb3#ccc#ddd1#ddd2"

并行Streams

前面提到过Stream有串行和并行两种,串行Stream上的操作是在一个线程中依次完成,而并行Stream则是在多个线程上同时执行。

下面的例子展示了是如何通过并行Stream来提升性能:

首先我们创建一个没有重复元素的大表:

代码如下:

int max = 1000000;
List values = new ArrayList<>(max);
for (int i = 0; i < max; i++) {
 UUID uuid = UUID.randomUUID();
 values.add(uuid.toString());
}

然后我们计算一下排序这个Stream要耗时多久,

串行排序:

代码如下:

long t0 = System.nanoTime();
long count = values.stream().sorted().count();
System.out.println(count);
long t1 = System.nanoTime();
long millis = TimeUnit.NANOSECONDS.toMillis(t1 - t0);
System.out.println(String.format("sequential sort took: %d ms", millis));
// 串行耗时: 899 ms

并行排序:

代码如下:

long t0 = System.nanoTime();
long count = values.parallelStream().sorted().count();
System.out.println(count);
long t1 = System.nanoTime();
long millis = TimeUnit.NANOSECONDS.toMillis(t1 - t0);
System.out.println(String.format("parallel sort took: %d ms", millis));
// 并行排序耗时: 472 ms

上面两个代码几乎是一样的,但是并行版的快了50%之多,唯一需要做的改动就是将stream()改为parallelStream()。

Map

前面提到过,Map类型不支持stream,不过Map提供了一些新的有用的方法来处理一些日常任务。

代码如下:

Map<Integer, String> map = new HashMap<>();
for (int i = 0; i < 10; i++) {
   map.putIfAbsent(i, "val" + i);
}
map.forEach((id, val) -> System.out.println(val));

以上代码很容易理解, putIfAbsent 不需要我们做额外的存在性检查,而forEach则接收一个Consumer接口来对map里的每一个键值对进行操作。

下面的例子展示了map上的其他有用的函数:

代码如下:

computeIfPresent函数讲解

map.computeIfPresent(3, (num, val) -> val + num);
map.get(3); // val33
map.computeIfPresent(9, (num, val) -> null);
map.containsKey(9); // false
map.computeIfAbsent(23, num -> "val" + num);
map.containsKey(23); // true
map.computeIfAbsent(3, num -> "bam");
map.get(3); // val33

接下来展示如何在Map里删除一个键值全都匹配的项:

代码如下:

map.remove(3, "val3");
map.get(3); // val33
map.remove(3, "val33");
map.get(3); // null

另外一个有用的方法:

getOrDefault(Object key, V defaultValue)

意思就是当Map集合中有这个key时,就使用这个key对应的value值,如果没有就使用默认值defaultValue

代码如下:

map.getOrDefault(42, "not found"); // not found

对Map的元素做合并也变得很容易了:

代码如下:

map.merge(9, "val9", (value, newValue) -> value.concat(newValue));
map.get(9); // val9
map.merge(9, "concat", (value, newValue) -> value.concat(newValue));
map.get(9); // val9concat

Merge做的事情是如果键名不存在则插入,否则则对原键对应的值做合并操作并重新插入到map中。


相关文章
|
2天前
|
存储 监控 Java
【Java并发】【线程池】带你从0-1入门线程池
欢迎来到我的技术博客!我是一名热爱编程的开发者,梦想是编写高端CRUD应用。2025年我正在沉淀中,博客更新速度加快,期待与你一起成长。 线程池是一种复用线程资源的机制,通过预先创建一定数量的线程并管理其生命周期,避免频繁创建/销毁线程带来的性能开销。它解决了线程创建成本高、资源耗尽风险、响应速度慢和任务执行缺乏管理等问题。
88 60
【Java并发】【线程池】带你从0-1入门线程池
|
1月前
|
自然语言处理 Java
Java中的字符集编码入门-增补字符(转载)
本文探讨Java对Unicode的支持及其发展历程。文章详细解析了Unicode字符集的结构,包括基本多语言面(BMP)和增补字符的表示方法,以及UTF-16编码中surrogate pair的使用。同时介绍了代码点和代码单元的概念,并解释了UTF-8的编码规则及其兼容性。
113 60
|
2月前
|
Java 开发者 微服务
Spring Boot 入门:简化 Java Web 开发的强大工具
Spring Boot 是一个开源的 Java 基础框架,用于创建独立、生产级别的基于Spring框架的应用程序。它旨在简化Spring应用的初始搭建以及开发过程。
111 7
Spring Boot 入门:简化 Java Web 开发的强大工具
|
2月前
|
监控 架构师 Java
Java虚拟机调优的艺术:从入门到精通####
本文作为一篇深入浅出的技术指南,旨在为Java开发者揭示JVM调优的神秘面纱,通过剖析其背后的原理、分享实战经验与最佳实践,引领读者踏上从调优新手到高手的进阶之路。不同于传统的摘要概述,本文将以一场虚拟的对话形式,模拟一位经验丰富的架构师向初学者传授JVM调优的心法,激发学习兴趣,同时概括性地介绍文章将探讨的核心议题——性能监控、垃圾回收优化、内存管理及常见问题解决策略。 ####
|
2月前
|
存储 Java 数据挖掘
Java 8 新特性之 Stream API:函数式编程风格的数据处理范式
Java 8 引入的 Stream API 提供了一种新的数据处理方式,支持函数式编程风格,能够高效、简洁地处理集合数据,实现过滤、映射、聚合等操作。
110 6
|
2月前
|
Java API 开发者
Java中的Lambda表达式与Stream API的协同作用
在本文中,我们将探讨Java 8引入的Lambda表达式和Stream API如何改变我们处理集合和数组的方式。Lambda表达式提供了一种简洁的方法来表达代码块,而Stream API则允许我们对数据流进行高级操作,如过滤、映射和归约。通过结合使用这两种技术,我们可以以声明式的方式编写更简洁、更易于理解和维护的代码。本文将介绍Lambda表达式和Stream API的基本概念,并通过示例展示它们在实际项目中的应用。
|
3月前
|
监控 安全 Java
Java中的多线程编程:从入门到实践####
本文将深入浅出地探讨Java多线程编程的核心概念、应用场景及实践技巧。不同于传统的摘要形式,本文将以一个简短的代码示例作为开篇,直接展示多线程的魅力,随后再详细解析其背后的原理与实现方式,旨在帮助读者快速理解并掌握Java多线程编程的基本技能。 ```java // 简单的多线程示例:创建两个线程,分别打印不同的消息 public class SimpleMultithreading { public static void main(String[] args) { Thread thread1 = new Thread(() -> System.out.prin
|
2月前
|
Rust 安全 Java
Java Stream 使用指南
本文介绍了Java中Stream流的使用方法,包括如何创建Stream流、中间操作(如map、filter、sorted等)和终结操作(如collect、forEach等)。此外,还讲解了并行流的概念及其可能带来的线程安全问题,并给出了示例代码。
165 0
|
3月前
|
Java 大数据 API
14天Java基础学习——第1天:Java入门和环境搭建
本文介绍了Java的基础知识,包括Java的简介、历史和应用领域。详细讲解了如何安装JDK并配置环境变量,以及如何使用IntelliJ IDEA创建和运行Java项目。通过示例代码“HelloWorld.java”,展示了从编写到运行的全过程。适合初学者快速入门Java编程。
|
3月前
|
存储 安全 Java
🌟Java零基础-反序列化:从入门到精通
【10月更文挑战第21天】本文收录于「滚雪球学Java」专栏,专业攻坚指数级提升,希望能够助你一臂之力,帮你早日登顶实现财富自由🚀;同时,欢迎大家关注&&收藏&&订阅!持续更新中,up!up!up!!
127 5

热门文章

最新文章