Optional.map - how does it work exactly? - java

I'm trying to get into Optional issues in Java 8. I've written an extremely simple program, consisting of one class and main() method.
I expect the output data to be [aaa, DDD, ccc]. However, I'm getting [aaa, bbb, ccc]. But if change to s = new TestClass("DDD") line to the commented one, I get what I want.
So how does map() work? Can it map an object only by editing it? Why doesn't it work properly if I create a new instance and return it?
class:
public class TestClass {
String str;
public TestClass(String str) {
this.str = str;
}
#Override
public String toString() {
return str;
}
}
main() method:
public static void main(String[] args) {
List<TestClass> list = new ArrayList<TestClass>();
list.add( new TestClass("aaa") );
list.add( new TestClass("bbb") );
list.add( new TestClass("ccc") );
list.stream()
.filter( s -> s.str.equals("ccc") || s.str.equals("bbb") )
.findFirst()
.map( s -> {
// s.str = "DDD"; this works just fine
s = new TestClass("DDD");
return s;
} );
System.out.println(list);
}

You are assigning a new Object within a method.
Your new reference will be valid only within the method scope, but as soon as you return, the reference will point to its original instance.
In java this is the expected behavior.
E.g:
String s = "foo";
changeString(s);
print(s); // prints "foo"
where
void changeString(String s) {
s = "bar";
}
Nobody forbids you to change your object properties though, as you do with s.str = "DDD" (unless your object is immutable, of course).
In your specific case, you are not doing anything with the map lambda result, therefore your changes are lost.
Actually map is useless in your case even when you just do s.str = "DDD" as it could be done within a forEach.
But. since you are working on only one result, which may not even exist (Optional), you should use
...findFirst().ifPresent(s -> s.str = "DDD" );
You should use map only when you need to transform an object into a different type for further processing.

Please change your code to :
Optional<Object> result = list.stream()
.filter( s -> s.str.equals("ccc") || s.str.equals("bbb") )
.findFirst()
.map( s -> {
// s.str = "DDD"; this works just fine
s = new TestClass("DDD");
return s;
} );
System.out.println(result);
and run again - it should give you some hint.
Please notice that you don't use result in your version at all, and modifying source list in such cases is not the best habit (possible multi threading issues)

With s.str = "DDD" you are modifying or mutating the TestClass instance in the list setting its str field to value DDD.
With s = new TestClass("DDD"), you are not touching the instances in the list. The variable s is a local variable to that lambda block. Assigning a new object reference to it will not change the str field of the object it was pointing to earlier.
Usually, with a map, you have to collect it or do something with the result. But here you are not doing anything with the mapped result.

the ‘map’ method is used to map each element to its corresponding result. If you want to change one element of your list, you need to collect the results into a new List. using the .collect() method.
List<TestClass> list = new ArrayList<TestClass>();
list.add( new TestClass("aaa") );
list.add( new TestClass("bbb") );
list.add( new TestClass("ccc") );
List<TestClass> result = list.stream()
.map( s -> {
if (s.toString().equals("bbb")) {
s = new TestClass("DDD");
}
return s;
}).collect(Collectors.toList());
for(TestClass t : result){
System.out.println(t);
}
The result of this is:
aaa
DDD
ccc
Tidying up the stream:
List<TestClass> result = list.stream()
.map( s -> s.toString().equals("bbb") ? new TestClass("DDD") : s)
.collect(Collectors.toList());

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.

Java 8 - Update two properties in same Stream code

I'm wondering if there is a way I can update two times an object in a Stream lambda code, I need to update two properties of a class, I need to update the value and the recordsCount properties
Object:
public class HistoricDataModelParsed {
private Date startDate;
private Date endDate;
private Double value;
private int recordsCount;
}
I tried doing something like this:
val existingRecord = response.stream()
.filter(dateTime ->fromDate.equals(dateTime.getStartDate()))
.findAny()
.orElse(null);
response.stream()
.filter(dateTime ->fromDate.equals(dateTime.getStartDate()))
.findAny()
.orElse(existingRecord)
.setValue(valueAdded)
.setRecordsCount(amount);
But I got this error: "Cannot invoke setRecordsCount(int) on the primitive type void"
So I ended up doing the stream two times to update each of the two fields I needed
response.stream()
.filter(dateTime ->fromDate.equals(dateTime.getStartDate()))
.findAny()
.orElse(existingRecord)
.setValue(valueAdded);
response.stream()
.filter(dateTime ->fromDate.equals(dateTime.getStartDate()))
.findAny()
.orElse(existingRecord)
.setRecordsCount(amount);
Is there a way I can achieve what I need without the need to stream two times the list?
The return type of setValue is void and not HistoricDataModelParsed. So you cannot invoke the method setRecordsCount which is in HistoricDataModelParsed class.
You could have added a method in HistoricDataModelParsed which takes two parameters for value and recordsCount:
public void setValueAndCount(Double value, int count) {
this.value = value;
this.recordsCount = count;
}
Then call this method after orElse:
response.stream()
.filter(dateTime ->fromDate.equals(dateTime.getStartDate()))
.findAny()
.orElse(existingRecord)
.setValueAndCount(valueAdded, amount);
The state of an object should not change within a stream. It can lead to inconsistent results. But you can create new instances of the objects and pass new values via the constructor. Here is a simple record that demonstrates the method. Records are basically immutable classes that have no setters. The getters are the names of the variables. A class would also work in this example.
record Temp(int getA, int getB) {
#Override
public String toString() {
return "[" + getA + ", " + getB +"]";
}
}
Some data
List<Temp> list = List.of(new Temp(10, 20), new Temp(50, 200),
new Temp(100, 200));
And the transformation. A new instance of Temp with new values is created along with the old ones to completely populate the constructor. Otherwise, the existing object is passed along.
List<Temp> result = list.stream().map(
t -> t.getA() == 50 ? new Temp(2000, t.getB()) : t)
.toList();
System.out.println(result);
Prints
[[10, 20], [2000, 200], [100, 200]]
To answer the void error you got it's because a stream expects values to continue thru out the stream so if a method is void, it isn't returning anything so you would have to return it. Here is an example:
stream.map(t->{voidReturnMethod(t); return t;}).toList();
The return ensures the pipeline continues.
Simply store the result of orElse and then call your methods on it.
HistoricDataModelParsed record =
response.stream()
.filter(dateTime -> fromDate.equals(dateTime.getStartDate()))
.findAny()
.orElse(existingRecord);
record.setValue(valueAdded)
record.setRecordsCount(amount);

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

How to get a list output from forEach loop in Java 8 Streams

I have two different lists of same objects but different properties and with a common identifier in those objects. I would like to iterate over the first list and get the corresponding object from the second (which has common properties) and then wrap those objects around and finally add that object to a list using Java Streams.
This is the example I have taken.
private class Person {
private String name;
private boolean isSenior;
private Person(String name, boolean isSenior) {
this.name = name;
this.isSenior = isSenior;
}
public String getName() {
return name;
}
public boolean isSenior() {
return isSenior;
}
#Override
public String toString() {
return name + ": " + isSenior;
}
}
private class PersonWrapper {
private Person jrPerson;
private Person srPerson;
private PersonWrapper(Person jrPerson, Person srPerson) {
this.jrPerson = jrPerson;
this.srPerson = srPerson;
}
public Person getJrPerson() {
return jrPerson;
}
public Person getSrPerson() {
return srPerson;
}
#Override
public String toString() {
return jrPerson.toString() + "-" + srPerson.toString();
}
}
Now, in the main class, I will create two list instances like this
List<Person> jrPersons = new ArrayList<>();
List<Person> srPersons = new ArrayList<>();
and add the objects in the following manner
jrList.add(new Person("John", false));
jrList.add(new Person("Paul", false));
jrList.add(new Person("Mike", false));
seniorList.add(new Person("John", true));
seniorList.add(new Person("Paul", true));
seniorList.add(new Person("Mike", true));
Now, I want to iterate over the jrList and find the corresponding Person object in the srList (same name). Then I would wrap these objects as PersonWrapper and that object to a list.
So far, this is what I have been doing
List<PersonWrapper> wrapperList = new ArrayList<>();
jrList.forEach(jr -> seniorList.stream().filter(sr -> jr.getName().equals(sr.getName())).map(sr -> new PersonWrapper(jr, sr)).collect(Collectors.toList()));
Now, I would like to know how the Collectors.toList() can be substituted by wrapperList or how the output from Collectors.toList() be added to wrapperList.
Please help me in achieving this.
Instead of using a forEach just use streams from the beginning:
List<PersonWrapper> wrapperList = jrList.stream()
.flatMap(jr -> seniorList.stream()
.filter(sr -> jr.getName().equals(sr.getName()))
.map(sr -> new PersonWrapper(jr, sr))
)
.collect(Collectors.toList());
By using flatMap you can flatten a stream of streams (Stream<Stream<PersonWrapper>>) into a single stream (Stream<PersonWrapper>)
If you can't instantiate wrapperList by yourself or really need to append to it. You can alter above snippet to following:
List<PersonWrapper> wrapperList = new ArrayList<>();
jrList.stream()
.flatMap(jr -> seniorList.stream()
.filter(sr -> jr.getName().equals(sr.getName()))
.map(sr -> new PersonWrapper(jr, sr))
)
.forEach(wrapperList::add);
While Lino's answer is certainly correct. I would argue that if a given person object in jrList can only ever have one corresponding match in seniorList maximum, in other words, if it's a 1-1 relationship then you can improve upon the solution given by Lino by finding the first match as follows:
List<PersonWrapper> resultSet = jrList.stream()
.map(p -> seniorList.stream()
.filter(sr -> p.getName().equals(sr.getName()))
.findFirst()
.map(q -> new PersonWrapper(p, q))
.get())
.collect(Collectors.toList());
or if there is no guarantee that each person in jrList will have a corresponding match in seniorList then change the above query to:
List<PersonWrapper> resultSet = jrList.stream()
.map(p -> seniorList.stream()
.filter(sr -> p.getName().equals(sr.getName()))
.findFirst()
.map(q -> new PersonWrapper(p, q))
.orElse(null))
.filter(Objects::nonNull)
.collect(Collectors.toList());
The difference is that now instead of calling get() on the result of findFirst() we provide a default with orElse in case findFirst cannot find the corresponding value and then we filter the null values out in the subsequent intermediate operation as they are not needed.
Replace your looping logic with below code.
jrList.forEach(jr -> seniorList.stream().filter(sr -> jr.getName().equals(sr.getName()))
.map(sr -> wrapperList.add(new PersonWrapper(jr, sr))).collect(Collectors.toList()));

Filtering objects with circular reference to itself (ObjectA has property List<ObjectA>)

I have a large json structure with nested values that I have converted into a list of objects to work with. I'd like to filter out all objects that don't contain a specific property value. Problem is though, that so far all I've come up with is a for loop within a for loop within a for loop (and that's given we know the json structure is only three nested levels). I only want to filter out the objects that do contain an integer (if it's null, it could be a parent containing something valid) or parents that are empty). If I try to stream with flattened - I can filter out all my objects and nested objects but won't I lose my structure?
quick eg.
public class ObjectA {
Integer id;
List<ObjectA> sublist;
}
List<ObjectA> fullList;
Set<Integer> keeptheseIntegers;
for (ObjectA obj : fullList) {
if (obj.getId() != null && !keeptheseIntegers.contains(obj.getId()){
fullList.remove(obj);
} else if (obj.getId() == null && obj.getSubList().size() > 0) {
for (ObjectA subObj : obj.getSubList()){
(same thing as above)
}
}
edit - I did realize later that the remove was not working properly and used iterator.remove. still same logical issue though
First: instead of manipulating your original structure (remove unwanted) I would collect the wanted items into an own list during the algorithm.
Second: Iterating through nested structures is a good candidate for the recursive pattern.
Third: Using java 8 I would implement it using streams and lambdas.
Something like this:
public class ObjectA
{
Integer id;
List<ObjectA> sublist;
}
private static final Set<Integer> keeptheseIntegers = new HashSet<>();
static
{
keeptheseIntegers.add( 1 );
}
private List<ObjectA> filter( List<ObjectA> list)
{
List<List<ObjectA>> subLists = new ArrayList<>();
List<ObjectA> result = list.stream()
// get all objects with sublist and add the sublist to interim subLists:
.peek( obj -> {
if ( obj.sublist == null )
{
// assert your assumption that Integer is assigned
if ( obj.id == null )
{
throw new IllegalArgumentException();
}
}
else
{
subLists.add( obj.sublist );
}
} )
// filter for the objects you want in result:
.filter( (obj -> obj.id != null && keeptheseIntegers.contains(obj.id)))
// and convert the resulting stream to a list:
.collect( Collectors.toList());
// call this method recusively for all found sublists:
subLists.forEach( i -> result.addAll(filter( i)) );
return result;
}
and somewher in your main program flow you call it:
...
List<ObjectA> fullList = new ArrayList<>();
List<ObjectA> objWithInt = filter( fullList);
// process the received list. Your original fullList is unchanged.

Categories

Resources