List<String> 获取以另一个列表中的一个字符串结尾的所有元素的计数

List<String> get count of all elements ending with one of strings from another list

假设我有一个包含以下元素的列表:

List<String> endings= Arrays.asList("AAA", "BBB", "CCC", "DDD");

我还有另一个大的字符串列表,我希望从中 select 所有以上述列表中的任何字符串结尾的元素。

List<String> fullList= Arrays.asList("111.AAA", "222.AAA", "111.BBB", "222.BBB", "111.CCC", "222.CCC", "111.DDD", "222.DDD");

理想情况下,我想要一种方法来划分第二个列表,使其包含四个组,每个组仅包含那些以第一个列表中的一个字符串结尾的元素。所以在上述情况下,结果将是 4 组,每组 2 个元素。

我找到了这个示例,但我仍然缺少可以按不同列表中包含的所有结尾进行过滤的部分。

Map<Boolean, List<String>> grouped = fullList.stream().collect(Collectors.partitioningBy((String e) -> !e.endsWith("AAA")));

更新:MC Emperor's Answer 确实有效,但它在包含数百万个字符串的列表上崩溃,因此在实践中效果不佳。

如果您创建一个辅助方法 getSuffix() 接受 String 和 return 它的后缀(例如 getSuffix("111.AAA") 将 return "AAA"), 你可以筛选出另一个列表中包含后缀的Strings,然后将它们分组:

Map<String,List<String>> grouped =
    fullList.stream()
            .filter(s -> endings.contains(getSuffix(s)))
            .collect(Collectors.groupingBy(s -> getSuffix(s)));

例如,如果 suffix 始终从索引 4 开始,您可以:

public static String getSuffix(String s) {
    return s.substring(4);
}

和上面的 Stream 管道将 return Map:

{AAA=[111.AAA, 222.AAA], CCC=[111.CCC, 222.CCC], BBB=[111.BBB, 222.BBB], DDD=[111.DDD, 222.DDD]}

P.S。请注意,如果将 endings List 更改为 HashSet.

filter 步骤会更有效

使用groupingBy.

Map<String, List<String>> grouped = fullList
  .stream()
  .collect(Collectors.groupingBy(s -> s.split("\.")[1]));

s.split("\.")[1] 将采用 xxx.yyy.

yyy 部分

编辑:如果你想清空结尾不在列表中的值,你可以过滤掉它们:

grouped.keySet().forEach(key->{
  if(!endings.contains(key)){
    grouped.put(key, Collections.emptyList());
  }
});

更新

这个和原来答案的做法类似,但是现在fullList不再遍历很多次了。相反,它被遍历一次,并且对于每个元素,都会在结尾列表中搜索匹配项。这被映射到 Entry(ending, fullListItem),然后按列表项分组。分组时,值元素被展开为 List.

Map<String, List<String>> obj = fullList.stream()
    .map(item -> endings.stream()
        .filter(item::endsWith)
        .findAny()
        .map(ending -> new AbstractMap.SimpleEntry<>(ending, item))
        .orElse(null))
    .filter(Objects::nonNull)
    .collect(groupingBy(Map.Entry::getKey, mapping(Map.Entry::getValue, toList())));

原回答

你可以使用这个:

Map<String, List<String>> obj = endings.stream()
    .map(ending -> new AbstractMap.SimpleEntry<>(ending, fullList.stream()
        .filter(str -> str.endsWith(ending))
        .collect(Collectors.toList())))
    .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue));

它采用所有结尾并遍历 fullList 以查找以该值结尾的元素。

请注意,使用这种方法时,它会针对每个元素遍历整个列表。这是相当低效的,我认为你最好使用另一种方法来映射元素。例如,如果您对fullList中元素的结构有所了解,那么您可以立即对其进行分组。

如果您的 fullList 有一些元素的后缀在您的 endings 中不存在,您可以尝试类似的方法:

    List<String> endings= Arrays.asList("AAA", "BBB", "CCC", "DDD");
    List<String> fullList= Arrays.asList("111.AAA", "222.AAA", "111.BBB", "222.BBB", "111.CCC", "222.CCC", "111.DDD", "222.DDD", "111.EEE");
    Function<String,String> suffix = s -> endings.stream()
                                                 .filter(e -> s.endsWith(e))
                                                 .findFirst().orElse("UnknownSuffix");
    Map<String,List<String>> grouped = fullList.stream()
                                               .collect(Collectors.groupingBy(suffix));
    System.out.println(grouped);

可以使用 groupingBy 的子字符串和 filter 来确保最终的 Map 只包含 Collection 的相关值。这可能是 :

Map<String, List<String>> grouped = fullList.stream()
        .collect(Collectors.groupingBy(a -> getSuffix(a)))
        .entrySet().stream()
        .filter(e -> endings.contains(e.getKey()))
        .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue));

private static String getSuffix(String a) {
    return a.split(".")[1];
}

您可以将 groupingByendings 列表中的过滤器一起使用,

fullList.stream()
  .collect(groupingBy(str -> endings.stream().filter(ele -> str.endsWith(ele)).findFirst().get()))

要对流进行分区,意味着将每个元素放入两个组中的一个。由于您有更多后缀,因此您需要 grouping 代替,即使用 groupingBy 而不是 partitioningBy.

如果您想支持任意 endings 列表,您可能更喜欢比线性搜索更好的东西。

一种方法是使用排序集合,使用基于后缀的比较器。

比较器可以这样实现

Comparator<String> backwards = (s1, s2) -> {
    for(int p1 = s1.length(), p2 = s2.length(); p1 > 0 && p2 > 0;) {
        int c = Integer.compare(s1.charAt(--p1), s2.charAt(--p2));
        if(c != 0) return c;
    }
    return Integer.compare(s1.length(), s2.length());
};

逻辑类似于字符串的自然顺序,唯一不同的是它从末尾运行到开头。换句话说,它相当于Comparator.comparing(s -> new StringBuilder(s).reverse().toString()),但效率更高。

然后,给定一个像

这样的输入
List<String> endings= Arrays.asList("AAA", "BBB", "CCC", "DDD");
List<String> fullList= Arrays.asList("111.AAA", "222.AAA",
        "111.BBB", "222.BBB", "111.CCC", "222.CCC", "111.DDD", "222.DDD");

您可以执行任务

// prepare collection with faster lookup
TreeSet<String> suffixes = new TreeSet<>(backwards);
suffixes.addAll(endings);

// use it for grouping
Map<String, List<String>> map = fullList.stream()
    .collect(Collectors.groupingBy(suffixes::floor));

但是如果你只对每组的个数感兴趣,你应该在分组时正确计数,避免存储元素列表:

Map<String, Long> map = fullList.stream()
    .collect(Collectors.groupingBy(suffixes::floor, Collectors.counting()));

如果列表可以包含不匹配列表后缀的字符串,则必须将 suffixes::floor 替换为 s -> { String g = suffixes.floor(s); return g!=null && s.endsWith(g)? g: "_None"; } 或类似函数。