Java Stream介绍和实战

目录

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 list = Arrays.asList("apple", "banana", "orange");
Stream streamFromList = list.stream();

2. 从数组创建 Stream:

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

3. 从指定值创建 Stream:

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

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

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

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

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

4. Stream 的中间操作

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

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

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

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

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

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

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

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

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

5. Stream 的终端操作

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

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

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

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

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

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

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

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

List 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 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 childList) {
        this.id = id;
        this.name = name;
        this.parentId = parentId;
        this.childList = childList;
    }

}

递归组装树形结构: 

@Test
public void testtree(){
    //模拟从数据库查询出来
    List 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 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 getChildrens(Menu root, List all) {
    List 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;
}

格式化打印结果: 

 Java Stream介绍和实战_第1张图片

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的用法更加熟悉,提高自己的工作效率,今天的内容就分享到这里啦。

你可能感兴趣的:(java,开发语言)