如何使用 java 流将列表转换为具有多个键和值的映射

How to convert list to map with multiple keys and value as list using java stream

提问人:sunleo 提问时间:11/13/2023 最后编辑:Doesn't Mattersunleo 更新时间:11/20/2023 访问量:186

问:

我有列表,需要转换为 ssns 列表和 id 列表的映射。你能帮忙吗?Users(id,ssn,name)

List.of(new User(1, "ssn1", "user1"), new User(2, "ssn2", "user2"), new User(3, "ssn3", "user3"))

Map with 2 keys("SSNs","IDs") and list as value
"SSNs" -  List("ssn1","ssn2","ssn3")
"IDs" -  List(1,2,3)

法典

public class Test {

    public static void main(String[] args) {

        List<User> users = List.of(new User(1, "ssn1", "user1"), new User(2, "ssn2", "user2"),
                new User(3, "ssn3", "user3"));

        Map<String, List<Object>> userMap = users.stream().collect(Collectors.groupingBy(User::getSsn));
    
    }

}

class User {
    public int getId() {
        return id;
    }

    public String getSsn() {
        return ssn;
    }

    public String getName() {
        return name;
    }

    int id;
    String ssn;
    String name;

    public User(int id, String ssn, String name) {
        this.id = id;
        this.ssn = ssn;
        this.name = name;
    }
}

更新:更新为 .List<User>List<Object>

java-stream

评论

0赞 experiment unit 1998X 11/13/2023
所以你想要一个映射或将字段名称映射到该字段的现有值列表?<String, List<Object>><String, List<?>>
0赞 sunleo 11/13/2023
我需要带有 2 个键的映射,一个键的值为字符串列表,另一个键的值为 ints 列表

答:

5赞 Youcef LAIDANI 11/13/2023 #1

一个简单的for循环可以解决你的问题,而不是使用流,我建议使用这个简单可读的解决方案:

Map<String, List<Object>> userMap = new HashMap<>();
userMap.put("SSNs", new ArrayList<>());
userMap.put("IDs", new ArrayList<>());
for (User user: users) {
    userMap.get("IDs").add(user.getId());
    userMap.get("SSNs").add(user.getSsn());
}

结果

{IDs=[1, 2, 3], SSNs=[ssn1, ssn2, ssn3]}

评论

0赞 sunleo 11/13/2023
感谢您的回答,但我期待是否可以使用 java 流而不是常规流来做一些事情。
0赞 experiment unit 1998X 11/13/2023 #2

一定要考虑@Youcef的解决方案。试图做你想做的事似乎真的把事情弄得一团糟。见下文:

public class tester {
    public static void main(String[] args) {
        Map<String, Function<User, Object>> fields = new HashMap<>();
        fields.put("SSNs", User::getSsn);
        fields.put("Ids", User::getId);
        List<User> users = List.of(new User(1, "ssn1", "user1"), new User(2, "ssn2", "user2"),
                new User(3, "ssn3", "user3"));
        Map<String, List<Object>> map = fields.entrySet().stream().collect(Collectors.toMap(Map.Entry::getKey, e -> users.stream().map(u -> e.getValue().apply(u)).collect(Collectors.toList())));
        System.out.println(map);
//        Map<String, List<User>> userMap = users.stream().collect(Collectors.groupingBy(User::getSsn));
    }
}

输出:

{SSNs=[ssn1, ssn2, ssn3], Ids=[1, 2, 3]}
6赞 Holger 11/13/2023 #3

这不一定是对循环解决方案的改进,但您可以使用 Stream API 解决任务,如下所示:

Map<String, List<Object>> result = users.stream()
    .collect(Collectors.teeing(
        Collectors.mapping(User::getId, Collectors.toList()),
        Collectors.mapping(User::getSsn, Collectors.toList()),
        (id, ssn) -> Map.of("IDs", id, "SSNs", ssn)));

上面的例子适用于 Eclipse。看来,javac在类型推断方面存在问题。以下适用于两者:

Map<String, List<Object>> result = users.stream()
    .collect(Collectors.teeing(
        Collectors.mapping(User::getId, Collectors.<Object>toList()),
        Collectors.mapping(User::getSsn, Collectors.<Object>toList()),
        (id, ssn) -> Map.of("IDs", id, "SSNs", ssn)));

或者

Map<String, List<?>> result = users.stream()
    .collect(Collectors.teeing(
        Collectors.mapping(User::getId, Collectors.toList()),
        Collectors.mapping(User::getSsn, Collectors.toList()),
        (id, ssn) -> Map.of("IDs", id, "SSNs", ssn)));

评论

0赞 sunleo 11/13/2023
贪婪的回答!所以这是可以做到的。谢谢。
1赞 k314159 11/13/2023 #4

您请求的是一个具有已知常量条目数 (2) 和已知常量键集(“SSN”和“ID”)的。这听起来好像如果你只是将值保存在两个单独的变量中,而不是试图将它们塞进一个双条目的 Map 中,你的代码会简单得多。但是,如果您真的想要这样做,则必须创建列表,而不是更自然和.MapList<Object>List<String>List<Integer>

    List<Object> ids = users.stream().map(user -> (Object) user.getId()).toList();
    List<Object> ssns = users.stream().map(user -> (Object) user.getSsn()).toList();
    Map<String, List<Object>> userMap = Map.of("IDs", ids, "SSNs", ssns);
-1赞 Ilian Tetradev 11/13/2023 #5

我看不出你从这里创建一个有什么好处。如果你需要将两个列表都传递给一个函数,你最好创建一个包含你的 2 个列表的对象。Map

class UsersList {

    //... getters

    List<Integer> ids;
    List<String> ssns;

    public UsersList(List<User> users) {
        this.ids = new ArrayList<>();
        this.ssns = new ArrayList<>();
        users.forEach((u) -> {
            ids.add(u.getId());
            ssns.add(u.getSsn());
        });
    }
}

然后,您可以将列表传递给构造函数,并且可以像这样访问这两个列表:

   UsersList usersList = new UsersList(users);

   System.out.println("ids: " + usersList.ids);
   System.out.println("ssns: " + usersList.ssns);

编辑:更新了构造函数以改用 1 个流。

评论

1赞 sunleo 11/13/2023
我想知道如何通过单个流调用来做到这一点。我知道,使用多个流调用它会起作用。
0赞 Ilian Tetradev 11/13/2023
@sunleo 这是一个很好的观点。我已将构造函数更新为使用 1 个流。
1赞 WJS 11/17/2023 #6

这是另一种选择,但不如霍尔格斯优雅。至少,这个想法可以应用于类似的要求。例如,由于开球仅限于两个流(尽管我相信这个限制是可以克服的),您也可以将第三个条目作为要处理的条目,而无需进行其他更改。consumer.accept(Map.entry("Names", user.getName()));

  • 首先,流式传输用户实例。
  • 然后,使用 mapMulti(在 Java 16 中引入)与 结合使用,创建一个条目,其中包含要放入列表中的 和 关联。然后使用 ,将每个条目放在流上。Map.entryMap keyvalueconsumer.accept
  • 然后,按输入键对元素进行分组,并将输入值映射到列表。
Map<String, List<Object>> result = users.stream()
   .<Entry<String, Object>>mapMulti((user, consumer) -> {
         consumer.accept(Map.entry("SSNs", user.getSsn()));
         consumer.accept(Map.entry("IDs", user.getId())); 
    }).collect(Collectors.groupingBy(Entry::getKey,
        Collectors.mapping(Entry::getValue, Collectors.toList())));

result.entrySet().forEach(System.out::println);

指纹

SSNs=[ssn1, ssn2, ssn3]
IDs=[1, 2, 3]

请注意方法的类型见证。在许多情况下,这是必需的,因为编译器无法确定要放置在流上的结果类型。<Entry<String, Object>>mapMulti