Java 8 stream reduce Map - java-8

I have a LinkedHashMap which contains multiple entries. I'd like to reduce the multiple entries to a single one in the first step, and than map that to a single String.
For example:
I'm starting with a Map like this:
{"<a>"="</a>", "<b>"="</b>", "<c>"="</c>", "<d>"="</d>"}
And finally I want to get a String like this:
<a><b><c><d></d></c></b></a>
(In that case the String contains the keys in order, than the values in reverse order. But that doesn't really matter, I'd like an general solution)
I think I need map.entrySet().stream().reduce(), but I have no idea what to write in the reduce method, and how to continue.

Since you're reducing entries by concatenating keys with keys and values with values, the identity you're looking for is an entry with empty strings for both key and value.
String reduceEntries(LinkedHashMap<String, String> map) {
Entry<String, String> entry =
map.entrySet()
.stream()
.reduce(
new SimpleImmutableEntry<>("", ""),
(left, right) ->
new SimpleImmutableEntry<>(
left.getKey() + right.getKey(),
right.getValue() + left.getValue()
)
);
return entry.getKey() + entry.getValue();
}
Java 9 adds a static method Map.entry(key, value) for creating immutable entries.

here is an example about how I would do it :
import java.util.LinkedHashMap;
public class Main {
static String result = "";
public static void main(String [] args)
{
LinkedHashMap<String, String> map = new LinkedHashMap<String, String>();
map.put("<a>", "</a>");
map.put("<b>", "</b>");
map.put("<c>", "</c>");
map.put("<d>", "</d>");
map.keySet().forEach(s -> result += s);
map.values().forEach(s -> result += s);
System.out.println(result);
}
}
note: you can reverse values() to get d first with ArrayUtils.reverse()

Related

Group the data into a Map<Long, List<Long>> where Lists need to be sorted

Assume I have the following domain object:
public class MyObj {
private Long id;
private Long relationId;
private Long seq;
// getters
}
There is a list List<MyObj> list. I want to create a Map by grouping the data by relationId (*key) and sort values (value is a list of id).
My code without sort values:
List<MyObj> list = getMyObjs();
// key: relationId, value: List<Long> ids (needs to be sorted)
Map<Long, List<Long>> map = list.stream()
.collect(Collectors.groupingBy(
MyObj::getRelationId,
Collectors.mapping(MyObj::getId, toList())
));
public class MyObjComparator{
public static Comparator<MyObj> compare() {
...
}
}
I have created compare method MyObjComparator::compare, my question is how to sort this map's values in the above stream.
To obtain the Map having the sorted lists of id as Values, you can sort the stream elements by id before collecting them (as #shmosel has pointed out in the comment).
Collector groupingBy() will preserve the order of stream elements while storing them into Lists. In short, the only case when the order can break is while merging the partial results during parallel execution using an unordered collector (which has a leeway of combining results in arbitrary order). groupingBy() is not unordered, therefore the order of values in the list would reflect the initial order of elements in the stream. You can find detailed explanation in this answer by #Holger.
You don't need a TreeMap (or a LinkedHashMap), unless you want the Entries of the Map to be sorted as well.
List<MyObj> list = getMyObjs();
// Key: relationId, Value: List<Long> ids (needs to be sorted)
Map<Long, List<Long>> map = list.stream()
.sorted(Comparator.comparing(MyObj::getId))
.collect(Collectors.groupingBy(
MyObj::getRelationId,
Collectors.mapping(MyObj::getId, Collectors.toList())
));
As #dan1st said you can use TreeMap if you want to sort keys
If you want to sort values you can only sort them before they are grouped and then they are grouped again
#Data
#AllArgsConstructor
public class MyObj {
private Long relationId;
private Long id;
static int comparing(MyObj obj,MyObj obj2){
return obj.getId().compareTo(obj2.getId());
}
public static void main(String[] args) {
List<MyObj> list = new ArrayList<>();
list.add(new MyObj(2L, 3L));
list.add(new MyObj(2L, 1L));
list.add(new MyObj(2L, 5L));
list.add(new MyObj(1L, 1L));
list.add(new MyObj(1L, 2L));
list.add(new MyObj(1L, 3L));
Map<Long, List<Long>> collect = list.stream()
// value sort
// .sorted(MyObj::comparing)
.collect(groupingBy(MyObj::getRelationId,
// key sort
// (Supplier<Map<Long, List<Long>>>) () -> new TreeMap<>(Long::compareTo),
mapping(MyObj::getId, toList())));
try {
// collect = {"1":[1,2,3],"2":[1,3,5]}
System.out.println("collect = " + new ObjectMapper().writeValueAsString(collect));
} catch (JsonProcessingException e) {
throw new RuntimeException(e);
}
}
}
It appears from the presented code that the resulting map should look like Map<Long, List<Long>> with the key - relationId and the value - list of id. Therefore custom comparator for List<Long> should be implemented like this:
Comparator<List<Long>> cmp = (a, b) -> {
for (int i = 0, n = Math.min(a.size(), b.size()); i < n; i++) {
int res = Long.compare(a.get(i), b.get(i));
if (res != 0) {
return res;
}
}
return Integer.compare(a.size(), b.size());
};
This comparator should be applied to the entry set of the map, however, either the map should be converted into a SortedSet of the map entries, or a LinkedHashMap needs to be recreated on the basis of the comparator:
Map<Long, List<Long>> map = list.stream()
.collect(groupingBy(
MyObj::getRelationId, Collectors.mapping(MyObj::getId, toList())
))
.entrySet()
.stream()
.sorted(cmp)
.collect(toMap(Map.Entry::getKey, Map.Entry::getValue, (a, b) -> a, LinkedHashMap::new));
Not clear if you want to sort by ids or by elements and extract the ids.
In the first case you can use an ending operation after the collect to get a sorted list. As Collections.sort() can sort a list, you just have to call it at the appropriate place.
Map<Long, List<Long>> map = list.stream()
.collect(Collectors.groupingBy(
MyObj::getRelationId,
Collectors.collectingAndThen(Collectors.mapping(MyObj::getId,toList()),
l -> { Collections.sort(l, your_comparator); return l; })
));
In the second case you just need to sort the stream (if it is finite and not too big).

How to convert List<Person> into Map<String,List<Employee>>

Here is the piece of code which was written in Java7 and I wants to convert into Java8 by using Streams and Lambdas.
public static Map<String, List<Employee>> getEmployees(List<Person> personList) {
Map<String, List<Employee>> result = new HashMap<>();
for (Person person : personList) {
String[] perArr = person.getName().split("-");
List<Employee> employeeList = result.get(perArr[0]);
if (employeeList == null) {
employeeList = new ArrayList<>();
}
employeeList.add(new Employee(person.getPersonId(), perArr[1]));
result.put(perArr[0], employeeList);
}
return result;
}
Well you were somehow close I would say, problem being that you would need to pass along to the next stage of the stream pipeline 3 things actually: first token and second token (from split("-")) and also person::getPersonId; I've used a List here and some casting for this purpose (you could use a Triple for example, I've heard apache has it):
personList.stream()
.map(person -> {
String[] tokens = person.getName().split("-");
return Arrays.asList(tokens[0], tokens[1], person.getPersonId());
})
.collect(Collectors.groupingBy(
list -> (String) list.get(0),
Collectors.mapping(
list -> new Employee((Integer) list.get(2), (String) list.get(1)),
Collectors.toList())));
A straightforward way to improve your loop is to use Map.computeIfAbsent to manage creation of new map entries:
for (Person person : personList) {
String[] perArr = person.getName().split("-");
List<Employee> employeeList = result.computeIfAbsent(perArr[0], x -> new ArrayList<>());
employeeList.add(new Employee(person.getPersonId(), perArr[1]));
}
Doing this with streams is somewhat awkward because you cannot conveniently carry the result of an intermediate computation and so you would have to either complicate matters with intermediate objects or just split the string again:
import static java.util.stream.Collectors.*;
personList.stream()
.collect(groupingBy(
p -> p.getName().split("-")[0],
mapping(
p -> new Employee(p.getPersonId(), p.getName().split("-")[1]),
toList()
)
));

Ordering a list of objects in the same order as another list of objects with objects having some common attribute

I have two ArrayLists like below;
List<HashMap<String, String>> mapList;
List<myclass> sortedDtos;
As the name implies sortedDtos is already sorted.
`myclass` has a field called `jobNo`
The HashMap has a key called `jobNo`
Basically I want to order mapList based on sortedDtos by comparing the attribute jobNo.
How can I do this in Java 8?
This seems to work correctly, also can be probably be made more efficient.
List<String> jobNos = new ArrayList();
sortedDtos.stream().forEach(dto -> jobNos.add(dto.getJobNo()));
// sort maplist based on the sorting of the dtos
mapList.sort(Comparator.comparing(el -> jobNos.indexOf(el.get("jobNo"))));
I will suggest the following way. This approach involves providing your own custom comparator to sort the map.
Collections.sort(mapList, new Comparator<HashMap<String, String>>() {
List<String> list = sortedDtos.stream().map(l -> l.jobNo).collect(Collectors.toList());
public int compare(HashMap<String, String> map1, HashMap<String, String> map2) {
if(list.indexOf(map1.get("jobNo")) < list.indexOf(map2.get("jobNo"))) {
return -1;
}
else if(list.indexOf(map1.get("jobNo")) > list.indexOf(map2.get("jobNo"))) {
return 1;
}
else {
return 0;
}
}
});

java 8 use reduce and Collectors grouping by to get list

EDIT
**Request to provide answer to First approach also using reduce method **
public class Messages {
int id;
String message;
String field1;
String field2;
String field3;
int audId;
String audmessage;
//constructor
//getter or setters
}
public class CustomMessage {
int id;
String msg;
String field1;
String field2;
String field3;
List<Aud> list;
//getters and setters
}
public class Aud {
int id;
String message;
//getters and setters
}
public class Demo {
public static void main(String args[]){
List<Messages> list = new ArrayList<Messages>();
list.add(new Messages(1,"abc","c","d","f",10,"a1"));
list.add(new Messages(2,"ac","d","d","f",21,"a2"));
list.add(new Messages(3,"adc","s","d","f",31,"a3"));
list.add(new Messages(4,"aec","g","d","f",40,"a4"));
list.add(new Messages(1,"abc","c","d","f",11,"a5"));
list.add(new Messages(2,"ac","d","d","f",22,"a5"));
}
I want the message to be mapped with audits
CustomMessage must have ->1,"abc","c","d","f"----->List of 2 audits (10,a1) and (11,"a5");
There are two ways to do it
1.Reduce-I would like to use reduce also to create my own accumulator and combiner
List<CustomMessage> list1= list.stream().reduce(new ArrayList<CustomMessage>(),
accumulator1,
combiner1);
**I am unable to write a accumulator and combiner**
2.Collectors.groupingBy-
I do not want to use constructors for creating the Message and
neither for Custom Message.here I have less fields my actual object has many fields.Any way to have a static
method for object creation
Is there is a way to do it via reduce by writing accumulator or
combiner
List<CustomMessage> l = list.stream()
.collect(Collectors.groupingBy(m -> new SimpleEntry<>(m.getId(), m.getMessage()),
Collectors.mapping(m -> new Aud(m.getAudId(), m.getAudMessage()), Collectors.toList())))
.entrySet()
.stream()
.map(e -> new CustomMessage(e.getKey().getKey(), e.getKey().getValue(), e.getValue()))
.collect(Collectors.toList());
Can anyone help me with both the approaches.
This code will create a Collection of CustomMessage. I would recommend putting a constructor in CustomMessage that takes a Messages argument. And maybe also move the mergeFunction out of the collect.
Collection<CustomMessage> customMessages = list.stream()
.collect(toMap(
Messages::getId,
m -> new CustomMessage(m.getId(), m.getMessage(), m.getField1(), m.getField2(), m.getField3(),
new ArrayList<>(singletonList(new Aud(m.getAudId(), m.getAudmessage())))),
(m1, m2) -> {
m1.getList().addAll(m2.getList());
return m1;
}))
.values();
What toMap does here is : The first time a Messages id is encountered, it will put it to a Map as key with value the newly created CustomMessage by the second argument to toMap (the "valueMapper"). The next times it will merge two CustomMessage with the 3rd argument the "mergeFunction" that will effectively concatenate the 2 lists of Aud.
And if you absolutely need a List and not a Collection:
List<CustomMessage> lm = new ArrayList<>(customMessages);
You cannot do this by either grouping or reducing. You need both: group first and then reduce. I coded the reduction differently:
List<CustomMessage> list1 = list.stream()
.collect(Collectors.groupingBy(Messages::getId))
.values()
.stream() // stream of List<Messages>
.map(lm -> {
List<Aud> la = lm.stream()
.map(m -> new Aud(m.getAudId(), m.getAudmessage()))
.collect(Collectors.toList());
Messages m0 = lm.get(0);
return new CustomMessage(m0.getId(), m0.getMessage(),
m0.getField1(), m0.getField2(), m0.getField3(), la);
})
.collect(Collectors.toList());
I have introduced a constructor in Aud and then read your comment that you are trying to avoid constructors. I will revert to this point in the end. Anyway, you can rewrite the creation of Aud objects to be the same way as in your question. And the construction of CustomMessage objects too if you like.
Result:
[1 abc c d f [10 a1, 11 a5], 3 adc s d f [31 a3], 4 aec g d f [40 a4],
2 ac d d f [21 a2, 22 a5]]
I grouped messages only by ID since you said their equals method uses ID only. You may also group by more fields like in your question. A quick and dirty way wold be
.collect(Collectors.groupingBy(m -> "" + m.getId() + '-' + m.getMessage()
+ '-' + m.getField1() + '-' + m.getField2() + '-' + m.getField3()))
Avoiding public constructors and using static methods for object creation doesn’t change a lot. For example if you have
public static Aud createAud(int id, String message) {
return new Aud(id, message);
}
(well, this didn’t eliminate the constructor completely, but now you can declare it private; if still not satisfied, you can also rewrite the method into not using a declared constructor). Now in the stream you just need to do:
.map(m -> Aud.createAud(m.getAudId(), m.getAudmessage()))
You can do similarly for CustomMessage. In this case your static method may take a Messages argument if you like, a bit like Manos Nikolaidis suggested, this could simplify the stream code a bit.
Edit: You couldn’t just forget about the three-argument reduce method, could you? ;-) It can be used. If you want to do that, I suggest you first fit CustomMessage with a range of methods for the purpose:
private CustomMessage(int id, String msg,
String field1, String field2, String field3, List<Aud> list) {
this.id = id;
this.msg = msg;
this.field1 = field1;
this.field2 = field2;
this.field3 = field3;
this.list = list;
}
public static CustomMessage create(Messages m, List<Aud> la) {
return new CustomMessage(m.getId(), m.getMessage(),
m.getField1(), m.getField2(), m.getField3(), la);
}
/**
* #return original with the Aud from m added
*/
public static CustomMessage adopt(CustomMessage original, Messages m) {
if (original.getId() != m.getId()) {
throw new IllegalArgumentException("adopt(): incompatible messages, wrong ID");
}
Aud newAud = Aud.createAud(m.getAudId(), m.getAudmessage());
original.addAud(newAud);
return original;
}
public static CustomMessage merge(CustomMessage cm1, CustomMessage cm2) {
if (cm1.getId() != cm2.getId()) {
throw new IllegalArgumentException("Cannot merge non-matching custom messages, id "
+ cm1.getId() + " and " + cm2.getId());
}
cm1.addAuds(cm2.getList());
return cm1;
}
private void addAud(Aud aud) {
list.add(aud);
}
private void addAuds(List<Aud> list) {
this.list.addAll(list);
}
With these in place it’s not so bad:
List<CustomMessage> list2 = list.stream()
.collect(Collectors.groupingBy(Messages::getId))
.values()
.stream()
.map(lm -> lm.stream()
.reduce(CustomMessage.create(lm.get(0), new ArrayList<>()),
CustomMessage::adopt,
CustomMessage::merge))
.collect(Collectors.toList());

Java 8 is not maintaining the order while grouping

I m using Java 8 for grouping by data. But results obtained are not in order formed.
Map<GroupingKey, List<Object>> groupedResult = null;
if (!CollectionUtils.isEmpty(groupByColumns)) {
Map<String, Object> mapArr[] = new LinkedHashMap[mapList.size()];
if (!CollectionUtils.isEmpty(mapList)) {
int count = 0;
for (LinkedHashMap<String, Object> map : mapList) {
mapArr[count++] = map;
}
}
Stream<Map<String, Object>> people = Stream.of(mapArr);
groupedResult = people
.collect(Collectors.groupingBy(p -> new GroupingKey(p, groupByColumns), Collectors.mapping((Map<String, Object> p) -> p, toList())));
public static class GroupingKey
public GroupingKey(Map<String, Object> map, List<String> cols) {
keys = new ArrayList<>();
for (String col : cols) {
keys.add(map.get(col));
}
}
// Add appropriate isEqual() ... you IDE should generate this
#Override
public boolean equals(Object obj) {
if (obj == null) {
return false;
}
if (getClass() != obj.getClass()) {
return false;
}
final GroupingKey other = (GroupingKey) obj;
if (!Objects.equals(this.keys, other.keys)) {
return false;
}
return true;
}
#Override
public int hashCode() {
int hash = 7;
hash = 37 * hash + Objects.hashCode(this.keys);
return hash;
}
#Override
public String toString() {
return keys + "";
}
public ArrayList<Object> getKeys() {
return keys;
}
public void setKeys(ArrayList<Object> keys) {
this.keys = keys;
}
}
Here i am using my class groupingKey by which i m dynamically passing from ux. How can get this groupByColumns in sorted form?
Not maintaining the order is a property of the Map that stores the result. If you need a specific Map behavior, you need to request a particular Map implementation. E.g. LinkedHashMap maintains the insertion order:
groupedResult = people.collect(Collectors.groupingBy(
p -> new GroupingKey(p, groupByColumns),
LinkedHashMap::new,
Collectors.mapping((Map<String, Object> p) -> p, toList())));
By the way, there is no reason to copy the contents of mapList into an array before creating the Stream. You may simply call mapList.stream() to get an appropriate Stream.
Further, Collectors.mapping((Map<String, Object> p) -> p, toList()) is obsolete. p->p is an identity mapping, so there’s no reason to request mapping at all:
groupedResult = mapList.stream().collect(Collectors.groupingBy(
p -> new GroupingKey(p, groupByColumns), LinkedHashMap::new, toList()));
But even the GroupingKey is obsolete. It basically wraps a List of values, so you could just use a List as key in the first place. Lists implement hashCode and equals appropriately (but you must not modify these key Lists afterwards).
Map<List<Object>, List<Object>> groupedResult=
mapList.stream().collect(Collectors.groupingBy(
p -> groupByColumns.stream().map(p::get).collect(toList()),
LinkedHashMap::new, toList()));
Based on #Holger's great answer. I post this to help those who want to keep the order after grouping as well as changing the mapping.
Let's simplify and suppose we have a list of persons (int age, String name, String adresss...etc) and we want the names grouped by age while keeping ages in order:
final LinkedHashMap<Integer, List<String> map = myList
.stream()
.sorted(Comparator.comparing(p -> p.getAge())) //sort list by ages
.collect(Collectors.groupingBy(p -> p.getAge()),
LinkedHashMap::new, //keeps the order
Collectors.mapping(p -> p.getName(), //map name
Collectors.toList())));

Resources