Null safe concatenation of two nullable list of items - java

I'm trying to add two nullable list using below:
List<Employee> employees = null;
if (<some condition>) {
employees = employeeService.getEmployees(<some criteria>);
// Add another list of employees
if (<some condition>) {
List<Employee> employeesSubList = employeeService.getEmployees(<some other criteria>));
if (!isEmpty(employeesSubList)) {
if (!isEmpty(employees)) {
employees.addAll(employeesSubList);
} else {
employees = employeesSubList;
}
}
}
}
This works but the code looks horribly ugly. There is a hard condition that the parent List<Employee> employees will be null instead of empty list in case no employees are present.
Is there a cleaner way to do the same?
I tried Java 8 approach but IntelliJ throws some warnings on .orElse(emptyList()).addAll(employeesSubList):
List<Employee> employees = null;
if (<some condition>) {
employees = employeeService.getEmployees(<some criteria>);
// Add another list of employees
if (<some condition>) {
List<Employee> employeesSubList = employeeService.getEmployees(<some other criteria>));
if (!isEmpty(employeesSubList)) {
Optional.ofNullable(employees).orElse(emptyList()).addAll(employeesSubList);
}
}
}

The "problem" Intellij is probably telling you is that emptyList().addAll() is 1) an error because that's an immutable list 2) is never assigned to anything, and it returns a boolean, so you need a different way to get that data
Plus, you could get rid of the if statement because adding an empty list to another list is a no-op
For example
// assumes employees is not null
employees.addAll(
Optional.ofNullable(employeeService.getEmployees(<some other criteria>))).orElse(emptyList())
);

Is there a cleaner way?
IMO no.
You could argue that testing for null is cleaner than calling an isEmpty method, but I'm not convinced.
You could argue that hiding the null tests behind an Optional is cleaner, but I'm not convinced.
But the real problem is this:
There is a hard condition that the parent List employees will be null instead of empty list.
That is what is causing you to have to deal with the null-safety issue. The clean solution is to use an empty list; i.e. a list with no elements.
Then you can rewrite your example code as:
List<Employee> employees = Collections.emptyList();
if (<some condition>) {
employees = employeeService.getEmployees(<some criteria>);
// Add another list of employees
if (<some condition>) {
List<Employee> employeesSubList = employeeService.getEmployees(<some other criteria>));
if (!employees.isEmpty()) {
employees.addAll(employeesSubList);
} else {
employees = employeesSubList;
}
}
}
(Note: if you use Collections.emptyList(), be aware that it returns an immutable list. So you can't add elements to it. The alternative is to use new ArrayList<>() to create a mutable list that is initially empty.)
In short, if you want "clean" you are looking the wrong part of your code-base. IMO.

Related

Java stream - Performant way to update hierarchical object

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.

How to loop through a List of Objects

I have a class called Group. Inside Group there is a list the contains objects of type Customer, like so:
Group.java:
private List<Customers> customers;
public List<Customer> getCustomers() {
return customers;
}
Then, like above, the class Customer has list that contains object of type EmailAddress, like so:
Customer.java:
private List<EmailAddress> emailAddresses;
public List<EmailAddress> getEmailAddress() {
return emailAddresses;
}
I want to be able to put this list of email addresses into my own list that I can then manipulate how I want. So in my Main.java I have:
Main.java:
List<Customer> customerList = group.getCustomers(); //group is the object to get <Customer> List
for (int i=0; i<customerList.size(); i++) {
List<EmailAddress> emails = customerList.get(i).getEmailAddresses();
}
Will I then, outside of this for loop, have a List of all the emails of the customers that I can use? Is this the correct way to populate a list that I can then utilize and pull data from outside of this loop? Outside of the loop I want to be able to look at different parts of the list like:
emails.get(3);
or
emails.get(7);
at my own discretion.
You need to initialize the list outside the for cycle
List<EmailAddress> emails = new ArrayList<>();
for (int i=0; i<customerList.size(); i++) {
emails.add(customerList.get(i).getEmailAddresses());
}
List<Customer> customerList = group.getCustomers();
List<EmailAddress> finalEmailAddressList = new ArrayList<>();
for (Customer : customerList) {
if (customer != null) {
finalEmailAddressList.add(customer.getEmailAddresses());
}
}
This should give you a list of all email addresses in the end. You can also use the addAll method to add a collection to a collection (at specific indexes also), in this case, a list.
If access to data is required from an outer scope, the data has to exist in the outer scope. Therefore, you need to declare it in the outer scope of the loop:
List<Customer> customerList = group.getCustomers();
List<EmailAddress> emails = new ArrayList<>(customerList.size());
for (Customer customer : customerList) {
emails.addAll(customer.getEmailAddresses();
}
You can do it with streams:
List<Group> groupList = ...
List<EmailAddress> address = groupList
.stream()
.flatMap(g -> g.getCustomers().stream())
.flatMap(c -> c.getEmailAddresses().stream())
.collect(Collectors.toList());
You can do it in functional style:
List<EmailAddress> emails = costumerList.stream()
.map(Customer::getEmailAddresses)
.flatMap(Collection::stream)
.collect(Collectors.toList());
The most interesting part here is the flatMap() method which does two things:
It maps a List<EmailAddress> to a Stream<EmailAddress> by using method reference of the Collection.stream() method
It flattens the result so that the result is of type Stream<EmailAdress>
Consequently, the result of the collect() method will be of type List<EmailAdress>
In contrast, if the flatMap() line was omitted, the result would be of type List<List<EmailAddress>>.
Similarly, if the map() method was used instead of flatmap(), the result would be of type List<Stream<EmailAddress>>

How to know if an object exist in arraylist or no based on one variable in it

I am having array list let's consider it for users . Each user object has a variable called id I tried to use contains methods with the same object exactly but I surprised that it isn't working.. How can I know if the list contains a user with an id that I am looking for without looping on the list elements? I know that I can use loop but I am looking for a better way to do that
Contains in array list is always linear complexity meaning that it always loops through the elements. If you want your contains to work the way you intended you could override equals and make it use the id field in User class. Another way to solve this problem would be to not use array list but HashMap with your ID as a key and a user as value or maybe even a HashSet as it always uses equals before adding elements to the collection.
public class UserList extends ArrayList<User> {
#Override
public boolean contains(Object o) {
Object[] arr = this.toArray();
for (Object obj:arr) {
if(obj.toString().equals(String.valueOf(o))){
return true;
}
}
return false;
}}
class User {
String id;
String name;
#Override
public String toString() {
return id;
}
}
If you did not override equals in your User class, it will check if it is the same instance, not if the field values are equals.
If you don't want to override equals, using only the id, you can keep the users in a
Map<String,User> users; => users.containsKey(id)
where the key is the id, or keep just the ids in a separate
Set<String> ids; and call contains. => ids.contains(id)
As you mentioned, it is possible to iterate the array list to get the answer. If you do not want to do it, you can just copy the values into a hash map like:
Map<String, User> userMap = new HashMap();
for (int i=0; i<arrayList.size(); i++) {
userMap.put(arrayList.get(i).getId(), arrayList.get(i));
}
String isExist = userMap.containsKey(id);
Lets say you want to select user with id=23, using stream,
List<User> usersLlist = New LinkedList<>();
User user;
user = usersLlist.stream().filter( user -> user.id == 23).findAny();
if you are using a get method to get user id
user = usersLlist.stream().filter( user -> user.getId() == 23).findAny();

Updating a subsection of a list with an "id" field

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

Remove in depth elements using streams

I have the following classes.
Class A {
List<B> b
//getters and setters
}
CLass B {
List<C> c
//getters and setters
}
Class C {
List<D> d
//getters and setter
}
Class D {}
What i want to do is remove list d if a specific search term is not in the list. I have tried to do it but no luck. I think it removes but the reference is not saved.
a.stream()
.flatMap(a-> a.getB().stream())
.flatMap(b-> b.getC().stream())
.map(c-> c.getD())
.collect(Collectors.toList())
.removeIf(list -> {
boolean toBeRemoved = true;
boolean containsMatch = list.stream().anyMatch(w-> {return w.getId().equalsIgnoreCase(searchTerm);});
if(containsMatch) {
toBeRemoved = false;
}
return toBeRemoved;
});
Can someone help me?
A stream represents a view on the "underlying" collection. This means that when you call removeIf() on the stream, the "underlying" collection isn't affected at all.
You would need to do two things: first you "collect" all items you intend to delete, and then you simply remove them (in an explicit call) from the list that needs to be changed:
List<B> toBeDeleted = a.stream()....collect(Collectors.toList());
a.b.removeAll(toBeDeleted);
( the above is meant as pseudo code, I didn't run it through the compiler )
As said: the real problem here is your misconception: operations on the stream normally do not affect the underlying collection.
What you did builds a List<List<D>> and you remove List<D> elements that does not correponds, but that never changes the objects you have.
You need to iterate over all C elements,
You keep the ones that does not correpond (use noneMatch() to check this)
for these ones you replace the list by an empty one (or clear the actual c.getD().clear())
a.stream()
.flatMap(a-> a.getB().stream())
.flatMap(b-> b.getC().stream())
.filter(c -> c.getD().stream().noneMatch(w -> w.getId().equalsIgnoreCase(searchTerm)))
.forEach(c-> c.setD(new ArrayList<>())); // or .forEach(c-> c.getD().clear());

Categories

Resources