• 博客园logo
  • 会员
  • 众包
  • 新闻
  • 博问
  • 闪存
  • 赞助商
  • HarmonyOS
  • Chat2DB
    • 搜索
      所有博客
    • 搜索
      当前博客
  • 写随笔 我的博客 短消息 简洁模式
    用户头像
    我的博客 我的园子 账号设置 会员中心 简洁模式 ... 退出登录
    注册 登录
思想人生从关注生活开始
博客园    首页    新随笔    联系   管理    订阅  订阅

Java 8 中的流 API 和收集器完整指南

让我们举个例子,下面将制作一个 Person 实例列表。

List<Person> persons = new ArrayList<>();

现在,假设我们要计算此列表中年龄超过 20 岁的人的平均年龄。

我们将如何进行?

  1. 映射步骤-map
  • 映射采用人员列表并返回整数列表。
  • 两个列表的大小相同。

        2.过滤步骤-filter

  • 获取我们的年龄列表,这是一个整数列表,并返回一个整数列表。
  • 如果我的筛选只是一个大于 20 的谓词指令,那么在返回列表中,所有年龄都大于 20。

       3. 归约步骤-reduce 

  • 我们只会说它等效于 SQL 聚合。什么是 SQL 聚合?例如,求元素的总和、最大值、平均值等类似的东西。
  • 这只是一个简单的函数,它将恢复我们示例中的所有整数。。

什么是流?

public interface Stream<T> extends BaseStream<T,Stream<T>> {
   // ....
}
  • 流是 Java 新引入的泛型接口。其类型为 T。这意味着我们可以有各种各样的流,如整数流、人员流、客户流、字符串流等。
  • 它提供了一种在 JVM 内部高效处理数据的方法。它可以有效地处理大量数据。
  • 它可以并行处理数据,以利用多个CPU的计算能力。
  • 该过程在管道中执行,因而它将避免不必要的中间计算。

Java 8 中流的定义

为什么集合不能是流?

  • 流是一个新概念,设计者不想改变集合 API 的工作方式。

什么是流?

  • 可以在其上定义操作的对象。通常操作,您可以想到映射、筛选或归约等操作。
  • 不保存任何数据的对象。
  • 不允许流更改其处理的数据的对象。
  • 能够在一次传递中处理数据的对象。
  • 对象应该从算法的角度进行优化,并且能够并行处理数据。
  • 只能使用一次

构建和使用流

我们如何构建流?

嗯,事实上,我们有很多模式来构建流。

让我们看看第一个,可能是最有用的一个。我们有一个流方法,它已被添加到集合接口中,因此调用person.stream()

将从个人列表实例中获得流对象。

List<Person> persons = new ArrayList<Person>
Stream<Person> streams = person.stream();
  • forEach 每个流都定义了此方法
  • 在流接口上定义的 forEach 方法,并向其传递使用者。
@FunctionalInterface
public interface Consumer<T> {
// ..
}
  • 让我们看一下该消费者接口。它是一个函数接口,所以它只有一个抽象方法。函数接口可以通过 lambda 表达式实现。
streams.forEach(p -> System.out.println(p));

它也可以写成方法引用,System.out::println

streams.forEach(System.out::println);
  • 事实上,消费者使用是有点复杂,以下是它的定义。
@FunctionalInterface
public interface Consumer<T> {
    void accept(T t);
    default Consumer<T> andThen(Consumer<? super T> after){
       Objects.requireNonNull(after);
       return (T t) -> accept(t); after.accept(t); };
    }
}
  • 它将使我们能够将消费者联系起来,以下代码将展现此功能。
List<String> result = new ArrayList<>();
List<Person> persons = ... ;
Consumer<String> c1 = result::add;
Consumer<String> c2 = System.out::println;
persons.stream().forEach(c1.andThen(c2));
//
  • 因为 forEach() 不返回任何内容。

2. 过滤流

  • 它采用在数据源上定义的流,并在谓词之后筛选出部分数据。
List<Person> persons = ... ;
Stream<Person> stream = persons.stream();
Stream<Person> filtered = stream.filter( person -> person.age() > 20 ); 
  • 将谓词作为参数,检查该人的年龄是否大于 20。
Predicate<Person> p = person -> person.age() > 20 ;
  • 这是一个常规的 lambda 表达式,person.age()>20。
  • 让我们看一下该谓词接口。它有一个称为 test 的方法,该方法将对象作为参数并返回布尔值。
@FunctionalInterface
public interface Predicate<T> {
  boolean test(T t);
  default Predicate<T> and(Predicate<? super T> other);
  static <T> Predicate<T> isEqual(Object targetRef);
  default Predicate<T> negate();
  default Predicate<T>or(Predicate<? super T> other);
}
  • 我们必须对这种编写方式要小心一点,因为编写布尔运算时通常操作的优先级在这里没有考虑在内。
Predicate<Integer> p1 = i -> i > 20;
Predicate<Integer> p2 = i -> i < 30;
Predicate<Integer> p3 = i -> i == 0;
Predicate<Integer> p = p1.and(p2).or(p3); // (p1 AND p2) OR p3
Predicate<Integer> p = p3.or(p1).and(p2); // (p3 OR p1) AND p2
 
 
  • 警告:在这种情况下调用方法不处理优先级。
  • 谓词接口中也有一个名为isEqual的静态方法。
Predicate<String> p = Predicate.isEqual("two");
  • isEqual方法做什么?它通过比较作为参数传递的对象来创建新的谓词。
Predicate<String> p = Predicate.isEqual("two") ;
Stream<String> stream1 = Stream.of("one", "two", "three") ;
Stream<String> stream2 = stream1.filter(p) ;
  • 让我们来看一下Stream.of(...)方法,它是一个静态方法,这也是在 Java 中创建流的另一种方式。它是一种在 Java 8 接口上新声明代码模式的方法,同时在接口中也允许编写静态方法。
  • 每次由 filter 方法产生的流都是新实例,因此 stream1 和 stream2 对象是不同的对象。

消费者、谓词和筛选流的示例

import java.util.function.Predicate;
import java.util.stream.Stream;
/**
 * @author Jack.Yang
 */
public class FirstPredicates {
    public static void main(String[] args) {
  
         Stream<String> stream = Stream.of("one", "two", "three", "four", "five");
Predicate<String> p1 = Predicate.isEqual("two");
Predicate<String> p2 = Predicate.isEqual("three");

List<String> list = new ArrayList<>();
Consumer<String> c1 = list::add;
Consumer<String> c2 = System.out::println;
stream
.peek(str->System.out.println("peek:"+str))// Intermediary Operation
.filter(p1.or(p2))
//.peek(list::add); // Intermediary Operation
.forEach(c1.andThen(c2)); // Terminal/Final Operation
System.out.println("Done!");
System.out.println("size = " + list.size());
    }
}

流上的延迟操作

Predicate<String> p = Predicate.isEqual("two") ;
Stream<String> stream1 = Stream.of("one", "two", "three") ;
Stream<String> stream2 = stream1.filter(p) ;

在这个新的流Stream2中,有什么?

  • 在流中没有任何对象。这是流的定义。流不能保存任何数据。
  • 此代码不执行任何操作,即对给定流的操作声明,但在此调用中不处理任何数据。
  • 对筛选器方法的调用称为延迟调用。这意味着,当我调用该方法时,实际上,它只是一个被考虑的声明,但没有处理任何数据。
  • 返回另一个流的所有 Stream 方法都是懒惰的。
  • 另一种说法是,对返回流的流的操作称为中间操作。
List<String> result = new ArrayList<>();
List<Person> persons = ... ;
persons.stream().peek(System.out::println).filter(person -> person.getAge() > 20).peek(result::add); 
  • 如果我们回到消费者身上,考虑另一种叫做peek的消费方法。
  • peek方法类似于 forEach 方法。唯一的区别是 peek 方法返回流,而 forEach 方法不返回任何内容。由于 peek 方法返回一个流,我们可以安全地假设它是一个中间操作。
  • 然后调用了过滤器方法。同样,它只是一个声明,最后一个调用是另一个也是声明的 peek 方法。
  • 所以答案是这段代码不做任何事情。首先,它不打印任何内容。System.out::println
    永远不会被调用,result列表也保持为空,因为 result::add 永远不会在该代码中被调用。

示例:中间和未端操作

import java.util.ArrayList;
import java.util.List;
import java.util.function.Predicate;
import java.util.stream.Stream;
/**
 *
 * @author Jack.Yang
 */
public class IntermediaryAndFinal {
    public static void main(String[] args) {
        Stream<String> stream = Stream.of("one", "two", "three", "four", "five");
        Predicate<String> p1 = Predicate.isEqual("two");
        Predicate<String> p2 = Predicate.isEqual("three");
        List<String> list = new ArrayList<>();
        stream.peek(System.out::println)// Intermediary Operation

.filter(p1.or(p2))// Intermediary Operation
        //.peek(list::add) // Intermediary Operation
        .forEach(list::add); // Terminal/Final Operation
        System.out.println("Done!");
        System.out.println("size = " + list.size());
    }
}

 

map操作

List<Person> list = ... ;
Stream<Person> stream = list.stream();
Stream<String> names =stream.map(person -> person.getName());
  • map 操作实现了我们在本文开头看到的 map/filter/reduce 算法的第一步。
  • 映射操作返回一个 Stream,因此我们可以安全地假设它是一个中间操作。
@FunctionalInterface
public interface Function<T, R> {
      R apply(T t);
}
  • 映射器函数由函数接口建模。事实上,它只执行一种称为 Apply 的方法。此方法将一个对象作为参数,并返回另一个对象。
  • 我们还有一组默认方法来链接和组合映射。
@FunctionalInterface
public interface Function<T, R> {
         R apply(T t);
         default <V> Function<V, R> compose(Function<? super V, ? extends T> before);
         default <V> Function<T, V> andThen(Function<? super R, ? extends V> after);
}
  • 也就是说,我们有两个默认方法,compose 和 andThen。
  • 这些是完整的签名。小心泛型!在设计此类方法时必须格外小心,例如,如果您希望通过 person 类的扩展来允许调用。
  • 而且,我还有一个称为 identity 的静态实用程序方法。identity有什么作用?嗯,这很明显。它接受一个对象并返回相同的对象。
@FunctionalInterface
public interface Function<T, R> {
       R apply(T t);
       // default methods
       static <T> Function<T, T> identity() {
             return t -> t;
       }
}

flatMap操作

  • flatMap操作有点难以理解。
  • 让我们看一下此方法的签名。
<R> Stream<R> flatMap(Function<T, Stream<R>> flatMapper);
<R> Stream<R> map(Function<T, R> mapper);
  • flatMap 将函数作为参数,与 map 方法的函数相同。
  • 如果仔细检查会看到map接受一个对象并返回另一个对象,而flatMap接受一个对象并作为返回类型返回一个对象流。
  • 因此,flatMapper 接受一个 T 类型的元素,返回一个 Stream 类型的元素。
  • 如果 flatMap 是一个常规映射,它将返回由提供的函数返回的那些流的 Stream,从而返回一个流流。
  • 但是,由于它是一个 flatMap,它返回一个平展的流,并成为单个流。
  • 这是什么意思?这意味着包含的流中的所有对象。

Map和FlatMap示例

import java.util.Arrays;
import java.util.Collection;
import java.util.List;
import java.util.function.Function;
import java.util.stream.Stream;
/**
 *
 * @author Jack.Yang
 */
public class FlatMapExample {
 
public static void main(String... args) {
        List<Integer> list1 = Arrays.asList(1, 2, 3, 4, 5, 6, 7);
        List<Integer> list2 = Arrays.asList(2, 4, 6);
        List<Integer> list3 = Arrays.asList(3, 5, 7);
        List<List<Integer>> list = Arrays.asList(list1, list2, list3);
        System.out.println(list);
        Function<List<?>, Integer> size = List::size;
        Function<List<Integer>, Stream<Integer>> flatmapper = 
                l -> l.stream();
    // list.stream()
    //          .map(size)
    //          .forEach(System.out::println);
        list.stream()
                .flatMap(flatmapper)
                .forEach(System.out::println);
    }
 
}

在流上使用Map和filter

我们看到了 3 类操作

  • 以消费者为参数的ForEach和peek
  • 接受一个谓词作为参数的filter方法,
  • flatMap方法中的map,它接受mapper作为参数。
  • mapper是函数式接口function的一个实例。

Reduction, Functions, and Bifunctions

  • 我们的map/filter/reduce算法的最后一步就是Reduce步骤。
  • Stream API包含两种归约操作。
  • 第一种是基本和经典的SQL操作,如最小值,最大值,总和,平均值等。
        List<Integer> ages = Arrays.asList(1,2,3,4) ;
        Stream<Integer> stream = ages.stream();
        Integer sum = stream.reduce(0, (age1, age2) -> age1 + age2);//0为初始值
System.out.println("sum:"+sum);//0+1+2+3+4

sum:10
 
  • 该归约采用两个整数 age1 和 age2,并返回它们的总和。
  • 第一个参数,这个 0 是归约操作的初始值。
  • 第二个参数是 BinaryOperator<T> 类型的归约操作。此处,T 是整数类型。事实上,BinaryOperator是BiFunction的一个特例。
@FunctionalInterface
public interface BiFunction<T, U, R> {
     R apply(T t, U u);
     // plus default methods
}
  • BiFunction 看起来像一个函数。它在这里接受两个类型 T 和 U 的对象,并返回一个类型 R 的对象。
@FunctionalInterface
public interface BinaryOperator<T> extends BiFunction<T, T, T> {
    // T apply(T t1, T t2);
    // plus static methods
}
 
  • BinaryOperator只是BiFunction的扩展,其中所有这三种类型实际上都是相同的。因此,BiFunction 将两个对象作为相同类型的参数,并返回一个相同类型的对象。

   在空集上进行归约:归约操作的第一个参数

  • bifunction 有两个参数,所以我们可以问两个问题。
  • 如果流为空会发生什么情况?以及,如果流只有一个元素会发生什么?
  • 如果流为空,则空流的归约结果就是第一个参数值
  • 如果流只有一个元素,则此流的归约结果就是该元素与已提供的第一参数的相结合。
Stream<Integer> stream = ...;
BinaryOperation<Integer> sum = (i1, i2) -> i1 + i2;
Integer id = 0; // identity element for the sum
int red = stream.reduce(id, sum);
  • 求和的单位元素(初始值)是 0,然后可以通过提供这个单位元素 0 和 sum 操作来归约流,这个过程用 lambda 表达式建模如上所示。
Stream<Integer> stream = Stream.empty();
int red = stream.reduce(id, sum);
System.out.println(red);
  • 让我们拿一个空流来举例,可以通过在流接口上调用静态方法 empty 来构建空流,我们可以运行它,最后该流的归约为 0,因为第一个元素也是,所以0+0还是为0。
Stream<Integer> stream = Stream.of(1);
int red = stream.reduce(id, sum);
System.out.println(red);
  • 让我们再看一个例子,通过调用stream接口的静态方法of,提供一个只有一个元素的流。在这里,我们可以看到这个流的归约结果是1。
Stream<Integer> stream = Stream.of(1, 2, 3, 4);
int red = stream.reduce(id, sum);
System.out.println(red);
  • 作为最后一个例子,让我们取一个包含几个整数的普通流,1,2,3,4。让我们reduce这个流。它打印了10,当然,这是正确的答案。

 Optionals

BinaryOperation<Integer> max = (i1, i2) ->
                                    i1 > i2 ? i1 : i2;
  • 问题是 max 操作没有用于归约(reduce)方法的标识元素(初始值)。
  • 因此,不能以这种方式定义空流的最大值。
List<Integer> ages = ... ;
Stream<Integer> stream = ages.stream();
... max = stream.max(Comparator.naturalOrder());
 
  • 那么,这个max方法的返回类型是什么?
  • 如果返回类型是 int,即 Java 语言中的原始类型,则默认值为 0,而 0 显然不是 max 方法的标识元素。
  • 如果返回类型为 Integer,则默认值为 null,在这种情况下,我当然不想返回 null 值,因为我将不得不在我的代码中检查返回值是否为 null 以避免空点异常。
List<Integer> ages = ... ;
Stream<Integer> stream = ages.stream();
Optional<Integer> max = stream.max(Comparator.naturalOrder());
  • 实际上,这个调用的返回类型是optional, optional的类型是Integer。
  • 什么是optional?这是Java 8中的一个新概念。它是一个类,我们可以将其视为包装类型。
  • 好吧,integer类型的optional看起来像一个包装类型,唯一的区别是,在包装类型中,我总是有一个值,而在optional中,我可能没有值。
  • 返回optional意味着可能没有结果,这正是我在这里想要的意思,因为如果我取空流的最大值,我不知道最大值是多少。

可选项的模式

Optional<String> opt = ... ;
if (opt.isPresent()) {
     String s = opt.get() ;
} else {
     ...
}
  • 如果optional中有一个值,isPresent 方法将返回 true,如果不是这种情况,则返回 false。而且,如果有一个值,可以通过在这个optional对象上调用 get 方法来获取它。
String s = opt.orElse("") ; // defines a default value
  • orElse 方法封装了这两个调用。实际上,它只是调用isPresent,如果有对象,它将为我调用get方法。但是,如果我愿意,我也可以决定抛出异常。
 
String s = opt.orElseThrow(MyException::new) ; // lazy construct.
  • 此 orElseThrow 方法将生成一个新异常。我只是提供一个 lambda 表达式,它将以懒惰的方式为我创建该异常。这是例外。我们只会在需要时构建,此方法将返回字符串(如果存在),如果不存在,则会抛出此异常。

归约操作

  • 可用归约限制操作 — max(),min() 和 count() 将返回流中的元素数。
  • Boolean归约——allMatch()、noneMatch()、anyMatch()。这三个方法都将谓词作为参数,如果谓词对流中的所有元素都返回true,那么allMatch方法将返回true。
  • 返回optional对象归约(最大值和平均值除外)。findFirst() 和 findAny() 就是这样的方法。
  • 归约类操作称为终端操作。
  • 它们触发数据处理。

示例:Reductions, Optionals

import java.util.Arrays;
import java.util.List;
import java.util.Optional;
/**
 *
 * @author Jack.Yang
 */
public class ReductionExample {
 
    public static void main(String... args) {
        List<Integer> list = Arrays.asList();
        Optional<Integer> red = list.stream().reduce(Integer::max);
        System.out.println("red = " + red);
    }
}

包装Operations 与Optionals

  • 归约只是一个经典的 SQL 操作。
  • 中间操作和终端操作之间的区别,即中间只添加流的操作声明,在终端操作时触发流上的计算。
  • Optional返回类型是有必要存在的,因为默认值不能始终在归约步骤上定义。

收集器, 在字符串、列表中收集展示

  • 现在让我们看看第二种归约。如果您查看 Java 文档,您将看到这种归约称为可变归约。
  • 为什么?因为我们要归约可变容器中的流,因为我们要在该容器中添加流的所有元素。
 
List<Person> persons = ... ;
String result = persons.stream()
     .filter(person -> person.getAge() > 20)
     .map(Person::getLastName)
     .collect(Collectors.joining(","));
  • 这种收集方法需要收集者。使用字符串作为参数连接。
  • 因此,由于字符串,我们看到人员列表中超过 20 岁的人员的姓名,用逗号分隔。
List<Person> persons = ... ;
List<String> result =.  persons.stream()
       .filter(person -> person.getAge() > 20)
       .map(Person::getLastName)
       .collect(Collectors.toList());
  • 我们也可以在列表中收集。这基本上与我们可以对流进行的处理相同,但是这一次,我们不是收集字符串中的所有名称,而是将它们收集在一个列表中。

在Map中Collect

 
List<Person> persons = ... ;
Map<Integer, List<Person>> result = persons.stream()
    .filter(person -> person.getAge() > 20)
    .collect(Collectors.groupingBy(Person::getAge));
  • 我们把这条流建立在人身上。我们过滤掉所有20岁以下的人,并在地图上收集他们。现在,这张地图是由什么组成的?好吧,我们只是通过Person::getAge。再一次,这是一个方法引用。
List<Person> persons = ... ;
Map<Integer, Long> result = persons.stream()
      .filter(person -> person.getAge() > 20)
      .collect(Collectors.groupingBy(Person::getAge,
             Collectors.counting() // the downstream collector
             ));
  • 可以对值进行后处理。

示例:处理流

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.TreeSet;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import org.paumard.stream.model.Person;
/**
 *
 * @author Jack.Yang
 */
public class CollectorsExample {
    public static void main(String... args)  {
        List<Person> persons = new ArrayList<>();
        try (
                BufferedReader reader =
                        new BufferedReader(
                                new InputStreamReader(
                                        CollectorsExample.class.getResourceAsStream("people.txt")));
                Stream<String> stream = reader.lines();
        ) {
            stream.map(line -> {
                String[] s = line.split(" ");
                Person p = new Person(s[0].trim(), Integer.parseInt(s[1]));
                persons.add(p);
                return p;
            })
                    .forEach(System.out::println);
        } catch (IOException ioe) {
            System.out.println(ioe);
        }
        Optional<Person> opt =
                persons.stream().filter(p -> p.getAge() >= 20)
                        .min(Comparator.comparing(Person::getAge));
        System.out.println(opt);
        Optional<Person> opt2 =
                persons.stream().max(Comparator.comparing(Person::getAge));
        System.out.println(opt2);
        Map<Integer, String> map =
                persons.stream()
                        .collect(
                                Collectors.groupingBy(
                                        Person::getAge,
                                        Collectors.mapping(
                                                Person::getName,
                                                Collectors.joining(", ")
                                        )
                                )
                        );
        System.out.println(map);
    }
} 

总结

  • 我们对map/filter/reduce算法有一个快速的解释,再一次,这个算法不是Java平台的典型算法。这是一种通用算法。
  • 然后我们定义了什么是流。我们看到了几种构建流的模式。
  • 我们看到了中间操作和最终操作之间的区别——第一个是懒惰的,第二个是触发数据处理的。
  • 我们看到了几个消耗性操作 — forEach 操作(最终操作)和 peek 操作(中介操作)。
  • 我们看到了两个映射操作 — 首先是 map(),然后是 flatMap()。
  • 我们看到了使用过滤器方法的过滤器操作。
  • 我们看到了减少步骤,以及减少操作。我们有两种归约运算 — 第一种是 SQL 聚合,最大值、最小值、总和、计数等,第二种是可变归约。

可变缩减是一个非常强大的工具,具有收集方法、收集器接口和收集器类。它使我们能够非常快速地构建一个复杂的结构来减少我们流的元素。

result 
  • 接受一个谓词作为参数的filter方法

posted @ 2015-10-28 23:46  JackYang  阅读(2516)  评论(0)    收藏  举报
刷新页面返回顶部
博客园  ©  2004-2025
浙公网安备 33010602011771号 浙ICP备2021040463号-3