본문 바로가기
자바/김영한의 실전 자바 - 중급1, 2

김영한의 실전 자바 - 자바 중급 2편: 컬렉션 프레임워크(feat. Comparable, Compartor, Stream, Optional)

by limdae94 2025. 1. 1.

컬렉션 프레임워크

  1. Collection, Collections
  2. List(ArrayList, LinkedList)
  3. Set(HashSet, LinkedHashSet, TreeSet)
  4. Queue, Deque(PriorityQueue, ArrayDeque)
  5. Hash, Map (HashMap, TreeMap, ConcurrentHashMap)
  6. Comparable, Comparator
  7. Stream, Collector
  8. Optional

1️⃣ Collection, Collections

자바 컬렉션 프레임워크는 데이터를 효율적으로 저장하고 관리할 수 있는 데이터 구조와 알고리즘을 제공하는 라이브러리


1. Collection 특징

CollectionList, Set, Queue의 상위 인터페이스이다. 컬렉션 프레임워크는 제네릭을 지원하여 타입 안정성을 보장하고, 알고리즘이 포함된 Collections 클래스에서 정렬, 검색, 복사 등의 유틸리티 제공한다. 다음은 Collection을 구현한 세 가지 주요 인터페이스이다.

인터페이스 특징 주요 구현체
List 순서 유지, 중복 허용 ArrayList, LinkedList, Vector
Set 순서 없음, 중복 허용 안 함 HashSet, LinkedHashSet, TreeSet
Queue FIFO(선입선출), 우선순위 큐 지원 PriorityQueue, LinkedList, ArrayDeque

주요 메서드들은 모든 컬렉션에서 공통으로 사용된다.

Collection에서 중요한 메서드 활용

공통적으로 사용할 수 있는 메서드를 중심으로 실습하며 이해를 돕습니다.

메서드 설명 예제
add(E e) 요소 추가 collection.add("Element");
remove(Object o) 특정 요소 제거 collection.remove("Element");
contains(Object o) 특정 요소가 포함되어 있는지 확인 collection.contains("Element");
size() 컬렉션의 요소 개수 System.out.println(collection.size());
clear() 컬렉션의 모든 요소 삭제 collection.clear();
forEach(Consumer<E>) 컬렉션의 요소를 하나씩 순회 collection.forEach(System.out::println);

Iterable과의 관계

  • CollectionIterable 인터페이스를 상속받는다.
  • 이를 통해 forEachiterator() 같은 반복 작업이 가능하다.

2. 기본 사용법

// List
List<String> list = new ArrayList<>();
list.add("Apple");
list.add("Banana");
list.forEach(System.out::println); // Apple, Banana
// Set
Set<Integer> set = new HashSet<>();
set.add(1);
set.add(2);
set.add(2); // 중복 허용 안 함
set.forEach(System.out::println);
// Map
Map<String, String> map = new HashMap<>();
map.put("Key1", "Value1");
map.put("Key2", "Value2");
System.out.println(map.get("Key1"));
import java.util.*;

public class CollectionPractice {
    public static void main(String[] args) {
        // 1. Collection 생성 (ArrayList)
        Collection<String> collection = new ArrayList<>();

        // 2. 요소 추가
        collection.add("Apple");
        collection.add("Banana");
        collection.add("Cherry");

        // 3. 요소 포함 여부 확인
        System.out.println("Contains 'Apple': " + collection.contains("Apple"));

        // 4. 요소 제거
        collection.remove("Banana");
        System.out.println("After removal: " + collection);

        // 5. 반복
        System.out.println("Iterating elements:");
        collection.forEach(System.out::println);

        // 6. 크기 확인 및 비우기
        System.out.println("Size: " + collection.size());
        collection.clear();
        System.out.println("Is empty: " + collection.isEmpty());
    }
}

3. 선택 기준

  • List: 순서와 중복이 중요할 때
  • Set: 중복을 허용하지 않을 때
  • Map: 키-값 형태로 데이터를 저장할 때
  • Queue: FIFO(선입선출) 작업 시

4. Collections 특징

Collections 유틸리티 클래스컬렉션 데이터 구조를 다룰 때 유용한 메서드를 모아둔 도구 클래스이다. 정렬, 검색, 동기화, 불변 컬렉션 생성 등 다양한 작업을 간단히 처리할 수 있다.


5. Collections 클래스의 주요 메서드

(1) 정렬 (Sorting)

  • sort(List<T> list)
    • 리스트를 정렬
    • Comparable 구현 클래스일 경우 기본 정렬 기준을 사용
    • 커스텀 정렬이 필요하면 Comparator를 함께 사용
    import java.util.*;
    
    public class SortingExample {
        public static void main(String[] args) {
            List<String> list = new ArrayList<>(Arrays.asList("Banana", "Apple", "Cherry"));
            Collections.sort(list);
            System.out.println(list); // [Apple, Banana, Cherry]
    
            // Custom Comparator
            Collections.sort(list, Comparator.reverseOrder());
            System.out.println(list); // [Cherry, Banana, Apple]
        }
    }

(2) 최대/최소값 찾기

  • max(Collection<T> coll)
  • min(Collection<T> coll)
    • 컬렉션에서 최대값/최소값을 조회
    • Comparator를 사용해 기준을 커스터마이징 가능
    List<Integer> numbers = Arrays.asList(10, 20, 5, 15);
    System.out.println(Collections.max(numbers)); // 20
    System.out.println(Collections.min(numbers)); // 5

(3) 데이터 검색

  • binarySearch(List<T> list, T key)
    • 정렬된 리스트에서 이진 탐색으로 키를 검색
    • 반드시 정렬된 상태에서만 사용해야 정확한 결과를 얻을 수 있음
    List<String> list = new ArrayList<>(Arrays.asList("Apple", "Banana", "Cherry"));
    Collections.sort(list); // 정렬
    System.out.println(Collections.binarySearch(list, "Banana")); // 1
    System.out.println(Collections.binarySearch(list, "Orange")); // 음수 값 (존재하지 않음)

(4) 동기화된 컬렉션 생성

  • synchronizedList(List<T> list)
  • synchronizedSet(Set<T> set)
  • synchronizedMap(Map<K, V> map)
    • 멀티스레드 환경에서 안전하게 사용할 수 있는 동기화된 컬렉션을 반환
    • 반환된 컬렉션은 스레드 안전하지만, 성능이 저하될 수 있음
  • 예제:
  • List<String> syncList = Collections.synchronizedList(new ArrayList<>()); synchronized (syncList) { syncList.add("Thread-safe"); }

(5) 불변 컬렉션 생성

  • unmodifiableList(List<T> list)
  • unmodifiableSet(Set<T> set)
  • unmodifiableMap(Map<K, V> map)
    • 불변(immutable) 컬렉션을 생성
    • 반환된 컬렉션은 읽기만 가능하며, 수정 시 UnsupportedOperationException이 발생
  • 예제:
  • java코드 복사List<String> list = new ArrayList<>(Arrays.asList("A", "B", "C")); List<String> unmodifiableList = Collections.unmodifiableList(list); System.out.println(unmodifiableList); // [A, B, C] // unmodifiableList.add("D"); // UnsupportedOperationException

(6) 데이터 조작

  • reverse(List<T> list): 리스트의 요소 순서를 반대로 뒤집는다
  • shuffle(List<T> list): 리스트의 요소를 랜덤하게 섞는다
  • fill(List<T> list, T obj): 리스트의 모든 요소를 특정 값으로 채운다
  • copy(List<T> dest, List<T> src): 소스 리스트의 내용을 대상 리스트에 복사한다
    대상 리스트의 크기가 소스 리스트보다 작으면 IndexOutOfBoundsException이 발생
  • List<String> list = new ArrayList<>(Arrays.asList("A", "B", "C")); // Reverse Collections.reverse(list); System.out.println("Reversed: " + list); // [C, B, A] // Shuffle Collections.shuffle(list); System.out.println("Shuffled: " + list); // 랜덤 순서 // Fill Collections.fill(list, "X"); System.out.println("Filled: " + list); // [X, X, X]

(7) Singleton 컬렉션

  • singletonList(T obj)하나의 요소만 포함하는 불변 리스트를 생성
  • List<String> singleton = Collections.singletonList("Single"); System.out.println(singleton); // [Single]

6. Collections의 활용 예제

import java.util.*;

public class CollectionsExample {
    public static void main(String[] args) {
        List<String> list = new ArrayList<>(Arrays.asList("Banana", "Apple", "Cherry"));

        // 1. 정렬
        Collections.sort(list);
        System.out.println("Sorted: " + list);

        // 2. 최대/최소값
        System.out.println("Max: " + Collections.max(list));
        System.out.println("Min: " + Collections.min(list));

        // 3. 데이터 조작
        Collections.reverse(list);
        System.out.println("Reversed: " + list);

        // 4. 동기화된 리스트
        List<String> syncList = Collections.synchronizedList(list);
        synchronized (syncList) {
            syncList.add("Thread-safe");
        }
        System.out.println("Synchronized List: " + syncList);
    }
}

7. Collections 정리

  • Collections 클래스는 컬렉션 데이터를 조작하거나 생성하는 데 매우 유용하다.
  • 꼭 알아야 할 메서드:
    1. 정렬: sort, reverse, shuffle
    2. 최대/최소값: max, min
    3. 동기화 및 불변 컬렉션 생성: synchronizedList, unmodifiableList

8. Collection vs Collections

Collection

  • 정의: 인터페이스
  • 위치: java.util.Collection
  • 역할
    • 컬렉션 프레임워크의 상위 인터페이스로, List, Set, Queue가 이를 확장한다
    • 공통된 메서드를 정의하여 다양한 컬렉션 클래스에서 사용된다
  • : List, Set, Queue는 모두 Collection 인터페이스를 구현한다

주요 특징

  • 컬렉션 데이터 구조의 공통된 기능 제공
    • 데이터 추가/삭제: add, remove
    • 크기 확인: size, isEmpty
    • 반복: iterator
import java.util.*;

public class CollectionExample {
    public static void main(String[] args) {
        Collection<String> collection = new ArrayList<>();
        collection.add("Apple");
        collection.add("Banana");

        System.out.println("Size: " + collection.size()); // Size: 2
        collection.forEach(System.out::println); // Apple, Banana
    }
}

Collections

  • 정의: 유틸리티 클래스
  • 위치: java.util.Collections
  • 역할
    • 컬렉션 객체(List, Set, Map)를 조작하기 위한 정적 메서드를 제공한다.
    • 정렬, 검색, 동기화된 컬렉션 생성 등 다양한 작업을 간편하게 처리할 수 있다.
  • : Collections.sort, Collections.synchronizedList

주요 특징

  • 컬렉션 데이터를 조작하거나 새로운 컬렉션 생성에 사용
  • 주요 메서드
    • 정렬: sort
    • 최대/최소값: max, min
    • 동기화 컬렉션: synchronizedList, synchronizedSet
    • 불변 컬렉션: unmodifiableList, unmodifiableSet
import java.util.*;

public class CollectionsExample {
    public static void main(String[] args) {
        List<String> list = new ArrayList<>(Arrays.asList("Banana", "Apple", "Cherry"));

        // 정렬
        Collections.sort(list);
        System.out.println("Sorted: " + list); // Sorted: [Apple, Banana, Cherry]

        // 최대값/최소값
        System.out.println("Max: " + Collections.max(list)); // Max: Cherry
        System.out.println("Min: " + Collections.min(list)); // Min: Apple
    }
}

9. 차이점 비교

항목 Collection Collections
정의 인터페이스 유틸리티 클래스
역할 컬렉션의 공통 메서드 정의 컬렉션 조작 및 생성 메서드 제공
위치 java.util.Collection java.util.Collections
상속/구현 List, Set, Queue 등이 상속 없음 (정적 메서드로 구성)
예제 ArrayList, HashSet와 같은 구현체 Collections.sort, Collections.max

10. 결론

  • Collection컬렉션 인터페이스로, 데이터 구조의 기본 동작(추가, 삭제, 검색 등)을 정의한다
  • 데이터 구조를 정의: Collection
  • Collections유틸리티 클래스로, 컬렉션을 조작하거나 유용한 기능을 제공한다
  • 데이터를 조작: Collections

2️⃣ List

  • List는 순서가 있는 데이터 구조. 중복된 요소를 허용
  • ArrayList, LinkedList, Vector(Regacy)

1. List의 주요 메서드

  • 추가: add(E element), add(int index, E element)
  • 조회: get(int index)
  • 수정: set(int index, E element)
  • 삭제: remove(int index), remove(Object o)
  • 검색: indexOf(Object o), lastIndexOf(Object o)
  • 크기 확인: size()
  • 순회: forEach, iterator(), listIterator()

2. List 구현체

(1) ArrayList

  • 특징: 배열 기반. 랜덤 액세스가 빠름. 삽입/삭제는 느림(데이터 이동 필요)
  • 용도: 읽기와 탐색이 빈번한 경우
List<String> list = new ArrayList<>();
list.add("Apple");
list.add("Banana");
System.out.println(list.get(0)); // Apple
list.remove(1);
list.forEach(System.out::println); // Apple

(2) LinkedList

  • 특징: 노드 기반, 삽입/삭제가 빠름. 랜덤 액세스는 느림
  • 용도: 삽입/삭제가 빈번한 경우
List<String> list = new LinkedList<>();
list.add("Dog");
list.add("Cat");
list.add(1, "Bird"); // 특정 위치에 삽입
System.out.println(list); // [Dog, Bird, Cat]

(3) Vector(Regacy)

  • 특징: 동기화 지원(스레드 안전). 하지만 요즘 잘 쓰이지 않음
  • 용도: 동시성 환경에서 사용 가능
List<String> list = new Vector<>();
list.add("Red");
list.add("Blue");
System.out.println(list); // [Red, Blue]

3. ArrayList vs LinkedList

비교 항목 ArrayList LinkedList
구조 동적 배열 이중 연결 리스트
삽입/삭제 속도 느림 (데이터 이동 필요) 빠름 (노드 연결만 변경)
탐색 속도 빠름 (O(1), 배열 인덱스 접근) 느림 (O(n), 노드 순회 필요)
메모리 사용량 상대적으로 적음 상대적으로 많음 (노드 추가 메모리 필요)

4. 응용

1) 초기화

List<String> list = new ArrayList<>(Arrays.asList("A", "B", "C"));

2) 정렬

Collections.sort(list); // 오름차순
list.sort(Comparator.reverseOrder()); // 내림차순

3) 필터링

List<String> filtered = list.stream()
    .filter(item -> item.startsWith("A"))
    .collect(Collectors.toList());
System.out.println(filtered); // A로 시작하는 요소

4) 중복 제거

List<String> uniqueList = new ArrayList<>(new HashSet<>(list));

5. List 선택 기준

  • 데이터의 삽입/삭제가 많으면: LinkedList
  • 데이터의 검색/탐색이 많으면: ArrayList
  • 멀티스레드 환경: Vector (하지만 동기화 컬렉션 사용 추천)

6. 연습문제

  1. ArrayListLinkedList의 삽입/삭제/조회 속도를 비교하는 코드를 작성해보세요.
import java.util.ArrayList;
import java.util.LinkedList;
import java.util.List;

public class performance {
    public static void main(String[] args) {
        int size = 100_000; // 테스트 데이터 크기
        List<Integer> arrayList = new ArrayList<>();
        List<Integer> linkedList = new LinkedList<>();

        // 데이터 삽입 성능 측정
        System.out.println("=== 삽입 성능 비교 ===");
        measurePerformance(arrayList, size, "ArrayList 삽입");
        measurePerformance(linkedList, size, "LinkedList 삽입");

        // 데이터 조회 성능 측정
        System.out.println("\n=== 조회 성능 비교 ===");
        measureAccessPerformance(arrayList, size, "ArrayList 조회");
        measureAccessPerformance(linkedList, size, "LinkedList 조회");

        // 데이터 삭제 성능 측정
        System.out.println("\n=== 삭제 성능 비교 ===");
        measureRemovePerformance(arrayList, "ArrayList 삭제");
        measureRemovePerformance(linkedList, "LinkedList 삭제");
    }

    private static void measurePerformance(List<Integer> list, int size, String operation) {
        long start = System.nanoTime();
        for (int i = 0; i < size; i++) {
            list.add(i); // 삽입
        }
        long end = System.nanoTime();
        System.out.println(operation + ": " + (end - start) / 1_000_000 + " ms");
    }

    private static void measureAccessPerformance(List<Integer> list, int size, String operation) {
        long start = System.nanoTime();
        for (int i = 0; i < size; i++) {
            list.get(i); // 조회
        }
        long end = System.nanoTime();
        System.out.println(operation + ": " + (end - start) / 1_000_000 + " ms");
    }

    private static void measureRemovePerformance(List<Integer> list, String operation) {
        long start = System.nanoTime();
        while (!list.isEmpty()) {
            list.remove(0); // 삭제
        }
        long end = System.nanoTime();
        System.out.println(operation + ": " + (end - start) / 1_000_000 + " ms");
    }
}
// 실행 결과
=== 삽입 성능 비교 ===
ArrayList 삽입: 3 ms
LinkedList 삽입: 2 ms

=== 조회 성능 비교 ===
ArrayList 조회: 2 ms
LinkedList 조회: 2987 ms

=== 삭제 성능 비교 ===
ArrayList 삭제: 343 ms
LinkedList 삭제: 1 ms
  1. List를 사용해 중복된 숫자를 제거하고 정렬하는 코드를 구현해보세요.
import java.util.Arrays;
import java.util.List;

public class Performance {
    public static void main(String[] args) {
        List<Integer> numbers = Arrays.asList(5, 3, 8, 3, 1, 8, 9, 1, 2);

        // 중복 제거 및 정렬
        List<Integer> result = numbers.stream()
                .distinct() // 중복 제거
                .sorted()   // 정렬
                .toList();

        System.out.println("중복 제거 및 정렬 결과: " + result);
    }
}
// 실행 결과
중복 제거 및 정렬 결과: [1, 2, 3, 5, 8, 9]

3️⃣ Set

  • Set중복을 허용하지 않는 데이터 구조. 순서가 없고, 중복 허용 안 함
  • null 허용(보통 한 개의 null만 허용)
  • HashSet, LinkedHashSet, TreeSet

1. 주요 구현체

(1) HashSet

  • 순서를 보장하지 않음: 데이터의 삽입 순서를 유지하지 않음
  • 중복 허용 안 함: 같은 값을 다시 추가해도 무시
  • 시간 복잡도: 삽입, 삭제, 검색이 평균적으로 O(1)(해시 충돌이 없을 경우)
  • 내부적으로 HashMap을 사용
Set<String> set = new HashSet<>();
set.add("Apple");
set.add("Banana");
set.add("Apple"); // 중복 추가 시 무시(intellij 기준: 컴파일 수준 경고)
set.add("Cherry");

System.out.println(set); // [Banana, Cherry, Apple] (순서 보장 안 됨)

(2) LinkedHashSet

  • 삽입 순서 유지: 데이터가 추가된 순서대로 저장
  • 중복 허용 안 함
  • HashSet보다는 느리지만 삽입 순서를 유지하는 장점
  • 내부적으로 HashMap + LinkedList 구조를 사용
Set<String> set = new LinkedHashSet<>();
set.add("Cat");
set.add("Dog");
set.add("Cat");
System.out.println(set); // [Cat, Dog] (삽입 순서 유지)

(3) TreeSet

  • 정렬된 상태로 저장: 기본적으로 오름차순 정렬
  • 중복 허용 안 함
  • 삽입, 삭제, 검색이 O(log n).
  • 내부적으로 Red-Black Tree를 사용
  • Comparable 인터페이스 구현 필요(또는 Comparator 제공)
Set<Integer> set = new TreeSet<>();
set.add(5);
set.add(2);
set.add(9);
System.out.println(set); // [2, 5, 9] (오름차순)

2. 비교

특징 HashSet LinkedHashSet TreeSet
순서 보장 여부 순서 보장 안 함 삽입 순서 유지 정렬된 순서 유지
정렬 여부 정렬되지 않음 정렬되지 않음 자동 정렬 (기본: 오름차순)
속도 O(1) (평균) O(1) (평균) O(log n)
구조 HashMap 기반 HashMap + LinkedList 기반 Red-Black Tree 기반
사용 목적 빠른 삽입/삭제/검색 삽입 순서를 유지하며 빠른 작업 정렬된 데이터 유지가 필요할 때
  • HashSet: 순서가 중요하지 않고, 중복을 제거하면서 빠른 속도가 필요할 때
  • LinkedHashSet: 삽입 순서를 유지해야 하면서 중복을 제거할 때
  • TreeSet: 데이터가 항상 정렬된 상태여야 할 때

3. Set 주요 메서드

  • 추가: add(E element)
  • 삭제: remove(Object o)
  • 조회: contains(Object o) (존재 여부 확인)
  • 크기 확인: size()
  • 비우기: clear()
  • 순회: forEach, iterator()
Set<Integer> set = new HashSet<>();
set.add(1);
set.add(2);
System.out.println(set.contains(1)); // true
set.remove(2);
set.forEach(System.out::println); // 1

4. Set 활용

(1) 중복 제거

List<String> list = Arrays.asList("A", "B", "A", "C");
Set<String> set = new HashSet<>(list); // 리스트의 중복 제거
System.out.println(set); // [A, B, C]

(2) 합집합, 교집합, 차집합

Set<Integer> set1 = new HashSet<>(Arrays.asList(1, 2, 3));
Set<Integer> set2 = new HashSet<>(Arrays.asList(2, 3, 4));

// 합집합
Set<Integer> union = new HashSet<>(set1);
union.addAll(set2);
System.out.println("합집합: " + union); // [1, 2, 3, 4]

// 교집합
Set<Integer> intersection = new HashSet<>(set1);
intersection.retainAll(set2);
System.out.println("교집합: " + intersection); // [2, 3]

// 차집합
Set<Integer> difference = new HashSet<>(set1);
difference.removeAll(set2);
System.out.println("차집합: " + difference); // [1]

(3) 정렬된 Set 사용

Set<String> sortedSet = new TreeSet<>(Arrays.asList("Banana", "Apple", "Cherry"));
System.out.println(sortedSet); // [Apple, Banana, Cherry]

6. 연습문제

  1. 두 개의 Set에서 교집합과 차집합을 구하는 코드를 작성해보세요.
import java.util.*;

public class SetOperations {
    public static void main(String[] args) {
        Set<Integer> set1 = new HashSet<>(Arrays.asList(1, 2, 3, 4));
        Set<Integer> set2 = new HashSet<>(Arrays.asList(3, 4, 5, 6));

        // 교집합
        Set<Integer> intersection = new HashSet<>(set1);
        intersection.retainAll(set2); // set1에 set2와 겹치는 요소만 남김
        System.out.println("교집합: " + intersection); // [3, 4]

        // 차집합 (set1 - set2)
        Set<Integer> difference = new HashSet<>(set1);
        difference.removeAll(set2); // set1에서 set2에 있는 요소 제거
        System.out.println("차집합: " + difference); // [1, 2]

        // 합집합
        Set<Integer> union = new HashSet<>(set1);
        union.addAll(set2); // set1과 set2의 모든 요소를 합침
        System.out.println("합집합: " + union); // [1, 2, 3, 4, 5, 6]
    }
}
  1. TreeSet을 사용해 사용자 정의 객체를 정렬하는 코드를 작성해보세요. (예: 이름과 나이가 있는 객체)
import java.util.*;

class Person implements Comparable<Person> {
    private String name;
    private int age;

    public Person(String name, int age) {
        this.name = name;
        this.age = age;
    }

    public String getName() { return name; }
    public int getAge() { return age; }

    @Override
    public int compareTo(Person other) {
        // 나이를 기준으로 오름차순 정렬
        return Integer.compare(this.age, other.age); 
    }

    @Override
    public String toString() {
        return name + " (" + age + ")";
    }
}

public class TreeSetExample {
    public static void main(String[] args) {
        TreeSet<Person> people = new TreeSet<>();
        people.add(new Person("Alice", 30));
        people.add(new Person("Bob", 25));
        people.add(new Person("Charlie", 35));

        System.out.println("TreeSet 정렬 결과:");
        people.forEach(System.out::println);
        // 출력:
        // Bob (25)
        // Alice (30)
        // Charlie (35)
    }
}

좋아요! QueueDeque컬렉션 프레임워크에서 FIFO(First-In-First-Out)양방향 큐를 구현하기 위해 사용됩니다. 각각의 개념과 구현체를 자세히 정리해볼게요.


4️⃣ Queue, Deque

  • QueueFIFO(First-In-First-Out) 방식으로 동작하는 자료구조
  • 즉, 먼저 추가된 요소가 먼저 제거
  • LinkedList (큐와 덱 모두 구현 가능)
  • PriorityQueue (우선순위 큐)

1 Queue 인터페이스의 주요 메서드

메서드 설명
add(E e) 큐의 맨 뒤에 요소를 추가. 큐가 가득 차면 예외 발생 (IllegalStateException)
offer(E e) 큐의 맨 뒤에 요소를 추가. 큐가 가득 차면 예외 대신 false를 반환
remove() 큐의 맨 앞 요소를 제거하고 반환. 큐가 비어 있으면 예외 발생 (NoSuchElementException)
poll() 큐의 맨 앞 요소를 제거하고 반환. 큐가 비어 있으면 null 반환
element() 큐의 맨 앞 요소를 반환. 큐가 비어 있으면 예외 발생
peek() 큐의 맨 앞 요소를 반환. 큐가 비어 있으면 null 반환

2. PriorityQueue

  • 우선순위에 따라 요소를 정렬하며 저장
  • 기본적으로 요소는 자연 순서(Comparable) 또는 Comparator에 따라 정렬됨
  • 내부적으로 힙(heap) 자료구조를 사용
import java.util.PriorityQueue;

public class PriorityQueueExample {
    public static void main(String[] args) {
        PriorityQueue<Integer> pq = new PriorityQueue<>();
        pq.offer(10);
        pq.offer(5);
        pq.offer(15);
        System.out.println("PriorityQueue: " + pq); // 내부 힙 구조 출력
        while (!pq.isEmpty()) {
            System.out.println("Polled: " + pq.poll()); // 우선순위(오름차순)대로 출력
        }
    }
}
PriorityQueue: [5, 10, 15]
Polled: 5
Polled: 10
Polled: 15

3. Deque

  • Deque (Double-Ended Queue)는 양방향으로 삽입과 삭제가 가능한 자료구조
  • FIFOLIFO를 모두 지원
  • ArrayDeque: 배열 기반으로 빠르고 효율적인 덱 구현
  • LinkedList: 노드 기반으로 덱 구현

4. Deque 인터페이스의 주요 메서드

Deque는 양방향으로 동작하기 때문에 Queue와 비슷하지만, 양쪽에서 동작 가능한 추가 메서드를 제공한다.

메서드 설명
addFirst(E e) 덱의 맨 앞에 요소 추가. 큐가 가득 차면 예외 발생.
addLast(E e) 덱의 맨 뒤에 요소 추가. 큐가 가득 차면 예외 발생.
offerFirst(E e) 덱의 맨 앞에 요소 추가. 큐가 가득 차면 false 반환.
offerLast(E e) 덱의 맨 뒤에 요소 추가. 큐가 가득 차면 false 반환.
removeFirst() 덱의 맨 앞 요소 제거 및 반환. 덱이 비어 있으면 예외 발생.
removeLast() 덱의 맨 뒤 요소 제거 및 반환. 덱이 비어 있으면 예외 발생.
pollFirst() 덱의 맨 앞 요소 제거 및 반환. 덱이 비어 있으면 null 반환.
pollLast() 덱의 맨 뒤 요소 제거 및 반환. 덱이 비어 있으면 null 반환.
peekFirst() 덱의 맨 앞 요소 반환. 덱이 비어 있으면 null 반환.
peekLast() 덱의 맨 뒤 요소 반환. 덱이 비어 있으면 null 반환.

5. ArrayDeque

  • ArrayDeque스택 모두로 사용할 수 있는 고성능 클래스
  • 배열 기반으로 동작하며, 동적 크기 조정을 지원
  • null 요소는 허용하지 않음
import java.util.ArrayDeque;

public class ArrayDequeExample {
    public static void main(String[] args) {
        ArrayDeque<String> deque = new ArrayDeque<>();

        deque.offerFirst("First");
        deque.offerLast("Last");
        deque.offerLast("Middle");

        System.out.println("Deque: " + deque);

        System.out.println("Polled First: " + deque.pollFirst());
        System.out.println("Polled Last: " + deque.pollLast());
    }
}
Deque: [First, Last, Middle]
Polled First: First
Polled Last: Middle

6. Queue와 Deque 비교

특징 Queue Deque
방향 단방향 (FIFO) 양방향 (FIFO와 LIFO 모두 가능)
주요 구현체 LinkedList, PriorityQueue ArrayDeque, LinkedList
용도 큐 동작 (예: 작업 대기열) 큐 + 스택 동작

7. 결론

  1. Queue는 FIFO 방식으로 동작하며, PriorityQueueLinkedList가 대표적인 구현체
  2. Deque는 양방향 삽입/삭제가 가능한 자료구조로, ArrayDequeLinkedList가 주로 사용됨
  3. PriorityQueue는 우선순위 정렬을 지원하며, 힙 자료구조를 기반으로 동작
  4. ArrayDeque는 스택과 큐 역할을 모두 수행하는 고성능 클래스

지금까지 설명한 내용이면 QueueDeque의 기본 개념과 주요 구현체를 이해하기에 충분합니다. 그러나 심화 학습 또는 실무에서 활용하려면 다음 추가 내용도 알고 있으면 좋아요.


8. BlockingQueue와 ConcurrentLinkedDeque

BlockingQueue

BlockingQueue는 멀티스레드 환경에서 사용되는 큐로, 생산자-소비자 패턴에 적합하다. 큐가 비어 있을 때 요소를 꺼내거나, 큐가 가득 찼을 때 요소를 추가하려고 하면, 작업이 블로킹된다.

  • Java의 java.util.concurrent 패키지에서 제공
  • ArrayBlockingQueue: 고정 크기의 배열 기반 큐
  • LinkedBlockingQueue: 크기가 제한되거나 무제한인 연결 리스트 기반 큐
  • PriorityBlockingQueue: 우선순위를 지원하는 블로킹 큐
import java.util.concurrent.*;

public class BlockingQueueExample {
    public static void main(String[] args) throws InterruptedException {
        BlockingQueue<String> queue = new ArrayBlockingQueue<>(3);

        queue.put("A"); // 요소 추가 (블로킹)
        queue.put("B");
        queue.put("C");

        System.out.println(queue.take()); // 요소 제거 (블로킹)
    }
}

ConcurrentLinkedDeque

ConcurrentLinkedDeque는 멀티스레드 환경에서 비동기적으로 작동하는 덱

  • 고성능 비블로킹 덱 구현
  • 병렬 처리 시 성능이 뛰어남
import java.util.concurrent.ConcurrentLinkedDeque;

public class ConcurrentDequeExample {
    public static void main(String[] args) {
        ConcurrentLinkedDeque<String> deque = new ConcurrentLinkedDeque<>();

        deque.offerFirst("First");
        deque.offerLast("Last");

        System.out.println(deque.pollFirst()); // First
        System.out.println(deque.pollLast());  // Last
    }
}

9. 우선순위 큐의 Comparator 활용

  • PriorityQueue자연 순서(Comparable)를 기본으로 사용
  • 커스텀 정렬: Comparator를 사용해 우선순위를 커스터마이징 가능
import java.util.*;

public class CustomPriorityQueue {
    public static void main(String[] args) {
        PriorityQueue<String> pq = new PriorityQueue<>(Comparator.reverseOrder());

        pq.offer("A");
        pq.offer("B");
        pq.offer("C");

        while (!pq.isEmpty()) {
            System.out.println(pq.poll()); // C, B, A (내림차순)
        }
    }
}

10. 덱(Deque) 활용: 스택 대체

  • Deque스택(LIFO)의 대안으로 사용
  • ArrayDeque는 스택보다 효율적이며, Stack 클래스는 더 이상 권장되지 않음
  • import java.util.ArrayDeque; public class StackExample { public static void main(String[] args) { ArrayDeque<String> stack = new ArrayDeque<>(); stack.push("A"); stack.push("B"); stack.push("C"); while (!stack.isEmpty()) { System.out.println(stack.pop()); // C, B, A (LIFO) } } }

11. 큐의 크기 제한

  • ArrayDequePriorityQueue는 기본적으로 크기 제한이 없음
  • 크기 제한이 필요한 경우:
    • BlockingQueue: 큐가 가득 찼을 때 블로킹 동작
    • 사용자 정의 크기 제한
    • import java.util.*; public class LimitedQueue<E> extends LinkedList<E> { private final int limit; public LimitedQueue(int limit) { this.limit = limit; } @Override public boolean add(E e) { if (size() >= limit) { removeFirst(); // 가장 오래된 요소 제거 } return super.add(e); } } public class LimitedQueueExample { public static void main(String[] args) { LimitedQueue<String> queue = new LimitedQueue<>(3); queue.add("A"); queue.add("B"); queue.add("C"); queue.add("D"); // "A" 제거됨 System.out.println(queue); // [B, C, D] } }

12. 큐와 덱 활용 사례

Queue 활용 사례

작업 대기열
작업을 순서대로 처리해야 할 때 사용 (예: 프린터 작업 대기열).

BFS(너비 우선 탐색)
그래프 탐색 알고리즘에서 사용

Deque 활용 사례

양방향 탐색
앞뒤로 데이터를 추가하거나 제거할 필요가 있을 때 사용

최대값/최소값 문제
특정 범위에서 최대값 또는 최소값을 효율적으로 계산


13. 성능 비교

특징 LinkedList ArrayDeque PriorityQueue
구조 노드 기반 배열 기반 힙(heap) 기반
삽입/삭제 O(1) (앞뒤 삽입/삭제) O(1) (앞뒤 삽입/삭제) O(log n) (정렬 필요)
임의 접근 O(n) O(n) O(n)
정렬 유지 안 됨 유지 안 됨 유지됨 (우선순위 기준)

14. 결론

지금까지 학습한 내용은 QueueDeque의 기본 개념부터 주요 구현체의 활용까지 포함한다. 심화 학습으로 다음과 같다.

  1. BlockingQueueConcurrentLinkedDeque로 멀티스레드 환경을 고려한 큐를 학습
  2. 우선순위 큐의 Comparator를 활용한 커스텀 정렬
  3. 스택 대체로 ArrayDeque를 사용하는 패턴

5️⃣Hash, Map

1. Hash

Hash는 컴퓨터 과학에서 데이터를 고유한 값(해시 값)으로 변환하는 과정 또는 그 결과를 의미한다. Hashing은 주어진 입력 데이터를 특정 알고리즘을 사용해 고정된 크기의 숫자나 문자열로 변환하는 과정을 말하며, 자바에서 주로 해시 기반 자료구조에서 사용된다.


2 Hash의 핵심 개념

  1. 해시 함수(Hash Function)
    • 데이터를 입력받아 고정된 크기의 해시 값을 출력하는 함수
    • 동일한 입력값에 대해 항상 동일한 해시 값을 반환
    • 자바에서는 hashCode() 메서드가 해시 값을 생성
  2. 해시 값(Hash Value)
    • 해시 함수에 의해 생성된 고정된 크기의 정수 값(또는 문자열)
    • 해시 값은 데이터의 고유 식별자 역할을 하며, 주로 저장 위치를 결정하는 데 사용된다
  3. 해싱(Hashing)
    • 데이터를 해시 값을 이용해 빠르게 검색, 삽입, 삭제하는 기술

3. Hash가 사용되는 이유

  1. 빠른 데이터 검색
    해시 값은 데이터의 위치를 계산하기 위한 인덱스로 사용되므로, 데이터 검색이 매우 빠르다. (평균 O(1))
  2. 효율적인 데이터 저장
    데이터를 해시 테이블(Hash Table)에 저장하여, 공간을 효율적으로 사용하고 검색 시간을 줄인다

4. 자바에서 Hash의 활용

자바 컬렉션 프레임워크에서 Hash는 다음과 같은 자료구조에서 사용된다

  1. HashMap
    • 키의 해시 값을 기반으로 값을 저장
    • 해시 값 충돌 시 체이닝(Linked List) 또는 트리화로 처리
  2. HashSet
    • 중복을 허용하지 않는 집합 자료구조
    • 내부적으로 HashMap을 사용하여 요소를 저장
  3. Hashtable
    • 동기화된 해시 기반 맵(이제 잘 사용되지 않음)

5. HashMap의 동작 원리

해시 테이블 구조

  • 해시 테이블버킷(bucket)으로 구성된 배열
  • 데이터는 해시 함수를 사용해 적절한 버킷에 저장

작동 과정

  1. 저장 (put)
    • key의 해시 값을 계산하고, 이를 기반으로 버킷의 위치를 결정
    • 같은 위치에 여러 데이터가 충돌하면 체이닝(Linked List) 또는 트리로 관리
  2. 검색 (get)
    • key의 해시 값을 계산하고, 해당 버킷에서 값을 찾는다

코드 예제

import java.util.HashMap;

public class HashMapExample {
    public static void main(String[] args) {
        HashMap<String, Integer> map = new HashMap<>();

        // 데이터 저장
        map.put("Apple", 1);
        map.put("Banana", 2);

        // 데이터 검색
        System.out.println(map.get("Apple")); // 1

        // 해시 값 확인 -> HashCode for 'Apple': 63476538
        System.out.println("HashCode for 'Apple': " + "Apple".hashCode());
    }
}

5. 해시 충돌(Hash Collision)

충돌 발생

  • 두 개의 다른 입력값이 동일한 해시 값을 가질 때 발생
  • 예: hashCode("ABC") == hashCode("XYZ")

충돌 해결 방법

  1. 체이닝(Chaining)
    • 같은 해시 값을 가진 데이터를 Linked List로 저장
    • 자바의 HashMap은 체이닝 방식을 사용
  2. 오픈 어드레싱(Open Addressing)
    • 충돌이 발생하면 빈 공간을 찾아 데이터를 저장

6. 자바의 hashCode()equals()

hashCode()

  • 객체의 해시 값을 반환하는 메서드
  • 동일한 객체(equals()true)라면 같은 해시 값을 반환해야 함

equals()

  • 두 객체가 논리적으로 같은지를 비교하는 메서드

사용 예제

import java.util.HashMap;

class Person {
    private String name;

    public Person(String name) {
        this.name = name;
    }

    @Override
    public int hashCode() {
        return name.hashCode(); // 이름의 해시 값 반환
    }

    @Override
    public boolean equals(Object obj) {
        if (this == obj) return true;
        if (obj instanceof Person) {
            Person other = (Person) obj;
            return this.name.equals(other.name); // 이름으로 비교
        }
        return false;
    }
}

public class HashExample {
    public static void main(String[] args) {
        HashMap<Person, String> map = new HashMap<>();

        Person p1 = new Person("Alice");
        Person p2 = new Person("Alice");

        map.put(p1, "Developer");
        System.out.println(map.get(p2)); // Developer (p1과 p2가 동일한 객체로 간주됨)
    }
}

7. 요약

  1. Hash
    • 데이터를 고정된 크기의 고유 값(해시 값)으로 변환
    • 빠른 검색, 삽입, 삭제를 지원
  2. 해시 충돌
    • 두 개 이상의 입력값이 동일한 해시 값을 가질 때 발생
    • 체이닝 또는 오픈 어드레싱으로 해결
  3. 자바에서 Hash 사용
    • HashMap, HashSet, Hashtable에서 활용
    • hashCode()equals()를 올바르게 구현해야 정확히 동작

8. Map 특징

Map키와 값(key-value pair) 형태로 데이터를 저장하는 자료구조로, 컬렉션 프레임워크에서 키를 기반으로 데이터를 검색, 추가, 삭제하는 데 사용된다. Map의 기본 개념과 주요 구현체를 차근차근 정리한다.


9. Map의 기본 개념

특징

  1. 키와 값의 쌍으로 데이터 저장
    • 는 고유해야 하며, 중복을 허용하지 않음
    • 은 중복을 허용
  2. null 처리
    • 일부 구현체(HashMap, LinkedHashMap)는 null 키와 null 값을 허용
    • TreeMapnull 키를 허용하지 않음
  3. 주요 인터페이스 메서드
    • put(K key, V value): 키-값 쌍 추가
    • get(Object key): 키에 해당하는 값 반환
    • remove(Object key): 키에 해당하는 값 삭제
    • containsKey(Object key): 키가 존재하는지 확인
    • containsValue(Object value): 값이 존재하는지 확인
    • keySet(): 모든 키를 Set으로 반환
    • values(): 모든 값을 Collection으로 반환
    • entrySet(): 키-값 쌍을 Set<Map.Entry<K, V>>로 반환

10. Map의 주요 구현체

구현체 특징
HashMap - 키-값 저장 순서를 보장하지 않음
- 해시 기반으로 빠른 검색 성능(O(1))
- null 키와 null 값을 허용
LinkedHashMap - 삽입 순서를 유지
- HashMap과 동일한 시간 복잡도(O(1))
TreeMap - 키를 정렬된 상태로 유지
- null 키를 허용하지 않음
- 시간 복잡도: O(log n)
Hashtable - 동기화된 HashMap
- null 키와 null 값을 허용하지 않음
ConcurrentHashMap - 멀티스레드 환경에서 동기화를 지원
- null 키와 null 값을 허용하지 않음

11. Map 구현체별 특징

(1) HashMap

  • 빠른 데이터 검색과 삽입/삭제.
  • 해시 기반 저장 구조
  • null 키 1개, null 값 여러 개 허용
import java.util.HashMap;

public class HashMapExample {
    public static void main(String[] args) {
        HashMap<String, Integer> map = new HashMap<>();
        map.put("Apple", 1);
        map.put("Banana", 2);
        map.put("Cherry", 3);

        System.out.println("Value for 'Apple': " + map.get("Apple")); // 1
        map.remove("Banana");
        System.out.println("Keys: " + map.keySet()); // [Apple, Cherry]
        System.out.println("Values: " + map.values()); // [1, 3]
    }
}

(2) LinkedHashMap

  • 삽입 순서를 유지
  • HashMap보다 메모리 사용량이 다소 높음
import java.util.LinkedHashMap;

public class LinkedHashMapExample {
    public static void main(String[] args) {
        LinkedHashMap<String, Integer> map = new LinkedHashMap<>();
        map.put("Banana", 1);
        map.put("Apple", 2);
        map.put("Cherry", 3);

        System.out.println("LinkedHashMap: " + map); // {Banana=1, Apple=2, Cherry=3}
    }
}

(3) TreeMap

  • 키를 정렬된 상태로 유지 (기본적으로 오름차순)
  • null 키는 허용하지 않음
  • Comparator를 사용해 커스텀 정렬 가능
import java.util.TreeMap;

public class TreeMapExample {
    public static void main(String[] args) {
        TreeMap<String, Integer> map = new TreeMap<>();
        map.put("Banana", 1);
        map.put("Apple", 2);
        map.put("Cherry", 3);

        System.out.println("TreeMap: " + map); // {Apple=2, Banana=1, Cherry=3}
    }
}

(4) ConcurrentHashMap

  • 멀티스레드 환경에서 동기화를 지원
  • Hashtable보다 효율적이고, null 키와 null 값을 허용하지 않음
import java.util.concurrent.ConcurrentHashMap;

public class ConcurrentHashMapExample {
    public static void main(String[] args) {
        ConcurrentHashMap<String, Integer> map = new ConcurrentHashMap<>();
        map.put("Apple", 1);
        map.put("Banana", 2);
        System.out.println(map);
    }
}

12. Map 주요 메서드 활용

import java.util.*;

public class MapMethodsExample {
    public static void main(String[] args) {
        Map<String, Integer> map = new HashMap<>();
        map.put("Apple", 1);
        map.put("Banana", 2);
        map.put("Cherry", 3);

        // 1. 키로 값 가져오기
        System.out.println("Value for 'Apple': " + map.get("Apple")); // 1

        // 2. 키 확인
        System.out.println("Contains 'Banana': " + map.containsKey("Banana")); // true

        // 3. 값 확인
        System.out.println("Contains value 3: " + map.containsValue(3)); // true

        // 4. 모든 키와 값 출력
        System.out.println("Keys: " + map.keySet()); // [Apple, Banana, Cherry]
        System.out.println("Values: " + map.values()); // [1, 2, 3]

        // 5. 키-값 쌍 반복
        for (Map.Entry<String, Integer> entry : map.entrySet()) {
            System.out.println(entry.getKey() + " -> " + entry.getValue());
        }
    }
}

13. Map 활용 사례

  1. 키워드와 빈도수 계산
  2. Map<String, Integer> wordCount = new HashMap<>(); String[] words = {"apple", "banana", "apple", "cherry"}; for (String word : words) { wordCount.put(word, wordCount.getOrDefault(word, 0) + 1); } System.out.println(wordCount); // {apple=2, banana=1, cherry=1}
  3. TreeMap으로 정렬된 출력
  4. Map<String, Integer> map = new TreeMap<>(Comparator.reverseOrder()); map.put("A", 1); map.put("C", 2); map.put("B", 3); System.out.println(map); // {C=2, B=3, A=1}

14. Map 학습 순서

  1. HashMapLinkedHashMapTreeMap 순서로 학습
  2. 멀티스레드 환경을 고려한 ConcurrentHashMap 이해
  3. Map 주요 메서드(put, get, entrySet) 실습
  4. Map 활용 예제(키워드 빈도수 계산, 정렬된 출력) 실습

버킷(bucket)해시 테이블에서 데이터를 저장하는 슬롯 또는 저장 공간을 의미한다.

슬롯(Slot)

  • 해시 테이블 배열에서 하나의 인덱스를 의미
  • 슬롯은 데이터를 저장하는 물리적 공간으로, 하나의 버킷을 포함

버킷(Bucket)

  • 하나의 슬롯이 버킷 역할을 하며, 데이터를 저장하는 논리적 단위
  • 충돌이 없는 경우, 버킷에 단일 데이터만 저장
  • 충돌이 발생하면, 버킷에 연결 리스트(Linked List) 또는 트리(Tree) 형태로 데이터를 추가로 저장

해시 테이블의 핵심은 데이터를 특정 위치에 저장하는 것이고, 그 위치를 해시 값(hash value)으로 계산합니다. 버킷은 이렇게 계산된 위치에 데이터를 저장하거나, 충돌이 발생했을 때 데이터를 그룹화하여 관리하는 단위로 사용된다.


15. 버킷의 역할

image-20241231113828312

위 그림은 해시 테이블의 버킷 구조를 시각적으로 표현한 것이다.

  • 버킷은 배열의 하나의 슬롯을 의미
  • 데이터는 해시 함수를 통해 버킷에 저장
  • Bucket 0: 충돌로 인해 두 개의 데이터(Key1:Value1, Key3:Value3)가 체이닝으로 저장
  • Bucket 1: 하나의 데이터(Key2:Value2)만 저장
  • Bucket 2: 비어 있음
  • Bucket 3: 충돌로 두 개의 데이터(Key4:Value4, Key5:Value5) 저장
  • Bucket 4: 하나의 데이터(Key6:Value6) 저장

이런 식으로 해시 테이블은 데이터를 버킷에 나눠 저장하며, 충돌이 발생한 경우에는 체이닝 방식으로 데이터를 관리한다.

  1. 데이터 저장 공간
    • 해시 함수가 계산한 해시 값에 따라 데이터를 저장하는 공간
    • 해시 테이블은 내부적으로 배열로 구현되며, 각 배열 요소가 버킷 역할
  2. 해시 충돌 처리
    • 서로 다른 키가 동일한 해시 값을 가질 때, 충돌이 발생
    • 충돌이 발생한 데이터를 저장하고 관리하기 위해 버킷이 활용

정리하자면 해시 테이블(Hash Table) 구조는 내부적으로 배열 기반 구조로, 각 배열 요소가 버킷(bucket) 역할을 한다. 해시 함수는 키의 해시 값을 계산하고, 배열의 인덱스를 결정하여 데이터를 해당 버킷에 저장한다.


16. 해시 테이블에서 버킷 동작 방식

(1) 해시 함수와 버킷 매핑

  1. 해시 함수가 키의 해시 값을 계산
  2. 이 해시 값을 배열의 인덱스로 변환하여, 데이터가 저장될 버킷을 결정

(2) 버킷과 충돌 처리

  • 충돌이 발생하면 버킷은 하나의 데이터만 저장하지 않고, 같은 해시 값을 가지는 여러 데이터를 관리
  • 데이터 관리 방법
    1. 체이닝(Chaining): 버킷에 Linked List 또는 Tree 구조로 데이터를 저장
    2. 오픈 어드레싱(Open Addressing): 빈 공간을 찾아 데이터를 저장

17. 버킷 동작 방식 예제

해시 함수 예제

// 해시 함수 예시: 해시 값을 배열 크기로 나눈 나머지를 사용
int hashCode = key.hashCode(); // 키의 해시 값
int bucketIndex = hashCode % bucketArray.length; // 버킷 인덱스 결정

버킷 구조 예제

// 배열 기반 버킷
class HashTable {
    private LinkedList<Entry>[] buckets; // 각 버킷은 LinkedList로 구현

    public HashTable(int size) {
        buckets = new LinkedList[size];
        for (int i = 0; i < size; i++) {
            buckets[i] = new LinkedList<>();
        }
    }

    public void put(String key, String value) {
        int bucketIndex = key.hashCode() % buckets.length;
        buckets[bucketIndex].add(new Entry(key, value));
    }

    public String get(String key) {
        int bucketIndex = key.hashCode() % buckets.length;
        for (Entry entry : buckets[bucketIndex]) {
            if (entry.key.equals(key)) {
                return entry.value;
            }
        }
        return null; // 키가 없는 경우
    }

    static class Entry {
        String key;
        String value;

        Entry(String key, String value) {
            this.key = key;
            this.value = value;
        }
    }
}

18. 자바의 버킷 동작 방식 (HashMap 예제)

HashMap의 내부 구조

  1. 버킷 배열
    HashMap은 내부적으로 배열(Node<K,V>[] table)을 사용해 버킷을 관리
  2. Node
    각 버킷은 Node 객체(키-값 쌍) 또는 Linked List로 관리

HashMap 데이터 저장 과정

  1. 키의 해시 값 계산
    hashCode() 메서드로 키의 해시 값을 계산
  2. 버킷 인덱스 결정
    해시 값에 배열 크기를 적용해 인덱스(hash % bucketArray.length)를 계산
  3. 데이터 저장
    해당 인덱스에 버킷이 존재하면, 충돌을 처리하며 데이터를 추가
import java.util.HashMap;

public class HashMapBucketExample {
    public static void main(String[] args) {
        HashMap<String, Integer> map = new HashMap<>();

        // 데이터 추가
        map.put("Apple", 1);
        map.put("Banana", 2);
        map.put("Cherry", 3);

        // HashMap 내부 동작 확인
        System.out.println("HashCode for 'Apple': " + "Apple".hashCode());
        System.out.println("HashCode for 'Banana': " + "Banana".hashCode());
        System.out.println("HashCode for 'Cherry': " + "Cherry".hashCode());
    }
}
// 실행 결과
HashCode for 'Apple': 63476538
HashCode for 'Banana': 1982479237
HashCode for 'Cherry': 2017321401

19. 버킷 관련 주요 개념

  1. 버킷 크기
    • 해시 테이블의 배열 크기를 결정하며, 초기 용량(default capacity)확장 기준(load factor)에 따라 변경
  2. 해시 충돌
    • 서로 다른 키가 동일한 버킷에 할당되는 경우 발생
    • 체이닝(Linked List)이나 트리(Tree)로 충돌 해결
  3. 리사이징(Resizing)
    • 해시 테이블의 사용량이 일정 수준을 넘으면 배열 크기를 두 배로 늘려 버킷 크기를 확장
    • 예: HashMap의 기본 load factor는 0.75

20. 충돌 처리 방법

(1) 체이닝(Chaining)
동일한 버킷에 저장될 데이터가 충돌하면, 해당 버킷에서 데이터를 연결 리스트(Linked List)로 관리한다. 즉, 버킷이 단일 요소를 저장하는 대신, 충돌된 모든 데이터를 연결 리스트로 저장한다.

체이닝의 동작 과정

  1. 새로운 데이터가 동일한 버킷에 매핑되면, 리스트 끝에 추가
  2. 검색 시, 키의 해시 값을 기반으로 버킷을 찾고, 해당 리스트에서 일치하는 키를 검색
해시 함수 결과:
Key1 → 버킷 0
Key2 → 버킷 0

버킷 0의 상태:
[Key1: Value1] → [Key2: Value2]

(2) 트리화(Treeification)
충돌로 인해 체이닝의 연결 리스트가 너무 길어지는 경우, 연결 리스트를 이진 검색 트리(Tree)로 변환한다. Java 8부터 도입된 이 최적화는 검색 속도(O(log n))를 크게 향상시킨다.

  • Treeification 조건
    • 연결 리스트의 길이가 기본적으로 8개 이상이 되면 트리로 변환
    • 연결 리스트의 길이가 다시 줄어들면 트리에서 리스트로 복원

트리화의 장점
충돌이 많은 경우에도 일정한 성능을 유지할 수 있다

(3) 오픈 어드레싱(Open Addressing) (HashMap은 사용하지 않음)
자바의 HashMap은 체이닝 방식을 사용하지만, 일부 다른 해시 테이블 구현에서는 오픈 어드레싱 방식을 사용할 수 있다. 오픈 어드레싱은 충돌이 발생하면 다른 빈 버킷을 찾아 데이터를 저장한다.


21. HashMap 충돌 처리 예제

충돌 없는 경우
각 키가 고유한 해시 값을 가지면, 데이터는 개별 버킷에 저장된다

충돌이 발생하는 경우
동일한 해시 값을 가지는 키가 있으면, 같은 버킷에 추가 데이터가 저장된다.
체이닝(Chaining) 방식으로 관리된다.(여러 값이 같은 버킷에 저장되며, 이를 연결 리스트 형태로 관리한다.)

import java.util.HashMap;

public class HashTableExample {
    public static void main(String[] args) {
        // HashMap 생성
        HashMap<Integer, String> map = new HashMap<>();

        // 데이터 추가
        map.put(1, "Value1"); // Key: 1
        map.put(17, "Value2"); // Key: 17 (충돌 발생)

        // 해시 코드 확인 (충돌 예시)
        System.out.println("HashCode for Key 1: " + (1 % 16)); // 버킷 1
        System.out.println("HashCode for Key 17: " + (17 % 16)); // 버킷 1

        // 값 확인
        System.out.println("Value for Key 1: " + map.get(1)); // Value1
        System.out.println("Value for Key 17: " + map.get(17)); // Value2
    }
}
// 실행 결과
HashCode for Key 1: 1
HashCode for Key 17: 1
Value for Key 1: Value1
Value for Key 17: Value2

22. HashMap의 리사이징(Resizing)
해시 테이블의 버킷 배열 크기는 초기값이 16이며, 데이터가 일정 수준 이상 채워지면 2배로 확장된다. 로드 팩터(Load Factor)의 기본 값은 0.75로, 버킷의 75%가 차면 리사이징이 발생한다. 리사이징 시 모든 데이터를 다시 해싱하여 새로운 버킷에 재배치한다.


23. 요약

  1. HashMap은 기본적으로 체이닝을 사용하여 충돌을 처리하며, 필요에 따라 트리화(Treeification)를 통해 성능을 최적화
  2. 체이닝은 충돌이 발생한 데이터를 연결 리스트로 관리하며, 리스트가 길어지면 트리로 변환
  3. 충돌을 최소화하려면 해시 함수의 품질이 중요
  4. 슬롯(Slot)은 해시 테이블 배열의 인덱스를 의미하며, 각 슬롯은 버킷(bucket)을 포함
  5. 버킷은 데이터를 저장하는 논리적 단위로, 충돌이 발생하지 않으면 단일 값을 저장

충돌이 발생하면:

  • 체이닝을 사용하여 같은 버킷에 여러 데이터를 저장
  • 동일한 키가 아니면 같은 버킷에 다른 값을 추가로 저장 가능
  • 버킷은 해시 테이블에서 데이터를 저장하는 슬롯(저장 공간)
  • 해시 함수가 키의 해시 값을 계산하고, 해당 버킷의 인덱스를 결정
  • 해시 충돌이 발생하면 체이닝(Linked List) 또는 오픈 어드레싱을 통해 데이터를 관리
  • 자바의 HashMap은 배열 기반으로 버킷을 관리하며, 각 버킷은 Node 또는 Linked List로 구현

6️⃣ Comparable, Comparator

  • Comparable은 자바에서 객체의 기본 정렬 기준을 정의하는 인터페이스
  • 사용자 정의 객체를 정렬할 때 사용하는 핵심 요소

1. Comparable 특징

  • 정렬 기준 제공: 객체를 비교해 크기(순서)를 결정
  • 정렬 방식: 정렬 대상 클래스가 스스로 비교 기준을 정의
  • 필수 메서드: compareTo(T o). T는 비교할 객체의 타입
  • 정렬이 필요하거나, 정렬된 컬렉션(TreeSet, TreeMap)에서 사용할 때 유용

Comparable 사용법

  • implements Comparable<T>
  • T: 비교 대상의 타입을 지정. 보통 클래스 자신을 지정
  • compareTo(T o) 메서드 오버라이딩
  • 두 객체를 비교하는 로직을 compareTo 메서드에 작성

compareTo 메서드 동작

compareTo는 정렬 기준을 정의하는 메서드로, 다음 값을 반환한다

  • 음수: 현재 객체가 비교 대상보다 작음양수: 현재 객체가 비교 대상보다 큼
  • 0: 현재 객체와 비교 대상이 같음
@Override
public int compareTo(T o) {
    // 비교 로직 구현
    return this.기준값 - o.기준값; // 숫자 비교 시 사용 가능
}

2. 연습문제

  1. 문자열을 길이로 정렬하고 싶다면 다음과 같이 작성한다
class StringLengthComparator implements Comparable<String> {
    private String value;

    public StringLengthComparator(String value) {
        this.value = value;
    }

    @Override
    public int compareTo(String other) {
        return Integer.compare(this.value.length(), other.length());
    }

    @Override
    public String toString() {
        return value;
    }
}

(1) TreeSet과 함께 사용

TreeSet은 삽입과 동시에 정렬되기 때문에 Comparable이 필요하다

TreeSet<Person> peopleSet = new TreeSet<>();
peopleSet.add(new Person("Alice", 30));
peopleSet.add(new Person("Bob", 25));
peopleSet.add(new Person("Charlie", 35));

peopleSet.forEach(System.out::println);
// 출력: Bob (25), Alice (30), Charlie (35)

(2) List와 함께 사용

Collections.sort() 메서드는 Comparable 인터페이스를 구현한 클래스에서 사용할 수 있다


3. 정리

  1. Comparable은 클래스 자체에 기본 정렬 기준을 정의하고 싶을 때 사용
  2. compareTo 메서드를 통해 크기 비교를 구현
  3. 한 가지 기준만 지원하므로, 다양한 정렬 기준이 필요하면 Comparator

4. Comparator

  • Comparator는 자바에서 객체의 정렬 기준을 외부에서 정의하는 인터페이스

Comparable이 기본 정렬 기준을 객체 내부에서 지정한다면, Comparator는 여러 정렬 기준을 유연하게 정의할 수 있다. 이를 통해 동일한 객체를 다양한 기준으로 정렬할 수 있다.


5. Comparator의 특징

  • 외부에서 정렬 기준 정의
    • 클래스의 내부를 수정하지 않고도 정렬 기준을 제공
    • 동일한 클래스에 대해 여러 정렬 기준을 적용 가능
  • 메서드:compare(T o1, T o2). 두 객체를 비교
    • 음수: o1이 o2보다 작다.
    • 0: o1과 o2가 같다.
    • 양수: o1이 o2보다 크다.

6. Comparator 구현 방법

(1) 기본 구현

Comparator 인터페이스를 직접 구현한 클래스를 작성

import java.util.*;

class Person {
    private String name;
    private int age;

    public Person(String name, int age) {
        this.name = name;
        this.age = age;
    }

    public String getName() {
        return name;
    }

    public int getAge() {
        return age;
    }

    @Override
    public String toString() {
        return name + " (" + age + ")";
    }
}

class AgeComparator implements Comparator<Person> {
    @Override
    public int compare(Person p1, Person p2) {
        return Integer.compare(p1.getAge(), p2.getAge()); // 나이 오름차순
    }
}

public class ComparatorExample {
    public static void main(String[] args) {
        List<Person> people = new ArrayList<>();
        people.add(new Person("Alice", 30));
        people.add(new Person("Bob", 25));
        people.add(new Person("Charlie", 35));

        // Comparator를 사용하여 정렬
        people.sort(new AgeComparator());
        people.forEach(System.out::println);
        // 출력: Bob (25), Alice (30), Charlie (35)
    }
}

(2) 익명 클래스 사용

클래스를 따로 정의하지 않고, 익명 클래스를 사용해 정렬 기준을 정의할 수 있다.

people.sort(new Comparator<Person>() {
    @Override
    public int compare(Person p1, Person p2) {
        return p1.getName().compareTo(p2.getName()); // 이름 오름차순
    }
});

(3) 람다식 사용 (Java 8+)

람다를 사용하면 Comparator를 간결하게 정의할 수 있다.

people.sort((p1, p2) -> p2.getAge() - p1.getAge()); // 나이 내림차순

7. Comparator와 Comparable 비교

특징 Comparable Comparator
정렬 기준 정의 위치 클래스 내부 클래스 외부
유연성 한 가지 정렬 기준만 가능 여러 정렬 기준 제공 가능
메서드 이름 compareTo(T o) compare(T o1, T o2)
적용 방법 Collections.sort(list) list.sort(new Comparator<T>())

8. Comparator의 활용

(1) 여러 정렬 기준 사용

// 이름 기준 정렬
Comparator<Person> nameComparator = (p1, p2) -> p1.getName().compareTo(p2.getName());

// 나이 기준 정렬
Comparator<Person> ageComparator = (p1, p2) -> Integer.compare(p1.getAge(), p2.getAge());

Collections.sort(people, nameComparator); // 이름 오름차순
Collections.sort(people, ageComparator.reversed()); // 나이 내림차순

(2) 복합 정렬 기준

여러 정렬 기준을 조합할 수도 있다.

Comparator<Person> combinedComparator = Comparator
    .comparing(Person::getAge)         // 나이 기준 정렬
    .thenComparing(Person::getName);   // 나이가 같으면 이름 기준 정렬

people.sort(combinedComparator);

9. 실습 문제

  1. Comparator를 사용해 다음 기준으로 사람을 정렬해보세요.
    • 나이 오름차순
    • 이름 내림차순
    • 나이와 이름을 조합한 복합 기준

(1) 나이 오름차순

import java.util.*;

class Person {
    private String name;
    private int age;

    public Person(String name, int age) {
        this.name = name;
        this.age = age;
    }

    public String getName() {return name;}
    public int getAge() {return age;}

    @Override
    public String toString() {
        return name + " (" + age + ")";
    }
}

public class ComparatorExample {
    public static void main(String[] args) {
        List<Person> people = new ArrayList<>();
        people.add(new Person("Alice", 30));
        people.add(new Person("Bob", 25));
        people.add(new Person("Charlie", 35));

        // 나이 오름차순 정렬
        people.sort(Comparator.comparing(Person::getAge));

        System.out.println("나이 오름차순:");
        people.forEach(System.out::println);
    }
}
// 실행 결과
나이 오름차순:
Bob (25)
Alice (30)
Charlie (35)

(2) 이름 내림차순

// 이름 내림차순 정렬
people.sort((p1, p2) -> p2.getName().compareTo(p1.getName()));

System.out.println("\n이름 내림차순:");
people.forEach(System.out::println);
// 실행 결과
이름 내림차순:
Charlie (35)
Bob (25)
Alice (30)

(3) 나이와 이름 복합 기준

// 복합 정렬: 나이 오름차순, 나이가 같으면 이름 오름차순
people.sort(Comparator
    .comparing(Person::getAge)
    .thenComparing(Person::getName));

System.out.println("\n복합 정렬 (나이 오름차순 + 이름 오름차순):");
people.forEach(System.out::println);
// 실행 결과
복합 정렬 (나이 오름차순 + 이름 오름차순):
Bob (25)
Alice (30)
Charlie (35)
  1. TreeSet에 Comparator를 적용해 사용자 정의 객체를 정렬하세요.

TreeSet은 삽입 시 정렬이 이루어지므로, Comparator를 생성자에서 제공해야 한다.

import java.util.*;

public class TreeSetWithComparator {
    public static void main(String[] args) {
        // TreeSet에 나이 내림차순 Comparator 적용
        TreeSet<Person> peopleSet = new TreeSet<>(Comparator.comparing(Person::getAge).reversed());

        peopleSet.add(new Person("Alice", 30));
        peopleSet.add(new Person("Bob", 25));
        peopleSet.add(new Person("Charlie", 35));

        System.out.println("TreeSet 정렬 (나이 내림차순):");
        peopleSet.forEach(System.out::println);
    }
}
TreeSet 정렬 (나이 내림차순):
Charlie (35)
Alice (30)
Bob (25)

10. 정리

  1. Comparator는 유연한 정렬 기준을 제공하며, 람다식이나 메서드 참조로 간결하게 작성 가능
  2. TreeSet에 Comparator를 적용하면 삽입과 동시에 정렬 가능
  3. 복합 정렬 기준은 Comparator.comparingthenComparing을 활용

11. Comparable과 Comparator 비교

Comparable

  • 클래스 내부에서 정렬 기준 정의
  • compareTo(T o) 메서드 사용
  • 한 가지 정렬 기준만 가능

Comparator

  • 클래스 외부에서 정렬 기준 정의
  • compare(T o1, T o2) 메서드 사용
  • 다양한 정렬 기준 제공 가능
Collections.sort(people, new Comparator<Person>() {
    @Override
    public int compare(Person p1, Person p2) {
        return p1.getName().compareTo(p2.getName()); // 이름 기준 정렬
    }
});

7️⃣ Stream, Collector

Stream은 Java 8에서 도입된 강력한 기능으로, 컬렉션 또는 배열과 같은 데이터 소스를 효율적이고 선언적으로 처리할 수 있게 한다. Stream API는 데이터를 반복(iterate), 필터링(filter), 매핑(map), 집계(aggregate) 등의 작업을 간단하고 가독성 좋게 처리할 수 있다.


1. Stream이란?

  • Stream은 데이터 소스를 변경하지 않고, 데이터를 순차적 또는 병렬적으로 처리하기 위한 도구
  • 데이터 파이프라인으로 데이터를 필터링하고 변환하며, 최종적으로 결과를 생성

2. 특징

  • 선언형 프로그래밍
    기존의 반복문(명령형 프로그래밍)과 달리, 데이터 처리의 목적을 간결하게 표현
  • 비파괴적
    원본 데이터 소스를 변경하지 않음
  • 지연 연산
    중간 연산(filter, map 등)은 최종 연산(collect, forEach 등)이 호출되기 전까지 실행되지 않음
  • 병렬 처리 가능
    parallelStream을 통해 멀티코어 환경에서 병렬 작업을 수행할 수 있음

3. Stream의 구성 요소

(1) 데이터 소스

  • 컬렉션(List, Set, Map), 배열, 파일, 또는 생성된 데이터

(2) 중간 연산 (Intermediate Operations)

  • 스트림을 변환하거나 필터링한다. 결과는 또 다른 스트림.
  • 대표적인 연산
    • filter: 조건에 맞는 요소만 통과
    • map: 데이터를 변환
    • sorted: 데이터 정렬

(3) 최종 연산 (Terminal Operations)

  • 스트림의 처리를 종료하고 결과를 생성한다.
  • 대표적인 연산
    • collect: 결과를 List, Set 등으로 수집
    • forEach: 요소를 순회
    • reduce: 집계 결과를 생성

3. Stream 사용 예제

(1) 데이터 생성

import java.util.*;
import java.util.stream.*;

public class StreamExample {
    public static void main(String[] args) {
        List<String> names = Arrays.asList("Alice", "Bob", "Charlie", "David");

        // Stream 생성
        Stream<String> stream = names.stream();

        // Stream 처리
        stream.filter(name -> name.startsWith("A"))
              .forEach(System.out::println); // Alice
    }
}

(2) 중간 연산

import java.util.*;
import java.util.stream.*;

public class IntermediateOperations {
    public static void main(String[] args) {
        List<String> names = Arrays.asList("Alice", "Bob", "Charlie", "David");

        // 여러 중간 연산
        List<String> result = names.stream()
                                   .filter(name -> name.length() > 3) // 길이가 3 초과
                                   .map(String::toUpperCase)          // 대문자로 변환
                                   .sorted()                          // 정렬
                                   .collect(Collectors.toList());     // 최종 결과 수집

        System.out.println(result); // [ALICE, CHARLIE, DAVID]
    }
}

(3) 최종 연산

import java.util.*;
import java.util.stream.*;

public class TerminalOperations {
    public static void main(String[] args) {
        List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5);

        // 합계 계산
        int sum = numbers.stream()
                         .reduce(0, Integer::sum); // 초기값 0에서 합계
        System.out.println("Sum: " + sum); // 15
    }
}

4. 주요 연산 정리

중간 연산

연산 설명 예제
filter 조건에 맞는 요소만 통과 stream.filter(x -> x > 3)
map 요소를 다른 형태로 변환 stream.map(x -> x * 2)
sorted 요소 정렬 stream.sorted()
distinct 중복 제거 stream.distinct()
limit 스트림의 크기를 제한 stream.limit(3)
skip 처음 N개 요소를 건너뜀 stream.skip(2)

최종 연산

연산 설명 예제
collect 결과를 List, Set 등으로 수집 stream.collect(Collectors.toList())
forEach 요소를 순회하며 작업 수행 stream.forEach(System.out::println)
reduce 스트림을 집계하여 단일 값 반환 stream.reduce(0, Integer::sum)
count 스트림의 요소 개수 반환 stream.count()
anyMatch 조건에 맞는 요소가 있는지 확인 stream.anyMatch(x -> x > 3)

5. 병렬 스트림

  • 병렬 스트림(parallelStream)을 사용하면 데이터를 멀티코어 환경에서 병렬로 처리할 수 있다.
import java.util.*;

public class ParallelStreamExample {
    public static void main(String[] args) {
        List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5);

        int sum = numbers.parallelStream()
                         .reduce(0, Integer::sum);
        System.out.println("Sum: " + sum); // 15
    }
}

6. 고급 중간 연산

(1) flatMap

  • 다차원 데이터(flat한 형태가 아닌 데이터)를 1차원으로 평탄화하여 처리할 때 사용
  • 예를 들어, List 안에 List가 있을 때 이를 하나의 Stream으로 평탄화
  • import java.util.*; import java.util.stream.*; public class FlatMapExample { public static void main(String[] args) { List<List<String>> nestedList = Arrays.asList( Arrays.asList("Apple", "Banana"), Arrays.asList("Cherry", "Date"), Arrays.asList("Elderberry") ); List<String> flatList = nestedList.stream() .flatMap(List::stream) // 평탄화 .collect(Collectors.toList()); System.out.println(flatList); // [Apple, Banana, Cherry, Date, Elderberry] } }

(2) peek

  • 스트림의 각 요소를 처리하면서 디버깅하거나 작업을 확인할 때 사용
  • 예: 중간 연산 단계에서 현재 데이터 상태를 확인
  • List<String> names = Arrays.asList("Alice", "Bob", "Charlie"); List<String> upperCaseNames = names.stream() .peek(name -> System.out.println("Original: " + name)) .map(String::toUpperCase) .peek(name -> System.out.println("Transformed: " + name)) .collect(Collectors.toList());

7. 고급 최종 연산

(1) groupingBy

  • 데이터를 특정 기준으로 그룹화하고, 결과를 Map으로 반환
  • import java.util.*; import java.util.stream.*; public class GroupingByExample { public static void main(String[] args) { List<String> names = Arrays.asList("Alice", "Bob", "Charlie", "David"); Map<Integer, List<String>> groupedByLength = names.stream() .collect(Collectors.groupingBy(String::length)); System.out.println(groupedByLength); // 출력: {3=[Bob], 5=[Alice, David], 7=[Charlie]} } }

(2) partitioningBy

  • 데이터를 조건에 따라 두 개의 그룹(true/false)으로 분리
  • List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5); Map<Boolean, List<Integer>> partitioned = numbers.stream() .collect(Collectors.partitioningBy(n -> n % 2 == 0)); System.out.println(partitioned); // 출력: {false=[1, 3, 5], true=[2, 4]}

(3) joining

  • 스트림의 데이터를 하나의 문자열로 합치기
  • List<String> names = Arrays.asList("Alice", "Bob", "Charlie"); String result = names.stream() .collect(Collectors.joining(", ")); System.out.println(result); // Alice, Bob, Charlie

8. Primitive Stream

기본 타입(Primitive Type)을 처리하기 위한 스트림도 있다.

  • IntStream, LongStream, DoubleStream: 기본 데이터 타입 스트림을 제공
  • 효율적인 메모리 사용과 성능 향상이 목적
  • import java.util.stream.IntStream; public class PrimitiveStreamExample { public static void main(String[] args) { int sum = IntStream.range(1, 5) // 1부터 5 미만까지 .sum(); System.out.println("Sum: " + sum); // 10 } }

9. 병렬 스트림 (Parallel Stream)

병렬 처리의 장단점

  • 장점: 멀티코어 환경에서 병렬 처리를 통해 성능 향상
  • 단점: 적은 데이터에서는 병렬 처리 오버헤드로 인해 성능 저하 가능. 상태 공유에 신중해야 함
List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5);

int sum = numbers.parallelStream()
                 .reduce(0, Integer::sum);

System.out.println("Sum: " + sum); // 15

10. 무한 스트림 (Infinite Stream)

  • 데이터를 무한히 생성할 수 있는 스트림

Stream.generate: 무작위 값 생성

Stream.generate(Math::random)
      .limit(5) // 제한
      .forEach(System.out::println);

Stream.iterate: 초기 값과 연산을 기반으로 값 생성

Stream.iterate(1, n -> n + 1)
      .limit(10) // 10개의 값
      .forEach(System.out::println);

11. 스트림의 상태 관리

  • 상태 의존 연산을 피해야 함
    • Stream은 본질적으로 상태 비의존적(stateless)으로 설계됨
    • 상태를 공유하거나 의존하면 병렬 처리에서 문제가 발생할 수 있음

예제: 상태 공유의 문제

List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5);
int[] sum = {0};

numbers.stream()
       .forEach(n -> sum[0] += n); // 상태 공유 문제 발생 가능
System.out.println("Sum: " + sum[0]);

12. 스트림의 성능 최적화

필요한 연산만 수행
중간 연산은 최종 연산이 호출되기 전까지 수행되지 않으므로, 불필요한 연산을 피해야 함

데이터 크기 고려
스트림이 너무 큰 경우, 병렬 스트림을 고려하거나 기존 반복문을 사용할 수도 있음


13. Collector

Collector는 Java Stream API에서 데이터를 처리한 결과를 수집하거나 변환하는 데 사용되는 도구이다. java.util.stream.Collectors 클래스에 제공되는 다양한 정적 메서드를 사용하여 데이터를 원하는 형태(List, Set, Map 등)로 변환하거나 집계 작업을 수행할 수 있다.

  • CollectorStream최종 연산(Terminal Operation)을 지원하는 인터페이스
  • Stream의 요소를 특정한 방식으로 결과 컨테이너(예: List, Set, Map)로 수집하거나,
    데이터 집계(예: 합계, 평균 등)를 수행

14. Collector의 주요 메서드

Collectors 클래스는 자주 사용되는 다양한 Collector를 제공한다. 주요 메서드와 활용 예제는 다음과 같다.

(1) Collectors.toList()
스트림의 결과를 리스트(List)로 수집

import java.util.*;
import java.util.stream.*;

public class ToListExample {
    public static void main(String[] args) {
        List<String> names = Arrays.asList("Alice", "Bob", "Charlie");

        List<String> result = names.stream()
                                   .filter(name -> name.length() > 3)
                                   .collect(Collectors.toList());

        System.out.println(result); // [Alice, Charlie]
    }
}

(2) Collectors.toSet()
스트림의 결과를 집합(Set)으로 수집

import java.util.*;
import java.util.stream.*;

public class ToSetExample {
    public static void main(String[] args) {
        List<String> names = Arrays.asList("Alice", "Bob", "Alice", "Charlie");

        Set<String> result = names.stream()
                                  .collect(Collectors.toSet());

        System.out.println(result); // [Alice, Bob, Charlie]
    }
}

(3) Collectors.toMap()
스트림의 결과를 맵(Map)으로 수집. 키와 값 지정이 가능.

import java.util.*;
import java.util.stream.*;

public class ToMapExample {
    public static void main(String[] args) {
        List<String> names = Arrays.asList("Alice", "Bob", "Charlie");

        Map<String, Integer> result = names.stream()
                                           .collect(Collectors.toMap(
                                               name -> name,             // 키: 이름
                                               name -> name.length()     // 값: 이름의 길이
                                           ));

        System.out.println(result); // {Alice=5, Bob=3, Charlie=7}
    }
}

15. Collector를 사용한 집계

Collectors는 데이터의 합계, 평균, 최소값, 최대값 등을 계산하는 집계 메서드를 제공한다.

(1) Collectors.summingInt()
스트림 요소의 합계를 계산

import java.util.*;
import java.util.stream.*;

public class SummingExample {
    public static void main(String[] args) {
        List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5);

        int sum = numbers.stream()
                         .collect(Collectors.summingInt(n -> n));

        System.out.println(sum); // 15
    }
}

(2) Collectors.averagingInt()
스트림 요소의 평균을 계산

List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5);

double average = numbers.stream()
                        .collect(Collectors.averagingInt(n -> n));

System.out.println(average); // 3.0

16. Collector를 사용한 그룹화 및 분할

(1) Collectors.groupingBy()
데이터를 특정 기준으로 그룹화

List<String> names = Arrays.asList("Alice", "Bob", "Charlie", "David");

Map<Integer, List<String>> groupedByLength = names.stream()
                                                  .collect(Collectors.groupingBy(String::length));

System.out.println(groupedByLength);
// {3=[Bob], 5=[Alice, David], 7=[Charlie]}

(2) Collectors.partitioningBy()
데이터를 조건에 따라 두 그룹(true/false)으로 나눈다.

List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5);

Map<Boolean, List<Integer>> partitioned = numbers.stream()
                                                 .collect(Collectors.partitioningBy(n -> n % 2 == 0));

System.out.println(partitioned);
// {false=[1, 3, 5], true=[2, 4]}

17. Collector를 사용한 문자열 병합

Collectors.joining()
스트림의 데이터를 문자열로 병합. 구분자, 접두사, 접미사를 지정할 수 있다.

List<String> names = Arrays.asList("Alice", "Bob", "Charlie");

String result = names.stream()
                     .collect(Collectors.joining(", ", "[", "]"));

System.out.println(result); // [Alice, Bob, Charlie]

8️⃣Optional

  • Optionalnull 값을 안전하게 처리하기 위해 Java 8에서 도입된 도구
  • Optional을 활용하여 NullPointerException(NPE)을 방지하고 데이터를 더 안전하게 처리 가능

1. Optional의 개념 이해

Optional이란?

  • null 대신 사용되는 컨테이너 객체로, 값이 있을 수도 있고 없을 수도 있는 상황을 처리
  • NullPointerException 방지
  • 명시적으로 값이 없음을 표현

Optional의 상태

  1. 값이 있는 경우 (Present): 값이 존재하는 Optional 객체
  2. 값이 없는 경우 (Empty): 값이 없는 상태를 나타내는 Optional 객체

Optional 생성 메서드

  1. Optional.of(T value)
    • 값이 반드시 존재한다고 보장할 때 사용
    • null 값을 전달하면 예외 발생
  2. Optional.ofNullable(T value)
    • 값이 존재할 수도 있고, null일 수도 있을 때 사용
  3. Optional.empty()
    • 빈 Optional 객체 생성.
import java.util.Optional;

public class OptionalExample {
    public static void main(String[] args) {
        // 값이 존재
        Optional<String> presentValue = Optional.of("Hello");

        // 값이 없을 가능성
        Optional<String> nullableValue = Optional.ofNullable(null);

        // 빈 Optional
        Optional<String> emptyValue = Optional.empty();

        System.out.println(presentValue.isPresent()); // true
        System.out.println(nullableValue.isPresent()); // false
        System.out.println(emptyValue.isPresent()); // false
    }
}

2. Optional의 주요 메서드

(1) isPresentifPresent

  • isPresent(): 값이 존재하면 true를 반환
  • ifPresent(Consumer<T> action): 값이 존재할 때 동작을 수행
Optional<String> optional = Optional.of("Java");
optional.ifPresent(System.out::println); // Java

(2) 값 반환

  1. get(): 값이 존재하면 반환, 없으면 NoSuchElementException 발생
  2. orElse(T other): 값이 없으면 기본값 반환
  3. orElseGet(Supplier<? extends T> other): 값이 없으면 기본값을 계산하는 메서드 호출
  4. orElseThrow(Supplier<? extends X> exceptionSupplier): 값이 없으면 사용자 정의 예외를 던짐
Optional<String> optional = Optional.ofNullable(null);

// 기본값 제공
System.out.println(optional.orElse("Default")); // Default

// 동적 값 제공
System.out.println(optional.orElseGet(() -> "Generated Default")); // Generated Default

// 예외 발생
optional.orElseThrow(() -> new IllegalArgumentException("Value is missing!"));

(3) 데이터 변환

  1. map(Function<? super T, ? extends U>): Optional의 값을 변환
  2. flatMap(Function<? super T, Optional<U>>): 중첩된 Optional을 처리
Optional<String> optional = Optional.of("Java");

// 값을 변환
optional.map(String::toUpperCase)
        .ifPresent(System.out::println); // JAVA

// 중첩 Optional 처리
Optional<Optional<String>> nestedOptional = Optional.of(Optional.of("Nested"));
nestedOptional.flatMap(o -> o.map(String::toUpperCase))
              .ifPresent(System.out::println); // NESTED

Optional의 주요 메서드를 설명, 반환값, 예제와 함께 정리한 표이다.

메서드 설명 리턴 타입 예제
of(T value) null이 아닌 값을 Optional로 감쌈. null 값이면 예외 발생 Optional<T> Optional<String> opt = Optional.of("Hello");
ofNullable(T value) null 값을 허용하는 Optional 생성. null이면 빈 Optional 반환 Optional<T> Optional<String> opt = Optional.ofNullable(null);
empty() 빈 Optional 생성 Optional<T> Optional<String> opt = Optional.empty();
isPresent() 값이 존재하면 true, 없으면 false 반환 boolean opt.isPresent();
ifPresent(Consumer<? super T>) 값이 존재하면 지정된 작업 수행 void opt.ifPresent(System.out::println); // 값 출력
get() 값이 존재하면 반환, 없으면 NoSuchElementException 발생 T String value = opt.get();
orElse(T other) 값이 없으면 기본값 반환 T String value = opt.orElse("Default");
orElseGet(Supplier<? extends T>) 값이 없으면 동적으로 기본값을 생성해 반환 T String value = opt.orElseGet(() -> "Generated Default");
orElseThrow(Supplier<? extends X>) 값이 없으면 사용자 정의 예외를 던짐 T String value = opt.orElseThrow(() -> new IllegalArgumentException("Value missing"));
map(Function<? super T, ? extends U>) 값을 변환하고 Optional로 감쌈.값이 없으면 빈 Optional 반환 Optional<U> opt.map(String::toUpperCase).ifPresent(System.out::println); // HELLO
flatMap(Function<? super T, Optional<U>>) 중첩된 Optional을 평탄화하여 반환 Optional<U> Optional.of(Optional.of("Nested")).flatMap(o -> o.map(String::toUpperCase)); // Optional[NESTED]
filter(Predicate<? super T>) 조건을 만족하는 값만 남김. 조건에 맞지 않으면 빈 Optional 반환 Optional<T> opt.filter(value -> value.startsWith("H")).ifPresent(System.out::println); // Hello
stream() 값을 Stream으로 변환. 값이 없으면 빈 Stream 반환 Stream<T> opt.stream().forEach(System.out::println);

추가적으로 알아야 할 메서드

1. filter(Predicate<? super T>)

  • 값이 조건을 만족하면 Optional을 그대로 반환, 조건에 맞지 않으면 빈 Optional 반환
  • Optional<String> opt = Optional.of("Hello"); opt.filter(value -> value.startsWith("H")) .ifPresent(System.out::println); // Hello

2. stream()

  • Optional을 Stream으로 변환하여, Stream API와 결합해 처리할 수 있음
  • Optional<String> opt = Optional.of("Hello"); opt.stream() .map(String::toUpperCase) .forEach(System.out::println); // HELLO

3. orElseGet(Supplier<? extends T>)

  • 동적인 기본값 생성이 필요할 때 사용
  • Optional<String> opt = Optional.ofNullable(null); String value = opt.orElseGet(() -> "Generated Default"); System.out.println(value); // Generated Default

4. flatMap(Function<? super T, Optional<U>>)

  • 중첩된 Optional을 평탄화(flatten)하는 데 사용.
  • Optional<Optional<String>> nestedOpt = Optional.of(Optional.of("Nested")); nestedOpt.flatMap(o -> o.map(String::toUpperCase)) .ifPresent(System.out::println); // NESTED

3. Optional과 Stream의 결합

Optional과 Stream의 연계Optional 객체를 Stream으로 변환하여 Stream API를 활용하거나, Optional을 안전하게 처리하면서 Stream 연산을 적용하는 것을 의미한다. 이는 코드의 가독성과 효율성을 높이는 데 유용하다.

Stream 데이터 처리

Optional은 Stream API와 결합하면 더욱 간결하고 안전한 코드 작성을 돕는다.

Optional<String> optional = Optional.ofNullable("Stream");

optional.stream()
        .filter(value -> value.length() > 3)
        .map(String::toUpperCase)
        .forEach(System.out::println); // STREAM

1. Optional과 Stream의 연계 기본 개념

  1. Optional을 Stream으로 변환
    • Optional에 값이 있으면 Stream의 요소로 변환
    • Optional이 비어 있으면 빈 Stream을 반환
  2. Stream과 Optional 결합
    • Stream 처리 결과를 Optional로 감싸서 안전하게 반환
    • Optional을 사용해 Stream 데이터 흐름을 중단하거나 필터링

2. Optional의 stream() 메서드

  • Optional 값을 Stream으로 변환
  • Optional에 값이 있으면 Stream의 요소로 포함하고, 없으면 빈 Stream 반환
import java.util.Optional;
import java.util.stream.Stream;

public class OptionalStreamExample {
    public static void main(String[] args) {
        Optional<String> optional = Optional.of("Hello");

        // Optional -> Stream
        Stream<String> stream = optional.stream();

        // Stream 연산
        stream.map(String::toUpperCase)
              .forEach(System.out::println); // HELLO
    }
}

3. Optional과 Stream 연계 사용 사례

(1) Stream에서 Optional 사용

Stream의 결과를 Optional로 감싸서 안전하게 반환할 수 있다.

import java.util.Optional;
import java.util.stream.Stream;

public class StreamToOptionalExample {
    public static void main(String[] args) {
        Stream<String> stream = Stream.of("Alice", "Bob", "Charlie");

        // 조건에 맞는 첫 번째 요소 찾기
        Optional<String> result = stream.filter(name -> name.startsWith("B"))
                                        .findFirst();

        result.ifPresent(System.out::println); // Bob
    }
}

(2) Optional에서 Stream 사용

Optional 값을 Stream 연산에 결합하여 처리할 수 있다.

사import java.util.Optional;
import java.util.List;
import java.util.Arrays;

public class OptionalToStreamExample {
    public static void main(String[] args) {
        Optional<String> optional = Optional.of("Alice");
        List<String> names = Arrays.asList("Bob", "Charlie");

        // Optional -> Stream -> List와 결합
        optional.stream()
                .map(String::toUpperCase)
                .forEach(names::add);

        System.out.println(names); // [Bob, Charlie, ALICE]
    }
}

4. Stream과 Optional 연계 심화

(1) Optional 데이터 필터링

Optional 값을 Stream으로 변환 후, 여러 조건을 적용해 데이터를 처리한다.

import java.util.Optional;

public class FilterExample {
    public static void main(String[] args) {
        Optional<String> optional = Optional.of("Java Stream");

        // Optional -> Stream -> Filter
        optional.stream()
                .filter(value -> value.contains("Stream"))
                .forEach(System.out::println); // Java Stream
    }
}

(2) Optional과 flatMap

flatMap을 사용하면 중첩된 Optional이나 Stream을 평탄화할 수 있다.

import java.util.Optional;

public class FlatMapExample {
    public static void main(String[] args) {
        Optional<Optional<String>> nestedOptional = Optional.of(Optional.of("Nested Value"));

        // 중첩 Optional을 평탄화
        nestedOptional.flatMap(value -> value.map(String::toUpperCase))
                      .ifPresent(System.out::println); // NESTED VALUE
    }
}

5. 실용적인 활용 사례

(1) 데이터베이스 조회 결과 처리

Optional과 Stream을 사용해 데이터베이스 조회 결과를 안전하게 처리

import java.util.Optional;
import java.util.stream.Stream;

public class DatabaseExample {
    public static void main(String[] args) {
        Optional<String> optionalResult = queryDatabase("key1");

        // Optional -> Stream -> 데이터 처리
        optionalResult.stream()
                      .map(String::toUpperCase)
                      .forEach(System.out::println);
    }

    private static Optional<String> queryDatabase(String key) {
        // 가상의 데이터베이스 조회
        if ("key1".equals(key)) {
            return Optional.of("Value1");
        }
        return Optional.empty();
    }
}

(2) API 호출 응답 처리

API 호출의 결과를 Optional로 처리하고, Stream을 사용해 데이터를 정제

java코드 복사import java.util.Optional;

public class ApiExample {
    public static void main(String[] args) {
        Optional<String> apiResponse = callApi();

        // Optional -> Stream -> 데이터 정제
        apiResponse.stream()
                   .filter(response -> response.contains("Success"))
                   .forEach(System.out::println);
    }

    private static Optional<String> callApi() {
        // 가상의 API 호출
        return Optional.of("Success: Data retrieved");
    }
}

4. Optional의 한계와 주의점

  1. Optional은 필드로 사용하지 말 것
    • Optional은 메서드 반환 값에서만 사용하는 것이 권장된다
    • 필드에 사용할 경우, 메모리 누수와 성능 저하를 유발할 수 있다
    따라서 Optional은 메서드 반환 타입으로 사용하는 것이 권장되며, 필드 변수로 사용하는 것은 비효율적
  2. 과도한 사용 피하기
    • Optional은 null 대신 안전한 대안으로 사용되지만, 간단한 null 체크보다 복잡한 코드로 이어질 수 있다
  3. 성능 고려
    • Optional의 오버헤드가 크진 않지만, 성능이 중요한 곳에서는 주의해야 한다.
  4. 값이 반드시 존재할 경우 get()을 피하기
    • 값이 없을 경우 예외가 발생하므로, get()보다는 orElse, orElseGet을 사용하는 것이 안전하다.