프로그래밍 공부
[자바] #11. 컬렉션 프레임워크 - 2. Set (HashSet, TreeSet) 본문
📌 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의 내부 동작 과정
- 객체를 add()로 추가하면, hashCode()를 호출하여 해시 값(숫자)을 계산.
- 해시 값이 같은 그룹(버킷)에 저장됨.
- 객체를 검색할 때도 hashCode()를 사용하여 빠르게 위치를 찾음.
- 만약 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()를 간략하게 쓴 것.
'자바' 카테고리의 다른 글
| [자바] #13. 컬렉션 프레임워크 - 4. Map (0) | 2025.02.16 |
|---|---|
| [자바] #12. 컬렉션 프레임워크 - 3. Queue, Deque (0) | 2025.02.16 |
| [자바] #10. 컬렉션 프레임워크 - 1. 컬렉션에 대한 전반적인 이해와 ArrayList에 대해 (0) | 2025.02.16 |
| [자바] #09. 와일드카드 (Wildcard) - 2. 상한 제한과 하한 제한 (0) | 2025.02.15 |
| [자바] #08. 와일드카드 (Wildcard) - 1. 제네릭 <T>와 와일드카드 (?) (0) | 2025.02.15 |