您的位置:

Java HashMap详解

HashMap是Java中一个经典的数据结构,它实现了Map接口,提供了一种键值对的映射关系。因为它的高效性和易用性,HashMap在Java编程中被广泛使用。

一、HashMap的基本概念

HashMap是一个散列表,它存储的是键值对(key-value)映射的数据。

在HashMap中,每个键(key)都是唯一的,对应着一个值(value)。你可以通过键来访问对应的值,类似于字典。在Java中,键和值都可以是任何对象。

二、HashMap的构造方法

HashMap类有多个构造方法,其中比较常用的是以下两个:

/**
 * 创建一个空的HashMap
 */
HashMap()

/**
 * 创建一个具有指定初始容量和默认负载因子(0.75)的HashMap
 *
 * @param initialCapacity 初始容量
 */
HashMap(int initialCapacity)

第一个构造方法用来创建一个空的HashMap。第二个构造方法用来创建一个具有指定初始容量(initialCapacity)的HashMap。初始容量指的是HashMap能够容纳键值对的数量,当HashMap中存储的键值对数量超过了这个值,HashMap会自动进行扩容。

三、HashMap的常用方法

HashMap提供了许多常用的方法,下面介绍一些比较重要的方法:

1、put()

put()方法用来将键值对插入到HashMap中。如果给定的键已经存在了,则它对应的值会被更新为新的值。

/**
 * 将指定的键和值插入到HashMap中,如果键已经存在,则更新对应的值
 * @param key 键
 * @param value 值
 * @return 如果之前存在对应的值,则返回旧的值,否则返回null
 */
V put(K key, V value)

2、get()

get()方法用来根据键来获取对应的值。

/**
 * 根据键来获取对应的值
 * @param key 键
 * @return 值
 */
V get(Object key)

下面是一个简单的示例:

HashMap<String, Integer> map = new HashMap<>();
map.put("apple", 1);
map.put("banana", 2);
map.put("orange", 3);
System.out.println(map.get("apple"));
System.out.println(map.get("banana"));
System.out.println(map.get("orange"));

输出结果:

1
2
3

3、containsKey()

containsKey()方法用来检查HashMap中是否包含指定的键。

/**
 * 检查HashMap中是否包含指定的键
 * @param key 键
 * @return 如果包含指定的键,则返回true,否则返回false
 */
boolean containsKey(Object key)

4、containsValue()

containsValue()方法用来检查HashMap中是否包含指定的值。

/**
 * 检查HashMap中是否包含指定的值
 * @param value 值
 * @return 如果包含指定的值,则返回true,否则返回false
 */
boolean containsValue(Object value)

5、remove()

remove()方法用来根据指定的键来删除对应的键值对。

/**
 * 根据指定的键来删除对应的键值对
 * @param key 键
 * @return 如果存在对应的值,则返回被删除的值,否则返回null
 */
V remove(Object key)

6、keySet()

keySet()方法用来获取HashMap中所有键的集合。

/**
 * 获取HashMap中所有键的集合
 * @return 键的集合
 */
Set<K> keySet()

下面是一个简单的示例:

HashMap<String, Integer> map = new HashMap<>();
map.put("apple", 1);
map.put("banana", 2);
map.put("orange", 3);
Set<String> keys = map.keySet();
for (String key : keys) {
    System.out.println(key + " -> " + map.get(key));
}

输出结果:

banana -> 2
orange -> 3
apple -> 1

四、HashMap的扩容机制

HashMap的扩容机制非常重要。当HashMap中的键值对数量达到一定程度时,HashMap会自动进行扩容,这是为了保证HashMap的高效性。HashMap的扩容机制遵循以下两个原则:

1、容量(capacity)总是2的幂次方

HashMap的容量总是2的幂次方,这是为了方便计算哈希值。当你向HashMap中插入一个键值对时,HashMap会根据键的哈希值来计算该键值对应该放在哪个位置。如果HashMap的容量不是2的幂次方,那么计算哈希值的时候需要进行额外的处理,这会降低HashMap的效率。

2、负载因子(load factor)默认为0.75

负载因子决定了HashMap何时需要进行扩容。负载因子默认为0.75,这意味着当HashMap中存储的键值对数量达到总容量的75%时,HashMap就会进行扩容。这样做的好处是可以保证HashMap的效率,即当HashMap中存储的键值对数量不多时,HashMap的容量也不会过大,从而提高HashMap的检索效率。

下面是一个HashMap扩容的示例:

HashMap<Integer, Integer> map = new HashMap<>();
System.out.println(map.size());
for (int i = 0; i < 30; i++) {
    map.put(i, i);
    System.out.println("put:" + i);
}
System.out.println(map.size());

输出结果:

0
put:0
put:1
put:2
put:3
put:4
put:5
put:6
put:7
put:8
put:9
put:10
put:11
put:12
put:13
put:14
put:15
put:16
put:17
put:18
put:19
put:20
put:21
put:22
put:23
put:24
put:25
put:26
put:27
put:28
put:29
32

从上面的代码可以看出,初始容量为16的HashMap存储了30个键值对后,容量自动扩容为32。

五、HashMap和线程安全性

HashMap并不是线程安全的。如果多个线程同时读写一个HashMap,有可能会出现数据不一致的情况。如果需要在多线程环境中使用HashMap,可以考虑使用ConcurrentHashMap。

下面是一个对HashMap进行读写操作的示例:

HashMap<Integer, Integer> map = new HashMap<>();
ExecutorService executorService = Executors.newCachedThreadPool();
for (int i = 0; i < 1000; i++) {
    executorService.submit(() -> {
        for (int j = 0; j < 1000; j++) {
            map.put(j, j);
            System.out.println(map.get(j));
        }
    });
}
executorService.shutdown();

输出结果中可能会存在重复的数字,这是因为多个线程同时进行读写操作导致的。

下面是对ConcurrentHashMap进行读写操作的示例:

ConcurrentHashMap<Integer, Integer> map = new ConcurrentHashMap<>();
ExecutorService executorService = Executors.newCachedThreadPool();
for (int i = 0; i < 1000; i++) {
    executorService.submit(() -> {
        for (int j = 0; j < 1000; j++) {
            map.put(j, j);
            System.out.println(map.get(j));
        }
    });
}
executorService.shutdown();

输出结果中不会存在重复的数字,因为ConcurrentHashMap是线程安全的。

六、HashMap的性能分析

HashMap的性能非常优秀,它的常见操作的时间复杂度为O(1)。但是在实际使用中,我们需要注意其扩容机制可能会带来的性能影响。

下面是一个对HashMap进行性能测试的示例:

long start, end;
HashMap<Integer, Integer> map = new HashMap<>();
Random rand = new Random();
start = System.nanoTime();
for (int i = 0; i < 100000; i++) {
    map.put(i, rand.nextInt());
}
end = System.nanoTime();
System.out.println("插入100000个键值对,耗时:" + (end - start) + "ns");

start = System.nanoTime();
for (int i = 0; i < 100000; i++) {
    map.get(rand.nextInt(100000));
}
end = System.nanoTime();
System.out.println("随机访问100000个键值对,耗时:" + (end - start) + "ns");

在我的电脑上,上面的代码的输出结果大概如下:

插入100000个键值对,耗时:10503887ns
随机访问100000个键值对,耗时:68013ns

从上面的代码可以看出,HashMap插入100000个键值对的时间大约为10503887纳秒,随机访问100000个键值对的时间大约为68013纳秒。

七、总结

本文从HashMap的基本概念、构造方法、常用方法、扩容机制、线程安全性和性能分析等多个方面对HashMap进行了详细的介绍。HashMap是Java中一个经典的数据结构,它的高效性和易用性使得它在Java编程中被广泛使用。在使用HashMap时,我们需要注意其扩容机制可能会带来的性能影响,并且需要考虑线程安全性问题。