I have this two code examples:
Code A:
Stream<String> aStream = firstLevelList.stream()
.flatMap(firstLevelElement -> firstLevelElement.getSecondLevelList().stream()
.flatMap(secondLevelElement -> secondLevelElement.getThirdLevelList().stream()
.map(thirdLevelElement -> thirdLevelElement.toString())));
Code B:
Stream<String> aStream = firstLevelList.stream()
.flatMap(firstLevelElement -> firstLevelElement.getSecondLevelList().stream())
.flatMap(secondLevelElement -> secondLevelElement.getThirdLevelList().stream())
.map(thirdLevelElement -> thirdLevelElement.toString());
Both have exactly the same result, which is the best implementation? Why?
Option 1
The following reads nicely and feels more common:
firstLevelList.stream()
.flatMap(firstLevelElement -> firstLevelElement.getSecondLevelList().stream())
.flatMap(secondLevelElement -> secondLevelElement.getThirdLevelList().stream())
.map(Object::toString);
It avoids the nesting as #Holger pointed out, which is important for the reader.
Option 2
Alternatively, we could use a different indentation for the nesting if it's not at odds with the rest of the code base style:
firstLevelList.stream().flatMap(
firstLevelElement -> firstLevelElement.getSecondLevelList().stream().flatMap(
secondLevelElement -> secondLevelElement.getThirdLevelList().stream().map(Object::toString)
)
);
Option 3
If it still feels too cryptic, we could extract variables or methods to name the different streams:
firstLevelList.stream().flatMap(
firstLevelElement -> firstLevelElement.getSecondLevelList().stream().flatMap(
secondLevelElement -> getThirdLevelStream(secondLevelElement)
)
);
Stream<String> getThirdLevelStream(SecondLevelElement secondLevelElement) {
return secondLevelElement.getThirdLevelList().stream().map(Object::toString);
}
Option 4
We can move these methods to the element classes respecting now the Law of Demeter:
firstLevelList.stream().flatMap(FirstLevelElement::getSecondLevelStream);
class FirstLevelElement {
Stream<String> getSecondLevelStream() {
return this.getSecondLevelList().stream().flatMap(SecondLevelElement::getThirdLevelStream);
}
Collection<SecondLevelElement> getSecondLevelList() {...}
}
class SecondLevelElement {
Stream<String> getThirdLevelStream() {
return this.getThirdLevelList().stream().map(Object::toString);
}
Collection<ThirdLevelElement> getThirdLevelList() {...}
}
This will add some value if the stream methods are reused somewhere else in the code.
Conclusion
Different arguments can be made about which of these options is better. In general, they are all perfectly fine.
I prefer method references for neatness:
Stream<String> aStream = firstLevelList.stream()
.map(FirstLevelElement::getSecondLevelList)
.flatMap(List::stream)
.map(SecondLevelElement::getThirdLevelList)
.flatMap(List::stream)
.map(Object::toString);
Related
Class Package{
Long packageId;
String packageName;
List<Service> serviceList;
}
Class Service{
Long serviceId;
String location;
}
List <Package> packageList
Above there are my classes and the requirement is to collect packageId from the outer list packageList and serviceId from the inner list (the Services of each Package) where packageList.packageName == "Full" and serviceList.location == "Japan". Also, I need to know whether that kind of record exist or not. This is what I've written so far collecting the data into a HashMap.
HashMap<String,Object> stringObjectHashMap = new HashMap<>();
boolean isFound = packageList.stream()
.filter(package -> "Full".equalsIgnoreCase(package.getPackageName()))
.peek(package ->stringObjectHashMap.put("packageId",package.getPackageId()))
.flatMap(package -> package.getServiceList().stream())
.filter(service -> service.getLocation().equalsIgnoreCase("Japan"))
.peek(service -> stringObjectHashMap.put("serviceId",service.getServiceId()))
.anyMatch(service -> service.getLocation().equalsIgnoreCase("Japan"));
The problem is that Sonar complains about the use of peek() inside the stream. It says "According to its JavaDocs, the intermediate Stream operation java.util.Stream.peek() “exists mainly to support debugging” purposes."
Can anyone suggest a better solution?
sample input:
[
{
"packageId": 13,
"packageName": "Normal",
"serviceList": [
{
"serviceId": "100",
"location": "China"
}
]
},
{
"packageId": 10,
"packageName": "Full",
"serviceList": [
{
"serviceId": "100",
"location": "Spain"
}
]
},
{
"packageId": 5,
"packageName": "Full",
"serviceList": [
{
"serviceId": "100",
"location": "Japan"
}
]
}
]
expected output,
"packageId": 5 //this is from outer list
"serviceId": 100 //this is from inner list
Note: "Full" package can appear multiple times. As well as location "Japan" also can appear multiple times in services. But a combination of the packageName with a value "Full" and location "Japan" can appear only once.
By using .flatMap(package -> package.getServiceList().stream()) you actually bring all the services for all the packages into one huge stream and get everything mixed up.
If I understand the task correctly, you want to select all packages with name "Full", and then, for each such package, select any service in that package's list with location "Japan", or null, if such service is not there. You can just collect to such a map like this:
Map<String,Long> stringObjectMap = packageList.stream().filter(p -> "Full".equalsIgnoreCase(p.getPackageName()))
.collect(Collectors.toMap(
Package::getPackageId,
p -> p.getServiceList().stream()
.filter(service -> "Japan".equalsIgnoreCase(service.getLocation()))
.map(Service::getServiceId)
.findFirst().orElse(null) //you might want to use a stand-in default value here instead of null
));
Note: this will result in some obscure implementation of Map, not really a HashMap, so you might need to make adjustments.
Personally, I find such cases more easily solvable by using regular for-loops.
What you could do is filter through each of the packages to find all full packages, then perform a reduction on those packages to find services that are from japan.
List<Package> fullPackages = packageList
.stream()
.filter(p -> p.getPackageName().equals("Full"))
.toList();
List<Service> fromJapan = fullPackages
.stream()
.flatMap(p -> p.getServiceList().stream())
.filter(service -> service.getLocation().equalsIgnoreCase("Japan"))
.toList();
Also, from what it looks like, the key is not unique, so it's going to continue to get overwritten.
Stream-based solution
Assuming that packageName with value of "Full" and a service location equal to "Japan" and every packageId is unique, we can create an intermediate map by mapping an optional of service (service located in Japan) to a packageId and generate a final result based on it.
Map<String, Long> ids = packageList.stream()
.filter(pack -> "Full".equalsIgnoreCase(pack.getPackageName()))
.collect(Collectors.toMap( // Map<Long, Optional<Service>> - intermediate map
Package::getPackageId,
pack -> pack.getServiceList().stream()
.filter(service -> service.getLocation().equalsIgnoreCase("Japan"))
.findFirst()))
.entrySet().stream()
.filter(entry -> entry.getValue().isPresent())
.findFirst()
.map(entry -> Map.of("packageId", entry.getKey(),
"serviceId", entry.getValue().get().getServiceId()))
.orElse(Collections.emptyMap());
Can we do better? We can, currently, solution processes all the data which is not needed in case when the target package & service were found.
We can make the solution short-circuit by using findFirst() operation, which will also avoid double-checking (iterate over a list of services twice).
And it actually would be much more handy to use a simple DTO, for instance a Java 16 record with two attributes, rather than a map. But because your existing code depends on a map as a result I'll continue with map, and internally as an intermediate mean of string data I'll use Map.Entry.
Map<String, Long> ids = packageList.stream()
.filter(pack -> "Full".equalsIgnoreCase(pack.getPackageName()))
.map(pack -> Map.entry(pack.getPackageId(), pack.getServiceList().stream()
.filter(service -> service.getLocation().equalsIgnoreCase("Japan"))
.findFirst()))
.filter(entry -> entry.getValue().isPresent())
.findFirst() // <- will produce an intermediate result as Optional<Map.Entry<Long, <Optional<Service>>>
.map(entry -> Map.of("packageId", entry.getKey(),
"serviceId", entry.getValue().get().getServiceId()))
.orElse(Collections.emptyMap());
This version is more performant. It'll terminate the stream execution when the combination of "Full" package with a service located in "Japan" will be encountered, instead of processing all data set.
It's much better now, but one issue we can't resolve - it is not possible to eliminate the nested stream (because we need to iterate over the list of services) which spoils the readability.
Reminder: streams were introduces in Java as a mean of structuring the code in a simple well-readable way, not the opposite. Old plain loops perform better than sequential streams.
Imperative solution
Now let's implement the same logic using an imperative approach.
Map<String, Long> result = new HashMap<>();
for (Package pack: packageList) {
if (!pack.getPackageName().equalsIgnoreCase("Full")) continue;
for (Service service: pack.getServiceList()) {
if (service.getLocation().equalsIgnoreCase("Japan")) {
result.put("packageId", pack.getPackageId());
result.put("serviceId", service.getServiceId());
break;
}
}
}
Conditional logic is much easier to implement using plain loops. Very concise, easy to read, easy to maintain.
And as a result, imperative solution is not only much cleaner, but also more performant because doesn't require entails unnecessary actions: map will be updated only once.
You can also compare it with your initial approach that uses side effects via peek() which continuously updates the map.
If you're using Java 12 or above, you could achieve your goal by using the teeing operation to create a first downstream of Packages and a second downstream of Services. Both of them would return a Map<String, Long> containing the fixed Strings with the expected ids. Also, if you need to know if the record has been found you could check the number of entries of your returned Map.
Map<String, Long> mapRes = packageList.stream()
.filter(pack -> pack.getPackageName().equalsIgnoreCase("Full") && pack.getServiceList().stream().anyMatch(s -> s.getLocation().equalsIgnoreCase("Japan")))
.collect(Collectors.teeing(
Collectors.toMap(p -> "packageId", Package::getPackageId, (id1, id2) -> id1),
Collectors.flatMapping(pack -> pack.getServiceList().stream(), Collectors.toMap(s -> "serviceId", Service::getServiceId, (id1, id2) -> id1)),
(map1, map2) -> {
map1.putAll(map2);
return map1;
}));
boolean isFound = mapRes.entrySet().size() == 2;
Alternatively, if Sonar is signaling you the abuse of the peek operation and you still want to employ stateful lambdas although not advisable, then you could replace each peek with a map doing the exact operations and then returning the given element. This is your original code with the peek replaced by map.
boolean isFound = packageList.stream()
.filter(pack -> "Full".equalsIgnoreCase(pack.getPackageName()))
.map(pack -> {
stringObjectHashMap.put("packageId", pack.getPackageId());
return pack;
})
.flatMap(pack -> pack.getServiceList().stream())
.filter(service -> service.getLocation().equalsIgnoreCase("Japan"))
.map(service -> {
stringObjectHashMap.put("serviceId", service.getServiceId());
return service;
})
.anyMatch(service -> service.getLocation().equalsIgnoreCase("Japan"));
Here there is also a link to test the code:
https://ideone.com/uV5PIQ
I'm really curious about this simple performance/best_use/best_practice related case scenario:
If I have this simple snippet:
List<String> list = Arrays.asList("Hello1", "Hello2", "Hello3", "Jhon", "Doe", "Hello4");
list.stream()
.map(s -> {
if (s.contains("Hello")) {
return "World";
}
return null;
})
.filter(Objects::nonNull)
.collect(Collectors.toList());
OR
list.stream()
.flatMap(s -> {
if (s.contains("Hello")) {
Stream.of("World");
}
return Stream.empty();
})
.collect(Collectors.toList());
NOTE: I know that maybe Map the String just to convert it to another String doesn't make much sense, but that is just for the example representation purposes, it could be a pojo or an integer or anything else.
Which one will perform better? or which would be the best option?
I'm trying to understand which is the better option in cases in which some conditional appears in the logic when we use streams chains.
Thank you.
I wouldn't create and wrapper like another Stream for avoiding nulls, nor return the null value for non matching strings, you can simply filter the strings having Hello word and then use map for value modification
list.stream()
.filter(s->s.contains("Hello"))
.map(s -> "world")
.collect(Collectors.toList());
I have the following expression:
scheduleIntervalContainers.stream()
.filter(sic -> ((ScheduleIntervalContainer) sic).getStartTime() != ((ScheduleIntervalContainer)sic).getEndTime())
.collect(Collectors.toList());
...where scheduleIntervalContainers has element type ScheduleContainer:
final List<ScheduleContainer> scheduleIntervalContainers
Is it possible to check the type before the filter?
You can apply another filter in order to keep only the ScheduleIntervalContainer instances, and adding a map will save you the later casts :
scheduleIntervalContainers.stream()
.filter(sc -> sc instanceof ScheduleIntervalContainer)
.map (sc -> (ScheduleIntervalContainer) sc)
.filter(sic -> sic.getStartTime() != sic.getEndTime())
.collect(Collectors.toList());
Or, as Holger commented, you can replace the lambda expressions with method references if you prefer that style:
scheduleIntervalContainers.stream()
.filter(ScheduleIntervalContainer.class::isInstance)
.map (ScheduleIntervalContainer.class::cast)
.filter(sic -> sic.getStartTime() != sic.getEndTime())
.collect(Collectors.toList());
A pretty elegant option is to use method reference of class:
scheduleIntervalContainers
.stream()
.filter( ScheduleIntervalContainer.class::isInstance )
.map( ScheduleIntervalContainer.class::cast )
.filter( sic -> sic.getStartTime() != sic.getEndTime())
.collect(Collectors.toList() );
There is a small problem with #Eran solution - typing class name in both filter and map is error-prone - it is easy to forget to change the name of the class in both places. An improved solution would be something like this:
private static <T, R> Function<T, Stream<R>> select(Class<R> clazz) {
return e -> clazz.isInstance(e) ? Stream.of(clazz.cast(e)) : null;
}
scheduleIntervalContainers
.stream()
.flatMap(select(ScheduleIntervalContainer.class))
.filter( sic -> sic.getStartTime() != sic.getEndTime())
.collect(Collectors.toList());
However there might be a performance penalty in creating a Stream for every matching element. Be careful to use it on huge data sets. I've learned this solution from #Tagir Vailev
Instead of a filter + map like other answers suggest, I would recommend this utility method:
public static <Super, Sub extends Super> Function<Super, Stream<Sub>> filterType(Class<Sub> clz) {
return obj -> clz.isInstance(obj) ? Stream.of(clz.cast(obj)) : Stream.empty();
}
Use it as:
Stream.of(dog, cat fish)
.flatMap(filterType(Dog.class));
Compared to filter + map it has the following advantages:
If the class does not extend your class you will get a compile error
Single place, you can never forget to change a class in either filter or map
Filter by class type with StreamEx
StreamEx.of(myCollection).select(TheThing.class).toList();
Consider this code:
Mono.just(myVar)
.flatMap(MyClass::heavyOperation)
.flatMap(MyClass::anotherHeavyOperation)
.flatMap(res -> doSomething(res, MyClass.heavyOperation(myVar)));
I don't want to call twice MyClass.heavyOperation(myVar) with the same input for the sake of performance.
How can I reuse the result of the second operation in the fourth one?
I want to do something like this, which is forbidden:
Object myObj;
Mono.just(myVar)
.flatMap(var -> {
myObj = MyClass.heavyOperation(var);
return myObj;
})
.flatMap(MyClass::anotherHeavyOperation)
.flatMap(res -> doSomething(res, myObj));
Probably the best solution is to put everything that uses myObj in the same pipeline step.
Like this:
Mono.just(myVar)
.flatMap(MyClass::heavyOperation)
.flatMap(myObj -> MyClass.anotherHeavyOperation(myObj)
.flatMap(res -> doSomething(res, myObj)));
The step that uses myObj can in turn be de-composed into a number of smaller sub-pipelines, and the top level pipeline can also continue as normally.
This is the basis of monadic operations in functional languages!
You can create a tuple in the second flat map:
Mono.just(myVar)
.flatMap(MyClass::heavyOperation)
.flatMap(x -> Tuples.of(x, MyClass.anotherHeavyOperation(myVar))
.flatMap(res -> doSomething(res.getT2(), res.getT1()));
Consider keeping the scope:
Mono.just(myVar)
.flatMap(var -> {
Object myObj = MyClass.heavyOperation(var);
return MyClass.anotherHeavyOperation(myObj)
.flatMap(res -> doSomething(res, myObj));
});
You could save Mono to variable and then just zip it again with Mono after anotherHeavyOperation.
var heavyOperation = Mono.just(myVar)
.flatMap(MyClass::heavyOperation)
.cache();
heavyOperation
.flatMap(MyClass::anotherHeavyOperation)
.zipWith(heavyOperation, (res, ho) -> doSomething(res, ho));
I'm changing from ugly nested for loops to a beautiful designed lambda expressions in java.
Here is my actual code
for (String foo : foos) {
for (Bar bar : bars) {
if (bar.getFoo().equals(foo)) {
FooBar fooBar = new FooBar();
fooBar.setBar(bar);
listOfFooBar.add(fooBar);
break;
}
}
}
My actual lambda code to replace code above
foos.forEach(i -> bars.stream().filter(p -> p.getFoo().equals(i)).findFirst().ifPresent(p -> {
FooBar s = new FooBar();
fooBar.setBar(bar);
listOfFooBar.add(fooBar);
}));
My question is, there is a way to populate listOfFooBar with some kind of collect() method?
Something like listOfFooBar = foos.forEach(.....).collect(Collectors.toList());
One fact is that bars will always contain every foo, foos is basically a small part of bars.
If there is a better way (in terms of performance or elegance) to do that lambda, please share.
Since there is only one Bar per Foo, you could start by creating a map linking Foos to Bars:
Map<String, Bar> barsByFoo = bars.stream().collect(toMap(Bar::getFoo, b -> b));
If you have a lot more bars than foos, you can filter:
Map<String, Bar> barsByFoo = bars.stream()
.filter(b -> foos.contains(b.getFoo()))
.collect(toMap(Bar::getFoo, b -> b));
Your nested for loops can then be written:
List<FooBar> listOfFooBar = foos.stream()
.map(barsByFoo::get)
.filter(Objects::nonNull)
.map(FooBar::new)
.collect(toList());
This assumes there is a FooBar(Bar) constructor.
Or you could take the problem from the other side and use an (I think) equivalent algo (you would probably benefit from using a Set<Foo> in that case):
List<FooBar> listOfFooBar = bars.stream()
.filter(bar -> foos.contains(bar.getFoo()))
.map(FooBar::new)
.collect(toList());
Either way, it generally helps to step back from your initial loop as a different algo/approach is generally beneficial to a clean lambda solution.
If you want to go the whole nine yards:
List<FooBar> listOfFooBar = foos.stream()
.flatMap(foo -> bars.stream().filter(bar-> bar.getFoo().equals(foo)).findFirst()
.map(Stream::of).orElse(Stream.empty()))
.map(bar -> {
FooBar fooBar = new FooBar();
fooBar.setBar(bar);
return fooBar;
})
.collect(Collectors.toList());
If you had a FooBar constructor that accepts a Bar then you could save some lines and write
.map(FooBar::new)
FWIW in Java 9 you will be able to write
.findFirst().stream()
Assuming a suitable constructor it would then shorten to
List<FooBar> listOfFooBar = foos.stream()
.flatMap(foo -> bars.stream().filter(bar-> bar.getFoo().equals(foo)).findFirst().stream()))
.map(FooBar::new)
.collect(Collectors.toList());
EDIT:
Using #Misha's suggestion you can shorten it even more:
List<FooBar> listOfFooBar = foos.stream()
.flatMap(foo -> bars.stream().filter(bar-> bar.getFoo().equals(foo)).limit(1)))
.map(FooBar::new)
.collect(Collectors.toList());
If FooBar has a constructor that accepts a Bar as an argument:
public class FooBar {
public FooBar(Bar bar) {
// do something with bar, assign it, etc
}
}
Then, you could do it as follows:
List<FooBar> fooBars = foos.stream()
.map(foo -> bars.stream()
.filter(bar -> bar.getFoo().equals(foo))
.findFirst()
.map(FooBar::new))
.filter(Optional::isPresent)
.map(Optional::get)
.collect(Collectors.toList());
This streams your foos, and for each foo, it streams your bars until it finds the first one that matches the current foo. If a foo is actually found, a new FooBar is created from the inner stream's current bar. This leaves us with a stream of Optional<FooBar>, which is then filtered to only keep the non-empty optionals. Then, the optionals are transformed to the values they contain (which are the FooBars created in the previous step), and finally, these FooBars are collected to a List<FooBar>.
EDIT: That was my first attempt. Much better to use #zeroflagL's approach:
List<FooBar> fooBars = foos.stream()
.flatMap(foo -> bars.stream()
.filter(bar -> bar.getFoo().equals(foo))
.findFirst()
.map(Stream::of).orElse(Stream.empty()))
.map(FooBar::new)
.collect(Collectors.toList());