Notice
Recent Posts
Recent Comments
Link
«   2026/03   »
1 2 3 4 5 6 7
8 9 10 11 12 13 14
15 16 17 18 19 20 21
22 23 24 25 26 27 28
29 30 31
Tags more
Archives
Today
Total
관리 메뉴

프로그래밍 공부

[자바] #11. 컬렉션 프레임워크 - 2. Set (HashSet, TreeSet) 본문

자바

[자바] #11. 컬렉션 프레임워크 - 2. Set (HashSet, TreeSet)

하 냥 2025. 2. 16. 06:46

📌 Set<E>의 주요 메서드 (중복 허용 X, 순서 보장 X)

메서드 설명
boolean add(E e) 요소 추가 (이미 존재하면 추가되지 않음)
boolean remove(Object o) 특성 요소 삭제
int size() 집합(Set)의 크기 반환
boolean contains(Object o) 특정 요소 포함 여부 확인
void clear() 모든 요소 제거
boolean isEmpty() 집합이 비어있는지 확인
Iterator<E> iterator() 반복자를 반환하여 요소 순회

 

🔹 HashSet 예제

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

class Num {
	private int num;
    public Num(int n) { num = n; }
    @Override
    public String toString() { return String.valueOf(num); }
}

public class HashSetExample {
    public static void main(String[] args) {
        Set<Num> set = new HashSet<>();
        set.add(new Num(10));
        set.add(new Num(20));
        set.add(new Num(10));
         // 중복 요소 추가가 안 될 것 같지만 추가된다.
         // 이 경우, 인스턴스가 다르면 Object 클래스의 hashCode 메소드는 다른 값을 반환하기 때문!

        System.out.println(set); // [10, 20, 10]
    }
}

📌 1. hashCode()의 역할

HashSet의 내부 동작 과정

  1. 객체를 add()로 추가하면, hashCode()를 호출하여 해시 값(숫자)을 계산.
  2. 해시 값이 같은 그룹(버킷)에 저장됨.
  3. 객체를 검색할 때도 hashCode()를 사용하여 빠르게 위치를 찾음.
  4. 만약 hashCode()가 같은 객체가 있다면(해시 충돌), equals()를 사용하여 실제로 같은 객체인지 확인.

💡 즉, hashCode()는 HashSet이 요소를 빠르게 저장하고 검색할 수 있도록 도와주는 핵심 메서드이다!

public class HashCodeExample {
    public static void main(String[] args) {
        String str1 = "hello";
        String str2 = "hello";

        System.out.println(str1.hashCode()); // 같은 문자열이면 같은 해시 코드
        System.out.println(str2.hashCode()); // 같은 문자열이면 같은 해시 코드
    }
}

📌 2. hashCode()와 equals()의 관계

HashSet은 요소를 추가할 때 hashCode()와 equals()를 같이 사용해서 중복을 체크함.

  • 1: hashCode()를 비교하여 같은 해시 값인지 확인.
  • 2: 해시 값이 같다면 equals()를 호출하여 실제 같은 객체인지 확인.

💡 즉, hashCode()만 같다고 해서 같은 객체가 아님! equals()도 true여야 같은 객체로 판단된다.

hashCode()와 equals()를 함께 사용하는 예제

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

class Person {
    String name;

    public Person(String name) {
        this.name = name;
    }
    @Override
    public int hashCode() {
        return name.hashCode(); // 이름의 해시 코드 사용
    }
    @Override
    public boolean equals(Object obj) {
        if (obj instanceof Person) {
            Person other = (Person) obj;
            return this.name.equals(other.name); // 이름이 같으면 같은 객체로 간주
        }
        return false;
    }
}

public class HashSetExample {
    public static void main(String[] args) {
        Set<Person> set = new HashSet<>();
        set.add(new Person("Alice"));
        set.add(new Person("Alice")); // 중복 체크됨

        System.out.println(set.size()); // 1 (중복 제거됨)
    }
}

hashCode()가 같고 equals()도 같으므로 중복이 제거된다!

 

그런데 클래스를 정의할 때마다 이렇게 hashCode 메소드를 정의하는 것은 참 번거로운 일이다.

그래서 자바에서는 다음 메소드를 우리에게 제공해주고 있다.

Objects.hash() -> java.util.Objects에 정의된 메소드. 전달된 인자 기반의 해쉬 값을 반환

@Override
public int hashCode() {
    return Objects.hash(name);
}

 


 

📌 TreeSet<E>의 주요 특징

특징 설명
자동 정렬 요소를 추가할 때 자동으로 정렬된 상태를 유지
중복 허용 X 같은 값이 여러 번 추가되지 않음 (Set의 특성)
null 값 저장 불가 null을 추가하려 하면 NullPointerException 발생
이진 검색 트리(Red-Black Tree) 기반 검색, 삽입, 삭제 모두 O(log N)의 시간 복잡도를 가짐

🚀 빠른 검색이 필요하면 HashSet, 정렬이 필요하면 TreeSet을 사용하면 좋다!

🔹 TreeSet 예제

import java.util.*;

public class TreeSetExample {
    public static void main(String[] args) {
        TreeSet<Integer> numbers = new TreeSet<>();

        numbers.add(40); numbers.add(10);
        numbers.add(30); numbers.add(20);

        System.out.println(numbers); // [10, 20, 30, 40] (자동 정렬됨)
    }
}

 

수의 경우 이렇게 일반적으로 작은 수와 큰 수에 대한 비교 기준이 있어서 알아서 정렬되지만,

사용자 정의 객체(ex. Person 클래스)의 경우는 기준을 어떻게 정하느냐에 따라서 정렬 순서가 달라지기 때문에

TreeSet에서 사용자 정의 객체를 저장하려면 Comparable 인터페이스를 구현하거나 Comparator를 제공해야 한다.


✅ Comparable 인터페이스 구현 예제

import java.util.TreeSet;

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

    public Person(String name, int age) {
        this.name = name;
        this.age = age;
    }
    @Override
    public int compareTo(Person other) {
        return Integer.compare(this.age, other.age); // 나이 기준 정렬 (오름차순)
    }
    @Override
    public String toString() {
        return name + " (" + age + ")";
    }
}

public class TreeSetComparableExample {
    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(people);
    }
}

 

✅Comparable와 Comparator의 차이

방식 특징 클래스 수정 여부 여러 정렬 기준 예제
Comparable<T> 클래스 내부에서 정렬 기준을 정의 클래스(객체) 내부에서
compareTo() 오버라이딩 필요
단일 정렬 기준만 가능 class Person implements Comparable<Person>
Comparator<T> 클래스 외부에서 정렬 기준을 지정 클래스 외부에서
정렬 기준을 설정 가능
(클래스 수정 불필요)
여러 개의 Comparator를 만들 수 있음 new TreeSet<> (new PersonComaparator())

💡 객체 내부에서 정렬 기준을 지정하려면 Comparable,

     외부에서 정렬 기준을 지정하려면 Comparator를 사용한다.

Comparator를 사용하면 객체 내부에서 compareTo()를 오버라이딩하지 않고도,

TreeSet 등을 생성할 때 정렬 방식을 지정할 수 있다!

 

📌 Comparator 인터페이스란?

  • Comparator<T> 인터페이스 객체의 정렬 기준을 외부에서 정의하는 인터페이스
  • compare(T o1, T o2) 메서드를 오버라이딩하여 정렬 기준을 설정
  • TreeSet을 생성할 때 정렬 기준을 생성자에 전달하여 사용

✅ Comparator 인터페이스 구현 예제

import java.util.TreeSet;
import java.util.Comparator;

class Person {
    String name;
    int age;

    public Person(String name, int age) {
        this.name = name;
        this.age = age;
    }
    @Override
    public String toString() {
        return name + " (" + age + ")";
    }
}

// Comparator 구현 (나이 기준 정렬)
class AgeComparator implements Comparator<Person> {
    @Override
    public int compare(Person p1, Person p2) {
        return Integer.compare(p1.age, p2.age); // 나이가 적은 순서대로 정렬
    }
}

public class TreeSetComparatorExample {
    public static void main(String[] args) {
        // TreeSet 생성 시 Comparator 전달
        TreeSet<Person> people = new TreeSet<>(new AgeComparator());

        people.add(new Person("Alice", 30));
        people.add(new Person("Bob", 25));
        people.add(new Person("Charlie", 35));
        people.add(new Person("David", 20));

        System.out.println(people); // 나이순 정렬됨
    }
}

나이 순이 아닌, 이름순(사전순)으로 정렬하려면 Comparator를 밑의 코드와 같이 구현하면 된다.

// Comparator 구현 (이름 기준 정렬)
class NameComparator implements Comparator<Person> {
    @Override
    public int compare(Person p1, Person p2) {
        return p1.name.compareTo(p2.name); // 이름이 사전순(오름차순)으로 정렬됨
    }
}

또, 여러 기준으로 정렬할 수도 있다.

가령 일단 나이순으로 정렬하고, 같은 나이면 이름순으로 정렬하는 Comparator는 다음과 같이 작성 가능하다.

// Comparator 구현 (나이 → 같은 나이면 이름 순 정렬)
class AgeNameComparator implements Comparator<Person> {
    @Override
    public int compare(Person p1, Person p2) {
        if (p1.age == p2.age) {
            return p1.name.compareTo(p2.name); // 나이가 같으면 이름 순 정렬
        }
        return Integer.compare(p1.age, p2.age); // 나이가 적은 순서대로 정렬
    }
}
메서드 정의 위치 비교 대상 예시
compareTo(T o) Comparable<T> 인터페이스 자기 자신 vs 다른 객체 p1.name.compareTo(p2.name)
compare(T o1, T o2) Comparator<T> 인터페이스 두 객체 비교 Integer.compare(p1.age, p2.age)

🔹 이름 순 정렬 – p1.name.compareTo(p2.name)

  • String 클래스는 이미 Comparable<String>을 구현하고 있어서
  • compareTo 메서드를 통해 사전 순 정렬을 쉽게 할 수 있다.
  • 예: "Alice".compareTo("Bob") → 음수 (Alice가 앞에 옴)

🔹 나이 순 정렬 – Integer.compare(p1.age, p2.age)

  • int는 기본 타입이라 직접 compareTo를 쓰지 못한다.
  • 대신 Integer.compare()는 정수끼리 비교해서
    • p1.age < p2.age → 음수
    • 같으면 0
    • p1.age > p2.age → 양수
      이런 방식으로 일관성 있는 정수 비교를 해준다.

즉,

  • String은 이미 Comparable을 구현했으니 compareTo를 활용
  • int는 직접 비교 로직이 필요하니 Integer.compare()를 사용

📌 Comparator.comparing()을 활용한 정렬 (람다 표현식 사용)

자바 8 이상에서는 Comparator.comparing()을 활용하면 더 간단하게 정렬 기준을 설정할 수 있다.

// 나이 순 정렬
TreeSet<Person> people = new TreeSet<>(Comparator.comparing(Person::getAge));

// 이름 순 정렬
TreeSet<Person> people = new TreeSet<>(Comparator.comparing(Person::getName));

// 나이 순 → 같은 나이면 이름 순 정렬
TreeSet<Person> people = new TreeSet<>(
    Comparator.comparing(Person::getAge).thenComparing(Person::getName)
);

여기서, Person::getAge는 Person 클래스에 정의된 getAge() 메서드를 참조하는 것을 의미한다.

이는 메서드 참조(Method Reference) 방식으로, 람다 표현식의 축약형!

class Person {
    private String name;
    private int age;

    public Person(String name, int age) {
        this.name = name;
        this.age = age;
    }
    public int getAge() { // ✅ getAge() 메서드 정의
        return age;
    }
    public String getName() { // ✅ getName()도 같이 정의
        return name;
    }
    @Override
    public String toString() {
        return name + " (" + age + ")";
    }
}

 

Comparator.comparing(Person::getAge)

   사실상 위 예제에서의 ageComparator와 동일하다:

Comparator<Person> ageComparator = (p1, p2) -> Integer.compare(p1.getAge(), p2.getAge());

즉, Person::getAge는 getAge()를 호출하는 람다 표현식 (p) -> p.getAge()를 간략하게 쓴 것.