【问题标题】:General way to group objects by several properties in Java在 Java 中按多个属性对对象进行分组的一般方法
【发布时间】:2016-07-20 00:20:45
【问题描述】:

我有一组物品:

class Item {        
    String type;
    boolean flag;
    int size;
    ...
}

有几种可能的类型(比如“a”、“b”和“c”),因此有几种可能的类型标志值组合(["a" ; false], ["a" ; true"], ["b" ; false], ...)。我需要折叠具有相同值组合的项目,所以我有 collapse 带有此签名的方法

Item collapse(Collection<Item> items)

我需要将输入项列表分成具有相同typeflag 值的组

List<Collection<Item>> getGroups(Collection<Item> items) // method I need

这样我就可以折叠每个组

List<Item> r = getGroups(items).stream().map(Item::collapse).collect(toList());

所以我可以创建地图地图或制作一些复合键,但它需要一些我想避免的样板代码。将来我可以有更多的属性进行分组,所以解决方案不应该在这两个属性上硬编码,而是可以很容易地扩展到新的属性。

如何才能很好地做到这一点?该问题是否有众所周知的解决方案?

【问题讨论】:

    标签: java algorithm list collections grouping


    【解决方案1】:

    为了知道哪个Item 逻辑上等于其他Item 用于折叠,我将把责任放在Item 类本身上。您可以覆盖 equals 方法,但如果您要将它们放在 Set 某处,这可能会导致不良结果,因此最好使用单独的方法进行检查。

    另一种选择是获取将用于此检查的那些字段并将它们转换为Item 的内部类。然后,equalshashCode 可以仅被内部类覆盖,其实例用作 Map 的键。

    但是,这些都不会自动包含您以后添加的任何新字段。因此,由维护类的人来确保将需要包含在检查(或 equals/hashCode)中的任何内容添加到方法中。

    我真正想到的接近这一点的唯一方法是使用反射。如果必须考虑的任何事情只放在内部类中,那将起作用。如果您必须将其直接保留在 Item 类上,那么定义注释(具有运行时保留)可能会很有用。进行检查的代码(或 equals/hashCode,如果使用)可以反映类并使用每个带注释的字段。

    注释可能如下所示:

    import java.lang.annotation.ElementType;
    import java.lang.annotation.Retention;
    import java.lang.annotation.RetentionPolicy;
    import java.lang.annotation.Target;
    
    
    @Retention(RetentionPolicy.RUNTIME)
    @Target(ElementType.FIELD)
    public @interface CollapseField {
    
    }
    

    然后这样使用:

    class Item {      
        @CollapseField  
        String type;
        @CollapseField
        boolean flag;
        int size;
        ...
    }
    

    然后使用它的代码需要检查哪些字段被注释并获取它们的值(这两个操作都使用反射)并检查与其他对象的相等性以确定哪些属于一起。由于这可能会对性能产生相当大的影响,因此对哈希码之类的内容使用缓存将是一个好主意。

    最后,我不确定是否值得对使用的值进行硬编码,除非您要在大量类中使用它,否则字段的数量可能会变得非常大。

    最后,这对 Java 来说可能看起来很奇怪,但也许使用属性模式而不是字段可能是有意义的。尽管您会失去一些类型安全性。 Steve Yegge 发表了一篇很长但很有趣的帖子:http://steve-yegge.blogspot.be/2008/10/universal-design-pattern.html

    这几乎是我能想到的全部。据我所知,没有标准的方法。也许有人知道一些提供解决方案的便捷库。

    编辑:这是一个示例,其中要用于键的字段被制成一个内部类,它实现了equalshashCode,因此它可以用作Map 的键:

    import java.util.Objects;
    
    public class Item {
    
        int size;
        final Key key;
    
        public class Key {
    
            String type;
            boolean flag;
    
            public Key(String type, boolean flag) {
                this.type = type;
                this.flag = flag;
            }
    
            public String getType() {
                return type;
            }
    
            public void setType(String type) {
                this.type = type;
            }
    
            public boolean isFlag() {
                return flag;
            }
    
            public void setFlag(boolean flag) {
                this.flag = flag;
            }
    
            @Override
            public int hashCode() {
                int hash = 5;
                hash = 89 * hash + Objects.hashCode(this.type);
                hash = 89 * hash + (this.flag ? 1 : 0);
                return hash;
            }
    
            @Override
            public boolean equals(Object obj) {
                if (obj == null) {
                    return false;
                }
                if (getClass() != obj.getClass()) {
                    return false;
                }
                final Key other = (Key) obj;
                if (!Objects.equals(this.type, other.type)) {
                    return false;
                }
                if (this.flag != other.flag) {
                    return false;
                }
                return true;
            }
    
        }
    
        public Item(String type, boolean flag, int size) {
            key = new Key(type, flag);
            this.size = size;
        }
    
        public String getType() {
            return key.type;
        }
    
        public void setType(String type) {
            this.key.type = type;
        }
    
        public boolean isFlag() {
            return key.flag;
        }
    
        public void setFlag(boolean flag) {
            this.key.flag = flag;
        }
    
        public int getSize() {
            return size;
        }
    
        public void setSize(int size) {
            this.size = size;
        }
    
        public Key getKey() {
            return key;
        }
    
    }
    

    Item 级别的 getter 和 setter 将一些字段委托给 Key。请注意,如果您只通过Item,则可能不需要Key 中的getter 和setter,因为包含类可以直接访问这些字段。如果您需要添加必须是键的一部分的字段,请将其添加到Key。如果一定不能用于识别,直接添加到ItemequalshashCode 如果您必须更新它们,任何体面的 IDE 都可以轻松地自动生成它们。

    请注意,如果您在某些进行反射或自省的框架中使用该类,则此解决方案可能会中断。根据处理方式,Key 中的字段最终可能会被视为Item 的属性(由于 getter/setter)。 JPA 或 EJB 容器之类的东西直接通过反射接近字段可能无法使用。

    【讨论】:

    • 嗯,这看起来过度设计,尤其是反射部分
    • 那是你的 Java,我的朋友。在没有更强大的语言和元编程工具的情况下,奇怪的过度设计模式开始出现。这就是为什么有如此多的工厂和抽象工厂,谁知道整个 Java API 和框架中还有什么。
    • 哦,在我忘记之前,这绝对是造成如此大量样板文件的主要原因。也许研究一下龙目岛计划,让事情变得不那么痛苦。我不确定它是否会帮助您解决这种情况,但它可能会(使用一个注释轻松生成 equals/hashCode)。
    • 我添加了我当前的版本作为起点,它看起来更简单,虽然我不喜欢将所有内容都转换为字符串
    • @AdamSkywalker 我编辑了我的答案以添加使用内部类的示例。它避免了必须使用字符串连接来创建 Map 键。
    【解决方案2】:

    你可以使用Collectors.groupingBy:

    public static void main(String[] args) {
        List<Item> list = new ArrayList<>();
        list.add(new Item(1, true, 1));
        list.add(new Item(1, true, 2));
    
        list.add(new Item(1, false, 3));
        list.add(new Item(1, false, 4));
    
        list.add(new Item(2, true, 5));
        list.add(new Item(2, false, 6));
    
        Collection<List<Item>> result = list.stream()
                .collect(Collectors.groupingBy(x -> Arrays.<Object>asList(x.keyA, x.keyB)))
                .values();
    
        for (List<Item> items : result) {
            System.out.println(items);
        }
    }
    
    static class Item {
        Integer keyA;
        Boolean keyB;
        Integer value;
    
        public Item(Integer keyA, Boolean keyB, Integer value) {
            this.keyA = keyA;
            this.keyB = keyB;
            this.value = value;
        }
    
        @Override
        public String toString() {
            return "Item{" +
                    "keyA=" + keyA +
                    ", keyB=" + keyB +
                    ", value=" + value +
                    '}';
        }
    }
    

    【讨论】:

    • 整洁!看来我必须深入研究 Java SE 8 API 才能更新。
    【解决方案3】:

    将我目前使用复合键的方法放在这里,但仍在等待新的想法

    Collection<Collection<Item>> getGroups(Collection<Item> items) {
        Map<String, Collection<Item>> itemsByKey = new HashMap<>();
        for (Item item : items) {
            String key = item.key();
            Collection<Item> byKey = itemsByKey.get(key);
            if (byKey == null) {
                itemsByKey.put(key, (byKey = new ArrayList<>()));
            }
            byKey.add(item);
        }
        return itemsByKey.values();
    }
    
    class Item {
        public String key() { type + "-" + flag }
    

    可以轻松添加新字段,但字符串键看起来不太好。

    【讨论】:

    • 不过,这并不能真正解决必须对使用的字段进行硬编码的问题。除此之外,我认为使用字符串作为键没有问题。除非您的 type 可以包含使用的分隔符,否则它应该可以正常工作,字符串的相等性和 hashCode 检查工作良好,并且在 Maps 中非常频繁地完成。也许尝试通过构造一次键值然后缓存它来优化一点。由于密钥可能会被频繁使用,因此每次调用 key() 时都构造它是一种浪费。我相信 String 实现可能也缓存了它的哈希码。
    猜你喜欢
    • 2016-02-11
    • 2018-08-28
    • 1970-01-01
    • 2015-12-31
    • 1970-01-01
    • 1970-01-01
    • 2018-11-16
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多