Keep (minim) object in a collection by two attribute keys in java

147 views Asked by At

For example I have a list that contains 3 objects:

List<Student> studentList= new ArrayList<Student>();
list.add(new Student("name1", 5);
list.add(new Student("name3", 6);
list.add(new Student("name1", 7);

class Student{ String name; Integer grade;}

My filtering logic: if name is equal then i need to filter out the objects that have maximum grade - so keep the minimum one.

Also I do not know there the duplicates by name are in the collection.

I am stuck with this implementation:

Set<Student> setStudents= new TreeSet<Student>(new Comparator<Student>() {
            @Override
            public int compare(Student st1, Student st2) {
                int compareName= st1.getName().compareTo(st2.getName());
                if (compareName== 0){
                    int compareGrade = st1.getGrade().compareTo(st2.getGrade());
                  // ?

                }
                return compareName;
            }
        });
        setStudents.addAll(studentList);

Expected output:

List with

Student("name1", 5)
Student("name3", 6);

Thank you

5

There are 5 answers

0
Mehdi On BEST ANSWER

This solution is faster than TreeSet because use HashSet rather than TreeSet and it solve your problem completely.

here will be your student class :

public class Student {

    private String name;
    private int grade;

    public Student(String name, int grade) {
        this.name = name;
        this.grade = grade;
    }

    public String getName() {
        return name;
    }

    public int getGrade() {
        return grade;
    }

    public void setName(String name) {
        this.name = name;
    }

    public void setGrade(int grade) {
        this.grade = grade;
    }

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

    @Override
    public boolean equals(Object obj) {
        Student external = (Student) obj;
        if (external.getGrade() > this.getGrade()) {
            external.setGrade(this.getGrade());
            return true;
        } else {
            return true;
        }

    }
}

and this is your test :

public class Launcher {

    public static void main (String ... args){
        Set<Student> stSet= new HashSet<Student>();
        stSet.add(new Student("name1", 7));
        stSet.add(new Student("name3", 6));
        stSet.add(new Student("name1", 5));
        stSet.add(new Student("name1", 5));
        stSet.add(new Student("name1", 1));
        stSet.add(new Student("name1", 10));
        stSet.add(new Student("name1", 2));
        stSet.add(new Student("name1", 9));
        stSet.add(new Student("name1", 4));
    }
}

the output is 1 because in String "name1", the minimum value is 1;

if you want to create list first and then get that set, this is also possible:

public class Launcher {

    public static void main (String ... args){

        List<Student> stList = new ArrayList<Student>();
        stList.add(new Student("name1", 7));
        stList.add(new Student("name3", 6));
        stList.add(new Student("name1", 5));
        stList.add(new Student("name1", 5));
        stList.add(new Student("name1", 1));
        stList.add(new Student("name1", 10));
        stList.add(new Student("name1", 2));
        stList.add(new Student("name1", 9));
        stList.add(new Student("name1", 4));
        Set<Student> stSet= new HashSet<Student>(stList);
    }
}

this will give you the same result !

3
Erik Pragt On

I would encapsulate the grades in a separate class. Also, I wouldn't use a List, it's an inefficient storage mechanism for your use case. I'd do something like this:

public class Grades {

    private Map<String, Integer> store = new HashMap<>();

    public void storeMinimumGrade(Student student) {
        Integer grade = store.get(student.getName());
        if(grade == null || student.getGrade() < grade) {
            grade.put(name, grade);
        }
    }
}

Would the above work for you?

0
OldCurmudgeon On

If you are allowed to use Java 8 you can stream the data, group it by name and use a downstream minBy collector.

public void test() {
    List<Student> studentList = new ArrayList<>();
    studentList.add(new Student("name1", 5));
    studentList.add(new Student("name3", 6));
    studentList.add(new Student("name1", 7));
    // Gather min grade of all students.
    Map<String, Optional<Student>> minGrade = studentList.stream()
            // Group by student name.
            .collect(Collectors.groupingBy(Student::getName,
                            // Get minimum grade.
                            Collectors.minBy((s1, s2) -> s1.getGrade().compareTo(s2.getGrade())))
            );
    System.out.println(minGrade);
}

static class Student {

    final String name;
    final Integer grade;

    public String getName() {
        return name;
    }

    public Integer getGrade() {
        return grade;
    }

    private Student(String name, int grade) {
        this.name = name;
        this.grade = grade;
    }

    @Override
    public String toString() {
        return "Student{" + "name=" + name + ", grade=" + grade + '}';
    }

}
0
Paraneetharan Saravanaperumal On

try this code :

List<Student> studentList= new ArrayList<Student>();
studentList.add(new Student("name1", 5));
studentList.add(new Student("name3", 6));
studentList.add(new Student("name1", 7));

and

Set<Student> setStudents= new TreeSet<Student>(new Comparator<Student>() {
    @Override
    public int compare(Student st1, Student st2) {
        int compareName= st1.name.compareTo(st2.name);
        if (compareName== 0){
            int compareGrade = (st1.grade).compareTo(st2.grade);
            if(compareGrade==-1){
             // compare two student current minimum grade is replaced with old minimum value 
                st2.grade=st1.grade;
            }
           // can not add new student to set, 
                return 0;

        }
        return compareName;
    }
});

setStudents.addAll(studentList);

check

for (Student element : setStudents) {
        System.out.println(element.name);
    }
1
mkrakhin On

If you use Java 8, you could write your own collector. Something like this:

public class MyCollector implements Collector<Student, Map<String, Student>, Set<Student>> {
    @Override
    public Supplier<Map<String, Student>> supplier() {
        return HashMap::new;
    }

    @Override
    public BiConsumer<Map<String, Student>, Student> accumulator() {
        return (nameStudentMap, student) -> nameStudentMap.merge(
                student.name, student, (student1, student2) -> student1.grade < student2.grade ? student1 : student2
        );
    }

    @Override
    public BinaryOperator<Map<String, Student>> combiner() {
        return (nameStudentMap, nameStudentMap2) -> {
            nameStudentMap2.forEach(
                    (s, student) -> nameStudentMap.merge(
                            s, student, (student1, student2) -> student1.grade < student2.grade ? student1 : student2
                    )
            );
            return nameStudentMap;
        };
    }

    @Override
    public Function<Map<String, Student>, Set<Student>> finisher() {
        return nameStudentMap -> new HashSet<>(nameStudentMap.values());
    }

    @Override
    public Set<Characteristics> characteristics() {
        return EnumSet.of(Collector.Characteristics.UNORDERED);
    }
}

Then, you could give this collector to a stream of students:

List<Student> studentList = new ArrayList<>();
studentList.add(new Student("mike", 5));
studentList.add(new Student("mike", 1));
studentList.add(new Student("jenn", 8));
studentList.add(new Student("jenn", 2));
studentList.add(new Student("alex", 2));
studentList.add(new Student("mike", 3));
Set<Student> res = studentList.stream().collect(new MyCollector());

Check:

res.forEach((Student s) -> {
    System.out.println(s.name + " " + s.grade);
});

Output:

jenn 2
mike 1
alex 2