CodeGym /행동 /JAVA 25 SELF /NavigableSet/NavigableMap

NavigableSet/NavigableMap

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

1. 소개

Java 표준 라이브러리에는 다양한 컬렉션이 있지만, 범위 처리, ‘가장 가까운’ 요소 검색, 우선순위 부여, 시간 슬롯 관리가 필요할 때는 NavigableSetNavigableMap 인터페이스가 빛을 발합니다.

NavigableSetSortedSet을 확장하여 주어진 값에 대해 상대적인 요소(작음, 큼, 가장 가까움 등)를 찾고 범위를 다루는 메서드를 추가합니다.
NavigableMapSortedMap을 확장하여 키 기준 검색과 범위 작업을 위한 유사한 메서드를 추가합니다.

가장 잘 알려진 구현: TreeSetTreeMap.

NavigableSet/NavigableMap이 필요한 경우

  • 고유한 요소/키를 정렬해 저장하는 것만이 아니라 ‘이웃’을 빠르게 찾아야 할 때(예: 가장 가까운 빈 시간, 우선순위가 높은 작업, 값의 구간).
  • 범위를 다뤄야 할 때: “X와 Y 사이의 모든 요소”, “주어진 값보다 큰/작은 모든 키”, “가장 가까운 키 찾기”.

2. 범위: subSet, headSet, tailSet

범위 작업을 위한 메서드

NavigableSetNavigableMap은 컬렉션의 부분집합에 대한 ‘라이브’ 표현(view)을 제공합니다:

  • subSet(fromElement, fromInclusive, toElement, toInclusive) — [fromElement; toElement] 범위의 요소(포함/제외 선택 가능).
  • headSet(toElement, inclusive) — toElement보다 작거나(또는 작거나 같게) 모든 요소.
  • tailSet(fromElement, inclusive) — fromElement보다 크거나(또는 크거나 같게) 모든 요소.

예:

NavigableSet<Integer> set = new TreeSet<>(List.of(10, 20, 30, 40, 50));

// 15(포함)부터 45(제외)까지의 범위
NavigableSet<Integer> sub = set.subSet(15, true, 45, false); // [20, 30, 40]
System.out.println(sub); // [20, 30, 40]

// 모든 요소 <= 30
NavigableSet<Integer> head = set.headSet(30, true); // [10, 20, 30]

// 모든 요소 > 20
NavigableSet<Integer> tail = set.tailSet(20, false); // [30, 40, 50]

포함 여부:

  • true — 포함(요소가 범위에 포함됨)
  • false — 제외(요소가 포함되지 않음)

NavigableMap의 경우도 메서드는 유사하지만 키에 대해 동작합니다.

3. 가까운 값 연산: floor, ceiling, lower, higher; pollFirst/Last

가장 가까운 요소 찾기

NavigableSet:

  • lower(E e) — e보다 작은(엄격히) 요소 중 최댓값
  • floor(E e) — e보다 작거나 같은(≤) 요소 중 최댓값
  • ceiling(E e) — e보다 크거나 같은(≥) 요소 중 최솟값
  • higher(E e) — e보다 큰(엄격히) 요소 중 최솟값

예:

NavigableSet<Integer> set = new TreeSet<>(List.of(10, 20, 30, 40, 50));

System.out.println(set.lower(30));   // 20
System.out.println(set.floor(30));   // 30
System.out.println(set.ceiling(25)); // 30
System.out.println(set.higher(40));  // 50

pollFirst() / pollLast()

  • pollFirst() — 가장 작은 요소를 제거하고 반환(null이면 비어 있음)
  • pollLast() — 가장 큰 요소를 제거하고 반환

예:

System.out.println(set.pollFirst()); // 10
System.out.println(set.pollLast());  // 50
System.out.println(set);             // [20, 30, 40]

NavigableMap: 키에 대해 동작하는 유사 메서드 — lowerKey, floorKey, ceilingKey, higherKey, 그리고 pollFirstEntry, pollLastEntry가 있습니다.

4. 뷰: descendingSet/descendingMap; view와 그 ‘라이브’ 특성

역순: descendingSet/descendingMap

  • descendingSet() — 집합을 역순으로 보여주는 뷰를 반환합니다.
  • descendingMap() — 키의 역순으로 보여주는 맵 뷰를 반환합니다.

예:

NavigableSet<Integer> set = new TreeSet<>(List.of(10, 20, 30, 40, 50));
NavigableSet<Integer> desc = set.descendingSet();
System.out.println(desc); // [50, 40, 30, 20, 10]

중요: 이는 복사본이 아니라 동일한 데이터 집합에 대한 ‘라이브’ 표현(view)입니다. 한쪽의 변경이 다른 쪽에도 반영됩니다.

뷰(view)와 ‘라이브’ 특성

  • subSet, headSet, tailSet, descendingSet, descendingMapview — 즉, 원본 컬렉션에 대한 ‘라이브’ 표현을 반환합니다.
  • view에서의 모든 변경은 원본 컬렉션에 반영되고 그 반대도 마찬가지입니다.
  • subSet에서 요소를 삭제하면 원본 집합에서도 사라집니다.

예:

NavigableSet<Integer> set = new TreeSet<>(List.of(10, 20, 30, 40, 50));
NavigableSet<Integer> sub = set.subSet(20, true, 40, true); // [20, 30, 40]
sub.remove(30);
System.out.println(set); // [10, 20, 40, 50]

주의: 원본 컬렉션을 변경하여 요소가 view의 범위를 벗어나게 만들면, 이후 view에 접근할 때 ConcurrentModificationException이 발생할 수 있습니다.

5. NavigableSet/NavigableMap 사용 사례

1. 시간 슬롯/스케줄링

과제: 회의를 위한 가장 가까운 빈 시간 슬롯 찾기.

NavigableSet<LocalTime> slots = new TreeSet<>(List.of(
    LocalTime.of(9, 0),
    LocalTime.of(10, 0),
    LocalTime.of(11, 0),
    LocalTime.of(14, 0)
));

LocalTime requested = LocalTime.of(10, 30);
LocalTime nextSlot = slots.ceiling(requested); // 11:00
System.out.println("가장 가까운 시간: " + nextSlot);

2. 우선순위 부여(우선순위 큐)

과제: 항상 가장 높은/가장 낮은 우선순위의 요소를 빠르게 가져오기.

NavigableSet<Task> tasks = new TreeSet<>(Comparator.comparingInt(Task::priority));
tasks.add(new Task("메일", 2));
tasks.add(new Task("전화", 1));
tasks.add(new Task("보고서", 3));

Task next = tasks.pollFirst(); // 가장 높은 우선순위의 작업(1)

3. ‘가장 가까운 키’(예: Map에서 구간 검색)

과제: 키로 값의 구간을 찾거나, 가장 가까운 키를 찾기.

NavigableMap<Integer, String> grades = new TreeMap<>();
grades.put(50, "미흡");
grades.put(65, "보통");
grades.put(75, "좋음");
grades.put(85, "우수");

int score = 78;
int key = grades.floorKey(score); // 75
System.out.println("평가: " + grades.get(key)); // 좋음

6. 실습: 미니 예제

예제 1: 날짜 범위

NavigableSet<LocalDate> holidays = new TreeSet<>(List.of(
    LocalDate.of(2024, 1, 1),
    LocalDate.of(2024, 5, 1),
    LocalDate.of(2024, 12, 31)
));
LocalDate from = LocalDate.of(2024, 1, 1);
LocalDate to = LocalDate.of(2024, 6, 1);
NavigableSet<LocalDate> spring = holidays.subSet(from, true, to, false);
System.out.println(spring); // [2024-01-01, 2024-05-01]

예제 2: 가장 가까운 값 찾기

NavigableSet<Integer> numbers = new TreeSet<>(List.of(10, 20, 30, 40, 50));
int x = 25;
System.out.println("작음: " + numbers.lower(x));              // 20
System.out.println("작거나 같음: " + numbers.floor(x));    // 20
System.out.println("크거나 같음: " + numbers.ceiling(x));  // 30
System.out.println("큼: " + numbers.higher(x));             // 30

7. 흔한 실수와 주의점

오류 №1: subSet/headSet/tailSet에서 포함/제외를 혼동.
항상 inclusive 매개변수를 주의 깊게 확인하세요. 예전 Java 버전에서는 기본이 제외 범위였지만, NavigableSet/NavigableMap에서는 포함/제외를 명시할 수 있습니다.

오류 №2: view가 복사본이라고 기대함.
view는 ‘라이브’ 표현이지 복사본이 아닙니다! view의 변경은 원본 컬렉션에도 반영됩니다.

오류 №3: ConcurrentModificationException — view 밖에서 원본 컬렉션을 변경하는 경우.
원본 컬렉션을 변경하여 요소가 view의 범위를 벗어나면, 다음에 view에 접근할 때 예외가 발생합니다.

오류 №4: Comparable 또는 Comparator 없이 NavigableSet/NavigableMap 사용.
이 컬렉션은 요소(또는 키)가 비교 가능(Comparable)해야 하거나 Comparator를 전달해야 합니다. 그렇지 않으면 ClassCastException이 발생합니다.

오류 №5: pollFirst/pollLast가 컬렉션을 변경하지 않는다고 기대함.
이 메서드들은 요소를 삭제합니다! 단순히 확인만 필요하다면 first()/last()를 사용하세요.

코멘트
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION