Java 8 Stream with map and multiples sets - java

I am trying to write these lines using java8 streams:
for (Town town : getAllTowns(routes)) {
if (originTown.equals(town))
continue;
for (Route route : routes) {
if (route.hasOrigin(originTown) && route.hasDestine(town)) {
distances.put(town, route.getDistance());
break;
}
distances.put(town, maxDistance);
}
}
return distances; //Map<Town,Integer>
The result that I got so far is:
Map<Town, Integer> distances = getAllTowns(routes).stream()
.filter(town -> !originTown.equals(town))
.forEach(town -> routes.stream()
.filter(route -> route.hasOrigin(originTown) && route.hasDestine(town)
...)
return distances;
How can I collect after the inner filter and build the Map< Town,Integer> where the integer is the route.getDistance()?
I tried to use:
.collect(Collectors.toMap(route -> route.getDestineTown(), route -> route.getDistance()))
But it is inside the forEach call, then I can't return it to my variable distances because it generates the map only for the inner call. I did not understand it. Any input would be really helpful. Thanks.

You can use findFirst() to build a list that contains, for each town, the first route that has that town as the destination, and then call toMap() on it. The default values for missing cities can be handled separately.
Collection<Town> towns = getAllTowns(routes);
Map<Town, Integer> distances = towns.stream()
.filter(town -> !originTown.equals(town))
.map(town -> routes.stream()
.filter(route -> route.hasOrigin(originTown) && route.hasDestine(town))
.findFirst())
.filter(Optional::isPresent)
.collect(toMap(route -> route.get().getDestine(), route -> route.get().getDistance()));
towns.stream()
.filter(town -> !distances.containsKey(town))
.forEach(town -> distances.put(town, maxDistance));
(Note that town is no longer available in collect(), but you can take advantage of the fact that each route got added only if its destination town was town.)
Also note that toMap() doesn't accept duplicate keys. If there can be multiple routes to any town (which I assume there might be), you should use groupingBy() instead.

I think you have two options to solve this. Either you create your resulting Map beforehand and use nested foreachs:
Map<Town, Integer> distances = new HashMap<>();
getAllTowns(routes).stream().filter(town -> !originTown.equals(town))
.forEach(town -> routes.stream().forEach(route -> distances.put(town,
route.hasOrigin(originTown) && route.hasDestine(town) ? route.getDistance() : maxDistance)));
The other option is to collect your stream to a Map by creating an intermediate Object which is essentially a Pair of Town and Integer:
Map<Town, Integer> distances = getAllTowns(routes).stream().filter(town -> !originTown.equals(town))
.flatMap(town -> routes.stream()
.map(route -> new AbstractMap.SimpleEntry<Town, Integer>(town,
route.hasOrigin(originTown) && route.hasDestine(town) ? route.getDistance()
: maxDistance)))
.collect(Collectors.toMap(entry -> entry.getKey(), entry -> entry.getValue()));

Related

How to gather a keyset from multiple maps from a stream that is filtered?

I am trying to learn to work with streams and collectors, I know how to do it with multiple for loops but I want to become a more efficient programmer.
Each project has a map committedHoursPerDay, where the key is the employee and the value is the amount of hours expressed in Integer. I want to loop through all project's committedHoursPerDay maps and filter the maps where the committedHoursPerDay is more than 7(fulltime), and add each of the Employee who works fulltime to the set.
The code that i have written so far is this:
public Set<Employee> getFulltimeEmployees() {
// TODO
Map<Employee,Integer> fulltimeEmployees = projects.stream().filter(p -> p.getCommittedHoursPerDay().entrySet()
.stream()
.filter(map -> map.getValue() >= 8)
.collect(Collectors.toMap(map -> map.getKey(), map -> map.getValue())));
return fulltimeEmployees.keySet();
}
however the filter recognizes the map because I can access the key and values, but in the .collect(Collectors.toMap()) it doesnt recognize the map and only sees it as a lambda argument
There is one to many notion here. You can first flatten the maps using flatMap and then apply filter to the map entries.
Map<Employee,Integer> fulltimeEmployees = projects.stream()
.flatMap(p -> p.getCommittedHoursPerDay()
.entrySet()
.stream())
.filter(mapEntry -> mapEntry.getValue() >= 8)
.collect(Collectors.toMap(mapEntry -> mapEntry.getKey(), mapEntry -> mapEntry.getValue()));
The flatMap step returns a Stream<Map.Entry<Employee, Integer>>. The filter thus operates on a Map.Entry<Employee, Integer>.
You can also use method reference on the collect step as .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue))

Java Stream - Combine Two Streams

Is there a way I can combine these two streams into one?
Here's the first stream
Map<String, String> rawMapping = tokens.getColumnFamilies().stream()
.filter(family -> family.getName().equals("first_family"))
.findAny()
.map(columns -> columns.getColumns().stream()).get()
.collect(Collectors.toMap(
Column::getPrefix,
Column::getValue
));
Second stream
List<Token> tokenValues = tokens.getColumnFamilies().stream()
.filter(family -> family.getName().equals("second_family"))
.findAny()
.map(columns -> columns.getColumns().stream()).get()
.map(token -> {
return Token.builder()
.qualifier(token.getPrefix())
.raw(rawMapping.get(token.getPrefix()))
.token(token.getValue())
.build();
})
.collect(Collectors.toList());
Basically tokens is a list which has two column family, my goal is to create a list which will combine the value of the two-column family based on their qualifier. The first stream is storing the first column family into a map. The second stream is traversing the second family and getting the value thru the map using the qualifier and storing it into a new list.
you can use double filtering and then later you might use a flat map then to get a list:
Map<String, String> tokenvalues = tokens.getColumnFamilies().stream()
.filter(family -> family.getName().equals("first_family"))
.filter(family -> family.getName().equals("second_family"))
.map(columns -> columns.getColumns().stream())
//etc..
.stream()
.flatMap(Collection::stream)
.collect(Collectors.toList()));
you can remake a stream out of it inline
https://www.baeldung.com/java-difference-map-and-flatmap

More efficient solution on coding task using Stream API?

I recently had a technical interview and got small coding task on Stream API.
Let's consider next input:
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);
The task is to find Students with unique subjects using Stream API.
So for the provided input expected result (ignoring order) is [John, Anthony].
I presented the solution using custom Collector:
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);
But the solution was considered as not optimal/efficient.
Could you please share your ideas on more efficient solution for this task?
UPDATE: I got another opinion from one guy that he would use reducer (Stream.reduce() method).
But I cannot understand how this could increase efficiency. What do you think?
Here is another one.
// 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);
(Intentionally using a sentinel value, more about that below.)
The steps:
Set<Student> list = new HashSet<>(studentStream
We're creating a HashSet from the Collection we're going to collect. That's because we want to get rid of the duplicate students (students with multiple unique subjects, in your case Antony).
.flatMap(student -> student.subjects()
.map(subject -> new SimpleEntry(subject, student)))
We are flatmapping each student's subjects into a stream, but first we map each element to a pair with as key the subject and as value the student. This is because we need to retain the association between the subject and the student. I'm using AbstractMap.SimpleEntry, but of course, you can use any implementation of a pair.
.collect(Collectors.toMap(Entry::getKey, Entry::getValue, (l, r) -> Student.SENTINEL_VALUE)
We are collecting the values into a map, setting the subject as key and the student as value for the resulting map. We pass in a third argument (a BinaryOperator) to define what should happen if a key collision takes place. We cannot pass in null, so we use a sentinel value1.
At this point, we have inverted the relation student ↔ subject by mapping each subject to a student (or the SENTINEL_VALUE if a subject has multiple students).
.values());
We take the values of the map, yielding the list of all students with a unique subject, plus the sentinel value.
list.remove(Student.SENTINEL_VALUE);
The only thing left to do is getting rid of the sentinel value.
1 We cannot use null in this situation. Most implementations of a Map make no distinction between a key mapped to null or the absence of that particular key. Or, more accurately, the merge method of HashMap actively removes a node when the remapping function returns null. If we want to avoid a sentinel value, then we must implement or own merge method, which could be implemented like something like this: return (!containsKey(key) ? super.merge(key, value, remappingFunction) : put(key, null));.
Another solution. Looks kind of similar to Eugene.
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() );
Not the most readable solution, but here you go:
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);
You can probably do it in a simpler way as :
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());

Streams on nested map

I have the following usecase. I have a nested map with following structure:
Map<String, Map<WorkType, List<CostLineItem>>>
I have to iterate over the map and get the list of CLObject. If the single entry in the list has identifier as null. I have to generate the unique identifier per EnumType. I am not sure how to do it with streams? Following iteration logic will make clear what i want to accomplish
for(Map.Entry<String, Map<WorkType, List<CostLineItem>>> cliByWorkTypeIterator: clisByWorkType.entrySet()) {
Map<WorkType, List<CostLineItem>> entryValue = cliByWorkTypeIterator.getValue();
for(Map.Entry<WorkType, List<CostLineItem>>cliListIterator : entryValue.entrySet()) {
List<CostLineItem> clis = cliListIterator.getValue();
//if any CLI settlementNumber is zero this means we are in standard upload
//TODO: Should we use documentType here? Revisit this check while doing dispute file upload
if(clis.get(0).getSettlementNumber() == null) {
clis.forEach(f -> f.toBuilder().settlementNumber(UUID.randomUUID().toString()).build());
}
}
}
Nested loop makes the code bit boiler plate and dirty. Can someone help me with streams here?
You can use flatMap to iterate over all the List<CostLineItem> values of all the inner Maps.
clisByWorkType.values() // returns Collection<Map<WorkType, List<CostLineItem>>>
.stream() // returns Stream<Map<WorkType, List<CostLineItem>>>
.flatMap(v->v.values().stream()) // returns Stream<List<CostLineItem>>
.filter(clis -> clis.get(0).getSettlementNumber() == null) // filters that Stream
.forEach(clis -> {do whatever logic you need to perform on the List<CostLineItem>});
The following is equivalent to your for-loop:
clisByWorkType.entrySet()
.map(Map.Entry::getValue) // cliByWorkTypeIterator.getValue();
.flatMap(m -> m.entrySet().stream())
.map(Map.Entry::getValue)
.map(CostLineItem::getValue)
.filter(clis.get(0).getSettlementNumber() == null) //filter before flattening
.flatMap(List::stream)
.forEach(f -> f.toBuilder().settlementNumber(UUID.randomUUID().toString()).build());
clisByWorkType.values()
.stream()
.flatMap(e -> e.values().stream())
.filter(clis -> clis.get(0).getSettlementNumber() == null)
.flatMap(Collection::stream)
.forEach(f -> f.toBuilder().settlementNumber(UUID.randomUUID().toString()).build());

Nested for each loop returning map with Java 8 streams

I just started with Java 8 and streams, and not able to find out how to write this code in Java 8:
Map<Integer, CarShop> result = new HashMap<>();
for (Car car : someListOfCars) {
List<CarProduct> listOfCarProducts = car.getCarProducts();
for (CarProduct product : listOfCarProducts) {
result.put(product.getId(), car.getCarShop());
}
}
Any help?
You can often convert your iterative solution directly to a stream by using .collect:
Map<Integer, CarShop> result = someListOfCars.stream().collect(
HashMap::new,
(map, car) -> car.getCarProducts().forEach(
prod -> map.put(prod.getId(), car.getCarShop())
),
Map::putAll
);
You can make the solution more flexible at the cost of additional allocations:
Map<Integer, CarShop> result = someListOfCars.stream()
.flatMap(car -> car.getCarProducts().stream()
.map(prod -> new SimpleImmutableEntry<>(prod.getId(), car.getCarShop()))
).collect(toMap(Map.Entry::getKey, Map.Entry::getValue, (a, b) -> b));
This will allow you to collect any way you wish. For example, you would be able to remove (a,b)->b to force an exception if there are duplicate ids instead of silently overwriting the entry.

Categories