List<String> list = new ArrayList<>();
list.add("Java");
list.add("Spring");
list.add("Java");
System.out.println(list); // [Java, Spring, Java]
System.out.println(list.size()); // 3
Set<String> set = new HashSet<>();
set.add("Java");
set.add("Spring");
set.add("Java");
System.out.println(set); // [Java, Spring]
System.out.println(set.size()); // 2
List는 중복을 그대로 저장한다. Set은 두 번째 "Java"를 무시한다.
HashSet.add()의 반환값이 힌트다.
Set<String> set = new HashSet<>();
boolean first = set.add("Java"); // true — 새로 추가됨
boolean second = set.add("Java"); // false — 중복으로 추가 안 됨
내부적으로 두 단계를 거친다. 먼저 hashCode()로 버킷 위치를 결정하고, 해당 버킷에 이미 값이 있으면 equals()로 한 번 더 비교한다. 둘 다 같아야 중복으로 판단한다.
String a = "Java";
String b = new String("Java"); // 다른 객체
System.out.println(a == b); // false — 다른 참조
System.out.println(a.equals(b)); // true — 내용 같음
System.out.println(a.hashCode() == b.hashCode()); // true — 내용 기준으로 계산
Set<String> set = new HashSet<>();
set.add(a);
set.add(b);
System.out.println(set.size()); // 1 — 중복으로 잡힘
String은 equals()와 hashCode() 모두 내용 기준으로 재정의되어 있어서, 다른 객체여도 내용이 같으면 중복으로 판단한다.
class Tag {
String name;
Tag(String name) { this.name = name; }
}
Set<Tag> set = new HashSet<>();
set.add(new Tag("Java"));
set.add(new Tag("Java"));
System.out.println(set.size()); // 2 — 중복을 못 잡음
Tag 클래스는 equals()를 재정의하지 않았다. 기본 equals()는 참조(주소) 비교라서, 내용이 같아도 new로 만든 서로 다른 객체는 다르다고 판단한다. 결과적으로 둘 다 저장된다.
의도한 대로 동작하려면 equals()와 hashCode()를 함께 재정의해야 한다.
class Tag {
String name;
Tag(String name) { this.name = name; }
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (!(o instanceof Tag)) return false;
return name.equals(((Tag) o).name);
}
@Override
public int hashCode() {
return name.hashCode();
}
}
Set<Tag> set = new HashSet<>();
set.add(new Tag("Java"));
set.add(new Tag("Java"));
System.out.println(set.size()); // 1 — 중복 잡힘
equals()만 재정의하고 hashCode()를 빠뜨리면, 논리적으로 같은 객체인데 다른 버킷에 배정되어 HashSet이 중복을 못 잡는 버그가 생긴다.
Set<String> hashSet = new HashSet<>();
hashSet.add("Banana");
hashSet.add("Apple");
hashSet.add("Cherry");
System.out.println(hashSet); // [Apple, Cherry, Banana] — 순서 보장 안 됨
Set<String> linkedHashSet = new LinkedHashSet<>();
linkedHashSet.add("Banana");
linkedHashSet.add("Apple");
linkedHashSet.add("Cherry");
System.out.println(linkedHashSet); // [Banana, Apple, Cherry] — 삽입 순서 유지
Set<String> treeSet = new TreeSet<>();
treeSet.add("Banana");
treeSet.add("Apple");
treeSet.add("Cherry");
System.out.println(treeSet); // [Apple, Banana, Cherry] — 자동 정렬
중복 없는 데이터만 필요하면 HashSet, 입력 순서를 유지해야 하면 LinkedHashSet, 정렬된 상태가 필요하면 TreeSet을 선택한다. 그리고 커스텀 객체를 Set에 넣을 때는 반드시 equals()와 hashCode()를 함께 재정의해야 의도한 대로 동작한다.