2

I'm attempting to take a list and return a Map<String, List<String> containing certain aspects of the original list, where the original elements could occur multiple times as values under different keys using streams. For example:

class Employee {
  String firstName;
  String lastName;
  Status status;
}

enum Status {
  FULL_TIME,
  PART_TIME
}

List<Employee> employees = List.of(
  new Employee("f1", "l1", Status.PART_TIME),
  new Employee("f2", "l2", Status.FULL_TIME),
  new Employee("f3", "l3", Status.PART_TIME),
  new Employee("f4", "l4", Status.FULL_TIME));

The resulting map should have keys like the following and lists need to be immutable:

fName: ["f1", "f2", "f3", "f4"],
lName: ["l1", "l2", "l3", "l4"],
fullTime: ["f2 l2", "f4 l4"]

This would be easy with multiple streams, but the original data is quite large, so am trying to do it in one pass. I also have Guava and Apache Commons available if there is another method I'm not aware of. Thanks!

Edit 1: show how I'm doing it now

Here is what I'm doing in three streams that I'd like to simplify to one:

var firstName = employees.stream()
        .map(e -> e.getFirstName())
        .collect(Collectors.toUnmodifiableList());
var lastName = employees.stream()
        .map(e -> e.getLastName())
        .collect(Collectors.toUnmodifiableList());
var fullTime = employees.stream()
        .filter(e -> e.getStatus().equals(Status.FULL_TIME))
        .collect(Collectors.toUnmodifiableList());
6
  • 1
    What have you tried? If nothing, why not? Commented Mar 19, 2021 at 23:31
  • Is there any reason this has to be done with a stream? This particular problem doesn't lend itself very well to streams because each of the elements in your map is a different property, so they aren't related to each other in any way. A for (or forEach) loop seems like a much cleaner solution. Commented Mar 20, 2021 at 0:03
  • @Michael Apologies, I didn't think what I tried added anything to the original question. I have tried many incantations of stream, including using groupingBy, partitioningBy, mapping, collectingAndThen, etc. What I keep running into is I can modify and return single elements, but can't return a single element to multiple keys. Commented Mar 20, 2021 at 0:18
  • @CharlieArmstrong Thanks, I'd be more than happy to use another method, but I need immutability. A forEach works if I'm doing multiple passes, but can't figure out how to do it in one pass with immutability. Commented Mar 20, 2021 at 0:20
  • I would have recommended something similar to k1r0's answer, where you populate the lists, and then convert them to immutable lists. Reading your comment down there, I'm not sure I understand what you mean when you say you "need immutable lists from the start." You can't populate immutable lists, by dint of being immutable. It sounds like your working with more restrictions than you're telling us here. Can you try to elaborate on what you're really trying to do here? Commented Mar 20, 2021 at 0:36

5 Answers 5

3

You can convert the employee object to list of pairs where the pair key is the map key and the pair value is single value of the list. Then you can "group by" the pairs by the key.

Map<String, List<String>> vals = employees.stream()
  .map(e -> Arrays.asList(
    Pair.of("fname", e.firstName),
    Pair.of("lname", e.lastName),
    e.status == Status.FULL_TIME ? Pair.of("fullTime", e.firstName + " " + e.lastName) : null
  ))
  .flatMap(Collection::stream)
  .filter(Objects::nonNull)
  .collect(Collectors.groupingBy(p -> p.getLeft(), 
           Collectors.mapping(p -> p.getRight(), Collectors.toUnmodifiableList())));

However the memory consumption of this solution is worse than the one in the my previous answer, specially when dealing with large streams.

Sign up to request clarification or add additional context in comments.

Comments

2

Here is an example, but you will add a few additional lines and third class.

Main

var employees = List.of(
        new Employee("f1", "l1", Status.PART_TIME),
        new Employee("f2", "l2", Status.FULL_TIME),
        new Employee("f3", "l3", Status.PART_TIME),
        new Employee("f4", "l4", Status.FULL_TIME)
);

var maps = employees.stream()
        .flatMap(employee -> Stream.of(
                new Pair("fName", employee.firstName),
                new Pair("lName", employee.lastName),
                new Pair(
                        employee.status == Status.FULL_TIME ? "fullTime" : "partTime",
                        employee.firstName.concat(" ").concat(employee.lastName)
                )
        ))
        .filter(pair -> !pair.name.equals("partTime"))
        .collect(Collectors.groupingBy(
                pair -> pair.name,
                Collectors.mapping(pair -> pair.value, Collectors.toUnmodifiableList())
        ));

Related classes

class Employee {
    String firstName;
    String lastName;
    Status status;

    //constructors, setters and getters
}

enum Status {
    FULL_TIME,
    PART_TIME
}

class Pair {
    String name;
    String value;

    //constructors, setters and getters
}

Comments

1

I suggest that you put each stream that generate a list in method. like this :

public static List<String> getListFirstName(List<Employee> employees) {
    return   employees.stream()
            .map(e -> e.getFirstName())
            .collect(Collectors.toUnmodifiableList());
}

public static List<String> getListLasttName(List<Employee> employees) {
    return  employees.stream()
            .map(e -> e.getLastName())
            .collect(Collectors.toUnmodifiableList());
}

public static List<String> getListFullTime(List<Employee> employees) {
    return  employees.stream()
            .filter(e -> e.getStatus().equals(Status.FULL_TIME))
            .map(e -> e.getFirstName()+" "+e.getLastName())
            .collect(Collectors.toUnmodifiableList());
}

public static List<String> getListPartTime(List<Employee> employees) {
    return  employees.stream()
            .filter(e ->e.getStatus().equals(Status.PART_TIME))
            .map(e -> e.getFirstName()+" "+e.getLastName())
            .collect(Collectors.toUnmodifiableList());
}

Create a method that return a Map<String,List> that take a List of employees and a string criteria to generate a Map (the key is the criteria and the value is a list contains the wanted data) :

public static Map<String, List<String>> getByCriteria(String criteria , List<Employee> employees) {
    Map<String, List<String>>map =  new HashMap<>();
    
    if("fName".equals(criteria)) {
        map.put(criteria, getListFirstName(employees));
    }else if("lName".equals(criteria)) {
        map.put(criteria, getListFirstName(employees));
    }else if("fullTime".equals(criteria)) {
        map.put(criteria, getListFullTime(employees));
    }else if("partTime".equals(criteria)) {
        map.put(criteria, getListPartTime(employees));
    }
    return Collections.unmodifiableMap(map);
}

Create a List of criteria like :

    List<String> criterias = List.of("fName", "lName", "fullTime", "partTime");

Create a stream from criterias list to generate a final Map that contain what you want

Map<String, List<String>> collect = criterias.stream().parallel().map(c -> getByCriteria(c , employees)).flatMap(map -> map.entrySet().stream())
        .collect(Collectors.toMap(eM -> eM.getKey(), eM -> eM.getValue())); 

And here the complete class:

import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;

public class Main {

    public static void main(String[] args) {
        List<Employee> employees = List.of(new Employee("f1", "l1", Status.PART_TIME),
                new Employee("f2", "l2", Status.FULL_TIME), new Employee("f3", "l3", Status.PART_TIME),
                new Employee("f4", "l4", Status.FULL_TIME));
        List<String> criterias = List.of("fName", "lName", "fullTime", "partTime");
        
        Map<String, List<String>> collect = criterias.stream().parallel().map(c -> getByCriteria(c , employees)).flatMap(map -> map.entrySet().stream())
        .collect(Collectors.toMap(eM -> eM.getKey(), eM -> eM.getValue()));
        
        System.out.println(collect);
        

    }
    
    public static Map<String, List<String>> getByCriteria(String criteria , List<Employee> employees) {
        Map<String, List<String>>map =  new HashMap<>();
        
        if("fName".equals(criteria)) {
            map.put(criteria, getListFirstName(employees));
        }else if("lName".equals(criteria)) {
            map.put(criteria, getListFirstName(employees));
        }else if("fullTime".equals(criteria)) {
            map.put(criteria, getListFullTime(employees));
        }else if("partTime".equals(criteria)) {
            map.put(criteria, getListPartTime(employees));
        }
        return Collections.unmodifiableMap(map);
    }
    
    public static List<String> getListFirstName(List<Employee> employees) {
        return   employees.stream()
                .map(e -> e.getFirstName())
                .collect(Collectors.toUnmodifiableList());
    }
    
    public static List<String> getListLasttName(List<Employee> employees) {
        return  employees.stream()
                .map(e -> e.getLastName())
                .collect(Collectors.toUnmodifiableList());
    }
    
    public static List<String> getListFullTime(List<Employee> employees) {
        return  employees.stream()
                .filter(e -> e.getStatus().equals(Status.FULL_TIME))
                .map(e -> e.getFirstName()+" "+e.getLastName())
                .collect(Collectors.toUnmodifiableList());
    }
    
    public static List<String> getListPartTime(List<Employee> employees) {
        return  employees.stream()
                .filter(e ->e.getStatus().equals(Status.PART_TIME))
                .map(e -> e.getFirstName()+" "+e.getLastName())
                .collect(Collectors.toUnmodifiableList());
    }

}

1 Comment

Thanks, this is a nice idea too, might end up using something like this.
1

Since you said you can use apache commons, maybe something like this?

    Map<String, List<String>> map = new HashMap<>();
    List<Employee> employees = List.of(
            new Employee("f1", "l1", Status.PART_TIME),
            new Employee("f2", "l2", Status.FULL_TIME),
            new Employee("f3", "l3", Status.PART_TIME),
            new Employee("f4", "l4", Status.FULL_TIME));

    List<String> firstNames = new ArrayList<>();
    List<String> lastNames = new ArrayList<>();
    List<String> ftEmployees = new ArrayList<>();

    employees.forEach(employee -> {
        firstNames.add(employee.firstName);
        lastNames.add(employee.lastName);
        if (employee.status == Status.FULL_TIME) {
            ftEmployees.add(employee.firstName + " " + employee.lastName);
        }
    });

    map.put("fname", ImmutableList.copyOf(firstNames));
    map.put("lName", ImmutableList.copyOf(lastNames));
    map.put("fullTime", ImmutableList.copyOf(ftEmployees));

Comments

0

Here is example code that is doing what you want. There is no need for multiple streams only one stream is enough to iterate throught the objects.

        Map<String, List<String>> result = new HashMap<>();
        result.put("fname", new ArrayList<String>());
        result.put("lname", new ArrayList<String>());
        result.put("fullTime", new ArrayList<String>());
        List<Employee> employees = List.of(
                  new Employee("f1", "l1", Status.PART_TIME),
                  new Employee("f2", "l2", Status.FULL_TIME),
                  new Employee("f3", "l3", Status.PART_TIME),
                  new Employee("f4", "l4", Status.FULL_TIME));
        employees.forEach(e -> {
            result.get("fname").add(e.firstName);
            result.get("lname").add(e.lastName);
            result.get("fullTime").add(e.firstName + " " + e.lastName);
        });
        System.out.println(result);

I will leave it to you to convert the lists to Immutable lists.

4 Comments

Thanks for the response. Unfortunately, I need immutable lists from the start, not converting mutable to immutable.
You need to collect the values for each field, that cannot be done with immutable lists. Even if you find a function in some library that returns Immutable list internally it is implemented with mutable list.
Thanks for the clarification. I was thinking something akin to using .collect(Collectors.toUnmodifiableList()) as the terminal operation.
I edited the original post to provide more context into what I'm doing now and want to get down to one pass.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.