
Set은 중복을 허용하지 않고, 순서를 보장하지 않는 자료구조다.
public class MyHashSetV1 {
private static final int DEFAULT_INITIAL_CAPACITY = 16;
LinkedList<Integer>[] buckets;
private int size = 0;
private int capacity = DEFAULT_INITIAL_CAPACITY;
public MyHashSetV1() {
initBuckets();
}
public MyHashSetV1(int capacity) {
this.capacity = capacity;
initBuckets();
}
private void initBuckets() {
buckets = new LinkedList[capacity];
for (int i = 0; i < capacity; i++) {
buckets[i] = new LinkedList<>();
}
}
public boolean remove(int value) {
int hashIndex = hashIndex(value);
LinkedList<Integer> bucket = buckets[hashIndex];
// Integer.valueOf(value) : 래퍼 클래스로 변환
boolean removed = bucket.remove(Integer.valueOf(value));
if (removed) {
size--;
return true;
} else {
return false;
}
}
public boolean contains(int searchValue) {
int hashIndex = hashIndex(searchValue);
LinkedList<Integer> bucket = buckets[hashIndex];
return bucket.contains(searchValue);
}
private boolean add(int value) {
int hashIndex = hashIndex(value);
LinkedList<Integer> bucket = buckets[hashIndex];
if (bucket.contains(value)) {
return false;
}
bucket.add(value);
size++;
return true;
}
private int hashIndex(int value) {
return value % capacity;
}
}
public class MyHashSetV1Main {
public static void main(String[] args) {
MyHashSetV1 myHashSetV1 = new MyHashSetV1();
myHashSetV1.add(1);
myHashSetV1.add(2);
myHashSetV1.add(5);
myHashSetV1.add(8);
myHashSetV1.add(14);
myHashSetV1.add(99);
myHashSetV1.add(9);
System.out.println(myHashSetV1);
int searchValue = 9;
boolean contains = myHashSetV1.contains(searchValue);
System.out.println("myHashSetV1.contains(" + searchValue + ") = " + contains);
boolean removed = myHashSetV1.remove(searchValue);
System.out.println("myHashSetV1.remove(" + searchValue + ") = " + removed);
System.out.println(myHashSetV1);
}
}
remove 구현 도중 파라미터로 들어가는 값에 대한 고민을 한 결과 연결 리스트의 remove가 어떤 식으로 작성되어있는지를 확인한 결과

public class StringHashMain {
private static final int CAPACITY = 10;
public static void main(String[] args) {
char charA = 'A';
char charB = 'B';
System.out.println("charA = " + (int) charA);
System.out.println("charB = " + (int) charB);
int hashCodeA = hashCode("A");
System.out.println("hashCodeA = " + hashCodeA);
int hashCodeB = hashCode("B");
System.out.println("hashCodeB = " + hashCodeB);
int hashCodeAB = hashCode("AB");
System.out.println("hashCodeAB = " + hashCodeAB);
int hashIndexA = hashIndex(hashCodeA);
System.out.println("hashIndexA = " + hashIndexA);
int hashIndexB = hashIndex(hashCodeB);
System.out.println("hashIndexB = " + hashIndexB);
int hashIndexAB = hashIndex(hashCodeAB);
System.out.println("hashIndexAB = " + hashIndexAB);
}
private static int hashCode(String str) {
char[] charArray = str.toCharArray();
int sum = 0;
for (int i = 0; i < charArray.length; i++) {
sum += ((int) charArray[i]);
}
return sum;
}
private static int hashIndex(int value) {
return value % CAPACITY;
}
}

hashCode() 메서드를 사용하여 문자열을 해시 코드로 변경한다. 그러면 고유한 정수 값이 나오는데 이 값을 해시 코드라 한다.public class JavaHashCodeMain {
public static void main(String[] args) {
Object object1 = new Object();
Object object2 = new Object();
System.out.println("object1.hashCode() = " + object1.hashCode());
System.out.println("object2.hashCode() = " + object2.hashCode());
Member member1 = new Member("id-100");
Member member2 = new Member("id-100");
System.out.println("member1.hashCode() = " + member1.hashCode()); // 음수도 나올 수 있음
System.out.println("member2.hashCode() = " + member2.hashCode());
System.out.println("member1 == member2 → " + (member1 == member2));
System.out.println("member1.equals(member2) → " + member1.equals(member2));
}
}
Object가 제공하는 hashCode()는 객체의 참조값을 해시 코드로 사용한다. 따라서 각각의 인스턴스마다 다른 값을 반환한다.Integer, String과 같은 자바 기본 클래스들은 대부분 내부 값을 기반으로 해서 해시 코드를 구할 수 있도록 hashCode() 메서드를 재정의해두었다.== 연산자를 사용해서 두 객체의 참조가 동일한 객체를 가리키는지를 확인equals() 메서드를 사용해서 두 객체가 논리적으로 동등한지 확인public class MyHashSetV2 {
private static final int DEFAULT_INITIAL_CAPACITY = 10;
private LinkedList<Object>[] buckets;
private int size = 0;
private int capacity = DEFAULT_INITIAL_CAPACITY;
public MyHashSetV2() {
initBuckets();
}
public MyHashSetV2(int capacity) {
this.capacity = capacity;
initBuckets();
}
private void initBuckets() {
buckets = new LinkedList[capacity];
for (int i = 0; i < capacity; i++) {
buckets[i] = new LinkedList<>();
}
}
public boolean remove(Object value) {
int hashIndex = hashIndex(value);
LinkedList<Object> bucket = buckets[hashIndex];
boolean removed = bucket.remove(value);
if (removed) {
size--;
return true;
} else {
return false;
}
}
public boolean contains(Object searchValue) {
int hashIndex = hashIndex(searchValue);
LinkedList<Object> bucket = buckets[hashIndex];
return bucket.contains(searchValue);
}
public boolean add(Object value) {
int hashIndex = hashIndex(value);
LinkedList<Object> bucket = buckets[hashIndex];
if (bucket.contains(value)) {
return false;
}
bucket.add(value);
size++;
return true;
}
private int hashIndex(Object value) {
// 해시 코드 값 음수 나올 수 있으므로
int hashCode = value.hashCode();
return Math.abs(hashCode) % capacity;
}
@Override
public String toString() {
return "MyHashSetV2{" +
"buckets=" + Arrays.toString(buckets) +
", size=" + size +
'}';
}
}
MyHashSetV1은 Integer만 저장할 수 있었으나 여기서 Object 타입으로 수정하여 모든 타입을 저장할 수 있도록 바꿨다.Object 타입으로 변경했다.
MyHashSetV2는 Object타입을 저장할 수 있게끔 했다.equals()는 참조값을 기준으로 비교하기 때문에 직접 만든 객체의 경우 equals()와 hashCode() 메서드를 반드시 재정의해야한다.public class MyHashSetV2Main {
public static void main(String[] args) {
MyHashSetV2 set = new MyHashSetV2(10);
Member hi = new Member("hi");
Member jpa = new Member("JPA"); //대문자 주의!
Member java = new Member("java");
Member spring = new Member("spring");
System.out.println("hi.hashCode() = " + hi.hashCode());
System.out.println("jpa.hashCode() = " + jpa.hashCode());
System.out.println("java.hashCode() = " + java.hashCode());
System.out.println("spring.hashCode() = " + spring.hashCode());
set.add(hi);
set.add(jpa);
set.add(java);
set.add(spring);
System.out.println(set);
//검색
Member searchValue = new Member("JPA");
boolean result = set.contains(searchValue);
System.out.println("set.contains(" + searchValue + ") = " + result);
}
}

Object의 기본 기능
hashCode() : 객체의 참조값을 기반으로 해시 코드를 반환한다. equals() : 재정의하지않은 Object의 equals()는 == 동일성 비교를 한다. 따라서 객체의 참조값이 같아야 true가 반환된다.해시 자료 구조를 사용할 때 유의할 점에 대해서 세 가지 경우를 들어 이해해보자.
①. hashCode(), equals()를 모두 구현하지 않은 경우
②. hashCode()는 구현했는데 equals()를 구현하지 않은 경우
③. hashCode()와 equals()를 모두 구현한 경우
❗hashCode(), equals()를 모두 구현하지 않은 경우
public class MemberNoHashNoEq {
private String id;
public MemberNoHashNoEq(String id) {
this.id = id;
}
public String getId() {
return id;
}
@Override
public String toString() {
return "MemberNoHashNoEq{" +
"id='" + id + '\'' +
'}';
}
}
public class HashAndEqualsMain1 {
public static void main(String[] args) {
MyHashSetV2 myHashSetV2 = new MyHashSetV2();
MemberNoHashNoEq m1 = new MemberNoHashNoEq("A");
MemberNoHashNoEq m2 = new MemberNoHashNoEq("A");
System.out.println("m1.hashCode() : " + m1.hashCode());
System.out.println("m2.hashCode() : " + m2.hashCode());
System.out.println("m1.equals(m2) : " + m1.equals(m2));
myHashSetV2.add(m1);
myHashSetV2.add(m2);
System.out.println(myHashSetV2);
MemberNoHashNoEq searchValue = new MemberNoHashNoEq("A");
System.out.println("searchValue.hashCode() : " + searchValue.hashCode());
boolean contains = myHashSetV2.contains(searchValue);
System.out.println("contains = " + contains);
}
}
실행 결과
m1.hashCode() : 1922154895
m2.hashCode() : 960604060
m1.equals(m2) : false
MyHashSetV2{buckets=[[MemberNoHashNoEq{id='A'}], [], [], [], [], [MemberNoHashNoEq{id='A'}], [], [], [], []], size=2}
searchValue.hashCode() : 932172204
contains = false
equals(), hashCode()를 모두 구현하지 않고 Object가 제공하는 기본 기능을 그대로 사용했기 때문에 객체의 참조값을 기반으로 이루어진다.m1과 m2는 인스턴스의 값이 같더라도 서로 다른 해시 코드 값을 가지게 된다. 또한 equals() 역시 참조값을 활용한 동일성 비교가 이루어지는데 참조값이 다르므로 당연히 false가 된다.m1과 m2 인스턴스는 다르지만 둘 다 "A"라는 회원 ID를 가지고 있다. 따라서 논리적으로 같은 회원으로 보아야 한다.

❗hashCode()는 구현했는데 equals()를 구현하지 않은 경우
public class MemberOnlyHash {
private String id;
public MemberOnlyHash(String id) {
this.id = id;
}
public String getId() {
return id;
}
@Override
public String toString() {
return "MemberNoHashNoEq{" +
"id='" + id + '\'' +
'}';
}
@Override
public int hashCode() {
return Objects.hash(id);
}
}
public class HashAndEqualsMain2 {
public static void main(String[] args) {
MyHashSetV2 myHashSetV2 = new MyHashSetV2();
MemberOnlyHash m1 = new MemberOnlyHash("A");
MemberOnlyHash m2 = new MemberOnlyHash("A");
System.out.println("m1.hashCode() : " + m1.hashCode());
System.out.println("m2.hashCode() : " + m2.hashCode());
System.out.println("m1.equals(m2) : " + m1.equals(m2));
myHashSetV2.add(m1);
myHashSetV2.add(m2);
System.out.println(myHashSetV2);
MemberOnlyHash searchValue = new MemberOnlyHash("A");
System.out.println("searchValue.hashCode() : " + searchValue.hashCode());
boolean contains = myHashSetV2.contains(searchValue);
System.out.println("contains = " + contains);
}
}
실행 결과
m1.hashCode() : 96
m2.hashCode() : 96
m1.equals(m2) : false
MyHashSetV2{buckets=[[], [], [], [], [], [], [MemberNoHashNoEq{id='A'}, MemberNoHashNoEq{id='A'}], [], [], []], size=2}
searchValue.hashCode() : 96
contains = false
hashCode()를 재정의했기 때문에 같은 id를 사용하는 m1과 m2는 같은 해시 코드가 나오게 된다.equals()는 구현하지 않았기 때문에 참조값을 기준으로 동일성 비교를 하게 되며 이 결과 false가 나오게 된다.add() 메서드에서 중복 데이터를 체크하기 때문에 같은 데이터가 저장되면 안된다.contains() 메서드에서 데이터를 비교할 때 equals()를 사용하는데 재정의를 하지 않았기 때문에 인스턴스 참조값으로 비교를 하게 된다. m1, m2는 참조값이 서로 다른 인스턴스이기 때문에 비교에 실패한다.❗hashCode(), equals()를 모두 구현한 경우
public class HashAndEqualsMain3 {
public static void main(String[] args) {
MyHashSetV2 myHashSetV2 = new MyHashSetV2();
Member m1 = new Member("A");
Member m2 = new Member("A");
System.out.println("m1.hashCode() : " + m1.hashCode());
System.out.println("m2.hashCode() : " + m2.hashCode());
System.out.println("m1.equals(m2) : " + m1.equals(m2));
myHashSetV2.add(m1);
myHashSetV2.add(m2);
System.out.println(myHashSetV2);
Member searchValue = new Member("A");
System.out.println("searchValue.hashCode() = " + searchValue.hashCode());
boolean contains = myHashSetV2.contains(searchValue);
System.out.println("contains = " + contains);
}
}
실행 결과
m1.hashCode() : 65
m2.hashCode() : 65
m1.equals(m2) : true
MyHashSetV2{buckets=[[], [], [], [], [], [Member{id='A'}], [], [], [], []], size=1}
searchValue.hashCode() = 65
contains = true
equals(), hashCode()를 재정의하여 정상적인 결과를 얻을 수 있다.public interface MySet<E> {
boolean add(E e);
boolean contains(E e);
boolean remove(E e);
}
public class MyHashSetV3<E> implements MySet<E> {
private static final int DEFAULT_INITIAL_CAPACITY = 10;
private LinkedList<E>[] buckets;
private int size = 0;
private int capacity = DEFAULT_INITIAL_CAPACITY;
public MyHashSetV3() {
initBuckets();
}
public MyHashSetV3(int capacity) {
this.capacity = capacity;
initBuckets();
}
private void initBuckets() {
buckets = new LinkedList[capacity];
for (int i = 0; i < capacity; i++) {
buckets[i] = new LinkedList<>();
}
}
public boolean remove(E value) {
int hashIndex = hashIndex(value);
LinkedList<E> bucket = buckets[hashIndex];
boolean removed = bucket.remove(value);
if (removed) {
size--;
return true;
} else {
return false;
}
}
public boolean contains(E searchValue) {
int hashIndex = hashIndex(searchValue);
LinkedList<E> bucket = buckets[hashIndex];
return bucket.contains(searchValue);
}
public boolean add(E value) {
int hashIndex = hashIndex(value);
LinkedList<E> bucket = buckets[hashIndex];
if (bucket.contains(value)) {
return false;
}
bucket.add(value);
size++;
return true;
}
private int hashIndex(E value) {
// 해시 코드 값 음수 나올 수 있으므로
int hashCode = value.hashCode();
return Math.abs(hashCode) % capacity;
}
@Override
public String toString() {
return "MyHashSetV3{" +
"buckets=" + Arrays.toString(buckets) +
", size=" + size +
", capacity=" + capacity +
'}';
}
public int getSize() {
return size;
}
}
public class MyHashSetV3Main {
public static void main(String[] args) {
MySet<String> set = new MyHashSetV3<>();
set.add("A");
set.add("B");
set.add("C");
System.out.println(set);
String searchValue = "A";
boolean contains = set.contains(searchValue);
System.out.println("contains = " + contains);
}
}
실행 결과
MyHashSetV3{buckets=[[], [], [], [], [], [A], [B], [C], [], []], size=3, capacity=10}
contains = true