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