[TIL] 컬렉션과 스트림 간단 정리

NCOOKIE·2025년 3월 14일
0

TIL

목록 보기
15/20

배열

배열(Array)은 같은 타입의 데이터를 연속된 메모리 공간에 저장하는 자료 구조

배열의 특징

  1. 고정 크기 (Static Size)

    • 배열을 선언할 때 크기를 정하면, 한 번 정해진 크기를 동적으로 늘리거나 줄일 수 없다.
  2. 동일한 타입 (Homogeneous Elements)

    배열은 하나의 타입으로만 구성된 데이터를 저장할 수 있다.

  3. 인덱스(Index) 접근

    • 배열의 각 원소에 접근할 때에는 인덱스를 사용한다.
    • 인덱스는 0부터 시작하며, 배열길이 - 1까지만 유효하다.
  4. 간단한 구조 & 메모리 효율

    • 배열은 구조가 단순하고 연속된 메모리를 사용하여 성능 상 좋다.
    • 하지만 크기가 고정되어 있기 때문에, 중간 데이터 삽입/삭제가 어렵다.

컬렉션 (Collection)

컬렉션의 특징

  1. 동적 크기
    • 필요에 따라 크기가 자동으로 조절된다. (ex: ArrayList는 내부적으로 배열 크기를 늘려서 저장한다.)
  2. 유연한 데이터 타입
    • 제네릭(Generics)을 사용해 다양한 타입의 데이터를 저장할 수 있다.
  3. 다양한 자료 구조
    • 순서가 있는 List, 순서가 중요하지 않은 Set, 키-값 쌍으로 이루어진 Map 등 다양한 구조 제공
  4. 풍부한 메소드
    • 삽입, 삭제, 검색, 정렬 등의 기능을 손쉽게 수행할 수 있는 다양한 메소드를 제공

주요 인터페이스

인터페이스대표 구현 클래스특징
ListArrayList, LinkedList- 순서가 있는 데이터 집합
  • 중복 허용
  • 인덱스로 접근 가능 |
    | Set | HashSet, TreeSet | - 순서가 없는 데이터 집합
  • 중복 불가 |
    | Map | HashMap, TreeMap | - 키(key)와 값(value)의 쌍
  • 키 중복 불가, 값 중복 허용 |

컬렉션에서 래퍼 클래스를 사용하는 이유

  1. 객체 지향 설계
    • 자바의 컬렉션은 객체를 다루도록 설계되어 있다.
    • 원시 타입은 객체가 아니므로, 컬렉션에는 래퍼 클래스를 사용해야 한다.
  2. 기능 확장
    • 래퍼 클래스는 원시 타입에 없는 여러 메서드를 제공한다. (Integer.parseInt() 등)
  3. 제네릭(Generic) 사용
    • 자바의 제네릭은 객체 타입을 요구한다.
    • 원시 타입은 제네릭 타입 파라미터로 사용할 수 없기 때문에, 래퍼 클래스를 사용해야 한다.
  4. 값의 불변성
    • 래퍼 클래스는 불변(Immutable) 객체로, 멀티스레드 환경에서 안전하게 사용할 수 있다.

스트림 (Stream)

개념

스트림은 자바 8에서 도입된 기능으로, 배열이나 컬렉션의 데이터를 함수형(람다)으로 다루기 위한 추상화된 데이터 처리 방식이다.

반복문으로 하나씩 꺼내서 처리하던 방식에서 벗어나, 중간 연산(Intermediate Operation)최종 연산(Terminal Operation)을 체이닝(Chaining)하여 선언적으로 데이터를 처리할 수 있다.

특징

  1. 함수형 스타일
    • for문 대신 함수형 메서드를 연쇄적으로 적용하여 가독성을 높임
  2. 데이터 변경 없음
    • 스트림은 원본 데이터를 변경하지 않는다. (불변성)
  3. 지연 연산(Lazy Evaluation)
    • 중간 연산은 결과를 바로 내지 않고, 최종 연산이 실행될 때 한 번에 처리한다.
  4. 병렬 처리(Parallel Stream)
    • 스트림을 병렬로 처리하여 멀티코어 환경에서 성능을 높일 수 있다.

스트림의 처리 단계

  1. 스트림 생성 (Create)
    • 배열, 컬렉션, 혹은 기타 소스에서 스트림 객체를 생성
  2. 중간 연산 (Intermediate Operations)
    • 맵핑, 필터링 등으로 데이터를 가공
  3. 최종 연산 (Terminal Operation)
    • 결과를 집계하거나, 리스트로 다시 모으거나, 화면에 출력

이 과정을 체이닝(Chaining) 으로 연결하여 선언적으로 작성할 수 있다.

스트림 생성 방법

  1. 컬렉션(Collection)에서 생성
List<String> list = Arrays.asList("a", "b", "c", "d");
Stream<String> stream = list.stream();
  1. 배열에서 생성
String[] arr = {"a", "b", "c", "d"};
Stream<String> streamFromArray = Arrays.stream(arr);
  1. Stream.of() 메서드 사용
Stream<String> streamOf = Stream.of("a", "b", "c", "d");
  1. IntStream, LongStream, DoubleStream (기본형 특화 스트림)
    1. DoubleStream 과 Stream은 다르다! + DoubleStream이 훨씬 빠름
IntStream intStream = IntStream.range(1, 5); // 1,2,3,4
Stream<int> X

중간 연산 (Intermediate Operations)

중간 연산은 스트림을 변환하거나 필터링하며, 또 다른 스트림을 반환한다.

이러한 중간 연산은 실제로 실행되지 않고 최종 연산이 호출될 때 한꺼번에 실행된다.

필터링(Filtering) - filter()

  • 스트림 내 요소들을 조건(Predicate)에 따라 걸러낸다.
  • Predicate<T>boolean을 반환하는 람다식을 의미
List<String> list = Arrays.asList("apple", "banana", "avocado", "grape");

List<String> filteredList = list.stream() 
                                .filter(s -> s.startsWith("a"))
                                .collect(Collectors.toList()); 

// 결과: ["apple", "avocado"]

맵핑(Mapping) - map()

  • 스트림 내 각 요소를 다른 형태로 변환
  • Function<T, R>를 사용하여 요소를 매핑
List<String> list = Arrays.asList("apple", "banana", "orange");

List<String> mappedList = list.stream()
                              .map(String::toUpperCase)
                              .collect(Collectors.toList());

// 결과: ["APPLE", "BANANA", "ORANGE"]

정렬(Sorting) - sorted()

  • 스트림 내 요소들을 정렬
  • 기본적으로 오름차순으로 정렬하며, 커스텀 Comparator로 정렬 방식을 변경 가능
List<Integer> numbers = Arrays.asList(3, 1, 4, 2);

List<Integer> sortedList = numbers.stream()
                                  .sorted()
                                  .collect(Collectors.toList());

// 결과: [1, 2, 3, 4]

중복 제거(Distinct) - distinct()

  • 스트림 내 중복 요소를 제거
List<Integer> numbers = Arrays.asList(1, 2, 2, 3, 3, 3);

List<Integer> distinctList = numbers.stream()
                                    .distinct()
                                    .collect(Collectors.toList());

// 결과: [1, 2, 3]

제한/건너뛰기 - limit(n), skip(n)

  • limit(n): 스트림에서 최대 n개의 요소까지만 추출
  • skip(n): 스트림에서 처음 n개의 요소를 건너뛰고 이후 요소만 가져옴
List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5);

List<Integer> limited = numbers.stream()
                               .limit(3)
                               .collect(Collectors.toList());
// [1, 2, 3]

List<Integer> skipped = numbers.stream()
                               .skip(2)
                               .collect(Collectors.toList());
// [3, 4, 5]

최종 연산 (Terminal Operations)

최종 연산이 호출되면 스트림은 소모(consume)되며, 더 이상 재사용이 불가능

forEach()

  • 스트림의 각 요소를 소비하며, 주어진 동작을 수행합니다.
list.stream()
    .forEach(System.out::println);

collect()

  • 스트림의 요소를 List, Set, Map 등으로 모아서 반환
List<String> collectedList = list.stream()
                                 .collect(Collectors.toList());

reduce()

  • 스트림의 요소를 하나씩 줄여가면서(계산하면서) 하나의 결과를 생성
Optional<Integer> sum = numbers.stream(){1,2,3}
                              .reduce((a, b) -> a + b);
sum.ifPresent(System.out::println);
  • reduce(초기값, (누적값, 현재값) -> 새로운 누적값) 형태로 초기값을 포함해서 구현할 수도 있다.

그 외 최종 연산 메서드

  • count(): 스트림의 요소 개수 반환
  • max(), min(): 최대값, 최소값 반환 (Comparator 필요)
  • anyMatch(), allMatch(), noneMatch(): 특정 조건에 부합하는지 검사 후 boolean 결과 반환
  • findFirst(), findAny(): 스트림의 첫 번째 혹은 임의 요소 반환 (Optional)

스트림의 지연 연산(Lazy Evaluation) 예시

중간 연산은 최종 연산이 일어나기 전까지 실제로 실행되지 않는다. 이를 Lazy Evaluation(지연 연산) 이라고 한다.

import java.util.Arrays;
import java.util.List;
import java.util.stream.Collectors;
import java.util.stream.Stream;

public class LazyEvaluationExample {
    public static void main(String[] args) {
        List<String> list = Arrays.asList("apple", "banana", "orange", "grape", "melon");

        // 중간 연산만 정의
        Stream<String> stream = list.stream()
            .filter(s -> {
                System.out.println("filtering: " + s);
                return s.startsWith("a");
            })
            .map(s -> {
                System.out.println("mapping: " + s);
                return s.toUpperCase();
            });

        System.out.println("스트림이 정의되었지만 아직 처리되지 않았습니다.");

        // 최종 연산을 호출하는 순간, 그제야 모든 중간 연산이 실행됨
        List<String> result = stream.collect(Collectors.toList());

        System.out.println("결과: " + result);
    }
}
스트림이 정의되었지만 아직 처리되지 않았습니다.
filtering: apple
mapping: apple
filtering: banana
filtering: orange
filtering: grape
filtering: melon

실행 흐름

  1. filter()map()은 중간 연산으로, 즉시 실행되지 않음
  2. collect()(최종 연산)가 호출될 때 filter()map()이 실제 실행

병렬 스트림(Parallel Stream) 기초

병렬 스트림은 여러 쓰레드가 동시에 스트림의 요소를 처리하므로, 데이터 양이 많을 때 성능 향상을 기대할 수 있다.

import java.util.Arrays;
import java.util.List;

public class ParallelStreamExample {
    public static void main(String[] args) {
        List<String> list = Arrays.asList("apple", "banana", "orange", "grape", "melon");

        list.parallelStream()
            .map(String::toUpperCase)
            .forEach(s -> System.out.println(Thread.currentThread().getName() + " => " + s));
    }
}
ForkJoinPool.commonPool-worker-2 => APPLE
ForkJoinPool.commonPool-worker-4 => GRAPE
ForkJoinPool.commonPool-worker-1 => BANANA
main => ORANGE
ForkJoinPool.commonPool-worker-3 => MELON
  • 실행할 때마다 쓰레드 이름이 달라질 수 있고, 출력 순서도 달라질 수 있다.

정리

  1. 스트림의 핵심
    • 원본 데이터를 건드리지 않고, 중간 연산을 통해 원하는 형태로 가공한 뒤 최종 연산으로 결과를 얻음
    • 모든 중간 연산은 지연(Lazy)되어, 최종 연산이 실행될 때 한 번에 처리
  2. 주요 중간 연산
    • filter(), map(), sorted(), distinct(), limit(), skip()
    • 체이닝하여 여러 작업을 순차적으로 처리 가능
  3. 주요 최종 연산
    • forEach(), collect(), reduce(), count(), max(), min(), findFirst(), findAny()
    • 결과를 반환하거나 출력하며 스트림을 소모합니다.
  4. 병렬 스트림
    • parallelStream()을 이용해 멀티코어를 활용
    • 상황에 따라 성능 향상을 기대할 수 있지만, 쓰레드 안전성 등에 유의
  5. 전통적인 반복문과의 차이
    • 선언적(무엇을 할지 중심)으로 작성 가능
    • 가독성유연성 향상
    • 데이터가 많을 때 병렬화로 성능 이점을 얻을 수 있음
profile
일단 해보자

0개의 댓글