java.util.stream.Stream.distinct() 方法如何工作?我可以覆盖对象流的 equals() 方法吗?

How does java.util.stream.Stream.distinct() method work? Can I override the equals() method of the stream of the objects?

我的用例是我正在尝试使用不同的 Stream 方法从 class StudentCourseMapping 的对象列表中删除具有相同卷号的学生。 Pojo 详情如下

public class StudentCourseMapping implements Serializable{
    private String name;
    private String dept;
    private Integer roll;
    private String course;

下面是equals方法

    @Override
    public boolean equals(Object obj) {
        StudentCourseMapping other = (StudentCourseMapping) obj;
        if (roll == null) {
            if (other.roll != null)
                return false;
        } else if (!roll.equals(other.roll))
            return false;
        return true;
    }

下面是实现

public class RemoveDuplicateUsingStream {
    public static void main(String[] args) {
        List<StudentCourseMapping> studentCourceList = JacksonJSONReaderObjectMapper.jsonReader();
        
        studentCourceList.stream().distinct().forEach(System.out::println);
        StudentCourseMapping s0 = studentCourceList.get(0);
        StudentCourseMapping s1 = studentCourceList.get(1);
        System.out.println(s0.equals(s1));

        Set<Integer> st = new HashSet();
        List<StudentCourseMapping>studentCourceList2 = studentCourceList.stream().filter(s -> st.add(s.getRoll()))
                .collect(Collectors.toCollection(ArrayList::new));
        System.out.println(studentCourceList2.size());
    }
}

输出为

StudentCourseMapping [name=Alu, dept=Physics, roll=12, course=Quantum Theory]
StudentCourseMapping [name=Alu, dept=Physics, roll=12, course=English]
StudentCourseMapping [name=Sam, dept=Commerce, roll=16, course=English]
StudentCourseMapping [name=Sam, dept=Commerce, roll=16, course=Accounts]
StudentCourseMapping [name=Joe, dept=Arts, roll=19, course=English]
StudentCourseMapping [name=Joe, dept=Arts, roll=19, course=Hindi]
true
3

JacksonJSONReaderObjectMapper.jsonReader();是一个自定义方法,如下所示 JSON。我可以通过使用过滤器并添加到 HashSet 来实现相同的目的,但我真的想知道我的独特实现有什么问题。

{
    "studentCourseMapping": [
        {
            "name": "Alu",
            "dept": "Physics",
            "roll": 12,
            "course": "Quantum Theory"
        },
        {
            "name": "Alu",
            "dept": "Physics",
            "roll": 12,
            "course": "English"
        },
        {
            "name": "Sam",
            "dept": "Commerce",
            "roll": 16,
            "course": "English"
        },
        {
            "name": "Sam",
            "dept": "Commerce",
            "roll": 16,
            "course": "Accounts"
        },
        {
            "name": "Joe",
            "dept": "Arts",
            "roll": 19,
            "course": "English"
        },
        {
            "name": "Joe",
            "dept": "Arts",
            "roll": 19,
            "course": "Hindi"
        }
    ]
}

当我尝试直接测试 equals 方法时,它工作正常并返回 true,因为 s0 和 s1 都已滚动为 12。

        StudentCourseMapping s0 = studentCourceList.get(0);
        StudentCourseMapping s1 = studentCourceList.get(1);
        System.out.println(s0.equals(s1));

但是当我使用 distinct 时,所有对象都被打印出来,并且在 eclipse 中尝试调试时,我编写的 distinct 方法没有被调用。但文档说应该调用它。顺便说一句,这来自 Oracle 文档 8,但我使用的是 JDK 11

流不同() Returns 由该流的不同元素(根据 Object.equals(Object))组成的流。

正如@RealSkeptic 和@Jesper 在评论中已经说过的那样,您必须重写 StudentCourseMapping 中的 hashCode 方法才能正确比较流中的元素并且只保留根据您的 equals 实现的不同。

虽然,这在 Stream 文档和 distinct documentation. I believe this is considered implied, as the equals documentation 中都没有提及,但在覆盖 equals 方法时,充分涵盖了遵守一般哈希码合同的义务。

Note that it is generally necessary to override the hashCode method whenever this method is overridden, so as to maintain the general contract for the hashCode method, which states that equal objects must have equal hash codes.

事实上,一旦 hashCode() 方法被覆盖,您的代码片段就会产生所需的输出。

public class StudentCourseMapping implements Serializable {
    private String name;
    private String dept;
    private Integer roll;
    private String course;

    //... your class implementation ...

    @Override
    public boolean equals(Object obj) {
        if (obj == null) return false;
        if (obj == this) return true;
        if (obj.getClass() != getClass()) return false;
        StudentCourseMapping student = (StudentCourseMapping) obj;
        return Objects.equals(roll, student.roll);
    }

    @Override
    public int hashCode() {
        return Objects.hashCode(roll);
    }

    @Override
    public String toString() {
        return String.format("[name = %s, dept = %s, roll = %d, course = %s]", name, dept, roll, course);
    }
}

理论上您现有的实现会起作用,因为不同意味着不相等的值。但是 distinct()Stream() 实现使用 LinkedHashSet(直接使用对象的 hashCode 方法)来捕获重复项。由于每个 class 引用都是唯一的,并且 hashCode 的默认实现基于该引用的值,因此不会捕获来自 equals 视角的重复项。

这是一个使用默认 hashCode 的简单演示。创建了一个包含 10 个对象的列表。然后将该列表重复添加到自身并打乱顺序,产生一个包含 10,240 个对象的列表,总共有 10 个不同的 hashCode。所以当 distinct() 方法检查那些 hashcodes 时,它会找到重复项并忽略它们。

注:此class使用了简单且不完整的equals方法,足以满足此受控演示。

class Etest {
        int v;
        
        public Etest(int v) {
            this.v = v;
        }
        
        @Override
        public boolean equals(Object ob) {
            return ((Etest)ob).v == this.v;
        }
        
        @Override
        public String toString() {
            return v+"";
        }
}

List<Etest> list = new ArrayList<>(IntStream.range(1, 11)
        .mapToObj(i -> new Etest(i)).toList());

for(int i = 0; i < 10; i++) {
    Collections.shuffle(list);
    list.addAll(list);
}
System.out.println("Total entries = " + list.size());
list.stream().distinct().forEach(e->System.out.printf("%s ", e));

打印类似

的东西
Total entries = 10240
3 4 8 9 2 10 6 5 1 7