언어마다 다른 것 · 39
LINQ와 스트림
거르기·바꾸기·묶기·정렬을 반복문 대신 한 줄짜리 파이프라인으로 이어 써요.
이 단원은 Java · C#에서 배워요.
Python은 내포(컴프리헨션)로 같은 일을 하고(Python 단원 참고), JavaScript·TypeScript는 배열 메서드 filter·map·reduce를 이어 쓰고, C++20은 std::views::filter 같은 레인지를 써요.
C와 크레이콘은 반복문으로 해요.
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); }}using System;using System.Linq; class Program{ static void Main() { string[] words = Console.In.ReadToEnd() .Split((char[])null, StringSplitOptions.RemoveEmptyEntries); var longWords = words .Where(w => w.Length > 3) .ToList(); // 여기서 한 번 실행해 결과를 담아 둬요 var lines = longWords .GroupBy(w => w.Length) .OrderBy(g => g.Key) .Select(g => $"{g.Key}: {string.Join(" ", g.Order())}"); foreach (string line in lines) Console.WriteLine(line); int total = longWords.Sum(w => w.Length); Console.WriteLine($"total letters: {total}"); }}the quick brown fox jumps over the lazy dog quickly
4: lazy over 5: brown jumps quick 7: quickly total letters: 30
핵심
- 목록을 다루는 일은 대부분 거르기·바꾸기·묶기·정렬·합의 조합이에요.
반복문 대신 그 단계를 차례로 적어요. - 단계마다 이름이 짝지어져 있어요.
거르기Where/filter, 바꾸기Select/map처럼요. - 파이프라인은 만들 때가 아니라 결과를 꺼낼 때 실행돼요.
- 결과를 여러 번 쓸 거면 목록으로 담아 둬요(
ToList()/toList()). - 묶은 결과는 열쇠(키)와 그 무리로 나와요.
키 순서가 필요하면 직접 정렬해요.
언어별로 쓰는 법
| Java | import 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())로 키 순서를 맞춰요 |
|---|---|
| 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()로 담아 두세요.