CodeGym /행동 /JAVA 25 SELF /Set: HashSet 및 TreeSet, 요소의 고유성

Set: HashSet 및 TreeSet, 요소의 고유성

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

1. 소개

현실적인 예로 시작해 봅시다. 당신이 파티를 열고 초대 손님 명단을 만든다고 상상해 보세요. 초대장을 보냈는데, 같은 사람이 명단에 두 번(심지어 세 번 — 파티를 정말 좋아하니까요!) 들어가 있음을 알게 됩니다. 일반적인 리스트(List)를 사용하면 이런 중복이 쉽게 생길 수 있습니다. 하지만 같은 손님을 두 번 추가하지 못하게 하는 컬렉션이 있다면 — 훨씬 편해지겠죠.

바로 여기서 Set 컬렉션(집합)이 등장합니다.

Set은 오직 고유한 요소만 저장하는 컬렉션입니다. 이미 존재하는 요소를 추가하려고 하면 그냥 추가되지 않습니다(아무도 기분 상하지 않죠).

Set 인터페이스: 기본 속성

Java에서 Set은 중복이 없는 컬렉션의 동작을 정의하는 인터페이스입니다. 인터페이스 Collection을 상속하므로, 추가(add), 삭제(remove), 포함 여부 확인(contains), 순회 같은 연산을 지원합니다.

핵심 특징:

  • Set에는 동일한 요소가 둘 이상 존재할 수 없습니다.
  • 요소의 저장 순서는 구현에 따라 임의일 수 있습니다.
  • 인덱스가 없습니다: 리스트처럼 번호로 요소에 접근할 수 없습니다.

선언 문법

Set<String> guests = new HashSet<>();

2. HashSet: 빠르고 단순하지만 순서 없음

HashSetSet 인터페이스의 가장 인기 있는 구현입니다. 해시 테이블에 기반하며(HashMap과 유사하지만 "키-값" 쌍이 없고 고유한 값만 저장), 가장 큰 장점은 추가, 삭제, 검색 연산의 속도입니다.

HashSet은 어떻게 동작할까?

상자를 하나 떠올려 보세요. 안에 이미 비슷한 것이 있는지 빠르게 알아보려면, 각 물건에 고유한 "번호" — 해시 코드가 부여됩니다. HashSet에 요소를 추가할 때 먼저 이 해시 코드를 계산합니다. 이전에 없던 해시라면 요소가 컬렉션에 무난히 들어갑니다. 같은 해시가 있으면 equals()로 추가로 동일성 검사를 합니다. 객체가 실제로 동일하다면 새 요소는 추가되지 않습니다.

즉, HashSet은 고유성을 자동으로 보장합니다: 동일한 객체가 둘 이상 나타나지 않습니다.

흥미로운 점: 사용자 정의 클래스를 HashSet에 저장하려면 equals()hashCode() 메서드를 재정의해야 합니다. 그렇지 않으면 컬렉션이 예측 불가능하게 동작할 수 있습니다 — 겉보기엔 같은 객체가 서로 다른 것으로 간주될 수 있습니다.

HashSet의 주요 메서드

Set<String> guests = new HashSet<>();

guests.add("이반");
guests.add("마리야");
guests.add("표트르");
guests.add("이반"); // 중복! 추가되지 않습니다.

System.out.println(guests); // [이반, 마리야, 표트르] — 순서는 임의일 수 있습니다

guests.remove("표트르"); // 요소를 삭제
System.out.println(guests.contains("마리야")); // true
System.out.println(guests.size()); // 2

코드로 직접 시도해 봅시다

예를 들어, 우리 애플리케이션에서 작업 이름이 중복되지 않도록 고유한 작업 이름만 저장하고 싶다고 해봅시다:

import java.util.HashSet;
import java.util.Set;

public class UniqueTasksDemo {
    public static void main(String[] args) {
        Set<String> tasks = new HashSet<>();
        tasks.add("Java 숙제 하기");
        tasks.add("고양이 쓰다듬기");
        tasks.add("Java 숙제 하기"); // 중복!

        System.out.println("작업 목록:");
        for (String task : tasks) {
            System.out.println("- " + task);
        }
        // 목록에는 두 개의 작업만 있고, 중복은 추가되지 않습니다
    }
}

3. TreeSet: 순서가 중요할 때!

때로는 단순한 고유성뿐 아니라 정렬된 요소 집합이 필요합니다. 예컨대 손님 이름을 임의가 아니라 사전 순으로 보고 싶을 수 있죠. 이럴 때 TreeSet이 유용합니다.

TreeSet은 요소를 정렬된 순서(오름차순)로 저장하는 Set의 구현입니다. "레드-블랙 트리" 구조를 기반으로 합니다.

TreeSet 사용 예

import java.util.Set;
import java.util.TreeSet;

public class SortedGuestsDemo {
    public static void main(String[] args) {
        Set<String> guests = new TreeSet<>();
        guests.add("블라디미르");
        guests.add("알렉세이");
        guests.add("예카테리나");
        guests.add("알렉세이"); // 중복!

        System.out.println("손님(사전 순):");
        for (String guest : guests) {
            System.out.println("- " + guest);
        }
        // 출력:
        // - 알렉세이
        // - 블라디미르
        // - 예카테리나
    }
}

주의: 중복을 추가하면 집합에 나타나지 않습니다. 당연히 그래야 하죠!

언제 TreeSet을 사용할까?

  • 정렬된 고유 요소 집합이 필요할 때.
  • 검색이 중요하지만 추가 속도가 아주 중요하지 않을 때(HashSet보다 약간 느립니다).
  • 요소가 사용자 정의 클래스인 경우, 요소가 "비교 가능"(인터페이스 Comparable 구현)해야 하거나 Comparator를 제공해야 합니다.

4. 유용한 뉘앙스

HashSet vs TreeSet: 무엇을 선택할까?

기준 HashSet TreeSet
저장 순서 보장되지 않음 오름차순으로 정렬됨
연산 속도 더 빠름 (O(1)) 더 느림 (O(log n))
타입 요구사항 제한 없음 (단, equals()/hashCode() 필요) Comparable 또는 Comparator
일반적인 시나리오 고유 요소에 빠르게 접근해야 할 때 정렬된 출력/순회가 중요할 때

Set 사용상의 특징

  • 인덱스 없음. List와 달리 Set에는 get(int index) 메서드가 없습니다. 인덱스로 접근이 필요하면 List를 사용하세요.
  • 중복 없음. 이미 있는 요소를 추가하면 추가되지 않습니다. add 메서드는 false를 반환합니다.
  • 순서가 보장되지 않음(TreeSet 제외). HashSet의 요소 순서는 실행할 때마다 달라질 수 있습니다. 삽입 순서가 필요하면 LinkedHashSet을 사용하세요.
  • null 값.
    • HashSetnull 요소 하나를 허용합니다.
    • TreeSet은 특별한 Comparator 없이 null을 추가하면 NullPointerException이 발생합니다.

5. Set의 전형적인 사용 과제

리스트에서 중복 제거

예를 들어, 어떤 학생 리스트에 일부 학생이 두 번씩 기록되어 있다고 합시다. 고유한 이름만 남겨야 합니다:

import java.util.*;

public class RemoveDuplicatesDemo {
    public static void main(String[] args) {
        List<String> students = Arrays.asList("안나", "이고리", "안나", "마리야", "이고리", "파벨");

        Set<String> uniqueStudents = new HashSet<>(students);

        System.out.println("고유한 학생들: " + uniqueStudents);
        // 순서는 보장되지 않습니다!
    }
}

정렬된 결과가 필요하다면 TreeSet을 사용하세요:

Set<String> sortedUniqueStudents = new TreeSet<>(students);
System.out.println("고유한 학생들(사전 순): " + sortedUniqueStudents);

고유성 검사(예: 사용자 로그인)

Set<String> usedLogins = new HashSet<>();
usedLogins.add("student1");
usedLogins.add("java_lover");

String newLogin = "student1";
if (usedLogins.contains(newLogin)) {
    System.out.println("해당 로그인은 이미 사용 중입니다!");
} else {
    System.out.println("로그인을 사용할 수 있습니다!");
}

집합 요소 순회

for-each 루프를 사용합니다:

for (String name : uniqueStudents) {
    System.out.println(name);
}

6. Set 사용 시 흔한 실수

오류 №1: HashSet에서 특정한 요소 순서를 기대함. 많은 초보자들이 요소가 "이상한" 순서로 출력되는 것에 놀랍니다. 이는 정상입니다 — HashSet은 순서를 보장하지 않습니다. 삽입 순서가 필요하면 LinkedHashSet을, 정렬이 필요하면 TreeSet을 사용하세요.

오류 №2: 인덱스로 요소에 접근하려고 함. 가끔 set.get(0) 같은 코드를 작성하려고 합니다. 이렇게는 할 수 없습니다: Set은 인덱싱을 지원하지 않습니다. 인덱스로 접근이 필요하면 List를 사용하세요.

오류 №3: 변경 가능한 객체를 저장함. equals()/hashCode()에 참여하는 필드를 변경할 수 있는 객체를 저장하면, 해당 필드를 변경한 뒤에는 그 요소가 집합에서 "사라진" 것처럼 보일 수 있습니다. 요소를 불변으로 만들거나 식별 필드는 변경하지 마세요.

오류 №4: 중복이 추가될 것이라 기대함. 같은 요소를 여러 번 추가해도 집합의 크기는 늘어나지 않습니다 — 중복은 무시되며, add 메서드는 false를 반환합니다.

오류 №5: 기본형을 사용함. Set<int> 같은 표기는 컴파일되지 않습니다. 래퍼 클래스를 사용하세요: Set<Integer>, Set<Double> 등.

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