我最近进行了一次技术面试,并获得了有关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);
任务是使用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);
但是该解决方案被认为不是最佳/有效的方法。
您能否就此任务的更有效解决方案分享您的想法?
更新:我从另一个人那里得到另一种意见,即他将使用reducer(Stream.reduce()方法)。 但是我不知道这如何提高效率。你觉得呢?
答案 0 :(得分:4)
这是另一个。
// using SimpleEntry from java.util.AbstractMap
Set<Student> list = new HashSet<>(studentStream
.flatMap(student -> student.getSubjects().stream()
.map(subject -> new SimpleEntry<>(subject, student)))
.collect(Collectors.toMap(Entry::getKey, Entry::getValue, (l, r) -> Student.SENTINEL_VALUE)
.values());
list.remove(Student.SENTINEL_VALUE);
(有意使用前哨值,以下更多内容。)
步骤:
Set<Student> list = new HashSet<>(studentStream
我们正在从要收集的集合中创建一个HashSet。那是因为我们要摆脱重复的学生(在您的情况下,安东尼科有多个独特学科的学生)。
.flatMap(student -> student.subjects()
.map(subject -> new SimpleEntry(subject, student)))
我们正在将每个学生的科目映射到一个流中,但是首先我们将每个元素映射到一对,以该科目作为关键并重视学生的价值。这是因为我们需要保留主题与学生之间的联系。我正在使用AbstractMap.SimpleEntry
,但是,当然,您可以使用任何一对实现。
.collect(Collectors.toMap(Entry::getKey, Entry::getValue, (l, r) -> Student.SENTINEL_VALUE)
我们正在将值收集到地图中,将主题设置为关键,将学生设置为结果地图的值。我们传入第三个参数(BinaryOperator
)来定义如果发生键冲突应该发生的情况。 我们无法传递null
,因此我们使用前哨值 1 。
此时,我们通过将每个学科映射到一个学生(或者如果一个学科有多个学生,则为SENTINEL_VALUE
)来颠倒学生inverted学科的关系。
.values());
我们获取地图的值,得出具有唯一主题的所有学生的列表以及前哨值。
list.remove(Student.SENTINEL_VALUE);
剩下要做的就是摆脱哨兵值。
1 在这种情况下,我们不能使用null
。 Map的大多数实现在映射到null
的密钥与缺少该特定密钥之间没有区别。或者,更准确地说,当重映射函数返回HashMap
时,null
actively removes a node的合并方法。如果要避免使用哨兵值,则必须实现或拥有merge
方法,该方法可以像这样return (!containsKey(key) ? super.merge(key, value, remappingFunction) : put(key, null));
来实现。
答案 1 :(得分:2)
另一种解决方案。看起来有点类似于尤金。
Stream.of(stud1, stud2, stud3, stud4)
.flatMap( s -> s.getSubjects().stream().map( subj -> new AbstractMap.SimpleEntry<>( subj, s ) ) )
.collect( Collectors.groupingBy(Map.Entry::getKey) )
.entrySet().stream()
.filter( e -> e.getValue().size() == 1 )
.map( e -> e.getValue().get(0).getValue().getName() )
.collect( Collectors.toSet() );
答案 2 :(得分:1)
您可能可以通过以下更简单的方式进行操作:
Stream<Student> studentStream = Stream.of(stud1, stud2, stud3);
// collect all the unique subjects into a Set
Set<String> uniqueSubjects = studentStream
.flatMap(st -> st.getSubjects().stream()
.map(subj -> new AbstractMap.SimpleEntry<>(st.getName(), subj)))
// subject to occurence count map
.collect(Collectors.groupingBy(Map.Entry::getValue, Collectors.counting()))
.entrySet()
.stream()
.filter(x -> x.getValue() == 1) // occurs only once
.map(Map.Entry::getKey) // Q -> map keys are anyway unique
.collect(Collectors.toSet()); // ^^ ... any way to optimise this?(keySet)
// amongst the students, filter those which have any unique subject in their subject list
List<String> studentsStudyingUniqueSubjects = studentStream
.filter(stud -> stud.getSubjects().stream()
.anyMatch(uniqueSubjects::contains))
.map(Student::getName)
.collect(Collectors.toList());
答案 3 :(得分:1)
不是最易读的解决方案,但是您可以使用:
studentStream.flatMap(st -> st.getSubjects().stream().map(subj -> new SimpleEntry<>(st.getName(), subj)))
.collect(Collectors.toMap(
Entry::getValue,
x -> {
List<String> list = new ArrayList<>();
list.add(x.getKey());
return list;
},
(left, right) -> {
left.addAll(right);
return left;
}
))
.entrySet()
.stream()
.filter(x -> x.getValue().size() == 1)
.map(Entry::getValue)
.flatMap(List::stream)
.distinct()
.forEachOrdered(System.out::println);