java - 从列表中提取包含重复项的列表,并获取非重复列表java 8流

标签 java algorithm java-8 java-stream collectors

我正在使用 apache poi 从 Excel 文件读取数据并将其转换为对象列表。但现在我想根据某些规则将任何重复项提取到该对象的另一个列表中,并获取非重复列表。
检查重复的条件

  1. 姓名
  2. 电子邮件
  3. 电话号码
  4. 商品及服务税编号

任何这些属性都可能导致重复。这意味着而不是

派对类

public class Party {

    private String name;

    private Long number;

    private String email;

    private String address;

    private BigDecimal openingBalance;

    private LocalDateTime openingDate;

    private String gstNumber;
   // Getter Setter Skipped
}

假设这是迄今为止由 Excel 数据逻辑返回的列表

    var firstParty = new Party();
    firstParty.setName("Valid Party");
    firstParty.setAddress("Valid");
    firstParty.setEmail("Valid");
    firstParty.setGstNumber("Valid");
    firstParty.setNumber(1234567890L);
    firstParty.setOpeningBalance(BigDecimal.ZERO);
    firstParty.setOpeningDate(DateUtil.getDDMMDateFromString("01/01/2020"));

    var secondParty = new Party();
    secondParty.setName("Valid Party");
    secondParty.setAddress("Valid Address");
    secondParty.setEmail("Valid Email");
    secondParty.setGstNumber("Valid GST");
    secondParty.setNumber(7593612247L);
    secondParty.setOpeningBalance(BigDecimal.ZERO);
    secondParty.setOpeningDate(DateUtil.getDDMMDateFromString("01/01/2020"));

    var thirdParty = new Party();
    thirdParty.setName("Valid Party 1");
    thirdParty.setAddress("address");
    thirdParty.setEmail("email");
    thirdParty.setGstNumber("gst");
    thirdParty.setNumber(7593612888L);
    thirdParty.setOpeningBalance(BigDecimal.ZERO);
    secondParty.setOpeningDate(DateUtil.getDDMMDateFromString("01/01/2020"));

    var validParties = List.of(firstParty, secondParty, thirdParty);

到目前为止我已经尝试过:-


var partyNameOccurrenceMap = validParties.parallelStream()
        .map(Party::getName)
        .collect(Collectors.groupingBy(Function.identity(), HashMap::new, Collectors.counting()));

var partyNameOccurrenceMapCopy = SerializationUtils.clone(partyNameOccurrenceMap);

var duplicateParties = validParties.stream()
        .filter(party-> {
            var occurrence = partyNameOccurrenceMap.get(party.getName());
            if (occurrence > 1) {
                partyNameOccurrenceMap.put(party.getName(), occurrence - 1);
                return true;
            }
            return false;
        })
        .toList();
var nonDuplicateParties = validParties.stream()
        .filter(party -> {
            var occurrence = partyNameOccurrenceMapCopy.get(party.getName());
            if (occurrence > 1) {
                partyNameOccurrenceMapCopy.put(party.getName(), occurrence - 1);
                return false;
            }
            return true;
        })
        .toList();

上述代码仅检查当事人名称,但我们还需要检查电子邮件电话号码商品及服务税号码.

上面编写的代码工作得很好,但可读性、简洁性和性能可能是一个问题,因为数据集足够大,比如 Excel 文件中的 10k 行

最佳答案

永远不要忽略 Equals/hashCode 合约

name, email, number, gstNumber

Any of these properties can result in a duplicate, which mean or

您对重复的定义意味着这些属性中的任何一个都应该匹配,而其他属性可能

这意味着不可能提供一个与给定定义匹配且不违反hashCode contract的实现equals/hashCode .

If two objects are equal according to the equals method, then calling the hashCode method on each of the two objects must produce the same integer result.

即如果您以这种方式实现equals,则它们中的任何(不是全部)属性:nameemailnumber, gstNumber 可以匹配,这足以认为两个对象相等,那么就无法正确实现 hashCode

因此,您不能在基于哈希的 Collection 中使用具有损坏的 equals/hashCode 实现的对象,因为相等的对象可能最终会出现在不同的存储桶中(因为它们可以产生不同的哈希值)。 IE。 HashMap 将无法识别重复的键,因此 groupingBygroupingBy()Function.identity() 作为分类器函数将无法正常工作。

因此,要解决这个问题,您需要基于所有 4 属性实现 equals():nameemail numbergstNumber(即所有这些值必须相等),同样,所有这些值都必须对哈希码有贡献。

如何确定重复项

没有简单的方法可以通过多个条件确定重复项。您提供的解决方案可行,因为我们不能依赖equals/hashCode

唯一的方法是为每个属性的每一端单独生成一个 HashMap (即在本例中我们需要 4 映射)。但是我们可以改变这一点,避免对每个 map 重复相同的步骤并对逻辑进行硬编码吗?

是的,我们可以。

我们可以创建一个自定义的通用累积类型(它适用于任何类 - 无硬编码逻辑),它将封装确定重复项的所有逻辑并在后台维护任意数量的映射。在使用给定集合中的所有元素后,此自定义对象将了解其中的所有重复项。

这就是它的实现方式。

将用作自定义收集器的容器的自定义累积类型。它的构造函数需要函数的可变参数,每个函数对应于检查对象是否重复时应考虑的属性。

public static class DuplicateChecker<T> implements Consumer<T> {
    
    private List<DuplicateHandler<T>> handles;
    private Set<T> duplicates;

    @SafeVarargs
    public DuplicateChecker(Function<T, ?>... keyExtractors) {
        this.handles = Arrays.stream(keyExtractors)
            .map(DuplicateHandler::new)
            .toList();
    }

    @Override
    public void accept(T t) {
        handles.forEach(h -> h.accept(t));
    }
    
    public DuplicateChecker<T> merge(DuplicateChecker<T> other) {
        for (DuplicateHandler<T> handler: handles) {
            other.handles.forEach(handler::merge);
        }
        
        return this;
    }

    public DuplicateChecker<T> finish() {
        duplicates = handles.stream()
            .flatMap(handler -> handler.getDuplicates().stream())
            .flatMap(Set::stream)
            .collect(Collectors.toSet());
        
        return this;
    }
    
    public boolean isDuplicate(T t) {
        return duplicates.contains(t);
    }
}

代表单个创建者的辅助类(例如nameemail等),它封装了HashMapkeyExtractor 用于从 T 类型的对象获取key

public static class DuplicateHandler<T> implements Consumer<T> {
    private Map<Object, Set<T>> itemByKey = new HashMap<>();
    private Function<T, ?> keyExtractor;

    public DuplicateHandler(Function<T, ?> keyExtractor) {
        this.keyExtractor = keyExtractor;
    }

    @Override
    public void accept(T t) {
        itemByKey.computeIfAbsent(keyExtractor.apply(t), k -> new HashSet<>()).add(t);
    }

    public void merge(DuplicateHandler<T> other) {
        other.itemByKey.forEach((k, v) ->
            itemByKey.merge(k,v,(oldV, newV) -> { oldV.addAll(newV); return oldV; }));
    }
    
    public Collection<Set<T>> getDuplicates() {
        Collection<Set<T>> duplicates = itemByKey.values();
        duplicates.removeIf(set -> set.size() == 1); // the object is proved to be unique by this particular property
        
        return duplicates;
    }
}

这就是负责生成重复项映射的方法,它将在干净的代码中使用。给定的集合将分为两部分:一部分映射到 key true - 重复项,另一部分映射到 key false - 独特的对象。

public static <T> Map<Boolean, List<T>> getPartitionByProperties(Collection<T> parties,
                                                                 Function<T, ?>... keyExtractors) {
    
    DuplicateChecker<T> duplicateChecker = parties.stream()
        .collect(Collector.of(
            () -> new DuplicateChecker<>(keyExtractors),
            DuplicateChecker::accept,
            DuplicateChecker::merge,
            DuplicateChecker::finish
        ));
    
    return parties.stream()
        .collect(Collectors.partitioningBy(duplicateChecker::isDuplicate));
}

以及如何将其应用于您的特定案例。

main()

public static void main(String[] args) {
    List<Party> parties = // initializing the list of parties
    
    Map<Boolean, List<Party>> isDuplicate = partitionByProperties(parties,
            Party::getName, Party::getNumber,
            Party::getEmail, Party::getGstNumber);
}

关于java - 从列表中提取包含重复项的列表,并获取非重复列表java 8流,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/73223765/

相关文章:

c++ - 什么 STL 算法可以确定容器中的一项是否满足谓词?

java - 将 Java-8 Stream 收集到 Guava ImmutableList 的最佳方式

java - 打开 JDK 崩溃 eclipse

arrays - 从迭代器内的对象中删除元素的最佳方法是什么?

algorithm - 动态规划帮助 : Binary Tree Cost Edge

Java SE 6 和 Java SE 8 JRE 在 Windows 7 上的行为不同(文件权限)

java - 如何使用 Java 8 循环和打印二维数组

java - 如何将我的应用程序添加到 "Open With"对话框?

java - java中的Zlib解压缩不起作用

javax未知主机异常?