如何在执行 Collectors.toMap() 之前删除会导致冲突的键

How to remove Keys that would cause Collisions before executing Collectors.toMap()

,但是,我不想忽略重复值,而是想事先从该流中删除任何值并将它们打印出来。

例如,来自这个片段:

Map<String, String> phoneBook = people.stream()
                                      .collect(toMap(Person::getName,
                                                     Person::getAddress));

如果有重复条目,将导致抛出 java.lang.IllegalStateException: Duplicate key 错误。

该问题中提出的解决方案使用 mergeFunction 在发现碰撞时保留第一个条目。

Map<String, String> phoneBook = 
    people.stream()
          .collect(Collectors.toMap(
             Person::getName,
             Person::getAddress,
             (address1, address2) -> {
                 System.out.println("duplicate key found!");
                 return address1;
             }
          ));

如果流中的重复键发生冲突,我不想保留第一个条目,而是想知道是哪个值导致了冲突,并确保在生成的映射中没有出现该值。

即如果 "Bob" 在流中出现三次,它不应该出现在 map 中一次。

在创建该地图的过程中,我想过滤掉任何重复的名称并以某种方式记录它们。

我想确保在创建列表时不能有重复的条目,并且有某种方法可以知道哪些条目在传入流中有重复的键。我正在考虑事先使用 groupingByfilter 来查找重复键,但我不确定最好的方法是什么。

在处理完整个输入流之前,您无法知道哪些键是重复的。因此,任何 pre-processing 步骤都必须在您的主要逻辑之前完成输入的完整传递,这是一种浪费。

另一种方法可以是:

  1. 使用合并函数为违规键插入虚拟值
  2. 同时,将有问题的密钥插入 Set<K>
  3. 处理输入流后,迭代 Set<K> 以从主映射中删除有问题的键。

如果我没有正确理解您在评论中的说明,那么在您的列表中出现不止一次的人不应包含在最终地图中。

if "Bob" appeared three times in the stream, it should not be in the map even once.

并且每个出现不止一次的人都应该存储在List个重复的人中。

I would like to filter out any duplicate names and record them some way.

这应该是您要找的。

List<String> peopleDuplicated = new ArrayList<>();
Map<String, String> phoneBook2 = people.stream()
        .collect(Collectors.groupingBy(Person::getName)) //grouping people by name
        .entrySet().stream() //creating a stream from the entries of the grouped map
        .peek(e -> {
            //Adding to the duplicated names list the name of every person with more than one address
            if (e.getValue().size() > 1){
                peopleDuplicated.add(e.getKey());
            }
        })
        .filter(e -> e.getValue().size() == 1) //keeping only the people which have occurred only once (no duplicates)
        .collect(Collectors.toMap(e -> e.getKey(), e -> e.getValue().get(0).getAddress())); //mapping the entries into a new map

警告

由于重复元素是通过有状态的 lambda 存储的,因此该解决方案应 与 non-parallel 流一起使用,因为其结果可能是在并行执行中不可预测。

用数学术语来说,您想对分组聚合进行分区并分别处理这两个部分。

Map<String, String> makePhoneBook(Collection<Person> people) {
    Map<Boolean, List<Person>> phoneBook = people.stream()
            .collect(Collectors.groupingBy(Person::getName))
            .values()
            .stream()
            .collect(Collectors.partitioningBy(list -> list.size() > 1,
            Collectors.mapping(r -> r.get(0),
                    Collectors.toList())));

    // handle duplicates
    phoneBook.get(true)
            .forEach(x -> System.out.println("duplicate found " + x));

    return phoneBook.get(false).stream()
            .collect(Collectors.toMap(
                    Person::getName,
                    Person::getAddress));
}

I would like to remove any values from that stream beforehand.

正如@JimGarrison 指出的那样,预处理数据没有意义。

在处理所有数据集之前,您无法提前知道名称是否唯一。

您必须考虑的另一件事是,在流管道内部(在收集器之前)您了解之前遇到的数据。因为中间操作的结果不应该依赖于任何状态。

如果您认为流的行为就像一系列循环,因此假设可以在收集流元素之前对其进行预处理,那是不正确的。流管道的元素一次一个地被延迟处理。 IE。 管道中的所有操作将应用于单个元素并且每个操作仅在需要时应用(这就是懒惰的意思)。

有关更多信息,请查看 this tutorial and API documentation

实施

您可以通过利用 Collectors.teeing() 自定义对象 单流语句 中分离唯一值和重复项包含 duplicatedunique 条目的单独集合 phone book.

由于这个对象的主要功能只是携带数据,所以我将其实现为 Java 16 记录。

public record FilteredPhoneBook(Map<String, String> uniquePersonsAddressByName,
                                List<String> duplicatedNames) {}

Collector teeing() 需要三个参数:两个 collectors 和一个 function 合并两个收集器产生的结果。

groupingBy() 结合 counting() 生成的 map 用于确定重复的名称。

由于没有必要处理数据,用作第二个收集器toMap()将创建一个包含所有名称.

当两个收集器将结果交给 merger 函数时,它会负责删除重复项。

public static FilteredPhoneBook getFilteredPhoneBook(Collection<Person> people) {
    return people.stream()
        .collect(Collectors.teeing(
            Collectors.groupingBy(Person::getName, Collectors.counting()), // intermediate Map<String, Long>
            Collectors.toMap(                                              // intermediate Map<String, String>
                Person::getName,
                Person::getAddress,
                (left, right) -> left),
            (Map<String, Long> countByName, Map<String, String> addressByName) -> {
                countByName.values().removeIf(count -> count == 1);        // removing unique names
                addressByName.keySet().removeAll(countByName.keySet());    // removing all duplicates
                
                return new FilteredPhoneBook(addressByName, new ArrayList<>(countByName.keySet()));
            }
        ));
}

另一种解决此问题的方法是利用 Map<String,Boolean> 作为发现重复项的方法,正如@Holger 所建议的那样。

第一个收集器将使用 toMap() 编写。它将 true 与只遇到过一次的键相关联,如果至少找到一个重复项,它的 mergeFunction 将分配 false 的值。

其余逻辑不变

public static FilteredPhoneBook getFilteredPhoneBook(Collection<Person> people) {
    return people.stream()
        .collect(Collectors.teeing(
            Collectors.toMap(            // intermediate Map<String, Boolean>
                Person::getName,
                person -> true,          // not proved to be a duplicate and initially considered unique
                (left, right) -> false), // is a duplicate
            Collectors.toMap(            // intermediate Map<String, String>
                Person::getName,
                Person::getAddress,
                (left, right) -> left),
            (Map<String, Boolean> isUniqueByName, Map<String, String> addressByName) -> {
                isUniqueByName.values().removeIf(Boolean::booleanValue);   // removing unique names
                addressByName.keySet().removeAll(isUniqueByName.keySet()); // removing all duplicates

                return new FilteredPhoneBook(addressByName, new ArrayList<>(isUniqueByName.keySet()));
            }
        ));
}

main() - 演示

public static void main(String[] args) {
    List<Person> people = List.of(
        new Person("Alise", "address1"),
        new Person("Bob", "address2"),
        new Person("Bob", "address3"),
        new Person("Carol", "address4"),
        new Person("Bob", "address5")
    );

   FilteredPhoneBook filteredPhoneBook = getFilteredPhoneBook(people);
        
    System.out.println("Unique entries:");
    filteredPhoneBook.uniquePersonsAddressByName.forEach((k, v) -> System.out.println(k + " : " + v));
    System.out.println("\nDuplicates:");
    filteredPhoneBook.duplicatedNames().forEach(System.out::println);
}

输出

Unique entries:
Alise : address1
Carol : address4

Duplicates:
Bob