There is code
class Person {
private ZonedDateTime date ;
private int regionId;
private int centerId;
private int amount1;
private float percent1;
}
List<Person> entityList = new ArrayList<>();
I grouping by year of month like this:
listPerson.stream()
.collect(Collectors.groupingBy(i -> i.getDate().getMonth(),Collectors.collectingAndThen(Collectors.toList(),
l -> {
Integer sumAmount1 = l.stream().collect(Collectors.summingInt(i -> i.getAmount1()));
Double avgPerc1 = l.stream().collect(Collectors.averagingDouble(i -> i.getPercent1()));
List<String> data = new ArrayList<>();
data.add(Integer.toString(sumAmount1));
data.add(Double.toString(avgPerc1));
return data;
}
))).forEach((k,v) -> System.out.println(k.getValue() + "-" + v.toString()));
Also i group by year, regionId, centerId in same manner:
listPerson.stream()
.collect(Collectors.groupingBy(i -> i.getDate().getYear(),Collectors ......
But i got many duplicate code where part with
l -> {...}
repeated many times. How to instead of l -> {...} use a method reference?
IntelliJ can literally just do this for you. You don't even have to think about it.
Keyboard shortcut for hints (yellow) is AltEnter
Here's what I ended up with
public static void main(String[] args)
{
List<Person> listPerson = null;
listPerson.stream()
.collect(Collectors.groupingBy(i -> i.getDate().getMonth(), Collectors.collectingAndThen(Collectors.toList(),
Scratch::apply
)))
.forEach((k,v) -> System.out.println(k.getValue() + "-" + v.toString()));
}
private static List<String> apply(List<Person> l)
{
int sumAmount1 = l.stream().mapToInt(Person::getAmount1).sum();
Double avgPerc1 = l.stream().collect(Collectors.averagingDouble(Person::getPercent1));
List<String> data = new ArrayList<>();
data.add(Integer.toString(sumAmount1));
data.add(Double.toString(avgPerc1));
return data;
}
You can create a method reference like this:
private List<String> methodReference(List<Person> l) {
Integer sumAmount1 = l.stream().collect(Collectors.summingInt(i -> i.getAmount1()));
Double avgPerc1 = l.stream().collect(Collectors.averagingDouble(i -> i.getPercent1()));
List<String> data = new ArrayList<>();
data.add(Integer.toString(sumAmount1));
data.add(Double.toString(avgPerc1));
return data;
}
I have created a methodReference in my Test class. You can replace it with your own class name. And now in your stream() you can refer to it like this:
entityList.stream()
.collect(Collectors.groupingBy(i -> i.getDate().getMonth(), Collectors.collectingAndThen(Collectors.toList(),
Test::methodReference // replace Test with your class name
))).forEach((k, v) -> System.out.println(k.getValue() + "-" + v.toString()));
Might be a little of topic, but I think the beauty of using Stream API is allowing you to build a data pipeline. I would strive to build something that looks like a pipeline, with steps I can customize with pluggable functions.
I think the code would be more readable by refactoring towards a pipeline, and I would try below with the help of a new data structure called Tuple2, epscially its map method. It's easy to build, you can also use one from libraries like vavr.
For reuse, one can consider a function like groupAndSummarize (the name suggests it does two things, so is a smell).
class Tuple2<T1, T2> {
private final T1 t1;
private final T2 t2;
public Tuple2(final T1 t1, final T2 t2) {
this.t1 = t1;
this.t2 = t2;
}
public <U1, U2> Tuple2<U1, U2> map(final Function<T1, U1> func1,
final Function<T2, U2> func2) {
return new Tuple2<>(func1.apply(t1), func2.apply(t2));
}
public T1 _1() { return t1; }
public T2 _2() { return t2; }
}
private <T, K, V> List<Tuple2<K, V>> groupAndSummarize(final List<T> list, final Function<T, K> groupFn, final Function<List<T>, V> summarizeFn) {
return list.stream()
.collect(Collectors.groupingBy(groupFn))
.entrySet()
.stream()
.map(this::toTuple)
.map(t -> t.map(
Function.identity(),
summarizeFn
))
.collect(Collectors.toList());
}
private <K, V> Tuple2<K, V> toTuple(final Map.Entry<K, V> entry) {
return new Tuple2<>(entry.getKey(), entry.getValue());
}
private List<String> summarize(final List<Person> l) {
// your logic
}
public void test() {
final List<Person> entityList = new ArrayList<>();
groupAndSummarize(entityList, i -> i.getDate().getMonth(), this::summarize)
.forEach(t -> System.out.println(t.t1.getValue() + "-" + t.t2.toString()));
}
Related
I have a list of class A and class B with no duplicate elements. "code" attribute will be same across both class A and B. I want to convert them to Map<String, C> using java 8 streams. Please help
public class A {
private String code;
private boolean status;
field 3...
field 4..
}
public class B {
private String code;
private String location;
field 5...
field 5..
}
public class C {
private String location;
private boolean status;
}
List1 : [A(code=NY, status=false), A(code=NJ, status=true),A(code=TX, status=true), A(code=NM, status=false)]
List2 : [B(code=NY, location=NewYork), B(code=NJ, location=NewJersey),B( code=TX, location=Texas), B(code=NM, location=NewMexico)]
Map = map{NY=C(location=NewYork, status=false), NJ=C(location=NewJersey, status=true), TX=C(location=Texas, status=true),NM=C(location=NewMexico, status=false)}
Final map should be in the same order as the elements in List1
~A
UPDATE :
i updated the code to below, but its not compiling. any idea about whats wrong?
package test;
import java.util.*;
import java.util.stream.Collectors;
public class Test {
static void main(String[] args){
new Test().testFunction();
}
void testFunction(){
System.out.println("Hello World");
List<A> listA = Arrays.asList(new A("NY", false), new A("NJ", true), new A("TX", false), new A("AZ", true));
List<B> listB = Arrays.asList(new B("NY", "New York"), new B("NJ", "New Jersey"),
new B("TX", "Texas"), new B("NM", "New Mexico"));
Map<String, B> mapB = listB
.stream()
.collect(Collectors.toMap(B::getCode, b -> b, (b1, b2) -> b1));
Map<String, C> innerJoin = listA
.stream()
.filter(a -> mapB.containsKey(a.getCode())) // make sure a B instance exists with the code
.map(a -> Map.entry(a.getCode(), new C(mapB.get(a.getCode()).getLocation(), a.getStatus())))
.collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue, (c1, c2) -> c1, HashMap::new));
innerJoin.forEach((code, c) -> System.out.println(code + " -> " + c));
}
}
class A {
private String code;
private boolean status;
public A(String code, boolean status) {
this.code = code;
this.status = status;
}
public String getCode() {
return this.code;
}
public boolean getStatus() {
return this.status;
}
}
class B {
private String code;
private String location;
public B(String code, String location) {
this.code = code;
this.location = location;
}
public String getCode() {
return this.code;
}
public String getLocation() {
return this.location;
}
}
class C {
private String location;
private boolean status;
public C(String location, boolean status) {
this.location = location;
this.status = status;
}
public String getLocation() {
return this.location;
}
public boolean getStatus() {
return this.status;
}
}
You should use somethins like:
Map<String, B> mapList2 = list2.stream().collect(Collectors.toMap(B::getCode,
Function.identity()); // create temporary map
Map<String,B> result = list1.stream().collect(
LinkedHashMap::new,
(map, a) -> map.put(
a.getCode(), // get code from A
new C(
mapList2.get(a.getCode()).getLocation() // get location from B
a.getStatus() // get status from A
),
Map::putAll); // use LinkedHashMap to keep order
Map<String, C> map = IntStream.range(0, list1.size())
.mapToObj(i -> new CodeIndex(list1.get(i).code(), i))
.collect(Collectors.toMap(
CodeIndex::code,
ci -> new C(list2.get(ci.index()).location(), list1.get(ci.index()).status()),
(a, b) -> a,
LinkedHashMap::new
));
What happens is that we first get all valid indexes of both lists, then we map to a (custom) container class holding both the index and the code. (I called it CodeIndex, but you may as well use Map.Entry or some Pair class.)
At last, we collect it into a LinkedHashMap (because it retains insertion order), mapping the keys to simply CodeIndex::code and the values to new C(«location_from_list2», «status_from_list1»).
Note that this code traverses the lists once.
Of course, this assumes that:
both lists have the same codes;
all codes of both list1 and list2 are in the same order.
Note that I used record-style getters, that is, getters with the same name as their corresponding field names. For example, location() instead of getLocation().
This task seems like to join two lists of objects by one key property. Taking into account the requirements, it should be like inner join with the help of temporary map created from the listB.
Map<String, B> mapB = listB
.stream()
.(Collectors.toMap(B::getCode, b -> b, (b1, b2) -> b1)); // use merge function as a defense measure
Map<String, C> innerJoin = listA
.stream()
.filter(a -> mapB.containsKey(a.getCode())) // make sure a B instance exists with the code
.map(a -> Map.entry(a.getCode(), new C(mapB.get(a.getCode()).getLocation(), a.isStatus())))
.collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue, (c1, c2) -> c1, LinkedHashMap::new));
Test:
List<A> listA = Arrays.asList(new A("NY", false), new A("NJ", true), new A("TX", false), new A("AZ", true));
List<B> listB = Arrays.asList(new B("NY", "New York"), new B("NJ", "New Jersey"), new B("TX", "Texas"), new B("NM", "New Mexico"));
// ...
innerJoin.forEach((code, c) -> System.out.println(code + " -> " + c));
Output:
NY -> C(location=New York, status=false)
NJ -> C(location=New Jersey, status=true)
TX -> C(location=Texas, status=false)
The "simplest" solution may be based on assumption that the items in both lists are "ordered" by the same code field, then no temporary map is needed:
Map<String, C> simple = IntStream
.range(0, listA.size())
.mapToObj(i -> Map.entry(listA.get(i).getCode(), new C(listB.get(i).getLocation(), listA.get(i).isStatus())))
.collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue, (c1, c2) -> c1, LinkedHashMap::new));
However, such solution is too fragile.
I think you have to use something like this to really maintain the order and group.
List<A> aList = Arrays.asList(new A("NY", false), new A("NJ", true));
List<B> bList = Arrays.asList(new B("NY", "NewYork"), new B("NJ", "NewJersey"));
LinkedHashMap<String, A> mapA =
aList.stream().collect(
Collectors.toMap(
A::getCode,
a -> a,
(e1, e2) -> e1,
LinkedHashMap::new));
LinkedHashMap<String, B> mapB =
bList.stream().collect(
Collectors.toMap(
B::getCode,
b -> b,
(e1, e2) -> e1,
LinkedHashMap::new));
Set<String> keys = new LinkedHashSet<>(mapA.keySet());
keys.addAll(mapB.keySet());
LinkedHashMap<String, C> result2 = keys.stream().collect(
Collectors.toMap(
key -> key,
key -> new C(
mapB.get(key).getLocation(),
mapA.get(key).isStatus()),
(e1, e2) -> e1,
LinkedHashMap::new));
System.out.println(result2);
How do you do the equivalent of the following transform() method using pure functional programming (without the if-conditional).
Meta: I'd appreciate a title edit, I'm not sure how to word this question in "functionalese"
public class Playground {
private static Optional<Map<String,Integer>> transform(List<Tuple<String,Optional<Integer>>> input) {
if (input.stream().anyMatch(t->t.second.isEmpty())) return Optional.empty();
Map<String, Integer> theMap = input.stream()
.map(t -> new Tuple<>(t.first, t.second.get()))
.collect(Collectors.groupingBy(
t1 -> t1.first,
Collectors.mapping(t2 -> t2.second, toSingle())));
return Optional.of(theMap);
}
#Test
public void collect() {
List<Tuple<String,Optional<Integer>>> input1 = new ArrayList<>();
input1.add(new Tuple<>("foo", Optional.of(1)));
input1.add(new Tuple<>("bar", Optional.empty()));
Optional<Map<String,Integer>> result1 = transform(input1);
assertTrue(result1.isEmpty());
List<Tuple<String,Optional<Integer>>> input2 = new ArrayList<>();
input2.add(new Tuple<>("foo", Optional.of(1)));
input2.add(new Tuple<>("bar", Optional.of(2)));
Optional<Map<String,Integer>> result2 = transform(input2);
assertTrue(result2.isPresent());
assertEquals((int)1, (int)result2.get().get("foo"));
assertEquals((int)2, (int)result2.get().get("bar"));
}
private static class Tuple<T1,T2> {
public T1 first;
public T2 second;
public Tuple(T1 first, T2 second) {
this.first = first;
this.second = second;
}
}
public static <T> Collector<T, ?, T> toSingle() {
return Collectors.collectingAndThen(
Collectors.toList(),
list -> list.get(0)
);
}
}
This might work for you:
private static Optional<Map<String, Integer>> transform(
List<Tuple<String, Optional<Integer>>> input) {
return Optional.of(input)
.filter(t -> t.stream().allMatch(a -> a.second.isPresent()))
.map(
in ->
in.stream()
.filter(t -> t.second.isPresent())
.map(t -> new Tuple<>(t.first, t.second.get()))
.collect(
Collectors.groupingBy(
t1 -> t1.first, Collectors.mapping(t2 -> t2.second, toSingle()))));
}
Although my solution does not satisfy your result, I can offer a solution with the ternary operator
private static Map<String, Integer> transform(List<Tuple<String, Optional<Integer>>> input) {
return input.stream().anyMatch(t -> t.second.isEmpty()) ? Collections.emptyMap() :
input.stream()
.map(t -> new Tuple<>(t.first, t.second.get()))
.collect(Collectors.groupingBy(
t1 -> t1.first,
Collectors.mapping(t2 -> t2.second, toSingle())));
}
“pure functional programming” is not necessarily a sign of quality and not an end in itself.
If you want to make the code simpler and more efficient, which may include getting rid of the if-conditional, especially as it bears a second iteration over the source data, you can do it in various ways. E.g.
private static <K,V> Optional<Map<K,V>> transform(List<Tuple<K,Optional<V>>> input) {
final class AbsentValue extends RuntimeException {
AbsentValue() { super(null, null, false, false); }
}
try {
return Optional.of(input.stream().collect(Collectors.toMap(
t1 -> t1.first,
t2 -> t2.second.orElseThrow(AbsentValue::new),
(first,next) -> first)));
} catch(AbsentValue av) {
return Optional.empty();
}
}
When empty optionals are truly the exceptional case, you can make flagging via exception part of the method’s contract, e.g.
public static class AbsentValueException extends RuntimeException {
}
private static <K,V> Map<K,V> transform(List<Tuple<K,Optional<V>>> input)
throws AbsentValueException {
return input.stream().collect(Collectors.toMap(
t1 -> t1.first,
t2 -> t2.second.orElseThrow(AbsentValueException::new),
(first,next)->first));
}
#Test(expected = AbsentValueException.class)
public void collect1() {
List<Tuple<String,Optional<Integer>>> input1 = new ArrayList<>();
input1.add(new Tuple<>("foo", Optional.of(1)));
input1.add(new Tuple<>("bar", Optional.empty()));
Map<String,Integer> result1 = transform(input1);
}
#Test
public void collect2() {
List<Tuple<String,Optional<Integer>>> input2 = new ArrayList<>();
input2.add(new Tuple<>("foo", Optional.of(1)));
input2.add(new Tuple<>("bar", Optional.of(2)));
Map<String,Integer> result2 = transform(input2);
assertEquals((int)1, (int)result2.get("foo"));
assertEquals((int)2, (int)result2.get("bar"));
}
Even better would be not to put optionals into the list of tuples in the first place.
I have the following class structure
public class Store {
private Long storeId;
private Long masterStoreId;
private String operatorIdentifier;
}
public class StoreInfo {
private String operatorIdentifier;
private Set<Long> slaveStoreIds;
public StoreInfo(String operatorIdentifier, Set<Long> slaveStoreIds) {
super();
this.operatorIdentifier = operatorIdentifier;
this.slaveStoreIds = slaveStoreIds;
}
}
I want to collect the "List<Store" into a "Map<Long, StoreInfo>". Is it possible to do so in a single operation/iteration?
List<Store> stores;
Map<Long, Set<Long>> slaveStoresAgainstMasterStore = stores.stream().collect(Collectors
.groupingBy(Store::getMasterStoreId, Collectors.mapping(Store::getStoreId, Collectors.toSet())));
Map<Long, StoreInfo> storeInfoAgainstMasterStore = stores.stream()
.collect(
Collectors
.toMap(Store::getMasterStoreId,
val -> new StoreInfo(val.getOperatorIdentifier(),
slaveStoresAgainstMasterStore.get(val.getMasterStoreId())),
(a1, a2) -> a1));
As masterStoreId and operatorIdentifier are same same in group(comfirmed in comment) you can groupingBy both creating pair of them using AbstractMap.SimpleEntry. Then using Collectors.toMap create map.
Map<Long, StoreInfo> storeInfoMap =
stores.stream()
.collect(Collectors.groupingBy(
e -> new AbstractMap.SimpleEntry<>(e.getMasterStoreId(),
e.getOperatorIdentifier()),
Collectors.mapping(Store::getStoreId, Collectors.toSet())))
.entrySet()
.stream()
.collect(Collectors.toMap(e -> e.getKey().getKey(),
e -> new StoreInfo(e.getKey().getValue(), e.getValue())));
To complete the implementation, you were attempting. You need to ensure merging capability within StoreInfo such as :
public StoreInfo(String operatorIdentifier, Long slaveStoreId) {
this.operatorIdentifier = operatorIdentifier;
this.slaveStoreIds = new HashSet<>();
this.slaveStoreIds.add(slaveStoreId);
}
public static StoreInfo mergeStoreInfo(StoreInfo storeInfo1, StoreInfo storeInfo2) {
Set<Long> slaveIds = storeInfo1.getSlaveStoreIds();
slaveIds.addAll(storeInfo2.getSlaveStoreIds());
return new StoreInfo(storeInfo1.getOperatorIdentifier(), slaveIds);
}
this would simplify the implementation of collector and you an invoke these correspondingly:
Map<Long, StoreInfo> storeInfoAgainstMasterStore = stores.stream()
.collect(Collectors.toMap(Store::getMasterStoreId,
store -> new StoreInfo(store.getOperatorIdentifier(), store.getStoreId()),
StoreInfo::mergeStoreInfo));
I have POJO definition as follows:
class EmployeeDetails{
private String deptName;
private Double salary;
private Double bonus;
...
}
Currently, i have lambda expression for Group By 'deptName' as :
$set.stream().collect(Collectors.groupingBy(EmployeeDetails::getDeptName,
Collectors.summingLong(EmployeeDetails::getSalary));
Question Is it possible to Sum more than one column? I need to compute sum on both fields salary and bonus in one expression instead of multiple times?
SQL representation would be:
SELECT deptName,SUM(salary),SUM(bonus)
FROM TABLE_EMP
GROUP BY deptName;
You need to create an additional class that will hold your 2 summarised numbers (salary and bonus). And a custom collector.
Let's say you have
private static final class Summary {
private double salarySum;
private double bonusSum;
public Summary() {
this.salarySum = 0;
this.bonusSum = 0;
}
#Override
public String toString() {
return "Summary{" +
"salarySum=" + salarySum +
", bonusSum=" + bonusSum +
'}';
}
}
for holding sums. Then you need a collector like this:
private static class EmployeeDetailsSummaryCollector implements Collector<EmployeeDetails, Summary, Summary> {
#Override
public Supplier<Summary> supplier() {
return Summary::new;
}
#Override
public BiConsumer<Summary, EmployeeDetails> accumulator() {
return (summary, employeeDetails) -> {
summary.salarySum += employeeDetails.salary;
summary.bonusSum += employeeDetails.bonus;
};
}
#Override
public BinaryOperator<Summary> combiner() {
return (summary, summary1) -> {
summary.salarySum += summary1.salarySum;
summary.bonusSum += summary1.bonusSum;
return summary;
};
}
#Override
public Function<Summary, Summary> finisher() {
return Function.identity();
}
#Override
public Set<Characteristics> characteristics() {
return EnumSet.of(Collector.Characteristics.IDENTITY_FINISH);
}
}
With these classes you can collect your results like
final List<EmployeeDetails> employees = asList(
new EmployeeDetails(/* deptName */"A", /* salary */ 100d, /* bonus */ 20d),
new EmployeeDetails("A", 150d, 10d),
new EmployeeDetails("B", 80d, 5d),
new EmployeeDetails("C", 100d, 20d)
);
final Collector<EmployeeDetails, Summary, Summary> collector = new EmployeeDetailsSummaryCollector();
final Map<String, Summary> map = employees.stream()
.collect(Collectors.groupingBy(o -> o.deptName, collector));
System.out.println("map = " + map);
Which prints this:
map = {A=[salary=250.0, bonus=30.0], B=[salary=80.0, bonus=5.0], C=[salary=100.0, bonus=20.0]}
I know you've got your answer, but here is my take(I was writing while the other was posted). There is already a Pair in java in the form of AbstractMap.SimpleEntry.
System.out.println(Stream.of(new EmployeeDetails("first", 50d, 7d), new EmployeeDetails("first", 50d, 7d),
new EmployeeDetails("second", 51d, 8d), new EmployeeDetails("second", 51d, 8d))
.collect(Collectors.toMap(EmployeeDetails::getDeptName,
ed -> new AbstractMap.SimpleEntry<>(ed.getSalary(), ed.getBonus()),
(left, right) -> {
double key = left.getKey() + right.getKey();
double value = left.getValue() + right.getValue();
return new AbstractMap.SimpleEntry<>(key, value);
}, HashMap::new)));
Grouping by is a terminal operation that yields a map. The map produced by the groupingBy in the code below is a Map<String, List<EmployeeDetails>>. I create a new stream using the Map entrySet method. I then create a new Map using Collectors.toMap. This approach uses method chaining to avoid creating another class and create more concise code.
details.stream()
.collect(Collectors.groupingBy(EmployeeDetails::getDeptName))
.entrySet()
.stream()
.collect(Collectors.toMap(x->x.getKey(), x->x.getValue()
.stream()
.mapToDouble(y -> y.getSalary() + y.getBonus())
.sum()));
I am working with collectors' groupingBy and partioningBy functions. I an working with a list of persons, the list of persons is as follows:
List<Person> persons =
Arrays.asList(
new Person("Max", 18),
new Person("Peter", 23),
new Person("Pamela", 23),
new Person("David", 12),
new Person("Pam", 12));
What i want is to partition the list on the basis of persons whose name starts with letter "P" and then group them on the basis of their ages.
Here is my code which does the above filtration:
Map<Boolean, Map<Object, List<Person>>> rr = persons.stream()
.collect(Collectors.partitioningBy(p -> p.name.startsWith("P"),
Collectors.groupingBy(p -> p.age > 20)));
And the output which i got is:
rr = {false={false=[Max, David]}, true={false=[Pam], true=[Peter, Pamela]}}
Now, my requirement is to get only internal map from the above results. That is, i want to change the return values to:
{false=[Pam], true=[Peter, Pamela]}
That is, I want the results (or partitioned map) whose boolean value is true as returned by the partioningBy function. How can i achieve this?
You could create a custom collector (I've done it only as an exercise, please treat it as such):
static class MyCustom<T, U> implements Collector<Person, List<Person>, Map<T, List<U>>> {
private final Function<Person, T> function;
private final Predicate<Person> predicate;
private final Function<Person, U> transformingFunction;
public MyCustom(Predicate<Person> predicate, Function<Person, T> function,
Function<Person, U> transformingFunction) {
this.predicate = predicate;
this.function = function;
this.transformingFunction = transformingFunction;
}
#Override
public Supplier<List<Person>> supplier() {
return ArrayList::new;
}
#Override
public BiConsumer<List<Person>, Person> accumulator() {
return (list, person) -> {
if (predicate.test(person)) {
list.add(person);
}
};
}
#Override
public BinaryOperator<List<Person>> combiner() {
return (l1, l2) -> {
l1.addAll(l2);
return l1;
};
}
#Override
public Function<List<Person>, Map<T, List<U>>> finisher() {
return list -> {
return list.stream().collect(
Collectors.groupingBy(function, Collectors.mapping(transformingFunction, Collectors.toList())));
};
}
#Override
public Set<java.util.stream.Collector.Characteristics> characteristics() {
return EnumSet.of(Characteristics.UNORDERED);
}
}
And then apply it like this:
MyCustom<Integer, String> custom = new MyCustom<>((Person p) -> p.getName().startsWith("P"),
(Person p) -> p.getAge(), Person::getName);
System.out.println(persons.stream().collect(custom)); // {23=[Peter, Pamela], 12=[Pam]}
Filter by name
Apply partitioning by age
Map<Boolean, List<Person>> p1 = persons.stream().filter(p -> p.name.startsWith("P")).collect(Collectors.partitioningBy(p -> p.getAge() > 20));