java 8 stream groupingBy into collection of custom object - java

I have the following class structure
public class Store {
private Long storeId;
private Long masterStoreId;
private String operatorIdentifier;
}
public class StoreInfo {
private String operatorIdentifier;
private Set<Long> slaveStoreIds;
public StoreInfo(String operatorIdentifier, Set<Long> slaveStoreIds) {
super();
this.operatorIdentifier = operatorIdentifier;
this.slaveStoreIds = slaveStoreIds;
}
}
I want to collect the "List<Store" into a "Map<Long, StoreInfo>". Is it possible to do so in a single operation/iteration?
List<Store> stores;
Map<Long, Set<Long>> slaveStoresAgainstMasterStore = stores.stream().collect(Collectors
.groupingBy(Store::getMasterStoreId, Collectors.mapping(Store::getStoreId, Collectors.toSet())));
Map<Long, StoreInfo> storeInfoAgainstMasterStore = stores.stream()
.collect(
Collectors
.toMap(Store::getMasterStoreId,
val -> new StoreInfo(val.getOperatorIdentifier(),
slaveStoresAgainstMasterStore.get(val.getMasterStoreId())),
(a1, a2) -> a1));

As masterStoreId and operatorIdentifier are same same in group(comfirmed in comment) you can groupingBy both creating pair of them using AbstractMap.SimpleEntry. Then using Collectors.toMap create map.
Map<Long, StoreInfo> storeInfoMap =
stores.stream()
.collect(Collectors.groupingBy(
e -> new AbstractMap.SimpleEntry<>(e.getMasterStoreId(),
e.getOperatorIdentifier()),
Collectors.mapping(Store::getStoreId, Collectors.toSet())))
.entrySet()
.stream()
.collect(Collectors.toMap(e -> e.getKey().getKey(),
e -> new StoreInfo(e.getKey().getValue(), e.getValue())));

To complete the implementation, you were attempting. You need to ensure merging capability within StoreInfo such as :
public StoreInfo(String operatorIdentifier, Long slaveStoreId) {
this.operatorIdentifier = operatorIdentifier;
this.slaveStoreIds = new HashSet<>();
this.slaveStoreIds.add(slaveStoreId);
}
public static StoreInfo mergeStoreInfo(StoreInfo storeInfo1, StoreInfo storeInfo2) {
Set<Long> slaveIds = storeInfo1.getSlaveStoreIds();
slaveIds.addAll(storeInfo2.getSlaveStoreIds());
return new StoreInfo(storeInfo1.getOperatorIdentifier(), slaveIds);
}
this would simplify the implementation of collector and you an invoke these correspondingly:
Map<Long, StoreInfo> storeInfoAgainstMasterStore = stores.stream()
.collect(Collectors.toMap(Store::getMasterStoreId,
store -> new StoreInfo(store.getOperatorIdentifier(), store.getStoreId()),
StoreInfo::mergeStoreInfo));

Related

Java 8 : Iterate from 2 lists and create a Map<String, Custom Object>

I have a list of class A and class B with no duplicate elements. "code" attribute will be same across both class A and B. I want to convert them to Map<String, C> using java 8 streams. Please help
public class A {
private String code;
private boolean status;
field 3...
field 4..
}
public class B {
private String code;
private String location;
field 5...
field 5..
}
public class C {
private String location;
private boolean status;
}
List1 : [A(code=NY, status=false), A(code=NJ, status=true),A(code=TX, status=true), A(code=NM, status=false)]
List2 : [B(code=NY, location=NewYork), B(code=NJ, location=NewJersey),B( code=TX, location=Texas), B(code=NM, location=NewMexico)]
Map = map{NY=C(location=NewYork, status=false), NJ=C(location=NewJersey, status=true), TX=C(location=Texas, status=true),NM=C(location=NewMexico, status=false)}
Final map should be in the same order as the elements in List1
~A
UPDATE :
i updated the code to below, but its not compiling. any idea about whats wrong?
package test;
import java.util.*;
import java.util.stream.Collectors;
public class Test {
static void main(String[] args){
new Test().testFunction();
}
void testFunction(){
System.out.println("Hello World");
List<A> listA = Arrays.asList(new A("NY", false), new A("NJ", true), new A("TX", false), new A("AZ", true));
List<B> listB = Arrays.asList(new B("NY", "New York"), new B("NJ", "New Jersey"),
new B("TX", "Texas"), new B("NM", "New Mexico"));
Map<String, B> mapB = listB
.stream()
.collect(Collectors.toMap(B::getCode, b -> b, (b1, b2) -> b1));
Map<String, C> innerJoin = listA
.stream()
.filter(a -> mapB.containsKey(a.getCode())) // make sure a B instance exists with the code
.map(a -> Map.entry(a.getCode(), new C(mapB.get(a.getCode()).getLocation(), a.getStatus())))
.collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue, (c1, c2) -> c1, HashMap::new));
innerJoin.forEach((code, c) -> System.out.println(code + " -> " + c));
}
}
class A {
private String code;
private boolean status;
public A(String code, boolean status) {
this.code = code;
this.status = status;
}
public String getCode() {
return this.code;
}
public boolean getStatus() {
return this.status;
}
}
class B {
private String code;
private String location;
public B(String code, String location) {
this.code = code;
this.location = location;
}
public String getCode() {
return this.code;
}
public String getLocation() {
return this.location;
}
}
class C {
private String location;
private boolean status;
public C(String location, boolean status) {
this.location = location;
this.status = status;
}
public String getLocation() {
return this.location;
}
public boolean getStatus() {
return this.status;
}
}
You should use somethins like:
Map<String, B> mapList2 = list2.stream().collect(Collectors.toMap(B::getCode,
Function.identity()); // create temporary map
Map<String,B> result = list1.stream().collect(
LinkedHashMap::new,
(map, a) -> map.put(
a.getCode(), // get code from A
new C(
mapList2.get(a.getCode()).getLocation() // get location from B
a.getStatus() // get status from A
),
Map::putAll); // use LinkedHashMap to keep order
Map<String, C> map = IntStream.range(0, list1.size())
.mapToObj(i -> new CodeIndex(list1.get(i).code(), i))
.collect(Collectors.toMap(
CodeIndex::code,
ci -> new C(list2.get(ci.index()).location(), list1.get(ci.index()).status()),
(a, b) -> a,
LinkedHashMap::new
));
What happens is that we first get all valid indexes of both lists, then we map to a (custom) container class holding both the index and the code. (I called it CodeIndex, but you may as well use Map.Entry or some Pair class.)
At last, we collect it into a LinkedHashMap (because it retains insertion order), mapping the keys to simply CodeIndex::code and the values to new C(«location_from_list2», «status_from_list1»).
Note that this code traverses the lists once.
Of course, this assumes that:
both lists have the same codes;
all codes of both list1 and list2 are in the same order.
Note that I used record-style getters, that is, getters with the same name as their corresponding field names. For example, location() instead of getLocation().
This task seems like to join two lists of objects by one key property. Taking into account the requirements, it should be like inner join with the help of temporary map created from the listB.
Map<String, B> mapB = listB
.stream()
.(Collectors.toMap(B::getCode, b -> b, (b1, b2) -> b1)); // use merge function as a defense measure
Map<String, C> innerJoin = listA
.stream()
.filter(a -> mapB.containsKey(a.getCode())) // make sure a B instance exists with the code
.map(a -> Map.entry(a.getCode(), new C(mapB.get(a.getCode()).getLocation(), a.isStatus())))
.collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue, (c1, c2) -> c1, LinkedHashMap::new));
Test:
List<A> listA = Arrays.asList(new A("NY", false), new A("NJ", true), new A("TX", false), new A("AZ", true));
List<B> listB = Arrays.asList(new B("NY", "New York"), new B("NJ", "New Jersey"), new B("TX", "Texas"), new B("NM", "New Mexico"));
// ...
innerJoin.forEach((code, c) -> System.out.println(code + " -> " + c));
Output:
NY -> C(location=New York, status=false)
NJ -> C(location=New Jersey, status=true)
TX -> C(location=Texas, status=false)
The "simplest" solution may be based on assumption that the items in both lists are "ordered" by the same code field, then no temporary map is needed:
Map<String, C> simple = IntStream
.range(0, listA.size())
.mapToObj(i -> Map.entry(listA.get(i).getCode(), new C(listB.get(i).getLocation(), listA.get(i).isStatus())))
.collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue, (c1, c2) -> c1, LinkedHashMap::new));
However, such solution is too fragile.
I think you have to use something like this to really maintain the order and group.
List<A> aList = Arrays.asList(new A("NY", false), new A("NJ", true));
List<B> bList = Arrays.asList(new B("NY", "NewYork"), new B("NJ", "NewJersey"));
LinkedHashMap<String, A> mapA =
aList.stream().collect(
Collectors.toMap(
A::getCode,
a -> a,
(e1, e2) -> e1,
LinkedHashMap::new));
LinkedHashMap<String, B> mapB =
bList.stream().collect(
Collectors.toMap(
B::getCode,
b -> b,
(e1, e2) -> e1,
LinkedHashMap::new));
Set<String> keys = new LinkedHashSet<>(mapA.keySet());
keys.addAll(mapB.keySet());
LinkedHashMap<String, C> result2 = keys.stream().collect(
Collectors.toMap(
key -> key,
key -> new C(
mapB.get(key).getLocation(),
mapA.get(key).isStatus()),
(e1, e2) -> e1,
LinkedHashMap::new));
System.out.println(result2);

Group and sort by multiple attribute using stream: Java 8

I have List of MainEntity
public class MainEntity {
private String keyword;
private double cost;
private String company;
}
and I have CompanyEntity
public class CompanyEntity {
private double cost;
private String company;
}
I am trying to transform my list into Map<String,List<CompanyEntity>> where key will be keyword and List<CompanyEntity> will have average of all the costs and sorted too. I am trying to do it in stream and Java 8.
For a particular keyword as input I am doing this.
List<MainEntity> entityList = keyWordMap.get(entity.getKeyword());
entityList.add(entity);
keyWordMap.put(entity.getKeyword(), entityList);
Map<String, Double> average = (keyWordMap.get(keyword)).stream()
.collect(groupingBy(MainEntity::getCompany,
Collectors.averagingDouble(MainEntity::getCtr)));
result.put(keyword, new ArrayList<>());
for (Map.Entry<String, Double> entity : average.entrySet()) {
result.get(keyword).add(new CompanyEntity(entity.getKey(), entity.getValue()));
}
But I trying to create a map for all keywords. Is is possible or iterating whole list again makes sense?
Currently keyowordMap is of type Map<String,MainEntity> which I did by iterating list of MainEntity, but I want Map<String,List<MainEntity>>.
First, make a keyWordMap
Map<String, List<MainEntity>> keyWordMap =
mainEntityList
.stream()
.collect(Collectors.groupingBy(MainEntity::getKeyword));
Then iterate the map, for each keyword, you can directly get the list of CompanyEntity sort by average value and using map() to transform the data and collect as List, then put in result
Map<String,List<CompanyEntity>> result = ....
for (Map.Entry<String, List<MainEntity> entry : keyWordMap.entrySet()) {
List<CompanyEntity> list = entry.getValue().stream()
.collect(groupingBy(MainEntity::getCompany,
Collectors.averagingDouble(MainEntity::getCtr)))
.entrySet()
.stream()
.sorted(Comparator.comparing(e -> e.getValue()))
.map(e -> new CompanyEntity(e.getKey(), e.getValue()))
.collect(Collectors.toList());
result.put(entry.getKey(), list);
}
Or you want to do this in one-shot
Map<String,List<CompanyEntity>> mapData =
mainEntityList
.stream()
.collect(Collectors.groupingBy(MainEntity::getKeyWord,
Collectors.groupingBy(MainEntity::getCtr,
Collectors.averagingDouble(MainEntity::getCtr))))
.entrySet()
.stream()
.collect(Collectors.toMap(m -> m.getKey(),
m -> m.entrySet()
.stream()
.sorted(Comparator.comparing(e -> e.getValue()))
.map(e -> new CompanyEntity(e.getKey(), e.getValue()))
.collect(Collectors.toList())));
The other answer completely changed its answer after initially misunderstanding the question and in good StackOverflow spirits it attracted the first upvote so is now accepted and highest upvoted. But this has a few more steps in the code showing what's happening:
This should get you the result:
import java.io.IOException;
import java.util.AbstractMap.SimpleEntry;
import java.util.Comparator;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.function.Function;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import lombok.Value;
public class CompanyEntityStackOverflowQuestion {
public static void main(String[] args) throws IOException {
//setup test data
MainEntity one = new MainEntity("key1", 10D, "company1");
MainEntity two = new MainEntity("key2", 5D, "company2");
MainEntity three = new MainEntity("key1", 7D, "company3");
MainEntity four = new MainEntity("key2", 3D, "company4");
List<MainEntity> mainEntityList = List.of(one, two, three, four);
//group list by keyword
Map<String, List<MainEntity>> mainEntityByKeyword = mainEntityList.stream()
.collect(Collectors.groupingBy(MainEntity::getKeyword));
//map to companyEntity object
Stream<SimpleEntry<String, List<CompanyEntity>>> mapped = mainEntityByKeyword.entrySet().stream()
.map(entry -> new SimpleEntry<>(entry.getKey(), entry.getValue().stream().map(
getCompanyListFunction()).collect(Collectors.toList())));
//sort and calculate average
Stream<SimpleEntry<String, CompanyEntityListWithStats>> mappedToListWithStats = mapped
.map(entry -> new SimpleEntry<>(entry.getKey(),
new CompanyEntityListWithStats(entry.getValue().stream().mapToDouble(company -> company.cost).average().orElse(0D), //or use Collectors.averagingDouble(company -> company.cost))
sortList(entry.getValue()))));
//collect back to map
Map<String, CompanyEntityListWithStats> collect = mappedToListWithStats
.collect(Collectors.toMap(Entry::getKey, Entry::getValue));
//show result
System.out.println(collect);
}
//sort by cost
private static List<CompanyEntity> sortList(List<CompanyEntity> list) {
list.sort(Comparator.comparing(company -> company.cost));
return list;
}
//map MainEntity to CompanyEntity
private static Function<MainEntity, CompanyEntity> getCompanyListFunction() {
return mainEntity -> new CompanyEntity(mainEntity.cost, mainEntity.company);
}
#Value
public static class MainEntity {
public String keyword;
public double cost;
public String company;
}
#Value
public static class CompanyEntity {
public double cost;
public String company;
}
#Value
public static class CompanyEntityListWithStats {
public double average;
public List<CompanyEntity> companyList;
}
}
Output: {key1=CompanyEntityStackOverflowQuestion.CompanyEntityListWithStats(average=8.5, companyList=[CompanyEntityStackOverflowQuestion.CompanyEntity(cost=7.0, company=company3), CompanyEntityStackOverflowQuestion.CompanyEntity(cost=10.0, company=company1)]), key2=CompanyEntityStackOverflowQuestion.CompanyEntityListWithStats(average=4.0, companyList=[CompanyEntityStackOverflowQuestion.CompanyEntity(cost=3.0, company=company4), CompanyEntityStackOverflowQuestion.CompanyEntity(cost=5.0, company=company2)])}
You may be able to skip some steps, this is just quickly typed out. You can of course inline stuff to make it look a lot shorter/cleaner, but this format shows what's happening.

Java Stream API : How to use method reference?

There is code
class Person {
private ZonedDateTime date ;
private int regionId;
private int centerId;
private int amount1;
private float percent1;
}
List<Person> entityList = new ArrayList<>();
I grouping by year of month like this:
listPerson.stream()
.collect(Collectors.groupingBy(i -> i.getDate().getMonth(),Collectors.collectingAndThen(Collectors.toList(),
l -> {
Integer sumAmount1 = l.stream().collect(Collectors.summingInt(i -> i.getAmount1()));
Double avgPerc1 = l.stream().collect(Collectors.averagingDouble(i -> i.getPercent1()));
List<String> data = new ArrayList<>();
data.add(Integer.toString(sumAmount1));
data.add(Double.toString(avgPerc1));
return data;
}
))).forEach((k,v) -> System.out.println(k.getValue() + "-" + v.toString()));
Also i group by year, regionId, centerId in same manner:
listPerson.stream()
.collect(Collectors.groupingBy(i -> i.getDate().getYear(),Collectors ......
But i got many duplicate code where part with
l -> {...}
repeated many times. How to instead of l -> {...} use a method reference?
IntelliJ can literally just do this for you. You don't even have to think about it.
Keyboard shortcut for hints (yellow) is AltEnter
Here's what I ended up with
public static void main(String[] args)
{
List<Person> listPerson = null;
listPerson.stream()
.collect(Collectors.groupingBy(i -> i.getDate().getMonth(), Collectors.collectingAndThen(Collectors.toList(),
Scratch::apply
)))
.forEach((k,v) -> System.out.println(k.getValue() + "-" + v.toString()));
}
private static List<String> apply(List<Person> l)
{
int sumAmount1 = l.stream().mapToInt(Person::getAmount1).sum();
Double avgPerc1 = l.stream().collect(Collectors.averagingDouble(Person::getPercent1));
List<String> data = new ArrayList<>();
data.add(Integer.toString(sumAmount1));
data.add(Double.toString(avgPerc1));
return data;
}
You can create a method reference like this:
private List<String> methodReference(List<Person> l) {
Integer sumAmount1 = l.stream().collect(Collectors.summingInt(i -> i.getAmount1()));
Double avgPerc1 = l.stream().collect(Collectors.averagingDouble(i -> i.getPercent1()));
List<String> data = new ArrayList<>();
data.add(Integer.toString(sumAmount1));
data.add(Double.toString(avgPerc1));
return data;
}
I have created a methodReference in my Test class. You can replace it with your own class name. And now in your stream() you can refer to it like this:
entityList.stream()
.collect(Collectors.groupingBy(i -> i.getDate().getMonth(), Collectors.collectingAndThen(Collectors.toList(),
Test::methodReference // replace Test with your class name
))).forEach((k, v) -> System.out.println(k.getValue() + "-" + v.toString()));
Might be a little of topic, but I think the beauty of using Stream API is allowing you to build a data pipeline. I would strive to build something that looks like a pipeline, with steps I can customize with pluggable functions.
I think the code would be more readable by refactoring towards a pipeline, and I would try below with the help of a new data structure called Tuple2, epscially its map method. It's easy to build, you can also use one from libraries like vavr.
For reuse, one can consider a function like groupAndSummarize (the name suggests it does two things, so is a smell).
class Tuple2<T1, T2> {
private final T1 t1;
private final T2 t2;
public Tuple2(final T1 t1, final T2 t2) {
this.t1 = t1;
this.t2 = t2;
}
public <U1, U2> Tuple2<U1, U2> map(final Function<T1, U1> func1,
final Function<T2, U2> func2) {
return new Tuple2<>(func1.apply(t1), func2.apply(t2));
}
public T1 _1() { return t1; }
public T2 _2() { return t2; }
}
private <T, K, V> List<Tuple2<K, V>> groupAndSummarize(final List<T> list, final Function<T, K> groupFn, final Function<List<T>, V> summarizeFn) {
return list.stream()
.collect(Collectors.groupingBy(groupFn))
.entrySet()
.stream()
.map(this::toTuple)
.map(t -> t.map(
Function.identity(),
summarizeFn
))
.collect(Collectors.toList());
}
private <K, V> Tuple2<K, V> toTuple(final Map.Entry<K, V> entry) {
return new Tuple2<>(entry.getKey(), entry.getValue());
}
private List<String> summarize(final List<Person> l) {
// your logic
}
public void test() {
final List<Person> entityList = new ArrayList<>();
groupAndSummarize(entityList, i -> i.getDate().getMonth(), this::summarize)
.forEach(t -> System.out.println(t.t1.getValue() + "-" + t.t2.toString()));
}

Java 8 List to Map conversion

I have a problem with conversion List Object to Map String, List Object. I'm looking for Map with a keys name of all components in cars, and a value is represented by cars with this component
public class Car {
private String model;
private List<String> components;
// getters and setters
}
I write a solution but looking for a better stream solution.
public Map<String, List<Car>> componentsInCar() {
HashSet<String> components = new HashSet<>();
cars.stream().forEach(x -> x.getComponents().stream().forEachOrdered(components::add));
Map<String, List<Car>> mapCarsComponents = new HashMap<>();
for (String keys : components) {
mapCarsComponents.put(keys,
cars.stream().filter(c -> c.getComponents().contains(keys)).collect(Collectors.toList()));
}
return mapCarsComponents;
}
You could do it with streams too, but I find this a bit more readable:
public static Map<String, List<Car>> componentsInCar(List<Car> cars) {
Map<String, List<Car>> result = new HashMap<>();
cars.forEach(car -> {
car.getComponents().forEach(comp -> {
result.computeIfAbsent(comp, ignoreMe -> new ArrayList<>()).add(car);
});
});
return result;
}
Or using stream:
public static Map<String, List<Car>> componentsInCar(List<Car> cars) {
return cars.stream()
.flatMap(car -> car.getComponents().stream().distinct().map(comp -> new SimpleEntry<>(comp, car)))
.collect(Collectors.groupingBy(
Entry::getKey,
Collectors.mapping(Entry::getValue, Collectors.toList())
));
}
I know this is a Java question, and there is already a Java answer. However, I would like to add that Kotlin, which is a JVM language and perfectly interoperable with Java, you can do things like this very easily and cleanly:
val carsByComponent = cars
.flatMap { it.components }
.distinct()
.map { component -> component to cars.filter { car -> component in car.components } }
.toMap()
or even more concise, allthough less readable:
val carsByComponent = cars
.flatMap { car -> car.components.map { it to car } }
.groupBy { it.first }
.mapValues {it.value.map { it.second }}

Java8 Stream API: Group a list into a custom class

I'm trying to construct a custom class instance by Java8's stream API.
public class Foo {
Group group;
// other properties
public Group getGroup() { return this.group; }
public enum Group { /* ... */ };
}
public class FooModel {
private Foo.Group group;
private List<Foo> foos;
// Getter/Setter
}
...
List<Foo> inputList = getFromSomewhere();
List<FooModel> outputList = inputList
.stream()
.collect(Collectors.groupingBy(Foo::getGroup,
???));
But I don't know how the Collector downstream must be.
Do I have to implement a Collector myself (don't think so) or can this be accomplished by a combination of Collectors. calls?
You are looking for something like this:
List<FooModel> outputList = inputList
.stream()
.collect(Collectors.groupingBy(Foo::getGroup))// create Map<Foo.Group,List<Foo>>
.entrySet().stream() // go through entry set to create FooModel
.map(
entry-> new FooModel (
entry.getKey(),
entry.getValue()
)
).collect(Collectors.toList());

Categories