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

利用Java Map接口进行数据的分组与统计

2022-04-192.9k 阅读

Java Map接口基础

Map接口概述

在Java编程中,Map接口是一种非常重要的数据结构,它用于存储键值对(key - value pairs)。与ListSet不同,Map中的元素是通过键来访问的,而不是像List那样通过索引,也不像Set那样强调元素的唯一性(Set其实可以看作是值为null或者具有特殊语义值的Map)。

Map接口定义了一系列操作,例如向映射中添加键值对、通过键获取值、移除键值对以及检查映射中是否包含特定的键或值等。Java提供了多个实现Map接口的类,常见的有HashMapTreeMapLinkedHashMap等,每个实现类在性能、排序和线程安全性等方面都有不同的特点。

Map接口的常用方法

  1. put(K key, V value):将指定的键值对插入到Map中。如果Map中已经存在该键,则旧值会被新值替换,并返回旧值;如果不存在,则返回null
Map<String, Integer> map = new HashMap<>();
Integer oldValue = map.put("one", 1); // oldValue为null
oldValue = map.put("one", 2); // oldValue为1
  1. get(Object key):通过指定的键获取对应的值。如果Map中不存在该键,则返回null
Map<String, Integer> map = new HashMap<>();
map.put("one", 1);
Integer value = map.get("one"); // value为1
value = map.get("two"); // value为null
  1. containsKey(Object key):检查Map中是否包含指定的键。
Map<String, Integer> map = new HashMap<>();
map.put("one", 1);
boolean contains = map.containsKey("one"); // contains为true
contains = map.containsKey("two"); // contains为false
  1. remove(Object key):从Map中移除指定键及其对应的值,并返回被移除的值。如果不存在该键,则返回null
Map<String, Integer> map = new HashMap<>();
map.put("one", 1);
Integer removedValue = map.remove("one"); // removedValue为1
removedValue = map.remove("one"); // removedValue为null
  1. size():返回Map中键值对的数量。
Map<String, Integer> map = new HashMap<>();
map.put("one", 1);
map.put("two", 2);
int size = map.size(); // size为2

利用Map接口进行数据分组

简单对象分组

假设我们有一个包含学生信息的类Student,其中包含姓名和年级信息。

class Student {
    private String name;
    private int grade;

    public Student(String name, int grade) {
        this.name = name;
        this.grade = grade;
    }

    public String getName() {
        return name;
    }

    public int getGrade() {
        return grade;
    }
}

现在我们有一个List<Student>,想要按照年级对学生进行分组。可以使用Map<Integer, List<Student>>来实现,其中键是年级,值是该年级的学生列表。

import java.util.*;

public class StudentGrouping {
    public static void main(String[] args) {
        List<Student> students = Arrays.asList(
                new Student("Alice", 1),
                new Student("Bob", 2),
                new Student("Charlie", 1)
        );

        Map<Integer, List<Student>> gradeGroupMap = new HashMap<>();
        for (Student student : students) {
            int grade = student.getGrade();
            if (!gradeGroupMap.containsKey(grade)) {
                gradeGroupMap.put(grade, new ArrayList<>());
            }
            gradeGroupMap.get(grade).add(student);
        }

        for (Map.Entry<Integer, List<Student>> entry : gradeGroupMap.entrySet()) {
            System.out.println("Grade " + entry.getKey() + ": " + entry.getValue().size() + " students");
        }
    }
}

在上述代码中,通过遍历学生列表,先检查Map中是否已经存在对应年级的学生列表,如果不存在则创建一个新的ArrayList并添加到Map中,然后将当前学生添加到对应年级的列表中。

复杂对象分组

如果Student类还包含其他信息,比如所在班级,并且我们想要按照年级和班级同时进行分组,可以使用Map<Integer, Map<String, List<Student>>>,外层Map的键是年级,内层Map的键是班级,值仍然是学生列表。

class Student {
    private String name;
    private int grade;
    private String classSection;

    public Student(String name, int grade, String classSection) {
        this.name = name;
        this.grade = grade;
        this.classSection = classSection;
    }

    public String getName() {
        return name;
    }

    public int getGrade() {
        return grade;
    }

    public String getClassSection() {
        return classSection;
    }
}

public class ComplexStudentGrouping {
    public static void main(String[] args) {
        List<Student> students = Arrays.asList(
                new Student("Alice", 1, "A"),
                new Student("Bob", 2, "B"),
                new Student("Charlie", 1, "A")
        );

        Map<Integer, Map<String, List<Student>>> gradeClassGroupMap = new HashMap<>();
        for (Student student : students) {
            int grade = student.getGrade();
            String classSection = student.getClassSection();
            if (!gradeClassGroupMap.containsKey(grade)) {
                gradeClassGroupMap.put(grade, new HashMap<>());
            }
            Map<String, List<Student>> classMap = gradeClassGroupMap.get(grade);
            if (!classMap.containsKey(classSection)) {
                classMap.put(classSection, new ArrayList<>());
            }
            classMap.get(classSection).add(student);
        }

        for (Map.Entry<Integer, Map<String, List<Student>>> gradeEntry : gradeClassGroupMap.entrySet()) {
            int grade = gradeEntry.getKey();
            System.out.println("Grade " + grade + ":");
            Map<String, List<Student>> classMap = gradeEntry.getValue();
            for (Map.Entry<String, List<Student>> classEntry : classMap.entrySet()) {
                String classSection = classEntry.getKey();
                List<Student> studentsInClass = classEntry.getValue();
                System.out.println("  Class " + classSection + ": " + studentsInClass.size() + " students");
            }
        }
    }
}

这段代码展示了如何对更复杂的对象进行多层次的分组,通过嵌套的Map结构实现了按照年级和班级对学生进行分组。

利用Map接口进行数据统计

简单统计

假设我们有一个字符串数组,想要统计每个单词出现的次数。可以使用Map<String, Integer>,键是单词,值是该单词出现的次数。

import java.util.*;

public class WordCount {
    public static void main(String[] args) {
        String[] words = {"apple", "banana", "apple", "cherry", "banana"};

        Map<String, Integer> wordCountMap = new HashMap<>();
        for (String word : words) {
            if (!wordCountMap.containsKey(word)) {
                wordCountMap.put(word, 1);
            } else {
                wordCountMap.put(word, wordCountMap.get(word) + 1);
            }
        }

        for (Map.Entry<String, Integer> entry : wordCountMap.entrySet()) {
            System.out.println(entry.getKey() + ": " + entry.getValue() + " times");
        }
    }
}

在这个例子中,遍历单词数组,每次遇到一个单词,如果Map中不存在该单词,则将其计数设为1;如果已经存在,则将其计数加1。

复杂统计

如果我们有一个包含商品销售记录的类SaleRecord,其中包含商品名称、销售数量和销售金额,并且想要统计每种商品的总销售数量和总销售金额。

class SaleRecord {
    private String productName;
    private int quantity;
    private double amount;

    public SaleRecord(String productName, int quantity, double amount) {
        this.productName = productName;
        this.quantity = quantity;
        this.amount = amount;
    }

    public String getProductName() {
        return productName;
    }

    public int getQuantity() {
        return quantity;
    }

    public double getAmount() {
        return amount;
    }
}

import java.util.*;

public class ProductSalesStatistics {
    public static void main(String[] args) {
        List<SaleRecord> saleRecords = Arrays.asList(
                new SaleRecord("ProductA", 2, 100.0),
                new SaleRecord("ProductB", 3, 150.0),
                new SaleRecord("ProductA", 1, 50.0)
        );

        Map<String, SaleStatistics> productStatsMap = new HashMap<>();
        for (SaleRecord record : saleRecords) {
            String productName = record.getProductName();
            if (!productStatsMap.containsKey(productName)) {
                productStatsMap.put(productName, new SaleStatistics());
            }
            SaleStatistics stats = productStatsMap.get(productName);
            stats.totalQuantity += record.getQuantity();
            stats.totalAmount += record.getAmount();
        }

        for (Map.Entry<String, SaleStatistics> entry : productStatsMap.entrySet()) {
            String productName = entry.getKey();
            SaleStatistics stats = entry.getValue();
            System.out.println(productName + ": Total Quantity = " + stats.totalQuantity + ", Total Amount = " + stats.totalAmount);
        }
    }
}

class SaleStatistics {
    int totalQuantity;
    double totalAmount;
}

在上述代码中,我们定义了一个SaleStatistics类来存储每种商品的总销售数量和总销售金额。通过遍历销售记录列表,根据商品名称在Map中查找对应的统计信息,如果不存在则创建一个新的SaleStatistics对象,然后更新其数量和金额。

使用Java 8 Stream API进行分组和统计

使用Stream API进行分组

Java 8引入的Stream API为数据处理提供了更简洁、更高效的方式。对于前面按照年级对学生进行分组的例子,可以使用Stream API改写如下:

import java.util.*;
import java.util.stream.Collectors;

class Student {
    private String name;
    private int grade;

    public Student(String name, int grade) {
        this.name = name;
        this.grade = grade;
    }

    public String getName() {
        return name;
    }

    public int getGrade() {
        return grade;
    }
}

public class StudentGroupingWithStream {
    public static void main(String[] args) {
        List<Student> students = Arrays.asList(
                new Student("Alice", 1),
                new Student("Bob", 2),
                new Student("Charlie", 1)
        );

        Map<Integer, List<Student>> gradeGroupMap = students.stream()
               .collect(Collectors.groupingBy(Student::getGrade));

        for (Map.Entry<Integer, List<Student>> entry : gradeGroupMap.entrySet()) {
            System.out.println("Grade " + entry.getKey() + ": " + entry.getValue().size() + " students");
        }
    }
}

在这段代码中,students.stream()将学生列表转换为流,然后通过collect(Collectors.groupingBy(Student::getGrade))按照学生的年级进行分组,Collectors.groupingBy方法会自动根据年级创建对应的分组,并将学生添加到相应的组中。

使用Stream API进行多层次分组

对于按照年级和班级对学生进行分组的复杂情况,也可以使用Stream API来实现。

import java.util.*;
import java.util.stream.Collectors;

class Student {
    private String name;
    private int grade;
    private String classSection;

    public Student(String name, int grade, String classSection) {
        this.name = name;
        this.grade = grade;
        this.classSection = classSection;
    }

    public String getName() {
        return name;
    }

    public int getGrade() {
        return grade;
    }

    public String getClassSection() {
        return classSection;
    }
}

public class ComplexStudentGroupingWithStream {
    public static void main(String[] args) {
        List<Student> students = Arrays.asList(
                new Student("Alice", 1, "A"),
                new Student("Bob", 2, "B"),
                new Student("Charlie", 1, "A")
        );

        Map<Integer, Map<String, List<Student>>> gradeClassGroupMap = students.stream()
               .collect(Collectors.groupingBy(Student::getGrade,
                        Collectors.groupingBy(Student::getClassSection)));

        for (Map.Entry<Integer, Map<String, List<Student>>> gradeEntry : gradeClassGroupMap.entrySet()) {
            int grade = gradeEntry.getKey();
            System.out.println("Grade " + grade + ":");
            Map<String, List<Student>> classMap = gradeEntry.getValue();
            for (Map.Entry<String, List<Student>> classEntry : classMap.entrySet()) {
                String classSection = classEntry.getKey();
                List<Student> studentsInClass = classEntry.getValue();
                System.out.println("  Class " + classSection + ": " + studentsInClass.size() + " students");
            }
        }
    }
}

这里使用了两层Collectors.groupingBy,外层按照年级分组,内层按照班级分组,使得代码更加简洁和易读。

使用Stream API进行统计

对于单词统计的例子,使用Stream API可以这样实现:

import java.util.*;
import java.util.stream.Collectors;

public class WordCountWithStream {
    public static void main(String[] args) {
        String[] words = {"apple", "banana", "apple", "cherry", "banana"};

        Map<String, Long> wordCountMap = Arrays.stream(words)
               .collect(Collectors.groupingBy(
                        Function.identity(), Collectors.counting()));

        for (Map.Entry<String, Long> entry : wordCountMap.entrySet()) {
            System.out.println(entry.getKey() + ": " + entry.getValue() + " times");
        }
    }
}

Arrays.stream(words)将字符串数组转换为流,Collectors.groupingBy(Function.identity(), Collectors.counting())通过Function.identity()保持单词本身作为键,Collectors.counting()统计每个单词出现的次数。

使用Stream API进行复杂统计

对于商品销售统计的例子,使用Stream API可以改写为:

import java.util.*;
import java.util.stream.Collectors;

class SaleRecord {
    private String productName;
    private int quantity;
    private double amount;

    public SaleRecord(String productName, int quantity, double amount) {
        this.productName = productName;
        this.quantity = quantity;
        this.amount = amount;
    }

    public String getProductName() {
        return productName;
    }

    public int getQuantity() {
        return quantity;
    }

    public double getAmount() {
        return amount;
    }
}

class SaleStatistics {
    int totalQuantity;
    double totalAmount;
}

public class ProductSalesStatisticsWithStream {
    public static void main(String[] args) {
        List<SaleRecord> saleRecords = Arrays.asList(
                new SaleRecord("ProductA", 2, 100.0),
                new SaleRecord("ProductB", 3, 150.0),
                new SaleRecord("ProductA", 1, 50.0)
        );

        Map<String, SaleStatistics> productStatsMap = saleRecords.stream()
               .collect(Collectors.groupingBy(SaleRecord::getProductName,
                        Collectors.collectingAndThen(
                                Collectors.toList(),
                                records -> {
                                    SaleStatistics stats = new SaleStatistics();
                                    records.forEach(record -> {
                                        stats.totalQuantity += record.getQuantity();
                                        stats.totalAmount += record.getAmount();
                                    });
                                    return stats;
                                })));

        for (Map.Entry<String, SaleStatistics> entry : productStatsMap.entrySet()) {
            String productName = entry.getKey();
            SaleStatistics stats = entry.getValue();
            System.out.println(productName + ": Total Quantity = " + stats.totalQuantity + ", Total Amount = " + stats.totalAmount);
        }
    }
}

这里通过Collectors.groupingBy按照商品名称进行分组,Collectors.collectingAndThen先将同一种商品的销售记录收集到一个列表中,然后对列表进行遍历,统计总数量和总金额,最后生成SaleStatistics对象。

选择合适的Map实现类

HashMap

HashMap是最常用的Map实现类,它基于哈希表实现。HashMap允许null键和null值,并且在大多数情况下提供了很好的性能,特别是对于插入和查找操作。它的时间复杂度在理想情况下,插入、删除和查找操作的平均时间复杂度为O(1),但是在哈希冲突严重的情况下,性能会下降到O(n)。

Map<String, Integer> hashMap = new HashMap<>();
hashMap.put("key1", 1);
Integer value = hashMap.get("key1");

TreeMap

TreeMap基于红黑树实现,它会对键进行排序。这意味着如果需要按键的自然顺序或者自定义顺序遍历Map中的元素,TreeMap是一个很好的选择。TreeMap不允许null键,但是允许null值。它的插入、删除和查找操作的时间复杂度为O(log n)。

Map<String, Integer> treeMap = new TreeMap<>();
treeMap.put("key2", 2);
treeMap.put("key1", 1);
for (Map.Entry<String, Integer> entry : treeMap.entrySet()) {
    System.out.println(entry.getKey() + ": " + entry.getValue());
}

在上述代码中,TreeMap会按照键的自然顺序(字典序)输出键值对。

LinkedHashMap

LinkedHashMap继承自HashMap,它在维护哈希表的同时,还维护了一个双向链表来记录插入顺序或者访问顺序。如果需要按照插入顺序或者访问顺序遍历MapLinkedHashMap是一个不错的选择。它的性能与HashMap相近,但是由于需要额外维护链表,会占用更多的内存。

Map<String, Integer> linkedHashMap = new LinkedHashMap<>(16, 0.75f, true);
linkedHashMap.put("key1", 1);
linkedHashMap.put("key2", 2);
linkedHashMap.get("key1");
for (Map.Entry<String, Integer> entry : linkedHashMap.entrySet()) {
    System.out.println(entry.getKey() + ": " + entry.getValue());
}

在上述代码中,如果构造LinkedHashMap时设置了第三个参数为true,则会按照访问顺序遍历,即最近访问的元素会被移动到链表末尾。

总结与注意事项

  1. 数据分组与统计的应用场景:数据分组和统计在很多实际应用中都非常常见,比如数据分析、报表生成、日志处理等。通过合理使用Map接口及其实现类,可以高效地完成这些任务。
  2. 性能优化:在选择Map实现类时,要根据具体的需求和数据特点来选择。如果注重插入和查找的性能,且对顺序没有要求,HashMap是一个很好的选择;如果需要按键排序,TreeMap更为合适;如果需要按照插入或访问顺序遍历,LinkedHashMap比较适用。
  3. 注意空指针问题:不同的Map实现类对null键和null值的支持情况不同。HashMap允许null键和null值,而TreeMap不允许null键。在使用时要注意避免空指针异常。
  4. 线程安全性:上述介绍的HashMapTreeMapLinkedHashMap都不是线程安全的。如果在多线程环境下使用,需要采取额外的同步措施,比如使用Collections.synchronizedMap方法将其包装为线程安全的Map,或者使用ConcurrentHashMap

通过深入理解和熟练运用Map接口及其相关操作,开发人员可以在Java编程中更高效地处理数据分组和统计的任务,提升程序的性能和可读性。无论是简单的数据统计还是复杂的多层次分组,Map接口都提供了强大的工具来满足需求。同时,结合Java 8的Stream API,可以使代码更加简洁和优雅,进一步提高开发效率。