MK
摩柯社区 - 一个极简的技术知识社区
AI 面试

深入了解Java Map接口的常用方法及使用技巧

2021-05-243.7k 阅读

Java Map接口概述

在Java编程中,Map接口是Java集合框架的重要组成部分,它用于存储键值对(key - value pairs),提供了一种将一个值与一个键关联的方式。通过键可以高效地检索到对应的值。Map接口的实现类有很多,比如HashMapTreeMapLinkedHashMap等,它们各自有不同的特点和适用场景,但都遵循Map接口定义的基本行为。

Map接口与其他集合接口(如ListSet)的主要区别在于其存储元素的方式。List是有序的元素序列,允许重复元素;Set是无序的不重复元素集合;而Map则是通过键值对来存储和访问数据,键在Map中是唯一的,不允许重复。

常用方法介绍

1. put(K key, V value)

put(K key, V value)方法用于将指定的键值对插入到Map中。如果Map中之前不存在该键,那么就会添加这个新的键值对;如果Map中已经存在该键,那么会用新的值替换旧的值,并返回旧值。如果之前不存在该键,则返回null

下面是一个简单的HashMap示例:

import java.util.HashMap;
import java.util.Map;

public class MapPutExample {
    public static void main(String[] args) {
        Map<String, Integer> map = new HashMap<>();
        // 添加键值对
        Integer oldValue = map.put("one", 1);
        System.out.println("插入 'one' 时返回的旧值: " + oldValue);

        oldValue = map.put("one", 11);
        System.out.println("再次插入 'one' 时返回的旧值: " + oldValue);
    }
}

在上述代码中,首先向HashMap中插入键"one"和值1,由于此时Map中不存在该键,put方法返回null。然后再次插入键"one",但值为11,此时put方法返回旧值1,因为Map中已经存在键"one",旧值被新值11替换。

2. get(Object key)

get(Object key)方法用于根据指定的键获取对应的值。如果Map中存在该键,则返回与之关联的值;如果不存在,则返回null

以下是使用get方法的示例:

import java.util.HashMap;
import java.util.Map;

public class MapGetExample {
    public static void main(String[] args) {
        Map<String, Integer> map = new HashMap<>();
        map.put("one", 1);
        map.put("two", 2);

        Integer value = map.get("one");
        System.out.println("键 'one' 对应的值: " + value);

        value = map.get("three");
        System.out.println("键 'three' 对应的值: " + value);
    }
}

在这个示例中,通过get方法获取键"one"对应的值,返回1。而获取不存在的键"three"对应的值时,返回null

3. containsKey(Object key)

containsKey(Object key)方法用于检查Map中是否包含指定的键。如果包含,则返回true;否则返回false

示例代码如下:

import java.util.HashMap;
import java.util.Map;

public class MapContainsKeyExample {
    public static void main(String[] args) {
        Map<String, Integer> map = new HashMap<>();
        map.put("one", 1);

        boolean contains = map.containsKey("one");
        System.out.println("是否包含键 'one': " + contains);

        contains = map.containsKey("two");
        System.out.println("是否包含键 'two': " + contains);
    }
}

此代码中,首先检查Map是否包含键"one",返回true;然后检查是否包含键"two",返回false

4. containsValue(Object value)

containsValue(Object value)方法用于检查Map中是否包含指定的值。如果包含,则返回true;否则返回false。需要注意的是,由于值可能会重复,所以这个方法的查找效率相对较低,尤其是在大型Map中。

以下是示例:

import java.util.HashMap;
import java.util.Map;

public class MapContainsValueExample {
    public static void main(String[] args) {
        Map<String, Integer> map = new HashMap<>();
        map.put("one", 1);
        map.put("two", 2);

        boolean contains = map.containsValue(2);
        System.out.println("是否包含值 2: " + contains);

        contains = map.containsValue(3);
        System.out.println("是否包含值 3: " + contains);
    }
}

在这个例子中,检查Map是否包含值2,返回true;检查是否包含值3,返回false

5. remove(Object key)

remove(Object key)方法用于从Map中移除指定键对应的键值对。如果Map中存在该键,则移除并返回与之关联的值;如果不存在,则返回null

示例代码如下:

import java.util.HashMap;
import java.util.Map;

public class MapRemoveExample {
    public static void main(String[] args) {
        Map<String, Integer> map = new HashMap<>();
        map.put("one", 1);

        Integer removedValue = map.remove("one");
        System.out.println("移除的键 'one' 对应的值: " + removedValue);

        removedValue = map.remove("one");
        System.out.println("再次移除键 'one' 对应的值: " + removedValue);
    }
}

在上述代码中,第一次调用remove方法移除键"one",返回值1。第二次调用时,由于键"one"已被移除,返回null

6. size()

size()方法用于返回Map中键值对的数量。

示例如下:

import java.util.HashMap;
import java.util.Map;

public class MapSizeExample {
    public static void main(String[] args) {
        Map<String, Integer> map = new HashMap<>();
        map.put("one", 1);
        map.put("two", 2);

        int size = map.size();
        System.out.println("Map的大小: " + size);
    }
}

这个示例中,size方法返回Map中键值对的数量,这里是2

7. isEmpty()

isEmpty()方法用于判断Map是否为空。如果Map中没有任何键值对,则返回true;否则返回false

示例代码如下:

import java.util.HashMap;
import java.util.Map;

public class MapIsEmptyExample {
    public static void main(String[] args) {
        Map<String, Integer> map = new HashMap<>();
        boolean isEmpty = map.isEmpty();
        System.out.println("Map是否为空: " + isEmpty);

        map.put("one", 1);
        isEmpty = map.isEmpty();
        System.out.println("插入键值对后Map是否为空: " + isEmpty);
    }
}

在代码开始时,Map为空,isEmpty方法返回true。插入一个键值对后,isEmpty方法返回false

8. keySet()

keySet()方法返回一个包含Map中所有键的Set集合。通过这个Set集合,可以遍历Map中的所有键。

示例如下:

import java.util.HashMap;
import java.util.Map;
import java.util.Set;

public class MapKeySetExample {
    public static void main(String[] args) {
        Map<String, Integer> map = new HashMap<>();
        map.put("one", 1);
        map.put("two", 2);

        Set<String> keySet = map.keySet();
        for (String key : keySet) {
            System.out.println("键: " + key);
        }
    }
}

在上述代码中,通过keySet方法获取Map中所有的键,并使用增强型for循环遍历输出所有键。

9. values()

values()方法返回一个包含Map中所有值的Collection集合。通过这个集合,可以遍历Map中的所有值。

示例代码如下:

import java.util.HashMap;
import java.util.Map;
import java.util.Collection;

public class MapValuesExample {
    public static void main(String[] args) {
        Map<String, Integer> map = new HashMap<>();
        map.put("one", 1);
        map.put("two", 2);

        Collection<Integer> values = map.values();
        for (Integer value : values) {
            System.out.println("值: " + value);
        }
    }
}

此代码通过values方法获取Map中所有的值,并使用增强型for循环遍历输出所有值。

10. entrySet()

entrySet()方法返回一个包含Map中所有键值对的Set集合,集合中的每个元素都是一个Map.Entry对象。Map.Entry对象包含了键和值,可以通过它同时获取键和值。

示例如下:

import java.util.HashMap;
import java.util.Map;
import java.util.Set;

public class MapEntrySetExample {
    public static void main(String[] args) {
        Map<String, Integer> map = new HashMap<>();
        map.put("one", 1);
        map.put("two", 2);

        Set<Map.Entry<String, Integer>> entrySet = map.entrySet();
        for (Map.Entry<String, Integer> entry : entrySet) {
            System.out.println("键: " + entry.getKey() + ", 值: " + entry.getValue());
        }
    }
}

在这个示例中,通过entrySet方法获取Map中所有的键值对,并使用增强型for循环遍历输出每个键值对的键和值。

使用技巧

1. 选择合适的Map实现类

  • HashMapHashMap是最常用的Map实现类,它基于哈希表实现,具有很高的插入、查询和删除效率。在大多数情况下,如果对键的顺序没有要求,HashMap是一个很好的选择。例如,在缓存系统中,使用HashMap可以快速地根据键获取缓存的值。

  • TreeMapTreeMap基于红黑树实现,它会对键进行排序。如果需要按照键的自然顺序(如数字从小到大、字符串按字典序)或者自定义顺序来遍历Map,那么TreeMap是合适的选择。比如,在统计单词出现频率并按单词字母顺序输出时,TreeMap就很有用。

  • LinkedHashMapLinkedHashMap继承自HashMap,它不仅具有HashMap的高性能,还能维护插入顺序或访问顺序。如果需要按照插入顺序遍历Map,或者实现一个简单的LRU(最近最少使用)缓存,LinkedHashMap是个不错的选择。

2. 处理空值

在使用Map时,需要注意空值的处理。HashMap允许键和值为null,但TreeMap不允许键为null(值可以为null)。在使用get方法获取值时,如果键不存在会返回null,这可能会导致空指针异常。为了避免这种情况,可以在获取值之前先使用containsKey方法检查键是否存在,或者使用Java 8引入的Optional类来处理可能为null的值。

例如,使用Optional类处理Map中的值:

import java.util.HashMap;
import java.util.Map;
import java.util.Optional;

public class MapOptionalExample {
    public static void main(String[] args) {
        Map<String, Integer> map = new HashMap<>();
        map.put("one", 1);

        Optional<Integer> valueOptional = Optional.ofNullable(map.get("two"));
        valueOptional.ifPresentOrElse(
                value -> System.out.println("值: " + value),
                () -> System.out.println("键 'two' 不存在")
        );
    }
}

在上述代码中,通过Optional.ofNullable方法将map.get("two")的结果包装成Optional对象,然后使用ifPresentOrElse方法来处理可能为null的情况。

3. 遍历优化

在遍历Map时,不同的遍历方式可能会有不同的性能表现。一般来说,使用entrySet遍历Map是最常用且高效的方式,因为它可以同时获取键和值,避免了多次查找。

如果只需要遍历键,使用keySet;只需要遍历值,使用values。但要注意,values返回的是一个Collection,不是Set,如果值有重复,遍历values可能会有性能问题,因为在判断重复值时需要更多的操作。

例如,比较不同遍历方式的性能:

import java.util.HashMap;
import java.util.Map;
import java.util.Set;

public class MapTraversalPerformanceExample {
    public static void main(String[] args) {
        Map<String, Integer> map = new HashMap<>();
        for (int i = 0; i < 1000000; i++) {
            map.put("key" + i, i);
        }

        long startTime = System.currentTimeMillis();
        Set<Map.Entry<String, Integer>> entrySet = map.entrySet();
        for (Map.Entry<String, Integer> entry : entrySet) {
            String key = entry.getKey();
            Integer value = entry.getValue();
        }
        long endTime = System.currentTimeMillis();
        System.out.println("使用entrySet遍历的时间: " + (endTime - startTime) + " ms");

        startTime = System.currentTimeMillis();
        Set<String> keySet = map.keySet();
        for (String key : keySet) {
            Integer value = map.get(key);
        }
        endTime = System.currentTimeMillis();
        System.out.println("使用keySet遍历的时间: " + (endTime - startTime) + " ms");
    }
}

在上述代码中,创建了一个包含一百万个键值对的HashMap,然后分别使用entrySetkeySet进行遍历,并记录遍历时间。一般情况下,entrySet的遍历速度会更快,因为使用keySet遍历需要每次通过键去获取值,增加了查找操作。

4. 自定义键类型

当使用自定义类型作为Map的键时,需要注意重写equalshashCode方法。HashMap等基于哈希表的实现类通过hashCode方法计算键的哈希值来确定存储位置,通过equals方法来比较键是否相等。如果没有正确重写这两个方法,可能会导致键值对存储和检索出现问题。

例如,定义一个自定义类作为Map的键:

import java.util.HashMap;
import java.util.Map;

public class CustomKeyMapExample {
    static class CustomKey {
        private int id;
        private String name;

        public CustomKey(int id, String name) {
            this.id = id;
            this.name = name;
        }

        @Override
        public boolean equals(Object o) {
            if (this == o) return true;
            if (o == null || getClass() != o.getClass()) return false;
            CustomKey customKey = (CustomKey) o;
            return id == customKey.id && name.equals(customKey.name);
        }

        @Override
        public int hashCode() {
            int result = 17;
            result = 31 * result + id;
            result = 31 * result + name.hashCode();
            return result;
        }
    }

    public static void main(String[] args) {
        Map<CustomKey, Integer> map = new HashMap<>();
        CustomKey key1 = new CustomKey(1, "test");
        map.put(key1, 100);

        CustomKey key2 = new CustomKey(1, "test");
        Integer value = map.get(key2);
        System.out.println("获取的值: " + value);
    }
}

在上述代码中,CustomKey类重写了equalshashCode方法。通过正确重写这两个方法,当使用CustomKey的实例作为键时,HashMap能够正确地存储和检索键值对。如果不重写hashCode方法,可能会导致不同的CustomKey实例(即使它们逻辑上相等)被存储在不同的位置,从而在检索时找不到对应的值。

5. 使用compute系列方法

Java 8为Map接口引入了computecomputeIfAbsentcomputeIfPresent等方法,这些方法提供了更灵活和便捷的方式来更新Map中的值。

  • compute(K key, BiFunction<? super K,? super V,? extends V> remappingFunction):该方法会根据指定的键和重映射函数来计算新的值。如果键不存在,重映射函数的第一个参数为null;如果键存在,第一个参数为键,第二个参数为当前值。然后根据重映射函数的计算结果更新或插入键值对。

例如:

import java.util.HashMap;
import java.util.Map;

public class MapComputeExample {
    public static void main(String[] args) {
        Map<String, Integer> map = new HashMap<>();
        map.put("one", 1);

        Integer newVal = map.compute("one", (k, v) -> v == null? 1 : v + 1);
        System.out.println("新的值: " + newVal);

        newVal = map.compute("two", (k, v) -> v == null? 1 : v + 1);
        System.out.println("新的值: " + newVal);
    }
}

在上述代码中,对于存在的键"one",重映射函数将其值加1;对于不存在的键"two",重映射函数插入值1。

  • computeIfAbsent(K key, Function<? super K,? extends V> mappingFunction):该方法仅在键不存在时,才根据映射函数计算新的值并插入。如果键已存在,则返回当前值,不进行计算。

例如:

import java.util.HashMap;
import java.util.Map;

public class MapComputeIfAbsentExample {
    public static void main(String[] args) {
        Map<String, Integer> map = new HashMap<>();
        map.put("one", 1);

        Integer newVal = map.computeIfAbsent("one", k -> 2);
        System.out.println("键 'one' 的值: " + newVal);

        newVal = map.computeIfAbsent("two", k -> 2);
        System.out.println("键 'two' 的值: " + newVal);
    }
}

这里,键"one"已存在,computeIfAbsent返回当前值1;键"two"不存在,通过映射函数计算值为2并插入。

  • computeIfPresent(K key, BiFunction<? super K,? super V,? extends V> remappingFunction):该方法仅在键存在时,才根据重映射函数计算新的值并更新。如果键不存在,则不进行任何操作并返回null

例如:

import java.util.HashMap;
import java.util.Map;

public class MapComputeIfPresentExample {
    public static void main(String[] args) {
        Map<String, Integer> map = new HashMap<>();
        map.put("one", 1);

        Integer newVal = map.computeIfPresent("one", (k, v) -> v + 1);
        System.out.println("键 'one' 的新值: " + newVal);

        newVal = map.computeIfPresent("two", (k, v) -> v + 1);
        System.out.println("键 'two' 的新值: " + newVal);
    }
}

对于键"one",存在时重映射函数将其值加1;对于键"two",不存在则返回null

6. 使用merge方法

merge(K key, V value, BiFunction<? super V,? super V,? extends V> remappingFunction)方法用于将指定的键值对合并到Map中。如果键不存在,直接插入键值对;如果键已存在,则根据重映射函数将新值与旧值合并。

例如:

import java.util.HashMap;
import java.util.Map;

public class MapMergeExample {
    public static void main(String[] args) {
        Map<String, Integer> map = new HashMap<>();
        map.put("one", 1);

        map.merge("one", 2, (oldValue, newValue) -> oldValue + newValue);
        System.out.println("键 'one' 的值: " + map.get("one"));

        map.merge("two", 2, (oldValue, newValue) -> oldValue + newValue);
        System.out.println("键 'two' 的值: " + map.get("two"));
    }
}

在上述代码中,对于存在的键"one",重映射函数将旧值1和新值2相加得到3;对于不存在的键"two",直接插入值2。

7. 线程安全的Map

在多线程环境下使用Map时,需要考虑线程安全问题。HashMap是非线程安全的,如果多个线程同时访问和修改HashMap,可能会导致数据不一致或其他问题。

  • ConcurrentHashMapConcurrentHashMap是线程安全的Map实现类,它允许多个线程同时读,并且允许多个线程同时修改不同的部分,具有较高的并发性能。在大多数多线程场景下,ConcurrentHashMap是一个很好的选择。

例如,在多线程环境下使用ConcurrentHashMap

import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

public class ConcurrentHashMapExample {
    public static void main(String[] args) {
        ConcurrentHashMap<String, Integer> map = new ConcurrentHashMap<>();
        ExecutorService executorService = Executors.newFixedThreadPool(10);

        for (int i = 0; i < 10; i++) {
            executorService.submit(() -> {
                map.put("key" + Math.random(), (int) (Math.random() * 100));
            });
        }

        executorService.shutdown();
        while (!executorService.isTerminated()) {
        }

        System.out.println("Map的大小: " + map.size());
    }
}

在这个示例中,创建了一个ConcurrentHashMap,并使用线程池模拟多个线程同时向Map中插入数据。ConcurrentHashMap能够保证在多线程环境下的线程安全。

  • HashtableHashtable也是线程安全的Map实现类,但它的同步粒度较大,在多线程环境下性能相对ConcurrentHashMap较差,并且Hashtable不允许键或值为null。在新的代码中,通常推荐使用ConcurrentHashMap而不是Hashtable

8. 不可变Map

在某些情况下,需要创建一个不可变的Map,即一旦创建后,不能再添加、删除或修改键值对。Java 9引入了Map.ofMap.ofEntries等方法来创建不可变Map

例如:

import java.util.Map;

public class ImmutableMapExample {
    public static void main(String[] args) {
        Map<String, Integer> immutableMap = Map.of("one", 1, "two", 2);
        // 以下操作会抛出UnsupportedOperationException
        // immutableMap.put("three", 3);

        Map<String, Integer> anotherImmutableMap = Map.ofEntries(
                Map.entry("one", 1),
                Map.entry("two", 2)
        );
        // 同样,以下操作会抛出UnsupportedOperationException
        // anotherImmutableMap.put("three", 3);
    }
}

在上述代码中,通过Map.ofMap.ofEntries方法创建的Map是不可变的,尝试修改它们会抛出UnsupportedOperationException。不可变Map在需要确保数据不被意外修改的场景下非常有用,比如作为方法的返回值或者配置信息的存储。

通过深入了解Map接口的常用方法及这些使用技巧,可以在Java编程中更高效、灵活地使用Map来解决各种实际问题,无论是简单的键值对存储,还是复杂的多线程数据处理和高性能的缓存系统等。