2

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..!!

Youcef LAIDANI
  • 55,661
  • 15
  • 90
  • 140

3 Answers3

2

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(.....)
Dmitrii B
  • 2,672
  • 3
  • 5
  • 14
  • Thanks for the response. It seems what I was trying to do was a bit too much for what collect offers us. Seems this is the only way (separating the condition check and transformation from the stream operation) I can partition and process both parts without having to iterate again. Thanks for the suggestion...!! – Kingshuk Mukherjee Feb 07 '21 at 00:58
1

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;
}
Nikolas Charalambidis
  • 40,893
  • 16
  • 117
  • 183
  • Thanks for your response. Actually, the code I wrote might have been a bit misleading. What you have shared is definitely going to give me uppercase names for even ages but I also wanted to make the names with odd ages all lowercase. (They are currently in Init caps). I think we can add that logic '? person: ' here. – Kingshuk Mukherjee Feb 07 '21 at 01:02
1

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.

Youcef LAIDANI
  • 55,661
  • 15
  • 90
  • 140
  • Thanks for the response. This will work for sure. Was wondering if there was a way to process both partitions within the collect. I think I was trying to do too much with the collect and seems this is the only way I can partition and process both parts without having to iterate again. Thank you...!! – Kingshuk Mukherjee Feb 07 '21 at 00:55