I have list of students.
I want to return list of objects StudentResponse classes that has the course and the list of students for the course.
So I can write which gives me a map
Map<String, List<Student>> studentsMap = students.stream().
.collect(Collectors.groupingBy(Student::getCourse,
Collectors.mapping(s -> s, Collectors.toList()
)));
Now I have to iterate through the map again to create a list of objects of StudentResponse class which has the Course and List:
class StudentResponse {
String course;
Student student;
// getter and setter
}
Is there a way to combine these two iterations?
Not exactly what you've asked, but here's a compact way to accomplish what you want, just for completeness:
Map<String, StudentResponse> map = new LinkedHashMap<>();
students.forEach(s -> map.computeIfAbsent(
s.getCourse(),
k -> new StudentResponse(s.getCourse()))
.getStudents().add(s));
This assumes StudentResponse has a constructor that accepts the course as an argument and a getter for the student list, and that this list is mutable (i.e. ArrayList) so that we can add the current student to it.
While the above approach works, it clearly violates a fundamental OO principle, which is encapsulation. If you are OK with that, then you're done. If you want to honor encapsulation, then you could add a method to StudentResponse to add a Student instance:
public void addStudent(Student s) {
students.add(s);
}
Then, the solution would become:
Map<String, StudentResponse> map = new LinkedHashMap<>();
students.forEach(s -> map.computeIfAbsent(
s.getCourse(),
k -> new StudentResponse(s.getCourse()))
.addStudent(s));
This solution is clearly better than the previous one and would avoid a rejection from a serious code reviewer.
Both solutions rely on Map.computeIfAbsent, which either returns a StudentResponse for the provided course (if there exists an entry for that course in the map), or creates and returns a StudentResponse instance built with the course as an argument. Then, the student is being added to the internal list of students of the returned StudentResponse.
Finally, your StudentResponse instances are in the map values:
Collection<StudentResponse> result = map.values();
If you need a List instead of a Collection:
List<StudentResponse> result = new ArrayList<>(map.values());
Note: I'm using LinkedHashMap instead of HashMap to preserve insertion-order, i.e. the order of the students in the original list. If you don't have such requirement, just use HashMap.
Probably way overkill but it was a fun exercise :) You could implement your own Collector:
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
import java.util.function.*;
import java.util.stream.Collector;
import java.util.stream.Collectors;
public class StudentResponseCollector implements Collector<Student, Map<String, List<Student>>, List<StudentResponse>> {
#Override
public Supplier<Map<String, List<Student>>> supplier() {
return () -> new ConcurrentHashMap<>();
}
#Override
public BiConsumer<Map<String, List<Student>>, Student> accumulator() {
return (store, student) -> store.merge(student.getCourse(),
new ArrayList<>(Arrays.asList(student)), combineLists());
}
#Override
public BinaryOperator<Map<String, List<Student>>> combiner() {
return (x, y) -> {
x.forEach((k, v) -> y.merge(k, v, combineLists()));
return y;
};
}
private <T> BiFunction<List<T>, List<T>, List<T>> combineLists() {
return (students, students2) -> {
students2.addAll(students);
return students2;
};
}
#Override
public Function<Map<String, List<Student>>, List<StudentResponse>> finisher() {
return (store) -> store
.keySet()
.stream()
.map(course -> new StudentResponse(course, store.get(course)))
.collect(Collectors.toList());
}
#Override
public Set<Characteristics> characteristics() {
return EnumSet.of(Characteristics.UNORDERED);
}
}
Given Student and StudentResponse:
public class Student {
private String name;
private String course;
public Student(String name, String course) {
this.name = name;
this.course = course;
}
public String getName() {
return name;
}
public String getCourse() {
return course;
}
public String toString() {
return name + ", " + course;
}
}
public class StudentResponse {
private String course;
private List<Student> studentList;
public StudentResponse(String course, List<Student> studentList) {
this.course = course;
this.studentList = studentList;
}
public String getCourse() {
return course;
}
public List<Student> getStudentList() {
return studentList;
}
public String toString() {
return course + ", " + studentList.toString();
}
}
Your code where you collect your StudentResponses can now be very short and elegant ;)
public class StudentResponseCollectorTest {
#Test
public void test() {
Student student1 = new Student("Student1", "foo");
Student student2 = new Student("Student2", "foo");
Student student3 = new Student("Student3", "bar");
List<Student> studentList = Arrays.asList(student1, student2, student3);
List<StudentResponse> studentResponseList = studentList
.stream()
.collect(new StudentResponseCollector());
assertEquals(2, studentResponseList.size());
}
}
Just iterate over the entry set and map each entry to a StudentResponse:
List<StudentResponse> responses = studentsMap.entrySet()
.stream()
.map(e -> new StudentResponse(e.getKey(), e.getValue()))
.collect(Collectors.toList());
First, your downstream collector (mapping) is redundant and hence you can simplify your code by using the groupingBy overload without a downstream collector.
Given a List<T> as the source, after using the groupingBy overload taking a classifier alone the result map is Map<K, List<T>> so the mapping operation can be avoided.
As for your question, you can use collectingAndThen:
students.stream()
.collect(collectingAndThen(groupingBy(Student::getCourse),
m -> m.entrySet()
.stream()
.map(a -> new StudentResponse(a.getKey(), a.getValue()))
.collect(Collectors.toList())));
collectingAndThen basically:
Adapts a Collector to perform an additional finishing transformation.
This can be done in a very concise manner using the jOOλ library and its Seq.grouped method:
List<StudentResponse> responses = Seq.seq(students)
.grouped(Student::getCourse, Collectors.toList())
.map(Tuple.function(StudentResponse::new))
.toList();
It assumes StudentResponse has a constructor StudentResponse(String course, List<Student> students), and forwards to this constructor using the following Tuple.function overload.
As you can see from my other answer as well as shmosel's answer, you'll eventually need to invoke studentsMap.entrySet() to map each Entry<String, List<String>> in the resulting map to StudentResponse objects.
Another approach you could take is the toMap way; i.e.
Collection<StudentResponse> result = students.stream()
.collect(toMap(Student::getCourse,
v -> new StudentResponse(v.getCourse(),
new ArrayList<>(singletonList(v))),
StudentResponse::merge)).values();
This essentially groups the Student object by their course (Student::getCourse) as with the groupingBy collector; then in the valueMapper function maps from Student to a StudentResponse and finally in the merge function utilises StudentResponse::merge in the case of a key collision.
The above has a dependency on the StudentResponse class having at least the following fields, constructor and methods:
class StudentResponse {
StudentResponse(String course, List<Student> students) {
this.course = course;
this.students = students;
}
private List<Student> getStudents() { return students; }
StudentResponse merge(StudentResponse another){
this.students.addAll(another.getStudents());
// maybe some addition merging logic in the future ...
return this;
}
private String course;
private List<Student> students;
}
Related
In Java, how do I map and obtain a class member list, using a list within list.
public class CustomerSales {
public List<Product> productList;
....
}
public class Product {
public List<ProductSubItem> productSubItemList
....
}
public class ProductSubItem {
public String itemName;
Attempt:
However, this does not get the itemName. I'm looking for a clean efficient method, ideally may want to attempt 4-5 levels deep, however question has only 3 for simplicity, etc
List<String> itemNameList = customerSales.productList.stream()
.map(p -> p.productSubItemList())
.collect(Collectors.toList());
I'm using using Java 8.
Attempted using this resource: still not luck, How can I get a List from some class properties with Java 8 Stream?
Convert sub-list to a stream and use flatMap to convert stream of streams of elements to stream of elements.
Example:
package x.mvmn.demo;
import java.util.Arrays;
import java.util.List;
import java.util.stream.Collectors;
public class Demo {
public static class CustomerSales {
public List<Product> productList;
}
public static class Product {
public List<ProductSubItem> productSubItemList;
public List<ProductSubItem> getProductSubItemList() {
return productSubItemList;
}
}
public static class ProductSubItem {
public String itemName;
public ProductSubItem(String itemName) {
this.itemName = itemName;
}
public String getItemName() {
return itemName;
}
}
public static void main(String args[]) throws Exception {
// Setup mock data
CustomerSales customerSales = new CustomerSales();
Product p1 = new Product();
p1.productSubItemList = Arrays.asList(new ProductSubItem("p1 item one"), new ProductSubItem("p1 item two"));
Product p2 = new Product();
p2.productSubItemList = Arrays.asList(new ProductSubItem("p2 item one"), new ProductSubItem("p2 item two"));
customerSales.productList = Arrays.asList(p1, p2);
// Get list of item names
System.out.println(customerSales.productList.stream().map(Product::getProductSubItemList).flatMap(List::stream)
.map(ProductSubItem::getItemName).collect(Collectors.toList()));
// Alternative syntax
System.out.println(customerSales.productList.stream().flatMap(product -> product.productSubItemList.stream())
.map(subItem -> subItem.itemName).collect(Collectors.toList()));
}
}
Output:
[p1 item one, p1 item two, p2 item one, p2 item two]
[p1 item one, p1 item two, p2 item one, p2 item two]
Looks like you need to use flatMap:
https://www.baeldung.com/java-difference-map-and-flatmap
List<String> itemNameList = customerSales.productList.stream().map(p -> p.productSubItemList().stream()).collect(Collectors.toList());
Here is another example of flattening list of lists
https://www.baeldung.com/java-flatten-nested-collections
public <T> List<T> flattenListOfListsStream(List<List<T>> list) {
return list.stream()
.flatMap(Collection::stream)
.collect(Collectors.toList());
}
We can re-write the #mvmn answer using below.
public class Demo {
public static class CustomerSales {
public List<Product> productList;
public Stream<Product> getProductStream() {
return !CollectionUtils.isEmpty(productList) ? productList.stream() : Stream.empty();
}
}
public static class Product {
public List<ProductSubItem> productSubItemList;
public List<ProductSubItem> getProductSubItemList() {
return productSubItemList;
}
public Stream<ProductSubItem> getProductSubItemStream() {
return !CollectionUtils.isEmpty(productSubItemList) ? productSubItemList.stream() : Stream.empty();
}
}
public static class ProductSubItem {
public String itemName;
public ProductSubItem(String itemName) {
this.itemName = itemName;
}
public String getItemName() {
return itemName;
}
}
public static void main(String args[]) throws Exception {
// Setup mock data
CustomerSales customerSales = new CustomerSales();
Product p1 = new Product();
p1.productSubItemList = Arrays.asList(new ProductSubItem("p1 item one"), new ProductSubItem("p1 item two"));
Product p2 = new Product();
p2.productSubItemList = Arrays.asList(new ProductSubItem("p2 item one"), new ProductSubItem("p2 item two"));
customerSales.productList = Arrays.asList(p1, p2);
// With Utility method in our DTOs
System.out.println(customerSales.getProductStream()
.flatMap(Product::getProductSubItemStream)
.map(ProductSubItem::getItemName)
.collect(Collectors.toList()));
// Java16+
System.out.println(customerSales.getProductStream()
.flatMap(Product::getProductSubItemStream)
.map(ProductSubItem::getItemName)
.toList());
}
}
In case when you need to perform a one-to-one transformation - use map() operation. For one-to-many transformations, use flatMap() operation.
flatMap() expects a Function which consumes a stream and produces a Stream of resulting type.
Java 16 offers one more alternative for one-to-many transformations, namely mapMulti(), which would be more beneficial in terms of performance (because it doesn't require generating a new Stream internally like flatMap does) if the associated collection is expected to contain of a few elements or even might be empty. If you can't predict the number of elements in the resulting stream, then flatMap() is the way to go.
However, this does not get the itemName. looking for a clean efficient method, ideally may want to attempt 4-5 levels deep
To tackle the scenario, you've described, you can introduce a uniform method producing a Function which turns a stream element into another stream (a Function required by the flatMap), and utilize this method in the Stream for the purpose of conciseness.
public static <T, R> Function<T, Stream<R>> flatten(
Function<T, Collection<R>> mapper
) {
return t -> mapper.apply(t).stream();
}
Now let's consider the following object-graph with a diversity of nested collections:
ClassA -> List<ClassB>
ClassB -> Set<ClassC>
ClassC -> Map<Integer, ClassD> mapD
ClassD -> Queue<ClassE> queueE
ClassE -> String name
If we have a List of ClassA instances, the List name from ClassE can be obtained in the following way:
List<String> names = aList.stream() // Stream<ClassA>
.flatMap(flatten(ClassA::getListB)) // Stream<ClassB>
.flatMap(flatten(ClassB::getSetC)) // Stream<ClassC>
.flatMap(flatten(c -> c.getMapD().values())) // Stream<ClassD>
.flatMap(flatten(ClassD::getQueueE)) // Stream<ClassE>
.map(ClassE::getName) // Stream<String>
.toList(); // for Java 16 or collect(Collectors.toList()) for earlier versions
Here's a complete example.
Dummy classes:
public class ClassA {
private List<ClassB> listB;
// getters, all-args constructor
}
public class ClassB {
private Set<ClassC> setC;
// getters, all-args constructor
}
public class ClassC {
private Map<Integer, ClassD> mapD;
// getters, all-args constructor
}
public class ClassD {
private Queue<ClassE> queueE;
// getters, all-args constructor
}
public class ClassE {
private String name;
// getters, all-args constructor
}
main()
public static void main(String[] args) {
List<ClassA> aList = List.of(new ClassA(
List.of(
new ClassB(
Set.of(
new ClassC(Map.of(
1, new ClassD(new ArrayDeque<>(List.of(new ClassE("Alice")))),
2, new ClassD(new ArrayDeque<>(List.of(new ClassE("Bob"))))
)),
new ClassC(Map.of(1, new ClassD(new ArrayDeque<>(List.of(new ClassE("Carol"))))))
)
)
)
));
List<String> names = aList.stream()
.flatMap(flatten(ClassA::getListB))
.flatMap(flatten(ClassB::getSetC))
.flatMap(flatten(c -> c.getMapD().values()))
.flatMap(flatten(ClassD::getQueueE))
.map(ClassE::getName)
.toList(); // for Java 16 or collect(Collectors.toList()
System.out.println(names);
}
Output:
[Alice, Bob, Carol]
A link to Online Demo
I am learning some cool stuff about Java StreamAPI and got stuck'd into one problem:
I have a use case where I want to return newly create hashmap using stream. I am using the traditional way of defining a HashMap in the function and adding up values to it.
I was more interested in knowing some better ways to achieve so
public Map<String,String> constructMap(List<CustomObject> lists){
Map<String,String> newMap = new HashMap<>();
lists.stream().filter(x->x!=null).forEach(map -> newMap.putAll(map.getSomeMapping(studentId));
return newMap;
}
Can I achieve this using reduceAPI or any other way without having to create a custom hashmap (directly return the stream one liner)?
Edit:
for Example:
CustomObject c1 = new CustomObject("bookId1", "book1");
CustomObject c2 = new CustomObject("bookId2", "book2");
List<CustomObject> lists = new ArrayList();
lists.add(c1); lists.add(c2);
The getter in class CustomObject is: getSomeMapping(input)
which return Map<BookID, Book>
Expected output:
{"bookId1" : "book1", "bookId2" : "book2"}
Edit2:
One more thing to clarify, the CustomObject class does not have any other getters defined. The only function I have access to is getSomeMapping(input) which returns a mapping
thank you for any help.
Assuming CustomObject has the following structure and getter getSomeMapping which returns a map:
class CustomObject {
private Map<String, String> someMapping;
public CustomObject(String key, String value) {
this.someMapping = new HashMap<>();
someMapping.put(key, value);
}
public Map<String, String> getSomeMapping() {
return someMapping;
}
}
Then constructMap will use already mentioned Collectors.toMap after flattening the entries in someMapping:
public static Map<String, String> constructMap(List<CustomObject> list) {
return list.stream()
.filter(Objects::nonNull)
.map(CustomObject::getSomeMapping)
.flatMap(map -> map.entrySet().stream())
.collect(Collectors.toMap(
Map.Entry::getKey,
Map.Entry::getValue,
(v1, v2) -> v1, // merge function to handle possible duplicates
LinkedHashMap::new
));
}
Test
CustomObject c1 = new CustomObject("bookId1", "book1");
CustomObject c2 = new CustomObject("bookId2", "book2");
List<CustomObject> lists = Arrays.asList(c1, c2);
Map<String, String> result = constructMap(lists);
System.out.println(result);
Output:
{bookId1=book1, bookId2=book2}
You can use Collectors#toMap(Function<? super T,? extends K> keyMapper, Function<? super T,? extends U> valueMapper, BinaryOperator<U> mergeFunction, Supplier<M> mapSupplier) to create a LinkedHashMap using the bookId as the key, and bookName as the value.
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.stream.Collectors;
class CustomObject {
private String bookId;
private String bookName;
public CustomObject(String bookId, String bookName) {
this.bookId = bookId;
this.bookName = bookName;
}
public String getBookId() {
return bookId;
}
public String getBookName() {
return bookName;
}
// Other stuff e.g. equals, hashCode etc.
}
public class Main {
public static void main(String[] args) {
List<CustomObject> list = List.of(new CustomObject("bookId1", "book1"), new CustomObject("bookId2", "book2"));
System.out.println(constructMap(list));
}
public static Map<String, String> constructMap(List<CustomObject> list) {
return list.stream()
.filter(Objects::nonNull)
.collect(Collectors.toMap(CustomObject::getBookId, CustomObject::getBookName, (a, b) -> a, LinkedHashMap::new));
}
}
Output:
{bookId1=book1, bookId2=book2}
Note: The mergeFunction, (a, b) -> a resolves the collision between values associated with the same key e.g. in this case, we have defined it to select a out of a and b having the same key. If the order of elements does not matter, you can use Collectors#toMap(Function<? super T,? extends K> keyMapper, Function<? super T,? extends U> valueMapper) as shown below:
public static Map<String, String> constructMap(List<CustomObject> list) {
return list.stream()
.filter(Objects::nonNull)
.collect(Collectors.toMap(CustomObject::getBookId, CustomObject::getBookName));
}
A sample output:
{bookId2=book2, bookId1=book1}
To turn a stream into a map you're better off using collect(). For instance:
public Map<String,String> toMap(List<Entry<String,String>> entries) {
return entries.stream().collect(Collectors.toMap(Entry::getKey, Entry::getValue));
}
Or if your keys are non-unique and you want the values to be combined as a list:
public Map<String,List<CustomObject>> toMap(List<CustomObject> entries) {
return entries.stream().collect(Collectors.groupingBy(CustomObject::getKey));
}
Look into [Collectors.toMap()] 1. This can return the items as a new Map.
lists.stream().filter(x->x!=null).collect(Collectors.toMap(CustomObject::getMapKey(), CustomObject::getMapValue()));
getMapKey and getMapValue are here methods returning the key and value of the CustomObject for the map. Instead of using simple getters it might also be necessary to execute some more advanced logic.
lists.stream().filter(x->x!=null).collect(Collectors.toMap(l -> {...; return key;}, l -> { ...; return value;}));
Let's assume your CustomObject class has getters to retrieve a school id with a name. You could do it like this. I declared it static as it does not appear to depend on instance fields.
public static Map<String,String> constructMap(List<CustomObject> lists){
return lists.stream()
.filter(Objects::nonNull)
.collect(Collectors.toMap(CustomObject::getName, CustomObject::getID));
}
This presumes that names and Id's are one-to-one, as this does not handle duplicate keys.
I know this is a noob a question, but I couldn't find a simpe answer anywhere else. Question is: I need to write a method that returns a SortedMap, so a tree map should work just fine.
I have a HashMap< String, Skill>, the Skill class has both the methods getName and getNumApplicants and I need to return a SortedMap<String, Long>, with the name of the skill as a key and the number of applicants as value. This is where I stand:
private Map<String,Skill> skillMap = new HashMap<>();
public SortedMap<String, Long> skill_nApplicants() {
return skillMap.values().stream().collect(...);
}
This is the Skill class
public class Skill {
private String name;
private List <Position> reqPosition = new ArrayList<>();
private Long numApplicants;
public void plusOneApplicant() {
this.numApplicants++;
}
public Long getNumApplicants() {
return numApplicants;
}
public Skill(String name) {
super();
this.name = name;
this.numApplicants = 0L;
}
public String getName() {
return name;
}
public List<Position> getPositions() {
return reqPosition;
}
public void addReqPosition(Position p) {
this.reqPosition.add(p);
return;
}
}
I know this should be very easy, I just have a very hard time in understanding this all thing.
Don't collect the data to a HashMap first, then convert to a TreeMap. Collect the data directly to a TreeMap by using the overloaded toMap(keyMapper, valueMapper, mergeFunction, mapSupplier) method that allows you to specify which Map to create (4th parameter).
public SortedMap<String, Long> skill_nApplicants() {
return skillMap.values().stream().collect(Collectors.toMap(
Skill::getName,
Skill::getNumApplicants,
Math::addExact, // only called if duplicate names can occur
TreeMap::new
));
}
This is how you can do it
public SortedMap<String, Long> skill_nApplicants(Map<String, Skill> skillMap) {
Map<String, Long> result = skillMap.values().stream().collect(Collectors.toMap(Skill::getName, Skill::getNumApplicants));
return new TreeMap<>(result);
}
If, in your stream, you don't have two (or more) values which should be mapped with the same key, then you can avoid to use a Collector at all (and thus you don't need to think about a merge function).
All you need to do is to simply add each skill to the map with a forEach:
public SortedMap<String, Long> skill_nApplicants() {
Map<String, Long> result = new TreeMap<>();
skillMap.values()
.forEach((skill) -> result.put(skill.getName(), skill.getNumApplicants());
return result;
}
You can wrap result with Collections.unmodifiableSortedMap if you want to return an unmodifiable map.
class Student {
List<Integer> grades
}
I want grouping students by grades but groupingBy would work if each student had only one grade. Is any other lamba way to do it? I expect result like
Map<Integer, List<Student>>
groupingBy will do the job if you help it: you might find an equivalent version of this in the Javadoc for groupingBy.
final List<Student> students = ...;
// #formatter:off
students.stream()
.flatMap(student -> student.grades.stream()
.map(grade -> new StudentGrade(student, grade)))
.collect(groupingBy(StudentGrade::getGrade, mapping(StudentGrade::getStudent, toList())));
// #formatter:on
Now, you get a Map<Integer, List<Student>>. It is up to you to filter duplicates.
You will need these imports:
import static java.util.stream.Collectors.groupingBy;
import static java.util.stream.Collectors.mapping;
import static java.util.stream.Collectors.toList;
Class StudentGrade is simple:
class StudentGrade {
private final Student student;
private final Integer grade;
public StudentGrade(Student student, Integer grade) {
this.student = student;
this.grade = grade;
}
public Student getStudent() {return student;}
public Integer getGrade() {return grade;}
}
Map<Integer, List<Student>> collect = students.stream()
.flatMap(student -> student.grades.stream()
.map(grade -> new AbstractMap.SimpleEntry<>(grade,
student)))
.collect(Collectors.groupingBy(entry -> entry.getKey(),
Collectors.mapping(Entry::getValue, Collectors.toList())));
Because Java do not have Pair tuple implementation I used AbstractMap.SimpleEntry
I have a list of objects that I need to transform to a map where the keys are a function of each element, and the values are lists of another function of each element. Effectively this is grouping the elements by a function of them.
For example, suppose a simple element class:
class Element {
int f1() { ... }
String f2() { ... }
}
and a list of these:
[
{ f1=100, f2="Alice" },
{ f1=200, f2="Bob" },
{ f1=100, f2="Charles" },
{ f1=300, f2="Dave" }
]
then I would like a map as follows:
{
{key=100, value=[ "Alice", "Charles" ]},
{key=200, value=[ "Bob" ]},
{key=300, value=[ "Dave" ]}
}
Can anyone suggest a succinct way of doing this in Java without iterating? A combination of LambdaJ's group method with Guava's Maps.transform nearly gets there, but group doesn't generate a map.
Guava has Maps.uniqueIndex(Iterable values, Function keyFunction) and Multimaps.index(Iterable values, Function keyFunction), but they don't transform the values. There are some requests to add utility methods that do what you want, but for now, you'll have to roll it yourself using Multimaps.index() and Multimaps.transformValues():
static class Person {
private final Integer age;
private final String name;
public Person(Integer age, String name) {
this.age = age;
this.name = name;
}
public Integer getAge() {
return age;
}
public String getName() {
return name;
}
}
private enum GetAgeFunction implements Function<Person, Integer> {
INSTANCE;
#Override
public Integer apply(Person person) {
return person.getAge();
}
}
private enum GetNameFunction implements Function<Person, String> {
INSTANCE;
#Override
public String apply(Person person) {
return person.getName();
}
}
public void example() {
List<Person> persons = ImmutableList.of(
new Person(100, "Alice"),
new Person(200, "Bob"),
new Person(100, "Charles"),
new Person(300, "Dave")
);
ListMultimap<Integer, String> ageToNames = getAgeToNamesMultimap(persons);
System.out.println(ageToNames);
// prints {100=[Alice, Charles], 200=[Bob], 300=[Dave]}
}
private ListMultimap<Integer, String> getAgeToNamesMultimap(List<Person> persons) {
ImmutableListMultimap<Integer, Person> ageToPersons = Multimaps.index(persons, GetAgeFunction.INSTANCE);
ListMultimap<Integer, String> ageToNames = Multimaps.transformValues(ageToPersons, GetNameFunction.INSTANCE);
// Multimaps.transformValues() returns a *lazily* transformed view of "ageToPersons"
// If we want to iterate multiple times over it, it's better to create a copy
return ImmutableListMultimap.copyOf(ageToNames);
}
A re-usable utility method could be:
public static <E, K, V> ImmutableListMultimap<K, V> keyToValuesMultimap(Iterable<E> elements, Function<E, K> keyFunction, Function<E, V> valueFunction) {
ImmutableListMultimap<K, E> keysToElements = Multimaps.index(elements, keyFunction);
ListMultimap<K, V> keysToValuesLazy = Multimaps.transformValues(keysToElements, valueFunction);
return ImmutableListMultimap.copyOf(keysToValuesLazy);
}
I guess we could improve the generics in the signature by using Function<? extends E, K> or something, but I don't have the time to delve further...
Now with Java8 you can do it like:
static class Element {
final int f1;
final String f2;
Element(int f1, String f2) {
this.f1 = f1;
this.f2 = f2;
}
int f1() { return f1;}
String f2() { return f2; }
}
public static void main(String[] args) {
List<Element> elements = new ArrayList<>();
elements.add(new Element(100, "Alice"));
elements.add(new Element(200, "Bob"));
elements.add(new Element(100, "Charles"));
elements.add(new Element(300, "Dave"));
elements.stream()
.collect(Collectors.groupingBy(
Element::f1,
Collectors.mapping(Element::f2, Collectors.toList())
))
.forEach((f1, f2) -> System.out.println("{"+f1.toString() + ", value="+f2+"}"));
}
There has been some discussion in adding one API in Apache's CollectionUtils to transform a List to Map, but then I dont see any reason for not using a foreach contruct, Is there any problem that you are facing ? Transform will do the same thing which you can get easily by foreach, looping cannot be avoided.
EDIT:
Here is the link to discussion in Apache's forum http://apache-commons.680414.n4.nabble.com/Convert-List-to-Map-td747218.html
I don't know why you don't want to iterate. JDK does not support transform, but you can implement it yourself.
If you are worried about the performance, even if JDK had supported it, it would have also iterated it.