使用流分组时将字符串分为多个组

bbK*_*ing 2 java hashmap java-stream groupingby

我正在尝试做的事情的简化示例:

假设我有一个字符串列表,需要根据是否包含特定子字符串的条件将其分为 4 组。如果一个字符串包含Foo它应该属于该组FOO,如果它包含Bar它应该属于该组BAR,如果它包含两者它应该出现在两个组中。

List<String> strings = List.of("Foo", "FooBar", "FooBarBaz", "XXX");
Run Code Online (Sandbox Code Playgroud)

由于字符串被分组到第一个匹配组中,因此上述输入的简单方法无法按预期工作:

Map<String,List<String>> result1 =
strings.stream()
        .collect(Collectors.groupingBy(
                        str -> str.contains("Foo") ? "FOO" :
                                    str.contains("Bar") ? "BAR" :
                                            str.contains("Baz") ? "BAZ" : "DEFAULT"));
Run Code Online (Sandbox Code Playgroud)

结果1是

{FOO=[Foo, FooBar, FooBarBaz], DEFAULT=[XXX]}
Run Code Online (Sandbox Code Playgroud)

期望的结果应该是

{FOO=[Foo, FooBar, FooBarBaz], BAR=[FooBar, FooBarBaz], BAZ=[FooBarBaz], DEFAULT=[XXX]}
Run Code Online (Sandbox Code Playgroud)

经过一段时间的搜索,我找到了另一种方法,它接近我想要的结果,但不太完全

Map<String,List<String>> result2 =
List.of("Foo", "Bar", "Baz", "Default").stream()
        .flatMap(str -> strings.stream().filter(s -> s.contains(str)).map(s -> new String[]{str.toUpperCase(), s}))
        .collect(Collectors.groupingBy(arr -> arr[0], Collectors.mapping(arr -> arr[1], Collectors.toList())));

System.out.println(result2);
Run Code Online (Sandbox Code Playgroud)

结果2是

{BAR=[FooBar, FooBarBaz], FOO=[Foo, FooBar, FooBarBaz], BAZ=[FooBarBaz]}
Run Code Online (Sandbox Code Playgroud)

虽然这可以正确地将包含子字符串的字符串分组到所需的组中,但不包含子字符串并因此应属于默认组的字符串将被忽略。期望的结果如上所述(顺序无关紧要)

{BAR=[FooBar, FooBarBaz], FOO=[Foo, FooBar, FooBarBaz], BAZ=[FooBarBaz], DEFAULT=[XXX]}
Run Code Online (Sandbox Code Playgroud)

现在我正在使用结果图并做额外的事情:

result2.put("DEFAULT", result1.get("DEFAULT"));
Run Code Online (Sandbox Code Playgroud)

以上这些可以一步完成吗?有没有比我上面的方法更好的方法?

Ale*_*nko 5

与其使用字符串"Foo""Bar"及其相应的大写版本进行操作,不如定义一个.enum

我们称它为Keys

public enum Keys {
    FOO("Foo"), BAR("Bar"), BAZ("Baz"), DEFAULT("");
    
    private static final Set<Keys> nonDefaultKeys = EnumSet.range(FOO, BAZ); // Set of enum constants (not includes DEFAULT), needed to avoid creating EnumSet or array of constants via `values()` at every invocation of getKeys()
    private String keyName;
    
    Keys(String keyName) {
        this.keyName = keyName;
    }
    
    public static List<String> getKeys(String str) {
        List<String> keys = nonDefaultKeys.stream()
            .filter(key -> str.contains(key.keyName))
            .map(Enum::name)
            .toList();

        // if non-default keys not found, i.e. keys.isEmpty() - return the DEFAULT
        return keys.isEmpty() ? List.of(DEFAULT.name()) : keys;
    }
}
Run Code Online (Sandbox Code Playgroud)

它有一个方法getKeys(String)需要一个字符串并返回给 定字符串应映射到的列表。

通过使用封装在Keys 枚举中的功能,我们可以创建一个字符串映射,该映射被分成与常量Keys名称相对应的组。collect(supplier,accumulator,combiner)

main()

public static void main(String[] args) {
    List<String> strings = List.of("Foo", "FooBar", "FooBarBaz", "XXX");

    Map<String, List<String>> stringsByGroup = strings.stream()
        .collect(
            HashMap::new, // mutable container - which will contain results of mutable reduction
            (Map<String, List<String>> map, String next) -> Keys.getKeys(next)
                .forEach(key -> map.computeIfAbsent(key, k -> new ArrayList<>()).add(next)), // accumulator function - defines how to store stream elements into the container
            (left, right) -> right.forEach((k, v) ->
                left.merge(k, v, (oldV, newV) -> { oldV.addAll(newV); return oldV; }) // combiner function - defines how to merge container while executing the stream in parallel
        ));
    
    stringsByGroup.forEach((k, v) -> System.out.println(k + " -> " + v));
}
Run Code Online (Sandbox Code Playgroud)

输出:

BAR -> [FooBar, FooBarBaz]
FOO -> [Foo, FooBar, FooBarBaz]
BAZ -> [FooBarBaz]
DEFAULT -> [XXX]
Run Code Online (Sandbox Code Playgroud)

在线演示的链接


WJS*_*WJS 5

这是使用mapMulti 的理想选择。 MapMulti 采用流值的BiConsumerConsumer。消费者习惯于简单地将某些东西放回流中之所以将其添加到 Java 中是因为可能会产生不需要的开销。flatMaps

这是通过构建一个字符串数组来实现的,就像您之前对Token包含字符串所做的那样并收集(也像您之前所做的那样)。如果在字符串中找到键,则接受包含该键和包含字符串的字符串数组。否则,接受带有默认键和字符串的字符串数组。

List<String> strings =
        List.of("Foo", "FooBar", "FooBarBaz", "XXX", "YYY");
Map<String, List<String>> result = strings.stream()
        .<String[]>mapMulti((str, consumer) -> {

            boolean found = false;
            String temp = str.toUpperCase();
            for (String token : List.of("FOO", "BAR",
                    "BAZ")) {
                if (temp.contains(token)) {
                    consumer.accept(
                            new String[] { token, str });
                    found = true;
                }
            }
            if (!found) {
                consumer.accept(
                        new String[] { "DEFAULT", str });
            }
        })
        .collect(Collectors.groupingBy(arr -> arr[0],
                Collectors.mapping(arr -> arr[1],
                        Collectors.toList())));

result.entrySet().forEach(System.out::println);
Run Code Online (Sandbox Code Playgroud)

印刷

BAR=[FooBar, FooBarBaz]
FOO=[Foo, FooBar, FooBarBaz]
BAZ=[FooBarBaz]
DEFAULT=[XXX, YYY]
Run Code Online (Sandbox Code Playgroud)

请记住,流旨在让您的编码世界变得更轻松。但有时,只需要使用一些 Java 8 构造的常规循环即可。除了学术练习之外,我可能会像这样完成任务。

Map<String,List<String>> result2 = new HashMap<>();

for (String str : strings) {
     boolean added = false;
     String temp = str.toUpperCase();
     for (String token : List.of("FOO","BAR","BAZ")) {
         if(temp.contains(token)) {
             result2.computeIfAbsent(token, v->new ArrayList<>()).add(str);
             added = true;
         }
     }
     if (!added) {
         result2.computeIfAbsent("DEFAULT", v-> new ArrayList<>()).add(str);
     }
}
Run Code Online (Sandbox Code Playgroud)