F-LAB JAVA · 3주차 · Phase 6 · 객체 비교
🎯 마스터 프롬프트 깊이 Unit — 🏆 Phase 6 완주
이 Unit을 끝내면 다음을 답할 수 있어야 한다.
자바의 객체 비교는 4가지 메커니즘 — equals, hashCode, Comparable, Comparator — 의 정밀한 협조로 동작한다.
HashSet/HashMap 은hashCode → equals순서로, TreeSet/TreeMap 은compareTo또는 외부Comparator로만 비교.
같은 객체라도 어떤 컬렉션에 넣느냐에 따라 동작이 다르며, 일관성 을 깨면 예상 못한 버그가 발생.
Collections.sort 는 Timsort (안정 정렬), Arrays.sort(int[]) 는 Dual-Pivot QuickSort (불안정) 를 사용하며,
PriorityQueue 는 이진 힙 (Binary Heap) 자료구조로 O(log n) 우선순위 처리.
한 사람을 식별하는 4가지 방법:
equals = "같은 사람인가" 신원 확인
hashCode = "분류 번호 (지문 인식)"
Comparable = "키 순서로 줄 서기"
Comparator = "다양한 기준으로 줄 세우기 (심판)"
각각의 컬렉션이 어느 방법으로:
- HashSet/HashMap: 지문 인식 → 신원 확인 (hashCode → equals)
- TreeSet/TreeMap: 키 순서 줄 세우기 (compareTo)
- PriorityQueue: 외부 심판 (Comparator) 또는 자신 (Comparable)
- ArrayList: 신원 확인만 (equals)
→ 4가지 메커니즘의 정밀한 협조.
1. 비교의 4가지 메커니즘 통합
2. HashSet vs TreeSet 동작 정밀 분석
3. HashMap vs TreeMap 동작 정밀 분석
4. equals + hashCode + Comparable 일관성 종합
5. Collections.sort, binarySearch 의 정밀
6. PriorityQueue 의 동작 메커니즘
7. 정렬 알고리즘과 Comparator (Timsort, Dual-Pivot QuickSort)
8. 실무 패턴 종합 + Phase 6 졸업 시험 (50 문항)
9. Phase 6 완주 + Phase 7 (I/O) 예고
| 메커니즘 | 용도 | 위치 | 패키지 | 결과 |
|---|---|---|---|---|
== | 참조 비교 | 연산자 | (없음) | boolean |
equals | 논리 동등성 | Object.equals | java.lang | boolean |
hashCode | 정수 표현 | Object.hashCode | java.lang | int |
Comparable.compareTo | 자연 순서 | java.lang.Comparable | java.lang | int (음수/0/양수) |
Comparator.compare | 외부 순서 | java.util.Comparator | java.util | int (음수/0/양수) |
컬렉션 → 사용 메커니즘
ArrayList, LinkedList:
- 인덱스 기반
- contains, indexOf 시 equals
- sort 시 Comparable 또는 Comparator
HashSet, LinkedHashSet:
- hashCode → equals
- 순서 보장 X (또는 삽입 순서, LinkedHashSet)
TreeSet:
- Comparable 또는 Comparator
- equals 무시 (compareTo == 0 만 보고 같다고 판단)
- 자동 정렬
HashMap, LinkedHashMap:
- 키의 hashCode → equals
- 순서 보장 X (또는 삽입 순서, LinkedHashMap)
TreeMap:
- 키의 Comparable 또는 Comparator
- equals 무시 (키의 compareTo == 0 만 보고 같다고 판단)
- 자동 정렬
PriorityQueue:
- Comparable 또는 Comparator
- 힙 구조 유지
// 일관성 깨진 클래스
public class Person {
private String name;
private int age;
public Person(String name, int age) {
this.name = name;
this.age = age;
}
@Override
public boolean equals(Object obj) {
if (!(obj instanceof Person)) return false;
Person p = (Person) obj;
return name.equals(p.name) && age == p.age;
// 이름 + 나이 비교
}
@Override
public int hashCode() {
return Objects.hash(name, age);
}
}
// Comparator (외부)
Comparator<Person> byAge = Comparator.comparing(Person::getAge);
Person p1 = new Person("Alice", 25);
Person p2 = new Person("Bob", 25);
// HashSet — equals 사용
Set<Person> hashSet = new HashSet<>();
hashSet.add(p1);
hashSet.add(p2);
hashSet.size(); // 2 (name 다름 → equals false)
// TreeSet (with Comparator) — compare 사용
Set<Person> treeSet = new TreeSet<>(byAge);
treeSet.add(p1);
treeSet.add(p2);
treeSet.size(); // 1! (age 같음 → compare == 0)
// → 같은 객체, 다른 컬렉션, 다른 동작
시나리오별 선택:
1. "같은 객체인가?"
→ == (참조)
2. "같은 내용인가?"
→ equals
3. "분류용 정수?"
→ hashCode
4. "자연 순서로 정렬?"
→ Comparable
5. "다양한 기준으로 정렬?"
→ Comparator
6. "Set 으로 중복 제거?"
→ equals + hashCode (HashSet)
→ Comparable/Comparator (TreeSet)
7. "Map 으로 키-값?"
→ equals + hashCode (HashMap)
→ Comparable/Comparator (TreeMap)
public class Shipment implements Comparable<Shipment> {
@Id
private Long id;
private String blNo;
private BigDecimal weight;
private LocalDateTime createdAt;
private int priority;
// 1. equals — id 기반 (JPA 권장)
@Override
public boolean equals(Object obj) {
if (this == obj) return true;
if (!(obj instanceof Shipment)) return false;
return id != null && id.equals(((Shipment) obj).id);
}
// 2. hashCode — 클래스 기반 (영속 전 일관)
@Override
public int hashCode() {
return getClass().hashCode();
}
// 3. Comparable — 자연 순서 (id)
@Override
public int compareTo(Shipment other) {
return Long.compare(this.id, other.id);
}
}
// 4. 다양한 Comparator
public class ShipmentComparators {
public static final Comparator<Shipment> BY_WEIGHT =
Comparator.comparing(Shipment::getWeight);
public static final Comparator<Shipment> BY_CREATED_AT =
Comparator.comparing(Shipment::getCreatedAt);
public static final Comparator<Shipment> URGENT_FIRST =
Comparator.comparingInt(Shipment::getPriority).reversed();
}
// 활용
Shipment s1 = ...;
Shipment s2 = ...;
// 다양한 비교
s1.equals(s2); // id 비교
s1.compareTo(s2); // id 순서
ShipmentComparators.BY_WEIGHT.compare(s1, s2); // 무게 비교
// 컬렉션
HashSet<Shipment> uniqueById = new HashSet<>(); // id 중복 제거
TreeSet<Shipment> byNatural = new TreeSet<>(); // id 자연 순서
TreeSet<Shipment> byWeight = new TreeSet<>(ShipmentComparators.BY_WEIGHT);
PriorityQueue<Shipment> urgent = new PriorityQueue<>(ShipmentComparators.URGENT_FIRST);
4가지 비교 메커니즘의 통합적 활용은?
답:
1. ==: 참조 비교 (메모리 주소)
2. equals: 논리 동등성
3. hashCode: 정수 표현
4. Comparable.compareTo: 자연 순서
5. Comparator.compare: 외부 순서
컬렉션별 사용:
일관성의 중요성:
public class HashSet<E> {
private HashMap<E, Object> map;
private static final Object PRESENT = new Object();
public boolean add(E e) {
return map.put(e, PRESENT) == null;
}
}
// HashMap 의 put
public V put(K key, V value) {
// 1. hashCode 계산
int hash = hash(key);
// 2. 버킷 찾기
int index = hash & (table.length - 1);
Node<K, V> node = table[index];
// 3. 같은 키 검사 (equals)
if (node != null && node.hash == hash
&& (node.key == key || (key != null && key.equals(node.key)))) {
return node.value; // 이미 있음
}
// 4. 새 노드 추가
// ...
}
핵심:
public class TreeSet<E> {
private TreeMap<E, Object> m;
private static final Object PRESENT = new Object();
public boolean add(E e) {
return m.put(e, PRESENT) == null;
}
}
// TreeMap 의 put (Red-Black Tree)
public V put(K key, V value) {
Entry<K, V> t = root;
if (t == null) {
// 첫 노드
root = new Entry<>(key, value, null);
size = 1;
return null;
}
int cmp;
Comparator<? super K> cpr = comparator;
// ★ 비교 방법:
// - Comparator 가 있으면 cpr.compare(key, t.key)
// - 없으면 key 의 compareTo(t.key) 사용 (Comparable)
if (cpr != null) {
// Comparator 사용
do {
parent = t;
cmp = cpr.compare(key, t.key);
if (cmp < 0) t = t.left;
else if (cmp > 0) t = t.right;
else return t.setValue(value); // 이미 있음
} while (t != null);
} else {
// Comparable 사용
Comparable<? super K> k = (Comparable<? super K>) key;
do {
parent = t;
cmp = k.compareTo(t.key);
// ...
} while (t != null);
}
// 새 노드 추가 + Red-Black 균형 조정
}
핵심:
public class Money {
private BigDecimal amount;
public Money(BigDecimal amount) {
this.amount = amount;
}
@Override
public boolean equals(Object obj) {
if (!(obj instanceof Money)) return false;
return Objects.equals(amount, ((Money) obj).amount);
}
@Override
public int hashCode() {
return amount.hashCode();
}
}
// BigDecimal 자체의 일관성 깨짐
Money m1 = new Money(new BigDecimal("100.00"));
Money m2 = new Money(new BigDecimal("100.0"));
m1.equals(m2); // false! (BigDecimal.equals 는 scale 비교)
// 하지만 m1.amount.compareTo(m2.amount) == 0 (값 같음)
// HashSet
Set<Money> hashSet = new HashSet<>();
hashSet.add(m1);
hashSet.add(m2);
hashSet.size(); // 2 (equals 다름)
// TreeSet with Comparator
Comparator<Money> byAmount = (a, b) -> a.amount.compareTo(b.amount);
Set<Money> treeSet = new TreeSet<>(byAmount);
treeSet.add(m1);
treeSet.add(m2);
treeSet.size(); // 1! (compareTo 0)
// → 같은 두 객체가 다른 동작
| 작업 | HashSet | TreeSet |
|---|---|---|
| add | O(1) 평균 | O(log n) |
| remove | O(1) 평균 | O(log n) |
| contains | O(1) 평균 | O(log n) |
| 순회 | O(n), 순서 X | O(n), 정렬된 순서 |
| 추가 메서드 | (없음) | first, last, headSet, tailSet, subSet |
HashSet 선택:
✓ 단순 중복 제거
✓ 빠른 검색 필요
✓ 순서 무관
✓ equals + hashCode 정의됨
TreeSet 선택:
✓ 자동 정렬 필요
✓ 범위 검색 (headSet, tailSet)
✓ 첫/마지막 요소 (first, last)
✓ Comparable 또는 Comparator 있음
LinkedHashSet:
✓ 삽입 순서 보장
✓ 빠른 검색 (HashSet 기반)
✓ HashSet 의 비결정 순서 해결
// 함정 1: HashSet 에 mutable 객체
public class MutablePerson {
public String name;
@Override
public boolean equals(Object obj) { ... }
@Override
public int hashCode() {
return name.hashCode();
}
}
Set<MutablePerson> set = new HashSet<>();
MutablePerson p = new MutablePerson();
p.name = "Alice";
set.add(p);
p.name = "Bob"; // ★ 변경
set.contains(p); // false! (hashCode 변경됨)
set.remove(p); // ❌ 못 지움 → memory leak
// 해결: 불변 객체
// 함정 2: TreeSet 에서 equals 와 compareTo 불일치
public class BadClass implements Comparable<BadClass> {
@Override
public int compareTo(BadClass other) {
return 0; // 모두 같다고 함
}
@Override
public boolean equals(Object obj) {
return false; // 모두 다르다고 함
}
}
TreeSet<BadClass> set = new TreeSet<>();
set.add(new BadClass());
set.add(new BadClass());
set.size(); // 1! (compareTo 0)
// HashSet
Set<BadClass> hashSet = new HashSet<>();
hashSet.add(new BadClass());
hashSet.add(new BadClass());
hashSet.size(); // 2 (equals false)
// → 일관성 깨짐
HashSet vs TreeSet 의 결정적 차이는?
답:
1. 비교 방법:
자료 구조:
시간 복잡도:
순서:
결과 차이:
public class HashMap<K, V> {
Node<K, V>[] table; // 버킷 배열
int size;
int threshold; // resize 트리거
float loadFactor; // 기본 0.75
public V put(K key, V value) {
int hash = hash(key); // hashCode 변형
int index = hash & (table.length - 1); // 버킷 인덱스
Node<K, V> node = table[index];
if (node == null) {
// 빈 버킷 — 새 노드
table[index] = new Node<>(hash, key, value, null);
} else {
// collision — equals 로 확인
Node<K, V> existing = findNode(node, hash, key);
if (existing != null) {
V old = existing.value;
existing.value = value;
return old;
}
// 추가 (LinkedList 또는 Tree)
}
if (++size > threshold) {
resize(); // 2배 확장
}
return null;
}
}
핵심:
public class TreeMap<K, V> {
private Entry<K, V> root; // Red-Black Tree 루트
private final Comparator<? super K> comparator;
public V put(K key, V value) {
Entry<K, V> t = root;
if (t == null) {
// 첫 노드
if (comparator == null) {
// Comparable 필요
((Comparable<?>) key).compareTo(key);
}
root = new Entry<>(key, value, null);
return null;
}
Entry<K, V> parent;
int cmp;
// 비교 방법 결정
if (comparator != null) {
// 외부 Comparator
do {
parent = t;
cmp = comparator.compare(key, t.key);
if (cmp < 0) t = t.left;
else if (cmp > 0) t = t.right;
else return t.setValue(value);
} while (t != null);
} else {
// Comparable
Comparable<? super K> k = (Comparable<? super K>) key;
do {
parent = t;
cmp = k.compareTo(t.key);
if (cmp < 0) t = t.left;
else if (cmp > 0) t = t.right;
else return t.setValue(value);
} while (t != null);
}
// 새 노드 + 균형 조정
Entry<K, V> e = new Entry<>(key, value, parent);
if (cmp < 0) parent.left = e;
else parent.right = e;
fixAfterInsertion(e); // Red-Black 균형
size++;
return null;
}
}
핵심:
| 항목 | HashMap | TreeMap |
|---|---|---|
| 자료 구조 | 해시 테이블 | Red-Black Tree |
| 비교 | hashCode + equals | compareTo or Comparator |
| put | O(1) 평균 | O(log n) |
| get | O(1) 평균 | O(log n) |
| 순서 | 무순서 | 키 정렬 순서 |
| 추가 메서드 | (없음) | firstKey, lastKey, headMap, tailMap, subMap |
| null 키 | 허용 (1개) | 보통 불가 (Comparator 가 처리 시 가능) |
| null 값 | 허용 | 허용 |
// 같은 값, 다른 scale 의 BigDecimal
BigDecimal k1 = new BigDecimal("100.00");
BigDecimal k2 = new BigDecimal("100.0");
k1.equals(k2); // false
k1.hashCode() == k2.hashCode(); // false
k1.compareTo(k2); // 0 (값 같음)
// HashMap
Map<BigDecimal, String> hashMap = new HashMap<>();
hashMap.put(k1, "value1");
hashMap.get(k2); // null! (equals 다름, 다른 버킷)
hashMap.size(); // 1
hashMap.put(k2, "value2");
hashMap.size(); // 2! (다른 키로 인식)
// TreeMap
Map<BigDecimal, String> treeMap = new TreeMap<>();
treeMap.put(k1, "value1");
treeMap.get(k2); // "value1"! (compareTo 0, 같다고 봄)
treeMap.put(k2, "value2");
treeMap.size(); // 1! (덮어씀)
// → 완전히 다른 동작
// LinkedHashMap — HashMap + 양방향 LinkedList
public class LinkedHashMap<K, V> extends HashMap<K, V> {
// Entry 가 추가로 before/after 포인터
private LinkedHashMap.Entry<K, V> head;
private LinkedHashMap.Entry<K, V> tail;
// 삽입 순서 보장
// 또는 접근 순서 (accessOrder = true 면)
}
// 사용
Map<String, Integer> ages = new LinkedHashMap<>();
ages.put("Charlie", 30);
ages.put("Alice", 25);
ages.put("Bob", 28);
// 순회 — 삽입 순서
for (Map.Entry<String, Integer> e : ages.entrySet()) {
System.out.println(e.getKey());
}
// → Charlie, Alice, Bob (삽입 순서)
// 비교: HashMap 은 무순서
// 비교: TreeMap 은 정렬 순서 (Alice, Bob, Charlie)
HashMap 선택:
✓ 빠른 키-값 매핑
✓ 순서 무관
✓ 키가 equals + hashCode 정의
TreeMap 선택:
✓ 키 자동 정렬
✓ 범위 검색 (headMap, tailMap)
✓ 첫/마지막 키
✓ NavigableMap 메서드 (floorKey, ceilingKey 등)
LinkedHashMap 선택:
✓ 삽입 순서 보장
✓ LRU 캐시 구현
✓ HashMap 의 비결정 순서 해결
ConcurrentHashMap 선택:
✓ 멀티스레드
✓ 락 분할 (segment)
// TreeMap 의 풍부한 메서드
TreeMap<Integer, String> map = new TreeMap<>();
map.put(1, "a");
map.put(3, "c");
map.put(5, "e");
map.put(7, "g");
// 범위
map.firstKey(); // 1
map.lastKey(); // 7
map.floorKey(4); // 3 (≤ 4)
map.ceilingKey(4); // 5 (≥ 4)
map.lowerKey(3); // 1 (< 3)
map.higherKey(3); // 5 (> 3)
map.headMap(5); // {1=a, 3=c}
map.tailMap(5); // {5=e, 7=g}
map.subMap(3, 7); // {3=c, 5=e}
// 내림차순
NavigableMap<Integer, String> desc = map.descendingMap();
// → {7=g, 5=e, 3=c, 1=a}
public class ShipmentService {
// 빠른 검색 — HashMap
private final Map<Long, Shipment> cache = new HashMap<>();
// 시간순 정렬 — TreeMap
private final TreeMap<LocalDateTime, Shipment> timeline = new TreeMap<>();
// 일별 통계 — TreeMap
private final TreeMap<LocalDate, BigDecimal> dailySales = new TreeMap<>();
public Shipment findById(Long id) {
return cache.get(id); // O(1)
}
public Map<LocalDate, BigDecimal> getSalesInRange(LocalDate from, LocalDate to) {
return dailySales.subMap(from, true, to, true); // 범위 검색
}
public LocalDateTime getLastUpdate() {
return timeline.lastKey(); // 가장 최근
}
}
HashMap vs TreeMap 의 정밀한 차이는?
답:
1. 비교 방식:
자료 구조:
시간 복잡도:
순서:
추가 기능:
→ 빠른 검색은 HashMap, 정렬 + 범위는 TreeMap.
1. equals + hashCode 일관성:
x.equals(y) => x.hashCode() == y.hashCode()
2. equals + compareTo 일관성 (권장):
x.compareTo(y) == 0 <=> x.equals(y)
3. Comparator + equals 일관성 (이상):
compare(x, y) == 0 <=> x.equals(y)
하지만 Comparator 는 다양한 기준이라 강제 X
public class Person implements Comparable<Person> {
private String name;
private int age;
@Override
public boolean equals(Object obj) {
if (!(obj instanceof Person)) return false;
Person p = (Person) obj;
return name.equals(p.name) && age == p.age;
// 이름 + 나이
}
@Override
public int hashCode() {
return Objects.hash(name, age); // equals 와 일치
}
@Override
public int compareTo(Person other) {
return Integer.compare(age, other.age); // ❌ age 만
// equals 와 불일치
}
}
Person p1 = new Person("Alice", 25);
Person p2 = new Person("Bob", 25);
// HashSet (equals + hashCode 사용)
Set<Person> hashSet = new HashSet<>();
hashSet.add(p1);
hashSet.add(p2);
hashSet.size(); // 2 (다른 객체)
// TreeSet (compareTo 만 사용)
Set<Person> treeSet = new TreeSet<>();
treeSet.add(p1);
treeSet.add(p2);
treeSet.size(); // 1! (compareTo == 0)
// → 같은 두 객체가 다른 컬렉션에서 다른 크기
// 디버깅 매우 어려움
일관성 깨졌을 때 영향 받는 곳:
1. HashSet, HashMap (equals + hashCode)
- 일관성 깨지면 같은 객체 못 찾음
- put 후 get null
2. TreeSet, TreeMap (compareTo)
- 일관성 깨지면 동등 객체 무시
- 정렬 결과 예측 불가
3. ArrayList.contains, indexOf (equals)
- 일관성과 무관
4. Collections.sort, Stream.sorted (compareTo/Comparator)
- 결과만 보면 OK
- 동등 객체의 순서 비결정
5. PriorityQueue (Comparable/Comparator)
- 동등 객체의 순서 비결정
- 일관성과 무관
6. Collections.binarySearch (compareTo/Comparator)
- 일관성 깨지면 잘못된 인덱스
// 패턴 1: 같은 필드 사용
public class Person implements Comparable<Person> {
private String name;
private int age;
@Override
public boolean equals(Object obj) {
if (!(obj instanceof Person)) return false;
Person p = (Person) obj;
return age == p.age && Objects.equals(name, p.name);
}
@Override
public int hashCode() {
return Objects.hash(name, age);
}
@Override
public int compareTo(Person other) {
int cmp = Integer.compare(age, other.age);
if (cmp != 0) return cmp;
return name.compareTo(other.name);
// age + name (equals 와 같은 필드)
}
}
// 검증
Person p1 = new Person("Alice", 25);
Person p2 = new Person("Alice", 25);
p1.equals(p2); // true
p1.hashCode() == p2.hashCode(); // true
p1.compareTo(p2); // 0
// 모든 컬렉션에서 일관된 동작
// Java 16+ record
public record Person(String name, int age) implements Comparable<Person> {
// equals, hashCode 자동 생성 (모든 필드)
// compareTo 만 직접
@Override
public int compareTo(Person other) {
// 같은 필드 사용 권장
int cmp = name.compareTo(other.name);
if (cmp != 0) return cmp;
return Integer.compare(age, other.age);
}
}
// 자동 보장된 일관성 (모든 필드 기반)
// BigDecimal 은 의도적 불일치
BigDecimal a = new BigDecimal("100.00");
BigDecimal b = new BigDecimal("100.0");
a.equals(b); // false (scale 다름)
a.compareTo(b); // 0 (값 같음)
// 영향
Set<BigDecimal> hashSet = new HashSet<>();
hashSet.add(a);
hashSet.add(b);
hashSet.size(); // 2
Set<BigDecimal> treeSet = new TreeSet<>();
treeSet.add(a);
treeSet.add(b);
treeSet.size(); // 1
// 해결 — stripTrailingZeros
BigDecimal normalized = a.stripTrailingZeros();
// 정규화하여 사용
@Test
void testFullConsistency() {
Person p1 = new Person("Alice", 25);
Person p2 = new Person("Alice", 25);
Person p3 = new Person("Bob", 30);
// equals
assertEquals(p1, p2);
assertNotEquals(p1, p3);
// hashCode (equals 일치)
if (p1.equals(p2)) {
assertEquals(p1.hashCode(), p2.hashCode());
}
// compareTo (equals 일치)
assertEquals(0, p1.compareTo(p2));
assertNotEquals(0, p1.compareTo(p3));
if (p1.compareTo(p2) == 0) {
assertEquals(p1, p2);
}
// 컬렉션 일관성
Set<Person> hashSet = new HashSet<>(List.of(p1, p2, p3));
Set<Person> treeSet = new TreeSet<>(List.of(p1, p2, p3));
assertEquals(hashSet.size(), treeSet.size()); // 2 ✓
}
equals/hashCode/compareTo 의 일관성이 깨지면?
답:
1. HashSet/HashMap:
TreeSet/TreeMap:
컬렉션 간 불일치:
권장:
의도적 예외:
public static <T extends Comparable<? super T>> void sort(List<T> list) {
list.sort(null); // null = 자연 순서
}
public static <T> void sort(List<T> list, Comparator<? super T> c) {
list.sort(c);
}
// List.sort 의 기본 구현
default void sort(Comparator<? super E> c) {
Object[] a = this.toArray();
Arrays.sort(a, (Comparator) c);
ListIterator<E> i = this.listIterator();
for (Object e : a) {
i.next();
i.set((E) e);
}
}
핵심:
// 객체 배열 — TimSort
public static void sort(Object[] a) {
if (LegacyMergeSort.userRequested)
legacyMergeSort(a);
else
ComparableTimSort.sort(a, 0, a.length, null, 0, 0);
}
// primitive 배열 — Dual-Pivot QuickSort (int, long, double 등)
public static void sort(int[] a) {
DualPivotQuicksort.sort(a, 0, 0, a.length);
}
핵심:
public static <T> int binarySearch(
List<? extends Comparable<? super T>> list, T key) {
return indexedBinarySearch(list, key);
}
public static <T> int binarySearch(
List<? extends T> list, T key, Comparator<? super T> c) {
if (c == null) return binarySearch(list, key); // Comparable 활용
return indexedBinarySearch(list, key, c);
}
private static <T> int indexedBinarySearch(
List<? extends Comparable<? super T>> list, T key) {
int low = 0;
int high = list.size() - 1;
while (low <= high) {
int mid = (low + high) >>> 1;
Comparable<? super T> midVal = list.get(mid);
int cmp = midVal.compareTo(key);
if (cmp < 0) low = mid + 1;
else if (cmp > 0) high = mid - 1;
else return mid; // 발견
}
return -(low + 1); // 음수 반환
}
특징:
List<Integer> sorted = Arrays.asList(1, 3, 5, 7, 9);
int found = Collections.binarySearch(sorted, 5);
// 2 (인덱스)
int notFound = Collections.binarySearch(sorted, 4);
// -3 (음수 = 없음)
// 절댓값 - 1 = 2 (4 가 삽입될 위치, sorted.get(2) 앞)
// 삽입 위치 계산
int insertionPoint = (notFound < 0) ? -(notFound + 1) : notFound;
// 2
// 정렬 유지하며 삽입
List<Integer> mutable = new ArrayList<>(sorted);
mutable.add(insertionPoint, 4);
// → [1, 3, 4, 5, 7, 9]
// 정렬 안 된 List
List<Integer> unsorted = Arrays.asList(5, 1, 9, 3, 7);
int idx = Collections.binarySearch(unsorted, 3);
// 결과 예측 불가!
// 이진 검색이 잘못된 방향으로 진행
// 정답이 있어도 못 찾을 수 있음
// Stream.sorted — 자연 순서
List<Integer> sorted = Stream.of(3, 1, 4, 1, 5)
.sorted()
.toList();
// Stream.sorted(Comparator)
List<Person> sorted = people.stream()
.sorted(Comparator.comparing(Person::getAge))
.toList();
// 내부: TimSort 변형 (또는 안정 정렬)
안정 정렬 (Stable):
동일한 키의 원래 순서 유지
- TimSort (Collections.sort)
- MergeSort
불안정 정렬 (Unstable):
동일한 키의 순서 보장 X
- QuickSort (Arrays.sort for primitives)
- HeapSort
예시:
[(1, "a"), (1, "b"), (2, "c")]
안정 정렬 후 (키만 비교):
[(1, "a"), (1, "b"), (2, "c")] # a, b 순서 유지
불안정 정렬 후:
[(1, "b"), (1, "a"), (2, "c")] # a, b 순서 바뀔 수 있음
// 객체 — 안정 (TimSort)
Person[] people = ...;
Arrays.sort(people, Comparator.comparing(Person::getAge));
// primitive — 불안정 (Dual-Pivot QuickSort)
int[] nums = {3, 1, 4, 1, 5};
Arrays.sort(nums);
// 안정성 보장 X (primitive 라 어차피 같으면 같음)
// 객체 List.sort 도 안정
List<Person> list = new ArrayList<>(...);
list.sort(Comparator.comparing(Person::getAge));
Collections.sort 와 binarySearch 의 정밀 동작은?
답:
1. Collections.sort:
binarySearch:
안정 정렬 (TimSort):
불안정 정렬 (Dual-Pivot QuickSort):
public class PriorityQueue<E> extends AbstractQueue<E> {
private Object[] queue; // 이진 힙 배열
private int size;
private final Comparator<? super E> comparator;
public PriorityQueue() {
this(11, null); // Comparable 사용
}
public PriorityQueue(Comparator<? super E> comparator) {
this(11, comparator);
}
public PriorityQueue(int initialCapacity, Comparator<? super E> comparator) {
this.queue = new Object[initialCapacity];
this.comparator = comparator;
}
}
핵심:
이진 힙의 특성:
- 완전 이진 트리
- 부모 ≤ 자식 (최소 힙)
- 배열로 표현
- 부모 인덱스 i 이면:
- 왼쪽 자식: 2i + 1
- 오른쪽 자식: 2i + 2
- 부모: (i - 1) / 2
예: [1, 3, 5, 7, 4, 8, 9]
1
/ \
3 5
/ \ / \
7 4 8 9
배열 인덱스: 0, 1, 2, 3, 4, 5, 6
// offer — 추가 (sift-up)
public boolean offer(E e) {
int i = size;
size = i + 1;
if (i == 0) {
queue[0] = e; // 첫 요소
} else {
siftUp(i, e); // 부모와 비교, 작으면 교환
}
return true;
}
private void siftUp(int k, E x) {
while (k > 0) {
int parent = (k - 1) >>> 1;
Object e = queue[parent];
if (compare(x, (E) e) >= 0) break; // 부모보다 크면 멈춤
queue[k] = e;
k = parent;
}
queue[k] = x;
}
// poll — 제거 (sift-down)
public E poll() {
if (size == 0) return null;
int s = --size;
E result = (E) queue[0]; // 루트 = 최소값
E x = (E) queue[s]; // 마지막 요소
queue[s] = null;
if (s != 0) {
siftDown(0, x); // 마지막을 루트에, 자식과 비교
}
return result;
}
private void siftDown(int k, E x) {
int half = size >>> 1;
while (k < half) {
int child = (k << 1) + 1;
Object c = queue[child];
int right = child + 1;
if (right < size && compare((E) c, (E) queue[right]) > 0) {
c = queue[child = right];
}
if (compare(x, (E) c) <= 0) break;
queue[k] = c;
k = child;
}
queue[k] = x;
}
| 작업 | 시간 복잡도 |
|---|---|
| offer (add) | O(log n) — sift-up |
| poll (remove) | O(log n) — sift-down |
| peek | O(1) — 루트 |
| size | O(1) |
| contains | O(n) — 선형 검색 |
| remove(item) | O(n) |
// 기본 — 최소 힙
PriorityQueue<Integer> minHeap = new PriorityQueue<>();
minHeap.offer(5);
minHeap.offer(2);
minHeap.offer(8);
minHeap.poll(); // 2 (최소)
// 최대 힙 — reverseOrder
PriorityQueue<Integer> maxHeap = new PriorityQueue<>(Comparator.reverseOrder());
maxHeap.offer(5);
maxHeap.offer(2);
maxHeap.offer(8);
maxHeap.poll(); // 8 (최대)
// 사용자 정의 비교
PriorityQueue<Shipment> urgentFirst = new PriorityQueue<>(
Comparator.comparingInt(Shipment::getPriority).reversed()
);
// 1. K 번째 큰 원소
public int findKthLargest(int[] nums, int k) {
PriorityQueue<Integer> heap = new PriorityQueue<>();
for (int n : nums) {
heap.offer(n);
if (heap.size() > k) heap.poll();
}
return heap.peek();
}
// 2. 다익스트라 알고리즘 (최단 경로)
public int dijkstra(Graph g, int source) {
PriorityQueue<int[]> pq = new PriorityQueue<>(
Comparator.comparingInt(a -> a[1])
);
pq.offer(new int[]{source, 0});
// ...
}
// 3. ILIC — 긴급 출하 큐
PriorityQueue<Shipment> urgentQueue = new PriorityQueue<>(
Comparator.comparingInt(Shipment::getPriority).reversed()
.thenComparing(Shipment::getCreatedAt)
);
urgentQueue.offer(shipment1);
urgentQueue.offer(shipment2);
Shipment next = urgentQueue.poll(); // 가장 긴급한 것
PriorityQueue<Integer> queue = new PriorityQueue<>();
queue.offer(5);
queue.offer(1);
queue.offer(3);
queue.offer(2);
// ❌ 순회는 정렬 순서 보장 X
for (Integer n : queue) {
System.out.println(n);
}
// → 1, 5, 3, 2 (힙 구조 그대로)
// ✓ 정렬된 순서로 꺼내기
while (!queue.isEmpty()) {
System.out.println(queue.poll());
}
// → 1, 2, 3, 5
→ PriorityQueue 의 순회는 순서 보장 X. poll() 만 순서.
PriorityQueue 의 동작 메커니즘은?
답:
1. 자료 구조:
비교:
시간 복잡도:
활용:
주의:
| 알고리즘 | 평균 | 최악 | 공간 | 안정성 | 활용 |
|---|---|---|---|---|---|
| QuickSort | O(n log n) | O(n²) | O(log n) | 불안정 | 기본 |
| MergeSort | O(n log n) | O(n log n) | O(n) | 안정 | 객체 정렬 |
| HeapSort | O(n log n) | O(n log n) | O(1) | 불안정 | 메모리 제약 |
| TimSort | O(n log n) | O(n log n) | O(n) | 안정 | Java 표준 |
| Dual-Pivot QuickSort | O(n log n) | O(n²) | O(log n) | 불안정 | primitive |
TimSort:
- Tim Peters (Python 의 정렬 발명)
- MergeSort + InsertionSort 결합
- Java 7+ 의 객체 정렬 표준
- 안정 정렬
- 부분 정렬된 데이터에 매우 효율적
특징:
- 작은 청크 → InsertionSort
- 큰 청크 → MergeSort
- "Run" 개념 (이미 정렬된 부분 활용)
- 실세계 데이터에 빠름
Dual-Pivot QuickSort:
- 2개의 pivot 사용
- Java 7+ primitive 배열 정렬
- QuickSort 의 개선판
- 불안정 정렬
알고리즘:
- 2개 pivot p1, p2 (p1 < p2) 선택
- 3개 구간으로 분할:
- < p1
- p1 ~ p2
- > p2
- 각 구간 재귀
장점:
- 일반 QuickSort 보다 30% 빠름
- primitive 배열에 효율적
// TimSort 는 Comparator 활용
Person[] people = ...;
Arrays.sort(people, Comparator.comparing(Person::getAge));
// 내부:
// 1. TimSort 가 동작
// 2. 두 요소 비교 시 comparator.compare(p1, p2) 호출
// 3. 결과로 정렬
// 즉, Comparator 는 알고리즘의 비교 단계에 호출
// 안정성이 필요한 시나리오
// 1. 다단계 정렬
List<Person> people = ...;
// 1차 정렬: 이름
people.sort(Comparator.comparing(Person::getName));
// 2차 정렬: 나이
people.sort(Comparator.comparing(Person::getAge));
// 결과:
// 안정 정렬 → 같은 나이 내에서 이름순 유지 ✓
// 불안정 정렬 → 같은 나이 내 이름순 깨질 수 있음 ✗
// 2. 이미 정렬된 부분 활용
// 부분 정렬된 데이터에 TimSort 가 매우 효율적
// Stream.sorted 도 안정 정렬
List<Person> sorted = people.stream()
.sorted(Comparator.comparing(Person::getName))
.sorted(Comparator.comparing(Person::getAge))
.toList();
// 두 번 정렬해도 이름 순서 유지 (안정)
// 큰 데이터셋
List<Person> people = generateRandom(1_000_000);
// 일반 정렬
long start = System.nanoTime();
people.sort(Comparator.comparing(Person::getAge));
long t1 = System.nanoTime() - start;
// primitive 정렬 (int[] 추출 후)
int[] ages = people.stream().mapToInt(Person::getAge).toArray();
start = System.nanoTime();
Arrays.sort(ages);
long t2 = System.nanoTime() - start;
// t2 가 보통 더 빠름 (Dual-Pivot + autoboxing 회피)
public class ShipmentSortService {
// 단순 정렬 — TimSort
public List<Shipment> sortByCreatedAt(List<Shipment> shipments) {
return shipments.stream()
.sorted(Comparator.comparing(Shipment::getCreatedAt))
.toList();
}
// 다단계 정렬 — TimSort 의 안정성 활용
public List<Shipment> multiSort(List<Shipment> shipments) {
return shipments.stream()
.sorted(Comparator.comparing(Shipment::getCreatedAt)) // 2차
.sorted(Comparator.comparing(Shipment::getPriority).reversed()) // 1차
.toList();
// 같은 priority 내에서 createdAt 순서 유지
}
// primitive 정렬 — Dual-Pivot QuickSort
public int[] sortWeights(List<Shipment> shipments) {
int[] weights = shipments.stream()
.mapToInt(s -> s.getWeight().intValue())
.toArray();
Arrays.sort(weights); // primitive 정렬
return weights;
}
}
자바의 정렬 알고리즘과 Comparator 의 역할은?
답:
1. TimSort (객체):
Dual-Pivot QuickSort (primitive):
Comparator 의 역할:
안정성의 활용:
패턴 1: Entity 의 비교
class Shipment implements Comparable<Shipment> {
@Id Long id;
String blNo;
@Override
public boolean equals(Object obj) {
return id != null && id.equals(((Shipment) obj).id);
}
@Override
public int hashCode() {
return getClass().hashCode();
}
@Override
public int compareTo(Shipment other) {
return Long.compare(this.id, other.id);
}
}
// 다양한 Comparator
class ShipmentComparators {
public static final Comparator<Shipment> BY_WEIGHT = ...;
public static final Comparator<Shipment> URGENT_FIRST = ...;
}
패턴 2: 값 객체 (Value Object) — record
public record Money(BigDecimal amount, String currency)
implements Comparable<Money> {
@Override
public int compareTo(Money other) {
if (!currency.equals(other.currency)) {
throw new IllegalArgumentException();
}
return amount.compareTo(other.amount);
}
// equals, hashCode 자동
}
패턴 3: 정렬 + 페이지네이션
public Page<Shipment> findShipments(int page, int size, String sortBy) {
Comparator<Shipment> comparator = switch (sortBy) {
case "weight" -> Comparator.comparing(Shipment::getWeight);
case "createdAt" -> Comparator.comparing(Shipment::getCreatedAt);
case "priority" -> Comparator.comparingInt(Shipment::getPriority).reversed();
default -> Comparator.comparing(Shipment::getId);
};
List<Shipment> all = repository.findAll();
all.sort(comparator);
int from = page * size;
int to = Math.min(from + size, all.size());
return new Page<>(all.subList(from, to), all.size());
}
패턴 4: TreeMap 으로 시간 순 통계
TreeMap<LocalDate, BigDecimal> dailyTotal = shipments.stream()
.collect(Collectors.groupingBy(
s -> s.getCreatedAt().toLocalDate(),
TreeMap::new,
Collectors.reducing(
BigDecimal.ZERO,
Shipment::getFare,
BigDecimal::add
)
));
// 범위 조회
NavigableMap<LocalDate, BigDecimal> lastMonth = dailyTotal
.tailMap(LocalDate.now().minusDays(30));
패턴 5: PriorityQueue 로 작업 스케줄러
class TaskScheduler {
private PriorityQueue<Task> queue = new PriorityQueue<>(
Comparator.comparingInt(Task::getPriority).reversed()
);
public void enqueue(Task task) {
queue.offer(task);
}
public Task next() {
return queue.poll(); // 가장 우선순위 높은 것
}
}
Q1. == 와 equals 의 차이?
A1. 참조 vs 논리 비교
Q2. Object 의 기본 equals?
A2. this == obj (참조 비교)
Q3. equals 의 5가지 계약?
A3. 반사/대칭/추이/일관/null
Q4. hashCode 의 3가지 계약?
A4. 일관, equals 일치, 분포
Q5. equals override 시 왜 hashCode 도?
A5. HashMap/HashSet 동작 위해
Q6. JPA Entity equals 권장?
A6. id 비교 + null 처리
Q7. JPA hashCode 권장?
A7. getClass().hashCode()
Q8. record 의 자동 equals?
A8. 모든 필드 비교
Q9. 상속과 equals 문제?
A9. 추이성 깨짐 (Point/ColorPoint)
Q10. Objects.equals 효과?
A10. null 안전 비교
Q11. Comparable<T> 정의?
A11. java.lang, 단일 메서드
Q12. compareTo 반환값?
A12. 음수/0/양수 (부호만)
Q13. 자연 순서?
A13. Comparable 이 정의하는 순서
Q14. compareTo 5가지 계약?
A14. 반사/대칭/추이/일관/equals 일치
Q15. equals 와 compareTo 일관성?
A15. compareTo == 0 ↔ equals == true (권장)
Q16. Integer.compare 이유?
A16. 오버플로우 안전
Q17. BigDecimal 의 예외?
A17. equals 와 compareTo 불일치
Q18. 다중 필드 비교?
A18. if 문 또는 thenComparing
Q19. Enum.compareTo?
A19. ordinal, final
Q20. Comparable 없으면 sort?
A20. ClassCastException
Q21. Comparator<T> 정의?
A21. java.util, 함수형, 외부 비교
Q22. comparing 동작?
A22. 키 추출 + 자연 순서
Q23. thenComparing?
A23. 첫 0 이면 두 번째
Q24. reversed 동작?
A24. 역순 Comparator
Q25. naturalOrder vs reverseOrder?
A25. 자연 vs 역순 (정적)
Q26. nullsFirst/Last?
A26. null 처리
Q27. comparingInt 효과?
A27. autoboxing 회피
Q28. Stream.sorted?
A28. Comparator 활용 정렬
Q29. PriorityQueue 와 Comparator?
A29. 우선순위 결정
Q30. 람다 vs 익명 Comparator?
A30. 람다 권장
Q31. HashSet vs TreeSet 차이?
A31. equals+hashCode vs compareTo
Q32. HashMap vs TreeMap 차이?
A32. 같음 + Red-Black Tree
Q33. TreeSet 의 시간 복잡도?
A33. O(log n)
Q34. HashSet 의 시간 복잡도?
A34. O(1) 평균
Q35. PriorityQueue 자료 구조?
A35. 이진 힙
Q36. PriorityQueue offer 복잡도?
A36. O(log n)
Q37. TimSort?
A37. 객체 정렬, 안정, Java 표준
Q38. Dual-Pivot QuickSort?
A38. primitive 정렬, 불안정
Q39. binarySearch 결과 -3?
A39. 없음, 삽입 위치 = 2
Q40. NavigableMap 메서드?
A40. floorKey, ceilingKey, headMap, tailMap
Q41. equals + hashCode 깨지면?
A41. HashMap.get null
Q42. compareTo + equals 깨지면?
A42. TreeSet vs HashSet 크기 다름
Q43. record 의 자동 일관성?
A43. 모든 필드 기반
Q44. PriorityQueue 의 순회 순서?
A44. 보장 X (poll 만 정렬)
Q45. 안정 정렬의 의미?
A45. 같은 키의 순서 유지
Q46. 다단계 정렬?
A46. sort 여러 번 (안정 정렬)
Q47. parallelStream 정렬?
A47. 큰 데이터셋만 이점
Q48. LinkedHashMap?
A48. 삽입 순서 + HashMap
Q49. 비교 메커니즘 4가지?
A49. equals, hashCode, Comparable, Comparator
Q50. Phase 6 마스터 후?
A50. 자바 객체 비교 자유자재
50 / 50 → Phase 6 마스터
45-49 → 거의 마스터
40-44 → 핵심 복습
< 40 → Unit 6.1 ~ 6.4 재학습
Phase 6 — 객체 비교
Unit 6.1 — equals 와 hashCode 의 계약
- == vs equals
- 5+3 계약
- JPA Entity 패턴
- Lombok, record
Unit 6.2 — Comparable<T> 의 자연 순서
- compareTo 의 의미
- 자연 순서
- 5가지 계약
- 자바 표준 분석
Unit 6.3 — Comparator<T> 의 외부 비교
- 함수형 인터페이스
- comparing/thenComparing
- reversed, naturalOrder
- null 처리, primitive
Unit 6.4 — 비교의 종합 활용 (마스터)
- 4가지 메커니즘 통합
- HashSet vs TreeSet
- HashMap vs TreeMap
- 정렬 알고리즘
- PriorityQueue
1. 객체 비교 자유자재
- equals, hashCode, Comparable, Comparator
- 일관성 보장
- 적절한 도구 선택
2. 컬렉션 동작 정밀 이해
- HashSet, TreeSet
- HashMap, TreeMap, LinkedHashMap
- PriorityQueue
- 각각의 비교 메커니즘
3. 정렬 자유자재
- 단순/다중 정렬
- 안정/불안정 정렬
- TimSort, Dual-Pivot QuickSort
- Comparator 의 풍부한 활용
4. 면접 자신감
- 모든 비교 질문 즉답
- 컬렉션 동작 설명
- 정렬 알고리즘
5. 실무 패턴
- JPA Entity 비교
- 정렬 + 페이지네이션
- 작업 스케줄러 (PriorityQueue)
- 시간순 통계 (TreeMap)
Phase 7 — I/O 시스템 큰 그림
Unit 7.1 — try-with-resources
→ 자원 자동 해제
→ AutoCloseable 인터페이스
Unit 7.2 — NIO.2 Files, Path
→ 현대적 파일 API
→ Path, Files, Paths
Unit 7.3 — NIO Channel, Buffer
→ 채널 기반 I/O
→ Buffer, ByteBuffer
Unit 7.4 — Blocking vs Non-blocking (★ 마스터 깊이)
→ I/O 모델 정밀
→ Selector, NIO 의 본질
Unit 7.5 — 직렬화와 transient
→ ObjectInputStream/OutputStream
→ Serializable 인터페이스
Phase 6: 비교 (equals, hashCode, Comparable, Comparator)
↓
Phase 7: I/O (자원 처리, 파일, 네트워크)
연결:
- I/O 의 자원은 비교 객체와 다름
- 자원은 한정 + 닫아야
- try-with-resources 가 AutoCloseable 활용
- Blocking/Non-blocking 의 본질
✅ Phase 1 — Pass by Value (3 Unit)
✅ Phase 2 — 컬렉션 프레임워크 (6 Unit)
✅ Phase 3 — 해시의 원리 (4 Unit)
✅ Phase 4 — 추상화의 두 도구 (4 Unit)
✅ Phase 5 — 제네릭과 와일드카드 (5 Unit)
✅ Phase 6 — 객체 비교 (4 Unit) ← 완주
🚀 Phase 7 — I/O 시스템 큰 그림 (다음)
⏭ Phase 8 — Stream 실전
⏭ Phase 9 — I/O 강화
⏭ Phase 10 — 함수형 프로그래밍
총: 26/43 Unit 작성 (Phase 6 완주, 약 60%)
1. 4가지 비교 메커니즘
2. 컬렉션 동작의 정밀
3. 정렬 알고리즘
🚀 Phase 6 — 객체 비교
✅ Unit 6.1 equals 와 hashCode 의 계약
✅ Unit 6.2 Comparable<T> 의 자연 순서
✅ Unit 6.3 Comparator<T> 의 외부 비교
✅ Unit 6.4 비교의 종합 활용 (마스터 깊이) ← 여기, Phase 6 완주
→ 자바 객체 비교 정복
→ 4가지 메커니즘 통합
→ 컬렉션 동작 깊이 이해
→ 정렬 알고리즘 + PriorityQueue 마스터
다음 Phase 는 자바 I/O 의 정복.
Phase 7 — I/O 시스템 큰 그림
Unit 7.1 — try-with-resources
→ AutoCloseable 인터페이스
→ 자원 자동 관리
Unit 7.2 — NIO.2 Files, Path
→ 현대적 파일 API
→ Path, Paths, Files
Unit 7.3 — NIO Channel, Buffer
→ 채널 기반 I/O
→ ByteBuffer, FileChannel
Unit 7.4 — Blocking vs Non-blocking (★ 마스터 깊이)
→ I/O 모델 정밀
→ Selector, NIO 의 본질
→ Reactive Programming 기초
Unit 7.5 — 직렬화와 transient
→ ObjectInputStream/OutputStream
→ Serializable
→ 보안 위험과 대안
Phase 6 와의 연결:
F-LAB JAVA · 3주차 · Phase 6 · Unit 6.4 · 끝
🏆 Phase 6 완주 — 객체 비교 마스터 달성