1. zip 소개
프로그래밍에서 zip(“지퍼”, “묶기”)은 둘(또는 그 이상)의 리스트를 받아 각 리스트에서 하나씩 뽑아 쌍의 스트림으로 합치는 연산을 뜻합니다. Python에 익숙하다면, 거기에도 정확히 이 일을 하는 zip 함수가 있습니다.
예:
- 이름 목록: ["Anya", "Boris", "Vika"]
- 나이 목록: [20, 25, 19]
- zip으로 결합하면: [("Anya", 20), ("Boris", 25), ("Vika", 19)]
두 컬렉션을 동기적으로 처리해야 할 때 유용합니다 — 예를 들어 이름과 나이가 함께 들어가는 객체를 만들 때 등입니다.
왜 Stream API에 zip이 없을까?
표준 Stream API(Java 22 이전)에는 zip 메서드가 없습니다. 스트림은 무한할 수도 있고, 컬렉션 길이가 서로 다를 수 있으며, 한쪽이 더 길 때의 동작을 항상 명확히 정하기 어렵기 때문입니다. 하지만 실무에서는 zip이 자주 필요합니다.
2. Java에서 zip 구현: 내장 메서드 없이 어떻게 할까
가장 쉬운 방법 — 인덱스로 처리하기
두 리스트가 있고 그것들이 “보통” 컬렉션(예: List<A>, List<B>)임을 알고 있다면, 인덱스를 사용할 수 있습니다:
import java.util.*;
import java.util.stream.*;
public class ZipExample {
public static void main(String[] args) {
List<String> names = Arrays.asList("Anya", "Boris", "Vika");
List<Integer> ages = Arrays.asList(20, 25, 19);
int size = Math.min(names.size(), ages.size());
List<Person> people = IntStream.range(0, size)
.mapToObj(i -> new Person(names.get(i), ages.get(i)))
.collect(Collectors.toList());
people.forEach(System.out::println);
}
static class Person {
String name;
int age;
Person(String name, int age) { this.name = name; this.age = age; }
public String toString() { return name + " (" + age + ")"; }
}
}
출력:
Anya (20)
Boris (25)
Vika (19)
무슨 일이 일어나나?
- 두 리스트의 최소 크기를 구해 — 인덱스 범위를 벗어나지 않도록 합니다.
- IntStream.range(0, size) — 인덱스 스트림을 만듭니다.
- 각 인덱스에 대해 각 리스트에서 하나씩 꺼내 “맞물려” 결합합니다.
- Collectors.toList()로 결과를 리스트로 수집합니다.
Stream<T>에 zip을 만들 수 있을까?
기술적으로는 가능합니다. 다만 둘 다 유한하고, 인덱스에 빠르게 접근할 수 있는 구조(사실상 List)일 때 편리합니다. “진짜” 스트림(예: 무한 스트림)에 대한 올바른 zip 구현은 더 복잡하며 추가 로직이 필요합니다.
대안: 서드파티 라이브러리
“바로 쓸 수 있는” zip을 원한다면 다음 라이브러리를 사용할 수 있습니다:
- org.apache.commons.lang3.Streams.zip (Apache Commons Lang 3.10+)
- io.vavr.collection.Stream.zip (Vavr)
- com.codepoetics.protonpack.StreamUtils.zip (ProtonPack)
이 강의에서는 표준 라이브러리를 따르므로 ‘수작업’ 방법을 다룹니다.
3. zip 활용 실전 예제
예제 1. 두 컬렉션을 동기 순회(요소 합산)
List<Integer> a = Arrays.asList(1, 2, 3, 4);
List<Integer> b = Arrays.asList(10, 20, 30, 40);
List<Integer> sums = IntStream.range(0, Math.min(a.size(), b.size()))
.mapToObj(i -> a.get(i) + b.get(i))
.collect(Collectors.toList());
System.out.println(sums); // [11, 22, 33, 44]
예제 2. 문자열과 문자 zip 결합
String[] words = {"cat", "dog", "fox"};
char[] marks = {'!', '?', '.'};
List<String> zipped = IntStream.range(0, Math.min(words.length, marks.length))
.mapToObj(i -> words[i] + marks[i])
.collect(Collectors.toList());
System.out.println(zipped); // [cat!, dog?, fox.]
시각화(도식)
names: [Anya] [Boris] [Vika]
ages: [20 ] [25 ] [19 ]
| | |
zip ---> (Anya,20) (Boris,25) (Vika,19)
4. Stream.iterate와 Stream.generate — 새로운 스트림 생성
가끔은 기존 컬렉션을 처리하는 것뿐 아니라, 즉석에서 새로운 시퀀스를 생성해야 할 때가 있습니다. 이를 위해 Stream API에는 두 가지 유용한 메서드가 있습니다:
- Stream.iterate — 규칙에 따른 시퀀스를 생성합니다(예: 등차수열).
- Stream.generate — 각 요소를 Supplier로 계산해 생성합니다(예: 난수, 현재 시각 등).
Stream.iterate
구문:
Stream.iterate(seed, unaryOperator)
- seed — 초깃값;
- unaryOperator — 다음 요소를 계산하는 함수.
예제 1: 등차수열
Stream<Integer> numbers = Stream.iterate(0, n -> n + 2); // 0, 2, 4, 6, ...
numbers.limit(5).forEach(System.out::println);
// 출력: 0 2 4 6 8
예제 2: 날짜 생성
import java.time.LocalDate;
Stream<LocalDate> days = Stream.iterate(LocalDate.now(), date -> date.plusDays(1));
days.limit(3).forEach(System.out::println);
// 예: 2024-06-09, 2024-06-10, 2024-06-11
예제 3: 무한 스트림 — limit를 잊지 마세요!
Stream<Integer> endless = Stream.iterate(1, n -> n * 2);
endless.limit(5).forEach(System.out::println); // 1 2 4 8 16
Java 9+에서 조건(predicate)이 있는 오버로드가 추가되었습니다:
Stream.iterate(0, n -> n < 10, n -> n + 2)
.forEach(System.out::println); // 0 2 4 6 8
Stream.generate
구문:
Stream.generate(Supplier<T>)
각 요소는 Supplier.get() 호출로 계산됩니다.
예제 1: 난수
import java.util.Random;
Random random = new Random();
Stream<Integer> randoms = Stream.generate(random::nextInt);
randoms.limit(5).forEach(System.out::println);
예제 2: 동일한 값 생성
Stream<String> stars = Stream.generate(() -> "*");
stars.limit(4).forEach(System.out::print); // ****
예제 3: 고유 식별자
import java.util.UUID;
Stream<String> uuids = Stream.generate(() -> UUID.randomUUID().toString());
uuids.limit(3).forEach(System.out::println);
시각화(도식)
Stream.iterate:
[seed] -> op() -> op() -> op() -> ...
n n+1 n+2 n+3
Stream.generate:
Supplier() -> Supplier() -> Supplier() -> ...
val1 val2 val3
5. 사용 예: 애플리케이션용 데이터 생성
다음과 같은 Student 클래스가 있다고 합시다:
class Student {
String name;
int age;
Student(String name, int age) { this.name = name; this.age = age; }
public String toString() { return name + " (" + age + ")"; }
}
예제 1. 테스트용 학생 생성
List<String> names = Arrays.asList("Anya", "Boris", "Vika", "Gleb", "Dasha");
Stream<Student> students = IntStream.range(0, names.size())
.mapToObj(i -> new Student(names.get(i), 18 + i));
students.forEach(System.out::println);
// 예: Anya (18), Boris (19), Vika (20), Gleb (21), Dasha (22)
예제 2. 무작위 학생 생성
Random random = new Random();
List<String> pool = Arrays.asList("Ira", "Oleg", "Maksim", "Tanya", "Sergey");
Stream<Student> randomStudents = Stream.generate(() ->
new Student(
pool.get(random.nextInt(pool.size())),
18 + random.nextInt(5)
)
);
randomStudents.limit(3).forEach(System.out::println);
// 예: Tanya (19), Oleg (21), Ira (20)
예제 3. 보고서용 날짜 시퀀스 생성
import java.time.LocalDate;
Stream<LocalDate> dates = Stream.iterate(LocalDate.of(2024, 6, 1), d -> d.plusDays(1));
dates.limit(5).forEach(System.out::println);
// 2024-06-01, 2024-06-02, ..., 2024-06-05
비교: zip, iterate, generate를 언제 쓸까
- zip — 두(또는 그 이상) 리스트/스트림을 인덱스로 맞춰 동기적으로 처리할 때.
- iterate — 규칙에 따라 생성되는 시퀀스가 필요할 때(숫자, 날짜, 단계 등).
- generate — 각 요소를 서로 독립적으로 계산할 때(난수, 고유 ID 등).
6. zip과 스트림 생성에서 흔한 실수
오류 №1: limit 없이 무제한 스트림을 만들기. Stream.iterate 또는 Stream.generate를 limit 없이 사용하면 프로그램이 멈추거나 메모리를 모두 소모할 수 있습니다.
Stream.generate(() -> 1).forEach(System.out::println); // 절대 끝나지 않습니다!
오류 №2: zip 시 서로 다른 길이를 잘못 처리. 한쪽 리스트가 더 길다면 최소 길이까지만 순회해야 합니다. 그렇지 않으면 IndexOutOfBoundsException이 발생합니다.
오류 №3: 일반 Stream<T>에 zip을 시도. 일반 스트림은 인덱스 접근이 없습니다. 실용적인 zip은 보통 List에 대해 구현합니다.
오류 №4: 스트림 생성/순회 중 컬렉션 수정. 스트림을 흘려보내는 동안 컬렉션을 바꾸면 ConcurrentModificationException이 발생할 수 있습니다. 새 데이터를 생성하되 — 기존 데이터를 “즉석에서” 바꾸지 마세요.
오류 №5: 순서 손실. 순서가 중요하다면(예: zip), List를 사용하고 Set은 피하세요 — 그렇지 않으면 요소 순서가 예측 불가능해집니다.
GO TO FULL VERSION