본문 바로가기

복습

[Java 복습] 직접 구현해보는 Set (feat. hashCode)

목차

  • 해시 알고리즘을 사용한 Set 구현
  • 문자열은 해시 인덱스로 어떻게 바꿀까?
  • 자바의 HashCode 메서드
  • 직접 만든 객체 보관
  • 제네릭과 인터페이스 도입

 

 

직접 구현해보는 Set

Set은 중복을 허용하지 않고, 순서를 보장하지 않는 자료 구조이다.

 

 

MyHashSetV0의 단점

  • add() 로 데이터를 추가할 때 셋에 중복 데이터가 있는지 전체 데이터를 항상 확인해야 한다. 
  • 따라서 O(n)으로 입력 성능이 나쁘다. 
  • 중복 데이터가 있는지 검색 O(n) + 데이터 입력 O(1) O(n) contains() 로 데이터를 찾을 때는 셋에 있는 모든 데이터를 찾고 비교해야 하므로 평균 O(n)이 걸린다.

이렇게 성능이 느린 MyHashSetV0 를 해시 알고리즘을 사용해서 평균 O(1)로 개선해보자.

Set 을 구현하는 방법은 단순하다. 인덱스가 없기 때문에 단순히 데이터를 저장하고, 데이터가 있는지 확인하고, 데이
터를 삭제하는 정도면 충분하다. 그리고 Set 은 중복을 허용하지 않기 때문에 데이터를 추가할 때 중복 여부만 체크하
면 된다.

  • add(value) : 셋에 값을 추가한다. 중복 데이터는 저장하지 않는다.
  • contains(value) : 셋에 값이 있는지 확인한다.
  • remove(value) : 셋에 있는 값을 제거한다.

해시 알고리즘을 사용하도록 개선된 MyHashSetV1

public class MyHashSetV1 {
    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 add(int value) {
        int hashIndex = hashIndex(value);
        LinkedList<Integer> bucket = buckets[hashIndex];
        if (bucket.contains(value)) {
            return false;
        }
        bucket.add(value);
        size++;
        return true;
    }

    public boolean contains(int searchValue) {
        int hashIndex = hashIndex(searchValue);
        LinkedList<Integer> bucket = buckets[hashIndex];
        return bucket.contains(searchValue);
    }

    public boolean remove(int value) {
        int hashIndex = hashIndex(value);
        LinkedList<Integer> bucket = buckets[hashIndex];
        boolean result = bucket.remove(Integer.valueOf(value));
        if (result) {
            size--;
            return true;
        } else {
            return false;
        }
    }

    private int hashIndex(int value) {
        return value % capacity;
    }

    public int getSize() {
        return size;
    }

    @Override
    public String toString() {
        return "MyHashSetV1{" +
                "buckets=" + Arrays.toString(buckets) +
                ", size=" + size +
                ", capacity=" + capacity +
                '}';
    }
}

 

  • buckets : 연결 리스트를 배열로 사용한다.
    • 배열안에 연결 리스트가 들어있고, 연결 리스트 안에 데이터가 저장된다.
    • 해시 인덱스가 충돌이 발생하면 같은 연결 리스트 안에 여러 데이터가 저장된다.
  • initBuckets()
    • 연결 리스트를 생성해서 배열을 채운다. 
    • 배열의 모든 인덱스 위치에는 연결 리스트가 들어있다.
  • 초기 배열의 크기를 생성자를 통해서 전달할 수 있다.
    • 기본 생성자를 사용하면 DEFAULT_INITIAL_CAPACITY 의 값인 16이 사용된다.
  • add() : 해시 인덱스를 사용해서 데이터를 보관한다.
  • contains() : 해시 인덱스를 사용해서 데이터를 확인한다.
  • remove() : 해시 인덱스를 사용해서 데이터를 제거한다.

 

MyHashSetV1 사용해보기

public class MyHashSetV1Main {

    public static void main(String[] args) {

        //MyHashSetV1 set = new MyHashSetV1();
        MyHashSetV1 set = new MyHashSetV1(10); //capacity 조절
        set.add(1);
        set.add(2);
        set.add(5);
        set.add(8);
        set.add(14);
        set.add(99);
        boolean b = set.add(9); // hashIndex 중복
        System.out.println(set);
        System.out.println(b);

        // 검색
        int searchValue = 9;
        boolean result = set.contains(searchValue);
        System.out.println("set.contains("+ searchValue +") = " + result);
        
        // 삭제
        boolean removeResult = set.remove(searchValue);
        System.out.println("removeResult = " + removeResult);
        System.out.println(set);
    }
}
MyHashSetV1{buckets=[[], [1], [2], [], [14], [5], [], [], [8], [99, 9]], size=7, capacity=10}
true
set.contains(9) = true
removeResult = true
MyHashSetV1{buckets=[[], [1], [2], [], [14], [5], [], [], [8], [99]], size=6, capacity=10}

 

  • 생성: new MyHashSetV1(10) 을 사용해서 배열의 크기를 10으로 지정했다.
  • 저장: 실행 결과를 보면 99 , 9 의 경우 해시 인덱스가 9로 충돌하게 된다.
    • 따라서 배열의 같은 9번 인덱스 위치에 저장된 것을 확인할 수 있다. 
    • 그리고 그 안에 있는 연결 리스트에 99 , 9 가 함께 저장된다.
  • 검색: 9 를 검색하는 경우 해시 인덱스가 9 이다.
    •  따라서 배열의 9 번 인덱스에 있는 연결 리스트를 먼저 찾는다. 
    • 해당 연결 리스트에 있는 모든 데이터를 순서대로 비교하면서 9 를 찾는다.
    • 먼저 99 와 9 를 비교한다. => 실패
    • 다음으로 9 와 9 를 비교한다. => 성공

MyHashSetV1 은 해시 알고리즘을 사용한 덕분에 등록, 검색, 삭제 모두 평균 O(1)로 연산 속도를 크게 개선했다.

 

 

남은 문제
해시 인덱스를 사용하려면 데이터의 값을 배열의 인덱스로 사용해야 한다. 

그런데 배열의 인덱스는 0, 1, 2 같은 숫자만 사용할 수 있다.

"A", "B"와 같은 문자열은 배열의 인덱스로 사용할 수 없다.
숫자가 아닌 문자열 데이터를 저장할 때, 해시 인덱스를 사용하려면 어떻게 해야할까?

 

 

 

문자열 해시 코드

지금까지 해시 인덱스를 구할 때 숫자를 기반으로 해시 인덱스를 구했다. 

해시 인덱스는 배열의 인덱스로 사용해야 하므로 0, 1, 2, 같은 숫자(양의 정수)만 사용할 수 있다. 

따라서 문자를 사용할 수 없다. 문자 데이터를 기반으로 숫자 해시 인덱스를 구하려면 어떻게 해야 할까?

 

 

public class StringHashMain {

    public static void main(String[] args) {
        // char
        char charA = 'A';
        char charB = 'B';
        System.out.println("charA = " + (int)charA);
        System.out.println("charB = " + (int)charB);

        // hashCode
        System.out.println("hashCode('A') = " + hashCode("A"));
        System.out.println("hashCode('B') = " + hashCode("B"));
        System.out.println("hashCode('AB') = " + hashCode("AB"));

        // hashIndex
        int hashIndex = hashIndex(hashCode("AB"));
        System.out.println("hashIndex('AB') = " + hashIndex);
    }
    static int hashCode(String str) {
        // 65 + 66
        char[] charArray = str.toCharArray();
        int sum = 0;
        for (char c: charArray) {
            sum += (int)c;
        }
        return sum;
    }

    static int hashIndex(int value) {
        return value % CAPACITY;
    }
}
charA = 65
charB = 66
hashCode('A') = 65
hashCode('B') = 66
hashCode('AB') = 131
hashIndex('AB') = 1
  • 모든 문자는 본인만의 고유한 숫자로 표현할 수 있다. 
  • 예를 들어서 'A' 는 65 , 'B' 는 66 으로 표현된다. 
  • 가장 단순하게 char 형을 int 형으로 캐스팅하면 문자의 고유한 숫자를 확인할 수 있다.

 

  • hashCode() 메서드를 사용해서 문자열을 해시 코드로 변경한다. 
  • 그러면 고유한 정수 숫자 값이 나오는데, 이것을 해시 코드라 한다.
  • 숫자 값인 해시 코드를 사용해서 해시 인덱스를 생성한다.
  • 이렇게 생성된 해시 인덱스를 배열의 인덱스로 사용하면 된다.

 

용어 정리

해시 함수(Hash Function)

  • 해시 함수는 임의의 길이의 데이터를 입력으로 받아, 고정된 길이의 해시값(해시 코드)을 출력하는 함수이다.
    • 여기서 의미하는 고정된 길이는 저장 공간의 크기를 뜻한다. 예를 들어서 int 형 1 , 100 은 둘다 4byte를차지하는 고정된 길이는 뜻한다.
  • 같은 데이터를 입력하면 항상 같은 해시 코드가 출력된다.
  • 다른 데이터를 입력해도 같은 해시 코드가 출력될 수 있다. 이것을 해시 충돌이라 한다.
    • 해시 충돌의 예
    • "BC" => B(66) + C(67) = 133
    • "AD" => A(65) + D(68) = 133

 

해시 코드(Hash Code)

  • 해시 코드는 데이터를 대표하는 값을 뜻한다. 보통 해시 함수를 통해 만들어진다.
  • 데이터 A 의 해시 코드는 65
  • 데이터 B 의 해시 코드는 66
  • 데이터 AB 의 해시 코드는 131


해시 인덱스(Hash Index)

  • 해시 인덱스는 데이터의 저장 위치를 결정하는데, 주로 해시 코드를 사용해서 만든다.
  • 보통 해시 코드의 결과에 배열의 크기를 나누어 구한다. 

 

요약하면, 해시 코드는 데이터를 대표하는 값, 해시 함수는 이러한 해시 코드를 생성하는 함수, 

그리고 해시 인덱스는 해시 코드를 사용해서 데이터의 저장 위치를 결정하는 값을 뜻한다.

 

 

정리
문자 데이터를 사용할 때도, 해시 함수를 사용해서 정수 기반의 해시 코드로 변환한 덕분에, 

해시 인덱스를 사용할 수 있게 되었다. 따라서 문자의 경우에도 해시 인덱스를 통해 빠르게 저장하고 조회할 수 있다.

 


여기서 핵심은 해시 코드이다.
세상의 어떤 객체든지 정수로 만든 해시 코드만 정의할 수 있다면 해시 인덱스를 사용할 수 있다.
그렇다면 문자 뿐만 아니라 내가 직접 만든 Member , User 와 같은 객체는 어떻게 해시 코드를 정의할 수 있을까? 

자바의 hashCode() 메서드에 대해 알아보자!!

 

 

 

자바의 hashCode( )

해시 인덱스를 사용하는 해시 자료 구조는 데이터 추가, 검색, 삭제의 성능이 O(1)로 매우 빠르다. 

따라서 많은 곳에서 자주 사용된다. 그런데 앞서 학습한 것 처럼 해시 자료 구조를 사용하려면 정수로 된 숫자 값인 

해시 코드가 필요하다. 자바에는 정수 int , Integer 뿐만 아니라 char , String , Double , Boolean 등 수 많은 타입이 있다. 

뿐만 아니라 개발자가 직접 정의한 Member , User 와 같은 사용자 정의 타입도 있다.
이 모든 타입을 해시 자료 구조에 저장하려면 모든 객체가 숫자 해시 코드를 제공할 수 있어야 한다.

 

 

Object.hashCode()

자바는 모든 객체가 자신만의 해시 코드를 표현할 수 있는 기능을 제공한다. 

바로 Object 에 있는 hashCode() 메서드이다.

public class Object {
    public int hashCode();
}
  • 이 메서드를 그대로 사용하기 보다는 보통 재정의(오버라이딩)해서 사용한다.
  • 이 메서드의 기본 구현은 객체의 참조값을 기반으로 해시 코드를 생성한다.
  • 쉽게 이야기해서 객체의 인스턴스가 다르면 해시 코드도 다르다.

 

public class Member {

    private String id;

    Member(String id) {
        this.id = id;
    }

    public String getId() {
        return id;
    }

    @Override
    public boolean equals(Object object) {
        if (this == object) return true;
        if (object == null || getClass() != object.getClass()) return false;
        Member member = (Member) object;
        return Objects.equals(id, member.id);
    }

    @Override
    public int hashCode() {
        return Objects.hash(id);
    }

    @Override
    public String toString() {
        return "Member{" +
                "id='" + id + '\'' +
                '}';
    }
}
  • 여기서는 Member 의 id 값을 기준으로 equals 비교를 하고, hashCode도 생성한다.
public class JavaHashCodeMain {

    public static void main(String[] args) {
        
        // Object의 기본 hashCode는 객체의 참조값을 기반으로 생성
        Object obj1 = new Object();
        Object obj2 = new Object();
        System.out.println("obj1.hashCode() = " + obj1.hashCode());
        System.out.println("obj2.hashCode() = " + obj2.hashCode());
        System.out.println(obj1);
        System.out.println(Integer.toHexString(obj1.hashCode()));

        // 각 클래스마다 hashCode를 이미 오버라이딩 해두었다.
        Integer i = 10;
        String strA = "A";
        String strB = "AB";

        System.out.println("10.hashCode() = " + i.hashCode());
        System.out.println("'A'.hashCode() = " + strA.hashCode());
        System.out.println("'AB'.hashCode() = " + strB.hashCode());

        // hashCode는 마이너스 값이 들어올 수 있다.
        System.out.println("-1.hashCode() = " + Integer.valueOf(-1).hashCode());

        // 둘은 같을까?, 인스턴스는 다르지만, equals는 같다.
        Member member1 = new Member("idA");
        Member member2 = new Member("idA");
        
        // equals, hashCode를 오버라이딩 하지 않은 경우와, 한 경우를 비교
        System.out.println("member1 == member2 = " + (member1 == member2));
        System.out.println("member1 equals member2 = " + (member1.equals(member2)));
        System.out.println(member1.hashCode());
        System.out.println(member2.hashCode());
    }
}
obj1.hashCode() = 189568618
obj2.hashCode() = 664223387
java.lang.Object@b4c966a
b4c966a
10.hashCode() = 10
'A'.hashCode() = 65
'AB'.hashCode() = 2081
-1.hashCode() = -1
member1 == member2 = false
member1 equals member2 = true
104101
104101

 

 

Object의 해시 코드 비교

Object 가 기본으로 제공하는 hashCode() 는 객체의 참조값을 해시 코드로 사용한다. 

따라서 각각의 인스턴스마다 서로 다른 값을 반환한다.
그 결과 obj1 , obj2 는 서로 다른 해시 코드를 반환한다.

 


자바의 기본 클래스의 해시 코드

Integer , String 같은 자바의 기본 클래스들은 대부분 내부 값을 기반으로 해시 코드를 구할 수 있도록

hashCode() 메서드를 재정의해 두었다. 따라서 데이터의 값이 같으면 같은 해시 코드를 반환한다.
해시 코드의 경우 정수를 반환하기 때문에 마이너스 값이 나올 수 있다.

 

 

직접 구현하는 해시 코드

Member 의 경우 회원의 id 가 같으면 논리적으로 같은 회원으로 표현할 수 있다. 

따라서 회원 id 를 기반으로 동등성을 비교하도록 equals 를 재정의해야 한다.
여기에 hashCode() 도 같은 원리가 적용된다. 회원의 id 가 같으면 논리적으로 같은 회원으로 표현할 수 있다.

따라서 회원 id 를 기반으로 해시 코드를 생성해야 한다.

 

 

Member의 hashCode( ) 구현

  • Member 는 hashCode() 를 재정의했다.
  • hashCode() 를 재정의할 때 Objects.hash() 에 해시 코드로 사용할 값을 지정해주면 쉽게 해시 코드를 생성할 수 있다.
  • hashCode() 를 재정의하지 않으면 Object 가 기본으로 제공하는 hashCode() 를 사용하게 된다. 
  • 이것은 객체의 참조값을 기반으로 해시 코드를 제공한다. 
  • 따라서 회원의 id 가 같아도 인스턴스가 다르면 다른 해시 코드를 반환하게 된다.
  • hashCode() 를 id 를 기반으로 재정의한 덕분에 인스턴스가 달라도 id 값이 같으면 같은 해시 코드를 반환한다.
  • 따라서 인스턴스가 다른 member1 , member2 둘다 같은 해시 코드를 반환하는 것을 확인할 수 있다.

 

정리

  • 자바가 기본으로 제공하는 클래스 대부분은 hashCode() 를 재정의해두었다.
  • 객체를 직접 만들어야 하는 경우에 hashCode() 를 재정의하면 된다.
  • hashCode() 만 재정의하면 필요한 모든 종류의 객체를 해시 자료 구조에 보관할 수 있다.
  • 정리하면 해시 자료 구조에 데이터를 저장하는 경우 hashCode() 를 구현해야 한다.

 

직접 구현하는 Set - MyHashSetV2


MyHashSetV1 은 Integer 숫자만 저장할 수 있었다. 

여기서는 모든 타입을 저장할 수 있는 Set 을 만들어보자.

public class MyHashSetV2 {
    static final int DEFAULT_INITIAL_CAPACITY = 16;

    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 add(Object value) {
        int hashIndex = hashIndex(value);
        LinkedList<Object> bucket = buckets[hashIndex];
        if (bucket.contains(value)) {
            return false;
        }
        bucket.add(value);
        size++;
        return true;
    }

    public boolean contains(Object searchValue) {
        int hashIndex = hashIndex(searchValue);
        LinkedList<Object> bucket = buckets[hashIndex];
        return bucket.contains(searchValue);
    }

    public boolean remove(Object value) {
        int hashIndex = hashIndex(value);
        LinkedList<Object> bucket = buckets[hashIndex];
        boolean result = bucket.remove(value);
        if (result) {
            size--;
            return true;
        } else {
            return false;
        }
    }

    private int hashIndex(Object value) {
        return Math.abs(value.hashCode()) % capacity;
    }

    public int getSize() {
        return size;
    }

    @Override
    public String toString() {
        return "MyHashSetV2{" +
                "buckets=" + Arrays.toString(buckets) +
                ", size=" + size +
                ", capacity=" + capacity +
                '}';
    }
}

 

 

 

문자열을 Set에 보관

public class MyHashSetV2Main1 {

    public static void main(String[] args) {

        MyHashSetV2 set =  new MyHashSetV2(10);
        set.add("A");
        set.add("B");
        set.add("C");
        set.add("D");
        set.add("AB");
        set.add("SET");
        System.out.println(set);

        System.out.println("'A'.hashCode() = " + "A".hashCode());
        System.out.println("'B'.hashCode() = " + "B".hashCode());
        System.out.println("'AB'.hashCode() = " + "AB".hashCode());
        System.out.println("'SET'.hashCode() = " + "SET".hashCode());

        // 검색
        String searchValue = "SET";
        boolean result = set.contains(searchValue);
        System.out.println("set.contains(" + searchValue +") = " + result);
    }
}
MyHashSetV2{buckets=[[], [AB], [], [], [], [A], [B, SET], [C], [D], []], size=6, capacity=10}
'A'.hashCode() = 65
'B'.hashCode() = 66
'AB'.hashCode() = 2081
'SET'.hashCode() = 81986
set.contains(SET) = true

 

 

  • 자바의 String 은 hashCode() 를 재정의해 두었다. 우리는 이 값을 사용하면 된다.
  • hashIndex(Object value) 에서 value.hashCode() 를 호출하면 실제로는 String 에서 재정의한
  • hashCode() 가 호출된다.이렇게 반환된 해시 코드를 기반으로 해시 인덱스를 생성한다.
  • 참고로 자바의 해시 함수는 단순히 문자들을 더하기만 하는 것이 아니라 더 복잡한 연산을 사용해서 해시 코드를 구한다. 

 

직접 구현하는 Set - 직접 만든 객체 보관

MyHashSetV2 는 Object 를 받을 수 있다. 따라서 직접 만든 Member 와 같은 객체도 보관할 수 있다.
여기서 주의할 점은 직접 만든 객체가 hashCode() , equals() 두 메서드를 반드시 구현해야 한다는 점이다.

 

public class Member {

    private String id;

    public Member(String id) {
        this.id = id;
    }

    public String getId() {
        return id;
    }

    @Override
    public boolean equals(Object object) {
        if (this == object) return true;
        if (object == null || getClass() != object.getClass()) return false;
        Member member = (Member) object;
        return Objects.equals(id, member.id);
    }

    @Override
    public int hashCode() {
        return Objects.hash(id);
    }

    @Override
    public String toString() {
        return "Member{" +
                "id='" + id + '\'' +
                '}';
    }
}
public class MyHashSetV2Main2 {

    public static void main(String[] args) {

        Member member1 = new Member("hi");
        Member member2 = new Member("JPA"); // 대문자 주의!
        Member member3 = new Member("java");
        Member member4 = new Member("spring");

        System.out.println("hi.hashCode() = " + member1.hashCode());
        System.out.println("JPA.hashCode() = " + member2.hashCode());
        System.out.println("java.hashCode() = " + member3.hashCode());
        System.out.println("spring.hashCode() = " + member4.hashCode());

        MyHashSetV2 set =  new MyHashSetV2(10);
        set.add(member1);
        set.add(member2);
        set.add(member3);
        set.add(member4);
        System.out.println(set);

        // 검색
        Member searchValue = new Member("JPA");
        boolean result = set.contains(searchValue);
        System.out.println("set.contains (" + searchValue + ") = " + result);
    }
}
java.hashCode() = 3254849
spring.hashCode() = -895679956
MyHashSetV2{buckets=[[Member{id='hi'}, Member{id='JPA'}], [], [], [], [], [], [Member{id='spring'}], [], [], [Member{id='java'}]], size=4, capacity=10}
set.contains (Member{id='JPA'}) = true

 

  • Member 의 hashCode() 를 id 값을 기반으로 재정의해 두었다.
  • hashIndex(Object value) 에서 value.hashCode() 를 호출하면 실제로는 Member 에서 재정의한hashCode() 가 호출된다.
  • 이렇게 반환된 해시 코드를 기반으로 해시 인덱스를 생성한다.

 


equals( ) 사용처

 

그렇다면 equals() 는 언제 사용할까?
"JPA"를 조회할 때 해시 인덱스는 0이다. 따라서 배열의 0 번 인덱스를 조회한다.
여기에는 [hi, JPA] 라는 회원 두 명이 있다. 이것을 하나하나 비교해야 한다. 이때 equals() 를 사용해서 비교한다.
따라서 해시 자료 구조를 사용할 때는 hashCode() 는 물론이고, equals() 도 반드시 재정의해야 한다.

참고로 자바가 제공하는 기본 클래스들은 대부분 hashCode() , equals() 를 함께 재정의해 두었다.

 

 

 

 

직접 구현하는 Set - 제네릭과 인터페이스 도입

지금까지 만든 해시 셋에 제네릭을 도입해서 타입 안전성을 높여보자.

public interface MySet<E> {
    boolean add(E element);
    boolean remove(E value);
    boolean contains(E value);
}
  • 핵심 기능을 인터페이스로 뽑았다.
  • 이 인터페이스를 구현하면 해시 기반이 아니라 다른 자료 구조 기반의 Set도 만들 수 있다.
public class MyHashSetV3<E> implements MySet<E> {
    static final int DEFAULT_INITIAL_CAPACITY = 16;

    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 add(E value) {
        int hashIndex = hashIndex(value);
        LinkedList<E> bucket = buckets[hashIndex];
        if (bucket.contains(value)) {
            return false;
        }
        bucket.add(value);
        size++;
        return true;
    }

    public boolean contains(E searchValue) {
        int hashIndex = hashIndex(searchValue);
        LinkedList<E> bucket = buckets[hashIndex];
        return bucket.contains(searchValue);
    }

    public boolean remove(E value) {
        int hashIndex = hashIndex(value);
        LinkedList<E> bucket = buckets[hashIndex];
        boolean result = bucket.remove(value);
        if (result) {
            size--;
            return true;
        } else {
            return false;
        }
    }

    private int hashIndex(Object value) {
        return Math.abs(value.hashCode()) % capacity;
    }

    public int getSize() {
        return size;
    }

    @Override
    public String toString() {
        return "MyHashSetV3{" +
                "buckets=" + Arrays.toString(buckets) +
                ", size=" + size +
                ", capacity=" + capacity +
                '}';
    }
}
public class MyHashSetV3Main {

    public static void main(String[] args) {

        MySet<String> set = new MyHashSetV3<>(10);
        set.add("A");
        set.add("B");
        set.add("C");
        System.out.println(set);

        // 검색
        String searchValue = "A";
        boolean result = set.contains(searchValue);
        System.out.println("set.cotains(" + searchValue + ") = " + result);
    }
}
MyHashSetV3{buckets=[[], [], [], [], [], [A], [B], [C], [], []], size=3, capacity=10}
set.cotains(A) = true