Three lists, two lookups and one groupingBy. The join table is the one you stream - the other two are indexes, because they are the sides being looked up rather than the side being walked.
record Student(int id, String name) {}
record Course(int id, String title) {}
record Enrol(int studentId, int courseId) {}
var students = List.of(new Student(1, "Ava"), new Student(2, "Ben"));
var courses = List.of(new Course(10, "Streams"), new Course(20, "Kafka"));
var enrols = List.of(new Enrol(1, 10), new Enrol(1, 20), new Enrol(2, 20));
Map<Integer, String> studentName = students.stream().collect(Collectors.toMap(Student::id, Student::name));
Map<Integer, String> courseTitle = courses.stream().collect(Collectors.toMap(Course::id, Course::title));
Map<String, List<String>> byStudent = enrols.stream()
.collect(Collectors.groupingBy(e -> studentName.get(e.studentId()), TreeMap::new,
Collectors.mapping(e -> courseTitle.get(e.courseId()), Collectors.toList())));
byStudent.forEach((s, cs) -> System.out.println(s + " -> " + cs));
Ava -> [Streams, Kafka]
Ben -> [Kafka]
Run this yourself in the Online Java Compiler, spin up a live REST API in the API Sandbox, or practise with Java interview questions.
Published 2026-09-20