我最近进行了技术访谈,并在Stream API上获得了小编码任务.
让我们考虑下一个输入:
public class Student {
private String name;
private List<String> subjects;
//getters and setters
}
Student stud1 = new Student("John", Arrays.asList("Math", "Chemistry"));
Student stud2 = new Student("Peter", Arrays.asList("Math", "History"));
Student stud3 = new Student("Antony", Arrays.asList("Music", "History", "English"));
Stream<Student> studentStream = Stream.of(stud1, stud2, stud3);
Run Code Online (Sandbox Code Playgroud)
任务是使用Stream API查找具有独特主题的学生.
因此对于提供的输入预期结果(忽略顺序)是[John, Anthony].
我使用自定义收集器呈现了解决方案:
Collector<Student, Map<String, Set<String>>, List<String>> studentsCollector = Collector.of(
HashMap::new,
(container, student) -> student.getSubjects().forEach(
subject -> container
.computeIfAbsent(subject, s -> new HashSet<>())
.add(student.getName())),
(c1, c2) -> c1,
container -> container.entrySet().stream()
.filter(e -> e.getValue().size() == 1)
.map(e -> e.getValue().iterator().next())
.distinct()
.collect(Collectors.toList())
);
List<String> studentNames = studentStream.collect(studentsCollector);
Run Code Online (Sandbox Code Playgroud)
但该解决方案被认为不是最佳/有效的.
您能否就这项任务更有效的解决方案分享您的想法?
更新:我从一个人那里得到另一个意见,他会使用reducer(Stream.reduce()方法).但我无法理解这会如何提高效率.你怎么看?
这是另一张。
\n\n// using SimpleEntry from java.util.AbstractMap\nSet<Student> list = new HashSet<>(studentStream\n .flatMap(student -> student.getSubjects().stream()\n .map(subject -> new SimpleEntry<>(subject, student)))\n .collect(Collectors.toMap(Entry::getKey, Entry::getValue, (l, r) -> Student.SENTINEL_VALUE)\n .values());\nlist.remove(Student.SENTINEL_VALUE);\nRun Code Online (Sandbox Code Playgroud)\n\n(有意使用哨兵值,更多信息见下文。)
\n\n步骤:
\n\nSet<Student> list = new HashSet<>(studentStream\nRun Code Online (Sandbox Code Playgroud)\n\n我们正在从我们要收集的 Collection 中创建一个 HashSet。那是因为我们想摆脱重复的学生(具有多个独特科目的学生,在你的例子中是安东尼)。
.flatMap(student -> student.subjects()\n .map(subject -> new SimpleEntry(subject, student)))\nRun Code Online (Sandbox Code Playgroud)\n\n我们将每个学生的科目平面映射到一个流中,但首先我们将每个元素映射到一对,以科目为键,以学生为值。这是因为我们需要保留主体和学生之间的关联。我正在使用AbstractMap.SimpleEntry,但是当然,您可以使用对的任何实现。
.collect(Collectors.toMap(Entry::getKey, Entry::getValue, (l, r) -> Student.SENTINEL_VALUE)\nRun Code Online (Sandbox Code Playgroud)\n\n我们将这些值收集到地图中,将主题设置为键,将学生设置为结果地图的值。我们传入第三个参数 (a BinaryOperator) 来定义发生按键冲突时应该发生的情况。我们无法传入null,因此我们使用哨兵值1。
\n此时,我们通过将每个科目映射到一个学生(或者如果SENTINEL_VALUE一个科目有多个学生)来反转关系学生 \xe2\x86\x94 科目。
.values());\nRun Code Online (Sandbox Code Playgroud)\n\n我们获取地图的值,生成具有独特主题的所有学生的列表,加上哨兵值。
list.remove(Student.SENTINEL_VALUE);\nRun Code Online (Sandbox Code Playgroud)\n\n剩下要做的唯一一件事就是摆脱哨兵值。
1null在这种情况下我们不能使用。Map 的大多数实现不区分映射到该特定键的键null或不存在该特定键。或者,更准确地说,当重映射函数返回时,merge 方法会HashMap 主动删除节点null。如果我们想避免哨兵值,那么我们必须实现或拥有merge方法,可以像这样实现return (!containsKey(key) ? super.merge(key, value, remappingFunction) : put(key, null));: