Building immutable map using a list of objects - java

I have a list of students and a delegate that has a function to get a list of servers for a given student (getServersForStudent(student)). I would like to create a map for a list of students indexed for each server. A student can be in many servers.
private Map<Server, Student> getStudentsByServer(List<Student> students) {
Map<Server, List<Student>> map = new HashMap<>();
students.forEach(student ->
List<Server> servers = delegate.getServersForStudent(student);
if (!servers.isEmpty()) {
servers.forEach(server -> map.putIfAbsent(server, new ArrayList<>()).add(student));
}
);
return map
}
This works perfectly, but I would like to refactor this to use streams in order to make an immutable collection instead. I tried doing this with groupingBy, but I wasn't able to get the right result:
students
.stream()
.collect(
Collectors.groupingBy(
student -> delegate.getServersForStudent(student);
Collectors.mapping(Function.identity(), Collectors.toList())
)
);
This grouping doesn't have the same result as above since it is grouping by lists. Does anyone have any suggestions on how to best do this with Java streams?

Streams are not required to return an immutable collection; simply copy your collection into an immutable one in the end or wrap it in an unmodifiable wrapper:
private Map<Server, Student> getStudentsByServer(final List<Student> students) {
final Map<Server, List<Student>> map = new HashMap<>();
for (final Student student : students) {
for (final Server server : delegate.getServersForStudent(student)) {
map.computeIfAbsent(server, new ArrayList<>())
.add(student);
}
);
// wrap:
// return Collections.unmodifiableMap(map);
// or copy:
return Map.copyOf(map);
}
If you really want to do it stream-based, you have to first create a stream of tuples (student, server), which you can then group. Java does not have a specific tuple type, but short of creating a custom type, you can misuse Map.Entry<K, V> for that:
students
.stream()
.flatMap(student -> delegate.getServersForStudent(student)
.stream()
.map(server -> Map.entry(student, server)))
.collect(
Collectors.groupingBy(
tuple -> tuple.getValue(),
Collectors.mapping(
tuple -> tuple.getKey(),
Collectors.toList())));
Note that the collection return by Collectors don't make any promises about the (im)mutability. If you require immutability, you have to add another collection step using Collectors.collectingAndThen:
.collect(
Collectors.collectingAndThen(
Collectors.groupingBy(
tuple -> tuple.getValue(),
Collectors.mapping(
tuple -> tuple.getKey(),
Collectors.toList())),
Map::copyOf);
// or wrap with: Collections::unmodifiableMap
And it's definitely worthwhile to mention that an unmodifiable/immutable map as in the example above still allows to modify the list of servers, because that Collectors.toList() currently returns an ArrayList. If you require the value of the map to be immutable too, you have to take care of that yourself, e.g. using Collectors.toUnmodifiableList or by copying/wrapping the list again.

Related

Map objects by occurrences in list of pairs

I need to map a list of pairs of objects into <ocurrences, list of Objs with those ocurrences>, I've tried using streams directly on the input list of pairs but I'm still kind of new to java and couldn't figure it out, so I was trying to do something like this, but it's probably not close to the best way to do it.
public Map<Integer,ArrayList<Obj>> numBorders(List<Pair<Obj,Obj>> lf) {
Map<Integer,ArrayList<Obj>> nBorders = new HashMap<>();
List<Obj> list = new ArrayList<>();
for(Pair<Obj, Obj> pair : lf) {
list.add(pair.getKey());
list.add(pair.getValue());
}
nBorders = list.stream().collect(Collectors.groupingBy(...);
return nBorders;
}
so for example, for lf = {(o1,o2),(o3,o2),(o5,o4),(o4,o1),(o3,o4),(o7,o1),(o5,o8),(o3,o10),(o4,o5),(o3,o7),(o9,o8)} the result should be {(1,{o9,o10}),(2,{o2,o7,o8,}),(3,{o1,o5}),(4,{o3,o4})}.
I'm really confused on how to do this, if someone could help, I'd appreciate it, thanks.
This can be done this way:
create a stream from the pairs to concatenate first/second values using Stream::flatMap
count the occurrences - build an intermediate map <Obj, Integer> using Collectors.groupingBy + Collectors.summingInt (to keep integer)
create an inverse map <Integer, List> from the stream of the entries in the intermediate map using Collectors.groupingBy + Collectors.mapping
Optionally, if an order in the resulting map is critical, a LinkedHashMap may be created from the entries of the intermediate frequency map sorted by value.
public Map<Integer,ArrayList<Obj>> numBorders(List<Pair<Obj,Obj>> lf) {
return lf.stream() // Stream<Pair>
.flatMap(p -> Stream.of(p.getKey(), p.getValue())) // Stream<Obj>
.collect(Collectors.groupingBy(
obj -> obj,
Collectors.summingInt(obj -> 1)
)) // Map<Obj, Integer>
.entrySet()
.stream() // Stream<Map.Entry<Obj, Integer>>
.sorted(Map.Entry.comparingByValue())
.collect(Collectors.groupingBy(
Map.Entry::getValue, // frequency is key
LinkedHashMap::new,
Collectors.mapping(Map.Entry::getKey, Collectors.toList())
)); // Map<Integer, List<Obj>>
}

Intersecting List with keys of Map

We have a map of Student to record Map<Student, StudentRecord>.
Student class is as follows:
Student {
String id;
String grade;
Int age;
}
Additionally, we have a list of Student Id (List<String>) provided.
Using Java streams, what would be the most efficient way to filter out records of students whose Id exists in the provided list?
The expected outcome is the filtered list mapped against the Id(String) - <Map<Id, StudentRecord>>
You can stream set of entries:
map.entrySet().stream()
.filter(e -> list.contains(e.getKey()))
.collect(toMap(Map.Entry::getKey, Map.Entry::getValue));
If you also want to map keys to id field, then:
map.entrySet().stream()
.filter(e -> list.contains(e.getKey()))
.collect(toMap(e -> e.getKey().getId(), Map.Entry::getValue));
First of all, I'd convert your List to a Set, to avoid linear search time:
List<String> ids = ...
Set<String> idsSet = new HashSet<>(ids);
Now, you can stream over the entries of the Map, filter out those having ids in the List/Set, and collect the remaining ones to an output Map:
Map<String,StudentRecord> filtered =
input.entrySet()
.stream()
.filter(e -> !idsSet.contains(e.getKey().getId()))
.collect(Collectors.toMap(e -> e.getKey().getId(),Map.Entry::getValue));
Although other answers are correct but I think they are not more efficient since they use temporary memory or its complicity is not o(n).
the other answer is like this:
provided.stream()
.map(id -> new AbstractMap.SimpleEntry<>(id, map.entrySet()
.stream().filter(st -> st.getKey().id == id)
.map(Map.Entry::getValue).findFirst()))
.filter(simpleEntry ->simpleEntry.getValue().isPresent())
.map(entry-> new AbstractMap.SimpleEntry<>(entry.getKey(), entry.getValue().get()))
.collect(Collectors.toMap(Map.Entry::getKey,Map.Entry::getValue))

Partially Flatten Nested Collections in Java using Lambdas

I have a nested collection as such
Map<Integer, Map<Integer, List<Integer>>> nodes = new TreeMap<>()
I need to convert the inner map into a List<List<Integer>>. The order of the inner list has to be preserved. Essentially for each entry in the outer map, iterate through the inner map, add the List as is to the List of Lists.
I can do it the old fashioned way.
List<List<Integer>> result = new ArrayList<>();
for(Map.Entry<Integer, TreeMap<Integer, List<Integer>>> entry : nodes.entrySet()) {
Map<Integer, List<Integer>> outer = entry.getValue();
ArrayList<Integer> tmp = new ArrayList<>();
for (Map.Entry<Integer, List<Integer>> inner : outer.entrySet()) {
tmp.addAll(inner.getValue());
}
result.add(tmp);
}
How do to this with lambdas? This doesn't work
nodes.entrySet().stream().flatMap(e -> e.getValue().entrySet().stream()).map(e2 -> result.add(e2.getValue()))
How do to this with lambdas? This doesn't work
Here you never invoke a termination operation, so the stream is never consumed. :
nodes.entrySet().stream().flatMap(e -> e.getValue().entrySet().stream()).map(e2 -> result.add(e2.getValue()))
Add any terminal operation such as count() and you could see the stream operated.
Don't forget that Streams are lazy and so the computation is effectively performed only when the terminal operation is invoked.
So you guess that your way is not the right way to do things with Stream.
You don't need to use the List as a variable that you will populate in the stream. Streams are designed to collect as they produce a result and the collect to a List is finally the terminal operation that missed in your initial code.
Besides as a side note you should just stream the values of each Map level instead of the entries since you never use the keys.
Here the code with for each step the actual return type :
List<List<Integer>> result =
nodes.values() // Collection<Map<Integer, List<Integer>>>
.stream() // Stream<Map<Integer, List<Integer>>>
.flatMap(m -> m.values() // Collection<List<Integer>>>
.stream()) // Stream<List<Integer>>>
// flatMap() prevents Stream<Stream<...>>.
// Indeed we get just Stream<List...>>
.collect(Collectors.toList());
This one should do the trick:
Map<Integer, Map<Integer, List<Integer>>> nodes = new TreeMap<>();
List<List<Integer>> list = nodes.values()
.stream()
.flatMap( map -> map.values().stream() )
.collect( Collectors.toList() );
Explanation:
First you get stream of maps from map by using:
nodes.values().stream()
then you flatten those maps with:
.flatMap( map -> map.values().stream() )
And finally collect them with:
.collect( Collectors.toList() )

Use java stream to group by 2 keys on the same type

Using java stream, how to create a Map from a List to index by 2 keys on the same class?
I give here a code Example, I would like the map "personByName" to get all person by firstName OR lastName, so I would like to get the 3 "steves": when it's their firstName or lastname. I don't know how to mix the 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
}
I found a workaround by looping on the 2 maps, but it is not stream-oriented.
You can do it like this:
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())));
Assuming you add a toString() method to the Person class, you can then see result using:
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);
Output
Steve=[Steve Wozniac, Bill Steve, Steve Jobs]
Jobs=[Steve Jobs]
Bill=[Bill Steve, Bill Gates]
Wozniac=[Steve Wozniac]
Gates=[Bill Gates]
You could merge the two Map<String, Set<Person>> for example
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
One way would be by using the newest JDK12's 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 collects to two separate collectors and then merges the results into a final value. From the docs:
Returns a Collector that is a composite of two downstream collectors. Every element passed to the resulting collector is processed by both downstream collectors, then their results are merged using the specified merge function into the final result.
So, the above code collects to a map by first name and also to a map by last name and then merges both maps into a final map by iterating the byLast map and merging each one of its entries into the byFirst map by means of the Map.computeIfAbsent method. Finally, the byFirst map is returned.
Note that I've collected to a Map<String, List<Person>> instead of to a Map<String, Set<Person>> to keep the example simple. If you actually need a map of sets, you could do it as follows:
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;
}));
Keep in mind that if you need to have Set<Person> as the values of the maps, the Person class must implement the hashCode and equals methods consistently.
If you want a real stream-oriented solution, make sure you don't produce any large intermediate collections, else most of the sense of streams is lost.
If just you want to just filter all Steves, filter first, collect later:
persons.stream
.filter(p -> p.getFirstName().equals('Steve') || p.getLastName.equals('Steve'))
.collect(toList());
If you want to do complex things with a stream element, e.g. put an element into multiple collections, or in a map under several keys, just consume a stream using forEach, and write inside it whatever handling logic you want.
You cannot key your maps by multiple values. For what you want to achieve, you have three options:
Combine your "personByFirstName" and "personByLastName" maps, you will have duplicate values (eg. Bill Gates will be in the map under the key Bill and also in the map under the key Gates). #Andreas answer gives a good stream-based way to do this.
Use an indexing library like lucene and index all your Person objects by first name and last name.
The stream approach - it will not be performant on large data sets but you can stream your collection and use filter to get your matches:
persons
.stream()
.filter(p -> p.getFirstName().equals("Steve")
|| p.getLastName().equals("Steve"))
.collect(Collectors.asList());
(I've written the syntax from memory so you might have to tweak it).
If I got it right you want to map each Person twice, once for the first name and once for the last.
To do this you have to double your stream somehow. Assuming Couple is some existing 2-tuple (Guava or Vavr have some nice implementation) you could:
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));
I didn't test it, but the concept is: make a stream of (name, person), (surname, person)... for every person, then simply map for the left value of each couple. The asList is to have a collection as value. If you need a Set chenge the last line with .collect(Collectors.toMap(Couple::getLeft, c -> new HashSet(c.getRight), Collection::addAll))
Try SetMultimap, either from Google Guava or my library abacus-common
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);
});

Java List<E> to Map<P, List<E>> were key is some property of E and value is E with that property

I would like how to convert Java List to Map. Were key in a map is some property of the list element (different elements might have the same property) and value is a list of those list items (having the same property).
eg.List<Owner> --> Map<Item, List<Owner>>. I found a few List to Map questions, but it was not I want to do.
What I came with is:
List<Owner> owners = new ArrayList<>(); // populate from file
Map<Item, List<Owner>> map = new HashMap<>();
owners.parallelStream()
.map(Owner::getPairStream)
.flatMap(Function.identity())
.forEach(pair -> {
map.computeIfPresent(pair.getItem(), (k,v)-> {
v.add(pair.getOwner());
return v;
});
map.computeIfAbsent(pair.getItem(), (k) -> {
List<Owner> list = new ArrayList<>();
list.add(pair.getOwner());
return list;
});
});
PasteBin
I can put forEach part to a separate method, but it still feels too verbose. Plus I made a Pair class just to make it work. I tried to look in to Collectors but couldn't get my head around to do what I wanted.
From where this is, you can simplify your code by using groupingBy:
Map<Item, List<Owner>> map = owners.stream()
.flatMap(Owner::getPairStream)
.collect(Collectors.groupingBy(Pair::getItem,
Collectors.mapping(Pair::getOwner,
Collectors.toList())));
You can also dispense with the Pair class by using SimpleEntry:
Map<Item, List<Owner>> map = owners.stream()
.flatMap(owner -> owner.getItems()
.stream()
.map(item -> new AbstractMap.SimpleEntry<>(item, owner)))
.collect(Collectors.groupingBy(Entry::getKey,
Collectors.mapping(Entry::getValue,
Collectors.toList())));
Note that I'm assuming that Item has equals and hashCode overridden accordingly.
Side notes:
You can use map.merge instead of successively calling map.computeIfPresent and map.computeIfAbsent
HashMap and parallelStream make a bad combination (HashMap isn't thread-safe)

Categories

Resources