Преобразуйте список сотрудников в несколько карт, группируя их по каждому из атрибутов сотрудника

#java #collections #java-8 #java-stream

Вопрос:

У меня есть список сотрудников :

Вот пример :

 Employee e1 = {"empId": "1", "name": "Jack","languages": ["Java","React","R","Flutter"]}
Employee e2 = {"empId": "2", "name": "Jill","languages": ["Java","C"]}
Employee e3 = {"empId": "3", "name": "Mark","languages": ["Cobol"]}
Employee e4 = {"empId": "4", "name": "Michael","languages": ["React","R","Flutter"]}
Employee e5 = {"empId": "5", "name": "Jill","languages": ["Node","Cobol"]}
 

Эти сотрудники перечислены в Списке : List<Employee> employeeList

Объект сотрудник-это простое POJO:

 public class Employee {
    private String empId;
    private String name;
    private List<String> languages;
 

Мне нужно преобразовать этот список сотрудников в несколько карт :

( для каждого атрибута сотрудника )

 Map m1 >> key = id , value = List<String> containing id which are same 

Map m2 >> key = name , value = List<String> containing ids for employee's with the same name

Map m3 >> key = each value in languages list , value = List<String> containing ids for employee's 
who have the same language
 

Итак, вот результат, который мне нужен :

 Map mapById = [{"1",List<String>("1")},
               {"2",List<String>("2")},
               {"3",List<String>("3")},
               {"4",List<String>("4")}
               {"5",List<String>("5")}];
 

Согласитесь , группировка по идентификатору сотрудника не имеет смысла , так как никогда не может быть двух сотрудников с одинаковым идентификатором , поэтому мы можем игнорировать часть , основанную на идентификаторе сотрудника, но мне нужно сгруппироваться по многим другим атрибутам сотрудника ( фамилия, город и т. Д. )

 Map mapByName = [{"Jack",List<String>("1")},
                 {"Jill",List<String>("2","5")},
                 {"Mark",List<String>("3")},
                 {"Michael",List<String>("4")}]
 

Обратите внимание, что в приведенном выше примере Джилл встречается дважды, поэтому против Джилл в списке arraylist у нас есть идентификаторы сотрудников обеих Джилл

 Map mapByLanguage = [{"Java",List<String>("1","2")},
                     {"React",List<String>("1","4")},
                     {"R",List<String>("1","4")},
                     {"Flutter",List<String>("1","4")},
                     {"C",List<String>("2")},
                     {"Cobol",List<String>("3,5")},
                     {"Node",List<String>("5")}]
 

Итак, выше мы группируем по общим языкам

Вопрос № 1 Эти данные будут огромными , связанными со слиянием двух организаций, поэтому перебор коллекций, я думаю, не будет масштабироваться

Вопрос № 2 Я попробовал ниже код — ниже метод группирует данные на основе имени сотрудника , но нужно ли мне писать отдельные методы для группировки по идентификатору , языкам, а затем по другим атрибутам, таким как адрес, город и т. Д

можно ли все это сделать одним методом, который преобразует список в отдельные карты ?

 Map<String, List<String>> groupByName(List<Employee> empList) {

    Map<String, List<String>> mappedInfo = 
            empList.stream()
            .collect(
                Collectors.toMap(
                    Employee::getName,
                    emp -> {
                        List list = new ArrayList<String>();
                        list.add(emp.getEmpId());
                        return list;
                    },
                    (s, a) -> {
                        s.add(a.get(0));
                        return s;
                    }
                )
            );
    return mappedInfo;
}
 

Вопрос № 3
Мне еще предстоит перенести список языков, содержащихся в каждом сотруднике, в приведенное выше решение
, и я не знаю, как это сделать …..
Я не хочу перебирать каждого сотрудника по каждому из их языков

Комментарии:

1. Относительно Map m1 >> key = id , value = List<String> containing id which are same того, не отличается ли исходная карта, то есть только один Employee объект для каждого идентификатора? Поэтому нет смысла сопоставлять список Employee объектов с идентификатором, когда в этом списке будет только один элемент.

2. согласен , и я уже заявил то же самое в отношении идентификатора, но для краткости я не упомянул различные другие атрибуты / поля сотрудника, для которых мне нужно сгруппировать

3. исправлены имена переменных экземпляра сотрудника

4. Почему бы не использовать полный объект сотрудника в списках карт? Объем потребляемой памяти идентичен.

Ответ №1:

Для краткости давайте используем запись в Java 16 для хранения наших выборочных данных о сотрудниках. Запись уместна, когда основная цель передачи данных прозрачна и неизменна. Компилятор неявно создает конструктор по умолчанию, геттеры, equals amp; hashCode , и toString .

 public record Employee( int id , String name , List < String > languages ) { }
 

С таким же успехом мы могли бы использовать обычный класс для более ранних версий, чем Java 16.

 package work.basil.emp;

import java.util.List;
import java.util.Objects;

public final class Employee {
    private final int id;
    private final String name;
    private final List < String > languages;

    public Employee ( int id , String name , List < String > languages ) {
        this.id = id;
        this.name = name;
        this.languages = languages;
    }

    public int id () { return id; }

    public String name () { return name; }

    public List < String > languages () { return languages; }

    @Override
    public boolean equals ( Object obj ) {
        if ( obj == this ) return true;
        if ( obj == null || obj.getClass() != this.getClass() ) return false;
        var that = ( Employee ) obj;
        return this.id == that.id amp;amp;
                Objects.equals( this.name , that.name ) amp;amp;
                Objects.equals( this.languages , that.languages );
    }

    @Override
    public int hashCode () {
        return Objects.hash( id , name , languages );
    }

    @Override
    public String toString () {
        return "Employee["  
                "id="   id   ", "  
                "name="   name   ", "  
                "languages="   languages   ']';
    }
}
 

Приведите несколько примеров данных.

 List < Employee > employees =
        List.of(
                new Employee( 1 , "Jack" , List.of( "Java" , "React" , "R" , "Flutter" ) ) ,
                new Employee( 2 , "Jill" , List.of( "Java" , "C" ) ) ,
                new Employee( 3 , "Mark" , List.of( "Cobol" ) ) ,
                new Employee( 4 , "Michael" , List.of( "React" , "R" , "Flutter" ) ) ,
                new Employee( 5 , "Jill" , List.of( "Node" , "Cobol" ) )
        );
 

Общее имя

Составьте свою новую карту, чтобы упорядочить ее по свойствам. Здесь мы сопоставляем каждое имя с набором сотрудников, использующих это имя.

 Map < String, Set < Integer > > nameInCommonMap = new HashMap <>();
 

Для каждого из наших объектов сотрудника назначьте ему набор. Это называется мультимапом, когда ключ сопоставляется с набором значений, а не с одним значением. Этот Map#computeIfAbsent метод позволяет легко создать многомерную карту, создавая нашу вложенную коллекцию для каждой новой записи, прежде чем мы введем новый ключ.

 for ( Employee employee : employees ) {
    nameInCommonMap.computeIfAbsent( employee.name() , k -> new HashSet < Integer >() ).add( employee.id() );
}
 

Наконец, я считаю, что, как правило, лучше всего возвращать неизменяемые коллекции, когда это практически возможно. Итак, давайте сделаем нашу мультиплексную карту неизменяемой в два этапа. Первый шаг: Замените каждый вложенный набор объектов сотрудников неизменяемым набором, вызвав Set.copyOf Java 10 .

 nameInCommonMap.replaceAll( ( k , v ) -> Set.copyOf( v ) ); // Make nested set unmodifiable.
 

Второй шаг: Создайте неизменяемую карту, позвонив Map.copyOf .

 nameInCommonMap = Map.copyOf( nameInCommonMap ); // Make outer map unmodifiable.
 

И мы закончили.

 nameInCommonMap = {Mark=[3], Michael=[4], Jill=[5, 2], Jack=[1]}
 

Общий язык

Логика почти такая же, чтобы получить карту языка для набора идентификаторов сотрудников для сотрудников, у которых этот язык содержится во вложенном списке. Единственная загвоздка в том, что в нашем цикле Employee объектов мы должны добавить вложенный цикл языков.

Весь пример класса:

 package work.basil.emp;

import java.util.*;

public class App {
    public static void main ( String[] args ) {
        App app = new App();
        app.demo();
    }

    private void demo () {
        record Employee( int id , String name , List < String > languages ) { }
        
        List < Employee > employees =
                List.of(
                        new Employee( 1 , "Jack" , List.of( "Java" , "React" , "R" , "Flutter" ) ) ,
                        new Employee( 2 , "Jill" , List.of( "Java" , "C" ) ) ,
                        new Employee( 3 , "Mark" , List.of( "Cobol" ) ) ,
                        new Employee( 4 , "Michael" , List.of( "React" , "R" , "Flutter" ) ) ,
                        new Employee( 5 , "Jill" , List.of( "Node" , "Cobol" ) )
                );

        Map < String, Set < Integer > > languageInCommonMap = new HashMap <>();
        for ( Employee employee : employees ) {
            for ( String language : employee.languages() ) {
                languageInCommonMap.computeIfAbsent( language , k -> new HashSet < Integer >() ).add( employee.id() );
            }
        }
        languageInCommonMap.replaceAll( ( k , v ) -> Set.copyOf( v ) ); // Make nested set unmodifiable.
        languageInCommonMap = Map.copyOf( languageInCommonMap ); // Make outer map unmodifiable.

        System.out.println( "languageInCommonMap = "   languageInCommonMap );
    }
}
 

Когда бегут.

 languageInCommonMap = {React=[4, 1], Cobol=[5, 3], Java=[2, 1], Flutter=[4, 1], Node=[5], C=[2], R=[4, 1]} 
 

Ваши Вопросы

Вопрос № 1 Эти данные будут огромными , связанными со слиянием двух организаций, поэтому перебор коллекций, я думаю, не будет масштабироваться

Это кажется противоречием. Ваш вопрос и примеры начинаются с коллекций. Это означает, что все данные этих коллекций уже находятся в памяти. Зацикливание данных в памяти для создания новых структур данных, содержащих те же данные, не займет много больше памяти.

Все ссылки на объекты, копируемые между старой и новой коллекциями, — это просто ссылки на объекты. Объекты (строка имени сотрудника, строка языка и т.д.) Не копируются.

Вопрос № 2 … можно ли все это сделать одним методом, который преобразует список в отдельные карты ?

Возможно.

Но я не вижу особой пользы. Код, который я показал здесь, состоит всего из четырех строк: создание карты в одной строке и 3 строки for цикла для заполнения этой карты.

Объем выполняемой работы одинаков, независимо от того, используете ли вы потоки или циклы.

Вопрос № 3 Мне еще предстоит перенести список языков, содержащихся в каждом сотруднике … Я не хочу перебирать каждого сотрудника по каждому из их языков

Нет никакого способа обойти петлю.

Либо вы зацикливаете список вложенных языков, либо вызываете утилиту, такую как поток, для зацикливания списка вложенных языков.

В вычислительной технике не существует волшебной операции «извлечения данных», которая позволяла бы избежать прикосновения к каждому исследуемому элементу данных.

Комментарии:

1. во — первых-спасибо за подробное объяснение . Одно сомнение: чтобы получить список языков, которые я вижу, у вас есть два цикла, по одному для каждого сотрудника и по одному для повторения каждого языка для сотрудника . Я не эксперт , но просто интересно, будет ли это дорогостоящей операцией ?

2. @akila Почему ты думаешь, что это дорого? С помощью какого другого механизма вы могли бы выполнить эту работу? Как вы можете обрабатывать каждый язык для каждого сотрудника, не рассматривая каждый язык для каждого сотрудника? Смотрите мой последний раздел, посвященный вашему вопросу № 3.

Ответ №2:

Вы можете использовать отражение для динамического перебора объявленных полей и использовать лучшую функцию Java 8, поток, для достижения своей цели.

Сделай это:

 Field[] fields = Employee.class.getDeclaredFields();

Map<String, Map<String, List<String>>> result = new HashMap<>();
for (Field field : fields) {
    if (field.getType() == List.class) {
        result.put(field.getName(), employees.stream()
                .flatMap(e -> e.getLanguages().stream()
                        .map(l -> new AbstractMap.SimpleEntry<>(l, e.getEmpId())))
                .collect(Collectors.groupingBy(Map.Entry::getKey,
                        Collectors.mapping(Map.Entry::getValue,
                                Collectors.toList()))));
    } else {
        result.put(field.getName(), employees.stream()
                .collect(Collectors.groupingBy(e -> (String) callGetter(e, field.getName()),
                        Collectors.mapping(Employee::getEmpId,
                                Collectors.toList()))));
    }
}

System.out.println(result);
 

Здесь метод callGetter выглядит следующим образом:

 private static Object callGetter(Object obj, String fieldName) {
    PropertyDescriptor pd;
    try {
        pd = new PropertyDescriptor(fieldName, obj.getClass());
        return pd.getReadMethod().invoke(obj);
    } catch (IntrospectionException | IllegalAccessException | IllegalArgumentException | InvocationTargetException e) {
        // TODO Auto-generated catch block
        e.printStackTrace();
        return new Object();
    }
}
 

И программа выдает вам результат в виде следующей карты:

 {empId={1=[1], 2=[2], 3=[3], 4=[4], 5=[5]}, 
languages={Java=[1, 2], R=[1, 4], C=[2], Node=[5], Cobol=[3, 5], Flutter=[1, 4], React=[1, 4]}, 
name={Michael=[4], Mark=[3], Jill=[2, 5], Jack=[1]}}
 

Ответ №3:

Попробуй это.

 public static void main(String[] args) {
    List<Employee> employees = List.of(
        new Employee("1", "Jack", List.of("Java", "React", "R", "Flutter")),
        new Employee("2", "Jill", List.of("Java", "C")),
        new Employee("3", "Mark", List.of("Cobol")),
        new Employee("4", "Michael", List.of("React", "R", "Flutter")),
        new Employee("5", "Jill", List.of("Node", "Cobol")));

    Map<String, List<String>> mapById = employees.stream()
        .collect(Collectors.toMap(Employee::getEmpId, e -> List.of(e.getEmpId())));
    Map<String, List<String>> mapByName = employees.stream()
        .collect(Collectors.groupingBy(Employee::getName,
            Collectors.mapping(Employee::getEmpId, Collectors.toList())));
    Map<String, List<String>> mapByLanguage = employees.stream()
        .flatMap(e -> e.getLanguages().stream()
            .map(l -> Map.entry(l, e.getEmpId())))
        .collect(Collectors.groupingBy(Entry::getKey,
            Collectors.mapping(Entry::getValue, Collectors.toList())));

    System.out.println(mapById);
    System.out.println(mapByName);
    System.out.println(mapByLanguage);
}
 

выход:

 {1=[1], 2=[2], 3=[3], 4=[4], 5=[5]}
{Michael=[4], Mark=[3], Jill=[2, 5], Jack=[1]}
{Java=[1, 2], R=[1, 4], C=[2], Node=[5], Cobol=[3, 5], Flutter=[1, 4], React=[1, 4]}
 

Для java8

     Map<String, List<String>> mapByLanguage = employees.stream()
        .flatMap(e -> e.getLanguages().stream()
            .map(l -> new AbstractMap.SimpleEntry<>(l, e.getEmpId())))
        .collect(Collectors.groupingBy(Entry::getKey,
            Collectors.mapping(Entry::getValue, Collectors.toList())));
 

Или

     Map<String, List<String>> mapByLanguage = employees.stream()
        .flatMap(e -> e.getLanguages().stream()
            .map(l -> new String[] {l, e.getEmpId()}))
        .collect(Collectors.groupingBy(e -> e[0],
            Collectors.mapping(e -> e[1], Collectors.toList())));
 

Комментарии:

1. Спасибо,я попробовал ваш пример для «mapByLanguage», и код отказывается компилироваться ( Java 8 ) — /**Несоответствие типов: не удается преобразовать объект в карту<Строка, Список<Строка><Строка>> — Безопасность типов: Метод collect(Коллектор) принадлежит потоку необработанных типов. Ссылки на поток универсального типа<T> должны быть параметризованы Картой типов.Запись не определяет getKey(Объект), который применим здесь**/

2. @akila Добавлено для Java8

3. спасибо, но, к сожалению, все еще возникают ошибки компиляции

4. @akila Добавил еще одно решение.