Java Stream multi list iteration - java

I have 2 lists. 1 list is of Ids and the other list is full of Foo objects, call it list A. The Foo class looks like this:
public class Foo {
private String id;
/* other member variables */
Foo(String id) {
this.id = id;
}
public String getId() {
return id;
}
}
I have a plain list of ids like List<Integer>, call it list B. What I want to do is iterate over list B one element at a time, grab the id, compare it to list A and grab Foo object with the equivalent id and then add the Foo object to a new list, list C.
I'm trying to concatenate streams but I'm new to streams and I'm getting bogged down with all the methods like map, filter, forEach. I'm not sure what to use when.

The straightforward way would be what you have in your post: loop over the ids, select the first Foo having that id and if one if found, collect it into a List. Put into code, it would look like the following: each id is mapped to the corresponding Foo that is found by calling findFirst() on the foos having that id. This returns an Optional that are filtered out it the Foo doesn't exist.
List<Integer> ids = Arrays.asList(1, 2, 3);
List<Foo> foos = Arrays.asList(new Foo("2"), new Foo("1"), new Foo("4"));
List<Foo> result =
ids.stream()
.map(id -> foos.stream().filter(foo -> foo.getId().equals(id.toString())).findFirst())
.filter(Optional::isPresent)
.map(Optional::get)
.collect(Collectors.toList());
The big problem with this approach is that you need to traverse the foos list as many times as there are id to look. A better solution would first be to create a look-up Map where each id maps to the Foo:
Map<Integer, Foo> map = foos.stream().collect(Collectors.toMap(f -> Integer.valueOf(f.getId()), f -> f));
List<Foo> result = ids.stream().map(map::get).filter(Objects::nonNull).collect(Collectors.toList());
In this case, we look-up the Foo and filter out null elements that means no Foo was found.
Another whole different approach is not to traverse the ids and search the Foo, but filter the Foos having an id that is contained in the wanted list of ids. The problem with approach is that it requires to, then, sort the output list so that the order of the resulting list matches the order of the ids.

I would implement it like this :
List<Foo> list = Arrays.asList(
new Foo("abc"),
new Foo("def"),
new Foo("ghi")
);
List<String> ids = Arrays.asList("abc", "def", "xyz");
//Index Foo by ids
Map<String, Foo> map = list.stream()
.collect(Collectors.toMap(Foo::getId, Function.identity()));
//Iterate on ids, find the corresponding elements in the map
List<Foo> result = ids.stream().map(map::get)
.filter(Objects::nonNull) //Optional...
.collect(Collectors.toList());

Related

How can I Merge two Lists of custom objects of a Different Type using Java 8

I have two lists of custom objects. And I want to merge both lists by id, using Java 8.
I have a class Employee with the fields (All String): id, name, city.
And I have another class Person with the fields (All String): id, city.
Here is an example :
List<Employee> employeeList = Stream.of(
new Employee("100","Alex",""),
new Employee("200","Rida",""),
new Employee("300","Ganga",""))
.collect(Collectors.toList());
List<Person> personList = Stream.of(
new Person("100","Atlanta"),
new Person("300","Boston"),
new Person("400","Pleasanton"))
.collect(Collectors.toList());
After merging the two lists I want to get the result shown below.
How can I do it?
List<Employee>
[
Employee(id=100, name=Alex, city=Atlanta),
Employee(id=200, name=Rida, city=null),
Employee(id=300, name=Ganga, city=Boston),
Employee(id=400, name=null, city=Pleasanton)
]
In order to achieve that, firstly, you can create two maps based on the two lists using id as a key.
Then create a stream over the key sets of these maps. Then inside the map() operation you need to create a new Employee object for every key by using passing a name extracted from the employeeList city taken from the personById .
When id is not present in either of the maps the object returned by get() will be null and attempt to invoke method on it will triger the NullPointerException. In order to handle this situation, we can make use of Null-object pattern, by defining two variables that could be safely accessed and will be provided as an argument to getOfDefault().
Then collect the stream element into a list with Collectors.toList().
public static void main(String[] args) {
List<Employee> employeeList = Stream.of(
new Employee("100","Alex",""),
new Employee("200","Rida",""),
new Employee("300","Ganga",""))
.collect(Collectors.toList());
List<Person> personList = Stream.of(
new Person("100","Atlanta"),
new Person("300","Boston"),
new Person("400","Pleasanton"))
.collect(Collectors.toList());
Map<String, Employee> employeeById = employeeList.stream()
.collect(Collectors.toMap(Employee::getId, Function.identity()));
Map<String, Person> personById = personList.stream()
.collect(Collectors.toMap(Person::getId, Function.identity()));
Person nullPerson = new Person("", null); // null-object
Employee nullEmployee = new Employee("", null, null); // null-object
List<Employee> result = Stream.concat(employeeById.keySet().stream(),
personById.keySet().stream())
.distinct() // eliminating duplicated keys
.map(key -> new Employee(key,
employeeById.getOrDefault(key, nullEmployee).getName(),
personById.getOrDefault(key, nullPerson).getCity()))
.collect(Collectors.toList());
result.forEach(System.out::println);
}
Output
Employee{id='100', name='Alex', city='Atlanta'}
Employee{id='200', name='Rida', city='null'}
Employee{id='300', name='Ganga', city='Boston'}
Employee{id='400', name='null', city='Pleasanton'}
Not sure if Employee class here extends Person. Ideally, it should.
An Employee can be a Person, but not all Person has to be an Employee. So, collecting it as a List<Employee> makes no sense, due to this.
Ideally, you should collect it as a List<Person> and Employee should extend Person. Please relook at your class structure instead of focusing on collecting it as List<Employee>, that should solve your problem.
you can use mapping and grouping from stream or search the second list by id and update the employee to the first list. Java does not include built-in functions that will automatically merge different objects

Mapping pair of elements into one with Stream IPA

So I'm wondering what is the best solution to the following problem:
I have a list of items (a custom class) in a java collection ex
List<Item> itemList = ... item1,item2,item3 etc
Each item in the collection however has a corresponding logical pair also in the collection (so the pair's are not necessarily following each other by index in the collection)
I have a helper method like
Item calculateCorrectItem(Item item1, Item item2)
which can return the correct one of a pair based on some business logic (the details of that is not relevant)
I would like to replace an item and its pair in the collection, with the result of the method above - so that every 2 elements of a pair in the collection are replaced with the calculated one based on those two.
Some details:
We can assume that every element has one and only one pair.
Each item has the pair's ID as a property, like
public class Item {
private String id;
private String pairId;
the equal method is true when the ID of two items are the same.
...getters,setters
}
Also, the references in the collection which i want to filter are also existing in a global cache, where every Item can be easily retrieved from, like
globalCache.getItemById(String id)
So an actual pair reference can be easily retrieved if the ID of the pair is known.
What could be an elegant solution (maybe by utilizing the Stream IPA)? In the end, the only expectation is that the collection contains one Item of each pair, the ordering doesn't matter.
With streams, you would have to do this using indexed access:
List<Item> calculated =
IntStream.range(0, itemList.size() / 2)
.mapToObj(i -> calculateCorrectItem(itemList.get(2*i+0), itemList.get(2*i+1))
.collect(toList());
If you want to merge items based on their IDs, you can group the items by their ID:
itemList.stream()
.collect(groupingBy(Item::getId)) // Yields a Map<IdType, List<Item>>
.values() // Yields a Collection<List<Item>>, where each List<Item> contains items with the same id.
.stream()
.map(is -> /* invoke calculateCorrectItem, handling 1, 2 or more items in the list */)
.collect(...)
Here's another approach that performs a mutable reduction using a map (you can use a hash map if preserving the source list's order of pair IDs is unimportant):
Collection<Item> correctItems1 = itemList.stream().collect(
LinkedHashMap<String, Item>::new,
(map, item) -> map.merge(item.getPairId(), item, this::calculateCorrectItem),
Map::putAll)
.values();
List<Item> result = new ArrayList<>(correctItems1);
I'm assuming that method calculateCorrectItem(Item item1, Item item2) will produce the same result regardless of the order of the arguments and that duplicated results has to be removed from the resulting list.
List<Item> items = ... ; // obtain the items
Map<String, Item> itemById = items.stream()
.collect(Collectors.toMap(Item::getId, // relies on uniquness of Id
Function.identity()));
// set is used to alliminate duplicates since their order is not important
Set<Item> itemSet = items.stream()
.map(item-> pairById.containsKey(item.getPairId()) ? item : // if pair isn't present return the same item, othewise merge them
calculateCorrectItem(item, pairById.get(item.getPairId())))
.collect(Collectors.toSet());
List<Item> result = new ArrayList<>(itemSet);
Here is another approach using a Collectors.toMap with a merge function:
First, create a record for demo and intialize a list with some data
record Item(String getId, int getValue) {
}
Random r = new Random();
List<Item> items = r.ints(10, 1, 5)
.mapToObj(id -> new Item(id+"", r.nextInt(100) + 1))
.toList();
System.out.println("The raw data");
items.forEach(System.out::println);
System.out.println();
Now stream the list
use the third argument of toMap to "merge" the new items.
Collection<Item> collection = items.stream()
.collect(Collectors.toMap(Item::getId, item->item,
(item1, item2) -> calculateCorrectItem(item1,
item2)))
.values();
System.out.println("The new list of combined items");
collection.forEach(System.out::println);
Prints
The raw data
Item[getId=1, getValue=14]
Item[getId=4, getValue=42]
Item[getId=4, getValue=19]
Item[getId=2, getValue=16]
Item[getId=4, getValue=20]
Item[getId=3, getValue=57]
Item[getId=3, getValue=47]
Item[getId=3, getValue=22]
Item[getId=1, getValue=3]
Item[getId=4, getValue=73]
The new list of combined items
Item[getId=1, getValue=17]
Item[getId=2, getValue=16]
Item[getId=3, getValue=126]
Item[getId=4, getValue=154]
The method used for the above. It just sums the values and returns a new Item instance.
public static Item calculateCorrectItem(Item one, Item two) {
return new Item(one.getId(), one.getValue() + two.getValue());
}
And a simple non-stream solution prints out the same results as before.
Map<String, Item> result = new HashMap<>();
for (Item item : items) {
result.compute(item.getId(),
(k, v) -> v == null ? item : new Item(v.getId(),
v.getValue() + item.getValue()));
}
result.values().forEach(System.out::println);

Java8 lambda approach

I have this piece of code that filters from a list of objects based on a set of String identifiers passed in and returns a map of string-id and objects. Something similar to follows:
class Foo {
String id;
String getId() {return id};
};
// Get map of id --> Foo objects whose string are in fooStr
Map<String,Foo> filterMethod (Set<String> fooStr) {
List<Foo> fDefs; // list of Foo objects
Map<String,Foo> fObjMap = new HashMap<String, Foo>(); // map of String to Foo objects
for (Foo f : fDefs) {
if (fooStr.contains(f.getId()))
fObjMap.put(f.getId(),f);
}
return (fObjMap);
}
Is there a better Java8 way of doing this using filter or map?
I could not figure it out and tried searching on stackoverflow but could not find any hints, so am posting as a question.
Any help is much appreciated.
~Ash
Just use the filter operator with the same predicate as above and then the toMap collector to build the map. Also notice that your iterative solution precludes any possibility of key conflict, hence, I have omitted that, too.
Map<String, Foo> idToFooMap = fDefs.stream()
.filter(f -> fooStr.contains(f.getId()))
.collect(Collectors.toMap(Foo::getId, f -> f));
When including items conditionally in the final output use filter and when going from stream to a map use Collectors.toMap. Here's what you end up with:
Map<String,Foo> filterMethod (final Set<String> fooStr) {
List<Foo> fDefs; // list of Foo objects
return fDefs.stream()
.filter(foo -> fooStr.contains(foo.getId()))
.collect(Collectors.toMap(Foo::getId, Function.identity()));
}
Though ggreiner has already provided a working solution, when there are duplicates you'd better handle it including a mergeFunction.
Directly using Collectors.toMap(keyMapper, valueMapper), one or another day you will encounter this following issue.
If the mapped keys contains duplicates (according to Object.equals(Object)), an IllegalStateException is thrown when the collection operation is performed. If the mapped keys may have duplicates, use toMap(Function, Function, BinaryOperator) instead.
Based on the OP's solution, I think it would be better using
import static java.util.stream.Collectors.*; // save some typing and make it cleaner;
fDefs.stream()
.filter(foo -> fooStr.contains(foo.getId()))
.collect(toMap(Foo::getId, foo -> foo, (oldFoo, newFoo) -> newFoo));
Maybe something like this?
Map<String,Foo> filterMethod (Set<String> fooStr) {
List<Foo> fDefs; // get this list from somewhere
Map<String, Foo> fObjMap = new HashMap<> ();
fDefs.stream()
.filter(foo -> fooStr.contains(foo.getId()))
.forEach(foo -> fObjMap.put(foo.getId(), foo))
return fObjMap;
}

Java 8 stream groupBy pojo

I have a collection of pojos:
public class Foo {
String name;
String date;
int count;
}
I need to iterate over collection, groupBy Foos by name and sum counts, then create new collection with pojos with summed count.
Here is how I do it now:
List<Foo> foosToSum = ...
Map<String, List<Foo>> foosGroupedByName = foosToSum.stream()
.collect(Collectors.groupingBy(Foo::getName));
List<Foo> groupedFoos = foosGroupedByName.keySet().stream().map(name -> {
int totalCount = 0;
String date = "";
for(Foo foo: foosGroupedByName.get(name)) {
totalCount += foo.getCount();
date = foo.getDate() //last is used
}
return new Foo(name, date, totalCount);
}).collect(Collectors.toList());
Is there a more beauty way to do it with streams?
UPDATE Thanks everyone for help. All answers were great.
I decided to create merge function in pojo.
The final solution looks like:
Collection<Foo> groupedFoos = foosToSum.stream()
.collect(Collectors.toMap(Foo::getName, Function.identity(), Foo::merge))
.values();
You can do it either using groupingBy or using toMap collector, as for which to use is debatable so I'll let you decide on the one you prefer.
For better readability, I'd create a merge function in Foo and hide all the merging logic inside there.
This also means better maintainability as the more complex the merging gets, you only have to change one place and that is the merge method, not the stream query.
e.g.
public Foo merge(Foo another){
this.count += another.getCount();
/* further merging if needed...*/
return this;
}
Now you can do:
Collection<Foo> resultSet = foosToSum.stream()
.collect(Collectors.toMap(Foo::getName,
Function.identity(), Foo::merge)).values();
Note, the above merge function mutates the objects in the source collection, if instead, you want to keep it immutable then you can construct new Foo's like this:
public Foo merge(Foo another){
return new Foo(this.getName(), null, this.getCount() + another.getCount());
}
Further, if for some reason you explicitly require a List<Foo> instead of Collection<Foo> then it can be done by using the ArrayList copy constructor.
List<Foo> resultList = new ArrayList<>(resultSet);
Update
As #Federico has mentioned in the comments the last merge function above is expensive as it creates unnecessary objects that could be avoided. So, as he has suggested, a more friendly alternative is to proceed with the first merge function I've shown above and then change your stream query to this:
Collection<Foo> resultSet = foosToSum.stream()
.collect(Collectors.toMap(Foo::getName,
f -> new Foo(f.getName(), null, f.getCount()), Foo::merge))
.values();
Yes, you could use a downstream collector in your groupingBy to immediately sum the counts. Afterwards, stream the map and map to Foos.
foosToSum.stream()
.collect(Collectors.groupingBy(Foo::getName,
Collectors.summingInt(Foo::getCount)))
.entrySet()
.stream()
.map(entry -> new Foo(entry.getKey(), null, entry.getValue()))
.collect(Collectors.toList());
A more efficient solution could avoid grouping into a map only to stream it immediately, but sacrifices some readability (in my opinion):
foosToSum.stream()
.collect(Collectors.groupingBy(Foo::getName,
Collectors.reducing(new Foo(),
(foo1, foo2) -> new Foo(foo1.getName(), null, foo1.getCount() + foo2.getCount()))))
.values();
By reducing Foos instead of ints, we keep the name in mind and can immediately sum into Foo.

Emit one List item at a time from a Flowable

I have a method which returns a Flowable<RealmResults<MyClass>>. For those not familiar with Realm, RealmResults is just a simple List of items.
Given a Flowable<RealmResults<MyClass>>, I'd like to emit each MyClass item so that I can perform a map() operation on each item.
I am looking for something like the following:
getItems() // returns a Flowable<RealmResults<MyClass>>
.emitOneAtATime() // Example operator
.map(obj -> obj + "")
// etc
What operator will emit each List item sequentially?
You would flatMap(aList -> Flowable.fromIterable(aList)). Then you can map() on each individual item. There is toList() if you want to recollect the items (note: this would be a new List instance). Here's an example illustrating how you can use these methods to get the different types using List<Integer>.
List<Integer> integerList = new ArrayList<>();
Flowable<Integer> intergerListFlowable =
Flowable
.just(integerList)//emits the list
.flatMap(list -> Flowable.fromIterable(list))//emits one by one
.map(integer -> integer + 1);
The question is, do you want to keep the results as a Flowable<List<MyClass>> or as a Flowable<MyClass> with retained order?
If the first,
getItems()
.concatMap(results -> Flowable
.fromIterable(results)
.map(/* do your mapping */)
.toList()
)
If the second, this should suffice:
getItems()
.concatMap(Flowable::fromIterable)
.map(/* do your mapping */)

Categories

Resources