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);
Related
I have two lists that are populated from two different sources
List<MyObject> updatedObjs;
List<MyObject> currentObjs;
All of the elements in updatedObjs have the same string identifier as an element within the currentObjs list. This string Identifier can be simply accessed by getID() which returns a String.
What I would like to do is to find each match in the currentObjs list for each element in updatedObjs and then call a method passing the matching objects from both lists in.
I have figured out code for finding the match in both lists:
updatedObjs.stream().filter(updatedObj -> currentObjs.stream().anyMatch(currentObj -> updatedObj.getID().equals(currentObj.getID()))).collect(Collectors.toList());
But this will just find the matches...is there any way of calling anyMatch and then calling a method on both matching objects?
So I want to called a method like myMethod(currentObj, updatedObj);
I know I can do this with nested for loops but I am looking for a more elegant solution if one exists. Thanks
I think you need it:
updatedObjs.forEach(s -> currentObjs.stream().filter(s1 -> s.getId().equals(s2.getId)).forEach(s1 -> yourMethod(s, s1)));
My approach would be to create a Map whose key is the id and whose values are all the objects from each list that have that key. Then iterate over the entries in the map and call your myMethod() on each list that has more than 1 entry.
List<MyObj> updatedObjs = new ArrayList<>(); // assuming this has values
List<MyObj> currentObjs = new ArrayList<>(); // assuming this has values
List<MyObj> allObjs = new ArrayList( updatedObjs ); // create new list with references to the objects in updatedObjs
allObjs.addAll( currentObjs ); // add everything from currentObjs to our new list
// map of id -> list of all elements with that id
Map<String,List<MyObj>> idMap = allObjs
.stream()
.collect( Collectors.groupingBy( obj -> obj.getID() ) );
Now there's two different approaches below to calling the method
// you could stream the entry set, filter down to only entries with more than one value in the list and run the method on each
idMap.entrySet()
.stream()
.filter( ( mapEntry ) -> mapEntry.getValue().size() > 1 )
.forEach( mapEntry -> mapEntry.getValue().forEach( obj -> obj.getID() ) );
// I prefer a for-loop for this instead of streaming the entry set because it is a lot easier to read
for ( Map.Entry<String,List<MyObj>> entry : idMap.entrySet() ) { // iterate each entry
String key = entry.getKey();
List<MyObj> value = entry.getValue();
if ( value.size() > 1 ) {
// has more than one entry with the given key, so run myMethod() on each
value.forEach( obj -> obj.getID() );
}
}
By iterating over the first collection then the second, you're basically having an expensive algorithm O(n²)
You might want to consider something more straight forward like having a Map<String, List<MyObj>>
This hereunder is O(n)
Map<String, List<MyObj>> map = updatedObjs.stream()
.collect(Collectors.groupingBy(MyObj::getId));
currentObjs.forEach(obj -> map.get(obj.getId).add(obj)); // No Stream
map.forEach((k, v) -> myMethod(v.get(1), v.get(0));
That out of the way, the fact that you have two lists maintaining the state of the same entity is probably a bad idea. You should probably rethink your design from scratch.
Try the following approach:
void perform(List<MyObject> currentObjects, List<MyObject> updatedObjects) {
Map<String, MyObject> currentObjById = currentObjects.stream()
.collect(Collectors.toMap(MyObject::getID, Function.identity()));
Map<String, MyObject> updatedObjById = updatedObjects.stream()
.collect(Collectors.toMap(MyObject::getID, Function.identity()));
for (Map.Entry<String, MyObject> entry : currentObjById.entrySet()) {
MyObject updatedObj = updatedObjById.get(entry.getKey());
if (updatedObj != null) {
myMethod(entry.getValue(), updatedObj);
}
}
}
It is more efficient than the solutions which use anyStream. This will work provided that you have no two elements with the same ID in each of the lists.
I would like how to convert Java List to Map. Were key in a map is some property of the list element (different elements might have the same property) and value is a list of those list items (having the same property).
eg.List<Owner> --> Map<Item, List<Owner>>. I found a few List to Map questions, but it was not I want to do.
What I came with is:
List<Owner> owners = new ArrayList<>(); // populate from file
Map<Item, List<Owner>> map = new HashMap<>();
owners.parallelStream()
.map(Owner::getPairStream)
.flatMap(Function.identity())
.forEach(pair -> {
map.computeIfPresent(pair.getItem(), (k,v)-> {
v.add(pair.getOwner());
return v;
});
map.computeIfAbsent(pair.getItem(), (k) -> {
List<Owner> list = new ArrayList<>();
list.add(pair.getOwner());
return list;
});
});
PasteBin
I can put forEach part to a separate method, but it still feels too verbose. Plus I made a Pair class just to make it work. I tried to look in to Collectors but couldn't get my head around to do what I wanted.
From where this is, you can simplify your code by using groupingBy:
Map<Item, List<Owner>> map = owners.stream()
.flatMap(Owner::getPairStream)
.collect(Collectors.groupingBy(Pair::getItem,
Collectors.mapping(Pair::getOwner,
Collectors.toList())));
You can also dispense with the Pair class by using SimpleEntry:
Map<Item, List<Owner>> map = owners.stream()
.flatMap(owner -> owner.getItems()
.stream()
.map(item -> new AbstractMap.SimpleEntry<>(item, owner)))
.collect(Collectors.groupingBy(Entry::getKey,
Collectors.mapping(Entry::getValue,
Collectors.toList())));
Note that I'm assuming that Item has equals and hashCode overridden accordingly.
Side notes:
You can use map.merge instead of successively calling map.computeIfPresent and map.computeIfAbsent
HashMap and parallelStream make a bad combination (HashMap isn't thread-safe)
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 */)
I need to sort every value of a Map with multiple objects in each value, but I am struggling to see how. My Map looks like this:
HashMap<String, List<Carrier>> carriers_list;
I already have a Custom Comparator set up to compare a List of my objects.
static class CarrierComparator implements Comparator<Carrier> {
#Override
public int compare(Carrier c1, Carrier c2) {
return c1.get_name().compareTo(c2.get_name());
}
}
I can use this Comparator like so:
List<Carrier> carrierList = getAllCarriers();
Collections.sort(carrierList, new CustomComparators.CarrierComparator());
How can I get each List for every key in my Map?
To get all the values combined in one big list:
List<Carrier> values = map.values().stream()
.flatMap(List::stream)
.collect(Collectors.toList());
You can do the sorting in the same expression:
List<Carrier> values = map.values().stream()
.flatMap(List::stream)
.sorted(comparator)
.collect(Collectors.toList());
You can iterate over the map's values:
CustomComparators.CarrierComparator comparator = new CustomComparators.CarrierComparator();
for (List<Carrier> l : carriers_list.values())
Collections.sort(l, comparator);
What you want to do is around the likes of :
Map<Object,List<Object>> multimap = new HashMap<>();
multimap.values().stream() // stream the values
.flatMap(List::stream) // stream on the items in every list
.sorted(CarrierComparator::compare) // use your comparator
.collect(Collectors.toList()); // collect in a single List
Here is a basic code which shows how to extract all Carrier objects into one list and then sort them.
Set<String> keys = map.keySet();
List<Carrier> carriers = new ArrayList<Carrier>();
for (String key : keys) {
List<Carrier> temp = map.get(key);
for (Carrier carrier : temp) {
carriers.add(carrier);
}
}
carriers.sort(new CarrierComparator());
"carriers" will contain sorted list of objects.
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());