使用Java流,如何在同一类上通过2个键创建从列表到索引的映射?
我在这里给出一个代码示例,我希望地图“ personByName”通过firstName或lastName获取所有人,因此我想获取3个“ steve”:当它们是firstName或lastname时。我不知道如何混合使用2个Collectors.groupingBy。
public static class Person {
final String firstName;
final String lastName;
protected Person(String firstName, String lastName) {
super();
this.firstName = firstName;
this.lastName = lastName;
}
public String getFirstName() {
return firstName;
}
public String getLastName() {
return lastName;
}
}
@Test
public void testStream() {
List<Person> persons = Arrays.asList(
new Person("Bill", "Gates"),
new Person("Bill", "Steve"),
new Person("Steve", "Jobs"),
new Person("Steve", "Wozniac"));
Map<String, Set<Person>> personByFirstName = persons.stream().collect(Collectors.groupingBy(Person::getFirstName, Collectors.toSet()));
Map<String, Set<Person>> personByLastName = persons.stream().collect(Collectors.groupingBy(Person::getLastName, Collectors.toSet()));
Map<String, Set<Person>> personByName = persons.stream().collect(Collectors.groupingBy(Person::getLastName, Collectors.toSet()));// This is wrong, I want bot first and last name
Assert.assertEquals("we should search by firstName AND lastName", 3, personByName.get("Steve").size()); // This fails
}
我通过在两张地图上循环找到了一种解决方法,但是它不是面向流的。
答案 0 :(得分:7)
您可以这样做:
Map<String, Set<Person>> personByName = persons.stream()
.flatMap(p -> Stream.of(new SimpleEntry<>(p.getFirstName(), p),
new SimpleEntry<>(p.getLastName(), p)))
.collect(Collectors.groupingBy(SimpleEntry::getKey,
Collectors.mapping(SimpleEntry::getValue, Collectors.toSet())));
假设您向toString()
类添加了Person
方法,则可以使用以下命令查看结果:
List<Person> persons = Arrays.asList(
new Person("Bill", "Gates"),
new Person("Bill", "Steve"),
new Person("Steve", "Jobs"),
new Person("Steve", "Wozniac"));
// code above here
personByName.entrySet().forEach(System.out::println);
输出
Steve=[Steve Wozniac, Bill Steve, Steve Jobs]
Jobs=[Steve Jobs]
Bill=[Bill Steve, Bill Gates]
Wozniac=[Steve Wozniac]
Gates=[Bill Gates]
答案 1 :(得分:3)
例如,您可以合并两个Map<String, Set<Person>>
Map<String, Set<Person>> personByFirstName =
persons.stream()
.collect(Collectors.groupingBy(
Person::getFirstName,
Collectors.toCollection(HashSet::new))
);
persons.stream()
.collect(Collectors.groupingBy(Person::getLastName, Collectors.toSet()))
.forEach((str, set) -> personByFirstName.merge(str, set, (s1, s2) -> {
s1.addAll(s2);
return s1;
}));
// personByFirstName contains now all personByName
答案 2 :(得分:3)
一种方法是使用最新的JDK12的Collector.teeing
:
Map<String, List<Person>> result = persons.stream()
.collect(Collectors.teeing(
Collectors.groupingBy(Person::getFirstName,
Collectors.toCollection(ArrayList::new)),
Collectors.groupingBy(Person::getLastName),
(byFirst, byLast) -> {
byLast.forEach((last, peopleList) ->
byFirst.computeIfAbsent(last, k -> new ArrayList<>())
.addAll(peopleList));
return byFirst;
}));
Collectors.teeing
收集到两个单独的收集器,然后将结果合并为最终值。从文档中:
返回一个收集器,该收集器由两个下游收集器组成。传递给结果收集器的每个元素都由两个下游收集器处理,然后使用指定的合并功能将它们的结果合并到最终结果中。
因此,以上代码按名称收集地图,也按姓氏收集地图,然后通过迭代byLast
地图并将其每个条目合并到地图中,将两个地图合并为最终地图。 byFirst
通过Map.computeIfAbsent
方法进行映射。最后,返回byFirst
映射。
请注意,为了简化示例,我收集了一个Map<String, List<Person>>
而不是一个Map<String, Set<Person>>
。如果您确实需要集合图,则可以按照以下步骤操作:
Map<String, Set<Person>> result = persons.stream().
.collect(Collectors.teeing(
Collectors.groupingBy(Person::getFirstName,
Collectors.toCollection(LinkedHashSet::new)),
Collectors.groupingBy(Person::getLastName, Collectors.toSet()),
(byFirst, byLast) -> {
byLast.forEach((last, peopleSet) ->
byFirst.computeIfAbsent(last, k -> new LinkedHashSet<>())
.addAll(peopleSet));
return byFirst;
}));
请记住,如果需要将Set<Person>
作为地图的值,则Person
类必须一致地实现hashCode
和equals
方法。
答案 3 :(得分:0)
如果您想要一个真正的面向流的解决方案,请确保您不产生任何大的中间集合,否则大多数流的感觉就会消失。
如果只想过滤所有Steves,请先过滤,然后再收集:
,[^\-]*(?<=\d)-(?=\d)
如果您想使用流元素来做复杂的事情,例如将元素放入多个集合中,或在几个键下的映射中,只需使用+
消耗流,然后在其中编写所需的任何处理逻辑即可。
答案 4 :(得分:0)
您不能通过多个值来键入地图。对于您想要实现的目标,您有三种选择:
组合您的“ personByFirstName”和“ personByLastName”地图,您将获得重复的值(例如,比尔·盖茨将在地图上,位于Bill
键下,并且还将在地图中,位于{{{ 1}})。 @Andreas答案提供了一种基于流的良好方法。
使用像lucene这样的索引库,并按名字和姓氏索引所有Person对象。
流方法-在大型数据集上将不起作用,但是您可以流式处理集合并使用Gates
进行匹配:
filter
(我已经从内存中编写了语法,因此您可能需要对其进行调整)。
答案 5 :(得分:0)
如果我做对了,您希望每个人映射两次,一次映射为名字,一次映射为名字。 为此,您必须以某种方式使流增加一倍。假设Couple是一些现有的2元组(Guava或Vavr有一些不错的实现),您可以:
persons.stream()
.map(p -> new Couple(new Couple(p.firstName, p), new Couple(p.lastName, p)))
.flatMap(c -> Stream.of(c.left, c.right)) // Stream of Couple(String, Person)
.map(c -> new Couple(c.left, Arrays.asList(c.right)))
.collect(Collectors.toMap(Couple::getLeft, Couple::getRight, Collection::addAll));
我没有测试它,但是概念是:为每个人创建一个(名字,人),(姓氏,人)...的流,然后简单地映射每对夫妇的左值。 asList将有一个集合作为值。如果您需要Set chenge,请在最后一行加上.collect(Collectors.toMap(Couple::getLeft, c -> new HashSet(c.getRight), Collection::addAll))
答案 6 :(得分:0)
尝试Google Guava或我的图书馆Abacus-Util中的SetMultimap
SetMultimap<String, Person> result = Multimaps.newSetMultimap(new HashMap<>(), () -> new HashSet<>()); // by Google Guava.
// Or result = N.newSetMultimap(); // By Abacus-Util
persons.forEach(p -> {
result.put(p.getFirstName(), p);
result.put(p.getLastName(), p);
});