Java8 教程

Java8 终端和非终端操作

Java8 流接口有终端操作和非终端操作可供选择。非终端流操作是向流中添加监听器而不做任何其他操作的操作。终端流操作是一种启动元素内部迭代、调用所有监听器并返回结果的操作。

下面是一个 Java 流示例,其中包含一个非终端操作和一个终端操作:

package com.hxstrive.jdk8.stream_api;

import java.util.ArrayList;
import java.util.List;
import java.util.stream.Stream;

/**
 * 终端操作和非终端操作
 * @author hxstrive.com
 */
public class StreamExamples {

    public static void main(String[] args) {
        List<String> stringList = new ArrayList<String>();
        stringList.add("ONE");
        stringList.add("TWO");
        stringList.add("THREE");

        Stream<String> stream = stringList.stream();
        long count = stream
                // 非终端操作
                .map((value) -> {
                    return value.toLowerCase();
                })
                // 终端操作
                .count();
        System.out.println("count = " + count);
        //结果:count = 3
    }

}

上述示例,调用流接口的 map() 方法是一种非终端操作,它只是在流上设置一个 lambda 表达式,将每个元素转换为小写。而对 count() 方法的调用是一个终端操作,该调用会启动内部迭代,将每个元素转换为小写,然后进行计数。

非终端操作

Java Stream API 的非终端流操作是对流中的元素进行转换或过滤的操作。向流添加非终端操作时,会得到一个新流作为结果。新数据流代表原始数据流在应用非终端操作后产生的元素流。下面是一个向数据流添加非终端操作的示例,操作后会产生一个新的数据流:

package com.hxstrive.jdk8.stream_api;

import java.util.ArrayList;
import java.util.List;
import java.util.stream.Stream;

/**
 * 非终端操作
 * @author hxstrive.com
 */
public class StreamExamples2 {

    public static void main(String[] args) {
        List<String> stringList = new ArrayList<String>();
        stringList.add("ONE");
        stringList.add("TWO");
        stringList.add("THREE");

        Stream<String> stream = stringList.stream();
        // 在流上添加 map 非终端操作
        // 注意:非终端操作不会执行,只有等到终端操作的时候才会执行
        Stream<String> newStream = stream.map((value) -> {
            return value.toLowerCase();
        });
        System.out.println("stream: " + stream);
        System.out.println("newStream: "+ newStream);
        //输出:
        //stream: java.util.stream.ReferencePipeline$Head@4eec7777
        //newStream: java.util.stream.ReferencePipeline$3@3b07d329
    }

}

注意:对流 map() 的调用实际上会返回一个新的 Stream 实例,代表应用了 map 操作的原始字符串流。

您只能向给定的 Stream 实例添加一个操作。如果需要在多个操作之后进行链式操作,则需要将第二个操作应用到第一个操作产生的新 Stream 操作中。如下所示:

List<String> stringList = new ArrayList<String>();
stringList.add("ONE");
stringList.add("TWO");
stringList.add("THREE");

Stream<String> stream = stringList.stream();
// 在流上添加 map 非终端操作
// 注意:非终端操作不会执行,只有等到终端操作的时候才会执行
Stream<String> newStream = stream.map((value) -> {
    return value.toLowerCase(); // 转为小写
});

Stream<String> newStream2 = newStream.map(value -> {
    return value.toUpperCase(); // 转为大写
});
System.out.println("stream: " + stream);
System.out.println("newStream: " + newStream);
System.out.println("newStream2: " + newStream2);
//输出:
//stream: java.util.stream.ReferencePipeline$Head@3b07d329
//newStream: java.util.stream.ReferencePipeline$3@41629346
//newStream2: java.util.stream.ReferencePipeline$3@404b9385

请注意,对 Stream map() 的第二次调用是在第一次 map() 调用所返回的 Stream 上进行的。

在 Java 流上链式调用非终端操作是很常见的。下面是一个在 Java 流上链式调用非终端操作的示例:

List<String> stringList = new ArrayList<String>();
stringList.add("ONE");
stringList.add("TWO");
stringList.add("THREE");

Stream<String> stream = stringList.stream();
// 在流上添加 map 非终端操作
// 注意:非终端操作不会执行,只有等到终端操作的时候才会执行
Stream<String> newStream = stream.map((value) -> {
    return value.toLowerCase(); // 转为小写
}).map(value -> {
    return value.toUpperCase(); // 转为大写
});

许多非终端流操作都可以将 Java lambda 表达式作为参数。这个 lambda 表达式实现了与给定的非终端操作相匹配的 Java 函数接口。例如,Function 或 Predicate 接口。非终端操作方法的参数通常是一个函数式接口 —— 这就是为什么它也可以由 Java lambda 表达式来实现。

filter()

Java 流 filter() 可用于过滤 Java 流中的元素。filter 方法接收一个 Predicate 内置的函数式接口,该 Predicate 会针对流中的每个元素进行调用。如果该元素要包含在生成的流中,则 Predicate 应返回 true。如果不包含该元素,则 Predicate 应返回 false。

下面是一个调用 Java Stream filter() 方法的示例:

List<String> stringList = new ArrayList<String>();
stringList.add("ONE");
stringList.add("TWO");
stringList.add("THREE");

// 匿名函数写法
Object[] array1 = stringList.stream().filter(new Predicate<String>() {
    @Override
    public boolean test(String s) {
        return s.startsWith("T"); // 仅显示 T 开头的字符串
    }
}).toArray();
System.out.println("array1=" + Arrays.toString(array1)); // array1=[TWO, THREE]

// lambda 写法
Object[] array2 = stringList.stream().filter(s -> {
    return s.startsWith("T");
}).toArray();
System.out.println("array2=" + Arrays.toString(array2)); // array2=[TWO, THREE]

map()

Java 流 map() 方法将一个元素转换(映射)为另一个对象。例如,如果您有一个字符串列表,它可以将每个字符串转换为小写、大写、原始字符串的子串或其他完全不同的字符串。下面是一个 Java 流 map() 示例:

List<String> stringList = new ArrayList<String>();
stringList.add("ONE");
stringList.add("TWO");
stringList.add("THREE");

// 匿名函数写法
Object[] array1 = stringList.stream().map(new Function<String, String>() {
    @Override
    public String apply(String s) {
        return s.toLowerCase(); // 转换成小写
    }
}).toArray();
System.out.println("array1=" + Arrays.toString(array1)); // array1=[one, two, three]

// lambda 写法
Object[] array2 = stringList.stream().map(s -> {
    return s.toLowerCase();
}).toArray();
System.out.println("array2=" + Arrays.toString(array2)); // array2=[one, two, three]

flatMap()

Java 流 flatMap() 方法可将单个元素映射为多个元素。其原理是将每个元素从由多个内部元素组成的复杂结构 "扁平化(flatten)" 为仅由这些内部元素组成的 "扁平(flat)" 流。

例如,假设你有一个包含嵌套对象(子对象)的对象。然后,你可以将该对象映射到一个 "扁平" 流中,该流由对象本身及其嵌套对象组成,或者仅由嵌套对象组成。你也可以将元素列表流映射到元素本身。或者将字符串流映射为这些字符串中的单词流,或者映射为这些字符串中的单个字符实例。

下面是一个将字符串列表平面映射到每个字符串中单词的示例。通过这个示例,您可以了解如何使用 flatMap() 将单个元素映射为多个元素。

List<String> stringList = new ArrayList<String>();
stringList.add("ONE one");
stringList.add("TWO two");
stringList.add("THREE three");

Stream<String> stream = stringList.stream()
        // 将单个元素转换成一个 Stream
        .flatMap((value) -> {
            String[] split = value.split(" ");
            // 返回新的 Stream<String>
            return (Stream<String>) Arrays.asList(split).stream();
        });

// 迭代流
stream.forEach((value) -> {
    System.out.println(value);
});
//输出:
//ONE
//one
//TWO
//two
//THREE
//three

这个 Java 流 flatMap() 示例首先创建了一个包含 3 个字符串的 List。然后获取 List 的流,并调用 flatMap()。

在 Stream 上调用的 flatMap() 操作必须返回代表平面映射元素的另一个 Stream。在上面的示例中,每个原始字符串都被拆分成单词,变成一个 List,然后从该 List 获取并返回流。

请注意,这个示例最后调用了 forEach(),这是一个终端操作。该调用只是为了触发内部迭代,从而触发平面映射操作。如果没有在 Stream 链上调用终端操作,就不会发生任何事情。实际上不会进行平面映射。

distinct()

Java Stream distinct() 方法是一种非终端操作,它返回一个新的 Stream,其中只包含与原始 Stream 不同的元素。任何重复的元素都将被消除。下面是 Java Stream distinct() 方法的示例:

List<String> stringList = new ArrayList<String>();
stringList.add("one");
stringList.add("two");
stringList.add("three");
stringList.add("one");

List<String> distinctStrings = stringList.stream()
        .distinct() // 去重复元素
        .collect(Collectors.toList());
System.out.println(distinctStrings); // [one, two, three]

在本例中,元素 one 在原始数据流中出现了 2 次。distinct() 函数返回的流中只包含第一次出现的元素。因此,调用 collect() 得到的 List 只包含 one、two 和 three。

limit()

Java 流 limit() 方法可将流中元素的数量限制在作为参数给 limit() 方法的数字范围内。limit() 方法会返回一个新的数据流,其中最多包含给定数量的元素。下面是一个 Java 流 limit() 示例:

List<String> stringList = new ArrayList<String>();
stringList.add("one");
stringList.add("two");
stringList.add("three");

stringList.stream().limit(2) // 限制返回前2个元素
    .forEach( e -> {
        System.out.println(e);
    });
//输出:
//one
//two

这个示例首先创建了一个流,然后调用 limit(),然后用一个 lambda 调用 forEach(),打印出流中的元素。由于调用了 limit(2),所以只打印了前两个元素。

peek()

Java 流 peek() 方法是一种非终端操作,它将 Consumer(java.util.function.Consumer) 作为参数。流中的每个元素都会调用 Consumer。peek() 方法会返回一个新的流,其中包含原始流中的所有元素。

正如该方法所说,peek() 方法的目的是窥视流中的元素,而不是转换它们。请记住,peek 方法不会启动流中元素的内部迭代。为此,您需要调用终端操作。下面是一个 Java 流 peek() 示例:

List<String> stringList = new ArrayList<String>();
stringList.add("one");
stringList.add("two");
stringList.add("three");

// 匿名内部类方式
stringList.stream().peek(new Consumer<String>() {
    @Override
    public void accept(String s) {
        System.out.println("消费:"+s);
    }
}).toArray();
//输出:
//消费:one
//消费:two
//消费:three

// lambda 表达式方式
stringList.stream().peek(s -> System.out.println("消费:"+s)).toArray();
//输出:
//消费:one
//消费:two
//消费:three

终端操作

Java Stream 接口的终端操作通常只返回一个值。一旦在流上调用了终端操作,流和任何链流的迭代就会开始。一旦迭代完成,就会返回终端操作的结果。

终端操作通常不会返回新的流实例。因此,一旦在流上调用终端操作,非终端操作的流实例链就会结束。下面是一个在 Java 流上调用终端操作的示例:

List<String> stringList = new ArrayList<String>();
stringList.add("one");
stringList.add("two");
stringList.add("three");

long count = stringList.stream()
        // 非终端操作
        .map((value) -> { return value.toLowerCase(); })
        .map((value) -> { return value.toUpperCase(); })
        .map((value) -> { return value.substring(0,3); })
        // 终端操作
        .count();
System.out.println("count: "+ count); // count: 3

示例结尾处对 count() 的调用才是终端操作。由于 count() 返回一个 long,所以非终端操作(调用 map())的流链结束了。

anyMatch()

Java 流 anyMatch() 方法是一种终端操作,它将单个 Predicate 作为参数,启动流的内部迭代,并将 Predicate 参数应用于每个元素。如果 Predicate 对任何元素都返回 true,则 anyMatch() 方法返回 true。如果没有元素与 Predicate 匹配,anyMatch() 方法将返回 false。下面是一个 Java 流 anyMatch() 示例:

List<String> stringList = new ArrayList<String>();
stringList.add("One flew over the cuckoo's nest");
stringList.add("To kill a muckingbird");
stringList.add("Gone with the wind");

//
boolean anyMatch = stringList.stream().anyMatch((value) -> {
    return value.startsWith("One");
});
System.out.println(anyMatch); // true

在上例中,anyMatch() 方法调用将返回 true,因为数据流中的第一个字符串元素以 "One" 开头。

allMatch()

Java Stream allMatch() 方法是一种终端操作,它将单个Predicate 作为参数,开始对 Stream 中的元素进行内部迭代,并将Predicate 参数应用于每个元素。如果Predicate 对流中的所有元素都返回 true,则 allMatch() 将返回 true。如果不是所有元素都与 Predicate 匹配,则 allMatch() 方法返回 false。下面是一个 Java Stream allMatch() 示例:

List<String> stringList = new ArrayList<String>();
stringList.add("One flew over the cuckoo's nest");
stringList.add("To kill a muckingbird");
stringList.add("Gone with the wind");

// 判断 stringList 的元素是否全部以 One 字符串开头
boolean allMatch = stringList.stream().allMatch((value) -> {
    return value.startsWith("One");
});
System.out.println(allMatch); // false

在上面的示例中,allMatch() 方法将返回 false,因为流中只有一个字符串以 "One" 开头。

noneMatch()

Java 流 noneMatch() 方法是一种终端操作,它将遍历流中的元素,并根据流中是否没有元素与作为参数传递给 noneMatch() 的 Predicate 匹配,返回 true 或 false。如果没有元素与 Predicate 匹配,noneMatch() 方法将返回 true;如果有一个或多个元素匹配,noneMatch() 方法将返回 false。下面是一个 Java 流 noneMatch() 示例:

List<String> stringList = new ArrayList<String>();
stringList.add("One flew over the cuckoo's nest");
stringList.add("To kill a muckingbird");
stringList.add("Gone with the wind");

// 判断 stringList 的元素是否全部不以 Two 字符串开头
boolean allMatch = stringList.stream().noneMatch((value) -> {
    return value.startsWith("Two");
});
System.out.println(allMatch); // true

collect()

Java Stream collect() 方法是一种终端操作,用于启动元素的内部迭代,并将流中的元素收集到某个集合或对象中。下面是一个简单的 Java 流 collect() 方法示例:

List<String> stringList = new ArrayList<String>();
stringList.add("One flew over the cuckoo's nest");
stringList.add("To kill a muckingbird");
stringList.add("Gone with the wind");

// 将 stringList 中的元素转换为大写,然后通过 Collectors.toList() 收集到一个新的列表中
List<String> stringsAsUppercaseList = stringList.stream()
        .map(value -> value.toUpperCase())
        .collect(Collectors.toList());
for(String str : stringsAsUppercaseList) {
    System.out.println(str);
}
//输出:
//ONE FLEW OVER THE CUCKOO'S NEST
//TO KILL A MUCKINGBIRD
//GONE WITH THE WIND

collect() 方法的参数是一个 Collector(java.util.stream.Collector)。实现 Collector 需要对 Collector 接口进行一些研究。幸运的是,Java 类 java.util.stream.Collectors 包含了一组预实现的 Collector 实现,你可以用它们来实现最常见的操作。在上面的示例中,使用的就是 Collectors.toList() 返回的 Collector 实现。该 Collector 简单地将流中的所有元素收集到一个标准的 Java List。

count()

Java Stream count() 方法是一种终端操作,它启动 Stream 中元素的内部迭代,并对元素进行计数。下面是一个 Java Stream count() 示例:

List<String> stringList = new ArrayList<String>();
stringList.add("One flew over the cuckoo's nest");
stringList.add("To kill a muckingbird");
stringList.add("Gone with the wind");

// 统计个数
long count = stringList.stream().flatMap((value) -> {
            String[] split = value.split(" ");
            return (Stream<String>) Arrays.asList(split).stream();
        })
        .count();
System.out.println("count = " + count); // count = 14

此示例首先创建了一个字符串列表,然后获取了该列表的流,为其添加了一个 flatMap() 操作,最后调用了 count()。count() 方法将开始迭代 Stream 中的元素,这将导致在 flatMap() 操作中将字符串元素分割成字,然后进行计数。最后打印出的结果是 14。

findAny()

Java Stream findAny() 方法可以从 Stream 中查找单个元素。找到的元素可以来自流中的任何地方。但并不保证元素来自流中的任何位置。下面是一个 Java Stream findAny() 示例:

List<String> stringList = new ArrayList<String>();

stringList.add("one");
stringList.add("two");
stringList.add("three");
stringList.add("one");

Stream<String> stream = stringList.stream();

Optional<String> anyElement = stream.findAny();

System.out.println(anyElement.get());

请注意 findAny() 方法是如何返回一个 Optional 的。流可能是空的,因此不会返回任何元素。你可以通过 Optional isPresent() 方法检查是否找到了元素。

findFirst()

Java Stream findFirst() 方法可查找 Stream 中的第一个元素(如果 Stream 中存在任何元素)。如果存在元素,findFirst() 方法会返回一个 Optional,您可以从中获取该元素。下面是一个 Java Stream findFirst() 示例:

List<String> stringList = new ArrayList<String>();
stringList.add("one");
stringList.add("two");
stringList.add("three");
stringList.add("one");

// 找出第一个元素
Optional<String> result = stringList.stream().findFirst();
result.ifPresent(e -> {
    System.out.println(e);
});
//输出:
//one

您可以通过其 isPresent() 方法检查返回的可选项是否包含元素。

forEach()

Java Stream forEach() 方法是一个终端操作,它开始对 Stream 中的元素进行内部迭代,并对 Stream 中的每个元素应用一个 Consumer(java.util.function.Consumer)。forEach() 方法返回 void。下面是一个 Java Stream forEach() 示例:

List<String> stringList = new ArrayList<String>();
stringList.add("one");
stringList.add("two");
stringList.add("three");
stringList.add("one");

stringList.stream().forEach( element -> {
    System.out.println(element);
});
//输出:
//one
//two
//three
//one

min()

Java 流 min() 方法是一种终端操作,可返回流中最小的元素。哪个元素最小由您传递给 min() 方法的比较器实现决定。我在关于 Java 集合排序的教程中解释了比较器接口的工作原理。下面是一个 Java 流 min() 示例:

List<String> stringList = new ArrayList<String>();
stringList.add("abc");
stringList.add("def");

// 找出最小的字符串
Optional<String> min = stringList.stream().min((val1, val2) -> {
    return val1.compareTo(val2);
});
String minString = min.get();
System.out.println(minString);
//输出:
//abc

请注意 min() 方法如何返回一个 Optional ,其中可能包含也可能不包含结果。如果流为空,则 Optional get() 方法将抛出 NoSuchElementException 异常。

max()

Java 流 max() 方法是一种终端操作,可返回流中最大的元素。哪个元素最大由您传递给 max() 方法的比较器实现决定。我在关于 Java 集合排序的教程中解释了比较器接口的工作原理。下面是一个 Java Stream max() 示例:

List<String> stringList = new ArrayList<String>();
stringList.add("abc");
stringList.add("def");

// 找出最大的字符串
Optional<String> min = stringList.stream().max((val1, val2) -> {
    return val1.compareTo(val2);
});
String minString = min.get();
System.out.println(minString);
//输出:
//def

请注意 max() 方法如何返回一个 Optional,其中可能包含也可能不包含结果。如果流为空,则 Optional get() 方法将抛出 NoSuchElementException 异常。

reduce()

Java Stream reduce() 方法是一种终端操作,可将流中的所有元素还原为单个元素。下面是一个 Java Stream reduce() 示例:

List<String> stringList = new ArrayList<String>();
stringList.add("One flew over the cuckoo's nest");
stringList.add("To kill a muckingbird");
stringList.add("Gone with the wind");

// 将 stringList 列表中的元素连接成一个长字符串
Optional<String> reduced = stringList.stream()
        .reduce((value, combinedValue) -> {
            return combinedValue + " + " + value;
        });
System.out.println(reduced.get());
//输出:
//Gone with the wind + To kill a muckingbird + One flew over the cuckoo's nest

请注意 reduce() 方法返回的 Optional。该 Optional 包含传递给 reduce() 方法的 lambda 表达式返回的值(如果有的话)。你可以通过调用 Optional get() 方法获得该值。

toArray()

Java Stream toArray() 方法是一个终端操作,它启动流中元素的内部迭代,并返回一个包含所有元素的对象数组。下面是一个 Java Stream toArray() 示例:

List<String> stringList = new ArrayList<String>();
stringList.add("One flew over the cuckoo's nest");
stringList.add("To kill a muckingbird");
stringList.add("Gone with the wind");

//将 stringList 列表转换成数组
Object[] objects = stringList.stream().toArray();
System.out.println(Arrays.toString(objects));
//输出:
//[One flew over the cuckoo's nest, To kill a muckingbird, Gone with the wind]


说说我的看法
全部评论(
没有评论
关于
本网站专注于 Java、数据库(MySQL、Oracle)、Linux、软件架构及大数据等多领域技术知识分享。涵盖丰富的原创与精选技术文章,助力技术传播与交流。无论是技术新手渴望入门,还是资深开发者寻求进阶,这里都能为您提供深度见解与实用经验,让复杂编码变得轻松易懂,携手共赴技术提升新高度。如有侵权,请来信告知:hxstrive@outlook.com
公众号