Java 8中如何按多个字段名称进行分组

138

我找到了一个可以将POJO对象按字段名分组的代码。以下是该代码:

public class Temp {

    static class Person {

        private String name;
        private int age;
        private long salary;

        Person(String name, int age, long salary) {

            this.name = name;
            this.age = age;
            this.salary = salary;
        }

        @Override
        public String toString() {
            return String.format("Person{name='%s', age=%d, salary=%d}", name, age, salary);
        }
    }

    public static void main(String[] args) {
        Stream<Person> people = Stream.of(new Person("Paul", 24, 20000),
                new Person("Mark", 30, 30000),
                new Person("Will", 28, 28000),
                new Person("William", 28, 28000));
        Map<Integer, List<Person>> peopleByAge;
        peopleByAge = people
                .collect(Collectors.groupingBy(p -> p.age, Collectors.mapping((Person p) -> p, toList())));
        System.out.println(peopleByAge);
    }
}

输出结果为(正确):

{24=[Person{name='Paul', age=24, salary=20000}], 28=[Person{name='Will', age=28, salary=28000}, Person{name='William', age=28, salary=28000}], 30=[Person{name='Mark', age=30, salary=30000}]}

但是如果我想按多个字段分组怎么办?我可以明显地在groupingBy()方法中传递一些POJO,然后在该POJO中实现equals()方法,但是否有其他选项,例如我可以按照给定POJO中的多个字段进行分组吗?

例如,在我的情况下,我想按姓名和年龄分组。


3
一个技巧是从所有字段生成一个唯一的字符串。 - Marko Topolnik
4
顺便提一下,在你发布的代码中,将mapping作为下游收集器是多余的。 - Marko Topolnik
10
快速且简单的解决方案是 people.collect(groupingBy(p -> Arrays.asList(p.name, p.age))) - Misha
9个回答

242

你有几个选择。最简单的方法是将收集器链接在一起:

Map<String, Map<Integer, List<Person>>> map = people
    .collect(Collectors.groupingBy(Person::getName,
        Collectors.groupingBy(Person::getAge));

要获取一个名为Fred的18岁人的列表,您可以使用以下方法:

map.get("Fred").get(18);

第二个选项是定义一个代表分组的类。它可以在Person类内部定义。这段代码使用了一个“record”,但是在JEP 359之前的Java版本中,它也可以很容易地定义为一个带有“equals”和“hashCode”的类:
class Person {
    record NameAge(String name, int age) { }

    public NameAge getNameAge() {
        return new NameAge(name, age);
    }
}

接着您可以使用:

Map<NameAge, List<Person>> map = people.collect(Collectors.groupingBy(Person::getNameAge));

并且使用搜索功能

map.get(new NameAge("Fred", 18));

如果您不想实现自己的组记录,则许多Java框架都有专门设计用于此类事情的pair类。例如:Apache Commons Pair。如果您使用其中一个库,则可以将地图的键设置为名称和年龄的一对。

Map<Pair<String, Integer>, List<Person>> map =
    people.collect(Collectors.groupingBy(p -> Pair.of(p.getName(), p.getAge())));

并使用以下代码检索:

map.get(Pair.of("Fred", 18));

就我个人而言,我认为通用元组现在没有太多的价值,因为语言中已经有了记录类型。 记录能更好地表达意图,并且需要很少的代码。


5
Function<T,U> 从这个意义上也隐藏了意图---但你不会看到任何人为每个映射步骤声明自己的函数式接口;意图已经在 lambda 表达式中存在。元组也是如此:它们非常适合作为 API 组件之间的粘合类型。顺便说一句,Scala 的 case classes 在简洁性和意图清晰度方面都是一个重大胜利。 - Marko Topolnik
1
是的,我明白你的观点。我猜(像往常一样),这取决于它们如何被使用。我上面给出的例子——将Pair用作Map的键——就是一个不好的例子。我对Scala不太熟悉——听说它很不错,我得开始学习一下。 - sprinter
1
想象一下,您可以将NameAge声明为一行代码:case class NameAge { val name: String; val age: Int }——然后您就可以获得equalshashCodetoString - Marko Topolnik
1
不错 - 又有一项任务被推到了我的“必须完成”队列上。不幸的是,这是先进先出的! - sprinter
@sprinter 第一个代码片段中的类型不正确,应更改为 Map<String,Map<Integer,List<Person>>> map - kasur
显示剩余5条评论

56

看这段代码:

你可以简单地创建一个函数,让它为你完成工作,有点像函数式编程风格!

Function<Person, List<Object>> compositeKey = personRecord ->
    Arrays.<Object>asList(personRecord.getName(), personRecord.getAge());

现在,您可以将其用作地图:

Map<Object, List<Person>> map =
people.collect(Collectors.groupingBy(compositeKey, Collectors.toList()));

干杯!


3
我使用了这个解决方案,但是有所不同。Function<Person, String> compositeKey = personRecord -> StringUtils.join(personRecord.getName(), personRecord.getAge()); - bpedroso
@bpedroso 这可能很危险,因为如果有人名字叫“Martin6”,年龄是6岁,他会和年龄为66岁的“Martin”被分到同一组。 - Gandalf
@bpedroso,为什么有人会允许在名字中使用数字,这本身就是一个数据完整性问题。 - undefined

15

你可以简单地将你的groupingByKey连接起来,如下所示:

Map<String, List<Person>> peopleBySomeKey = people
                .collect(Collectors.groupingBy(p -> getGroupingByKey(p), Collectors.mapping((Person p) -> p, toList())));



//write getGroupingByKey() function
private String getGroupingByKey(Person p){
return p.getAge()+"-"+p.getName();
}

12

你可以将List用作许多领域的分类器,但需要将空值包装成Optional:

Function<Item, List> classifier = (item) -> List.of(
    item.getFieldA(),
    item.getFieldB(),
    Optional.ofNullable(item.getFieldC())
);

Map<List, List<Item>> grouped = items.stream()
    .collect(Collectors.groupingBy(classifier));

1
我喜欢这个!但我认为它应该是Function<Item, List>而不是Function<String, List>。 - Justin Rowe

9

groupingBy方法的第一个参数是Function<T,K>,其中:

@param <T> 输入元素的类型

@param <K> 键的类型

如果我们用匿名类替换代码中的lambda表达式,就会看到类似于以下内容:

people.stream().collect(Collectors.groupingBy(new Function<Person, int>() {
            @Override
            public int apply(Person person) {
                return person.getAge();
            }
        }));

刚才更改了输出参数<K>。在这种情况下,例如,我使用了来自org.apache.commons.lang3.tuple的pair类按名称和年龄分组,但您可以根据需要创建自己的类来过滤组。

people.stream().collect(Collectors.groupingBy(new Function<Person, Pair<Integer, String>>() {
                @Override
                public YourFilter apply(Person person) {
                    return Pair.of(person.getAge(), person.getName());
                }
            }));

最终,使用lambda表达式替换后,代码如下所示:
Map<Pair<Integer,String>, List<Person>> peopleByAgeAndName = people.collect(Collectors.groupingBy(p -> Pair.of(person.getAge(), person.getName()), Collectors.mapping((Person p) -> p, toList())));

使用List<String>怎么样? - Alex78191

2

为您的组定义一个关键字定义类。

class KeyObj {

    ArrayList<Object> keys;

    public KeyObj( Object... objs ) {
        keys = new ArrayList<Object>();

        for (int i = 0; i < objs.length; i++) {
            keys.add( objs[i] );
        }
    }

    // Add appropriate isEqual() ... you IDE should generate this

}

现在在你的代码中,
peopleByManyParams = people
            .collect(Collectors.groupingBy(p -> new KeyObj( p.age, p.other1, p.other2 ), Collectors.mapping((Person p) -> p, toList())));

4
这只是重新发明 Arrays.asList() —— 顺便说一下,这对于原帖作者的情况是一个很好的选择。 - Marko Topolnik
还有一个类似于其他示例中提到的“Pair”示例,但没有参数限制。 - Benny Bottema
还有,你需要将其设为不可变对象(并且只需计算一次 hashCode)。 - Rob Audenaerde

2

我需要为一个提供午餐服务给不同客户的餐饮公司制作报告。换句话说,餐饮公司可能有一个或多个接受其订单的公司,它必须知道每天为所有客户生产多少午餐!

请注意,为了不过于复杂化此示例,我没有使用排序。

这是我的代码:

@Test
public void test_2() throws Exception {
    Firm catering = DS.firm().get(1);
    LocalDateTime ldtFrom = LocalDateTime.of(2017, Month.JANUARY, 1, 0, 0);
    LocalDateTime ldtTo = LocalDateTime.of(2017, Month.MAY, 2, 0, 0);
    Date dFrom = Date.from(ldtFrom.atZone(ZoneId.systemDefault()).toInstant());
    Date dTo = Date.from(ldtTo.atZone(ZoneId.systemDefault()).toInstant());

    List<PersonOrders> LON = DS.firm().getAllOrders(catering, dFrom, dTo, false);
    Map<Object, Long> M = LON.stream().collect(
            Collectors.groupingBy(p
                    -> Arrays.asList(p.getDatum(), p.getPerson().getIdfirm(), p.getIdProduct()),
                    Collectors.counting()));

    for (Map.Entry<Object, Long> e : M.entrySet()) {
        Object key = e.getKey();
        Long value = e.getValue();
        System.err.println(String.format("Client firm :%s, total: %d", key, value));
    }
}

0

是时候尝试新的Java特性了

自JDK 14引入record类以来,它完美地适用于这种情况。
其好处包括:

  1. hashcodeequals会自动生成。
  2. 更易读且更健壮(与使用List/连接字段作为键相比)

我们只需要稍微修改代码即可支持多个字段分组,如下所示:

public class Temp {

    static class Person {

        private String name;
        private int age;
        private long salary;

        Person(String name, int age, long salary) {

            this.name = name;
            this.age = age;
            this.salary = salary;
        }

        @Override
        public String toString() {
            return String.format("Person{name='%s', age=%d, salary=%d}", name, age, salary);
        }
    }

    public static void main(String[] args) {
        Stream<Person> people = Stream.of(new Person("Paul", 24, 20000),
                new Person("Mark", 30, 30000),
                new Person("Will", 28, 28000),
                new Person("William", 28, 28000));
        record AgeAndSalary(int age, long salary) {
        }
        Map<AgeAndSalary, List<Person>> peopleByAgeAndSalary;
        peopleByAgeAndSalary = people
                .collect(Collectors.groupingBy(p -> new AgeAndSalary(p.age, p.salary), Collectors.mapping((Person p) -> p, toList())));
        System.out.println(peopleByAgeAndSalary);
    }
}

0

这是我如何按多个字段branchCode和prdId进行分组的方法,只是为了帮助需要的人而发布

    import java.math.BigDecimal;
    import java.math.BigInteger;
    import java.util.ArrayList;
    import java.util.Iterator;
    import java.util.LinkedList;
    import java.util.List;
    import java.util.Map;
    import java.util.stream.Collectors;

    /**
     *
     * @author charudatta.joshi
     */
    public class Product1 {

        public BigInteger branchCode;
        public BigInteger prdId;
        public String accountCode;
        public BigDecimal actualBalance;
        public BigDecimal sumActBal;
        public BigInteger countOfAccts;

        public Product1() {
        }

        public Product1(BigInteger branchCode, BigInteger prdId, String accountCode, BigDecimal actualBalance) {
            this.branchCode = branchCode;
            this.prdId = prdId;
            this.accountCode = accountCode;
            this.actualBalance = actualBalance;
        }

        public BigInteger getCountOfAccts() {
            return countOfAccts;
        }

        public void setCountOfAccts(BigInteger countOfAccts) {
            this.countOfAccts = countOfAccts;
        }

        public BigDecimal getSumActBal() {
            return sumActBal;
        }

        public void setSumActBal(BigDecimal sumActBal) {
            this.sumActBal = sumActBal;
        }

        public BigInteger getBranchCode() {
            return branchCode;
        }

        public void setBranchCode(BigInteger branchCode) {
            this.branchCode = branchCode;
        }

        public BigInteger getPrdId() {
            return prdId;
        }

        public void setPrdId(BigInteger prdId) {
            this.prdId = prdId;
        }

        public String getAccountCode() {
            return accountCode;
        }

        public void setAccountCode(String accountCode) {
            this.accountCode = accountCode;
        }

        public BigDecimal getActualBalance() {
            return actualBalance;
        }

        public void setActualBalance(BigDecimal actualBalance) {
            this.actualBalance = actualBalance;
        }

        @Override
        public String toString() {
            return "Product{" + "branchCode:" + branchCode + ", prdId:" + prdId + ", accountCode:" + accountCode + ", actualBalance:" + actualBalance + ", sumActBal:" + sumActBal + ", countOfAccts:" + countOfAccts + '}';
        }

        public static void main(String[] args) {
            List<Product1> al = new ArrayList<Product1>();
            System.out.println(al);
            al.add(new Product1(new BigInteger("01"), new BigInteger("11"), "001", new BigDecimal("10")));
            al.add(new Product1(new BigInteger("01"), new BigInteger("11"), "002", new BigDecimal("10")));
            al.add(new Product1(new BigInteger("01"), new BigInteger("12"), "003", new BigDecimal("10")));
            al.add(new Product1(new BigInteger("01"), new BigInteger("12"), "004", new BigDecimal("10")));
            al.add(new Product1(new BigInteger("01"), new BigInteger("12"), "005", new BigDecimal("10")));
            al.add(new Product1(new BigInteger("01"), new BigInteger("13"), "006", new BigDecimal("10")));
            al.add(new Product1(new BigInteger("02"), new BigInteger("11"), "007", new BigDecimal("10")));
            al.add(new Product1(new BigInteger("02"), new BigInteger("11"), "008", new BigDecimal("10")));
            al.add(new Product1(new BigInteger("02"), new BigInteger("12"), "009", new BigDecimal("10")));
            al.add(new Product1(new BigInteger("02"), new BigInteger("12"), "010", new BigDecimal("10")));
            al.add(new Product1(new BigInteger("02"), new BigInteger("12"), "011", new BigDecimal("10")));
            al.add(new Product1(new BigInteger("02"), new BigInteger("13"), "012", new BigDecimal("10")));
            //Map<BigInteger, Long> counting = al.stream().collect(Collectors.groupingBy(Product1::getBranchCode, Collectors.counting()));
            // System.out.println(counting);

            //group by branch code
            Map<BigInteger, List<Product1>> groupByBrCd = al.stream().collect(Collectors.groupingBy(Product1::getBranchCode, Collectors.toList()));
            System.out.println("\n\n\n" + groupByBrCd);

             Map<BigInteger, List<Product1>> groupByPrId = null;
              // Create a final List to show for output containing one element of each group
            List<Product> finalOutputList = new LinkedList<Product>();
            Product newPrd = null;
            // Iterate over resultant  Map Of List
            Iterator<BigInteger> brItr = groupByBrCd.keySet().iterator();
            Iterator<BigInteger> prdidItr = null;    



            BigInteger brCode = null;
            BigInteger prdId = null;

            Map<BigInteger, List<Product>> tempMap = null;
            List<Product1> accListPerBr = null;
            List<Product1> accListPerBrPerPrd = null;

            Product1 tempPrd = null;
            Double sum = null;
            while (brItr.hasNext()) {
                brCode = brItr.next();
                //get  list per branch
                accListPerBr = groupByBrCd.get(brCode);

                // group by br wise product wise
                groupByPrId=accListPerBr.stream().collect(Collectors.groupingBy(Product1::getPrdId, Collectors.toList()));

                System.out.println("====================");
                System.out.println(groupByPrId);

                prdidItr = groupByPrId.keySet().iterator();
                while(prdidItr.hasNext()){
                    prdId=prdidItr.next();
                    // get list per brcode+product code
                    accListPerBrPerPrd=groupByPrId.get(prdId);
                    newPrd = new Product();
                     // Extract zeroth element to put in Output List to represent this group
                    tempPrd = accListPerBrPerPrd.get(0);
                    newPrd.setBranchCode(tempPrd.getBranchCode());
                    newPrd.setPrdId(tempPrd.getPrdId());

                    //Set accCOunt by using size of list of our group
                    newPrd.setCountOfAccts(BigInteger.valueOf(accListPerBrPerPrd.size()));
                    //Sum actual balance of our  of list of our group 
                    sum = accListPerBrPerPrd.stream().filter(o -> o.getActualBalance() != null).mapToDouble(o -> o.getActualBalance().doubleValue()).sum();
                    newPrd.setSumActBal(BigDecimal.valueOf(sum));
                    // Add product element in final output list

                    finalOutputList.add(newPrd);

                }

            }

            System.out.println("+++++++++++++++++++++++");
            System.out.println(finalOutputList);

        }
    }

输出如下:

+++++++++++++++++++++++
[Product{branchCode:1, prdId:11, accountCode:null, actualBalance:null, sumActBal:20.0, countOfAccts:2}, Product{branchCode:1, prdId:12, accountCode:null, actualBalance:null, sumActBal:30.0, countOfAccts:3}, Product{branchCode:1, prdId:13, accountCode:null, actualBalance:null, sumActBal:10.0, countOfAccts:1}, Product{branchCode:2, prdId:11, accountCode:null, actualBalance:null, sumActBal:20.0, countOfAccts:2}, Product{branchCode:2, prdId:12, accountCode:null, actualBalance:null, sumActBal:30.0, countOfAccts:3}, Product{branchCode:2, prdId:13, accountCode:null, actualBalance:null, sumActBal:10.0, countOfAccts:1}]

格式化后:

[
Product{branchCode:1, prdId:11, accountCode:null, actualBalance:null, sumActBal:20.0, countOfAccts:2}, 
Product{branchCode:1, prdId:12, accountCode:null, actualBalance:null, sumActBal:30.0, countOfAccts:3}, 
Product{branchCode:1, prdId:13, accountCode:null, actualBalance:null, sumActBal:10.0, countOfAccts:1}, 
Product{branchCode:2, prdId:11, accountCode:null, actualBalance:null, sumActBal:20.0, countOfAccts:2}, 
Product{branchCode:2, prdId:12, accountCode:null, actualBalance:null, sumActBal:30.0, countOfAccts:3}, 
Product{branchCode:2, prdId:13, accountCode:null, actualBalance:null, sumActBal:10.0, countOfAccts:1}
]

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接