제네릭 Set과 재해시
Object 저장소의 타입 손실을 컴파일 오류로 옮기고 add·remove·포함 여부·확장 규칙을 MyHashSet으로 통합합니다.
정수 전용 MyHashSetV1은 충돌 원리를 보여 주지만 PostTag나 PostKey를 저장할 수 없습니다.
Object로 바꾸면 호출자가 타입을 잘못 섞을 수 있습니다.
이번 리팩터링은 MySet<E> 역할을 먼저 선언하고 구현 내부에서만 Object 배열의 제약을 처리하도록 합니다.
적재 계수가 임계치를 넘을 때 재해시하는 경계도 포함합니다.
잘못된 원소 타입의 저장 거부
원시 타입을 사용하면 String 집합으로 만든 객체에 Integer를 넣을 수 있습니다.
컴파일러는 비검사 경고만 내고 실행은 계속됩니다.
나중에 순회하면서 String으로 사용할 때 ClassCastException이 발생합니다.
import java.util.HashSet;
import java.util.Set;
public final class RawSetHeapPollutionFailure {
public static void main(String[] args) {
Set<String> typed = new HashSet<>();
typed.add("hash");
Set raw = typed;
raw.add(100);
for (String value : typed) {
System.out.println(value.toUpperCase());
}
}
}java.lang.ClassCastException원시 타입은 이전 바이너리 호환을 위한 통로이지 일반 구현 전략이 아닙니다.
MySet<String> 변수의 add에 Integer를 넘기면 그 호출 줄에서 컴파일 실패가 됩니다.
내부 배열 생성 때문에 필요한 비검사 변환은 생성자와 resize의 배열 생성 부분에 제한합니다.
Set 연산의 결과 의미
boolean add(E)는 새 동등성 그룹이 생겼는지, boolean remove(E)는 실제 원소가 사라졌는지 말합니다.
포함 여부는 상태를 바꾸지 않습니다.
size는 저장 요청 횟수가 아니라 현재 서로 다른 원소 수입니다.
반복자 순서는 MySet의 공통 규칙에 넣지 않아 해시 구현이 자유롭게 그룹 순회할 수 있게 합니다.
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import java.util.NoSuchElementException;
import java.util.Objects;
public final class GenericMyHashSet {
public static void main(String[] args) {
MySet<String> set = new HashMySet<>(4);
System.out.println(set.add("hash"));
System.out.println(set.add(new String("hash")));
System.out.println(set.add("set"));
System.out.println(set.add("bucket"));
System.out.println("contains=" + set.contains("set") + ", size=" + set.size());
for (String value : set) {
System.out.println(value);
}
}
private interface MySet<E> extends Iterable<E> {
boolean add(E value);
boolean contains(E value);
boolean remove(E value);
int size();
}
private static final class HashMySet<E> implements MySet<E> {
private static final double MAX_LOAD = 0.75;
private List<E>[] buckets;
private int size;
@SuppressWarnings("unchecked")
HashMySet(int capacity) {
if (capacity <= 0) throw new IllegalArgumentException("capacity");
buckets = (List<E>[]) new List<?>[capacity];
initialize(buckets);
}
public boolean add(E value) {
Objects.requireNonNull(value, "value");
if (contains(value)) return false;
if ((size + 1.0) / buckets.length > MAX_LOAD) resize(buckets.length * 2);
bucket(value).add(value);
size++;
return true;
}
public boolean contains(E value) {
Objects.requireNonNull(value, "value");
return bucket(value).contains(value);
}
public boolean remove(E value) {
Objects.requireNonNull(value, "value");
boolean removed = bucket(value).remove(value);
if (removed) size--;
return removed;
}
public int size() {
return size;
}
private List<E> bucket(E value) {
return buckets[Math.floorMod(value.hashCode(), buckets.length)];
}
@SuppressWarnings("unchecked")
private void resize(int capacity) {
List<E>[] old = buckets;
buckets = (List<E>[]) new List<?>[capacity];
initialize(buckets);
for (List<E> bucket : old) {
for (E value : bucket) {
bucket(value).add(value);
}
}
}
private static <E> void initialize(List<E>[] target) {
for (int i = 0; i < target.length; i++) {
target[i] = new ArrayList<>();
}
}
public Iterator<E> iterator() {
return new Iterator<>() {
private int bucketIndex;
private int elementIndex;
public boolean hasNext() {
advanceEmptyBuckets();
return bucketIndex < buckets.length;
}
public E next() {
if (!hasNext()) throw new NoSuchElementException();
return buckets[bucketIndex].get(elementIndex++);
}
private void advanceEmptyBuckets() {
while (bucketIndex < buckets.length
&& elementIndex >= buckets[bucketIndex].size()) {
bucketIndex++;
elementIndex = 0;
}
}
};
}
}
}네 번의 add와 확장하지 않은 용량 4
중복이 포함된 네 add의 최대 예상 부하는 0.75이고 capacity 4가 유지됩니다. 실제 main에 resize가 없다는 조건을 단계별 호출로 설명합니다.
동등한 hash를 두 번 요청해도 고유 원소는 세 개이며 마지막 예상 적재 계수 0.75는 확장 조건을 넘지 않습니다.
확장 조건 · 0.75보다 클 때
네 번의 add와 확장하지 않은 용량 4
중복 hash는 확장 검사 전에 반환합니다. 마지막 새 값까지 사용률이 정확히 0.75이므로 이 main은 resize를 호출하지 않습니다.
실선: 원 main의 add 순서원 main은 resize를 실행 안 함별도 본문 검산은 key를 더함
add("hash")- 중복·확장 조건의 소스 추적
- 새 값;
1.0 / 4 = 0.250.25 > 0.75는 거짓 - 반환값과 직후 상태
truesize=1, capacity=4
add(new String("hash"))- 중복·확장 조건의 소스 추적
- 기존 문자열과 동등
contains(value)=true; 확장 검사 전에add종료 - 반환값과 직후 상태
falsesize=1, capacity=4
add("set")- 중복·확장 조건의 소스 추적
- 새 값;
2.0 / 4 = 0.50.5 > 0.75는 거짓 - 반환값과 직후 상태
truesize=2, capacity=4
add("bucket")- 중복·확장 조건의 소스 추적
- 새 값;
3.0 / 4 = 0.750.75 > 0.75는 거짓 - 반환값과 직후 상태
truesize=3, capacity=4
이어 contains=true, size=3을 출력합니다. 네 번의 요청 중 고유 값은 세 개여서 이 main은 resize를 호출하지 않습니다. 용량과 조건값은 소스 추적이며 별도 계측 출력이 아닙니다.
이 예의 조건
- 원 main은 resize를 수행하지 않는다.
- 임계값 초과 조건은 >0.75이다.
관계도의 보충 설명
| 구분 | 내용 |
|---|---|
| 2 | 원래 네 add 중 동등한 hash 추가는 중복 검사에서 false이고 새 값 세 개만 남습니다. 예상 적재 0.75는 0.75보다 크지 않아 용량 4 그대로입니다. 실선은 원래 main 순서이며 본문의 별도 key 검산과 구분합니다. |
| 6 | size=1 |
| 7 | 용량 4 |
| 10 | size=1 |
| 11 | 용량 4 유지 |
| 15 | size=2 |
| 16 | 용량 4 |
| 21 | size=3 |
| 22 | 용량 4 |
| 구분 | 내용 |
|---|---|
| 설명 | 원래 네 add 중 동등한 hash 추가는 중복 검사에서 false이고 새 값 세 개만 남습니다. 예상 적재 0.75는 0.75보다 크지 않아 용량 4 그대로입니다. 실선은 원래 main 순서이며 본문의 별도 key 검산과 구분합니다. |
| 전체 읽기 정보 | 네 번의 add와 확장하지 않은 용량 4 네 번의 add와 확장하지 않은 용량 4 동등한 hash를 두 번 요청해도 고유 원소는 세 개이며 마지막 예상 적재 계수 0.75는 확장 조건을 넘지 않습니다. 확장 조건 · 0.75보다 클 때 네 번의 add와 확장하지 않은 용량 4 중복 hash는 확장 검사 전에 반환합니다. 마지막 새 값까지 사용률이 정확히 0.75이므로 이 main은 resize를 호출하지 않습니다. add("hash") 중복·확장 조건의 소스 추적 새 값; 1.0 / 4 = 0.25 0.25 > 0.75 는 거짓 반환값과 직후 상태 true size=1, capacity=4 add(new String("hash")) 중복·확장 조건의 소스 추적 기존 문자열과 동등 contains(value)=true ; 확장 검사 전에 add 종료 반환값과 직후 상태 false size=1, capacity=4 add("set") 중복·확장 조건의 소스 추적 새 값; 2.0 / 4 = 0.5 0.5 > 0.75 는 거짓 반환값과 직후 상태 true size=2, capacity=4 add("bucket") 중복·확장 조건의 소스 추적 새 값; 3.0 / 4 = 0.75 0.75 > 0.75 는 거짓 반환값과 직후 상태 true size=3, capacity=4 이어 contains=true, size=3 을 출력합니다. 네 번의 요청 중 고유 값은 세 개여서 이 main은 resize 를 호출하지 않습니다. 용량과 조건값은 소스 추적이며 별도 계측 출력이 아닙니다. |
| 관찰·상황 | 값·보충 정보 | 변화·이유 |
|---|---|---|
| 첫 등록 hash를 넣고 같은 hash 재추가는 거절합니다. |
|
|
| 둘째 새 값 set을 추가합니다. |
|
|
| 셋째 새 값 bucket을 추가해도 용량이 유지됩니다. |
|
|
네 번의 add와 확장하지 않은 용량 4
중복이 포함된 네 add의 최대 예상 부하는 0.75이고 capacity 4가 유지됩니다. 실제 main에 resize가 없다는 조건을 단계별 호출로 설명합니다.
동등한 hash를 두 번 요청해도 고유 원소는 세 개이며 마지막 예상 적재 계수 0.75는 확장 조건을 넘지 않습니다.
확장 조건 · 0.75보다 클 때
네 번의 add와 확장하지 않은 용량 4
중복 hash는 확장 검사 전에 반환합니다. 마지막 새 값까지 사용률이 정확히 0.75이므로 이 main은 resize를 호출하지 않습니다.
실선: 원 main의 add 순서원 main은 resize를 실행 안 함별도 본문 검산은 key를 더함
add("hash")- 중복·확장 조건의 소스 추적
- 새 값;
1.0 / 4 = 0.250.25 > 0.75는 거짓 - 반환값과 직후 상태
truesize=1, capacity=4
add(new String("hash"))- 중복·확장 조건의 소스 추적
- 기존 문자열과 동등
contains(value)=true; 확장 검사 전에add종료 - 반환값과 직후 상태
falsesize=1, capacity=4
add("set")- 중복·확장 조건의 소스 추적
- 새 값;
2.0 / 4 = 0.50.5 > 0.75는 거짓 - 반환값과 직후 상태
truesize=2, capacity=4
add("bucket")- 중복·확장 조건의 소스 추적
- 새 값;
3.0 / 4 = 0.750.75 > 0.75는 거짓 - 반환값과 직후 상태
truesize=3, capacity=4
이어 contains=true, size=3을 출력합니다. 네 번의 요청 중 고유 값은 세 개여서 이 main은 resize를 호출하지 않습니다. 용량과 조건값은 소스 추적이며 별도 계측 출력이 아닙니다.
이 예의 조건
- 원 main은 resize를 수행하지 않는다.
- 임계값 초과 조건은 >0.75이다.
관계도의 보충 설명
| 구분 | 내용 |
|---|---|
| 2 | 원래 네 add 중 동등한 hash 추가는 중복 검사에서 false이고 새 값 세 개만 남습니다. 예상 적재 0.75는 0.75보다 크지 않아 용량 4 그대로입니다. 실선은 원래 main 순서이며 본문의 별도 key 검산과 구분합니다. |
| 6 | size=1 |
| 7 | 용량 4 |
| 10 | size=1 |
| 11 | 용량 4 유지 |
| 15 | size=2 |
| 16 | 용량 4 |
| 21 | size=3 |
| 22 | 용량 4 |
| 구분 | 내용 |
|---|---|
| 설명 | 원래 네 add 중 동등한 hash 추가는 중복 검사에서 false이고 새 값 세 개만 남습니다. 예상 적재 0.75는 0.75보다 크지 않아 용량 4 그대로입니다. 실선은 원래 main 순서이며 본문의 별도 key 검산과 구분합니다. |
| 전체 읽기 정보 | 네 번의 add와 확장하지 않은 용량 4 네 번의 add와 확장하지 않은 용량 4 동등한 hash를 두 번 요청해도 고유 원소는 세 개이며 마지막 예상 적재 계수 0.75는 확장 조건을 넘지 않습니다. 확장 조건 · 0.75보다 클 때 네 번의 add와 확장하지 않은 용량 4 중복 hash는 확장 검사 전에 반환합니다. 마지막 새 값까지 사용률이 정확히 0.75이므로 이 main은 resize를 호출하지 않습니다. add("hash") 중복·확장 조건의 소스 추적 새 값; 1.0 / 4 = 0.25 0.25 > 0.75 는 거짓 반환값과 직후 상태 true size=1, capacity=4 add(new String("hash")) 중복·확장 조건의 소스 추적 기존 문자열과 동등 contains(value)=true ; 확장 검사 전에 add 종료 반환값과 직후 상태 false size=1, capacity=4 add("set") 중복·확장 조건의 소스 추적 새 값; 2.0 / 4 = 0.5 0.5 > 0.75 는 거짓 반환값과 직후 상태 true size=2, capacity=4 add("bucket") 중복·확장 조건의 소스 추적 새 값; 3.0 / 4 = 0.75 0.75 > 0.75 는 거짓 반환값과 직후 상태 true size=3, capacity=4 이어 contains=true, size=3 을 출력합니다. 네 번의 요청 중 고유 값은 세 개여서 이 main은 resize 를 호출하지 않습니다. 용량과 조건값은 소스 추적이며 별도 계측 출력이 아닙니다. |
| 관찰·상황 | 값·보충 정보 | 변화·이유 |
|---|---|---|
| 첫 등록 hash를 넣고 같은 hash 재추가는 거절합니다. |
|
|
| 둘째 새 값 set을 추가합니다. |
|
|
| 셋째 새 값 bucket을 추가해도 용량이 유지됩니다. |
|
|
resize는 기존 그룹 목록을 새 배열 위치에 그대로 복사하지 않습니다.
용량이 바뀌면 floorMod 결과가 달라질 수 있으므로 모든 원소의 새 index를 계산합니다.
재배치 중 size를 증가시키지 않아 논리 원소 수가 두 번 세어지지 않습니다.
이 구현의 불변식은 size가 모든 그룹의 원소 수 합과 같고, 동등한 원소는 한 번만 저장된다는 것입니다. 중복 add와 실패 remove는 size를 유지하며, 성공한 add/remove만 각각 1을 더하거나 뺍니다. resize는 원소를 이동할 뿐 이 합을 바꾸지 않습니다. List.contains·remove가 equals를 사용하므로 ch15-4의 동등성·해시 일치와 저장 중 키 안정성도 전제입니다.
원 main의 네 add에는 hash 중복이 한 번 있어 고유 값은 세 개입니다. 용량 4에서 예상 적재 계수 3/4는 정확히 0.75라 > 조건이 거짓이고 실제 resize를 실행하지 않습니다. 재해시 코드를 따라가려면 다음처럼 별도 main 본문으로 네 번째 고유 값 key를 더해 보세요. GenericMyHashSet의 MySet·HashMySet 구현은 그대로 둡니다.
예시
HashMySet<String> set = new HashMySet<>(4);
set.add("hash");
set.add("set");
set.add("bucket");
System.out.println("before=" + set.size() + "/" + set.buckets.length);
System.out.println("fourth=" + set.add("key"));
System.out.println("after=" + set.size() + "/" + set.buckets.length);
System.out.println("kept=" + (set.contains("hash") && set.contains("set")
&& set.contains("bucket") && set.contains("key")));이 진단 main은 HashMySet을 포함한 같은 바깥 클래스 안에 있어 private buckets 길이를 읽습니다. 일반 소비자의 MySet 역할에 용량 조회 메서드를 추가한 것은 아닙니다. 출력은 before=3/4, fourth=true, after=4/8, kept=true입니다. 재배치는 기존 세 원소의 새 그룹 위치만 바꾸며 size는 세 개로 유지합니다. 그 뒤 key를 저장할 때 한 번 증가하여 네 개가 됩니다. 조회 성공만으로 모든 충돌·변경 사례를 검증했다고 결론 내리지는 않습니다.
한 번의 확장 비용과 연속 삽입 비용
원소 수를 n, 그룹 배열 길이를 m, 조회 그룹 안의 원소 수를 k라고 합시다. contains는 그 목록을 선형 검색하므로 O(k)이며 모두 한 그룹에 충돌하면 O(n)입니다. 좋은 분산과 적정 적재 계수에서 평균 k가 작을 때만 평균 O(1)로 설명합니다.
resize는 새 그룹 m개를 초기화하고 기존 n개를 재배치하므로 O(n + m)입니다. 한 번의 add가 확장을 일으키면 그 호출은 O(1)이 아닙니다. 용량을 두 배씩 늘리면 이전 재배치 양이 등비급수로 합쳐져, 좋은 분산과 hashCode·equals의 일정한 평가 비용을 가정한 연속 삽입에서 확장 비용을 나눠 부담하는 분할상환 O(1)이 됩니다. 모든 호출의 최악 비용이라는 뜻은 아닙니다.
반복자는 빈 그룹도 건너뛰므로 전체 순회는 O(n + m)입니다. 원소가 적다고 초기 용량을 지나치게 크게 잡으면 순회 비용이 늘어납니다. 이 학습 구현의 resize·iterator 코드를 근거로 한 분석이며, 표준 HashSet의 분산·순회 조건과도 구분해 읽습니다.
충돌 중에도 size 불변식 확인하기
다음 코드를 GenericMyHashSet의 같은 바깥 클래스 안에서 별도 main 본문으로 실행하세요. MySet·HashMySet 구현은 바꾸지 않습니다. 네 add, 중복 add, 없는 원소 제거, 실제 제거 뒤의 반환값과 size를 먼저 예측하세요.
record CollisionKey(int id) {
public int hashCode() { return -7; }
}
HashMySet<CollisionKey> set = new HashMySet<>(4);
for (int id = 1; id <= 4; id++) set.add(new CollisionKey(id));
System.out.println("capacity=" + set.buckets.length + ", size=" + set.size());
System.out.println("duplicate=" + set.add(new CollisionKey(1)) + ", size=" + set.size());
System.out.println("missing=" + set.remove(new CollisionKey(9)) + ", size=" + set.size());
System.out.println("removed=" + set.remove(new CollisionKey(2)) + ", size=" + set.size());
System.out.println("kept=" + (set.contains(new CollisionKey(1))
&& set.contains(new CollisionKey(3)) && set.contains(new CollisionKey(4))));음수 해시·충돌·재해시·크기 확인
출력은 capacity=8, size=4 / duplicate=false, size=4 / missing=false, size=4 / removed=true, size=3 / kept=true입니다. Math.floorMod(-7, 4)와 Math.floorMod(-7, 8)은 모두 1이므로 모두 같은 그룹에서 equals로 구별됩니다. 네 번째 고유 값에서만 0.75를 넘어 확장하며, 재해시는 기존 세 원소의 수를 늘리지 않습니다. 중복 등록과 실패 제거는 size를 유지하고 실제 제거만 하나 줄입니다.
같은 상태에서 순회를 끝낸 뒤 next가 NoSuchElementException을 던지는지도 확인하세요. 키가 바뀌지 않았고 순회 중 구조 변경도 없는 조건입니다.
null 정책과 iterator 변경 정책
표준 HashSet은 null 하나를 허용하지만 게시글 구현은 Objects.requireNonNull로 금지했습니다.
이 구현은 null을 금지하지만 빈 문자열까지 거부하지는 않습니다. 빈 태그를 금지하려면 별도 도메인 검사가 필요합니다.
인터페이스 사용자가 알 수 있도록 문서와 예외를 함께 고정해야 합니다.
위 반복자는 순회 도중 add나 remove를 감지하지 않습니다.
제품 수준 컬렉션은 modCount 같은 구조 변경 횟수로 즉시 실패 진단을 제공할 수 있습니다.
여기서는 순회 중 변경 금지를 사용 규칙으로 두고, Iterator 구현 자체는 그룹을 한 번씩만 방문하는 데 집중합니다.
반복해서 hasNext를 호출해도 원소를 소비하지 않고, 원소를 다 읽은 뒤 next는 NoSuchElementException을 던집니다. remove를 구현하지 않았으므로 이 반복자의 remove는 기본 구현에 따라 UnsupportedOperationException을 던집니다. Iterator 계약을 따르는 두 경계와, 구조 변경을 감지하지 않는 이 학습 구현의 제한을 구별합니다. 표준 HashSet의 fail-fast 진단도 동시 접근의 안전성을 보장하는 장치는 아닙니다.
제네릭이라고 런타임에 E별 배열이 생기는 것은 아닙니다.
타입 소거 뒤에도 그룹에는 실제 객체 참조가 저장됩니다.
안전성은 모든 쓰기가 add(E)를 통과한다는 컴파일 규칙에서 나옵니다.
게시글 공개 키 집합을 역할로 수신
서비스는 HashSet 구체 타입 대신 Set<PostKey>를 생성자로 받습니다.
이 서비스의 생성자는 전달받은 Set을 Set.copyOf로 고정하므로, LinkedHashSet을 넘겨도 반복 순서를 보장하지 않습니다.
공개 처리 결과는 add boolean을 그대로 도메인 결과로 번역합니다.
import java.util.HashSet;
import java.util.Set;
public final class PublicationSetService {
public static void main(String[] args) {
PublicationService service = new PublicationService(new HashSet<>());
System.out.println(service.publish(new PostKey("java", "hash")));
System.out.println(service.publish(new PostKey("java", "hash")));
System.out.println(service.publish(new PostKey("java", "set")));
System.out.println("count=" + service.count());
}
private static final class PublicationService {
private final Set<PostKey> published;
private PublicationService(Set<PostKey> published) {
this.published = Set.copyOf(published);
}
String publish(PostKey key) {
Set<PostKey> mutable = new HashSet<>(published);
return mutable.add(key) ? "new" : "already";
}
int count() {
return published.size();
}
}
private record PostKey(String category, String title) {}
}이 예제는 의도적으로 버그가 있습니다.
생성자에서 Set.copyOf로 고정하고 공개마다 임시 복사본만 바꾸므로 서비스 상태가 누적되지 않습니다.
출력은 매번 new이고 count는 0입니다.
불변 스냅숏과 가변 내부 상태를 혼동한 실패를 찾아 다음 연습에서 고칩니다.
연습 문제
내부 Set의 소유권은 서비스가 갖되 외부 초기값 변경은 받지 않게 하세요.
같은 PostKey를 두 번 공개하면 new, already를 출력하고, 다른 키까지 공개한 뒤 count는 2가 되어야 합니다.
정답과 해설
생성자에서 new HashSet<>(initial)로 방어적 가변 복사를 만들고 공개는 그 필드에 직접 add합니다.
조회 결과를 외부로 보낼 때만 Set.copyOf를 사용합니다.
import java.util.HashSet;
import java.util.Set;
public final class PublicationServiceSolution {
public static void main(String[] args) {
Service service = new Service(Set.of());
System.out.println(service.publish(new Key("java", "hash")));
System.out.println(service.publish(new Key("java", "hash")));
System.out.println(service.publish(new Key("java", "set")));
System.out.println("count=" + service.snapshot().size());
}
private static final class Service {
private final Set<Key> published;
Service(Set<Key> initial) {
published = new HashSet<>(initial);
}
String publish(Key key) {
return published.add(key) ? "new" : "already";
}
Set<Key> snapshot() {
return Set.copyOf(published);
}
}
private record Key(String category, String title) {}
}복사 위치가 바꾸는 공개 상태의 누적
매번 임시 복사하여 누적을 잃는 예제와 필드 소유 집합이 0→1→1→2로 누적되는 예제를 비교합니다. 두 독립 저장소의 수명과 상태가 변합니다.
실패 코드는 매번 임시 집합만 바꾸어 count가 0이고 연습 해답은 소유한 가변 필드에 누적하여 count가 2입니다.
상태 소유 · 변경 대상이 누적을 결정
복사 위치가 바꾸는 공개 상태의 누적
매 호출의 임시 복사본을 바꾸면 필드는 계속 비어 있습니다. 가변 필드를 소유하고 직접 추가해야 공개 기록이 누적됩니다.
의도된 실패
- 생성과 publish의 변경 대상
- 필드:
Set.copyOf(published)매 호출new HashSet<>(published)임시 복사본만 변경필드는 계속 빈 집합 - 세 반환값과 마지막 개수
new → new → newcount=0
연습 해답
- 생성과 publish의 변경 대상
- 필드:
new HashSet<>(initial)published.add(key)로 같은 필드에 누적snapshot()에서만Set.copyOf(published) - 세 반환값과 마지막 개수
new → already → new필드 크기0 → 1 → 1 → 2count=2
두 main은 각각 java/hash, 같은 java/hash, 다른 java/set 키를 공개합니다. 연습 해답은 생성 시 가변 복사와 필드에 직접 추가라는 두 변경을 함께 적용합니다. 스냅숏은 원소 구성을 고정하며 원소 객체의 깊은 복사는 아닙니다.
이 예의 조건
- 두 main은 독립 실행이며 원소 깊은 복사를 하지 않는다.
관계도의 보충 설명
| 구분 | 내용 |
|---|---|
| 2 | 별도의 두 main을 비교합니다. 실패는 빈 필드에서 매번 새 임시 집합을 바꾸어 new/new/new, count 0이며 해답은 생성 시 방어적 가변 복사 후 같은 필드에 누적해 new/already/new, count 2입니다. 실선은 각 프로그램의 변경·소유 관계입니다. |
| 8 | 매번 빈 초깃값에서 시작 |
| 23 | already |
| 구분 | 내용 |
|---|---|
| 설명 | 별도의 두 main을 비교합니다. 실패는 빈 필드에서 매번 새 임시 집합을 바꾸어 new/new/new, count 0이며 해답은 생성 시 방어적 가변 복사 후 같은 필드에 누적해 new/already/new, count 2입니다. 실선은 각 프로그램의 변경·소유 관계입니다. |
| 전체 읽기 정보 | 복사 위치가 바꾸는 공개 상태의 누적 복사 위치가 바꾸는 공개 상태의 누적 실패 코드는 매번 임시 집합만 바꾸어 count가 0이고 연습 해답은 소유한 가변 필드에 누적하여 count가 2입니다. 상태 소유 · 변경 대상이 누적을 결정 복사 위치가 바꾸는 공개 상태의 누적 매 호출의 임시 복사본을 바꾸면 필드는 계속 비어 있습니다. 가변 필드를 소유하고 직접 추가해야 공개 기록이 누적됩니다. 의도된 실패 생성과 publish의 변경 대상 필드: Set.copyOf(published) 매 호출 new HashSet<>(published) 임시 복사본만 변경 필드는 계속 빈 집합 세 반환값과 마지막 개수 new → new → new count=0 연습 해답 생성과 publish의 변경 대상 필드: new HashSet<>(initial) published.add(key) 로 같은 필드에 누적 snapshot() 에서만 Set.copyOf(published) 세 반환값과 마지막 개수 new → already → new 필드 크기 0 → 1 → 1 → 2 count=2 두 main은 각각 java/hash , 같은 java/hash , 다른 java/set 키를 공개합니다. 연습 해답은 생성 시 가변 복사와 필드에 직접 추가라는 두 변경을 함께 적용합니다. 스냅숏은 원소 구성을 고정하며 원소 객체의 깊은 복사는 아닙니다. |
| 관찰·상황 | 값·보충 정보 | 변화·이유 |
|---|---|---|
| 초기 소유 실패는 빈 snapshot, 해답은 방어적 가변 필드를 보관합니다. |
|
|
| hash 두 호출 임시 사본은 매번 새 등록, 필드는 두 번째 중복입니다. |
|
|
| set 호출과 조회 마지막 새 키 처리 뒤 서로 다른 결과가 남습니다. |
|
|
복사 위치가 바꾸는 공개 상태의 누적
매번 임시 복사하여 누적을 잃는 예제와 필드 소유 집합이 0→1→1→2로 누적되는 예제를 비교합니다. 두 독립 저장소의 수명과 상태가 변합니다.
실패 코드는 매번 임시 집합만 바꾸어 count가 0이고 연습 해답은 소유한 가변 필드에 누적하여 count가 2입니다.
상태 소유 · 변경 대상이 누적을 결정
복사 위치가 바꾸는 공개 상태의 누적
매 호출의 임시 복사본을 바꾸면 필드는 계속 비어 있습니다. 가변 필드를 소유하고 직접 추가해야 공개 기록이 누적됩니다.
의도된 실패
- 생성과 publish의 변경 대상
- 필드:
Set.copyOf(published)매 호출new HashSet<>(published)임시 복사본만 변경필드는 계속 빈 집합 - 세 반환값과 마지막 개수
new → new → newcount=0
연습 해답
- 생성과 publish의 변경 대상
- 필드:
new HashSet<>(initial)published.add(key)로 같은 필드에 누적snapshot()에서만Set.copyOf(published) - 세 반환값과 마지막 개수
new → already → new필드 크기0 → 1 → 1 → 2count=2
두 main은 각각 java/hash, 같은 java/hash, 다른 java/set 키를 공개합니다. 연습 해답은 생성 시 가변 복사와 필드에 직접 추가라는 두 변경을 함께 적용합니다. 스냅숏은 원소 구성을 고정하며 원소 객체의 깊은 복사는 아닙니다.
이 예의 조건
- 두 main은 독립 실행이며 원소 깊은 복사를 하지 않는다.
관계도의 보충 설명
| 구분 | 내용 |
|---|---|
| 2 | 별도의 두 main을 비교합니다. 실패는 빈 필드에서 매번 새 임시 집합을 바꾸어 new/new/new, count 0이며 해답은 생성 시 방어적 가변 복사 후 같은 필드에 누적해 new/already/new, count 2입니다. 실선은 각 프로그램의 변경·소유 관계입니다. |
| 8 | 매번 빈 초깃값에서 시작 |
| 23 | already |
| 구분 | 내용 |
|---|---|
| 설명 | 별도의 두 main을 비교합니다. 실패는 빈 필드에서 매번 새 임시 집합을 바꾸어 new/new/new, count 0이며 해답은 생성 시 방어적 가변 복사 후 같은 필드에 누적해 new/already/new, count 2입니다. 실선은 각 프로그램의 변경·소유 관계입니다. |
| 전체 읽기 정보 | 복사 위치가 바꾸는 공개 상태의 누적 복사 위치가 바꾸는 공개 상태의 누적 실패 코드는 매번 임시 집합만 바꾸어 count가 0이고 연습 해답은 소유한 가변 필드에 누적하여 count가 2입니다. 상태 소유 · 변경 대상이 누적을 결정 복사 위치가 바꾸는 공개 상태의 누적 매 호출의 임시 복사본을 바꾸면 필드는 계속 비어 있습니다. 가변 필드를 소유하고 직접 추가해야 공개 기록이 누적됩니다. 의도된 실패 생성과 publish의 변경 대상 필드: Set.copyOf(published) 매 호출 new HashSet<>(published) 임시 복사본만 변경 필드는 계속 빈 집합 세 반환값과 마지막 개수 new → new → new count=0 연습 해답 생성과 publish의 변경 대상 필드: new HashSet<>(initial) published.add(key) 로 같은 필드에 누적 snapshot() 에서만 Set.copyOf(published) 세 반환값과 마지막 개수 new → already → new 필드 크기 0 → 1 → 1 → 2 count=2 두 main은 각각 java/hash , 같은 java/hash , 다른 java/set 키를 공개합니다. 연습 해답은 생성 시 가변 복사와 필드에 직접 추가라는 두 변경을 함께 적용합니다. 스냅숏은 원소 구성을 고정하며 원소 객체의 깊은 복사는 아닙니다. |
| 관찰·상황 | 값·보충 정보 | 변화·이유 |
|---|---|---|
| 초기 소유 실패는 빈 snapshot, 해답은 방어적 가변 필드를 보관합니다. |
|
|
| hash 두 호출 임시 사본은 매번 새 등록, 필드는 두 번째 중복입니다. |
|
|
| set 호출과 조회 마지막 새 키 처리 뒤 서로 다른 결과가 남습니다. |
|
|
내부는 누적 가능하고 반환값은 불변입니다.
제네릭 Set 규칙, 소유권, 중복 결과가 한 구현에서 함께 맞습니다.
제네릭 MySet 완성 여부는 잘못된 타입이 컴파일에서 막히는지, 재해시 뒤 포함 여부가 유지되는지, 중복 add와 실패 remove가 size를 바꾸지 않는지로 판단합니다.