How to sort a list by a private field?

My entity class looks like this:

public class Student {

private int grade;

// other fields and methods
}


and I use it like that:

List<Student> students = ...;


How can I sort students by grade, taking into account that it is a private field?

You have these options:


make grade visible
define a getter method for grade
define a Comparator inside Student
make Student implement Comparable
use reflection (in my opinion this is not a solution, it is a workaround/hack)




Example for solution 3:

public class Student {
private int grade;

public static Comparator<Student> byGrade = Comparator.comparing(s -> s.grade);
}


and use it like this:

List<Student> students = Arrays.asList(student2, student3, student1);
students.sort(Student.byGrade);
System.out.println(students);


This is my favorite solution because:


You can easily define several Comparators
It is not much code
Your field stays private and encapsulated




Example of solution 4:

public class Student implements Comparable {
private int grade;

@Override
public int compareTo(Object other) {
if (other instanceof Student) {
return Integer.compare(this.grade, ((Student) other).grade);
}
return -1;
}
}


You can sort everywhere like this:

List<Student> students = Arrays.asList(student2, student3, student1);
Collections.sort(students);
System.out.println(students);


In general, if you need a behaviour that depends on student's grade, than this information must be accessible - add a method or a property that allows other code to access it.

A simplest fix would be thus:

public class Student implements IStudent {

...
private int grade;
...
// other fields and methods

public int getGrade() {
return grade;
}
}


You should probably extend the interface IStudent as well :)

However, if you need this only for sorting you can go with an idea already suggested in other answers: implement Comparable interface. This way you can keep the grade hidden, and use it inside the int compareTo method.

Implement Comparable interface for the Student class and implement the method int compareTo(T o). This way you can keep the grade property private.

Your class could implement the Comparable interface. Then, you can easily sort the list:

public class Student implements IStudent, Comparable<Student>
{
...

private int grade;
...

@Override
public int compareTo(Student other)
{
return (grade - other.grade);
}

}

public class Section
{
private List<IStudent> studentsList;

...

public void sortStudents()
{
studentsList.sort(null);
}

}


If you really need to sort by field you don't have access to you can use reflection:

private static int extractGrade(Student student) {
try {
Field field = Student.class.getDeclaredField("grade");
field.setAccessible(true);
return field.getInt(student);
} catch (Exception e) {
throw new RuntimeException(e);
}
}

public static void main(String[] args) {
Comparator<Student> studentComparator = Comparator.comparingInt(DemoApplication::extractGrade);
List<Student> students = Arrays.asList(new Student(1), new Student(10), new Student(5));
students.sort(studentComparator);
}


But I have to say that this method is kinda unsafe.

Don't use it unless absolutely necessary. It is better to give access to given field using getter method for example.

Also you might get issues if you're running this code on modulepath against Java 9+ (you can get InaccessibleObjectException thrown).

About implementing Comparable

From Comparable docs:


This interface imposes a total ordering on the objects of each class that implements it. This ordering is referred to as the class's natural ordering, and the class's {@code compareTo} method is referred to as its natural comparison method.


But what might be a natural ordering for Student?
First name? Last name? Their combination?

It's easy to answer this question for numbers, but not for classes like Student.

So I don't think that Students should be Comparable, they are humans and not dates or numbers. And you can't say who is greater, who is equal and who is lesser.

An option provided by JDK 1.8 is using stream library sorted()method which does not require Comparable interface to be implemented.
You need to implement accessor (getter) method for field grade

public class Student {

private int grade;

public int getGrade() {
return grade;
}

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


Then given having the unsortedStudentList you are able to sort it like the below code:

List<Student> sortedStudentList = unsortedStudentList
.stream()
.sorted(Comparator.comparing(Student::getGrade))
.collect(Collectors.toList());


Also, sorted() method enables you to sort students based on other fields (if you have) as well. For instance, consider a field name for student and in this case you'd like to sort studentList based on both grade and name. So Student class would be like this:

public class Student {

private int grade;
private String name;

public int getGrade() {
return grade;
}

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

public String getName() {
return name;
}

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


To sort based on both fields:

List<Student> sortedStudentList = unsortedStudentList
.stream()
.sorted(Comparator.comparing(Student::getGrade)
.thenComparing(Comparator.comparing(Student::getName)))
.collect(Collectors.toList());


Second comparator comes into play when the first one is comparing two equal objects.

Another option that was mentioned before but not shown as an example is implementing a special Comparator for a comparison by grade.

This example consists of a class Student implementing an interface IStudent, a StudentGradeComparator and a little class Main that uses sample data.

Further explanations are given as code comments, please read them

/**
* A class that compares students by their grades.
*/
public class StudentGradeComparator implements Comparator<IStudent> {

@Override
public int compare(IStudent studentOne, IStudent studentTwo) {
int result;
int studentOneGrade = studentOne.getGrade();
int studentTwoGrade = studentTwo.getGrade();

/* The comparison just decides if studentOne will be placed
* in front of studentTwo in the sorted order or behind
* or if they have the same comparison value and are considered equal
*/
if (studentOneGrade > studentTwoGrade) {
/* larger grade is considered "worse",
* thus, the comparison puts studentOne behind studentTwo
*/
result = 1;
} else if (studentOneGrade < studentTwoGrade) {
/* smaller grade is considered "better"
* thus, the comparison puts studentOne in front of studentTwo
*/
result = -1;
} else {
/* the students have equal grades,
* thus, there will be no swap
*/
result = 0;
}

return result;
}
}


You can apply this class in the sort(Comparator<? super IStudent> comparator) method of a List:

/**
* The main class for trying out the sorting by Comparator
*/
public class Main {

public static void main(String[] args) {
// a test list for students
List<IStudent> students = new ArrayList<IStudent>();

// create some example students
IStudent beverly = new Student("Beverly", 3);
IStudent miles = new Student("Miles", 2);
IStudent william = new Student("William", 4);
IStudent deanna = new Student("Deanna", 1);
IStudent jeanLuc = new Student("Jean-Luc", 1);
IStudent geordi = new Student("Geordi", 5);

// add the example students to the list
students.add(beverly);
students.add(miles);
students.add(william);
students.add(deanna);
students.add(jeanLuc);
students.add(geordi);

// print the list in an unordered state first
System.out.println("———— BEFORE SORTING ————");
students.forEach((IStudent student) -> {
System.out.println(student.getName() + ": " + student.getGrade());
});

/*---------------------------------------*
* THIS IS HOW YOU APPLY THE COMPARATOR *
*---------------------------------------*/
students.sort(new StudentGradeComparator());

// print the list ordered by grade
System.out.println("———— AFTER SORTING ————");
students.forEach((IStudent student) -> {
System.out.println(student.getName() + ": " + student.getGrade());
});
}
}


Just for completeness, here are the interface IStudent and its implementing class Student:

public interface IStudent {

String getName();
int getGrade();

}


/**
* A class representing a student
*/
public class Student implements IStudent {

private String name;
private int grade;

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

public String getName() {
return name;
}

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

public int getGrade() {
return grade;
}

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

}


Getters are not a bad practice, they are exactly made for your problem: Accessing private fields to read them.
Add a getter and you can do:

studentsList.stream().sorted((s1, s2) -> s1.getGrade()compareTo(s2.getGrade)).collect(Collectors.toList())


Update: If you really want to keep the grade private, you need to implement Comparable and override the compare-method.

I belive the best option here is to create a Comparator where the sorted list is needed, because you may need to sort by other fields in other places and that would inflate your domain class:

List<Student> sorted = list.stream()
.sorted(Comparator.comparingInt(o -> o.grade))
.collect(Collectors.toList());


You can do it this way when u want to keep grad private:

students = students.stream().sorted((s1, s2) -> {
try {
Field f = s1.getClass().getDeclaredField("grade");
f.setAccessible(true);
int i = ((Integer)f.getInt(s1)).compareTo((Integer) f.get(s2));
f.setAccessible(false);
return i;
} catch (IllegalArgumentException | IllegalAccessException | NoSuchFieldException | SecurityException e) {
e.printStackTrace();
}
return 0;
}).collect(Collectors.toList());

Comments

Popular posts from this blog

Meaning of `{}` for return expression

Get current scroll position of ScrollView in React Native

Chart JS +ng2-charts not working on Angular+2