Java Stream总结

393 阅读7分钟

Docs

image.png

Stream操作流程

filter().jpeg

  • 中间操作 一个流可以后面跟随零个或多个中间操作。其目的主要是打开流,做出某种程度的数据映射/过滤,然后返回一个新的流,交给下一个操作使用。这类操作都是惰性化的,仅仅调用到这类方法,并没有真正开始流的遍历,真正的遍历需等到终端操作时,常见的中间操作有下面即将介绍的filtermap

  • 终端操作 一个流有且只能有一个终端操作,当这个操作执行后,流就被关闭了,无法再被操作,因此一个流只能被遍历一次。 image.png

Eemployee Class

@Data
@AllArgsConstructor
@NoArgsConstructor
@Builder
public class Employee {
    private Integer id;
    private String name;
    private Double salary;
    private void salaryIncrement(Double money){
        this.salary += money;
    }
}

流的创建

已有数组创建流

private static Employee[] arrayOfEmps = {
    new Employee(1, "TianLe Zhou", 100000.0), 
    new Employee(2, "San Zhang", 200000.0), 
    new Employee(3, "Si Li", 300000.0)
};

Stream.of(arrayOfEmps);

已有容器创建流

private static List<Employee> empList = Arrays.asList(arrayOfEmps);
empList.stream();

Stream.builder()创建流

Stream.Builder<Employee> empStreamBuilder = Stream.builder();

empStreamBuilder.accept(arrayOfEmps[0]);
empStreamBuilder.accept(arrayOfEmps[1]);
empStreamBuilder.accept(arrayOfEmps[2]);

Stream<Employee> empStream = empStreamBuilder.build();

流的常用操作

foreach方法

forEach() 是最简单最常用的操作,它遍历流元素,在每个元素上调用提供的函数

@Test
public void whenIncrementSalaryForEachEmployee_thenApplyNewSalary() {    
    empList.stream().forEach(e -> e.salaryIncrement(10000.0));
    //为每一位员工增加10000元的工资
    assertThat(empList, contains(
      hasProperty("salary", equalTo(110000.0)),
      hasProperty("salary", equalTo(220000.0)),
      hasProperty("salary", equalTo(330000.0))
    ));
}

map方法

map().jpeg
map()会接受一个函数作为参数,这个函数会被应用到每个元素上,并将其映射成一个新的元素。就是根据指定函数获取流中的每个元素的数据并重新组合成一个新的元素

@Test
public void whenMapIdToEmployees_thenGetEmployeeStream() {
    Integer[] empIds = { 1, 2, 3 };
    
    //根据empIds查询雇员并返回list集合
    List<Employee> employees = Stream.of(empIds)
      .map(employeeRepository::findById)
      .collect(Collectors.toList());
    
    assertEquals(employees.size(), empIds.length);
}

collect方法

collect().svg
collect()主要用于收集流中的集合,组成所需要对象或集合

@Test
public void whenCollectStreamToList_thenGetList() {
    //返回雇员的list集合
    List<Employee> employees = empList.stream().collect(Collectors.toList());
    
    assertEquals(empList, employees);
}

filter方法

filter().jpeg
通过使用filter方法进行条件筛选,filter的方法参数为一个条件
filter()会接受一个谓词(一个返回boolean的函数)作为参数,并返回一个包括所有符合谓词条件的元素的流

@Test
public void whenFilterEmployees_thenGetFilteredStream() {
    Integer[] empIds = { 1, 2, 3, 4 };
    
    //找出工资大于200000的雇员
    List<Employee> employees = Stream.of(empIds)
      .map(employeeRepository::findById)
      .filter(e -> e != null)
      .filter(e -> e.getSalary() > 200000)
      .collect(Collectors.toList());
    
    assertEquals(Arrays.asList(arrayOfEmps[2]), employees);
}

findFirst方法

findFirst.png
返回流中的首个元素

@Test
public void whenFindFirst_thenGetFirstEmployeeInStream() {
    Integer[] empIds = { 1, 2, 3, 4 };
    //找到工资大于100000的第一位雇员
    Employee employee = Stream.of(empIds)
      .map(employeeRepository::findById)
      .filter(e -> e != null)
      .filter(e -> e.getSalary() > 100000)
      .findFirst()
      .orElse(null);
    
    assertEquals(employee.getSalary(), new Double(200000));
}

flatMap方法

flatMap()一般用于将流的结果进行合并

image.png

@Test
public void whenFlatMapEmployeeNames_thenGetNameStream() {
    List<List<String>> namesNested = Arrays.asList( 
      Arrays.asList("TianLe", "Zhou"), 
      Arrays.asList("San", "Zhang"), 
      Arrays.asList("Si", "Li"));
    //将namesNested分解为[TianLe, Zhou, San, Zhang, Si, Li]
    List<String> namesFlatStream = namesNested.stream()
      .flatMap(Collection::stream)
      .collect(Collectors.toList());

    assertEquals(namesFlatStream.size(), namesNested.size() * 2);
}

peek方法

peek.jpeg
peek()就是在流的每个元素恢复运行之前,插入执行一个动作

@Test
public void whenIncrementSalaryUsingPeek_thenApplyNewSalary() {
    Employee[] arrayOfEmps = {
        new Employee(1, "TianLe Zhou", 100000.0), 
        new Employee(2, "San Zhang", 200000.0), 
        new Employee(3, "Si Li", 300000.0)
    };

    List<Employee> empList = Arrays.asList(arrayOfEmps);
    
    //为每个员工涨10000的工资
    empList.stream()
      .peek(e -> e.salaryIncrement(10000.0))
      .peek(System.out::println)
      .collect(Collectors.toList());

    assertThat(empList, contains(
      hasProperty("salary", equalTo(110000.0)),
      hasProperty("salary", equalTo(220000.0)),
      hasProperty("salary", equalTo(330000.0))
    ));
}

count()方法

count()返回当前流的总数

@Test
public void whenStreamCount_thenGetElementCount() {
    //计算当前工资高于200000的雇员数
    Long empCount = empList.stream()
      .filter(e -> e.getSalary() > 200000)
      .count();

    assertEquals(empCount, new Long(1));
}

流的比较以及匹配操作

sorted()方法

根据谓词进行排序,并返回最终结果

@Test
public void whenSortStream_thenGetSortedStream() {
    //根据雇员的名字进行排序
    List<Employee> employees = empList.stream()
      .sorted((e1, e2) -> e1.getName().compareTo(e2.getName()))
      .collect(Collectors.toList());

    assertEquals(employees.get(0).getName(), "TianLe Zhou");
    assertEquals(employees.get(1).getName(), "Si Li");
    assertEquals(employees.get(2).getName(), "San Zhang");
}

min() and max()方法

max().png
在方法中传入一个Comparator比较器,作为比较依据,并返回比较的结果,最大或最小值

@Test
public void whenFindMax_thenGetMaxElementFromStream() {
    //找出工资最高的雇员
    Employee maxSalEmp = empList.stream()
      .max(Comparator.comparing(Employee::getSalary))
      .orElseThrow(NoSuchElementException::new);

    assertEquals(maxSalEmp.getSalary(), new Double(300000.0));
}

distinct()方法

distinct.jpeg
如果是基本数据类型则直接比较大小,如果是引用类型则调用引用类型的equal和hashcode方法,去除集合中的重复值

@Test
public void whenApplyDistinct_thenRemoveDuplicatesFromStream() {
    //对list集合去重
    List<Integer> intList = Arrays.asList(2, 5, 3, 2, 4, 3);
    List<Integer> distinctIntList = intList.stream().distinct().collect(Collectors.toList());
    
    assertEquals(distinctIntList, Arrays.asList(2, 5, 3, 4));
}

allMatch, anyMatch,  and noneMatch方法

匹配方式含义
allMatch流中元素与谓词全部匹配,则返回true,否则返回false
anyMatch流中有一个元素与谓词匹配,则返回true,否则返回false
noneMatch方法流中没有元素与谓词匹配,则返回true,否则返回false

根据谓词进行条件匹配并返回boolean值

@Test
public void whenApplyMatch_thenReturnBoolean() {
    List<Integer> intList = Arrays.asList(2, 4, 5, 6, 8);
    //集合中是否全部元素都是2的倍数
    boolean allEven = intList.stream().allMatch(i -> i % 2 == 0);
    //集合中是否包含2的倍数
    boolean oneEven = intList.stream().anyMatch(i -> i % 2 == 0);
    //集合中没有3的倍数
    boolean noneMultipleOfThree = intList.stream().noneMatch(i -> i % 3 == 0);
    
    assertEquals(allEven, false);
    assertEquals(oneEven, true);
    assertEquals(noneMultipleOfThree, false);
}

Reduce操作

reduce.png
reduce接受两个参数,一个初始值这里是0,一个BinaryOperator<T> accumulator 来将两个元素结合起来产生一个新值, 另外reduce方法还有一个没有初始化值的重载方法

@Test
public void whenApplyReduceOnStream_thenGetValue() {
    //计算所有雇员的工资总和
    Double sumSal = empList.stream()
      .map(Employee::getSalary)
      .reduce(0.0, Double::sum);

    assertEquals(sumSal, new Double(600000));
}

进阶的流collect操作

toSet()方法

将当前流中的元素组成set集合

@Test
public void whenCollectBySet_thenGetSet() {
    //获取所有雇员的名字并构成集合
    Set<String> empNames = empList.stream()
            .map(Employee::getName)
            .collect(Collectors.toSet());
    
    assertEquals(empNames.size(), 3);
}

toCollection()方法

将流中的元素转为指定的collection容器

@Test
public void whenToVectorCollection_thenGetVector() {
    //将所有雇员的名字转为一个Vector容器
    Vector<String> empNames = empList.stream()
            .map(Employee::getName)
            .collect(Collectors.toCollection(Vector::new));
            //也可以自定义转位ArrayList等~
    
    assertEquals(empNames.size(), 3);
}

summarizingDouble,summarizingInt()方法

通过summarizingDouble可以直接获取最大值,最小值,平均值等

@Test
public void whenApplySummarizing_thenGetBasicStats() {
    DoubleSummaryStatistics stats = empList.stream()
      .collect(Collectors.summarizingDouble(Employee::getSalary));

    assertEquals(stats.getCount(), 3);
    assertEquals(stats.getSum(), 600000.0, 0);
    assertEquals(stats.getMin(), 100000.0, 0);
    assertEquals(stats.getMax(), 300000.0, 0);
    assertEquals(stats.getAverage(), 200000.0, 0);
}

partitioningBy()方法

partitioningBy().png
分区是特殊的分组,它分类依据是true和false所以返回的结果最多可以分为两组
传入一个boolean类型谓词,按照真值对集合进行分类组成map集合

@Test
public void whenStreamPartition_thenGetMap() {
    //按照是否为2的倍数进行分类
    List<Integer> intList = Arrays.asList(2, 4, 5, 6, 8);
    Map<Boolean, List<Integer>> isEven = intList.stream().collect(
      Collectors.partitioningBy(i -> i % 2 == 0));
    
    assertEquals(isEven.get(true).size(), 4);
    assertEquals(isEven.get(false).size(), 1);
}

groupingBy()方法

groupBy.png

传入一个谓词并以此进行分组操作,谓词为分类函数,组成新的map集合

@Test
public void whenStreamGroupingBy_thenGetMap() {
    //按照雇员的名字首字母进行分类
    Map<Character, List<Employee>> groupByAlphabet = empList.stream().collect(
      Collectors.groupingBy(e -> new Character(e.getName().charAt(0))));

    assertEquals(groupByAlphabet.get('B').get(0).getName(), "TianLe Zhou");
    assertEquals(groupByAlphabet.get('J').get(0).getName(), "Si Li");
    assertEquals(groupByAlphabet.get('M').get(0).getName(), "San Zhang");
}

mapping()方法

mapping方法接收两个参数:
第一个参数是lambda表达式,用来进行属性转换,即一个谓词。
第二个参数是一个收集器,用来收集汇总第一个参数的lambda表达式的结果。

@Test
public void whenStreamMapping_thenGetMap() {
    Map<Character, List<Integer>> idGroupedByAlphabet = empList.stream().collect(
      Collectors.groupingBy(e -> new Character(e.getName().charAt(0)),
        Collectors.mapping(Employee::getId, Collectors.toList())));

    assertEquals(idGroupedByAlphabet.get('B').get(0), new Integer(2));
    assertEquals(idGroupedByAlphabet.get('J').get(0), new Integer(1));
    assertEquals(idGroupedByAlphabet.get('M').get(0), new Integer(3));
}
public void groupByPersonSexAndNmae() {
    List<Person> personList = new ArrayList<>();
        // 四个参与测试的小伙伴
        Person tom = new Person("tom", "男", 11);
        Person lucy = new Person("lucy", "女", 13);
        Person jack = new Person("jack", "男", 12);
        Person tank = new Person("tank", "男", 13);
        personList.add(tom);
        personList.add(amy);
        personList.add(ali);
        personList.add(daming);
        // 对小伙伴先按照性别age进行分组,再按照名字进行分组
        Map<String, Set<String>> resultMap = personList.stream().collect(Collectors.groupingBy(Person::getSex, Collectors.mapping(Person::getName, Collectors.toSet())));
}

并行流操作

parallel-streams.svg
需要注意并行流并不一定比普通stream快因为涉及到多核操作,反而可能使效率变低,处处使用并行流不是一个明智的选择

@Test
public void whenParallelStream_thenPerformOperationsInParallel() {
    Employee[] arrayOfEmps = {
      new Employee(1, "TianLe Zhou", 100000.0), 
      new Employee(2, "San Zhang", 200000.0), 
      new Employee(3, "Si Li", 300000.0)
    };

    List<Employee> empList = Arrays.asList(arrayOfEmps);
    
    empList.stream().parallel().forEach(e -> e.salaryIncrement(10.0));
    
    assertThat(empList, contains(
      hasProperty("salary", equalTo(110000.0)),
      hasProperty("salary", equalTo(220000.0)),
      hasProperty("salary", equalTo(330000.0))
    ));
}

总结

Stream最大好处便是大大优化了代码的书写过程,例如避免了冗长的for循环,极强的增加了代码的可读性,希望你在看完这篇文章后,也能熟练地使用Stream流,在工作项目中写出优雅的code,不会因为代码的复杂繁琐而被同事diss~
同时需要注意一点:不要将声明式和命令式编程混合使用,具体可以看下面的示例

  • 错误的做法 image.png
  • 正确的做法 image.png