Java 8 Streams Grouping by an Optional Attribute - java

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

Related

handle duplicate key in Collectors.toMap() function

I am creating a map which its (key,value) will be (name, address) in my Person object:
Map<String, String> myMap = persons.stream.collect(Collector.toMap(person.getName(), person.getAddress(), (address1, address2) -> address1));
In the duplicate key situation, I would like to skip to add the second address to the map and would like to log the name also. Skipping the duplicate address I can do already using mergeFunction, but in oder to log the name I need in this mergeFunction the person object, something like:
(address1, address2) -> {
System.out.println("duplicate "+person.name() + " is found!");
return address1;
}
I am getting stuck by passing person object to this merge function.
I believe the forEach approach along with Map.merge would be much simpler and appropriate for the current use case :
Map<String, String> myMap = new HashMap<>();
persons.forEach(person -> myMap.merge(person.getName(), person.getAddress(), (adrs1, adrs2) -> {
System.out.println("duplicate " + person.getName() + " is found!");
return adrs1;
}));
Note: Map.merge also uses BiFunction (parent of BinaryOperator as used in toMap), hence you could correlate the merge function here to your existing desired functionality easily.
#Aomine: solution looks good and works for me too. Just wanted to confirm that with this it iterates twice right ?? Cause with simple solution like below it iterates only once but achieve what is required.
Map<String, String> myMap = new HashMap<>();
persons.forEach(item -> {
if(myMap.containsKey(item.getName()))
{/*do something*/}
else
myMap.put(item.getName(), item.getAddress());
});
if you want to access the whole person object in the merge function then pass Function.identity() for the valueMapper:
Map<String, Person> myMap =
persons.stream()
.collect(toMap(p -> p.getName(),
Function.identity(), // or p -> p
(p1, p2) -> { /* do logic */ }));
But as you can see the resulting map values are Person objects, if you still want a Map<String, String> as a result and still access the whole Person object in the mergeFunction then you can do the following:
persons.stream()
.collect(toMap(p -> p.getName(), Function.identity(),(p1, p2) -> { /* do logic */ }))
.entrySet()
.stream()
.collect(toMap(Map.Entry::getKey, p -> p.getValue().getAddress()));
Here is another possibility using peek
import java.util.*;
import java.util.stream.*;
class App {
public static void main(String[] args) {
List<Person> persons = Arrays.asList(new Person("foo", "bar"), new Person("baz", "qux"), new Person("foo", "zuz"));
Set<String> names = new HashSet<>();
Map<String, String> nameAddress = persons.stream().peek(p -> {
if (names.contains(p.getName())) {
System.out.println("Duplicate key being skipped: " + p);
} else {
names.add(p.getName());
}
}).collect(Collectors.toMap(person -> person.getName(), person -> person.getAddress(), (addr1, addr2) -> addr1));
}
}
class Person {
String name;
String address;
public Person(String name, String address) {
this.name = name;
this.address = address;
}
public String getName() {
return name;
}
public String getAddress() {
return address;
}
#Override
public String toString() {
return name + " " + address;
}
}
Output for me will be as follows for the snippet above:
Duplicate key being skipped: foo zuz

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

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

Why cannot I access the type field when using Collectors.toMap?

why cannot I access the id property of Device ?
final List<Device> devicesList = jsonFileHandlerDevice.getList();
ConcurrentMap<Integer, Device> map =
devicesList.stream()
.collect(Collectors.toMap(item -> item.id, item -> item));
where
public class Device {
public MobileOs mobileOs;
public Integer id;
public Device() {
}
public Device(MobileOs mobileOs, double osVersion, int allocatedPort, Integer id, String uuid) {
this.mobileOs = mobileOs;
this.id = id;
}
}
see here:
You got a misleading error message. The actual error is using a ConcurrentMap<Integer, Device> type when the type returned by the collector is Map<Integer, Device>.
If you want the returned Map to be a ConcurrentMap, you can use the toMap variant that accepts a supplier (which determines the type of the Map to be returned).
Something like this should work :
ConcurrentMap<Integer, Device> map =
devicesList.stream()
.collect(Collectors.toMap(item -> item.id,
item -> item,
(item1,item2)->item2,
ConcurrentHashMap::new));
or as Alexis commented, just use Collector.toConcurrentMap.

Transform Stream into a Map, when key/value mapping functions have a computational step in common

I have a collection of Employee objects and need to turn it into a map of hyperlink widgets for presentation purposes.
For each employee, an entry is added to the result, with the key being an identifier (here a National Insurance number), and the value being the hyperlink widget. Here's a first attempt:
static Map<String, Hyperlink> toHyperlinksByNIN(Collection<Employee> employees) {
return employees.stream()
.collect(Collectors.toMap(
Employee::determineUniqueNINumber,
employee -> new Hyperlink(
employee.getName(), employee.determineUniqueNINumber())));
}
Unfortunately, this solution won't do, because the NI number is actually not part of the employee model, but needs to be fetched from an expensive remote service on every call to Employee.determineUniqueNINumber. This method is simply too costly to call more than once per employee record.
How can I obtain the desired Map
doing a single pass through the collection using the Stream API,
while applying the common part in the key/value mapping functions (Employee.determineUniqueNINumber) only once per stream element?
Does Hyperlink class stores UniqueNINumber in instance field and expose getter method? Then you can first create Hyperlink object, and then create the map :
return employees
.stream()
.map(employee -> new Hyperlink(employee.getName(), employee
.determineUniqueNINumber()))
.collect(Collectors.toMap(Hyperlink::getUniqueNINumber, i -> i));
Here is Hyperlink class :
public class Hyperlink {
private String name;
private String uniqueNINumber;
public Hyperlink(String name, String uniqueNINumber) {
this.name = name;
this.uniqueNINumber = uniqueNINumber;
}
public String getName() {
return name;
}
public String getUniqueNINumber() {
return uniqueNINumber;
}
// Other stuff
}
As others have suggested, the easiest way is to map the elements of the stream to a container object so that you can then collect the cached NINumber from that container object together with the other details.
If you don't want to write your own custom class for every such use, you can utilize an existing type, such as AbstractMap.SimpleEntry.
You will then be able to write:
return employees.stream()
.map(emp -> new AbstractMap.SimpleEntry<>(emp.determineUniqueNINumber(),emp.getName()))
.collect(Collectors.toMap(
mapEntry -> mapEntry.getKey(),
mapEntry -> new Hyperlink(mapEntry.getValue(), mapEntry.getKey())));
This saves you writing your own class for a simple case like this. Of course, if you need more than just the getName() from Employee, your second element can be the Employee object itself.
I would go with caching but you can always create your own collector / use a custom reduction operation:
return employees.stream()
.collect(HashMap::new,
(map, e) -> {
String number = e.determineUniqueNINumber();
map.put(number, new Hyperlink( e.getName(), number));
},
Map::putAll);
I think the easiest solution to your problem is to implement a simple caching procedure inside the method determineUniqueNINumber:
public class Employee {
private String niNumber;
public String determineUniqueNINumber() {
if (niNumber == null) {
niNumber = resultOfLongAndCostlyMethod();
}
return niNumber;
}
}
This way, on the second call, the costly method is not called and you simply return the already calculated value.
Another solution is to store the insurance number inside a custom typed Tuple class. It would store the employee along with its insurance number.
static Map<String, Hyperlink> toHyperlinksByNIN(Collection<Employee> employees) {
return employees.stream()
.map(e -> new Tuple<>(e, e.determineUniqueNINumber()))
.collect(Collectors.toMap(
t -> t.getValue2(),
t -> new Hyperlink(t.getValue1().getName(), t.getValue2())));
}
class Tuple<T1, T2> {
private final T1 value1;
private final T2 value2;
public Tuple(T1 value1, T2 value2) {
this.value1 = value1;
this.value2 = value2;
}
public T1 getValue1() {
return value1;
}
public T2 getValue2() {
return value2;
}
}
You could transform to a helper data class to ensure that you only get the number once:
private static class EmployeeAndNINumber {
private Employee employee;
private String niNumber;
public EmployeeAndNINumber(Employee employee) {
this.employee = employee;
this.niNumber = employee.determineUniqueNINumber();
}
public Employee getEmployee() { return this.employee; }
public String getNINumber() { return this.niNumber; }
public Hyperlink toHyperlink() {
return new Hyperlink(employee.getName(), this.getNINumber());
}
}
Then you could transform to this data class, getting the NI number once and only once, then use that information to build up your map:
employees.stream()
.map(EmployeeAndNINumber::new)
.collect(toMap(EmployeeAndNINumber::getNINumber,
EmployeeAndNINumber::toHyperlink));

Categories