在Java 8中,基于多个属性从对象列表中删除重复项

3

我想比较getCode和getMode,并找到重复的记录。

然后还有一个产品属性getVode,在两个记录中始终具有不同的值(true或false)。

P1   getCode  getMode  getVode
1    001      123      true
P2   getCode  getMode  getVode
2    001      123      false

我尝试过以下代码,但它仅能找到重复项:

List<ProductModel> uniqueProducts = productsList.stream()
    .collect(Collectors.collectingAndThen(
        toCollection(() -> new TreeSet<>(
            Comparator.comparing(ProductModel::getCode)
                .thenComparing(ProductModel::getMode)
        )),
        ArrayList::new));

当我发现重复记录时,我想检查getVode值是否为false,并将其从列表中删除。任何帮助将不胜感激?


如果有三个条目分别为 001/123/false001/123/true001/123/false,应该删除其中 false 存在的条目吗? - Eugene
@Eugene,我们需要删除所有重复的且对于getVode为false的内容。 - ASMA2412
还有...在vode中是否可能存在多个true的实例? - Eugene
4个回答

4
据我理解,您希望仅在元素是重复项并且它们的getVode方法返回false时才删除它们。
我们可以直接这样做。首先,我们需要确定哪些元素是重复的:
Map<Object, Boolean> isDuplicate = productsList.stream()
    .collect(Collectors.toMap(pm -> Arrays.asList(pm.getCode(), pm.getMode()),
                              pm -> false, (a, b) -> true));

然后,删除满足条件的那些内容:

productsList.removeIf(pm -> !pm.getVode()
                         && isDuplicate.get(Arrays.asList(pm.getCode(), pm.getMode())));

或者,不修改旧列表:

List<ProductModel> uniqueProducts = new ArrayList<>(productsList);
uniqueProducts.removeIf(pm -> !pm.getVode()
                           && isDuplicate.get(Arrays.asList(pm.getCode(), pm.getMode())));

这也可以通过流操作完成:

List<ProductModel> uniqueProducts = productsList.stream()
    .filter(pm -> pm.getVode()
              || !isDuplicate.get(Arrays.asList(pm.getCode(), pm.getMode())))
    .collect(Collectors.toList());

1
我非常喜欢使用merge()技巧来检测重复。 - davidxxx
@Holger非常好的答案,但最后一个问题是,如果getVode()的值不是布尔类型,而是带有字符串类型呢?这种情况可能存在吗?因为removeif只适用于布尔类型,对吗? - ASMA2412
1
问问自己,何时应该移除元素。然后,将条件写成Java代码,例如pm.getVode().equals(…)pm.getVode().matches(…)等。 - Holger
这个在Java 7中能做到吗?还是只有在Java 8中才行? - User2413
1
@User2413,这并没有什么神奇的地方,所以你可以编写Java 7代码来完成相同的任务。但它不会像现在这样紧凑。 - Holger

3

根据传递给 TreeSetComparator,您可以删除重复项,无论 getVode() 值如何,因为它在比较中不予考虑。
使用您的方法并不容易。
您可以创建一个 Map<ProductModelId, List<ProductModelId>>,根据它们的 getCode()getMode() 值将元素分组,并使用 ProductModelId 类表示它们。 然后对于 Map 的每个条目进行处理:如果列表包含单个元素,则保留它,否则不保留所有具有 getVode() 为 false 的元素。

Map<ProductModelId, List<ProductModel>> map = 
productsList.stream()
            .collect(groupingBy(p -> new ProductModelId(p.getCode(), p.getMode());

List<ProductModel> listFiltered =
        map.values()
           .stream()
           .flatMap(l -> {
                        if (l.size() == 1) {
                          return Stream.of(l.get(0));
                        } else {
                          return l.stream().filter(ProductModel::getVode);
                        }
                    }
           )
           .collect(toList());

请注意,ProductModelId 应通过考虑两个字段的值来覆盖 equals/hashCode,以便在映射中正确地分组它们。
public class ProductModelId {

    private String code;
    private String mode;

    public ProductModelId(String code, String mode) {
        this.code = code;
        this.mode = mode;
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (!(o instanceof ProductModelId)) return false;
        ProductModelId that = (ProductModelId) o;
        return Objects.equals(code, that.code) &&
                Objects.equals(mode, that.mode);
    }

    @Override
    public int hashCode() {
        return Objects.hash(code, mode);
    }
}

1
使用map.values()而不是map.entrySet()可以避免在不关心键时调用条目的getValue()。此外,将if(condition) return a; else return b;替换为return condition? a: b;允许您将lambda从语句语法更改为表达式语法:.flatMap(v -> v.size() == 1? Stream.of(v.get(0)): v.stream().filter(p -> !p.getVode())) - Holger
同意第一个观点。对于第二个观点,我知道但通常避免使用三目运算符,因为它与不平凡的语句相关。 - davidxxx
1
没问题,但问题是什么是非平凡的语句。也许,14个令牌的噪声(您只需要两个)使其看起来不平凡。顺便说一下,您可以使用.flatMap(l -> l.stream().filter(p -> l.size() == 1 || p.getVode()))而无需使用三元运算符,但我更喜欢三元运算符。顺便说一下,要删除假值,但“filter”谓词指定要保留哪些,因此应为p.getVode()而不带 - Holger
.flatMap(l -> l.stream().filter(p -> l.size() == 1 || p.getVode())) 是一种可能性,它是正确的,但它关联了比较两个不同的事物:列表和元素:我认为它并不一定容易阅读。对于三元运算符,这可能是一个习惯问题。 - davidxxx

0
你可以通过代码和模式的组合进行分组。然后在 mergeFunction 中获取具有 true 代码的元素:
 Collection<ProductModel> uniqueProducts  = products.stream()
        .collect(toMap(
                    p -> Arrays.asList(p.getCode(), p.getMode()),
                    Function.identity(),
                    (p1, p2) -> p1.getVode() ? p1 : p2))
        .values();

请查看toMap的javaDoc文档


1
p -> p.getCode() + p.getMode() 这种写法很糟糕,我认为应该使用 Arrays.asList;但是合并器本身就有问题,如果 p1.getVodefalse,但是 p2.getVode 也是 false 呢? - Eugene
@Eugene 根据 OP 的说法,还有一个产品属性 getVode,它的值总是不同的。并且同意使用 Arrays.asList.. 谢谢。 - Ruslan
正是因为这个原因,我在问题下方询问了一个评论。 - Eugene

0
如果你的 vode 可以对多个 ProductModel 实例返回 true(否则,如果你只期望一个单一的 true - 这会更简单,我会让你自己练习),并且你想要保留它们所有,也许这就是你想要的:
    List<ProductModel> models = List.of(
        new ProductModel(1, 123, false),
        new ProductModel(1, 123, true)); // just an example

    Map<List<Integer>, List<ProductModel>> map = new HashMap<>();

    models.forEach(x -> {

        map.computeIfPresent(Arrays.asList(x.getMode(), x.getCode()),
                             (key, value) -> {
                                 value.add(x);
                                 value.removeIf(xx -> !xx.isVode());
                                 return value;
                             });
        map.computeIfAbsent(Arrays.asList(x.getMode(), x.getCode()),
                            key -> {
                                List<ProductModel> list = new ArrayList<>();
                                list.add(x);
                                return list;
                            });
    });

    map.values()
       .stream()
       .flatMap(List::stream)
       .forEachOrdered(x -> System.out.println(x.getCode() + "  " + x.getMode()));

其中ProductModel类似于以下内容:

    static class ProductModel {

    private final int code;
    private final int mode;
    private final boolean vode;

    // some other fields, getters, setters

}

这并不是那么容易实现的。首先,您需要找到是否存在重复项,并且仅在发现此类重复项时才采取相应措施。map.computeIfAbsent负责将键(KeyCode/Mode包装在Arrays::asList中 - 它正确地覆盖了hashCode/equals)放入映射中。

当基于该键找到重复项时,我们希望通过map.computeIfPresent对其进行操作。 "操作"也不是很简单,考虑到vode可能跨多个实例为true(是的,这是我的假设)。您不知道先前的键中放入了什么样的vode - 是false吗?如果是,则必须将其删除。但是当前的false也是吗?如果是,它也必须被删除。


我可以将其放入列表而不是映射中吗? - ASMA2412
@ASMA2412 是的... 你可以轻松地这样做。只需将 forEachOrdered(x -> System.out.println(x.getCode() + " " + x.getMode())) 更改为 collect(Collectors.toList());但说实话,Holger 已经提供了更好的答案。 - Eugene

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