Java Map与Set数据结构深度解析

57 阅读7分钟

一、Map与Set概述

1.1 基本概念

Map和Set是Java集合框架中专门用于高效搜索的数据结构:

  • Map:存储键值对(key-value)的映射关系
  • Set:存储唯一元素的集合(纯key模型)

1.2 核心实现类

接口Tree实现Hash实现
MapTreeMapHashMap
SetTreeSetHashSet

二、Map接口详解

2.1 Map的基本操作

创建Map对象

java

import java.util.*;

public class MapDemo {
    public static void main(String[] args) {
        // 创建HashMap (无序)
        Map<String, Integer> hashMap = new HashMap<>();
        
        // 创建TreeMap (按键排序)
        Map<String, Integer> treeMap = new TreeMap<>();
        
        // 添加元素
        hashMap.put("Alice", 25);
        hashMap.put("Bob", 30);
        hashMap.put("Charlie", 28);
        
        treeMap.put("Alice", 25);
        treeMap.put("Bob", 30);
        treeMap.put("Charlie", 28);
        
        System.out.println("HashMap: " + hashMap);
        System.out.println("TreeMap: " + treeMap);
    }
}

核心方法演示

java

public class MapOperations {
    public static void main(String[] args) {
        Map<String, Integer> map = new HashMap<>();
        
        // 1. put() - 添加或更新键值对
        map.put("Alice", 25);
        map.put("Bob", 30);
        map.put("Charlie", 28);
        map.put("Alice", 26); // 更新Alice的值
        
        System.out.println("初始Map: " + map);
        
        // 2. get() - 获取值
        Integer age = map.get("Bob");
        System.out.println("Bob的年龄: " + age);
        
        // 3. getOrDefault() - 安全获取
        Integer unknownAge = map.getOrDefault("David", 0);
        System.out.println("David的年龄(默认): " + unknownAge);
        
        // 4. remove() - 删除键值对
        map.remove("Charlie");
        System.out.println("删除Charlie后: " + map);
        
        // 5. containsKey() - 检查键是否存在
        boolean hasAlice = map.containsKey("Alice");
        System.out.println("是否包含Alice: " + hasAlice);
        
        // 6. containsValue() - 检查值是否存在
        boolean hasAge30 = map.containsValue(30);
        System.out.println("是否包含年龄30: " + hasAge30);
        
        // 7. size() - 获取大小
        System.out.println("Map大小: " + map.size());
        
        // 8. isEmpty() - 判断是否为空
        System.out.println("Map是否为空: " + map.isEmpty());
        
        // 9. clear() - 清空Map
        map.clear();
        System.out.println("清空后Map: " + map);
    }
}

2.2 Map的遍历方式

方法1:keySet()遍历

java

public class MapIteration {
    public static void keySetIteration(Map<String, Integer> map) {
        System.out.println("=== keySet()遍历 ===");
        Set<String> keys = map.keySet();
        
        for (String key : keys) {
            Integer value = map.get(key);
            System.out.println("Key: " + key + ", Value: " + value);
        }
    }
}

方法2:values()遍历

java

public static void valuesIteration(Map<String, Integer> map) {
    System.out.println("=== values()遍历 ===");
    Collection<Integer> values = map.values();
    
    for (Integer value : values) {
        System.out.println("Value: " + value);
    }
}

方法3:entrySet()遍历(推荐)

java

public static void entrySetIteration(Map<String, Integer> map) {
    System.out.println("=== entrySet()遍历 ===");
    Set<Map.Entry<String, Integer>> entries = map.entrySet();
    
    // 方式1: 增强for循环
    for (Map.Entry<String, Integer> entry : entries) {
        System.out.println("Key: " + entry.getKey() + ", Value: " + entry.getValue());
    }
    
    // 方式2: 迭代器
    Iterator<Map.Entry<String, Integer>> iterator = entries.iterator();
    while (iterator.hasNext()) {
        Map.Entry<String, Integer> entry = iterator.next();
        System.out.println("Key: " + entry.getKey() + ", Value: " + entry.getValue());
    }
    
    // 方式3: forEach + Lambda表达式 (Java 8+)
    map.forEach((key, value) -> 
        System.out.println("Key: " + key + ", Value: " + value));
}

方法4:Java 8 Stream API

java

public static void streamIteration(Map<String, Integer> map) {
    System.out.println("=== Stream API遍历 ===");
    
    map.entrySet().stream()
        .forEach(entry -> 
            System.out.println("Key: " + entry.getKey() + ", Value: " + entry.getValue()));
    
    // 带过滤的遍历
    map.entrySet().stream()
        .filter(entry -> entry.getValue() > 25)
        .forEach(entry -> 
            System.out.println("年龄大于25: " + entry.getKey() + " - " + entry.getValue()));
}

2.3 Map.Entry详解

java

public class MapEntryDemo {
    public static void main(String[] args) {
        Map<String, Integer> map = new HashMap<>();
        map.put("Alice", 25);
        map.put("Bob", 30);
        map.put("Charlie", 28);
        
        Set<Map.Entry<String, Integer>> entries = map.entrySet();
        
        for (Map.Entry<String, Integer> entry : entries) {
            // 获取键
            String key = entry.getKey();
            // 获取值
            Integer value = entry.getValue();
            // 修改值
            entry.setValue(value + 1);
            
            System.out.println("修改后 - Key: " + key + ", Value: " + entry.getValue());
        }
        
        System.out.println("最终Map: " + map);
    }
}

三、Set接口详解

3.1 Set的基本操作

java

import java.util.*;

public class SetOperations {
    public static void main(String[] args) {
        // 创建HashSet (无序)
        Set<Integer> hashSet = new HashSet<>();
        
        // 创建TreeSet (排序)
        Set<Integer> treeSet = new TreeSet<>();
        
        // 1. add() - 添加元素
        hashSet.add(10);
        hashSet.add(20);
        hashSet.add(30);
        hashSet.add(10); // 重复元素,不会添加
        
        treeSet.add(30);
        treeSet.add(10);
        treeSet.add(20);
        
        System.out.println("HashSet: " + hashSet);
        System.out.println("TreeSet: " + treeSet);
        
        // 2. contains() - 检查元素是否存在
        boolean has20 = hashSet.contains(20);
        System.out.println("HashSet是否包含20: " + has20);
        
        // 3. remove() - 删除元素
        hashSet.remove(20);
        System.out.println("删除20后HashSet: " + hashSet);
        
        // 4. size() - 获取大小
        System.out.println("HashSet大小: " + hashSet.size());
        
        // 5. isEmpty() - 判断是否为空
        System.out.println("HashSet是否为空: " + hashSet.isEmpty());
    }
}

3.2 Set的遍历方式

java

public class SetIteration {
    public static void main(String[] args) {
        Set<String> set = new HashSet<>();
        set.add("Apple");
        set.add("Banana");
        set.add("Orange");
        set.add("Grape");
        
        // 方法1: 增强for循环
        System.out.println("=== 增强for循环 ===");
        for (String fruit : set) {
            System.out.println(fruit);
        }
        
        // 方法2: 迭代器
        System.out.println("=== 迭代器 ===");
        Iterator<String> iterator = set.iterator();
        while (iterator.hasNext()) {
            System.out.println(iterator.next());
        }
        
        // 方法3: forEach + Lambda
        System.out.println("=== forEach ===");
        set.forEach(fruit -> System.out.println(fruit));
        
        // 方法4: Stream API
        System.out.println("=== Stream API ===");
        set.stream().forEach(System.out::println);
    }
}

3.3 Set的特殊操作

java

public class SetAdvancedOperations {
    public static void main(String[] args) {
        Set<Integer> set1 = new HashSet<>(Arrays.asList(1, 2, 3, 4, 5));
        Set<Integer> set2 = new HashSet<>(Arrays.asList(4, 5, 6, 7, 8));
        
        // 并集
        Set<Integer> union = new HashSet<>(set1);
        union.addAll(set2);
        System.out.println("并集: " + union);
        
        // 交集
        Set<Integer> intersection = new HashSet<>(set1);
        intersection.retainAll(set2);
        System.out.println("交集: " + intersection);
        
        // 差集 (set1 - set2)
        Set<Integer> difference = new HashSet<>(set1);
        difference.removeAll(set2);
        System.out.println("差集(set1-set2): " + difference);
        
        // 对称差集 (只在其中一个集合中)
        Set<Integer> symmetricDifference = new HashSet<>(union);
        symmetricDifference.removeAll(intersection);
        System.out.println("对称差集: " + symmetricDifference);
    }
}

四、TreeMap vs HashMap 对比

4.1 特性对比表

特性TreeMapHashMap
底层结构红黑树哈希表+链表/红黑树
时间复杂度O(log n)O(1)平均
排序按键自然排序无序
线程安全不安全不安全
Null键不允许允许一个null键
内存使用较少较多
适用场景需要排序高性能查询

4.2 实际性能测试

java

public class MapPerformanceTest {
    private static final int ELEMENT_COUNT = 100000;
    
    public static void main(String[] args) {
        // HashMap性能测试
        long startTime = System.currentTimeMillis();
        Map<Integer, String> hashMap = new HashMap<>();
        for (int i = 0; i < ELEMENT_COUNT; i++) {
            hashMap.put(i, "Value" + i);
        }
        long hashMapTime = System.currentTimeMillis() - startTime;
        
        // TreeMap性能测试
        startTime = System.currentTimeMillis();
        Map<Integer, String> treeMap = new TreeMap<>();
        for (int i = 0; i < ELEMENT_COUNT; i++) {
            treeMap.put(i, "Value" + i);
        }
        long treeMapTime = System.currentTimeMillis() - startTime;
        
        System.out.println("HashMap插入" + ELEMENT_COUNT + "个元素耗时: " + hashMapTime + "ms");
        System.out.println("TreeMap插入" + ELEMENT_COUNT + "个元素耗时: " + treeMapTime + "ms");
    }
}

五、实际应用练习题

5.1 统计元素出现次数

java

public class ElementCounter {
    /**
     * 统计数组中每个元素的出现次数
     */
    public static Map<Integer, Integer> countElements(int[] arr) {
        Map<Integer, Integer> countMap = new HashMap<>();
        
        for (int num : arr) {
            // 方法1: 使用getOrDefault
            countMap.put(num, countMap.getOrDefault(num, 0) + 1);
            
            // 方法2: 使用containsKey检查
            // if (countMap.containsKey(num)) {
            //     countMap.put(num, countMap.get(num) + 1);
            // } else {
            //     countMap.put(num, 1);
            // }
        }
        
        return countMap;
    }
    
    /**
     * 统计字符串中每个字符的出现次数
     */
    public static Map<Character, Integer> countCharacters(String str) {
        Map<Character, Integer> charMap = new HashMap<>();
        
        for (char c : str.toCharArray()) {
            charMap.put(c, charMap.getOrDefault(c, 0) + 1);
        }
        
        return charMap;
    }
    
    public static void main(String[] args) {
        int[] numbers = {23, 352, 46, 47, 84, 1, 44, 6, 1, 23};
        Map<Integer, Integer> countMap = countElements(numbers);
        
        System.out.println("元素出现次数统计:");
        countMap.forEach((key, value) -> 
            System.out.println("元素 " + key + " 出现 " + value + " 次"));
            
        // 按出现次数排序
        System.out.println("\n按出现次数排序:");
        countMap.entrySet().stream()
            .sorted(Map.Entry.<Integer, Integer>comparingByValue().reversed())
            .forEach(entry -> 
                System.out.println("元素 " + entry.getKey() + " 出现 " + entry.getValue() + " 次"));
    }
}

5.2 数组去重

java

public class ArrayDeduplication {
    /**
     * 使用HashSet去重
     */
    public static int[] deduplicateWithSet(int[] arr) {
        Set<Integer> set = new HashSet<>();
        for (int num : arr) {
            set.add(num);
        }
        
        // 转换回数组
        int[] result = new int[set.size()];
        int index = 0;
        for (int num : set) {
            result[index++] = num;
        }
        return result;
    }
    
    /**
     * 使用LinkedHashSet保持原始顺序
     */
    public static int[] deduplicateWithOrder(int[] arr) {
        Set<Integer> set = new LinkedHashSet<>();
        for (int num : arr) {
            set.add(num);
        }
        
        int[] result = new int[set.size()];
        int index = 0;
        for (int num : set) {
            result[index++] = num;
        }
        return result;
    }
    
    /**
     * 使用TreeSet排序去重
     */
    public static int[] deduplicateWithSort(int[] arr) {
        Set<Integer> set = new TreeSet<>();
        for (int num : arr) {
            set.add(num);
        }
        
        int[] result = new int[set.size()];
        int index = 0;
        for (int num : set) {
            result[index++] = num;
        }
        return result;
    }
    
    public static void main(String[] args) {
        int[] arr = {23, 352, 46, 47, 84, 1, 44, 6, 1, 23};
        
        System.out.println("原始数组: " + Arrays.toString(arr));
        System.out.println("HashSet去重: " + Arrays.toString(deduplicateWithSet(arr)));
        System.out.println("LinkedHashSet去重(保序): " + Arrays.toString(deduplicateWithOrder(arr)));
        System.out.println("TreeSet去重(排序): " + Arrays.toString(deduplicateWithSort(arr)));
    }
}

5.3 查找第一个重复元素

java

public class FirstDuplicateFinder {
    /**
     * 找到第一个重复的元素
     */
    public static Integer findFirstDuplicate(int[] arr) {
        Set<Integer> seen = new HashSet<>();
        
        for (int num : arr) {
            if (seen.contains(num)) {
                return num; // 找到第一个重复元素
            }
            seen.add(num);
        }
        return null; // 没有重复元素
    }
    
    /**
     * 找到所有重复的元素
     */
    public static Set<Integer> findAllDuplicates(int[] arr) {
        Set<Integer> duplicates = new HashSet<>();
        Set<Integer> seen = new HashSet<>();
        
        for (int num : arr) {
            if (!seen.add(num)) { // add返回false表示元素已存在
                duplicates.add(num);
            }
        }
        return duplicates;
    }
    
    /**
     * 找到每个元素的第一次出现位置
     */
    public static Map<Integer, Integer> findFirstOccurrence(int[] arr) {
        Map<Integer, Integer> firstOccurrence = new HashMap<>();
        
        for (int i = 0; i < arr.length; i++) {
            firstOccurrence.putIfAbsent(arr[i], i); // 只在键不存在时放入
        }
        
        return firstOccurrence;
    }
    
    public static void main(String[] args) {
        int[] arr = {23, 352, 46, 47, 84, 1, 44, 6, 1, 23};
        
        Integer firstDuplicate = findFirstDuplicate(arr);
        if (firstDuplicate != null) {
            System.out.println("第一个重复元素: " + firstDuplicate);
        } else {
            System.out.println("没有重复元素");
        }
        
        Set<Integer> allDuplicates = findAllDuplicates(arr);
        System.out.println("所有重复元素: " + allDuplicates);
        
        Map<Integer, Integer> firstOccurrence = findFirstOccurrence(arr);
        System.out.println("元素第一次出现位置: " + firstOccurrence);
    }
}

六、高级应用场景

6.1 缓存实现

java

public class SimpleCache<K, V> {
    private final Map<K, V> cache;
    private final int maxSize;
    
    public SimpleCache(int maxSize) {
        this.cache = new LinkedHashMap<K, V>(maxSize, 0.75f, true) {
            @Override
            protected boolean removeEldestEntry(Map.Entry<K, V> eldest) {
                return size() > maxSize;
            }
        };
        this.maxSize = maxSize;
    }
    
    public V get(K key) {
        return cache.get(key);
    }
    
    public void put(K key, V value) {
        cache.put(key, value);
    }
    
    public boolean contains(K key) {
        return cache.containsKey(key);
    }
    
    public void remove(K key) {
        cache.remove(key);
    }
    
    public int size() {
        return cache.size();
    }
    
    public void clear() {
        cache.clear();
    }
}

6.2 单词频率统计

java

public class WordFrequencyCounter {
    public static Map<String, Integer> countWordFrequency(String text) {
        Map<String, Integer> frequencyMap = new HashMap<>();
        
        // 分割单词(简单的空格分割,实际应用需要更复杂的分词)
        String[] words = text.toLowerCase().split("\s+");
        
        for (String word : words) {
            // 移除标点符号
            word = word.replaceAll("[^a-zA-Z]", "");
            if (!word.isEmpty()) {
                frequencyMap.put(word, frequencyMap.getOrDefault(word, 0) + 1);
            }
        }
        
        return frequencyMap;
    }
    
    public static void printTopWords(Map<String, Integer> frequencyMap, int topN) {
        frequencyMap.entrySet().stream()
            .sorted(Map.Entry.<String, Integer>comparingByValue().reversed())
            .limit(topN)
            .forEach(entry -> 
                System.out.println(entry.getKey() + ": " + entry.getValue()));
    }
    
    public static void main(String[] args) {
        String text = "Hello world hello Java world programming Java programming";
        Map<String, Integer> frequencyMap = countWordFrequency(text);
        
        System.out.println("单词频率统计:");
        frequencyMap.forEach((word, count) -> 
            System.out.println(word + ": " + count));
            
        System.out.println("\n出现频率最高的3个单词:");
        printTopWords(frequencyMap, 3);
    }
}

七、最佳实践总结

7.1 选择指南

  • 需要排序:TreeMap / TreeSet
  • 高性能查询:HashMap / HashSet
  • 保持插入顺序:LinkedHashMap / LinkedHashSet
  • 线程安全:ConcurrentHashMap / CopyOnWriteArraySet
  • 内存敏感:考虑使用原始类型特化版本(如Trove库)

7.2 性能优化技巧

  1. 为HashMap设置合适的初始容量和负载因子
  2. 合理实现hashCode()和equals()方法
  3. 使用entrySet()进行Map遍历
  4. 考虑使用不可变对象作为键

7.3 常见陷阱

  1. 在遍历时修改集合(使用迭代器的remove方法)
  2. 使用可变对象作为HashMap键
  3. 忽略null值处理
  4. 不正确的hashCode/equals实现

通过深入理解Map和Set的特性及使用方法,可以显著提升Java程序的性能和代码质量。