I am trying to learn how to use the lambda functions for sleeker code but struggling to make this work.
I have two lists. The "old" list is always shorter or the same length as the "updated list".
I want to take the objects from the "updated list" and overwrite the "stale objects" in the shorter "old list".
The lists have a unique field for each object.
For example, it is a bit like updating books in a library with new editions. The UUID (title+author) remains the same but the new object replaces the old on the shelf with a new book/object.
I know I could do it the "long way" and make a HashMap<MyUniqueFieldInMyObject, MyObject> and then take the new List<MyUpdatedObjects> and do the same.
I.e. Have HashMap<UniqueField, MyOldObject> and HashMap<UniqueField, MyUpdatedObject>, then iterate over the old objects with a pseudo "if updated objects have an entry with the same key, overwrite the value with the updated value"...
But...
Is there a "nicer" shorted way to do this with functional lambda statements?
I was thinking along the lines of:
List<MyObject> updatedList;
List<MyObject> oldList;
updatedList.forEach(MyObject -> {
String id = MyObject.getId();
if (oldList.stream().anyMatcher(MyObject ->
MyObject.getId().matches(id)) {
//Do the replacement here? If so...how?
}
}
Which is where I am lost!
Thanks for any guidance.
If you want to update the list in place rather than making a new list, you can use List.replaceAll:
oldList.replaceAll(old ->
updateListe.stream()
.filter(updated -> updated.getId().equals(old.getId())
.findFirst()
.orElse(old)
);
The main problem with this solution is that its complexity is O(size-of-old*size-of-updated). The approach you described as "long way" can protect you from having to iterate over the entire updated list for every entry in the old list:
// note that this will throw if there are multiple entries with the same id
Map<String, MyObject> updatedMap = updatedList.stream()
.collect(toMap(MyObject::getId, x->x));
oldList.replaceAll(old -> updatedMap.getOrDefault(old.getId(), old));
I recommend you to iterate over the oldList - the one you want to update. For each of the object iterated match the equivalent one by its id and replace it using Stream::map. If an object is not found, replace it with self (doesn't change the object) using Optional::orElse.
List<MyObject> newList = oldList
.stream() // Change values with map()
.map(old -> updatedList.stream() // Iterate each to find...
.filter(updated -> old.getId() == updated.getId()) // ...by the same id
.findFirst() // Get new one to replace
.orElse(old)) // Else keep the old one
.collect(Collectors.toList()); // Back to List
List<Foo> updatedList = List.of(new Foo(1L, "new name", "new desc."));
List<Foo> oldList = List.of(new Foo(1L, "old name", "old desc."));
List<Foo> collect = Stream.concat(updatedList.stream(), oldList.stream())
.collect(collectingAndThen(toMap(Foo::getId, identity(), Foo::merge),
map -> new ArrayList(map.values())));
System.out.println(collect);
This will print out:
[Foo{id=1, name='new name', details='old desc.'}]
In Foo::merge you can define which fields need update:
class Foo {
private Long id;
private String name;
private String details;
/*All args constructor*/
/*getters*/
public static Foo merge(Foo newFoo, Foo oldFoo) {
return new Foo(oldFoo.id, newFoo.name, oldFoo.details);
}
}
I think it's best to add the objects to be updated into a new list to avoid changing a list you are streaming on and then you can simply replace the old with the new list
private List<MyObject> update(List<MyObject> updatedList, List<MyObject> oldList) {
List<MyObject> newList = new ArrayList<>();
updatedList.forEach(object -> {
if (oldList.stream().anyMatch(old -> old.getUniqueId().equals(object.getUniqueId()))) {
newList.add(object);
}
}
return newList;
}
Related
I need to update an internal object matching criteria. This internal object is deep inside a large object with a hierarchy. The object is something like
ObjectA {
List ObjectB {
List Object C{
int customerId;
String customerStatus;
}
}
}
I need to update "customerStatus" only if customerId is matched to "123".
This entire objectA is stored in the database as a single object (in the real world, this is a protobuf object. Therefore this object is not updated in place)
The non-stream way involves a bunch of loops
List<ObjectB> objectBList = objectA.getObjectBList();
List<ObjectB> updatedObjectBList = new ArrayList<>();
for(objectB: objectBList) {
List<ObjectC> objectCList = objectB.getObjectCList();
List<ObjectC> updatedObjectCList = new ArrayList<>();
for(objectC: objectCList) {
if(objectC.getCustomerId() == 123) {
objectC = createNewObjectCwithUpdatedStatus("UpdatedStatus");
}
updatedObjectCList.add(objectC);
}
updatedObjectBList.addObjectCList(updatedObjectCList);
}
updatedObjectA.addObjectBList(updatedObjectBList);
writeUpdateObjectA_to_storage(updatedObjectA);
Is there a way to write this multiple IF condition using streams option?
It's a bit unclear from your code why you are adding the lists back to the objects once you do the update. As far as I can see you are updating the c objects in place (i.e. they are mutable) so it's not clear why they need to be re-added to the A and B objects.
Assuming that's a mistake, you could just flatten out the hierarchy and then do the updates:
getObjectBList().stream().flatMap(ObjectB::getObjectCList)
.filter(c -> c.getCustomerId() == 123)
.forEach(c -> c.setCustomerStatus("updated"));
If there's a reason to create a new list then that can be achieved as well but how to do it best depends on why you want to do that.
This is another option if you don't want to flat it
// Say you have objA reference
objA.getObjectBList().forEach(objBList -> objBList.getObjectCList().
stream().filter(objCList-> objCList.getCustomerId() == 123)
.forEach(c -> c.setCustomerStatus("updated"));
If all objects are immutable, you can try following solution.
record C(int customerId, String customerStatus){}
record B(List<C> getObjectCList){}
record A(List<B> getObjectBList){}
public static void main(String[] args){
var objectA = new A(new ArrayList<>());
var newObjectBList = objectA.getObjectBList().stream().map(objectB -> {
var newObjectCList = objectB.getObjectCList().stream().map(objectC -> {
return objectC.customerId == 123 ? new C(objectC.customerId, "UpdatedStatus") : objectC;
}).toList();
return new B(newObjectCList);
}).toList();
var newObjectA = new A(newObjectBList);
}
Actually, this is a functional programming style.
I have a java list consisting of objects. Most of the objects have common fields and I need to keep just the one object from the list of candidates that have a specific field set. How can I achieve this? Example
class A{
String field1;
String field2;
String field3;
LocalDate dateField;
}
With the following values;
A first = new A("field1","field2","field3",null);
A second = new A("field1","field2","field3",LocalDate.now());
A third= new A("field1","field2","field3",LocalDate.now().plusMonths(3));
A forth= new A("4","5","6",LocalDate.now().plusMonths(3));
A fifth = new A("7","8","9",LocalDate.now().plusMonths(3));
I need to write a method that returns a list consisting of second, forth and fifth. So if field1 field2 and field3 are identical, I need to keep the minimum localdate field. How to achieve this?
I understand you have a List<A> of objects of type A.
It seems want to filter the list, querying it for items A that match some requirement. What kinds of things you want to search when filtering that list isn't very clear.
List<A> items = ...; // setup the items
List<A> items_filtered = items.stream()
.filter( x -> x.field1.equals("value") )
.collect(Collectors.toList());
List<A> items_filtered_2 = items.stream()
.filter( x -> !x.field2.equals("other_value") )
.collect(Collectors.toList());
These filters can be applied to any list, including list that is the result of a previous filter, or you can combine two checks in the same filter.
List<A> fitlered_both = items.stream()
.filter( x -> x.field1.equals("value") && !x.field2.equals("other_value") )
.collect(Collectors.toList());
You can try this
public static void main(String[] args) {
A first = new A("field1","field2","field3",null);
A second = new A("field1","field2","field3",LocalDate.now());
A third= new A("field1","field2","field3",LocalDate.now().plusMonths(3));
A forth= new A("4","5","6",LocalDate.now().plusMonths(3));
A fifth = new A("7","8","9",LocalDate.now().plusMonths(3));
List<A> collect = Stream.of(first, second, third, forth, fifth)
.collect(
Collectors.groupingBy(a -> Objects.hash(a.field1, a.field2, a.field3),
Collectors.minBy((o1, o2) -> {
if(o1 == null || o2 == null || o1.dateField == null || o2.dateField == null){
return 1;
}
return o1.dateField.compareTo(o2.dateField);
})))
.values().stream().map(Optional::get).collect(Collectors.toList());
System.out.println(collect);
}
You want to group the objects by similar objects first, Objects.hash(field1, field2, field3) will group objects based on field1, field2, and field3.
Next you want to sort the grouping using Localdate.
Next, we collect the first elements of each group, which is our answer.
I would suggest to write a comparator and sort the list using it. Then you will be able to retrieve all the desired objects within one pass
I can use the below snippet to retrieve the name if there is 1 entry in the list by retrieving element 0 in the list, however, each NameResponse can have several names (e.g. a first name, a middle name and a surname). How can I retrieve x names associated with one customer? There could be 20 names for argument's sake. I would like to implement using a stream since I am using Java 8, but I am unsure how to implement this. Any suggestions?
private List<String> getNames(Customer customer) {
List<NameResponse> nameResponses = new ArrayList<>();
NameResponse nameResponse = new NameResponse();
nameResponse.setName("Test Name");
nameResponses.add(nameResponse);
customer.setNames(nameResponses);
return List.of(customer.getNames().get(0).getName());
}
Customer class:
private List<NameResponse> names;
NameResponse class:
private String name;
Something like below assuming you have the appropriate getters:
return customer.getNames()
.stream()
.map(NameResponse::getName)
.collect(Collectors.toList());
You could do that using the map operator on the stream and then collect to output a list:
return customer.getNames().stream()
.map(nameResponse -> nameResponse.getName())
.collect(Collectors.toList()));
I have the following problem:
I want to remove duplicate data from a list of a Vo depending if the registered field is the same, I show you the solution that I am trying. Then this is the data from the list that I am making
List<MyVo> dataList = new ArrayList<MyVo>();
MyVo data1 = new MyVo();
data1.setValidated(1);
data1.setName("Fernando");
data1.setRegistered("008982");
MyVo data2 = new MyVo();
data2.setValidated(0);
data2.setName("Orlando");
data2.setRegistered("008986");
MyVo data3 = new MyVo();
data3.setValidated(1);
data3.setName("Magda");
data3.setRegistered("008982");
MyVo data4 = new MyVo();
data4.setValidated(1);
data4.setName("Jess");
data4.setRegistered("006782");
dataList.add(data1);
dataList.add(data2);
dataList.add(data3);
dataList.add(data4);
The first thing I have to do and separate it into two different lists depending on whether the data is validated or not, for that the value of the registered validated.
List<MyVo> registeredBusinesses = new ArrayList<MyVo>();
List<MyVo> unregisteredBusinesses = new ArrayList<MyVo>();
for (MyVo map : dataList) {
if (map.getValidated == 0) {
unregisteredBusinesses.add(map);
}else {
registeredBusinesses.add(map);
}
}
now the list of registered businesses I want to remove the data that is repeated with the same value from its registered field and make a new list. this is what it took but it doesn't work right
List<MyVo> duplicateList = registeredBusinesses.stream().filter(distictByRegistered(MyVo::getRegistered)).collect(Collectors.toList());
public static <T> Predicate<T> distictByRegistered(Function<? super T, ?> keyExtractor) {
Set<Object> seen = ConcurrentHashMap.newKeySet();
return t -> seen.add(keyExtractor.apply(t));
}
however using this method I get the following output:
{["validated":1,"name":"Fernando","registered":"008982"],
["validated":1,"name":"Jess","registered":"006782"]}
the output I want to obtain is the following:
the unregisteredBusinesses list:
{["validated":0,"name":"Orlando","registered":"008986"]}
the registeredBusinesses list:
{["validated":1,"name":"Jess","registered":"006782"]}
the registeredDuplicateBusinesses list:
{["validated":1,"name":"Fernando","registered":"008982"],
["validated":1,"name":"Magda","registered":"008982"]}
I don't know how to do it, could you help me? I would like to use lambdas to reduce the code, for example of the first for when I separate into two lists
You are looking for both registered and unregistered businesses. This is where instead of making use of 0 and 1, you could choose to implement the attribute as a boolean isRegistered such as 0 is false and 1 is true going forward. Your existing code with if-else could be re-written as :
Map<Boolean, List<MyVo>> partitionBasedOnRegistered = dataList.stream()
.collect(Collectors.partitioningBy(MyVo::isRegistered));
List<MyVo> unregisteredBusinesses = partitionBasedOnRegistered.get(Boolean.FALSE); // here
List<MyVo> registeredBusinesses = partitionBasedOnRegistered.get(Boolean.TRUE);
Your approach looks almost correct, grouping by Function.identity() will properly flag duplicates (based on equals() implementation!), you could also group by an unique property/id in your object if you have one, what you're missing is to manipulate the resulting map to get a list with all duplicates. I've added comments describing what's happening here.
List<MyVo> duplicateList = registeredBusinesses.stream()
.collect(Collectors.groupingBy(Function.identity()))
.entrySet()
.stream()
.filter(e -> e.getValue().size() > 1) //this is a stream of Map.Entry<MyVo, List<MyVo>>, then we want to check value.size() > 1
.map(Map.Entry::getValue) //We convert this into a Stream<List<MyVo>>
.flatMap(Collection::stream) //Now we want to have all duplicates in the same stream, so we flatMap it using Collections::stream
.collect(Collectors.toList()); //On this stage we have a Stream<MyVo> with all duplicates, so we can collect it to a list.
Additionally, you could also use stream API to split dataList into registered and unRegistered.
First we create a method isUnregistered in MyVo
public boolean isUnregistered() {
return getrRegistered() == 0;
}
Then
Map<Boolean, List<MyVo>> registeredMap = dataList.stream().collect(Collectors.groupingBy(MyVo::isUnregistered));
Where map.get(true) will be unregisteredBusinesses and map.get(false) registeredBusinesses
Familiarizing yourself with the concept of the Collectors.partitioningBy shall help you problem-solve this further. There are two places amongst your current requirement where it could be implied.
You are looking for both registered and unregistered businesses. This is where instead of making use of 0 and 1, you could choose to implement the attribute as a boolean isRegistered such as 0 is false and 1 is true going forward. Your existing code with if-else could be re-written as :
Map<Boolean, List<MyVo>> partitionBasedOnRegistered = dataList.stream()
.collect(Collectors.partitioningBy(MyVo::isRegistered));
List<MyVo> unregisteredBusinesses = partitionBasedOnRegistered.get(Boolean.FALSE); // here
List<MyVo> registeredBusinesses = partitionBasedOnRegistered.get(Boolean.TRUE);
After you try to groupBy the registered businesses based on the registration number(despite of identity), you require both the duplicate elements and the ones which are unique as well. Effectively all entries, but again partitioned into two buckets, i.e. one with value size == 1 and others with size > 1. Since grouping would ensure, minimum one element corresponding to each key, you can collect the required output with an additional mapping.
Map<String, List<MyVo>> groupByRegistrationNumber = // group registered businesses by number
Map<Boolean, List<List<MyVo>>> partitionBasedOnDuplicates = groupByRegistrationNumber
.entrySet().stream()
.collect(Collectors.partitioningBy(e -> e.getValue().size() > 1,
Collectors.mapping(Map.Entry::getValue, Collectors.toList())));
If you access the FALSE values of the above map, that would provide you the groupedRegisteredUniqueBusiness and on the other hand values against TRUE key would provide you groupedRegisteredDuplicateBusiness.
Do take a note, that if you were to flatten this List<List<MyVo> in order to get List<MyVo> as output, you could also make use of the flatMapping collector which has a JDK inbuilt implementation with Java-9 and above.
So, I have sorted by condition list of objects
private Observable<CallServiceCode> getUnansweredQuestionList() {
return Observable.fromIterable(getServiceCodeArrayList())
.subscribeOn(Schedulers.computation())
.filter(iServiceCode -> iServiceCode.getServiceCodeFormStatus().isUnanswered());
}
and now what I need to do:
Every object has list servicePartList , I need to filter this list by condition and eventually if final size of this filtered list >0, so I need to add object that contains this list CallServiceCode object as a key and this filtered list as a value.
So it should be like this:
private Map<CallServiceCode, ArrayList<CallServicePart>> getSortedMap() {
Map<CallServiceCode, ArrayList<CallServicePart>> result = new HashMap<>();
getUnansweredQuestionList()
.filter(callServiceCode -> Observable.fromIterable(callServiceCode.getCallServicePartList()) //
.filter(servicePart -> servicePart.getServicePartFormStatus().isUnanswered())//
.isNotEmpty())
.subscribe(callServiceCode -> result.put(callServiceCode, Observable.fromIterable(callServiceCode.getCallServicePartList()) //
.filter(servicePart -> servicePart.getServicePartFormStatus().isUnanswered()));
return result;
}
But there is no such method isNotEmpty() in RxJava2 and also it is not right to add key like this:
Observable.fromIterable(callServiceCode.getCallServicePartList())
.filter(servicePart -> servicePart.getServicePartFormStatus().isUnanswered())
So question is how to make it properly?
One solution could be to use collect to create the Map directly from the observable:
return getUnansweredQuestionList()
.collect(HashMap<CallServiceCode, List<CallServicePart>>::new,(hashMap, callServiceCode) -> {
List<CallServicePart> callServiceParts = Observable.fromIterable(callServiceCode.getServicePartList())
.filter(s -> !s.getServicePartFormStatus().isUnanswered())
.toList().blockingGet();
if (!callServiceParts.isEmpty())
hashMap.put(callServiceCode, callServiceParts);
}).blockingGet();
If you extract filtering into a method (could be also member of CallServiceCode) then the code is much cleaner:
return getUnansweredQuestionList()
.collect(HashMap<CallServiceCode, List<CallServicePart>>::new, (hashMap, callServiceCode) -> {
List<CallServicePart> filteredParts = getFilteredServiceParts(callServiceCode.getServicePartList());
if (!filteredParts .isEmpty())
hashMap.put(callServiceCode, filteredParts);
}).blockingGet();