Transformation after grouping operation using Collectors in java

Viewed 77

I have a simple practice code where I have a simple list of Person objects.

What I wanted to do was to partition them based on their age being an even number of an odd number and then transform their names to upper case if they belonged to the even number group, names to lower case otherwise.

The difficulty I'm facing is performing this transformation using collect() operation.

Here's the simple person class

import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;

@AllArgsConstructor
@Data
@Builder
@NoArgsConstructor
public class Person {

    private String name;

    private Gender gender;

    private int age;

    @Override
    public String toString() {
        return String.format("%s -- %s -- %d", name, gender, age);
    }

}

And Here's a simple list of Person Objects

public static List<Person> getExpandedList() {
        return Arrays.asList(new Person("Sara", Gender.FEMALE, 20), new Person("Sara", Gender.FEMALE, 22),
                new Person("Bob", Gender.MALE, 20), new Person("Paula", Gender.FEMALE, 32),
                new Person("Paul", Gender.MALE, 31), new Person("Jack", Gender.MALE, 2),
                new Person("Jack", Gender.MALE, 72), new Person("Jill", Gender.FEMALE, 12),
                new Person("Mahi", Gender.FEMALE, 11), new Person("Natalie", Gender.FEMALE, 3));
    }

And the following is the code I was trying to use for the transformation.

So I want Mahi, Natalie, and Paul's name to appear in all lower case and rest of them should have names in all upper case and I want the entire person objects back, not just names.

Here's the code I was trying

import java.util.List;
import java.util.Map;

import static java.util.stream.Collectors.*;

public class PartitioningWithCollectors {

    public static void main(String[] args) {
        List<Person> personList = StreamsPracticeUtil.getExpandedList();

 
        Map<Boolean, List<Person>> updatedPersons = personList.stream()
                .collect(partitioningBy(person -> person.getAge() % 2 == 0,
                        //filtering(person -> person.getAge() % 2 == 0,
                        mapping(person -> Person.builder()
                                .age(person.getAge())
                                .gender(person.getGender())
                                .name(person.getName().toUpperCase())
                                .build(), toList())));//);

        System.out.println(updatedPersons);
    }

}

But of course, the problem is once I use any kind of filtering, the other result of the partitioning is gone.

In effect I want to perform the below operation in a functional way

for (Person person : personList) {
            if (person.getAge() % 2 == 0) {
                person.setName(person.getName().toUpperCase());
            } else {
                person.setName(person.getName().toLowerCase());
            }
        }

        System.out.println(personList);

Appreciate any help I can get. Thanks..!!

3 Answers

You can map each Person before .collect();

for example:

personList.stream()
                .map(SET_NAME_CASE)
                .collect(.....)

when SET_NAME_CASE is your custom function:

public static final UnaryOperator<Person> SET_NAME_CASE =
            source -> {
                    if(person.getAge % 2 == 0){
                       return Person.builder()                            
                                .name(person.getName().toUpperCase())
                                .build();
                    } else {
                       return Person.builder()                            
                                .name(person.getName().toLowerCase())
                                .build();
                    }
};

after that, you will have a Person object with a set name. You can do any grouping and also you can set other fields Person. Change your @Builder to @Builder(toBuilder = true) and you will add in your stream any mapping with setting another fields, for example:

  personList.stream()
                    .map(SET_NAME_CASE)
                    .map(
                         source -> Person.toBuilder()
                                    .age(person.getAge())
                                    .gender(person.getGender())
                                    .build()
                     )
                    .collect(.....)

You can achieve this using a downstream collector Collectors.mapping inside Collectors.partitioningBy. Of course, the Collectors.mapping needs Collectors.toList to define the final output.

I also recommend extracting the predicate testing whether the age is even, which will be reused for the partitioning at the first time and then for mapping the values with a ternary operator.

The following code is ready to run:

List<Person> personList = Arrays.asList(new Person("Sara", Gender.FEMALE, 20), 
    new Person("Sara", Gender.FEMALE, 22),
    new Person("Bob", Gender.MALE, 20), new Person("Paula", Gender.FEMALE, 32),
    new Person("Paul", Gender.MALE, 31), new Person("Jack", Gender.MALE, 2),
    new Person("Jack", Gender.MALE, 72), new Person("Jill", Gender.FEMALE, 12),
    new Person("Mahi", Gender.FEMALE, 11), new Person("Natalie", Gender.FEMALE, 3));

Predicate<Person> isAgeEven = person -> person.getAge() % 2 == 0;

Map<Boolean, List<Person>> updatedPersons = personList
    .stream()
    .collect(Collectors.partitioningBy(
        isAgeEven,                                       // partition by the age
        Collectors.mapping(
            person -> isAgeEven.test(person) ?           // is odd or even?
                person :                                 // even aged person is unchanged
                new Person(                              // a constructor/builder
                        person.getName().toUpperCase(),  // make the name uppercased
                        person.getGender(), 
                        person.getAge()),
            Collectors.toList())));                      // pack the values to a list
updatedPersons.forEach((k,v) -> System.out.println(k + " " + v));
false [PAUL -- MALE -- 31, MAHI -- FEMALE -- 11, NATALIE -- FEMALE -- 3]
true [Sara -- FEMALE -- 20, Sara -- FEMALE -- 22, Bob -- MALE -- 20, Paula -- FEMALE -- 32, Jack -- MALE -- 2, Jack -- MALE -- 72, Jill -- FEMALE -- 12]

Alternatively, this mapping function is possible considering a setter:

person -> {
    if (isAgeEven.test(person)) {
        person.setName(person.getName().toUpperCase());
    } return person;
}

If I understand your question, then you need to use:

Map<Boolean, List<Person>> updatedPersons = personList.stream()
        .collect(partitioningBy(person -> person.getAge() % 2 == 0,
                mapping(PartitioningWithCollectors::updatePerson, toList())));

Where updatePerson is a method to change the name to upper or lower case:

private static Person updatePerson(Person person) {
    String name = person.getName();
    if (person.getAge() % 2 == 0) {
        person.setName(name.toUpperCase());
    } else {
        person.setName(name.toLowerCase());
    }
    return person;
}

But: I would use last solution in your question, it is more readable.

Related