【问题标题】:Java 8:How to remove duplicates from the List based on multiple properties preserving the orderJava 8:如何根据保留顺序的多个属性从列表中删除重复项
【发布时间】:2021-07-21 17:54:02
【问题描述】:

我正在尝试从基于多个属性的学生对象列表中删除重复项,同时保留顺序,如下所示,我有一个学生对象列表,其中我们有多个同名且出勤率不同的学生...我需要删除同名且 studentAttendence 为 100 的重复学生,同时保留顺序。

Student{studentId=1, studentName='Sam', studentAttendence=100, studentAddress='New York'}
Student{studentId=2, studentName='Sam', studentAttendence=50, studentAddress='New York'}
Student{studentId=3, studentName='Sam', studentAttendence=60, studentAddress='New York'}
Student{studentId=4, studentName='Nathan', studentAttendence=40, studentAddress='LA'}
Student{studentId=5, studentName='Ronan', studentAttendence=100, studentAddress='Atlanta'}
Student{studentId=6, studentName='Nathan', studentAttendence=100, studentAddress='LA'}

去除重复后的输出:

Student{studentId=2, studentName='Sam', studentAttendence=50, studentAddress='New York'}
Student{studentId=3, studentName='Sam', studentAttendence=60, studentAddress='New York'}
Student{studentId=4, studentName='Nathan', studentAttendence=40, studentAddress='LA'}
Student{studentId=5, studentName='Ronan', studentAttendence=100, studentAddress='Atlanta'}

我现在只根据名称删除重复项,而不考虑百分比(100)......也没有保留订单......非常感谢任何帮助。(学生供应商是一个简单的供应商功能学生名单)

studentsSupplier.get().stream()
                .sorted(Comparator.comparing(Student::getStudentName))
                .collect(Collectors.collectingAndThen(
                        Collectors.toCollection(
                                () -> new TreeSet<>(Comparator.comparing(Student::getStudentName))), ArrayList::new));

注意:只有学生姓名匹配且百分比为 100 的重复记录必须删除,(记录 Ronon 的百分比为 100 但没有相同学生姓名的重复记录,因此不能删除)

【问题讨论】:

  • 问题不清楚。考虑添加更多案例,例如出勤率仅为 100% 的学生。
  • 必须通过Stream的处理来处理?
  • 最简单的方法是覆盖等号和哈希码,你只需要LinkedHashSet,那么它就是List&lt;Student&gt; filteredStudents = new LinkedList&lt;&gt;(new LinkedHashSet&lt;&gt;(students));。很好很简单。如果没有正确的等号和哈希码,您不能使用集合来删除重复项,在您的情况下,应该按姓名和出勤率构建。
  • studentId 在整个表(或此输入列表)中是否唯一?

标签: java collections java-8 duplicates java-stream


【解决方案1】:

如果你想保持秩序,显然不要打电话给.sorted,这会扰乱秩序。

这里更普遍地使用流是复杂的。如果您要对流中的每个元素执行的操作是独立的,则流喜欢它(除了正在考虑的一个元素之外,不需要查看任何内容,即无需查看邻居)。这里不是这样。

如果删除出勤率为 100 的 任何 学生是正确的(顺便说一句,这是一个错字,正确的词是出勤率),那么所有关于“重复”的内容都是红色的鲱鱼,你只需要:

list.removeIf(s -> s.getStudentAttendence() >= 100);

但如果想法是:仅在出席人数超过 100 人时删除一条记录,并且列表中至少有一条其他同名记录,它会变得更加复杂。

主要问题是您的数据存储机制不适合这项工作。如果您只是停止使用 lambda,这并不难。将您的列表视为包含 1 亿个条目会有所帮助。对于整个流操作来说,将 1 亿个条目的名称保存在“内存”中显然是不可行的。你没有那么多记忆。数据结构(List)也不提供任何快速查找;没有办法编写代码来回答“这个列表中有多少条studentName Sam 的记录?)而不遍历1 亿个条目,这是一项非常重要的工作。

因此,鉴于以下限制:

  • 输入数据为List形式。
  • 输入数据尚未排序。
  • 输出必须与输入保持相同的顺序。

那么从表面上看,这项工作不可能

因此,相反,您需要接受这不是一个简单的单行程序,并且您需要首先制作确实存储您需要的相同数据存储的替代版本。

还有其他问题。特别是,如果您有 3 个Sam 学生并且每条记录都有studentAttendence = 100,会发生什么情况?应该全部删除吗?不应该删除吗?删除2个任意的?

通常,如果您在编写算法时遇到问题,实际问题是您没有完全指定您想要的行为,因此您的挣扎主要是因为您没有完全理解问题,而不仅仅是编码问题。

假设规则很简单:删除所有出勤率 = 100 的学生,但前提是存在出勤率低于 100 的同名记录。如果所有记录的出勤率都为 100,则保留所有记录,然后:

List<Students> students = ...;
Set<String> dupeNames = students.stream()
  .filter(s -> s.getAttendence() < 100)
  .map(Student::getStudentName)
  .collect(Collectors.toSet());

students.removeIf(s -> s.getAttendence() < 100 && dupeNames.contains(s.getStudentName());

会完成这项工作,而且会很快完成。 (O(n),具体到算法上:制作基于集合的副本需要每个学生记录的固定时间步长,因此 O(n),并且 removeIf 调用同样需要检查每个学生,但只需要执行固定时间每一步工作,因为 .contains() 在集合上是恒定时间,假设良好的哈希分布,字符串通常具有),因此,恒定数量的 O(n) 操作意味着整个操作是 O(n):它花费的时间随着输入列表中的学生数量线性增长(与每次处理列表中的单个条目时扫描整个列表的解决方案相比,它随着输入大小的平方而增长)。

【讨论】:

  • 正如我所解释的,这并不能解释一切。很多人给了你一些奇怪场景的例子。如果您有 2 条记录,名称 = Sam 和出席人数 = 100。没有其他 Sams。现在呢?
  • 那么如果dupeNames 包含Sam,这里的removeIf 会不会删除所有名称为Sam 且出席人数少于100 的条目?与所需的输出并不完全相同。
  • dupeNames 并不真正包含欺骗名称,而是所有名称(带有getAttendence() &gt;= 100)。
  • 最终的sn-p有一个bug;我现在修好了。目的是:首先获取所有名称的列表,其中至少有一个出席人数低于 100 的条目。然后,删除该名称在此列表中且出席人数为 100+ 的所有条目。这是对该要求的几种不同的合理看法之一,并未完全涵盖所有边缘情况。
  • 打错了,removeIf 中的s.getAttendence() &lt; 100 应该是&gt;= 100。但它仍然无法处理两个条目具有相同名称和s.getAttendence() &gt;= 100(没有具有相同名称和&lt; 100 的条目)的情况。
【解决方案2】:

假设 studentId 是示例中给出的唯一性,您可以使用方法 List.removeIf 和 BiPredicate 接受学生和学生列表。

BiPredicate<Student,List<Student>> pred = 
    (stud,list) -> list.stream()
                       .filter(s -> s.getStudentId() != stud.getStudentId())
                       .anyMatch(s -> s.getStudentName().equals(stud.getStudentName()) && stud.getStudentAttendence() == 100);

students.removeIf(stud -> pred.test(stud, students));

【讨论】:

  • 谢谢厄立特里亚......你的解决方案已经涵盖了我的场景......非常感谢......我没有足够的票数来支持你的解决方案......非常感谢
  • @Robertishwick 很高兴我能帮上忙。
【解决方案3】:

你应该像这样过滤记录:


studentList.stream().filter(s -> s.getStudentAttendence() != 100)
                .filter(distinctByKeys(Student::getStudentName, Student::getStudentAttendence))
                .collect(Collectors.toList());


distinctByKeys 方法:

private static <T> Predicate<T> distinctByKeys(Function<? super T, ?>... keyExtractors) {
        final Map<List<?>, Boolean> seen = new ConcurrentHashMap<>();

        return t -> {
            final List<?> keys = Arrays.stream(keyExtractors).map(ke -> ke.apply(t)).collect(Collectors.toList());

            return seen.putIfAbsent(keys, Boolean.TRUE) == null;
        };
    }

过滤后

[Student [studentId=2, studentName=Sam, studentAttendence=50, studentAddress=New York], 
Student [studentId=3, studentName=Sam, studentAttendence=60, studentAddress=New York], 
Student [studentId=4, studentName=Nathan, studentAttendence=40, studentAddress=LA], 
Student [studentId=5, studentName=Ronan, studentAttendence=76, studentAddress=Atlanta]]

【讨论】:

  • 如何根据键区分输出?如果Ronan 有 100 人出席呢?
  • @Naman 如果Ronan 有 100 人参加,则将其删除
  • 不知道为什么有人投了反对票,这个答案给出了想要的输出
  • 没有投票,但是输入的Ronan没有重复的名字,那你为什么要删除呢?
  • 如果 ronan 有 100 个百分比怎么办......它不应该被删除,因为它不是重复的......因为只有一个名为 ronan 的条目
【解决方案4】:

试试这个:

Map<String, Integer> checkList = new HashMap<>();
Student [] buffer = studentsSupplier.get()
    .stream()
    .map( student -> 
    {
        checkList.compute( student.getStudentName(), (v,k) -> (v == null) ? Integer.valueOf( 1 ) : Integer.valueOf( v.intValue() + 1 ) );
        return student;
    }
    .toArray( Student []::new );
List<Student> result = buffer.stream()
    .filter( student -> (checkList.get( student.getStudentName() ).intValue() == 1) || (student.getAttendance() != 100) )
    .collect( Collectors.toList() );

假设出勤率为 100 的记录保留在列表中,如果该记录是该学生的唯一记录。

相同代码的讨厌版本看起来像这样,但更难理解:

Map<String, Integer> checkList = new HashMap<>();
List<Student> result = studentsSupplier.get()
    .stream()
    .peek( student -> checkList.compute( student.getStudentName(), (v,k) -> (v == null) ? Integer.valueOf( 1 ) : Integer.valueOf( v.intValue() + 1 ) )
    .collect( Collectors.toList() )
    .stream()
    .filter( student -> (checkList.get( student.getStudentName() ).intValue() == 1) || (student.getAttendance() != 100) )
    .collect( Collectors.toList() );

【讨论】:

  • 不管你滥用map还是peek,都是“讨厌的”。而且完全没有必要,因为有一个Student 的集合作为起点,所以没有必要将它们收集到另一个数组中。相反,请使用像 collect(Collectors.groupingBy(Student::getStudentName, Collectors.counting())) 这样的 Stream 操作,它会以干净的方式生成 Map&lt;String, Long&gt;。如果你发现你的代码不可读,那么你就在正确的轨道上。无需手动调用Integer.valueOf(…)intValue()。 Java 从版本 5 开始具有自动装箱功能。并且数组没有 stream() 方法。
  • @Holger – 集合是必需的,因为您不能两次处理流;我需要将流“发球”成两个结果。对collect() 的建议调用只给了我一个输出,即清单,但原始流会丢失。只有当studentsSupplier.get() 总是返回相同的结果时,缓冲区才会过时,但这在这里是非法的假设。我更正了丢失的array.stream() 呼叫。
  • 那么,将单个studentsSupplier.get() 调用的结果存储到局部变量中会带来如此大的挑战,以至于您会走上不必要的复制操作路线,但会产生不利的副作用?而且你仍然没有解释为什么你使用像 compute( student.getStudentName(), (v,k) -&gt; (v == null) ? Integer.valueOf( 1 ) : Integer.valueOf( v.intValue() + 1 ) 这样的手动装箱而不是 compute(student.getStudentName(), (v,k) -&gt; v == null? 1: v + 1) 或者更简单的 merge(student.getStudentName(), 1, Integer::sum)
  • @Holger – studentsSupplier.get() 返回什么?我们唯一知道的是它允许在返回值上调用stream()。我们不知道连续两次调用stream() 是否会返回相同的结果。所以是的,存储studentsSupplier.get() 的返回值——基于当前可用信息——是一个挑战。通常是您提出需要考虑的模糊限制(compare() 的实现)。
  • 问题是关于“从列表中删除重复项......保留顺序”,表明源是一个列表并且有一个有意义的顺序要保留。但如果您不信任来源,您可以简单地使用List&lt;Student&gt; snapshot = new ArrayList&lt;&gt;(studentsSupplier.get());我不知道你指的是哪个“模糊限制”,但compare的合约是well defined
猜你喜欢
  • 2019-11-21
  • 2010-10-03
  • 1970-01-01
  • 1970-01-01
  • 2019-02-13
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多