CodeGym /행동 /JAVA 25 SELF /zip 결합 (zip), 스트림 생성 (iterate, generate)

zip 결합 (zip), 스트림 생성 (iterate, generate)

JAVA 25 SELF
레벨 32, 레슨 3
사용 가능

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은 피하세요 — 그렇지 않으면 요소 순서가 예측 불가능해집니다.

1
과제
JAVA 25 SELF, 레벨 32, 레슨 3
잠금
제한된 코드 상품을 위한 라벨 생성 🏷️
제한된 코드 상품을 위한 라벨 생성 🏷️
1
과제
JAVA 25 SELF, 레벨 32, 레슨 3
잠금
소셜 네트워크용 테스트 프로필 생성 👥
소셜 네트워크용 테스트 프로필 생성 👥
코멘트
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION