Java8 Stream API: Group a list into a custom class - java

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());

Related

java 8 stream groupingBy into collection of custom object

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

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

How to filter nested objects with Stream?

I want to filter nested objects with using Stream API. Problem is there are too many nested classes and with below method I am writing too many duplicate code.
Is there any way to handle this stream without duplicate codes?
public class Country{
Map<String, City> cities;
}
public class City{
Map<String, School> schools;
}
public class School{
String name;
String address;
Model model;
}
public class Model{
String name;
Teacher teacher;
}
public class Teacher{
String name;
String id;
}
My Stream;
country.getCities().values().stream().foreach(
(City city) ->
city.getSchools()
.entrySet()
.stream()
.filter(schoolEntry -> schoolEntry.getValue().getName().equals("test"))
.filter(schoolEntry -> schoolEntry.getValue().getModel().getName().equals("test2"))
.filter(schoolEntry -> schoolEntry.getValue().getModel().getTeacher().getName().equals("test2"))
.foreach(schoolEntry -> {
String schoolKey = schoolEntry.getKey();
resultList.put(schoolKey, schoolEntry.getValue().getModel().getTeacher().getId());
})
);
You could define a method to use it as Predicate to filter the schools.
public static boolean matches(School school, String schoolName, String modelName, String teacherId) {
return school.getName().equals(schoolName)
&& school.getModel().getName().equals(modelName)
&& school.getModel().getTeacher().getId().equals(teacherId);
}
Applying this to the stream:
public static Map<String, String> getSchoolAndTeacherFrom(Country country, Predicate<School> schoolWithModelAndTeacher) {
return country.getCities().values().stream()
.flatMap(c -> c.getSchools().entrySet().stream())
.filter(s -> schoolWithModelAndTeacher.test(s.getValue()))
.collect(Collectors.toMap(Entry::getKey, schoolEntry -> schoolEntry.getValue().getModel().getTeacher().getId()));
}
Using this like that:
Country country = <county>
Predicate<School> schoolWithModelAndTeacher = school -> matches(school, "test1", "test2", "test2");
getSchoolAndTeacherFrom(country, schoolWithModelAndTeacher);
Some further thoughts:
If the map schools uses School.getName() as keys, then we can write:
public static Map<String, String> getSchoolAndTeacherFrom(Country country, Predicate<School> schoolWithModelAndTeacher) {
return country.getCities().values().stream()
.flatMap(city -> city.getSchools().values().stream())
.filter(schoolWithModelAndTeacher::test)
.collect(Collectors.toMap(School::getName, school -> school.getModel().getTeacher().getId()));
}
Assuming that school names and teacher ids in a country are unique (while model names are common), the filtering would result in a single value if any. But then there is no need for Map as result type. A result of type Entry<String String> would do it.
And if the parameters of the predicate are still known (school, model, teacher), then this whole thing is just a question whether there is a given teacher on a given school for a given model in a certain country. Then we can write it even shorter:
public static boolean isMatchingSchoolInCountryPresent(Country country, Predicate<School> schoolWithModelAndTeacher) {
return country.getCities().values().stream()
.flatMap(c -> c.getSchools().values().stream())
.anyMatch(schoolWithModelAndTeacher::test);
}
You can just use a "bigger lambda":
.filter(schoolEntry -> {
School value = schoolEntry.getValue();
return value.getName().equals("test")
&& value.getModel().getName().equals("test2")
&& value.getModel().getTeacher().getName().equals("test2")
}
Alternatively you can also create a makePredicate method inside the School class like so:
public static Predicate<School> makePredicate(String first, String second) {
return (school) -> school.name.equals(first) && this.model.getName().equals("test2") && this.model.getTeacher().getName().equals("test2");
}
And use it as a filter predicate:
.filter(School.makePredicate("test", "test2"))
replace the name with a more appropriate name if you can find one
First you need to create a Predicate based on the condition you want to make a filter on the stream
Predicate<School> func1 = (school)-> "test".equals(school.name)
&& "test2".equals(school.model.getName())
&& "test2".equals(school.model.getTeacher().getName());
an then you can easily achieve your aim by:
country.cities.
entrySet()
.stream()
.map(Map.Entry::getValue)
.flatMap(x->x.schools.entrySet().stream())
.filter(s->func1.test(s.getValue()))
.collect(toMap(Map.Entry::getKey, schoolEntry -> schoolEntry.getValue().getModel().getTeacher().id));

Java 8 Streams Grouping by an Optional Attribute

I am trying to groupby a computed value from an attribute. The computed value is optional - to be a little more clear, his is a simplified example:
class Foo:
int id;
Group group;
.. some other stuff
class Group:
String groupId;
... (some other stuff)
class SomeName:
String someAttribute;
class Converter:
public Optional<SomeName> getSomenameFromGroup(Group)
I cannot change the methods in Converter since it doesn't belong to me.
I have a list of Foo, that I want to filter by SomeName's "someAttribute".
For example, I have something like this:
Map<String, List<Foo>> fooBySomeName =
fooList.stream().collect(Collectors
.groupingBy(foo -> {
Optional<SomeName> name =
converter.getSomenameFromGroup(foo.getGroup.getGroupId());
return name.isPresent() ? name.get().someAttribute() : "";
}));
But the thing is, I don't want anything in my map if the name isn't present in the groupingBy statement. I had something like this:
fooBySomeNames.remove("")
which I think can remove anything from the map which was grouped by that key, but is there a cleaner or more correct way to do this in the groupingBy statement?
You can remove entries with a filter as follows.
Map<String, List<Foo>> fooBySomeName = fooList.stream()
.filter(foo -> fooToSomeAttribute(foo).isPresent())
.collect(Collectors.groupingBy(foo -> fooToSomeAttribute(foo).get()));
private static Optional<String> fooToSomeAttribute(Foo foo)
{
return Optional.ofNullable(foo)
.map(Foo::getGroup)
.flatMap(new Converter()::getSomenameFromGroup)
.map(SomeName::getSomeAttribute);
}
Or, with a pair object, you can avoid double computation of someAttribute for every Foo:
Map<String, List<Foo>> fooBySomeName = fooList.stream()
.filter(Objects::nonNull)
.map(FooAndSomeAttribute::new)
.filter(pair -> pair.getSomeAttribute().isPresent())
.collect(Collectors.groupingBy(
pair -> pair.getSomeAttribute().get(),
Collectors.mapping(
FooAndSomeAttribute::getFoo,
Collectors.toList())));
private static class FooAndSomeAttribute
{
private final Foo foo;
private final Optional<String> someAttribute;
public FooAndSomeAttribute(Foo foo)
{
this.foo = foo;
this.someAttribute = Optional.ofNullable(foo)
.map(Foo::getGroup)
.flatMap(new Converter()::getSomenameFromGroup)
.map(SomeName::getSomeAttribute);
}
public Foo getFoo()
{
return foo;
}
public Optional<String> getSomeAttribute()
{
return someAttribute;
}
}

map and apply in java stream

I currently have something like below
List<String> myNewList = myList
.stream()
.map(item->{
return mappedItem
})
.collect(Collectors.toList());
repository.save(myNewList);
In Optional, I can perform operations on the mapped item by using ifPresent method like below
myOptional
.map(item -> {
return mappedItem
})
.ifPresent(newItem -> {
repository.save(newItem);
});
I was wondering if I can do something like the above on stream. Rather than declaring myNewList, is there a way I can collect the new List and apply my function on the new list?
Update: Based on the answer from #tagir-valeev, I modified my code as below
myList
.stream()
.map(item->{
return mappedItem
})
.collect(Collectors.collectingAndThen(Collectors.toList(),
list -> {
repository.save(list);
return list;
}
));
You can create your custom collector like this:
myList.stream().map(..)
.collect(Collectors.collectingAndThen(Collectors.toList(), repository::save));
If save return type is void, it would be more ugly as you need to return something from collect:
myList.stream().map(..)
.collect(Collectors.collectingAndThen(Collectors.toList(),
list -> {repository.save(list);return list;}));
You may declare special method in your Repository class:
class Repository {
Collector<MyItemType, ?, List<MyItemType>> saving() {
return Collectors.collectingAndThen(Collectors.toList(),
list -> {this.save(list);return list;});
}
void save(List<MyItemType> list) { ... }
}
And use it:
myList.stream().map(..).collect(repository.saving());

Categories