Remove in depth elements using streams - java

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());

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.

Use Java 8 stream to create and a Collection<Stream>

I have a class
class ColumnTags {
String Name;
Collection<String> columnSemanticTags;
// constructor and getter and setters and other relevant attributes
}
I want to get the columnSemanticTags from a list of ColumnTags for a given name.
The corresponding method is as follows
public Collection<String> getTags(String colName, List<ColumnTags> colList)
{
Collection<String> tags = new ArrayList();
for(ColumnTag col:colList){
if(colName.equals(col.getName())){
tags = col.getColumnSemanticTags();
break;
}
}
return tags;
}
Want to convert the for loop to a java stream . I have tried
tags = colList.stream().filter(col -> colName.equals(col.getName()))
.map(col -> col.getColumnSemanticTags())
.collect(Collectors.toCollection());
I am getting compilation error. I am not aware what should be the Supplier . Have tried ArrayList::new . I have also tried casting it to ArrayList , but no success.
Can someone advice me what am I assuming wrong or what should be the expected way to handle this scenario.
With the solution , can someone explain as to why .collect() is a wrong way of tackling this solution.
public Collection<String> getTags(String colName, List<ColumnTags> colList) {
return colList.stream().filter(col -> colName.equals(col.getName()))
.map(col -> col.getColumnSemanticTags())
.findFirst().orElse(new ArrayList<>());
}
An easier way of going about this would be to simply filter a Stream to find exactly what you're looking for. If it is found, then return it, otherwise return an empty ArrayList:
return colList.stream()
.filter(c -> colName.equals(c.getName()))
.map(ColumnTag::getColumnSemanticTags)
.findFirst()
.orElseGet(ArrayList::new);
If you really want to use collect, you must call flatMap. That merges all of the lists (which are come from map(col -> col.getColumnSemanticTags())) into a single stream which contains all of the items.
List<String> tags = colList.stream()
.filter(col -> colName.equals(col.getName()))
.map(col -> col.getColumnSemanticTags())
.flatMap(collection -> collection.stream())
.collect(Collectors.toList());

Getting filtered records from streams using lambdas in java

I have an entity Employee
class Employee{
private String name;
private String addr;
private String sal;
}
Now i have list of these employees. I want to filter out those objects which has name = null and set addr = 'A'. I was able to achieve like below :
List<Employee> list2= list.stream()
.filter(l -> l.getName() != null)
.peek(l -> l.setAddr("A"))
.collect(Collectors.toList());
Now list2 will have all those employees whose name is not null and then set addr as A for those employees.
What i also want to find is those employees which are filtered( name == null) and save them in DB.One way i achieved is like below :
List<Employee> list2= list.stream()
.filter(l -> filter(l))
.peek(l -> l.setAddr("A"))
.collect(Collectors.toList());
private static boolean filter(Employee l){
boolean j = l.getName() != null;
if(!j)
// save in db
return j;
}
1) Is this the right way?
2) Can we do this directly in lambda expression instead of writing separate method?
Generally, you should not use side effect in behavioral parameters. See the sections “Stateless behaviors” and “Side-effects” of the package documentation. Also, it’s not recommended to use peek for non-debugging purposes, see “In Java streams is peek really only for debugging?”
There’s not much advantage in trying to squeeze all these different operations into a single Stream pipeline. Consider the clean alternative:
Map<Boolean,List<Employee>> m = list.stream()
.collect(Collectors.partitioningBy(l -> l.getName() != null));
m.get(false).forEach(l -> {
// save in db
});
List<Employee> list2 = m.get(true);
list2.forEach(l -> l.setAddr("A"));
Regarding your second question, a lambda expression allows almost everything, a method does. The differences are on the declaration, i.e. you can’t declare additional type parameters nor annotate the return type. Still, you should avoid writing too much code into a lambda expression, as, of course, you can’t create test cases directly calling that code. But that’s a matter of programming style, not a technical limitation.
If you are okay in using peek for implementing your logic (though it is not recommended unless for learning), you can do the following:
List<Employee> list2= list.stream()
.peek(l -> { // add this peek to do persistence
if(l.getName()==null){
persistInDB(l);
}
}).filter(l -> l.getName() != null)
.peek(l -> l.setAddr("A"))
.collect(Collectors.toList());
You can also do something like this:
List<Employee> list2 = list.stream()
.filter(l->{
boolean condition = l.getName()!=null;
if(condition){
l.setAddr("A");
} else {
persistInDB(l);
}
return condition;
})
.collect(Collectors.toList());
Hope this helps!

lambda Java 8, how to map a list that is a filed of the result of filter operation

I have a catalog-like object hierarchy where every object has a name field.
class A {
List<A> list;
String name;
}
A{A{A{A...}AA},A{AAA},A{AAA}} // the depth is finite (~4)
I would like to provide a set of methods that return a list of child names (a a.getName()) of any parent element for a given name.
So for level 1 I have
a.getAs().stream().map(a1 -> a1.getName()).collect(Collectors.toList());
Level 2 I have already troubles with:
a1.getAs().stream().filter(a2 -> a2.getName() == name)
now I want to access the As and map them to their names but I don't know how
EDIT:
I have just realized that from the third level on it wouldn't be possible to find the list with just providing a single name. I would need a name for each level to be able to navigate to the node where the child list could be collected.
On one hand I could keep all the objects in one Set and access them with an id. They would still have references to each other. On the other hand by not knowing the root element I couldn't get the structure right.
I think I have to rethink the problem.
You can do it like this:
public static List<String> getChildNames(A node, String... path) {
Stream<A> s = node.getAs().stream();
for(String name: path)
s = s.filter(a -> a.getName().equals(name)).flatMap(a -> a.getAs().stream());
return s.map(A::getName).collect(Collectors.toList());
}
but if the names beneath an A node are unique, you should consider maintaining a Map<String,A>, mapping from child name to actual child, instead of a List<A>. That would make traversing a path via unique name/ID as simple as node.get(name1).get(name2). The logic of the method above would still be useful if you incorporate pattern matching, which doesn’t need to have a unique result.
public static List<String> getChildNames(A node, String... pathPatterns) {
Stream<A> s = node.getAs().stream();
for(String namePattern: pathPatterns) {
Pattern compiledPattern = Pattern.compile(namePattern);
s = s.filter( a -> compiledPattern.matcher(a.getName()).find())
.flatMap(a -> a.getAs().stream());
}
return s.map(A::getName).collect(Collectors.toList());
}
It works only for one level of the hierarchy:
public List<A> getSubcategoriesByParentName(A category, String name) {
return category.getSubcategories()
.stream()
.filter(subcategory -> subcategory.getName().equals(name))
.collect(Collectors.toList());
}
To achieve the next level, you could use a flatMap:
category.getSubcategories().stream()
.flatMap(s -> s.getSubcategories().stream())
.filter(s -> s.getName().equals(name))
.collect(Collectors.toList());
As you can see, there is a need of recursion, it is not a work for Stream API.
Of course, being aware of the depth, we could access to all levels (by using a flatMap(s -> s.getSubcategories().stream()) several times), but it will look ugly.

Group and Reduce list of objects

I have a list of objects with many duplicated and some fields that need to be merged. I want to reduce this down to a list of unique objects using only Java 8 Streams (I know how to do this via old-skool means but this is an experiment.)
This is what I have right now. I don't really like this because the map-building seems extraneous and the values() collection is a view of the backing map, and you need to wrap it in a new ArrayList<>(...) to get a more specific collection. Is there a better approach, perhaps using the more general reduction operations?
#Test
public void reduce() {
Collection<Foo> foos = Stream.of("foo", "bar", "baz")
.flatMap(this::getfoos)
.collect(Collectors.toMap(f -> f.name, f -> f, (l, r) -> {
l.ids.addAll(r.ids);
return l;
})).values();
assertEquals(3, foos.size());
foos.forEach(f -> assertEquals(10, f.ids.size()));
}
private Stream<Foo> getfoos(String n) {
return IntStream.range(0,10).mapToObj(i -> new Foo(n, i));
}
public static class Foo {
private String name;
private List<Integer> ids = new ArrayList<>();
public Foo(String n, int i) {
name = n;
ids.add(i);
}
}
If you break the grouping and reducing steps up, you can get something cleaner:
Stream<Foo> input = Stream.of("foo", "bar", "baz").flatMap(this::getfoos);
Map<String, Optional<Foo>> collect = input.collect(Collectors.groupingBy(f -> f.name, Collectors.reducing(Foo::merge)));
Collection<Optional<Foo>> collected = collect.values();
This assumes a few convenience methods in your Foo class:
public Foo(String n, List<Integer> ids) {
this.name = n;
this.ids.addAll(ids);
}
public static Foo merge(Foo src, Foo dest) {
List<Integer> merged = new ArrayList<>();
merged.addAll(src.ids);
merged.addAll(dest.ids);
return new Foo(src.name, merged);
}
As already pointed out in the comments, a map is a very natural thing to use when you want to identify unique objects. If all you needed to do was find the unique objects, you could use the Stream::distinct method. This method hides the fact that there is a map involved, but apparently it does use a map internally, as hinted by this question that shows you should implement a hashCode method or distinct may not behave correctly.
In the case of the distinct method, where no merging is necessary, it is possible to return some of the results before all of the input has been processed. In your case, unless you can make additional assumptions about the input that haven't been mentioned in the question, you do need to finish processing all of the input before you return any results. Thus this answer does use a map.
It is easy enough to use streams to process the values of the map and turn it back into an ArrayList, though. I show that in this answer, as well as providing a way to avoid the appearance of an Optional<Foo>, which shows up in one of the other answers.
public void reduce() {
ArrayList<Foo> foos = Stream.of("foo", "bar", "baz").flatMap(this::getfoos)
.collect(Collectors.collectingAndThen(Collectors.groupingBy(f -> f.name,
Collectors.reducing(Foo.identity(), Foo::merge)),
map -> map.values().stream().
collect(Collectors.toCollection(ArrayList::new))));
assertEquals(3, foos.size());
foos.forEach(f -> assertEquals(10, f.ids.size()));
}
private Stream<Foo> getfoos(String n) {
return IntStream.range(0, 10).mapToObj(i -> new Foo(n, i));
}
public static class Foo {
private String name;
private List<Integer> ids = new ArrayList<>();
private static final Foo BASE_FOO = new Foo("", 0);
public static Foo identity() {
return BASE_FOO;
}
// use only if side effects to the argument objects are okay
public static Foo merge(Foo fooOne, Foo fooTwo) {
if (fooOne == BASE_FOO) {
return fooTwo;
} else if (fooTwo == BASE_FOO) {
return fooOne;
}
fooOne.ids.addAll(fooTwo.ids);
return fooOne;
}
public Foo(String n, int i) {
name = n;
ids.add(i);
}
}
If the input elements are supplied in the random order, then having intermediate map is probably the best solution. However if you know in advance that all the foos with the same name are adjacent (this condition is actually met in your test), the algorithm can be greatly simplified: you just need to compare the current element with the previous one and merge them if the name is the same.
Unfortunately there's no Stream API method which would allow you do to such thing easily and effectively. One possible solution is to write custom collector like this:
public static List<Foo> withCollector(Stream<Foo> stream) {
return stream.collect(Collector.<Foo, List<Foo>>of(ArrayList::new,
(list, t) -> {
Foo f;
if(list.isEmpty() || !(f = list.get(list.size()-1)).name.equals(t.name))
list.add(t);
else
f.ids.addAll(t.ids);
},
(l1, l2) -> {
if(l1.isEmpty())
return l2;
if(l2.isEmpty())
return l1;
if(l1.get(l1.size()-1).name.equals(l2.get(0).name)) {
l1.get(l1.size()-1).ids.addAll(l2.get(0).ids);
l1.addAll(l2.subList(1, l2.size()));
} else {
l1.addAll(l2);
}
return l1;
}));
}
My tests show that this collector is always faster than collecting to map (up to 2x depending on average number of duplicate names), both in sequential and parallel mode.
Another approach is to use my StreamEx library which provides a bunch of "partial reduction" methods including collapse:
public static List<Foo> withStreamEx(Stream<Foo> stream) {
return StreamEx.of(stream)
.collapse((l, r) -> l.name.equals(r.name), (l, r) -> {
l.ids.addAll(r.ids);
return l;
}).toList();
}
This method accepts two arguments: a BiPredicate which is applied for two adjacent elements and should return true if elements should be merged and the BinaryOperator which performs merging. This solution is a little bit slower in sequential mode than the custom collector (in parallel the results are very similar), but it's still significantly faster than toMap solution and it's simpler and somewhat more flexible as collapse is an intermediate operation, so you can collect in another way.
Again both these solutions work only if foos with the same name are known to be adjacent. It's a bad idea to sort the input stream by foo name, then using these solutions, because the sorting will drastically reduce the performance making it slower than toMap solution.
As already pointed out by others, an intermediate Map is unavoidable, as that’s the way of finding the objects to merge. Further, you should not modify source data during reduction.
Nevertheless, you can achieve both without creating multiple Foo instances:
List<Foo> foos = Stream.of("foo", "bar", "baz")
.flatMap(n->IntStream.range(0,10).mapToObj(i -> new Foo(n, i)))
.collect(collectingAndThen(groupingBy(f -> f.name),
m->m.entrySet().stream().map(e->new Foo(e.getKey(),
e.getValue().stream().flatMap(f->f.ids.stream()).collect(toList())))
.collect(toList())));
This assumes that you add a constructor
public Foo(String n, List<Integer> l) {
name = n;
ids=l;
}
to your Foo class, as it should have if Foo is really supposed to be capable of holding a list of IDs. As a side note, having a type which serves as single item as well as a container for merged results seems unnatural to me. This is exactly why to code turns out to be so complicated.
If the source items had a single id, using something like groupingBy(f -> f.name, mapping(f -> id, toList()), followed by mapping the entries of (String, List<Integer>) to the merged items was sufficient.
Since this is not the case and Java 8 lacks the flatMapping collector, the flatmapping step is moved to the second step, making it look much more complicated.
But in both cases, the second step is not obsolete as it is where the result items are actually created and converting the map to the desired list type comes for free.

Categories

Resources