Using Java 8 streams for aggregating list objects - java-8

We are using 3 lists ListA,ListB,ListC to keep the marks for 10 students in 3 subjects (A,B,C).
Subject B and C are optional, so only few students out of 10 have marks in those subjects
Class Student{
String studentName;
int marks;
}
ListA has records for 10 students, ListB for 5 and ListC for 3 (which is also the size of the lists)
Want to know how we can sum up the marks of the students for their subjects using java 8 steam.
I tried the following
List<Integer> list = IntStream.range(0,listA.size() -1).mapToObj(i -> listA.get(i).getMarks() +
listB.get(i).getMarks() +
listC.get(i).getMarks()).collect(Collectors.toList());;
There are 2 issues with this
a) It will give IndexOutOfBoundsException as listB and listC don't have 10 elements
b) The returned list if of type Integer and I want it to be of type Student.
Any inputs will be very helpful

You can make a stream of the 3 lists and then call flatMap to put all the lists' elements into a single stream. That stream will contain one element per student per mark, so you will have to aggregate the result by student name. Something along the lines of:
Map<String, Integer> studentMap = Stream.of(listA, listB, listC)
.flatMap(Collection::stream)
.collect(groupingBy(student -> student.name, summingInt(student -> student.mark)));
Alternatively, if your Student class has getters for its fields, you can change the last line to make it more readable:
Map<String, Integer> studentMap = Stream.of(listA, listB, listC)
.flatMap(Collection::stream)
.collect(groupingBy(Student::getName, summingInt(Student::getMark)));
Then check the result by printing out the studentMap:
studentMap.forEach((key, value) -> System.out.println(key + " - " + value));
If you want to create a list of Student objects instead, you can use the result of the first map and create a new stream from its entries (this particular example assumes your Student class has an all-args constructor so you can one-line it):
List<Student> studentList = Stream.of(listA, listB, listC)
.flatMap(Collection::stream)
.collect(groupingBy(Student::getName, summingInt(Student::getMark)))
.entrySet().stream()
.map(mapEntry -> new Student(mapEntry.getKey(), mapEntry.getValue()))
.collect(toList());

I would do it as follows:
Map<String, Student> result = Stream.of(listA, listB, listC)
.flatMap(List::stream)
.collect(Collectors.toMap(
Student::getName, // key: student's name
s -> new Student(s.getName(), s.getMarks()), // value: new Student
(s1, s2) -> { // merge students with same name: sum marks
s1.setMarks(s1.getMarks() + s2.getMarks());
return s1;
}));
Here I've used Collectors.toMap to create the map (I've also assumed you have a constructor for Student that receives a name and marks).
This version of Collectors.toMap expects three arguments:
A function that returns the key for each element (here it's Student::getName)
A function that returns the value for each element (I've created a new Student instance that is a copy of the original element, this is to not modify instances from the original stream)
A merge function that is to be used when there are elements that have the same key, i.e. for students with the same name (I've summed the marks here).
If you could add the following copy constructor and method to your Student class:
public Student(Student another) {
this.name = another.name;
this.marks = another.marks;
}
public Student merge(Student another) {
this.marks += another.marks;
return this;
}
Then you could rewrite the code above in this way:
Map<String, Student> result = Stream.of(listA, listB, listC)
.flatMap(List::stream)
.collect(Collectors.toMap(
Student::getName,
Student::new,
Student::merge));

Related

Looping over a SplayTreeMap doesn't give values with duplicated keys

I have a SplayTreeMap of players with their levels, and I wanna print a ranking of these players based on their level.
When I use .forEach to iterate over this SplayTreeMap, it ignores the players with the same level (means that while looping over SplayTreeMap, it doesn't take into consideration values with duplicated keys).
Here is my code:
SplayTreeMap<int, String> map = SplayTreeMap<int, String>();
map[5] = 'player1';
map[2] = 'player2';
map[6] = 'player3';
map[7] = 'player4';
map[7] = 'player5';
map[7] = 'player6';
map.forEach((level, player) {
print('$player -> $level');
});
Here is the output of this code:
player2 -> 2
player1 -> 5
player3 -> 6
player6 -> 7
So I'm asking myself why it doesn't print player4 and player5.
If there is no solution to this, what's the best alternative to SplayTreeMap, to have a map, that's sorted based on its keys
A SplayTreeMap is a type of Map, and Maps cannot have duplicate keys:
There is a finite number of keys in the map, and each key has exactly one value associated with it.
(There are Map-like classes such as package:quiver's Multimap, but they cannot derive from Map since they must provide a different signature for some methods (e.g. operator []).)
If you want a SplayTreeMap with multiple values for a single key, then you should store Lists as the values. For example:
import 'dart:collection';
extension SplayTreeMultiMapExtension<K, V> on SplayTreeMap<K, List<V>> {
void add(K key, V value) {
(this[key] ??= []).add(value);
}
}
void main() {
var map = SplayTreeMap<int, List<String>>();
map.add(5, 'player1');
map.add(2, 'player2');
map.add(6, 'player3');
map.add(7, 'player4');
map.add(7, 'player5');
map.add(7, 'player6');
map.forEach((level, players) {
for (var player in players) {
print('$player -> $level');
}
});
}
Prints:
player2 -> 2
player1 -> 5
player3 -> 6
player4 -> 7
player5 -> 7
player6 -> 7
Note that the above implementation allows duplicate players within each level. If you don't want that, you can use a Set<String> instead of a List<String>, or you can use a SplayTreeSet<String> if you want the players to be sorted within each level.

Assign random UUID on a key's first occurrence in a stream

I'm looking for a solution on how to assign a random UUID to a key only on its first occurrence in a stream.
Example:
time key value assigned uuid
| 1 A fff17a1e-9943-11eb-a8b3-0242ac130003
| 2 B f01d2c42-9943-11eb-a8b3-0242ac130003
| 3 C f8f1e880-9943-11eb-a8b3-0242ac130003
| 1 X fff17a1e-9943-11eb-a8b3-0242ac130003 (same as above)
v 1 Y fff17a1e-9943-11eb-a8b3-0242ac130003 (same as above)
As you can see fff17a1e-9943-11eb-a8b3-0242ac130003 is assigned to key "1" on its first occurrence. This uuid is subsequently reused on its second and third occurrence. The order doesn't matter, though. There is no seed for the generated uuid either.
My idea was to use a leftJoin() with a KStream and a KTable with key/uuid mappings. If the right side of the leftJoin is null I have to create a new UUID and add it to the mapping table. However, I think this does not work when there are several new entries with the same key in a short period of time. I guess this will create several UUIDs for the same key.
Is there an easy solution for this or is this simply not possible with streaming?
I don't think you need a join in your use case because joins are to merge to different streams that arrive with equal IDs. You said that you receive just one stream of events. So, your use case is an aggregation over one stream.
What I understood of your question is that you receive events: A, B, C, ... Then you want to assign some ID. You say that the ID is random. So, this is very uncertain. If it is random how would you know that A -> fff17a1e-9943-11eb-a8b3-0242ac130003 and X -> fff17a1e-9943-11eb-a8b3-0242ac130003 (the same). I suppose that you might have a seed to generate this UUID. And then you create a key based also on this seed.
I suggest you start with this sample of word count. then on the first map:
.map((key, value) -> new KeyValue<>(value, value))
you replace it with your map function. Something like this:
.map((k, v) -> {
if (v.equalsIgnoreCase("A")) {
return new KeyValue<String, ValueWithUUID>("1", new ValueWithUUID(v));
} else if (v.equalsIgnoreCase("B")) {
return new KeyValue<String, ValueWithUUID>("2", new ValueWithUUID(v));
} else {
return new KeyValue<String, ValueWithUUID>("0", new ValueWithUUID(v));
}
})
...
class ValueWithUUID {
String value;
String uuid;
public ValueWithUUID(String value) {
this.value = value;
// generate your UUID based on the value. It is random, but as you show in your question it might have a seed.
this.uuid = generateRandomUUIDWithSeed();
}
public String generateRandomUUIDWithSeed() {
return "fff17a1e-9943-11eb-a8b3-0242ac130003";
}
}
Then you decide if you want to use a windowed aggregation, every 30 seconds for instance. Or a non-windowing aggregation that updates the results for every event that arrives. Here is one nice example.
You can aggregate the raw stream as ktable, in the processing, generate or reuse the uuid; then use the stream of ktable.
final KStream<String, String> streamWithoutUUID = builder.stream("topic_name");
KTable<String, String> tableWithUUID = streamWithoutUUID.groupByKey().aggregate(
() -> "",
(k, v, t) -> {
if (!t.startsWith("uuid:")) {
return "uuid:" + "call your buildUUID function here" + ";value:" + v;
} else {
return t.split(";", 2)[0] + ";value:" + v;
}
},
Materialized.<String, String, KeyValueStore<Bytes, byte[]>>as("state_name")
.withKeySerde(Serdes.String()).withValueSerde(Serdes.String()));
final KStream<String, String> streamWithUUID = tableWithUUID.toStream();

Java 8 stream. Convert array to LinkedHashMap and continue work with entrySet in single stream

I need to count the frequency of words in array. And handle result(I think it must be an entrySet )... An order of the entrySet is also important. So I suppose, that array must be converted to LinkedHashMap...
Map<String, Integer> map = new LinkedHashMap<>();
for(String word : words) {
Integer count = map.get(word);
count = (count == null) ? 1: ++count;
map.put(word, count);
}
I found next solution but the order is not respected.
And is it possible use that stream without collect operation(but with map or flatMap)?
Map<String, Long> collect =
wordsList.stream().collect(groupingBy(Function.identity(), counting()));
Thank you.
you're close but you'll need to use the groupingBy collector that takes 3 arguments like this:
LinkedHashMap<String, Long> resultSet =
wordsList.stream()
.collect(groupingBy(Function.identity(),
LinkedHashMap::new,
counting()));
The second argument to the groupingBy collector being the supplier and in your case since you want a LinkedHashMap then that's what you'll need to provide as shown above.

Java 8 Stream Collect Sets

I have two maps, namely
Map<String, Set<String>> courseTeacherMap = {course1: [teacher1, teacher2], ...}
Map<String, Set<String>> teacherStudentMap = {teacher1: [student1, student2], ...}
And I defined a class courseStudentPair that has a very simple structure
public class courseStudentPair{
String studentName; // Taken from teacherStudentMap
String courseName; // Taken from courseTeacherMap
}
And my goal is to get a Set<courseStudentPair> out of the two maps. As long as a teacher A is teaching a course C, every student that is in the value set of key A in teacherStudentMap is considered to be a student taking C.
For example, given
Map<String, Set<String>> courseTeacherMap = {c1: [t1], c2:[t2], c3:[t1, t2]}
Map<String, Set<String>> teacherStudentMap = {t1: [s1], t2:[s1, s2]}
The result should be *(student, course) denotes a courseStudentPair object in the example below*
Set<courseStudentPair> result = [(c1, s1), (c2, s1), (c2, s2), (c3, s1), (c3, s2)]
It's quite straightforward to do it with for loops, but I am learning the stream function in Java 8 and this seems quite complicated to me. You can assume the courseStudentPair class has constructor or builder defined.
In the same spirit, you can generate each combination of (course, teacher) and then lookup for the students associated with this teacher. This may generate duplicates (for instance (c3, s1)), so be sure your CourseStudentPair class implements equals() and hashCode() based on those two fields.
import static java.util.Collections.emptySet;
import static java.util.stream.Collectors.toSet;
...
Set<CourseStudentPair> result =
courseTeacherMap.entrySet()
.stream()
.flatMap(e -> e.getValue()
.stream()
.flatMap(t -> teacherStudentMap.getOrDefault(t, emptySet()).stream().map(s -> new CourseStudentPair(e.getKey(), s))))
.collect(toSet());
/*
Output:
CourseStudentPair{studentName='c1', courseName='s1'}
CourseStudentPair{studentName='c2', courseName='s2'}
CourseStudentPair{studentName='c2', courseName='s1'}
CourseStudentPair{studentName='c3', courseName='s2'}
CourseStudentPair{studentName='c3', courseName='s1'}
*/
List<Pair<String, String>> result = courseTeacherMap.entrySet()
.stream()
.flatMap(entry -> Optional.ofNullable(entry.getValue())
.orElse(new HashSet<>())
.stream()
.flatMap(teacher -> Optional.ofNullable(teacherStudentMap.get(teacher))
.orElse(new HashSet<>())
.stream()
.map(student -> Pair.of(entry.getKey(), student))))
.distinct()
.collect(Collectors.toList());
I edited to make it null-safe, in case a teacher has no students for example or your Map might have a key mapped to null for example.

ArrayIndexOutOfBounds, while using Java 8 streams to iterate a list

I have a List of Objects called md. Each of this objects has an activityName, a startTime and an endTime(for the activity).
I want to iterate over this list and for each activity, get the startTime and endTime.
Map<String,Long> m1 = new HashMap<String,Long>();
m1 = md
.stream()
.map(s->s.activityName)
.collect(HashMap<String,Long>::new,
(map,string)->{
String d1 = md.get(md.indexOf(string)).startTime;
String d2 = md.get(md.indexOf(string)).endTime;
.
.
.
},HashMap<String,Long>::putAll);
It gives me java.lang.ArrayIndexOutOfBoundsException: -1 when I try to get the index of string String d1 = md.get(md.indexOf(string)).startTime;
Is there any other way to simplify the code using Lambda expressions?
What if I have two activities with the same name (Drinking for ex).Will it only return the index of the first Drinking activity it finds?
It seems that you are missing that fact that once you do:
md.stream().map(s -> s.activityName)
your Stream has become Stream<String>; while your md is still List<YourObject>
And in the map operation you are trying to find a String inside md, this obviously does not exist, thus a -1.
So you need a Map<String, Long> that is activitaName -> duration it takes(could be Date/Long)
md.stream()
.collect(Collectors.toMap(s -> s.activityName, x -> {
Date start = // parse s.startTime
Date end = // parse s.endTime
return end.minus(start);
}));
Now the parsing depends on the dates you use.

Resources