Java Stream介绍和实战

发布时间:2024年01月04日

目录

1. 引言

2. Stream 的基本特性

3. 创建 Stream

4. Stream 的中间操作

5. Stream 的终端操作

6. Stream 的性能优化

7. 实例演示

8. 注意事项

9. 结语


1. 引言

Java 中的 Stream 是 Java 8 引入的一种用于对集合进行操作的工具,为开发者提供了一种更便捷、更流畅的方式来处理集合数据。Stream 可以让我们以声明式的方式对集合进行各种操作,如筛选、映射、过滤、排序等,而无需显式地使用循环和临时变量。下面就带大家一起看看Java Stream的使用吧。

2. Stream 的基本特性

Stream 是 Java 8 引入的一种用于对集合进行函数式操作的工具。提供了丰富的 API,支持丰富的操作,如筛选、映射、过滤、排序等。

基本概念

  • 数据源:Stream 可以来自不同类型的数据源,如集合、数组、I/O 等。
  • 流水线:Stream 可以进行一系列的操作,形成一个流水线,但这些操作并不会立即执行,而是在遇到终端操作时才会被触发执行。
  • 中间操作:Stream 提供了多种中间操作方法,用于对流中的元素进行处理,如 filter、map、sorted、distinct 等。
  • 终端操作:Stream 最终需要通过终端操作来触发流水线的执行,产生结果,如 forEach、collect、reduce、count 等。
  • 惰性求值:Stream 的中间操作是惰性求值的,只有遇到终端操作时才会被触发执行。
  • 并行流:Stream 提供了并行流的功能,通过并行处理数据,可以提高处理效率。

Stream 提供了一种更简洁、更高效的方式对集合进行操作,也提供了更多的操作手段来满足各种数据处理的需求。通过流式操作,可以更容易地编写出简洁、清晰的代码。

3. 创建 Stream

下面就用Java代码演示一下从不同数据源创建 Stream 的示例:

1. 从集合创建 Stream:

List<String> list = Arrays.asList("apple", "banana", "orange");
Stream<String> streamFromList = list.stream();

2. 从数组创建 Stream:

String[] array = { "apple", "banana", "orange" };
Stream<String> streamFromArray = Arrays.stream(array);

3. 从指定值创建 Stream:

Stream<String> streamOfValues = Stream.of("apple", "banana", "orange");

4. 从文件创建 Stream(以文本文件为例):

Path filePath = Paths.get("c://file.txt");
try {
    Stream<String> streamFromFile = Files.lines(filePath);
} catch (IOException e) {
    e.printStackTrace();
}

5. 创建无限流(如生成一系列连续的整数):

Stream<Integer> infiniteStream = Stream.iterate(0, n -> n + 1);

4. Stream 的中间操作

使用 Stream可以通过中间操作对流中的元素进行处理和转换。以下是常见的几种中间操作方法及其作用、用法和示例代码:

filter 方法:保留符合条件的元素,丢弃不符合条件的元素。

List<String> fruits = Arrays.asList("apple", "banana", "orange", "pear", "grape");
Stream<String> filteredStream = fruits.stream().filter(fruit -> fruit.startsWith("a"));
// 过滤出以字母"a"开头的水果

map 方法:对流中的每个元素执行指定的映射函数,并将映射后的结果组成一个新的流。

List<String> fruits = Arrays.asList("apple", "banana", "orange", "pear", "grape");
Stream<Integer> lengthsStream = fruits.stream().map(String::length);
// 获取每个水果字符串的长度组成新的流

sorted 方法:用于对流中的元素进行排序。

List<String> fruits = Arrays.asList("apple", "banana", "orange", "pear", "grape");
Stream<String> sortedStream = fruits.stream().sorted(); // 自然排序
// 或者
Stream<String> sortedByLengthStream = fruits.stream().sorted(Comparator.comparingInt(String::length));
// 根据字符串长度排序

distinct 方法:用于去除流中重复的元素。

List<String> fruits = Arrays.asList("apple", "banana", "orange", "apple", "pear", "banana");
Stream<String> distinctStream = fruits.stream().distinct();
// 去除重复的水果元素

5. Stream 的终端操作

使用 Stream 可以通过终端操作方法触发流水线的执行,并产生最终结果。下面演示一下常见的几种终端操作方法及其作用、用法和示例代码:

forEach 方法:对流中的每个元素执行指定的操作。

List<String> fruits = Arrays.asList("apple", "banana", "orange", "pear", "grape");
fruits.stream().forEach(System.out::println);
// 打印每个水果元素

collect 方法:将流中的元素收集到一个集合或其他数据结构中。

List<String> fruits = Arrays.asList("apple", "banana", "orange", "pear", "grape");
List<String> collectedList = fruits.stream().collect(Collectors.toList());
// 将流中的元素收集到一个新的列表中

reduce 方法:将流中的元素反复结合起来,得到一个最终的结果值。

List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5);
int sum = numbers.stream().reduce(0, Integer::sum);
// 对流中的所有元素求和,初始值为0

count 方法:返回流中元素的数量。

List<String> fruits = Arrays.asList("apple", "banana", "orange", "pear", "grape");
long count = fruits.stream().count();
// 统计流中元素的数量

6. Stream 的性能优化

在使用 Java Stream 时,下面是常用的性能优化策略:

  • 避免过度使用中间操作:过多的中间操作可能会增加额外的计算开销。应该尽量合并中间操作,或者选择合适的时机执行终端操作,以减少不必要的中间步骤。

  • 及时使用并行流:对于大型数据集,使用并行流可能提升性能。但是在小型数据集或者简单的计算中,并行流可能会增加额外的线程管理开销,导致性能下降。因此,要谨慎使用并行流,根据实际情况选择是否使用。

  • 避免自动装箱和拆箱:Stream 操作中的基本类型(int、double、long 等)会被自动装箱成对应的包装类型,这会引入额外的性能开销。如果可能,尽量使用原始类型的流(IntStream、DoubleStream、LongStream)以避免自动装箱和拆箱的开销。

  • 考虑数据结构选择:在特定场景下,选择合适的数据结构来存储数据可能会影响到性能。例如,如果需要频繁的插入和删除操作,LinkedList 可能更合适;如果需要随机访问和搜索,ArrayList 可能更优。

  • 避免过度计算:Stream 提供了延迟计算的特性,即在终端操作执行之前,中间操作不会立即执行。但是,在某些情况下可能会产生不必要的计算。避免过度计算,根据需要使用 limit()、filter() 等限制数据集大小。

  • 使用基本方法:在一些情况下,使用原始的循环和操作可能比 Stream 更高效。尤其是在性能要求较高的场景下,原始的迭代和循环可能更适合。

在代码开发过程中,可以通过测试和性能分析来验证优化策略的有效性,根据实际情况进行调整。

7. 实例演示

平时项目中经常会常会遇到一些需求,比如构建菜单,构建树形结构,数据库一般就使用父id来表示,为了降低数据库的查询压力,我们可以使用Java8中的Stream流一次性把数据查出来,然后通过流式处理。

实体类:Menu.java

/**
 * Menu
 */
@Data
@Builder
public class Menu {
    /**
     * id
     */
     public Integer id;
     /**
     * 名称
     */
     public String name;
     /**
     * 父id ,根节点为0
     */
     public Integer parentId;
     /**
     * 子节点信息
     */
     public List<Menu> childList;


    public Menu(Integer id, String name, Integer parentId) {
        this.id = id;
        this.name = name;
        this.parentId = parentId;
    }
    
    public Menu(Integer id, String name, Integer parentId, List<Menu> childList) {
        this.id = id;
        this.name = name;
        this.parentId = parentId;
        this.childList = childList;
    }

}

递归组装树形结构:?

@Test
public void testtree(){
    //模拟从数据库查询出来
    List<Menu> menus = Arrays.asList(
            new Menu(1,"根节点",0),
            new Menu(2,"子节点1",1),
            new Menu(3,"子节点1.1",2),
            new Menu(4,"子节点1.2",2),
            new Menu(5,"根节点1.3",2),
            new Menu(6,"根节点2",1),
            new Menu(7,"根节点2.1",6),
            new Menu(8,"根节点2.2",6),
            new Menu(9,"根节点2.2.1",7),
            new Menu(10,"根节点2.2.2",7),
            new Menu(11,"根节点3",1),
            new Menu(12,"根节点3.1",11)
    );

    //获取父节点
    List<Menu> collect = menus.stream().filter(m -> m.getParentId() == 0).map(
            (m) -> {
                m.setChildList(getChildrens(m, menus));
                return m;
            }
    ).collect(Collectors.toList());
    System.out.println("-------转json输出结果-------");
    System.out.println(JSON.toJSON(collect));
}

/**
 * 递归查询子节点
 * @param root  根节点
 * @param all   所有节点
 * @return 根节点信息
 */
private List<Menu> getChildrens(Menu root, List<Menu> all) {
    List<Menu> children = all.stream().filter(m -> {
        return Objects.equals(m.getParentId(), root.getId());
    }).map(
            (m) -> {
                m.setChildList(getChildrens(m, all));
                return m;
            }
    ).collect(Collectors.toList());
    return children;
}

格式化打印结果:?

?

8. 注意事项

使用 Java Stream 时需要注意以下事项:

  • 空指针异常:在使用 Stream 时,如果流中的元素可能为 null,应该小心处理空指针异常。例如,在调用 map() 或 filter() 方法时,可能会返回 null 值,导致空指针异常。

  • 懒加载特性:Stream 具有延迟计算特性,中间操作不会立即执行。如果程序没有正确使用终端操作方法来触发计算,可能会导致流水线操作不执行,进而出现预期之外的结果。

  • 并行流的使用:虽然并行流可以提高性能,但是并不是所有场景都适合使用。如果数据量不大或者计算简单,使用并行流反而可能会带来额外的性能开销。应该根据实际情况进行评估和选择。

  • 状态ful 操作:避免在并行流中使用有状态的中间操作,这可能会引发竞争条件和不确定的结果。例如,在 forEachOrdered()、sorted() 等操作中使用状态。

  • 数据源共享:避免多个线程共享可变数据源。当流是从共享的可变数据源创建时,可能会引发线程安全问题。确保数据源是线程安全的或者在流创建时进行合适的同步。

  • 使用 limit() 操作:limit() 操作可能会限制数据集的大小,但要注意在无限流中使用 limit() 可能导致无法终止的流操作。

  • 使用findFirst() 和 findAny():在并行流中使用 findFirst() 和 findAny() 可能会得到不同的结果。在并行操作时,findAny() 可能更高效,但结果并不稳定。

  • 自动装箱拆箱:Stream 操作中的基本类型(int、double、long 等)会被自动装箱成对应的包装类型。频繁的自动装箱拆箱可能会引入性能问题,应该尽量避免。

9. 结语

正确使用 Stream 可以大大简化集合操作的代码量,提高代码的可读性和维护性。但需要注意合适的使用场景和方法,避免潜在的问题,以发挥 Stream 的最大优势。希望通过本文介绍能够让大家对Java Stream的用法更加熟悉,提高自己的工作效率,今天的内容就分享到这里啦。

文章来源:https://blog.csdn.net/EverythingAtOnce/article/details/135384108
本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。