Java 8 Stream API toMap converting to TreeMap - java

public class Message {
private int id;
private User sender;
private User receiver;
private String text;
private Date senddate;
..
}
I have
List<Message> list= new ArrayList<>();
I need to transform them to
TreeMap<User,List<Message>> map
I know how to do transform to HashMap using
list.stream().collect(Collectors.groupingBy(Message::getSender));
But I need TreeMap with:
Key - User with newest message senddate first
Value - List sorted by senddate newest first
Part of User class
public class User{
...
private List<Message> sendMessages;
...
public List<Message> getSendMessages() {
return sendMessages;
}
}
User comparator:
public class Usercomparator implements Comparator<User> {
#Override
public int compare(User o1, User o2) {
return o2.getSendMessages().stream()
.map(message -> message.getSenddate())
.max(Date::compareTo).get()
.compareTo(o1.getSendMessages().stream()
.map(message1 -> message1.getSenddate())
.max(Date::compareTo).get());
}
}

You can use overloaded groupingBy method and pass TreeMap as Supplier:
TreeMap<User, List<Message>> map = list
.stream()
.collect(Collectors.groupingBy(Message::getSender,
() -> new TreeMap<>(new Usercomparator()), toList()));

If your list is sorted then just use this code for sorted map.
Map<String, List<WdHour>> pMonthlyDataMap = list
.stream().collect(Collectors.groupingBy(WdHour::getName, TreeMap::new, Collectors.toList()));

Related

Flatten a map after Collectors.groupingBy in java

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;
}

How to collect a stream into a TreeMap

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.

Java 8 filter List by highest values of each id

Is there a way to do the following with java 8 features and lambdas?
I have this class
public class A{
private String id;
private int version;
// getters...
}
Input (the list is ordered by id and versions ascending):
[{"1",1},{"1",2},{"2",1},{"2",2}]
I want to have a List with the highest versions of each id, so the result should look like:
[{"1",2},{"2",2}]
I already have a solution but I dont like it that much. I figured maybe there is a better way with java 8.
static List<A> removeOldVersions(List<A> aList) {
Map<String, A> map = new HashMap<>();
aList.forEach(a -> map.put(a.getId(), a));
return (List<A>) map.values();
}
Well, you could collect to a Map and merge entries via BinaryOperator.maxBy. Once that is computed call values on it (that will return a Collection<A>):
yourList.stream()
.collect(Collectors.toMap(
A::getId,
Function.identity(),
BinaryOperator.maxBy(Comparator.comparing(A::getVersion))))
.values()
Introduction
There are two approaches to this, both of them use Comparators, I suggest you read more about them at the following link:
https://docs.oracle.com/javase/8/docs/api/java/util/Comparator.html
I would like to show you my most simple approach without writing your own filter/distinct alghorytm. This solution will use the Stream API of Java 8.
(Let's say you are using an ArrayList or HashSet in both examples.)
Using Streams
public class Sorter {
private final class A {
private String id;
private int version;
private A(String id, int version) {
this.id = id;
this.version = version;
}
public String getId() {
return id;
}
public int getVersion() {
return version;
}
}
public void sort() {
final ArrayList<A> list = new ArrayList<A>() {{
add(new A("1", 1));
add(new A("1", 2));
add(new A("2", 1));
add(new A("2", 2));
}};
//Using List as we want to be sure that our List is in order
list.stream().sorted(Comparator.comparingInt(o -> o.version)).filter(distinctByKey(a -> a.id)).collect(Collectors.toList());
}
private static <T> Predicate<T> distinctByKey(Function<? super T, Object> keyExtractor)
{
Map<Object, Boolean> map = new ConcurrentHashMap<>();
return t -> map.putIfAbsent(keyExtractor.apply(t), Boolean.TRUE) == null;
}
}
Hopefully this has answered your question.
Add each pair to a Map, checking if it already exists and has a lower value before overwriting.
Classic approach:
{
List<A> in = Arrays.asList(new A("'1'", 1),
new A("'1'", 2),
new A("'2'", 1),
new A("'2'", 2));
Map<String, Integer> map = new LinkedHashMap<>(in.size()); // 'Linked' just keeps the original ordering
for (A element : in) {
#Nullable Integer version = map.get(element.id);
if (version == null || version < element.version)
map.put(element.id, element.version);
}
System.out.println(map);
}
Output:
{'1'=2, '2'=2}

Java streams: Add to map but avoid mutation

I often find myself in a situation where I need to create a Map of objects from a Set or List.
The key is usually some String or Enum or the like, and the value is some new object with data lumped together.
The usual way of doing this, for my part, is by first creating the Map<String, SomeKeyValueObject> and then iterating over the Set or List I get in and mutate my newly created map.
Like the following example:
class Example {
Map<String, GroupedDataObject> groupData(final List<SomeData> list){
final Map<String, GroupedDataObject> map = new HashMap<>();
for(final SomeData data : list){
final String key = data.valueToGroupBy();
map.put(key, GroupedDataObject.of(map.get(key), data.displayName(), data.data()));
}
return map;
}
}
class SomeData {
private final String valueToGroupBy;
private final Object data;
private final String displayName;
public SomeData(final String valueToGroupBy, final String displayName, final Object data) {
this.valueToGroupBy = valueToGroupBy;
this.data = data;
this.displayName = displayName;
}
public String valueToGroupBy() {
return valueToGroupBy;
}
public Object data() {
return data;
}
public String displayName() {
return displayName;
}
}
class GroupedDataObject{
private final String key;
private final List<Object> datas;
private GroupedDataObject(final String key, final List<Object> list) {
this.key = key;
this.datas = list;
}
public static GroupedDataObject of(final GroupedDataObject groupedDataObject, final String key, final Object data) {
final List<Object> list = new ArrayList<>();
if(groupedDataObject != null){
list.addAll(groupedDataObject.datas());
}
list.add(data);
return new GroupedDataObject(key, list);
}
public String key() {
return key;
}
public List<Object> datas() {
return datas;
}
}
This feels very unclean. We create a map, and then mutate it over and over.
I've taken a liking to java 8s use of Streams and creating non-mutating data structures (or rather, you don't see the mutation). So is there a way to turn this grouping of data into something that uses a declarative approach rather than the imperative way?
I tried to implement the suggestion in https://stackoverflow.com/a/34453814/3478016 but I seem to be stumbling. Using the approach in the answer (the suggestion of using Collectors.groupingBy and Collectors.mapping) I'm able to get the data sorted into a map. But I can't group the "datas" into one and the same object.
Is there some way to do it in a declarative way, or am I stuck with the imperative?
You can use Collectors.toMap with a merge function instead of Collectors.groupingBy.
Map<String, GroupedDataObject> map =
list.stream()
.collect(Collectors.toMap(SomeData::valueToGroupBy,
d -> {
List<Object> l = new ArrayList<>();
l.add(d.data());
return new GroupedDataObject(d.valueToGroupBy(), l);
},
(g1,g2) -> {
g1.datas().addAll(g2.datas());
return g1;
}));
The GroupedDataObject constructor must be made accessible in order for this to work.
If you avoid the GroupedDataObject and simply want a map with a key and a list you can use Collectors.groupingBy that you have been looking into.
Collectors.groupingBy will allow you to do this:
List<SomeObject> list = getSomeList();
Map<SomeKey, List<SomeObject>> = list.stream().collect(Collectors.groupingBy(SomeObject::getKeyMethod));
This will require SomeKey to have proper implementations of equals and hashValue
Sometimes streams are not the way to go. I believe this is one of those times.
A little refactoring using merge() gives you:
Map<String, MyTuple> groupData(final List<SomeData> list) {
Map<String, MyTuple> map = new HashMap<>();
list.forEach(d -> map.merge(d.valueToGroupBy(), new MyTuple(data.displayName(), data.data()),
(a, b) -> {a.addAll(b.getDatas()); return a;});
Assuming a reasonable class to hold your stuff:
class MyTuple {
String displayName;
List<Object> datas = new ArrayList<>();
// getters plus constructor that takes 1 data and adds it to list
}

java - sorting a hashmap with date inside value object [duplicate]

This question already has answers here:
Closed 10 years ago.
Possible Duplicate:
How to sort a Map<Key, Value> on the values in Java?
Suppose am having a map like
Map<String, Student> studDetails = new HashMap<String, Student>();
And the map contains entries like
studDetails.put("1",student1);
studDetails.put("2",student2);
studDetails.put("3",student3);
studDetails.put("4",student4);
Student entity is Something like
Class Student{
private String studName;
private List<Group> groups;
}
Group entity will be like
Class Group{
private String groupName;
private Date creationDate;
}
OK, so what I need is when am displaying student details, it will be in the order of group creation date.
So if student is mapped to more than one group, we can take the creationDate of first group.
How can I given the soring on my HashMap studDetails using this scenario.?
Can anyone help..please..
HashMap is not sorted you should use a SortedMap implementation instead for example TreeMap.
Then you could create your own Comparator<String> which will sort by the groups attribute of the actual Student instance but you'll need the actual map for it because TreeMap sorts by the keys, so this is a possible but not nice solution.
So with TreeMap:
public class StudentGroupComparator implements Comparator<String> {
private Map<String, Student> sourceMap;
public StudentGroupComparator(Map<String, Student> sourceMap) {
this.sourceMap = sourceMap;
}
#Override
public int compare(String key1, String key2) {
// TODO: null checks
Student student1 = sourceMap.get(key1);
Student student2 = sourceMap.get(key2);
Date o1CreationDate = student1.groups.get().creationDate;
Date o2CreationDate = student2.groups.get().creationDate;
return o1CreationDate.compareTo(o2.creationDate);
}
}
SortedMap<String, Student> sortedMap = new TreeMap<String, Student>(new StudentGroupComparator(sourceMap));
sortedMap.putAll(sourceMap);
How can I given the soring on my HashMap studDetails using this scenario?
You can't, because HashMap is fundamentally unordered (or at least, the ordering is unstable and unhelpful).
Even for sorted maps like TreeMap, the sort order is based on the key, not the value.
Add Students Objects to List and Use Collections.sort(list, custom_comparetor).
Prepare one Custom comparator to sort Students Objects.
Try this code it might helpful
StudentComparator.java
class StudentComparator implements Comparator {
public int compare(Object stud1, Object stud2) {
List<Group> list1Grp = ((Student) stud1).getGroups();
List<Group> list2Grp = ((Student) stud2).getGroups();
Collections.sort(list1Grp, new GroupComparator());
Collections.sort(list2Grp, new GroupComparator());
return list1Grp.get(0).getCreationDate().compareTo(list2Grp.get(0).getCreationDate());
}
}
GroupComparator.java
public class GroupComparator implements Comparator {
public int compare(Object grp1, Object grp2) {
return ((Group) grp1).getCreationDate().compareTo(
((Group) grp2).getCreationDate());
}
}
main method
add student object to one new List
then use
Collections.sort(new_stud_list, new StudentComparator());
Add Comparable to Group
class Group implements Comparable {
private String groupName;
private Date creationDate;
public Date getCreationDate() {
return creationDate;
}
#Override
public int compareTo(Object t) {
Group g = (Group) t;
return getCreationDate().compareTo(g.getCreationDate());
}
}
Use TreeSet instead of List in Student for Groups
public class Student implements Comparable {
private String studName;
private TreeSet<Group> groups;
public TreeSet<Group> getGroups() {
return groups;
}
#Override
public int compareTo(Object t) {
Student t1 = (Student) t;
return groups.first().getCreationDate()
.compareTo(t1.getGroups().first().getCreationDate());
}
}
Now use
TreeSet<Student> studDetails = new TreeSet();
then add students. It will be ordered one. Hope you can take care of null pointer exceptions

Categories

Resources