将员工列表转换为多个地图,按每个员工属性对它们进行分组

Convert a List of employees into Multiple Maps grouping them by each of the employee attributes

我有一份员工名单:

这是一个例子:

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

Employee 对象是一个简单的 POJO:

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

我需要将这个 Employee List 转换成多个 Maps :

(对于员工的每个属性)

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")}];

同意按员工 ID 分组没有意义,因为永远不会有两个具有相同 ID 的员工,因此我们可以忽略基于员工 ID 的部分,但我需要对员工的更多属性进行分组(姓氏、城市等)

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

请注意,在上面的 Jill 中出现了两次,因此在数组列表中我们有 Jill 的员工 ID

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 我尝试了下面的代码 - 下面的方法根据员工姓名对数据进行分组,但我是否必须编写单独的方法来按 id 、按语言分组,然后按其他属性(如 byAddress 、 byCity 等

所有这些都可以在将列表转换为单独的地图的单一方法中完成吗?

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 我还没有将每个员工中包含的语言列表转换为上述解决方案 我对如何去做感到难过..... 我不想遍历每个员工的每种语言

试试这个。

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

为简洁起见,让我们使用 Java 16+ 中的 record 来保存我们的示例员工数据。当以透明和不可变的方式传递数据为主要目的时,记录是合适的。编译器隐式创建默认构造函数、getter、equals & hashCodetoString.

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

对于早于 Java 16 的情况,我们也可以使用传统的 class。

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 &&
                Objects.equals( this.name , that.name ) &&
                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 <>();

为我们的每个员工对象分配一个集合。这被称为 multimap, when a key maps to a collection of values rather than a single value. The Map#computeIfAbsent 方法,通过在我们放置新键之前为每个新条目创建我们的嵌套集合,可以很容易地制作 multipmap。

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

最后,我认为通常最好 return 在可行的情况下使用不可修改的集合。因此,让我们分两步使我们的 multipmap 不可修改。第一步:用 Java 10+.

中的 unmodifiable set by calling Set.copyOf 替换每个嵌套的员工对象集
nameInCommonMap.replaceAll( ( k , v ) -> Set.copyOf( v ) ); // Make nested set unmodifiable.

第二步:制作unmodifiable map by calling Map.copyOf.

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

我们完成了。

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

共同语言

将语言映射到嵌套列表中包含该语言的员工的员工 ID 集的逻辑几乎相同。唯一的问题是,在我们的 Employee 对象循环中,我们必须添加语言的嵌套循环。

整个示例class:

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 ......所有这些都可以在将列表转换为单独的地图的单一方法中完成吗?

也许。

但我看不出有什么好处。我在这里展示的代码只有四行:在一行中建立一个地图,然后用 for 循环的 3 行来填充该地图。

无论您使用流还是循环,完成的工作量都是一样的。

问题 #3 我还没有转换包含在每个员工中的语言列表……我不想遍历每个员工的每种语言

无法绕过循环。

要么循环嵌套语言列表,要么调用诸如流之类的实用程序来循环嵌套语言列表。

在计算中没有神奇的“提取数据”操作可以避免接触每个被检查的数据元素。

您可以使用 反射 动态迭代声明的字段并使用 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]}}