{ }codelper

学习 › Java

各语言特有 · 39

LINQ 与流

把筛选、转换、分组、排序连成一条管道,代替手写循环。

本单元适用于 Java · C#。
Python 用推导式做同样的事(见 Python 一课),JavaScript 和 TypeScript 串联数组方法 filter、map、reduce,C++20 有 std::views::filter 这样的范围(ranges)。
C 和 Craykon 用循环来写。

C# 的 LINQ 和 Java 的流把“筛选、转换、分组、排序、汇总”这些步骤用方法串起来,写成一条流水线。
下面的示例只留下长于三个字母的单词,按长度分组输出,再把剩下单词的字母数加起来。

import java.util.*;import java.util.stream.*; public class Main {    public static void main(String[] args) {        Scanner sc = new Scanner(System.in);        List<String> words = new ArrayList<>();        while (sc.hasNext()) words.add(sc.next());         List<String> longWords = words.stream()            .filter(w -> w.length() > 3)            .toList(); // 스트림은 한 번만 쓸 수 있어서 결과를 목록으로 담아 둬요         Map<Integer, List<String>> byLength = longWords.stream()            .collect(Collectors.groupingBy(String::length, TreeMap::new, Collectors.toList()));         byLength.entrySet().stream()            .map(e -> e.getKey() + ": " + e.getValue().stream().sorted().collect(Collectors.joining(" ")))            .forEach(System.out::println);         int total = longWords.stream().mapToInt(String::length).sum();        System.out.println("total letters: " + total);    }}
stdin
the quick brown fox jumps over the lazy dog quickly
stdout
4: lazy over
5: brown jumps quick
7: quickly
total letters: 30

要点

  • 处理列表的工作大多是筛选、转换、分组、排序、求和的组合。
    不写循环,而是按顺序写出这些步骤。
  • 每个步骤在两种语言里都有对应的名字,比如筛选是 Where/filter,转换是 Select/map。
  • 流水线不是在创建时执行,而是在取出结果时才执行。
  • 结果要用多次的话,先存进列表(ToList()/toList())。
  • 分组的结果是键和对应的一组元素。
    需要按键排序时要自己排。

各语言的写法

Javaimport java.util.stream.*; —— 先用 words.stream() 从列表取出流,再用 filter、map、sorted、Collectors.groupingBy、mapToInt(...).sum()、toList()(或 collect(Collectors.toList()))。
groupingBy 默认用 HashMap,顺序不确定,所以示例用 groupingBy(String::length, TreeMap::new, Collectors.toList()) 改用 TreeMap
C#using System.Linq; —— Where(筛选)、Select(转换)、OrderBy/ThenBy(按值本身排序用 Order())、GroupBy、Sum、ToList()。
GroupBy 按首次出现的顺序返回带 Key 的分组,所以示例用 OrderBy(g => g.Key) 排序。
类似 SQL 的查询语法 from w in words where w.Length > 3 group w by w.Length 编译后也是同样的方法调用

常见错误

同一个流用了两次Java

Stream<String> s = words.stream();
s.forEach(System.out::println);
long n = s.count();

→ IllegalStateException: stream has already been operated upon or closed —— 一个流只能做一次终端操作(forEach、collect、sum 等)

要重复使用的数据先收集成列表,每次需要时重新调用 stream()。

用 Stream<Integer> 处理数字Java

int sum = nums.stream().reduce(0, Integer::sum);

→ 每个数字都被包装成 Integer 对象(装箱),速度慢,而且没有 sum() 方法

用 mapToInt 得到 IntStream,就能直接用 sum()、average()、max()(要变回对象用 boxed())。
C# 的 IEnumerable<int> 不装箱。

建好 LINQ 查询后又改了原列表C#

var q = nums.Where(n => n > 1);
nums.Add(5);
Console.WriteLine(q.Count());

→ q 存的不是结果,而只是“怎么筛选”(延迟执行)。后加的 5 也会被算进去,而且每次 foreach 或 Count() 都从头重新执行

算过一次的结果要多次使用,就用 ToList() 存起来。

各语言的差异
流水线在“取出结果时”才执行。
C# 查询每次取出都会重新执行,Java 流用一次就结束,所以要多次使用的结果请用 ToList()/toList() 存进列表。

练习题