I currently have something like below
List<String> myNewList = myList
.stream()
.map(item->{
return mappedItem
})
.collect(Collectors.toList());
repository.save(myNewList);
In Optional, I can perform operations on the mapped item by using ifPresent method like below
myOptional
.map(item -> {
return mappedItem
})
.ifPresent(newItem -> {
repository.save(newItem);
});
I was wondering if I can do something like the above on stream. Rather than declaring myNewList, is there a way I can collect the new List and apply my function on the new list?
Update: Based on the answer from #tagir-valeev, I modified my code as below
myList
.stream()
.map(item->{
return mappedItem
})
.collect(Collectors.collectingAndThen(Collectors.toList(),
list -> {
repository.save(list);
return list;
}
));
You can create your custom collector like this:
myList.stream().map(..)
.collect(Collectors.collectingAndThen(Collectors.toList(), repository::save));
If save return type is void, it would be more ugly as you need to return something from collect:
myList.stream().map(..)
.collect(Collectors.collectingAndThen(Collectors.toList(),
list -> {repository.save(list);return list;}));
You may declare special method in your Repository class:
class Repository {
Collector<MyItemType, ?, List<MyItemType>> saving() {
return Collectors.collectingAndThen(Collectors.toList(),
list -> {this.save(list);return list;});
}
void save(List<MyItemType> list) { ... }
}
And use it:
myList.stream().map(..).collect(repository.saving());
Related
I have an expensive method that I only want to call it when necessary in a stream. Here is an example:
public static Optional<MyObject> findTarget(String input, List<MyObject> myList) {
return Stream.concat(myList.stream(), expensive().stream()).filter(o -> o.hasName(input)).findFirst();
}
The goal is to find the target MyObject from myList based on the input value, but if its not in myList ONLY then it will call expensive() to return a bigger list and look from there.
The above example does not do that, as it seems Stream.concat will call expensive() already before consuming all of myList.
An ugly solution I can think of is to do it in two steps, e.g.:
return myList.stream().filter(o -> o.hasName(input)).findFirst().or(
() -> expensive().stream().filter(o -> o.hasName(input)).findFirst());
But then I will have to repeat the filter and the rest twice.
Is there any better solution or even a single liner of Stream that does that?
You can lazily evaluate by concatenating Supplier<List<MyObject>> instead of List<MyObject>.
public static Optional<MyObject> findTarget(String input, List<MyObject> myList) {
List<Supplier<List<MyObject>>> concat = List.of(() -> myList, () -> expensive());
return concat.stream()
.flatMap(supplier -> supplier.get().stream())
.filter(o -> o.hasName(input))
.findFirst();
}
Test:
record MyObject(String s) {
public boolean hasName(String in) {
return s.equals(in);
}
}
static List<MyObject> expensive() {
System.out.println("expensive() called");
return List.of(new MyObject("z"));
}
public static void main(String[] args) {
List<MyObject> myList = List.of(new MyObject("a"));
System.out.println("case 1: " + findTarget("a", myList));
System.out.println("case 2: " + findTarget("x", myList));
}
Output:
case 1: Optional[MyObject[s=a]]
expensive() called
case 2: Optional.empty
Alternatively you can do this:
public static Optional<MyObject> findTarget(String input, List<MyObject> myList) {
return Stream.of(
(Supplier<List<MyObject>>) () -> myList,
(Supplier<List<MyObject>>) () -> expensive())
.flatMap(supplier -> supplier.get().stream())
.filter(o -> o.hasName(input))
.findFirst();
}
Another alternative, which might be simpler to understand, is to extract the stream logic in a separate method:
private static Optional<MyObject> findInternal(String input, List<MyObject> myList) {
return myList.stream().filter(o -> o.hasName(input)).findFirst();
}
and then simply call it twice:
public static Optional<MyObject> findTarget(String input, List<MyObject> myList) {
return findInternal(input, myList).or(() -> findInternal(input, expensive()));
}
I have a problem with conversion List Object to Map String, List Object. I'm looking for Map with a keys name of all components in cars, and a value is represented by cars with this component
public class Car {
private String model;
private List<String> components;
// getters and setters
}
I write a solution but looking for a better stream solution.
public Map<String, List<Car>> componentsInCar() {
HashSet<String> components = new HashSet<>();
cars.stream().forEach(x -> x.getComponents().stream().forEachOrdered(components::add));
Map<String, List<Car>> mapCarsComponents = new HashMap<>();
for (String keys : components) {
mapCarsComponents.put(keys,
cars.stream().filter(c -> c.getComponents().contains(keys)).collect(Collectors.toList()));
}
return mapCarsComponents;
}
You could do it with streams too, but I find this a bit more readable:
public static Map<String, List<Car>> componentsInCar(List<Car> cars) {
Map<String, List<Car>> result = new HashMap<>();
cars.forEach(car -> {
car.getComponents().forEach(comp -> {
result.computeIfAbsent(comp, ignoreMe -> new ArrayList<>()).add(car);
});
});
return result;
}
Or using stream:
public static Map<String, List<Car>> componentsInCar(List<Car> cars) {
return cars.stream()
.flatMap(car -> car.getComponents().stream().distinct().map(comp -> new SimpleEntry<>(comp, car)))
.collect(Collectors.groupingBy(
Entry::getKey,
Collectors.mapping(Entry::getValue, Collectors.toList())
));
}
I know this is a Java question, and there is already a Java answer. However, I would like to add that Kotlin, which is a JVM language and perfectly interoperable with Java, you can do things like this very easily and cleanly:
val carsByComponent = cars
.flatMap { it.components }
.distinct()
.map { component -> component to cars.filter { car -> component in car.components } }
.toMap()
or even more concise, allthough less readable:
val carsByComponent = cars
.flatMap { car -> car.components.map { it to car } }
.groupBy { it.first }
.mapValues {it.value.map { it.second }}
I'm trying to clone a list to a new list and set a property in the new list.
I'm trying to use Java8 Stream as it makes cloning simple.
My code works but it gives this code smell from Sonar:
Local variables should not be declared and then immediately returned or thrown (squid:S1488)
Is there a way to do this without using a local variable?
code:
List<myObject> clonedList = listToClone.stream()
.map(item -> {
cloned = new myObject(item);
cloned.setLifeCycle("someLifeCycle");
return cloned;
})
.collect(Collectors.toList());
Thanks
It is a warning because you have used a new variable cloned unnecessarily instead of directly chaining functions like
List<myObject> clonedList = listToClone.stream()
.map(item -> {return (new myObject(item)).setLifeCycle("someLifeCycle");})
.collect(Collectors.toList());
You can try this:
List<myObject> clonedList = listToClone.stream()
.map(myObject::new)
.map(o -> {
o.setLifeCycle("someLifeCycle");
return o;
})
.collect(Collectors.toList());
public class MyObject{
private String someLifeCycle;
private Item item;
public MyObject(final String someLifeCycle,final Item item){
this.someLifeCycle = someLifeCycle;
this.item = item;
}
//getters and setters
}
And your code will be like this :
List<MyObject> clonedList = listToClone.stream()
.map(item -> new MyObject(item,"someLifeCycle")).collect(Collectors.toList());
I have a collection constisting of Map<Pair<DateTime, String>, List<Entity>> which was previously grouped using streams. Entity is a simple class with int property and getValue() method.
Now, I want to aggregate values of Entity with usage of my simple EntityAccumulator modyfing the type of the previous map to Map<Pair<DateTime, String>, EntityAccumulator>. The only way to achieve this as far as I understand is to create my own custom collector, howevere I've stucked at finisher() method which should return Pair.
Or, maybe is there simpler way to achieve the result I want ?
StreamProcessing
Map<Pair<DateTime, String>, EntityAccumulator> collect = entities.stream()
.collect(Collectors.groupingBy(entity-> Pair.of(entity.getTimestamp(), entity.getName())))
.entrySet().stream()
.collect(new EntityCollector()));
EntityAccumulator
private static class EntityAccumulator {
private int result = 0.0;
public EntityAccumulator() { }
public EntityAccumulator(int result) {
this.result = result;
}
public void calculate(Entity entity) {
result += entity.getValue();
}
public EntityAccumulatoradd(EntityAccumulator other) {
return new EntityAccumulator(this.result + other.result);
}
}
Collector
public class EntityCollector implements Collector<Map.Entry<Pair<DateTime, String>, List<Entity>>, EntityAccumulator, Map.Entry<Pair<DateTime, String>, EntityAccumulator>> {
#Override
public Supplier<EntityAccumulator> supplier() {
return EntityAccumulator::new;
}
#Override
public BiConsumer<EntityAccumulator, Map.Entry<Pair<DateTime, String>, List<Entity>>> accumulator() {
return (result, pairListEntry) -> pairListEntry.getValue().forEach(result::calculate);
}
#Override
public BinaryOperator<EntityAccumulator> combiner() {
return EntityAccumulator::add;
}
#Override
public Function<EntityAccumulator, Map.Entry<Pair<DateTime, String>, EntityAccumulator>> finisher() {
return (k) -> {
return null; // ??? HELP HERE
}
}
#Override
public Set<Characteristics> characteristics() {
return EnumSet.of(Characteristics.UNORDERED);
}
}
Apparently, you actually want to do
Map<Pair<DateTime, String>, Double> collect = entities.stream()
.collect(Collectors.groupingBy(
entity -> Pair.of(entity.getTimestamp(), entity.getName()),
Collectors.summingDouble(Entity::getValue)));
or
Map<Pair<DateTime, String>, Integer> collect = entities.stream()
.collect(Collectors.groupingBy(
entity -> Pair.of(entity.getTimestamp(), entity.getName()),
Collectors.summingInt(Entity::getValue)));
depending on the actual value type. Your declaration int result = 0.0 isn’t quite clear.
First, if you want to perform reduction on the groups, you should provide the Collector for the values as a second argument to the groupingBy collector. Then, it doesn’t have to deal with neither, Map nor Map.Entry.
Since it’s basically folding the entities to a single number (for each group), you can use an existing collector, i.e. summingInt or summingDouble.
When you create your own collector, you can’t reconstitute information in the finisher function that you have dropped in the accumulator function. If your container type EntityAccumulator contains a single number only, there is no way to produce a Map.Entry<Pair<DateTime, String>, EntityAccumulator> from it.
By the way, you rarely need to implemented the Collector interface with a class, even when creating a custom collector. You can simply use Collector.of, specifying the functions and characteristics, to create a Collector.
So using your original EntityAccumulator class (assuming, result should be int and 0.0 is a typo), you could use
Map<Pair<DateTime, String>, Integer> collect = entities.stream()
.collect(Collectors.groupingBy(
entity -> Pair.of(entity.getTimestamp(), entity.getName()),
Collector.of(EntityAccumulator::new,
EntityAccumulator::calculate,
EntityAccumulator::add,
ea -> ea.result,
Collector.Characteristics.UNORDERED)));
to achieve the same as above. It would also be possible to perform the operation in two steps, like in your attempt, using
Map<Pair<DateTime, String>, Integer> collect = entities.stream()
.collect(Collectors.groupingBy(e -> Pair.of(e.getTimestamp(), e.getName())))
.entrySet().stream()
.collect(Collectors.toMap(Map.Entry::getKey, e -> e.getValue().stream().collect(
Collector.of(EntityAccumulator::new,
EntityAccumulator::calculate,
EntityAccumulator::add,
ea -> ea.result,
Collector.Characteristics.UNORDERED))));
but, of course, this is only for completeness. The solution shown at the beginning of this answer is simpler and more efficient.
I'm trying to construct a custom class instance by Java8's stream API.
public class Foo {
Group group;
// other properties
public Group getGroup() { return this.group; }
public enum Group { /* ... */ };
}
public class FooModel {
private Foo.Group group;
private List<Foo> foos;
// Getter/Setter
}
...
List<Foo> inputList = getFromSomewhere();
List<FooModel> outputList = inputList
.stream()
.collect(Collectors.groupingBy(Foo::getGroup,
???));
But I don't know how the Collector downstream must be.
Do I have to implement a Collector myself (don't think so) or can this be accomplished by a combination of Collectors. calls?
You are looking for something like this:
List<FooModel> outputList = inputList
.stream()
.collect(Collectors.groupingBy(Foo::getGroup))// create Map<Foo.Group,List<Foo>>
.entrySet().stream() // go through entry set to create FooModel
.map(
entry-> new FooModel (
entry.getKey(),
entry.getValue()
)
).collect(Collectors.toList());