Problem overview
Java can sort numbers and strings on its own because it knows which of two of them comes first. For objects of your own class it doesn't, until the class says how two of its objects compare. In Java a class says so by implementing Comparable. After that, Collections.sort can sort a list of those objects with no comparison written at the call.
You write:
- a class
Student, created withStudent(name, grade), that implementsComparable<Student>. A student comes first if the grade is higher. When the grades are equal, the name that comes first alphabetically comes first; rankStudents(students)inSolution, which sorts the students withCollections.sortand returns their names in that order.
For Ana (90), Bob (85) and Cid (90) the result is [Ana, Cid, Bob]. Ana and Cid tie on 90, so their names decide, and Bob's 85 puts him last.
How compareTo works
The one method of Comparable<Student> is compareTo(Student other). It returns a number:
- negative when
thisstudent comes first; - positive when
othercomes first; - 0 when they are equal.
The sort uses only the sign of the result. For the students above, the calls give these values:
ana.compareTo(cid) -> -2
cid.compareTo(ana) -> 2
ana.compareTo(ana) -> 0
bob.compareTo(ana) -> 5
The numbers' sizes don't matter, only that they are negative, positive or zero. Ana comes before Cid, Cid after Ana, and Bob, with the lower grade, after Ana.
Approach: two keys, compared in order
The ranking has two rules: first the grade, highest first, then the name, A to Z. compareTo checks them in that order:
- If the grades differ, the grade decides. Returning
other.grade - gradeis negative exactly when this student's grade is bigger, which is what puts higher grades first. Note the order of the subtraction: it is reversed on purpose, because a normal ascending sort would usegrade - other.grade. - If the grades are equal, the names decide.
Stringis itselfComparable, soname.compareTo(other.name)already gives a negative number whennameis earlier alphabetically.
Because the names are all different, two distinct students never compare as 0.
Step by step
- Declare the fields
public String name;andpublic int grade;.rankStudentsreadsnamelater. - Store the parameters in the constructor with
this.name = name;andthis.grade = grade;. - Compare the grades. If they differ, return
other.grade - grade. - Compare the names when the grades are equal: return
name.compareTo(other.name). - Sort in
rankStudentswithCollections.sort(students), with no comparator. Java findscompareTobecauseStudentisComparable. - Collect the names in a new
ArrayList<>by looping over the sorted students, and return it.
The solution in Java
class Student implements Comparable<Student> {
public String name;
public int grade;
public Student(String name, int grade) {
this.name = name;
this.grade = grade;
}
@Override
public int compareTo(Student other) {
if (grade != other.grade) {
return other.grade - grade;
}
return name.compareTo(other.name);
}
}
class Solution {
public List<String> rankStudents(List<Student> students) {
Collections.sort(students);
List<String> names = new ArrayList<>();
for (Student student : students) {
names.add(student.name);
}
return names;
}
}
Details worth knowing
- No sorting loop. The sort is Java's own, and
Studentonly supplies the comparison. IfStudentdid not implementComparable, the callCollections.sort(students)would not compile: the compiler reports that no suitablesortmethod was found for the list. - Sorting changes the list.
Collections.sortrearranges the list it receives in place.rankStudentsthen builds a separate list of names. - String comparison is by characters.
"Ann".compareTo("Anna")is negative, because when one name is the start of the other, the shorter one comes first. In the third exampleAnncomes beforeAnna. - Subtraction is safe here. Grades are between 0 and 100, so
other.grade - gradecan't overflow. For values over a wider range,Integer.compare(other.grade, grade)is the safe form.
Sorting by hand-made rules
Once compareTo exists, the rule travels with the class. Any code that sorts a list of students gets the same order, whether it uses Collections.sort or another library method that relies on Comparable. Here the list starts as Bob, Cid, Ana:
List<Student> students = new ArrayList<>(List.of(bob, cid, ana));
Collections.sort(students);
for (Student s : students) System.out.println(s.name + " " + s.grade);
Ana 90
Cid 90
Bob 85
The list must be modifiable for Collections.sort to work, which is why it is wrapped in a new ArrayList here: the list returned by List.of is fixed, and sorting it throws an UnsupportedOperationException.
Complexity
Sorting n students takes O(n log n) comparisons, and each compareTo takes constant time (the names have at most 10 letters). Collecting the names is one more pass of O(n). The list of names takes O(n) extra space.
In the second example every grade is different, so the grades alone decide, from 100 down to 0, and the names are never compared. In the third, every grade is 88, so the order is purely alphabetical.
Testing
| Students | Result |
|---|---|
| Ana 90, Bob 85, Cid 90 | [Ana, Cid, Bob] |
| Eve 55, Dan 100, Bea 0, Cal 99 | [Dan, Cal, Eve, Bea] |
| Max 88, Leo 88, Anna 88, Ann 88 | [Ann, Anna, Leo, Max] |
Common mistakes
- Subtracting in the wrong order.
grade - other.graderanks the lowest grade first. - Writing your own sorting loop. The exercise is to let the language's sort do the work, using your comparison.
- Returning a boolean.
compareToreturns anint. A "true or false" answer can't say "before", "after" and "equal". - Using
==on names. It compares references, not contents. Usename.compareTo(other.name)orequals. - Forgetting the tie-break. If you only compare grades, students with equal grades come out in whatever order they started in (the sort is stable, so they keep their original order), not alphabetically.
