Hash
해시(Hash)는 임의의 크기를 갖는 데이터[Key]를 고정된 크기의 값으로 변환하는 알고리즘입니다. 이렇게 변환된 값은 해시 값 또는 해시 코드라고도 합니다. 해시 함수는 입력 데이터의 작은 변화에도 결과 값이 크게 달라지도록 설계되어 있습니다. 따라서 입력 데이터가 달라지면 해시 값도 크게 달라집니다.
Key로 많이 사용되는 것은 다양한데, 일반적으로 다음과 같은 요소들이 키로 많이 활용됩니다
1. 숫자: 숫자는 고유한 값을 가지고 있고, 비교적 간단하게 처리할 수 있으므로 키로 많이 사용됩니다. 예를 들어, 고객 ID, 주문 번호, 학번 등은 숫자로 된 키로 사용될 수 있습니다.
2. 문자열: 문자열도 키로 많이 사용됩니다. 예를 들어, 이메일 주소, 사용자명, 도메인 이름 등은 문자열로 된 키로 사용될 수 있습니다. 문자열은 다양한 길이와 형식을 가지고 있어 다양한 데이터를 식별하는 데에 유용합니다.
3. UUID(Universally Unique Identifier): UUID는 범용 고유 식별자로, 일반적으로 128비트 숫자로 표현됩니다. UUID는 충돌 가능성이 매우 낮아서 중복되지 않는 키로 사용하기에 좋습니다. 주로 분산 시스템에서 데이터 식별을 위해 사용됩니다.
4. 해시 값: 해시 함수를 사용하여 데이터를 해시 값으로 변환하고, 이를 키로 사용하는 경우도 많이 있습니다. 해시 값은 고유한 값을 가지며, 해시 함수의 특성에 따라 작은 변화에도 큰 차이를 보이므로 키로 적합합니다. 예를 들어, 파일의 체크섬이나 데이터의 무결성 검사를 위해 해시 값이 키로 사용될 수 있습니다.
5. 조합된 키: 여러 속성이나 값들을 조합하여 키로 사용하는 경우도 많습니다. 예를 들어, 이름과 생년월일을 조합하여 고유한 식별자를 생성하는 경우가 있습니다.
위의 요소들은 주요하게 사용되는 키의 예시입니다. 그러나 실제로 키로 사용되는 요소들은 데이터의 특성과 사용 환경에 따라 다양할 수 있습니다. 키 선택은 데이터베이스, 애플리케이션 또는 시스템의 요구사항을 고려하여 결정되어야 합니다.
해시 함수는 입력 데이터의 작은 변화에도 결과 값이 크게 달라지도록 설계(일명 눈사태)되었으므로, 다음과 같은 의미가 있습니다
1. 일부 데이터 변경의 전파 제한: 입력 데이터의 작은 변화가 해시 함수의 결과에 큰 영향을 미치기 때문에, 해시 값의 작은 차이는 입력 데이터의 작은 차이에 대응합니다. 이러한 특성은 입력 데이터의 미세한 변경이 해시 값에 큰 영향을 미치는 것을 방지하며, 입력 데이터의 일부분만 변경되더라도 전체 해시 값이 크게 달라지는 현상을 발생시킵니다. 이는 데이터의 무결성 검사에 유용하며, 예를 들어 파일의 내용이 하나의 비트만 변경되더라도 해시 값이 완전히 다른 값으로 변하게 됩니다.
2. 보안 및 무결성 검사: 해시 함수의 결과 값이 작은 변화에도 크게 달라지도록 설계되면, 누구나 원본 데이터를 알지 못하더라도 동일한 입력에 대해 동일한 해시 값을 얻을 수 있습니다. 이 특성은 패스워드 저장, 메시지 인증 코드 등 보안 및 무결성 검사에 사용됩니다. 작은 입력 변경은 해시 값의 완전히 다른 값으로 변환되므로, 데이터의 위조나 변조를 감지할 수 있습니다.
3. 해시 테이블의 성능 개선: 해시 함수의 결과 값이 입력 데이터의 작은 차이에 큰 영향을 받으면, 해시 테이블과 같은 자료 구조에서 충돌을 최소화할 수 있습니다. 입력 데이터가 조금만 다른 경우에도 서로 다른 해시 값이 생성되므로, 해시 테이블의 성능이 향상됩니다. 이는 검색, 캐싱 등의 작업에서 유용합니다.
결론적으로, 해시 함수가 입력 데이터의 작은 변화에 크게 반응하는 것은 데이터 무결성, 보안, 자료 구조의 성능 향상 등 다양한 응용 분야에서 유용한 특성을 가지게 됩니다.
해시 함수의 특징은 다음과 같습니다:
- 일방향성(One-way property): 해시 함수는 입력 값을 해시 값으로 변환하는 것은 가능하지만, 해시 값을 역으로 입력 값으로 변환하는 것은 거의 불가능합니다. 즉, 해시 값을 통해 원래 데이터를 복원하는 것은 매우 어렵습니다.
- 결정론적(결정적, Deterministic): 동일한 입력에 대해서는 항상 동일한 해시 값을 생성합니다. 예를 들어, "Hello, world!"라는 문자열을 해시 함수에 입력하면 항상 같은 해시 값을 얻을 수 있습니다.
- 충돌 저항성(Collision resistance): 두 개의 다른 입력에 대해 동일한 해시 값을 얻는 충돌이 발생하는 것을 매우 어렵게 만들어야 합니다. 좋은 해시 함수는 충돌이 거의 발생하지 않도록 설계되어야 합니다.
해시 함수는 다양한 분야에서 유용하게 활용됩니다. 몇 가지 예를 들어보면,
- 데이터 무결성 검사: 해시 함수는 데이터의 무결성을 확인하기 위해 사용될 수 있습니다. 데이터를 전송 또는 저장하기 전에 해시 값을 계산하고, 데이터를 받는 측에서 다시 해시 값을 계산하여 이를 비교함으로써 데이터가 변경되지 않았는지 확인할 수 있습니다.
- 암호화: 해시 함수는 암호화 알고리즘에서도 사용됩니다. 비밀번호 저장 시에는 실제 비밀번호를 저장하는 대신에 해시 값을 저장하여 비밀번호를 보호합니다. 사용자가 로그인할 때 입력한 비밀번호의 해시 값을 계산하여 저장된 해시 값과 비교하여 인증을 수행합니다.
- 데이터 검색: 해시 함수를 사용하여 데이터를 저장하고 검색할 수 있습니다. 해시 함수를 통해 데이터의 특정 속성을 기반으로 고유한 해시 값을 생성하고, 이를 인덱스로 사용하여 데이터를 빠르게 검색할 수 있습니다.
좋은 해시 함수는 다음과 같은 속성을 갖추어야 합니다:
- 빠른 계산 속도: 해시 함수는 빠르게 계산될 수 있어야 합니다.
- 충돌 저항성: 충돌이 발생할 확률이 매우 낮아야 합니다.
- 분포성: 입력 데이터의 작은 변화에도 해시 값이 균일하게 분포되어야 합니다.
주의할 점은 해시 함수가 충돌이 발생할 수 있다는 것입니다. 입력 공간이 무한하지만 해시 값의 공간은 제한적이기 때문에 두 개 이상의 다른 입력이 동일한 해시 값을 가질 수 있습니다. 이를 충돌(Collision)이라고 합니다. 좋은 해시 함수는 충돌이 발생할 확률을 매우 낮추는 것이 목표입니다. 충돌이 발생하면 해시 값이 중복되는 데이터를 구별하는 것이 어려워질 수 있습니다.
많은 알고리즘이 해시 함수로 사용될 수 있으며, 가장 널리 알려진 해시 함수 중 일부에는 MD5, SHA-1, SHA-256, SHA-3 등이 있습니다. 그러나 보안 요구 사항이나 응용 프로그램의 목적에 따라 적절한 해시 함수를 선택해야 합니다.
Hash Table
데이터 검색에서 사용되는 해시 알고리즘은 주로 해시 테이블(Hash Table)이라는 자료구조를 구현하는 데에 활용됩니다. 해시 테이블은 효율적인 데이터 검색을 위해 키(Key)와 값(Value)의 쌍으로 이루어진 데이터를 저장하는 자료구조입니다.
해시 테이블(Hash table)은 다음과 같은 구성 요소로 구성됩니다:
- 해시 함수 (Hash function): 해시 함수는 입력 데이터를 해시 코드로 변환하는 알고리즘입니다. 입력 데이터의 특성에 따라 해시 함수는 고유한 해시 코드를 생성해야 합니다. 이 해시 코드는 버킷의 인덱스로 사용되어 데이터를 저장하고 검색할 수 있게 합니다.
- 버킷 (Bucket): 버킷은 데이터를 저장하는 해시 테이블의 각 요소입니다. 일반적으로 배열 형태로 구성되며, 각 버킷은 데이터를 저장하기 위한 공간을 가지고 있습니다. 해시 코드에 따라 버킷의 인덱스로 매핑되어 데이터가 저장됩니다.
- 데이터 (Data): 해시 테이블에 저장되는 실제 데이터입니다. 데이터는 키-값 쌍의 형태로 저장될 수 있으며, 키를 통해 데이터에 접근하고 검색할 수 있습니다. 각 데이터는 해시 함수를 통해 생성된 해시 코드에 따라 적절한 버킷에 저장됩니다.
- 충돌 해결 메커니즘 (Collision resolution mechanism): 해시 함수를 통해 생성된 해시 코드는 고유해야 하지만, 서로 다른 데이터가 동일한 해시 코드를 가질 수 있습니다. 이를 "충돌(collision)"이라고 합니다. 충돌이 발생할 경우에는 충돌 해결 메커니즘이 필요합니다. 일반적으로 충돌을 해결하기 위해 체이닝(Chaining) 또는 개방 주소법(Open addressing)과 같은 방법을 사용합니다.
- 로드 팩터 (Load factor): 로드 팩터는 해시 테이블에 저장된 데이터의 양과 버킷의 수 사이의 관계를 나타냅니다. 로드 팩터는 해시 테이블의 성능에 영향을 미치는 중요한 요소로서, 적절한 로드 팩터를 유지하기 위해 필요에 따라 버킷의 크기를 동적으로 조절할 수 있습니다.
로드 팩터(Load factor)는 해시 테이블(Hash table)에서 현재 저장된 데이터의 양과 버킷의 수 사이의 비율을 나타내는 값입니다.
일반적으로 로드 팩터는 저장된 데이터의 수를 버킷의 수로 나눈 값으로 표현됩니다. 로드 팩터는 해시 테이블의 성능과 공간 활용에 영향을 미치는 중요한 요소입니다. 로드 팩터가 높을수록 해시 테이블에 저장된 데이터의 양이 버킷 수에 비해 많다는 의미이고, 낮을수록 데이터의 양이 적다는 의미입니다.
로드 팩터의 값에 따라 해시 테이블의 성능이 영향을 받습니다. 로드 팩터가 낮으면 충돌이 적어 성능이 향상될 수 있지만, 메모리 공간의 낭비가 발생할 수 있습니다. 반대로 로드 팩터가 높으면 충돌이 더 자주 발생하여 성능이 저하될 수 있지만, 메모리 공간을 보다 효율적으로 사용할 수 있습니다.
로드 팩터를 적절하게 조정하는 것은 해시 테이블의 성능 최적화에 중요합니다. 일반적으로 로드 팩터가 특정 임계값을 초과하면 해시 테이블의 크기를 조정하거나 재조정하는 작업이 필요할 수 있습니다. 이를 통해 해시 테이블의 성능을 유지하면서 공간을 효율적으로 활용할 수 있습니다.
로드 팩터의 선택은 해시 테이블을 사용하는 애플리케이션의 요구사항과 예상되는 데이터의 양에 따라 달라집니다. 일반적으로 0.7 또는 0.8을 넘지 않는 로드 팩터를 유지하는 것이 성능과 공간 활용을 균형있게 유지하는 방법일 수 있습니다.
해시 - 나무위키
이러한 특성에 힘입어 다양한 목적에 맞게 설계된 해시 함수가 존재하며 다음과 같은 다양한 분야에서 매우 유용하게 사용된다. 해시 테이블 (또는 해시 맵)해시셋(set)중복 레코드 검색유사 레
namu.wiki
buckets
해시 테이블(Hash Table)은 데이터를 저장하는 자료구조로, 효율적인 검색을 위해 사용됩니다. 해시 테이블은 해시 함수를 사용하여 데이터를 해시 값으로 변환하고, 이 해시 값을 배열의 인덱스로 사용하여 데이터를 저장합니다. 각 배열 요소를 버킷(Bucket)이라고 부릅니다.
버킷은 일반적으로 배열의 요소를 가리키는 포인터로 구성됩니다. 해시 테이블의 크기는 일반적으로 해시 값을 인덱스로 사용할 수 있는 배열의 크기로 결정됩니다. 예를 들어, 크기가 10인 해시 테이블은 10개의 버킷으로 구성된 배열을 의미합니다.
각 버킷은 연결 리스트(Chaining) 또는 개방 주소법(Open Addressing)의 일부로 구성될 수 있습니다. 연결 리스트 방식에서는 각 버킷이 데이터를 저장하는데 사용되며, 동일한 해시 값이 발생하는 경우 해당 버킷에 있는 연결 리스트에 데이터가 추가됩니다. 개방 주소법에서는 버킷 자체가 데이터를 저장하는데 사용되며, 충돌이 발생할 경우 다른 빈 버킷을 찾아 데이터를 저장합니다.
버킷의 역할은 데이터를 해시 값에 따라 저장하고, 데이터의 검색을 위해 해당 버킷을 참조하는 것입니다. 해시 테이블은 버킷을 사용하여 데이터를 저장하고 검색하기 때문에 빠른 검색 속도를 제공할 수 있습니다. 데이터의 해시 값에 따라 적절한 버킷으로 이동하여 데이터를 저장하고 검색함으로써 효율적인 데이터 관리가 가능해집니다.
해시 테이블은 다음과 같은 단계로 동작합니다:
- 해시 함수 선택: 데이터를 해시 테이블에 저장하기 전에 키를 해시 값으로 변환하는 해시 함수를 선택합니다. 해시 함수는 키의 일부분이나 전체를 입력으로 받아 해시 값으로 변환합니다.
- 해시 값에 따른 인덱스 계산: 해시 함수를 사용하여 각 키에 대한 해시 값을 계산합니다. 이 해시 값은 일반적으로 정수로 표현되며, 해시 테이블의 인덱스로 사용됩니다. 해시 값의 범위는 해시 테이블의 크기에 따라 결정됩니다.
- 충돌 해결: 서로 다른 키가 동일한 해시 값을 가질 수 있으므로, 이를 충돌(Collision)이라고 합니다. 충돌이 발생할 경우에는 충돌 해결 방법을 사용하여 충돌을 처리합니다. 대표적인 충돌 해결 방법으로는 개방 주소법(Open Addressing)과 연결 리스트(Chaining) 방식이 있습니다.
- 개방 주소법(Open Addressing): 충돌이 발생하면 다른 빈 슬롯을 찾아서 해당 슬롯에 데이터를 저장하는 방식입니다. 선형 탐사(Linear Probing), 이차 탐사(Quadratic Probing), 이중 해싱(Double Hashing) 등이 개방 주소법의 일부입니다
- 연결 리스트(Chaining): 충돌이 발생하면 같은 인덱스에 해당하는 슬롯에 연결 리스트를 이용하여 데이터를 연결하여 저장합니다. 충돌이 발생하면 연결 리스트에 데이터를 추가합니다.
4. 데이터 저장 및 검색: 충돌이 발생하지 않는 경우에는 해시 값을 인덱스로 사용하여 데이터를 저장합니다. 이후에 데이터를 검색할 때에도 동일한 과정을 거쳐 해시 값을 계산하여 해당 인덱스로 접근하여 검색을 수행합니다.
해시 테이블은 일반적으로 데이터 검색에서 매우 빠른 속도를 제공합니다. 해시 함수의 성능에 따라 충돌이 적게 발생하면 더욱 효율적인 검색이 가능합니다. 따라서 좋은 해시 함수 선택과 충돌 해결 방법의 적절한 선택이 해시 테이블의 성능에 중요한 영향을 미칩니다.
Java Hashtable
Java의 Hashtable 클래스는 추상 Dictionary 클래스의 구체적인 구현입니다.
Hashtable은 Java HashMap과 유사한 데이터 구조로, 키-값 쌍(엔트리) 형태의 요소(객체) 컬렉션을 저장할 수 있습니다.
키 객체는 값들을 Hashtable에서 저장하고 검색하기 위해 hashCode()와 equals() 메서드를 구현해야 합니다.
다시 말해, Hashtable은 Object 클래스에서 정의한 hashCode()와 equals() 메서드를 오버라이드한 키 객체만을 저장할 수 있습니다.
Hashtable과 HashMap의 주요 차이점은 스레드 액세스 방식입니다.
Hashtable 클래스는 동기화된 클래스로, 즉 스레드 안전하다는 의미입니다. 여러 스레드는 동시에(동시에) 동일한 Hashtable 인스턴스에 액세스할 수 없습니다.
HashMap 클래스는 동기화되지 않았기 때문에 스레드 안전하지 않습니다. 여러 스레드는 동시에 동일한 HashMap 인스턴스에 액세스할 수 있습니다. 따라서 오직 한 스레드가 객체를 사용할 때에만 안전하게 사용할 수 있습니다.
Java의 Hashtable은 JDK 1.0 버전에서 도입되었으며 java.util.Hashtable 패키지에 있습니다. JDK 1.2 버전 이전에는 Hashtable은 키와 값을 매핑하기 위해 사용되었습니다.
이후로 Java 1.2 버전에서 Hashtable은 Map 인터페이스를 구현하도록 수정되었습니다. 따라서 Hashtable은 Java 컬렉션 프레임워크와 통합되었습니다.
그러나 Java 컬렉션 프레임워크와 일관성이 부족하여 레거시 클래스로 간주됩니다.
Hierarchy of Java Hashtable
Hashtable 클래스는 Dictionary 클래스를 확장하고 Map, Cloneable, Serializable 인터페이스를 구현합니다. Java에서 Hashtable의 계층 구조 다이어그램은 아래 그림에 표시되어 있습니다.
Hashtable을 사용하기 위해 기억해야 할 Java의 몇 가지 기능이 있습니다.
1. Java Hashtable의 내부 데이터 구조는 해시 테이블입니다.
2. 삽입 순서는 유지되지 않습니다. 즉, 삽입 순서를 유지하지 않습니다.
3. 중복된 키는 허용되지 않지만 값은 중복될 수 있습니다.
4. 키와 값 모두에 대해 다른 유형의 객체를 허용합니다.
5. 키와 값 모두에 대해 null은 허용되지 않습니다. null 키 또는 값을 저장하려고 시도하면 NullPointerException이라는 RuntimeException이 발생합니다.
6. Java Hashtable은 Serializable 및 Cloneable 인터페이스를 구현하지만 Random Access를 구현하지 않습니다.
7. Hashtable에 있는 모든 메서드는 동기화되어 있습니다. 따라서 Hashtable 객체는 스레드 안전합니다.
8. 검색(검색) 작업이 빈번한 경우 Hashtable이 가장 좋은 선택입니다.
9. Hashtable은 동기화되어 있기 때문에 HashMap과 비교하여 연산이 느립니다.
Set Interface
자바에서 Set은 중복된 요소를 허용하지 않는 컬렉션 인터페이스입니다. Set은 수학적인 집합 개념을 모델링하며, 고유한 요소들의 모음을 나타냅니다. Set은 다양한 구현체를 가질 수 있으며, 가장 일반적인 구현체로는 HashSet, TreeSet, LinkedHashSet이 있습니다.
- HashSet: HashSet은 해시 테이블을 사용하여 요소를 저장하는 Set의 구현체입니다. HashSet은 요소의 순서를 유지하지 않습니다. HashSet은 O(1) 시간 복잡도로 요소를 삽입, 삭제, 검색할 수 있습니다. 그러나 요소의 순서가 중요하지 않거나, 중복 요소를 허용하지 않는 경우에 사용하기 적합합니다.
- TreeSet: TreeSet은 이진 검색 트리를 사용하여 요소를 저장하는 Set의 구현체입니다. TreeSet은 요소를 기본적으로 오름차순으로 정렬된 상태로 유지합니다. TreeSet은 O(log n) 시간 복잡도로 요소를 삽입, 삭제, 검색할 수 있습니다. 정렬된 상태로 요소를 유지하고 싶거나, 범위 기반의 검색이 필요한 경우에 사용하기 적합합니다.
- LinkedHashSet: LinkedHashSet은 해시 테이블과 연결 리스트를 사용하여 요소를 저장하는 Set의 구현체입니다. LinkedHashSet은 요소를 삽입한 순서대로 유지합니다. LinkedHashSet은 HashSet의 기능에 순서를 추가한 것으로 생각할 수 있습니다. HashSet과 마찬가지로 O(1) 시간 복잡도로 요소를 삽입, 삭제, 검색할 수 있습니다. 요소의 순서를 유지하고 싶으면서도 중복 요소를 허용하지 않는 경우에 사용하기 적합합니다.
package java.util;
public interface Set<E> extends Collection<E> {
// Query Operations
int size();
boolean isEmpty();
boolean contains(Object o);
Iterator<E> iterator();
Object[] toArray();
<T> T[] toArray(T[] a);
// Modification Operations
boolean add(E e);
boolean remove(Object o);
// Bulk Operations
boolean containsAll(Collection<?> c);
boolean addAll(Collection<? extends E> c);
boolean retainAll(Collection<?> c);
boolean removeAll(Collection<?> c);
void clear();
// Comparison and hashing
boolean equals(Object o);
int hashCode();
@Override
default Spliterator<E> spliterator() {
return Spliterators.spliterator(this, Spliterator.DISTINCT);
}
@SuppressWarnings("unchecked")
static <E> Set<E> of() {
return (Set<E>) ImmutableCollections.EMPTY_SET;
}
static <E> Set<E> of(E e1) {
return new ImmutableCollections.Set12<>(e1);
}
static <E> Set<E> of(E e1, E e2) {
return new ImmutableCollections.Set12<>(e1, e2);
}
static <E> Set<E> of(E e1, E e2, E e3) {
return new ImmutableCollections.SetN<>(e1, e2, e3);
}
static <E> Set<E> of(E e1, E e2, E e3, E e4) {
return new ImmutableCollections.SetN<>(e1, e2, e3, e4);
}
static <E> Set<E> of(E e1, E e2, E e3, E e4, E e5) {
return new ImmutableCollections.SetN<>(e1, e2, e3, e4, e5);
}
static <E> Set<E> of(E e1, E e2, E e3, E e4, E e5, E e6) {
return new ImmutableCollections.SetN<>(e1, e2, e3, e4, e5,
e6);
}
static <E> Set<E> of(E e1, E e2, E e3, E e4, E e5, E e6, E e7) {
return new ImmutableCollections.SetN<>(e1, e2, e3, e4, e5,
e6, e7);
}
static <E> Set<E> of(E e1, E e2, E e3, E e4, E e5, E e6, E e7, E e8) {
return new ImmutableCollections.SetN<>(e1, e2, e3, e4, e5,
e6, e7, e8);
}
static <E> Set<E> of(E e1, E e2, E e3, E e4, E e5, E e6, E e7, E e8, E e9) {
return new ImmutableCollections.SetN<>(e1, e2, e3, e4, e5,
e6, e7, e8, e9);
}
static <E> Set<E> of(E e1, E e2, E e3, E e4, E e5, E e6, E e7, E e8, E e9, E e10) {
return new ImmutableCollections.SetN<>(e1, e2, e3, e4, e5,
e6, e7, e8, e9, e10);
}
@SafeVarargs
@SuppressWarnings("varargs")
static <E> Set<E> of(E... elements) {
switch (elements.length) { // implicit null check of elements
case 0:
@SuppressWarnings("unchecked")
var set = (Set<E>) ImmutableCollections.EMPTY_SET;
return set;
case 1:
return new ImmutableCollections.Set12<>(elements[0]);
case 2:
return new ImmutableCollections.Set12<>(elements[0], elements[1]);
default:
return new ImmutableCollections.SetN<>(elements);
}
}
@SuppressWarnings("unchecked")
static <E> Set<E> copyOf(Collection<? extends E> coll) {
if (coll instanceof ImmutableCollections.AbstractImmutableSet) {
return (Set<E>)coll;
} else {
return (Set<E>)Set.of(new HashSet<>(coll).toArray());
}
}
}
Set 인터페이스는 Collection 인터페이스를 확장하고 있으며, 다양한 메서드를 제공합니다. 예를 들어, Set에 요소를 추가하려면 add() 메서드를 사용하고, 요소를 삭제하려면 remove() 메서드를 사용합니다. 또한, Set의 크기를 확인하려면 size() 메서드를 사용할 수 있습니다.
Set은 중복된 요소를 허용하지 않으므로, 요소의 동등성(equality)을 판단하기 위해 요소 클래스에 equals()와 hashCode() 메서드를 적절하게 구현해야 합니다. 동등성 판단은 HashSet 및 HashMap과 같은 내부 구조를 사용하는 Set 구현체에서 중요합니다.
Set은 중복된 요소를 제거하고 고유한 요소들로만 구성된 컬렉션을 다루고자 할 때 유용합니다. 예를 들어, 집합 연산을 수행하거나 중복을 제거한 목록을 생성하려는 경우에 Set을 사용할 수 있습니다.
HashSet
HashSet은 중복 요소가 없는 (고유한) 요소 (객체)의 정렬되지 않은 컬렉션입니다.
HashSet은 고유한 요소 집합을 저장하기 위해 내부적으로 Hashtable 데이터 구조를 사용합니다. 요소를 검색하고 검색한 요소를 검색하는 데에는 매우 빠르며, 상수 시간 성능을 제공합니다.
HashSet은 Java 1.2 버전에서 도입되었으며 java.util.HashSet 패키지에 포함되어 있습니다.
Hierarchy of HashSet in Java
HashSet 클래스는 AbstractSet 클래스를 확장하고 Set 인터페이스를 구현합니다. AbstractSet 클래스 자체는 AbstractCollection 클래스를 확장합니다.
또한, cloneable과 serializable 마커 인터페이스를 구현합니다. Java HashSet의 계층 구조 다이어그램은 아래 그림에 표시되어 있습니다.
Key Features of HashSet
Java HashSet의 몇 가지 중요한 기능을 염두에 두어야 합니다. 다음과 같습니다:
1. HashSet의 내부 데이터 구조는 Hashtable입니다. 해시라는 메커니즘을 사용하여 데이터를 저장합니다.
2. HashSet은 중복 요소를 허용하지 않습니다. 만약 HashSet에 중복된 요소를 추가하려고 하면, 이전 요소는 덮어쓰여집니다.
3. HashSet은 null 요소를 하나만 허용합니다. 여러 개의 null 요소를 추가하려고 하면, 여전히 하나의 null 값만 반환됩니다.
4. HashSet은 요소가 추가된 순서를 유지하지 않습니다. 요소의 순서는 예측할 수 없습니다. 무작위 순서로 반환됩니다.
5. 해싱 기법을 사용하여 빠르며, 추가 (삽입), 검색, 제거, 포함 여부 및 크기 조작에 대해 일정한 성능을 제공합니다.
해싱은 add(), remove(), contains(), size()와 같은 메서드에 대해 일정한 실행 시간을 제공합니다. 이는 큰 집합에 대해서도 마찬가지입니다.
6. HashSet 클래스는 동기화되지 않았기 때문에 스레드 안전하지 않습니다. 따라서 여러 스레드가 동시에 동일한 HashSet 객체를 사용할 수 있으며, 결정적인 최종 출력을 제공하지 않을 것입니다.
HashSet를 동기화하려면 Collections.synchronizedSet() 메서드를 사용하세요.
7. HashSet 클래스에서 반환된 반복자는 fail-fast입니다. 이는 반복 중에 HashSet에서 발생한 수정이 ConcurrentModificationException을 발생시킬 것이라는 의미입니다.
HashSet Class Methods in Java
Java HashSet 클래스는 추가적인 메서드를 정의하지 않습니다. 부모 클래스와 구현된 인터페이스의 메서드를 상속받습니다.
상속받은 슈퍼클래스와 인터페이스의 다양한 메서드는 다음과 같습니다:
1. boolean add(Object o): 이 메서드는 지정된 요소를 집합에 추가하는 데 사용됩니다. 지정된 요소가 집합에 이미 존재하지 않으면 true를 반환합니다.
2. boolean addAll(Collection c): 이 메서드는 다른 컬렉션에서 그룹으로 요소를 집합에 추가하는 데 사용됩니다.
컬렉션의 각 요소는 각 요소에 대해 add() 메서드를 호출하여 현재 집합에 추가됩니다. 현재 집합이 변경되면 true를 반환합니다.
요소가 추가되지 않은 경우 false가 반환됩니다. 현재 집합이 요소 추가를 지원하지 않을 경우
UnsupportedOperationException이 발생합니다.
3. boolean remove(Object o): 이 메서드는 집합에서 지정된 요소를 제거하는 데 사용됩니다. 요소를 제거하기 위해 집합은 각 요소에 대해 equals() 메서드를 내부적으로 사용합니다. 요소가 찾아지면 집합에서 요소가 제거되고 true를 반환합니다.
찾지 못한 경우 false가 반환됩니다. 삭제가 지원되지 않는 경우 UnsupportedOperationException이 발생합니다.
4. void clear(): 이 메서드는 집합에서 모든 요소를 제거하는 데 사용됩니다. 반환값은 없습니다.
5. boolean contains(Object element): 특정 요소가 집합에 있는지 확인하려면 contains() 메서드를 사용합니다.
지정된 요소가 집합에 있으면 true를 반환하고, 그렇지 않으면 false를 반환합니다. 요소의 동등성 검사는 요소의 equals() 메서드를 통해 이루어집니다.
6. int size(): 집합에 포함된 요소의 개수를 알고 싶을 때 size() 메서드를 호출합니다.
7. boolean isEmpty(): HashSet에 요소가 있는지 확인하려면 isEmpty() 메서드를 호출합니다.
8. Object clone(): HashSet은 cloneable 인터페이스를 구현했으므로, HashSet의 clone() 메서드를 호출하여 HashSet의 얕은 복사본을 만들 수 있습니다.
9. boolean equals(Object o): HashSet 클래스는 집합 내의 각 요소에 대해 equals() 메서드를 사용하여 동등성을 정의합니다.
10. int hashCode(): HashSet 클래스는 집합에 대한 적절한 해시 코드 값을 정의하기 위해 hashCode() 메서드를 재정의합니다. 모든 요소의 해시 코드를 합산하여 동일한 해시 코드를 반환합니다.
다음은 HashSet을 사용하는 몇 가지 예시 코드입니다.
1. 문자열 HashSet
import java.util.HashSet;
public class StringHashSetExample {
public static void main(String[] args) {
HashSet<String> stringSet = new HashSet<>();
stringSet.add("Apple");
stringSet.add("Banana");
stringSet.add("Orange");
System.out.println("HashSet의 크기: " + stringSet.size());
// 모든 요소 출력
for (String item : stringSet) {
System.out.println(item);
}
// 요소 제거
stringSet.remove("Banana");
System.out.println("HashSet의 크기: " + stringSet.size());
// 요소 존재 여부 확인
System.out.println("Orange가 HashSet에 포함되어 있는가? " + stringSet.contains("Orange"));
}
}
2. 정수 HashSet
import java.util.HashSet;
public class IntegerHashSetExample {
public static void main(String[] args) {
HashSet<Integer> integerSet = new HashSet<>();
integerSet.add(10);
integerSet.add(20);
integerSet.add(30);
System.out.println("HashSet의 크기: " + integerSet.size());
// 모든 요소 출력
for (int item : integerSet) {
System.out.println(item);
}
// 요소 제거
integerSet.remove(20);
System.out.println("HashSet의 크기: " + integerSet.size());
// 요소 존재 여부 확인
System.out.println("30이 HashSet에 포함되어 있는가? " + integerSet.contains(30));
}
}
3. 사용자 정의 클래스의 HashSet
import java.util.HashSet;
class Person {
private String name;
private int age;
public Person(String name, int age) {
this.name = name;
this.age = age;
}
public String getName() {
return name;
}
public int getAge() {
return age;
}
@Override
public boolean equals(Object obj) {
if (this == obj) {
return true;
}
if (obj == null || getClass() != obj.getClass()) {
return false;
}
Person person = (Person) obj;
return age == person.age && name.equals(person.name);
}
@Override
public int hashCode() {
return 31 * name.hashCode() + age;
}
}
public class CustomClassHashSetExample {
public static void main(String[] args) {
HashSet<Person> personSet = new HashSet<>();
Person person1 = new Person("Alice", 25);
Person person2 = new Person("Bob", 30);
Person person3 = new Person("Alice", 25);
personSet.add(person1);
personSet.add(person2);
personSet.add(person3);
System.out.println("HashSet의 크기: " + personSet.size());
// 모든 요소 출력
for (Person person : personSet) {
System.out.println(person.getName() + ", " + person.getAge());
}
// 요소 제거
personSet.remove(person2);
System.out.println("HashSet의 크기: " + personSet.size());
// 요소 존재 여부 확인
Person person4 = new Person("Alice", 25);
System.out.println("person4가 HashSet에 포함되어 있는가? " + personSet.contains(person4));
}
}
4. HashSet을 활용한 중복 요소 제거
import java.util.HashSet;
import java.util.ArrayList;
public class DuplicateRemovalUsingHashSet {
public static void main(String[] args) {
ArrayList<Integer> numbers = new ArrayList<>();
numbers.add(10);
numbers.add(20);
numbers.add(30);
numbers.add(10);
numbers.add(40);
numbers.add(20);
System.out.println("중복 요소가 포함된 리스트: " + numbers);
HashSet<Integer> uniqueNumbers = new HashSet<>(numbers);
numbers.clear();
numbers.addAll(uniqueNumbers);
System.out.println("중복 제거된 리스트: " + numbers);
}
}
5. HashSet을 활용한 교집합, 합집합, 차집합 계산
import java.util.HashSet;
public class SetOperationsExample {
public static void main(String[] args) {
HashSet<Integer> set1 = new HashSet<>();
set1.add(1);
set1.add(2);
set1.add(3);
HashSet<Integer> set2 = new HashSet<>();
set2.add(3);
set2.add(4);
set2.add(5);
// 교집합 계산
HashSet<Integer> intersection = new HashSet<>(set1);
intersection.retainAll(set2);
System.out.println("교집합: " + intersection);
// 합집합 계산
HashSet<Integer> union = new HashSet<>(set1);
union.addAll(set2);
System.out.println("합집합: " + union);
// 차집합 계산
HashSet<Integer> difference = new HashSet<>(set1);
difference.removeAll(set2);
System.out.println("차집합: " + difference);
}
}
6. HashSet을 사용하여 문자열 중복 제거
import java.util.HashSet;
import java.util.ArrayList;
public class RemoveDuplicateStrings {
public static void main(String[] args) {
ArrayList<String> strings = new ArrayList<>();
strings.add("Apple");
strings.add("Banana");
strings.add("Orange");
strings.add("Apple");
strings.add("Grape");
strings.add("Banana");
System.out.println("중복 요소가 포함된 리스트: " + strings);
HashSet<String> uniqueStrings = new HashSet<>(strings);
strings.clear();
strings.addAll(uniqueStrings);
System.out.println("중복 제거된 리스트: " + strings);
}
}
7. HashSet을 사용하여 배열에서 중복 요소 찾기
import java.util.HashSet;
public class FindDuplicatesInArray {
public static void main(String[] args) {
int[] numbers = {2, 4, 6, 8, 4, 10, 6, 12, 14, 8};
HashSet<Integer> uniqueNumbers = new HashSet<>();
HashSet<Integer> duplicateNumbers = new HashSet<>();
for (int number : numbers) {
if (!uniqueNumbers.add(number)) {
duplicateNumbers.add(number);
}
}
System.out.println("중복 요소: " + duplicateNumbers);
}
}
8. HashSet을 사용하여 두 개의 문자열 배열에서 공통 요소 찾기
import java.util.HashSet;
public class FindCommonElementsInArrays {
public static void main(String[] args) {
String[] array1 = {"Apple", "Banana", "Orange", "Grape"};
String[] array2 = {"Orange", "Grape", "Mango", "Pineapple"};
HashSet<String> set1 = new HashSet<>();
for (String element : array1) {
set1.add(element);
}
HashSet<String> commonElements = new HashSet<>();
for (String element : array2) {
if (set1.contains(element)) {
commonElements.add(element);
}
}
System.out.println("공통 요소: " + commonElements);
}
}
9. HashSet을 사용하여 배열에서 고유한 요소 추출하기
import java.util.HashSet;
public class ExtractUniqueElementsFromArray {
public static void main(String[] args) {
int[] numbers = {2, 4, 6, 8, 4, 10, 6, 12, 14, 8};
HashSet<Integer> uniqueElements = new HashSet<>();
for (int number : numbers) {
uniqueElements.add(number);
}
System.out.println("고유한 요소: " + uniqueElements);
}
}
10. HashSet을 사용하여 문자열 배열에서 중복되지 않는 요소 추출하기
import java.util.HashSet;
public class ExtractNonDuplicateElementsFromArray {
public static void main(String[] args) {
String[] strings = {"Apple", "Banana", "Orange", "Apple", "Grape", "Banana"};
HashSet<String> uniqueElements = new HashSet<>();
HashSet<String> nonDuplicateElements = new HashSet<>();
for (String str : strings) {
if (!uniqueElements.add(str)) {
nonDuplicateElements.remove(str);
} else {
nonDuplicateElements.add(str);
}
}
System.out.println("중복되지 않는 요소: " + nonDuplicateElements);
}
}
Map Interface
Map 인터페이스는 Java 컬렉션 프레임워크에서 Key-Value pair의 집합을 나타내는 인터페이스입니다. 이 인터페이스는 다양한 맵 구현체에서 사용되며, 키와 값의 관계를 저장하고 조회하는 기능을 제공합니다.
Map 인터페이스의 특징은 다음과 같습니다:
1. 키-값 쌍 저장: Map은 키와 값의 쌍을 저장합니다. 키는 중복될 수 없고, 각 키는 유일해야 합니다. 값은 중복 저장될 수 있습니다.
2. 키를 통한 값 조회: 주어진 키를 사용하여 해당 키에 연결된 값을 검색할 수 있습니다. get(key) 메서드를 사용하여 특정 키에 해당하는 값을 가져올 수 있습니다.
3. 키-값 쌍 삭제: remove(key) 메서드를 사용하여 특정 키-값 쌍을 삭제할 수 있습니다.
4. 특정 키의 존재 여부 확인: containsKey(key) 메서드를 사용하여 특정 키가 맵에 존재하는지 여부를 확인할 수 있습니다.
5. 맵의 크기 확인: size() 메서드를 사용하여 맵에 저장된 키-값 쌍의 개수를 확인할 수 있습니다.
6. 반복 작업 지원: keySet(), values(), entrySet() 메서드를 사용하여 맵의 키, 값 또는 키-값 쌍을 가져올 수 있습니다. 이를 통해 반복 작업을 수행하거나 맵의 요소를 순회할 수 있습니다.
7. 맵의 구체적인 구현체: Map 인터페이스의 구체적인 구현체로는 HashMap, TreeMap, LinkedHashMap 등이 있습니다. 각 구현체는 내부적으로 다른 데이터 구조를 사용하여 효율적인 검색 및 삽입 작업을 수행합니다.
Map 인터페이스는 다양한 상황에서 유용하게 사용될 수 있으며, 키-값 데이터를 저장하고 관리하는 기능을 제공하여 데이터 구조의 유연성과 편의성을 높여줍니다.
package java.util;
import java.util.function.BiConsumer;
import java.util.function.BiFunction;
import java.util.function.Function;
import java.io.Serializable;
public interface Map<K, V> {
// Query Operations
int size();
boolean isEmpty();
boolean containsKey(Object key);
boolean containsValue(Object value);
V get(Object key);
// Modification Operations
V put(K key, V value);
V remove(Object key);
// Bulk Operations
void putAll(Map<? extends K, ? extends V> m);
void clear();
// Views
Set<K> keySet();
Collection<V> values();
Set<Map.Entry<K, V>> entrySet();
interface Entry<K, V> {
K getKey();
V getValue();
V setValue(V value);
boolean equals(Object o);
int hashCode();
public static <K extends Comparable<? super K>, V> Comparator<Map.Entry<K, V>> comparingByKey() {
return (Comparator<Map.Entry<K, V>> & Serializable)
(c1, c2) -> c1.getKey().compareTo(c2.getKey());
}
public static <K, V extends Comparable<? super V>> Comparator<Map.Entry<K, V>> comparingByValue() {
return (Comparator<Map.Entry<K, V>> & Serializable)
(c1, c2) -> c1.getValue().compareTo(c2.getValue());
}
public static <K, V> Comparator<Map.Entry<K, V>> comparingByKey(Comparator<? super K> cmp) {
Objects.requireNonNull(cmp);
return (Comparator<Map.Entry<K, V>> & Serializable)
(c1, c2) -> cmp.compare(c1.getKey(), c2.getKey());
}
public static <K, V> Comparator<Map.Entry<K, V>> comparingByValue(Comparator<? super V> cmp) {
Objects.requireNonNull(cmp);
return (Comparator<Map.Entry<K, V>> & Serializable)
(c1, c2) -> cmp.compare(c1.getValue(), c2.getValue());
}
@SuppressWarnings("unchecked")
public static <K, V> Map.Entry<K, V> copyOf(Map.Entry<? extends K, ? extends V> e) {
Objects.requireNonNull(e);
if (e instanceof KeyValueHolder) {
return (Map.Entry<K, V>) e;
} else {
return Map.entry(e.getKey(), e.getValue());
}
}
}
// Comparison and hashing
boolean equals(Object o);
int hashCode();
// Defaultable methods
default V getOrDefault(Object key, V defaultValue) {
V v;
return (((v = get(key)) != null) || containsKey(key))
? v
: defaultValue;
}
default void forEach(BiConsumer<? super K, ? super V> action) {
Objects.requireNonNull(action);
for (Map.Entry<K, V> entry : entrySet()) {
K k;
V v;
try {
k = entry.getKey();
v = entry.getValue();
} catch (IllegalStateException ise) {
// this usually means the entry is no longer in the map.
throw new ConcurrentModificationException(ise);
}
action.accept(k, v);
}
}
default void replaceAll(BiFunction<? super K, ? super V, ? extends V> function) {
Objects.requireNonNull(function);
for (Map.Entry<K, V> entry : entrySet()) {
K k;
V v;
try {
k = entry.getKey();
v = entry.getValue();
} catch (IllegalStateException ise) {
// this usually means the entry is no longer in the map.
throw new ConcurrentModificationException(ise);
}
// ise thrown from function is not a cme.
v = function.apply(k, v);
try {
entry.setValue(v);
} catch (IllegalStateException ise) {
// this usually means the entry is no longer in the map.
throw new ConcurrentModificationException(ise);
}
}
}
default V putIfAbsent(K key, V value) {
V v = get(key);
if (v == null) {
v = put(key, value);
}
return v;
}
default boolean remove(Object key, Object value) {
Object curValue = get(key);
if (!Objects.equals(curValue, value) ||
(curValue == null && !containsKey(key))) {
return false;
}
remove(key);
return true;
}
default boolean replace(K key, V oldValue, V newValue) {
Object curValue = get(key);
if (!Objects.equals(curValue, oldValue) ||
(curValue == null && !containsKey(key))) {
return false;
}
put(key, newValue);
return true;
}
default V replace(K key, V value) {
V curValue;
if (((curValue = get(key)) != null) || containsKey(key)) {
curValue = put(key, value);
}
return curValue;
}
default V computeIfAbsent(K key,
Function<? super K, ? extends V> mappingFunction) {
Objects.requireNonNull(mappingFunction);
V v;
if ((v = get(key)) == null) {
V newValue;
if ((newValue = mappingFunction.apply(key)) != null) {
put(key, newValue);
return newValue;
}
}
return v;
}
default V computeIfPresent(K key,
BiFunction<? super K, ? super V, ? extends V> remappingFunction) {
Objects.requireNonNull(remappingFunction);
V oldValue;
if ((oldValue = get(key)) != null) {
V newValue = remappingFunction.apply(key, oldValue);
if (newValue != null) {
put(key, newValue);
return newValue;
} else {
remove(key);
return null;
}
} else {
return null;
}
}
default V compute(K key,
BiFunction<? super K, ? super V, ? extends V> remappingFunction) {
Objects.requireNonNull(remappingFunction);
V oldValue = get(key);
V newValue = remappingFunction.apply(key, oldValue);
if (newValue == null) {
// delete mapping
if (oldValue != null || containsKey(key)) {
// something to remove
remove(key);
return null;
} else {
// nothing to do. Leave things as they were.
return null;
}
} else {
// add or replace old mapping
put(key, newValue);
return newValue;
}
}
default V merge(K key, V value,
BiFunction<? super V, ? super V, ? extends V> remappingFunction) {
Objects.requireNonNull(remappingFunction);
Objects.requireNonNull(value);
V oldValue = get(key);
V newValue = (oldValue == null) ? value :
remappingFunction.apply(oldValue, value);
if (newValue == null) {
remove(key);
} else {
put(key, newValue);
}
return newValue;
}
@SuppressWarnings("unchecked")
static <K, V> Map<K, V> of() {
return (Map<K,V>) ImmutableCollections.EMPTY_MAP;
}
static <K, V> Map<K, V> of(K k1, V v1) {
return new ImmutableCollections.Map1<>(k1, v1);
}
static <K, V> Map<K, V> of(K k1, V v1, K k2, V v2) {
return new ImmutableCollections.MapN<>(k1, v1, k2, v2);
}
static <K, V> Map<K, V> of(K k1, V v1, K k2, V v2, K k3, V v3) {
return new ImmutableCollections.MapN<>(k1, v1, k2, v2, k3, v3);
}
static <K, V> Map<K, V> of(K k1, V v1, K k2, V v2, K k3, V v3, K k4, V v4) {
return new ImmutableCollections.MapN<>(k1, v1, k2, v2, k3, v3, k4, v4);
}
static <K, V> Map<K, V> of(K k1, V v1, K k2, V v2, K k3, V v3, K k4, V v4, K k5, V v5) {
return new ImmutableCollections.MapN<>(k1, v1, k2, v2, k3, v3, k4, v4, k5, v5);
}
static <K, V> Map<K, V> of(K k1, V v1, K k2, V v2, K k3, V v3, K k4, V v4, K k5, V v5,
K k6, V v6) {
return new ImmutableCollections.MapN<>(k1, v1, k2, v2, k3, v3, k4, v4, k5, v5,
k6, v6);
}
static <K, V> Map<K, V> of(K k1, V v1, K k2, V v2, K k3, V v3, K k4, V v4, K k5, V v5,
K k6, V v6, K k7, V v7) {
return new ImmutableCollections.MapN<>(k1, v1, k2, v2, k3, v3, k4, v4, k5, v5,
k6, v6, k7, v7);
}
static <K, V> Map<K, V> of(K k1, V v1, K k2, V v2, K k3, V v3, K k4, V v4, K k5, V v5,
K k6, V v6, K k7, V v7, K k8, V v8) {
return new ImmutableCollections.MapN<>(k1, v1, k2, v2, k3, v3, k4, v4, k5, v5,
k6, v6, k7, v7, k8, v8);
}
static <K, V> Map<K, V> of(K k1, V v1, K k2, V v2, K k3, V v3, K k4, V v4, K k5, V v5,
K k6, V v6, K k7, V v7, K k8, V v8, K k9, V v9) {
return new ImmutableCollections.MapN<>(k1, v1, k2, v2, k3, v3, k4, v4, k5, v5,
k6, v6, k7, v7, k8, v8, k9, v9);
}
static <K, V> Map<K, V> of(K k1, V v1, K k2, V v2, K k3, V v3, K k4, V v4, K k5, V v5,
K k6, V v6, K k7, V v7, K k8, V v8, K k9, V v9, K k10, V v10) {
return new ImmutableCollections.MapN<>(k1, v1, k2, v2, k3, v3, k4, v4, k5, v5,
k6, v6, k7, v7, k8, v8, k9, v9, k10, v10);
}
@SafeVarargs
@SuppressWarnings("varargs")
static <K, V> Map<K, V> ofEntries(Entry<? extends K, ? extends V>... entries) {
if (entries.length == 0) { // implicit null check of entries array
@SuppressWarnings("unchecked")
var map = (Map<K,V>) ImmutableCollections.EMPTY_MAP;
return map;
} else if (entries.length == 1) {
// implicit null check of the array slot
return new ImmutableCollections.Map1<>(entries[0].getKey(),
entries[0].getValue());
} else {
Object[] kva = new Object[entries.length << 1];
int a = 0;
for (Entry<? extends K, ? extends V> entry : entries) {
// implicit null checks of each array slot
kva[a++] = entry.getKey();
kva[a++] = entry.getValue();
}
return new ImmutableCollections.MapN<>(kva);
}
}
static <K, V> Entry<K, V> entry(K k, V v) {
// KeyValueHolder checks for nulls
return new KeyValueHolder<>(k, v);
}
@SuppressWarnings({"rawtypes","unchecked"})
static <K, V> Map<K, V> copyOf(Map<? extends K, ? extends V> map) {
if (map instanceof ImmutableCollections.AbstractImmutableMap) {
return (Map<K,V>)map;
} else {
return (Map<K,V>)Map.ofEntries(map.entrySet().toArray(new Entry[0]));
}
}
}
Views
Map 인터페이스에서 views란 용어는 맵의 부분 집합을 나타내는 개념입니다. 즉, 맵에서 특정한 관점으로 본 결과를 반환하는 메서드를 의미합니다. 이러한 뷰 메서드는 기존 맵에 대한 참조를 반환하며, 맵의 일부를 변경하면 뷰에도 영향을 미칩니다.
Map 인터페이스에서 제공되는 세 가지 주요 뷰는 다음과 같습니다:
1. Key Set View (키 집합 뷰): keySet() 메서드를 사용하여 맵의 모든 키를 포함하는 Set을 반환합니다. 이 Set은 맵의 키 집합에 대한 뷰로, 키 집합의 변경이 맵에 반영됩니다.
2. Value Collection View (값 컬렉션 뷰): values() 메서드를 사용하여 맵의 모든 값들을 포함하는 Collection을 반환합니다. 이 Collection은 맵의 값들에 대한 뷰로, 값 컬렉션의 변경이 맵에 반영됩니다.
3. Entry Set View (키-값 쌍 집합 뷰): entrySet() 메서드를 사용하여 맵의 키-값 쌍들을 포함하는 Set을 반환합니다. 이 Set은 맵의 키-값 쌍 집합에 대한 뷰로, 키-값 쌍 집합의 변경이 맵에 반영됩니다.
이러한 뷰 메서드를 사용하면 맵에 저장된 데이터에 쉽게 접근하고 조작할 수 있습니다. 예를 들어, keySet() 메서드를 사용하여 맵의 모든 키를 가져와서 반복 작업을 수행하거나, entrySet() 메서드를 사용하여 키-값 쌍을 가져와서 특정 조건에 따라 처리할 수 있습니다. 이러한 뷰 메서드를 활용하면 맵을 더 효율적으로 활용할 수 있습니다.
HashMap
Java의 HashMap은 키-값 쌍(엔트리라고도 함) 형태로 요소(객체)를 저장하는 정렬되지 않은 컬렉션입니다.
HashMap은 HashMap<Key, Value> 또는 HashMap<K, V>와 같이 표현되며, 여기서 K는 키를 나타내고 V는 값을 나타냅니다. 키와 값 모두 객체입니다. HashMap은 한 객체를 사용하여 다른 객체를 검색합니다.
만약 키가 제공되면, HashMap에서 해당하는 값은 쉽게 검색할 수 있습니다. 맵 내에서 키는 고유해야 하므로 중복된 데이터를 키로 사용할 수 없습니다.
중복된 키를 가진 엔트리를 삽입하려고 하면, 맵은 기존의 엔트리를 새로운 엔트리로 대체합니다.
Java의 HashMap 클래스는 Map 인터페이스의 네 가지 구현 중 하나입니다. 이는 Java 1.2 버전에 추가되었습니다. java.util.HashMap<K, V> 패키지에서 찾을 수 있습니다. 값의 위치를 찾는 것, 엔트리를 추가하는 것, 엔트리를 삭제하는 것에 효율적입니다.
Hierarchy of HashMap class in Java
Java의 HashMap 클래스는 AbstractMap 클래스를 확장하고 Map 인터페이스를 구현합니다. HashMap의 계층 구조 다이어그램은 아래 그림에서 확인할 수 있습니다.
Features of Java HashMap class
HashMap 클래스의 몇 가지 기능을 기억해야 합니다. 다음과 같습니다:
1. HashMap의 내부 데이터 구조는 HashTable입니다. 간단히 말해, HashMap은 엔트리를 저장하기 위해 내부적으로 해시 테이블을 사용합니다. 이는 배열에 접근하는 것과 거의 동일한 속도로 엔트리를 추가하고 접근할 수 있음을 의미합니다.
2. HashMap에서는 삽입 순서가 유지되지 않습니다 (즉, 순서를 유지하지 않습니다). 즉, HashMap에 삽입된 키와 값들을 동일한 순서로 검색할 수 없습니다.
3. HashMap은 키의 해시 코드를 기반으로 동작하며, 값의 해시 코드에는 의존하지 않습니다.
4. Java HashMap은 중복된 키를 허용하지 않고 고유한 키만을 포함합니다. 값은 중복될 수 있습니다. 값을 키를 기반으로 검색합니다.
5. HashMap에서는 키와 값 모두 다른 유형의 객체를 사용할 수 있습니다.
6. Java HashMap은 중복된 키를 허용하지 않기 때문에 null 키는 하나만 가질 수 있습니다.
7. HashMap에서는 여러 개의 null 값이 허용됩니다.
8. Java의 HashMap은 동기화되지 않았기 때문에, HashMap 객체에 여러 스레드를 사용할 경우 신뢰할 수 없는 결과를 얻을 수 있습니다.
9. Java HashMap은 Cloneable 및 Serializable 인터페이스를 구현하지만 Random Access를 구현하지 않습니다.
10. 검색 작업이 주로 수행되는 경우 HashMap이 가장 적합한 선택입니다.
11. HashMap에 요소가 없는 경우 NoSuchElementException이라는 예외가 발생합니다.
12. Java HashMap은 객체 참조만 저장합니다. 따라서 double이나 int와 같은 기본 데이터 유형을 사용할 수 없습니다. Integer나 Double과 같은 래퍼 클래스를 대신 사용해야 합니다.
HashMap Methods in Java
이 섹션에서는 HashMap 클래스에서 사용 가능한 여러 중요한 메서드를 나열했습니다. 다음과 같습니다:
1. void clear(): 지정된 맵에서 엔트리를 제거하는 데 사용됩니다.
2. boolean isEmpty(): 이 메서드는 맵이 비어 있는지 여부를 확인하는 데 사용됩니다. 해시 맵에 키-값 매핑이 없는 경우 true를 반환하고, 그렇지 않으면 false를 반환합니다.
3. Object clone(): 이 메서드는 이 HashMap의 얕은 복사본을 생성하는 데 사용됩니다. 해시 맵과 엔트리만 복제되며 요소는 복제되지 않습니다. 이 맵과 새 맵은 동일한 키와 값에 대한 참조를 공유합니다.
4. Set entrySet(): 이 메서드는 맵에 있는 모든 키/값 쌍을 포함하는 세트 뷰를 반환하는 데 사용됩니다.
5. Set keySet(): 이 메서드는 이 맵의 키들로 구성된 세트 뷰를 검색하는 데 사용됩니다.
6. V put(Object key, Object value): 이 메서드는 맵에 엔트리를 삽입하는 데 사용됩니다.
7. void putAll(Map map): 이 메서드는 지정된 맵의 모든 엔트리를 다른 맵에 삽입하는 데 사용됩니다.
8. V putIfAbsent(K key, V value): 이 메서드는 지정된 키와 값이 이미 지정되지 않은 경우에만 맵에 해당 값을 추가합니다.
9. V remove(Object key): 이 메서드는 지정된 키에 대한 엔트리를 삭제하는 데 사용됩니다.
10. boolean remove(Object key, Object value): 이 메서드는 맵에서 지정된 키에 대해 특정 값을 제거합니다.
11. int size(): 이 메서드는 맵에 있는 엔트리의 수를 반환합니다.
12. Collection values(): 이 메서드는 맵에 있는 모든 값들을 포함하는 컬렉션 뷰를 반환합니다.
13. V get(Object key): 이 메서드는 지정된 키와 연결된 값을 검색하는 데 사용됩니다. 반환 형식은 Object입니다.
14. V replace(K key, V value): 이 메서드는 지정된 키에 대해 지정된 값을 바꾸는 데 사용됩니다.
15. boolean replace(K key, V oldValue, V newValue): 이 메서드는 지정된 키에 대해 이전 값을 새 값으로 바꾸는 데 사용됩니다.
16. boolean containsValue(Object v): 이 메서드는 맵이 특정 값을 포함하는지 여부를 확인하는 데 사용됩니다. v와 같은 값을 포함하면 true를 반환합니다.
17. boolean containsKey(Object k): 이 메서드는 맵이 특정 키를 포함하는지 여부를 확인하는 데 사용됩니다. 이 맵에 키 k와 같은 키가 있으면 true를 반환합니다.
18. boolean equals(Object o): 이 메서드는 지정된 개체와 맵을 비교하는 데 사용됩니다.
HashMap을 사용하는 다양한 코드
1. HashMap 생성 및 요소 추가:
import java.util.HashMap;
public class HashMapExample {
public static void main(String[] args) {
// HashMap 생성
HashMap<String, Integer> hashMap = new HashMap<>();
// 요소 추가
hashMap.put("사과", 1500);
hashMap.put("바나나", 2000);
hashMap.put("오렌지", 1000);
// HashMap 출력
System.out.println(hashMap);
}
}
2. HashMap에서 요소 가져오기:
import java.util.HashMap;
public class HashMapExample {
public static void main(String[] args) {
// HashMap 생성 및 요소 추가
HashMap<String, Integer> hashMap = new HashMap<>();
hashMap.put("사과", 1500);
hashMap.put("바나나", 2000);
hashMap.put("오렌지", 1000);
// 요소 가져오기
int price = hashMap.get("사과");
System.out.println("사과의 가격: " + price);
}
}
3. HashMap 반복문으로 순회하기:
import java.util.HashMap;
import java.util.Map;
public class HashMapExample {
public static void main(String[] args) {
// HashMap 생성 및 요소 추가
HashMap<String, Integer> hashMap = new HashMap<>();
hashMap.put("사과", 1500);
hashMap.put("바나나", 2000);
hashMap.put("오렌지", 1000);
// HashMap 반복문으로 순회
for (Map.Entry<String, Integer> entry : hashMap.entrySet()) {
String key = entry.getKey();
int value = entry.getValue();
System.out.println(key + "의 가격: " + value);
}
}
}
4. HashMap에서 요소 삭제하기:
import java.util.HashMap;
public class HashMapExample {
public static void main(String[] args) {
// HashMap 생성 및 요소 추가
HashMap<String, Integer> hashMap = new HashMap<>();
hashMap.put("사과", 1500);
hashMap.put("바나나", 2000);
hashMap.put("오렌지", 1000);
// 요소 삭제
hashMap.remove("바나나");
// HashMap 출력
System.out.println(hashMap);
}
}
5. HashMap의 크기 확인하기:
import java.util.HashMap;
public class HashMapExample {
public static void main(String[] args) {
// HashMap 생성 및 요소 추가
HashMap<String, Integer> hashMap = new HashMap<>();
hashMap.put("사과", 1500);
hashMap.put("바나나", 2000);
hashMap.put("오렌지", 1000);
// 크기 확인
int size = hashMap.size();
System.out.println("HashMap의 크기: " + size);
}
}
6. HashMap에 중복된 키로 요소 추가하기:
import java.util.HashMap;
public class HashMapExample {
public static void main(String[] args) {
// HashMap 생성 및 요소 추가
HashMap<String, Integer> hashMap = new HashMap<>();
hashMap.put("사과", 1500);
hashMap.put("바나나", 2000);
hashMap.put("오렌지", 1000);
// 중복된 키로 요소 추가
hashMap.put("사과", 1200);
// HashMap 출력
System.out.println(hashMap);
}
}
7. HashMap의 특정 키가 존재하는지 확인하기:
import java.util.HashMap;
public class HashMapExample {
public static void main(String[] args) {
// HashMap 생성 및 요소 추가
HashMap<String, Integer> hashMap = new HashMap<>();
hashMap.put("사과", 1500);
hashMap.put("바나나", 2000);
hashMap.put("오렌지", 1000);
// 특정 키가 존재하는지 확인
boolean containsKey = hashMap.containsKey("사과");
System.out.println("사과가 HashMap에 존재하는가? " + containsKey);
}
}
8. HashMap의 모든 값 가져오기:
import java.util.HashMap;
import java.util.Collection;
public class HashMapExample {
public static void main(String[] args) {
// HashMap 생성 및 요소 추가
HashMap<String, Integer> hashMap = new HashMap<>();
hashMap.put("사과", 1500);
hashMap.put("바나나", 2000);
hashMap.put("오렌지", 1000);
// 모든 값 가져오기
Collection<Integer> values = hashMap.values();
System.out.println("HashMap의 값들: " + values);
}
}
9. HashMap의 모든 키 가져오기:
import java.util.HashMap;
import java.util.Set;
public class HashMapExample {
public static void main(String[] args) {
// HashMap 생성 및 요소 추가
HashMap<String, Integer> hashMap = new HashMap<>();
hashMap.put("사과", 1500);
hashMap.put("바나나", 2000);
hashMap.put("오렌지", 1000);
// 모든 키 가져오기
Set<String> keys = hashMap.keySet();
System.out.println("HashMap의 키들: " + keys);
}
}
10. HashMap의 모든 요소 제거하기:
import java.util.HashMap;
public class HashMapExample {
public static void main(String[] args) {
// HashMap 생성 및 요소 추가
HashMap<String, Integer> hashMap = new HashMap<>();
hashMap.put("사과", 1500);
hashMap.put("바나나", 2000);
hashMap.put("오렌지", 1000);
// 모든 요소 제거
hashMap.clear();
// HashMap 출력
System.out.println(hashMap);
}
}
11. HashMap에서 특정 값이 포함된 요소 찾기:
import java.util.HashMap;
import java.util.Map;
public class HashMapExample {
public static void main(String[] args) {
// HashMap 생성 및 요소 추가
HashMap<String, Integer> hashMap = new HashMap<>();
hashMap.put("사과", 1500);
hashMap.put("바나나", 2000);
hashMap.put("오렌지", 1000);
// 특정 값이 포함된 요소 찾기
int searchValue = 1500;
for (Map.Entry<String, Integer> entry : hashMap.entrySet()) {
if (entry.getValue() == searchValue) {
System.out.println("찾은 요소: " + entry.getKey());
break;
}
}
}
}
12. HashMap에서 기본값 설정하여 값 가져오기:
import java.util.HashMap;
public class HashMapExample {
public static void main(String[] args) {
// HashMap 생성 및 요소 추가
HashMap<String, Integer> hashMap = new HashMap<>();
hashMap.put("사과", 1500);
hashMap.put("바나나", 2000);
// 기본값 설정하여 값 가져오기
int defaultValue = 0;
int price = hashMap.getOrDefault("오렌지", defaultValue);
System.out.println("오렌지의 가격: " + price);
}
}
13. HashMap의 요소를 다른 HashMap으로 복사하기:
import java.util.HashMap;
public class HashMapExample {
public static void main(String[] args) {
// 첫 번째 HashMap 생성 및 요소 추가
HashMap<String, Integer> hashMap1 = new HashMap<>();
hashMap1.put("사과", 1500);
hashMap1.put("바나나", 2000);
hashMap1.put("오렌지", 1000);
// 두 번째 HashMap 생성 및 첫 번째 HashMap의 요소 복사
HashMap<String, Integer> hashMap2 = new HashMap<>(hashMap1);
// 두 번째 HashMap 출력
System.out.println("복사된 HashMap: " + hashMap2);
}
}
14. HashMap에서 특정 키에 대한 값을 업데이트하기:
import java.util.HashMap;
public class HashMapExample {
public static void main(String[] args) {
// HashMap 생성 및 요소 추가
HashMap<String, Integer> hashMap = new HashMap<>();
hashMap.put("사과", 1500);
hashMap.put("바나나", 2000);
hashMap.put("오렌지", 1000);
// 특정 키에 대한 값 업데이트
hashMap.put("사과", 1200);
// HashMap 출력
System.out.println(hashMap);
}
}
15. HashMap의 요소를 배열로 변환하기:
import java.util.HashMap;
import java.util.Map;
public class HashMapExample {
public static void main(String[] args) {
// HashMap 생성 및 요소 추가
HashMap<String, Integer> hashMap = new HashMap<>();
hashMap.put("사과", 1500);
hashMap.put("바나나", 2000);
hashMap.put("오렌지", 1000);
// HashMap의 요소를 배열로 변환
Map.Entry<String, Integer>[] entries = hashMap.entrySet().toArray(new Map.Entry[0]);
// 배열 출력
for (Map.Entry<String, Integer> entry : entries) {
System.out.println(entry.getKey() + ": " + entry.getValue());
}
}
}
이 코드들은 HashMap을 사용하여 요소 복사, 값 업데이트, 요소를 배열로 변환하는 방법을 보여줍니다.
'Java' 카테고리의 다른 글
Java Collection Framework-1 (0) | 2024.04.09 |
---|---|
Functional Interface (0) | 2024.04.09 |
자바 예외 처리 (0) | 2024.04.09 |
Callable & ExecutorService (0) | 2024.04.09 |
Generics 4 (0) | 2024.04.09 |