Java공부(코딩)

코딩초보의 자바(Java)공부 28일차 { HashSet }

동곤일상 2025. 1. 14. 15:54
반응형

2025.01.12 - [Java공부(코딩)] - 코딩초보의 자바(Java)공부 27일차 { 컬렉션프레임워크 -Hash }

 

코딩초보의 자바(Java)공부 27일차 { 컬렉션프레임워크 -Hash }

1.List vs Set 정의**: 리스트는 요소들의 순차적인 컬렉션이다. 요소들은 특정 순서를 가지며, 같은 요소가 여러 번 나타날 수 있다.**특징**:**순서 유지**: 리스트에 추가된 요소는 특정한 순서를

ddkk1120.tistory.com

이 글과 이어집니다!!

 

 

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

이전에 구현한 성능이 O(n)으로 느린 `MyHashSetV0` 다시 한번 확인해보자.

 

**MyHashSetV0 단점**

`add()` 데이터를 추가할 셋에 중복 데이터가 있는지 전체 데이터를 항상 확인해야 한다. 따라서 O(n)으로

입력 성능이 나쁘다. 중복 데이터가 있는지 검색 O(n) + 데이터 입력 O(1) O(n)

 

`contains()` 데이터를 찾을 때는 셋에 있는 모든 데이터를 찾고 비교해야 하므로 평균 O(n) 걸린다.


`add(value)` : 셋에 값을 추가한다. 중복 데이터는 저장하지 않는다.

`contains(value)` : 셋에 값이 있는지 확인한다.`

remove(value)` : 셋에 있는 값을 제거한다.

 

package collection.set;

import java.util.Arrays;
import java.util.LinkedList;

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));
        //Integer타입으로변환 후 remove키워드사용(객체만 사용할 수 있음)
		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 +
				'}';
	}
}

`buckets` : 연결 리스트를 배열로 사용한다.

배열안에 연결 리스트가 들어있고, 연결 리스트 안에 데이터가 저장된다.

해시 인덱스가 충돌이 발생하면 같은 연결 리스트 안에 여러 데이터가 저장된다.

`initBuckets()`연결 리스트를 생성해서 배열을 채운다. 배열의 모든 인덱스 위치에는 연결 리스트가 들어있다.

초기 배열의 크기를 생성자를 통해서 전달할 있다.

기본 생성자를 사용하면 `DEFAULT_INITIAL_CAPACITY` 값인 16 사용된다.

`add()` : 해시 인덱스를 사용해서 데이터를 보관한다.

`contains()` : 해시 인덱스를 사용해서 데이터를 확인한다.

`remove()` : 해시 인덱스를 사용해서 데이터를 제거한다.\


메인코드

package collection.set;

public class MyHashSetV1Main {

	public static void main(String[] args) {
		MyHashSetV1 set = new MyHashSetV1(10);
		set.add(1);
		set.add(2);
		set.add(11);//중복
		set.add(23);
		set.add(88);
		set.add(8);//hashIndex중복
		
		System.out.println(set);
		
		//검색
		int search = 88;
		boolean contains = set.contains(search);
		System.out.println("set에 "+search+"이(가) 존재하는가?? : "+contains);
		
		//삭제
		
		boolean remove = set.remove(23);
		System.out.println("remove : "+remove);
		System.out.println(set);

	}

}

MyHashSetV1{buckets=[[], [1, 11], [2], [23], [], [], [], [], [88, 8], []], size=6}

set에 88이(가) 존재하는가?? : true

remove : true

MyHashSetV1{buckets=[[], [1, 11], [2], [], [], [], [], [], [88, 8], []], size=5}

 

**남은 문제**

해시 인덱스를 사용하려면 데이터의 값을 배열의 인덱스로 사용해야 한다. 그런데 배열의 인덱스는 0, 1, 2 같은 숫자만

사용할 있다. "A", "B" 같은 문자열은 배열의 인덱스로 사용할 없다.

다음 예와 같이 숫자가 아닌 문자열 데이터를 저장할 , 해시 인덱스를 사용하려면 어떻게 해야할까?

)

MyHashSetV1 set = new MyHashSetV1(10);

set.add("A");

set.add("B");

set.add("HELLO");


package javaBasic2.collection.set;

public class StringHashMain {
	static final int CAPACITY = 10;

	public static void main(String[] args) {
		char charA = 'A';
		char charB = 'B';
		char charC = 'C';
		char charD = 'D';
		System.out.println(charA +" = "+(int)charA);
		System.out.println(charB +" = "+(int)charB);
		
		System.out.println("hashCode(A)  = "+hashCode("A"));
		System.out.println("hashCode(B)  = "+hashCode("B"));
		System.out.println("hashCode(AB)  = "+hashCode("AB"));
		
		System.out.println("hashIndex(A)  = "+hasIndex(hashCode("A")));
		System.out.println("hashIndex(B)  = "+hasIndex(hashCode("B")));
		
		
		
		
		

	}
	static int hashCode(String str) {
		char[] charArray = str.toCharArray();
		int sum = 0;
		for (char c : charArray) {
			sum+=c;
		}
		return sum;
	}
	
	static int hasIndex(int value) {
		return value % CAPACITY;
	}
}

char 형을 int 형으로 캐스팅하면 문자의 고유한 숫자를 확인할 수 있다.

해시 함수(Hash Function)
해시 함수는 임의의 길이의 데이터를 입력으로 받아, 고정된 길이의 해시값(해시 코드)을 출력하는 함수이다.
여기서 의미하는 고정된 길이는 저장 공간의 크기를 뜻한다. 예를 들어서 int 형 1 , 100 은 둘다 4byte를
차지하는 고정된 길이는 뜻한다.

같은데이터를입력하면항상 같은 해시코드가 출력됨

 

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

 

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

 

문제점

: 문자뿐 아니라 Animal 등의 객체의경우에는 어떻게 해시코드를정의할까??


자바의 hashCode()

 

Object.hashCode()

이 메서드는 그냥 사용하기보단

재정의(오버라이딩)해서 사용하곤 한다

 

긴말하지않고 코드를 구현해봅시다

 

 

package javaBasic2.collection.set;

public class MemberMain {
	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());
		 //각 클래스마다 hashCode를 이미 오버라이딩 해두었다.
		 Integer i = 10;
		 String strA = "A";
		 String strAB = "AB";
		 System.out.println("10.hashCode = " + i.hashCode());
		 System.out.println("'A'.hashCode = " + strA.hashCode());
		 System.out.println("'AB'.hashCode = " + strAB.hashCode());
		 //hashCode는 마이너스 값이 들어올 수 있다.
		 System.out.println("-1.hashCode = " + Integer.valueOf(-1).hashCode());
		 //둘은 같을까 다를까?, 인스턴스는 다르지만, equals는 같다.
		 MemberHash member1 = new MemberHash("idA");
		 MemberHash member2 = new MemberHash("idA");
		 MemberNotHash member3 = new MemberNotHash("idC");
		 MemberNotHash member4 = new MemberNotHash("idC");
		 
		 //equals, hashCode를 오버라이딩  한 경우
		 System.out.println("(member1 == member2) = " + (member1 == member2));
		 System.out.println("member1 equals member2 = " +
		member1.equals(member2));
		 System.out.println("member1.hashCode() = " + member1.hashCode());
		 System.out.println("member2.hashCode() = " + member2.hashCode());
		
		 //equals , hashCode를 오버라이딩 하지 않은 경우
		 System.out.println("(member3 == member4) = " + (member3 == member4));
		 System.out.println("member3 equals member4 = " +
		member3.equals(member4));
		 System.out.println("member3.hashCode() = " + member3.hashCode());
		 System.out.println("member4.hashCode() = " + member4.hashCode());
		 
		
		 
		 
		 }

}

 

결과값

obj1.hashCode() = 1848402763

obj2.hashCode() = 205797316

 

10.hashCode = 10

'A'.hashCode = 65

'AB'.hashCode = 2081

-1.hashCode = -1

 

(member1 == member2) = false

member1 equals member2 = true

member1.hashCode() = 104101

member2.hashCode() = 104101

 

(member3 == member4) = false

member3 equals member4 = false

member3.hashCode() = 443308702

member4.hashCode() = 935044096

 

Object의 해시 코드 비교
Object 가 기본으로 제공하는 hashCode() 는 객체의 참조값을 해시 코드로 사용한다. 따라서 각각의 인스턴
스마다 서로 다른 값을 반환한다.
그 결과 obj1 , obj2 는 서로 다른 해시 코드를 반환한다.

 

자바의 기본 클래스의 해시 코드
Integer , String 같은 자바의 기본 클래스들은 대부분 내부 값을 기반으로 해시 코드를 구할 수 있도록
hashCode() 메서드를 재정의해 두었다.
따라서 데이터의 값이 같으면 같은 해시 코드를 반환한다.
해시 코드의 경우 정수를 반환하기 때문에 마이너스 값이 나올 수 있다

 

동일성과 동등성 복습
잠깐 동일성과 동등성에서 학습한 내용을 복습해보자.
Object 는 동등성 비교를 위한 equals() 메서드를 제공한다.
자바는 두 객체가 같다는 표현을 2가지로 분리해서 사용한다.
동일성(Identity): == 연산자를 사용해서 두 객체의 참조가 동일한 객체를 가리키고 있는지 확인
동등성(Equality): equals() 메서드를 사용하여 두 객체가 논리적으로 동등한지 확인

 

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


직접 구현하는 Set2 - MyHashSetV2

전에는 Integer타입만 들어올수 있는 set이었지만

이번에는 바꿔보자

package javaBasic2.collection.set;

import java.util.Arrays;
import java.util.LinkedList;

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) {
		//hashCode의 결과로 음수가 나올 수 있다. abs()를 사용해서 마이너스를 제거한다.
		return Math.abs(value.hashCode()%capacity);
	}
	public int getSize() {
		return size;
	}
	@Override
	public String toString() {
		return "MyHashSetV1{" +
				"buckets=" + Arrays.toString(buckets) +
				", size=" + size +
				'}';
	}
}

 

private LinkedList<Object>[] buckets
MyHashSetV1 은 Integer 숫자만 저장할 수 있었다. 여기서는 모든 타입을 저장할 수 있도록 Object 를 사
용한다.
추가로 저장, 검색, 삭제 메서드의 매개변수도 Object 로 변경했다.

 

hashIndex()
hashIndex() 부분이 변경되었다.
먼저 Object 의 hashCode() 를 호출해서 해시 코드를 찾는다. 그리고 찾은 해시 코드를 배열의 크기
( capacity )로 나머지 연산을 수행한다. 이렇게 해시 코드를 기반으로 해시 인덱스를 계산해서 반환한다.
Object 의 hashCode() 를 사용한 덕분에 모든 객체의 hashCode() 를 구할 수 있다. 물론 다형성에 의해 오
버라이딩 된 hashCode() 가 호출된다.
hashCode() 의 실행 결과로 음수가 나올 수 있는데, 배열의 인덱스로 음수는 사용할 수 없다. Math.abs() 를
사용하면 마이너스를 제거해서 항상 양수를 얻을 수 있다

 

 

메인코드

package collection.set;

public class MyHashSetV2Main {

	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);
		}
}

MyHashSetV1{buckets=[[], [AB], [], [], [], [A], [B, SET], [C], [D], []], size=6}

A.hashCode=65

B.hashCode=66

AB.hashCode=2081

SET.hashCode=81986

set.contains(SET) = true

 

문자열이 들어가는것을 확인 할 수 있다.

자바의 `String` `hashCode()` 재정의해 두었다. 우리는 값을 사용하면 된다.

 


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

package collection.set;

public class MyHashSetV2Main2 {

	public static void main(String[] args) {
		MyHashSetV2 set = new MyHashSetV2(10);
		Member member1 = new Member("동곤자바");
		Member member2 = new Member("많관부");
		set.add("A");
		set.add("B");
		set.add("C");
		set.add("D");
		set.add("AB");
		set.add("SET");
		set.add(member1);
		set.add(member2);
		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());
		//검색
		Object searchMem = member1;
		boolean result = set.contains(searchMem);
		System.out.println("set.contains(" + searchMem + ") = " + result);
		}
}

 

MyHashSetV2{buckets=[[], [AB], [], [], [], [A], [B, SET], [C, Member [id=많관부]], [D, Member [id=동곤자바]], []], size=8}

A.hashCode=65

B.hashCode=66

AB.hashCode=2081

SET.hashCode=81986

set.contains(Member [id=동곤자바]) = true

이렇게 Member객체도 넣을 수 있습니다.

 

주의점

Member클래스에 꼭

hashcode()와 equals()를 오버라이딩 할 것.

 

equals() 사용처**

그렇다면 `equals()` 언제 사용할까?

"JPA" 조회할 해시 인덱스는 0이다. 따라서 배열의 `0` 인덱스를 조회한다.

여기에는 `[hi, JPA]` 라는 회원 명이 있다. 이것을 하나하나 비교해야 한다. 이때 `equals()` 사용해서 비교한.

따라서 해시 자료 구조를 사용할 때는 `hashCode()` 물론이고, `equals()`

 

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

반드시 재정의해야 한다

 

Hashcode()와 eqauls()의 중요성

 

한번 Object에서 기본적으로 제공하는 해쉬코드와

재정의한 해쉬코드의 차이를 한번 보도록할게요

package collection.set;

public class Hashcod_eqauls {
	public static void main(String[] args) {
		Member2 m1 = new Member2("유동");
		Member2 m2 = new Member2("유동");
		
		System.out.println("hashCode()와 eqauls() 구현X");
		System.out.println("m1.hascode() : "+m1.hashCode());
		System.out.println("m2.hascode() : "+m2.hashCode());
		System.out.println("m1.equals(m2) : "+m1.equals(m2));
		
		System.out.println("\nhashCode()와 eqauls() 구현O");
		Member m3 = new Member("자바");
		Member m4 = new Member("자바");
		System.out.println("m3.hascode() : "+m3.hashCode());
		System.out.println("m4.hascode() : "+m4.hashCode());
		System.out.println("m3.equals(m4) : "+m3.equals(m4));
		

	}

}

hashCode()와 eqauls() 구현X

m1.hascode() : 1450495309

m2.hascode() : 1528902577

m1.equals(m2) : false

 

hashCode()와 eqauls() 구현O

m3.hascode() : 1631907

m4.hascode() : 1631907

m3.equals(m4) : true

 

보시는바와 같이

구현(재정의)를 안했으면

분명 논리적으로는 "유동"으로

같은데 해시코드가 다른 모습을 알 수 있어요.

 

Object의 hashcode()는 참조값을

기반으로 만들어짐!!!!

그러므로 재정의를 해 값만 따져야함

 

hashCode()와 eqauls()중 하나라도 빼먹지말고

재정의 해야함!!!!!

 

hashCode()만 정의하고

eqauls()를 정의하지 않은 경우에는

둘이 같은 객체라고 판단하지않음

 즉

중복된값은 넣지않는 set에 

같은 객체 두개가 들어가게되는것임

 


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

 

public interface Myset<E> {
	boolean add(E e);
	boolean remove(E e);
	boolean contains(E e);
	

}

 

제네릭으로 만든 인터페이스를 구현해서

MyHashSetV3를 제네릭클래스로 만들어보자!!

 

package collection.set;

import java.util.Arrays;
import java.util.LinkedList;

public class MyHashSetV3<E> implements Myset<E> {
		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<>();
			}
		}
		@Override
		public boolean add(E e) {
			int hashIndex = hashIndex(e);
			LinkedList<E> bucket = buckets[hashIndex];
			if (bucket.contains(e)) {
				return false;
			}
			bucket.add(e);
			size++;
			return true;
			
		}
		@Override
		public boolean remove(E e) {
			int hashIndex = hashIndex(e);
			LinkedList<E> bucket = buckets[hashIndex];
			boolean result = bucket.remove(e);
			if (result) {
				size--;
				return true;
			} else {
				return false;
			}
			
		}
		@Override
		public boolean contains(E e) {
			int hashIndex = hashIndex(e);
			LinkedList<E> bucket = buckets[hashIndex];
			return bucket.contains(e);
			
		}
		
		
		private int hashIndex(E e) {
			//hashCode의 결과로 음수가 나올 수 있다. abs()를 사용해서 마이너스를 제거한다.
			return Math.abs(e.hashCode()%capacity);
		}
		public int getSize() {
			return size;
		}
		@Override
		public String toString() {
			return "MyHashSetV2{" +
					"buckets=" + Arrays.toString(buckets) +
					", size=" + size +
					'}';
		}
		
	

}

전에는 Object로 다루던 객체들이

모두 E(제네릭타입)으로 변경 됐다

 

 

메인코드

package collection.set;

public class MyHashSetV3Main {
	public static void main(String[] args) {
		MyHashSetV3<Integer> iSet = new MyHashSetV3<Integer>();
		iSet.add(10);
		iSet.add(20);
		iSet.add(30);
		iSet.add(6);
		iSet.add(18);
		iSet.add(6);//중복된값은 set에 들어가지않음
		System.out.println(iSet);
		
		int searchI = 10;
		boolean contains = iSet.contains(searchI);
		System.out.println(searchI+"가 있는가?? "+contains);
		
		MyHashSetV3<Member> mSet = new MyHashSetV3<Member>();
		Member m1 = new Member("동곤자바");
		Member m2 = new Member("많이 들어와 주세요!");
		Member m3 = new Member("hashCode,eqauls구현완료!");
		mSet.add(m1);
		mSet.add(m2);
		mSet.add(m3);
		
		Member search = new Member("동곤자바");
		boolean contains2 = mSet.contains(search);
		System.out.println(search+"가 있나요?  :"+contains);
		

	}

}

MyHashSetV2{buckets=[[10, 20, 30], [], [], [], [], [], [6], [], [18], []], size=5}

10가 있는가?? true

 

MyHashSetV2{buckets=[[Member [id=많이 들어와 주세요!]], [], [], [], [], [], [], [], [Member [id=동곤자바], Member [id=hashCode,eqauls구현완료!]], []], size=3}

Member [id=동곤자바]가 있나요? :true

 

제네릭 덕분에 Integer만을 받는 Set도 만들 수 있으며

Member의 객체만을 인자로받는 Set을 만들 수 있었다