Java create multiple lists with duplicate elements from stream

Viewed 549

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());
5 Answers

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.

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
}

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());
    }

}

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));

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.

Related