HashMap은 자바에서 가장 자주 사용되는 데이터 구조 중 하나이며, 其의 효율성이 知られています. HashMap에서 데이터는 键-值 쌍的形式으로 보관되며, 각 键(key)는 유일한 식별자로 구성되며 해시 함수를 통해 인덱스를 생성합니다. 이러한 키-값 쌍의 저장 방식은 快速하고 관리하기 容易한 데이터 strucutre를 제공합니다.

이 글에서는 자바의 HashMap에 대해 자세히 绍介해드릴 것입니다. 일반적인 HashMap 操作을 예시로 보고, 내부적으로 어떻게 동작하는지 精通해드릴 것입니다. 해시 함수와 인덱스 계산에 대해 이해하고, operaion의 시간 복잡도를 보며 동시 환경에서의 行为을 巡의 것입니다.

Java의 HashMap은何でしょう?

HashMap은 자바 コレク션 フレームワークの一部である Map 인터페이스를 実装しています. 其は Hashing 이라는 개념에 기반을 두고 있습니다.

Hashing는 任意의 입력 크기를 해시 함수를 통해 고정 사이즈의 출력으로 변환하는 기술입니다. 생성된 출력은 해시 코드라고 불립니다. 이러한 해시 코드는 자바에서 整数 값으로 表현됩니다. 해시 코드는 HashMap 내에서 efient 로 찾기와 보관 operaion을 위한 用途로 사용됩니다.

일반적인 Operaion

위에서 述べた 것 처럼, HashMap에서 데이터는 键-值 쌍的形式으로 보관되며, 键(key)는 유일한 식별자이며, 각 key는 값과 관련联立됩니다.

下記은 HashMap에서 지원하는 일반적인 操作의 一些를 보여드릴 것입니다. 이러한 method에 대해 간단한 예시 코드로 이해하도록 하겠습니다:

삽입

  • 이 method는 HashMap에 새로운 键-值 쌍을 삽입합니다.

  • 키-값 쌍의 삽입 순서는 보장되지 않습니다.

  • 삽입 과정에서, 기존에 키가 존재하면, 전달된 새 값으로 기존 값이 替わります.

  • HashMap에 하나의 null 키만 삽입할 수 있으며, 하나의 null 값만 갖추는 것은 아니다.

이 operatioin의 方法的 표현은 아래에 주어져 있으며, 예를 ollows:

public V put(K key, V value)
Map<String, Integer> map = new HashMap<>();
map.put("apple", 1);
map.put("banana", 2);

위의 코드에서는, String 형의 키와 Integer 형의 값을 갖추는 HashMap 예시가 있습니다.

조회:

  • 주어진 키와 관련된 값을 가져옵니다.

  • 키가 HashMap에 없으면 null를 리턴합니다.

이 operatioin의 方法的 표현은 아래에 주어져 있으며, 예를 ollows:

public V get(Object key)
Integer value = map.get("apple"); // 1을 리턴합니다.

위의 코드에서는, apple 키와 관련된 값을 가져옵니다.

기본적인 操作用户 包括:

  • remove: 지정한 키와 일치하는 键值对을 제거한다. 키를 찾지 못하면 null을 반환한다.

  • containsKey: HashMap에 지정한 키가 存在하는지 여부를 확인한다.

  • containsValue: HashMap에 지정한 값이 存在하는지 여부를 확인한다.

A HashMap의 내부 구조

HashMap은 布斯克ets 또는 빵의 형태의 배열을 사용한다. 每个布斯克et은 Node 형태의 링크드 리스트를 사용하여 HashMap의 键值对을 표현한다.

static class Node<K, V> {
    final int hash;
    final K key;
    V value;
    Node<K, V> next;

    Node(int hash, K key, V value, Node<K, V> next) {
        this.hash = hash;
        this.key = key;
        this.value = value;
        this.next = next;
    }
}

上图에서 Node クラ스의 구조를 볼 수 있으며, HashMap의 键值对을 저장하는 데 사용되는 것을 알 수 있다.

Node 클래스는 다음과 같은 필드를 가지고 있다:

  • hash: 键의 hashCode를 가리킨다.

  • key: 键值对의 键를 가리킨다.

  • value: 键와 관련된 값을 가리킨다.

  • next: 다음 노드를 참조하는 역할을 합니다.

HashMap는 기본적으로 해시 테이블 구현을 기반으로 하며, 그 성능은 두 가지 주요 매개 변수: 초기 용량과 로드 팩터에 의존합니다. 해시 테이블 클래스의 원본 자바 문서는 이 두 매개 변수를 다음과 같이 정의합니다:

  • 용량은 해시 테이블의 버킷 수입니다. 초기 용량은 단순히 해시 테이블이 생성된 시점의 용량입니다.

  • 로드 팩터는 해시 테이블이 자동으로 증가하기 전에 얼마나 가득 차게 되어야 하는지를 측정하는 지표입니다.

이제 HashMap에서 기본적인 연산인 putget이 어떻게 동작하는지 이해해 봅시다.

해시 함수

키-값 쌍을 삽입(put)할 때 HashMap는 먼저 키의 해시 코드를 계산합니다. 해시 함수는 키에 대한 정수를 계산합니다. 클래스는 Object 클래스의 hashCode 메서드를 사용하거나 이 메서드를 재정의하여 자신의 구현을 제공할 수 있습니다. (해시 코드 계약에 대해서는 여기를 읽어보세요). 그 다음 해시 코드는 그 위 16비트와 XOR 연산을 통해 (h >>> 16) 더 균일하게 분포되는 것을 달성합니다.

XOR는 두 비트를 비교하는 비트 단위 연산입니다. 비트가 다르면 1이 되고 같으면 0이 됩니다. 이 문맥에서, 해시 코드와 그 위 16비트 사이에 비트 단위 XOR 연산을 수행하면 비트를 섞어 더 균일하게 분포되는 해시 코드를 얻을 수 있습니다.

static final int hash(Object key) {
    int h;
    return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}

위에서 HashMap 클래스의 정적 해시 메서드를 볼 수 있습니다.

이상의 내용은 각 키에 대해 고유한 해시 코드를 가지고 있지만, 해시 함수는 서로 다른 키에 대해 같은 해시 코드를 생성할 수 있습니다. 이는 충돌이라는 상황을 유발합니다. 다음 섹션에서 충돌 처리 방법을 알아봅니다.

인덱스 계산

키의 해시 코드가 생성되면 HashMap는 버킷 배열 내의 인덱스를 계산하여 키-값 쌍이 저장될 위치를 결정합니다. 이는 배열 길이가 2의 지수일 때 모듈러 연산을 계산하는 효율적인 방법인 비트 단위 AND 연산을 사용하여 수행됩니다.

int index = (n - 1) & hash;

여기서는 버킷 배열의 길이를 n이라고 가정하여 인덱스를 계산합니다.

인덱스를 계산한 후, 키는 해시 함수를 통해 같은 인덱스에 버킷 배열에 저장됩니다. 그러나, 여러 키가 같은 인덱스를 얻게 되면 충돌이 발생합니다. 이러한 상황에서 HashMap은 두 가지 중 하나의 방법을 사용합니다.:

  • Chaining/Linking: 배열 中的 각 버킷은 노드의 linked list입니다. 특정 인덱스에 이미 키가 존재하고 다른 키가 同样한 인덱스로 해시되면, 그 목록에 추가됩니다.

  • Treeify: 노드의 수가 特定の 阙值를 초과하면, linked list가 树(이것은 Java 8에서 introduced되었습니다)로 변환됩니다.

static final int TREEIFY_THRESHOLD = 8;

이것은 treeification의 阙값입니다.

따라서, 좋은 해시 함수를 가지는 것이 중요합니다. 이를 통해 키들이 buckets를 균등하게 분산되고 충돌 가능성을 최소화합니다.

인sertion (put) operaion과 유사하게, retrieval (get) 과 deletion (remove) 操作이 동작합니다. 이렇게 동작하는 방법은 다음과 같습니다:

  • Retrieval (get): hash function을 사용하여 hash code를 계산한 다음 -> hash code를 이용하여 인덱스를 계산한 다음 -> linked list 또는 tree를 遍历하여 相符한 key를 가진 노드를 찾습니다.

  • 삭제 (remove): 해시 함수를 사용하여 해시 코드를 계산 -> 해시 코드를 사용하여 인덱스를 계산 -> 연결 리스트나 트리에서 노드를 제거.

시간 복잡도

HashMap의 기본 연산들은 put, get, remove 등은 일반적으로 키가 균일하게 분포되었다고 가정하면 상수 시간 성능 O(1)을 제공합니다. 키 분포가 나쁘고 충돌이 많이 발생하는 경우에는 이러한 연산은 선형 시간 복잡도 O(n)로 감소할 수 있습니다.

트리화를 통해 긴 충돌 체인이 균형있는 트리로 변환되면, 조회 연산은 더 효율적인 로그 시간 복잡도 O(log n)로 개선될 수 있습니다.

동기화

HashMap 구현은 동기화되지 않습니다. 여러 스레드가 동시에 HashMap 인스턴스에 액세스하고 맵을 이터레이트하고, 그 중 하나의 스레드가 구조적 수정(키-값 매핑을 추가하거나 제거하는 것)을 수행하면 ConcurrentModificationException이 발생합니다.

이를 방지하기 위해 Collections.synchronizedMap 메서드를 사용하여 스레드 안전한 인스턴스를 생성할 수 있습니다.

결론

요약하자면, 개발자가 정보를 토대로 결정을 내릴 수 있도록 HashMap의 내부 동작 원리를 이해하는 것이 중요합니다. 키가 매핑되는 방법, 충돌이 어떻게 발생하고 어떻게 피할 수 있는지 알고 있다면, HashMap을 효율적으로 사용할 수 있습니다.