Sorting a list in Java with null values

I am going to tell you a story with 2 characters, the developer and the quality analyst(QA). The developer is working on  a very simple application which deals with teenagers and the brand of cell phone they use.

I think the relation between developers and QA is like a one night stand, well, not the typical one! One fine day the QA finds bugs and thanks to your manager (who leaves home early), the developer and the QA are made to stay back and work all night until the bug is fixed and the code is completely bug free.(Yeah, right !)

Our developer friend has written a Teenager class which has an age field and a phoneBrand field.

public class Teenager {
private int age;
private String phoneBrand;
//getters,setters
}

view raw
Teenager.java
hosted with ❤ by GitHub

Manager: We have a new requirement,there is a need for a survey and hence the application needs to return the list of teenagers by increasing age. If two teenagers have the same age, then we need to return the list considering the brand of the cell phones in alphabetical order.

Developer: This will take time. I need to make sure that I write the code using Object Oriented Principles.

Manager: Alright, I give you a days time.( You know he does not understand Object oriented principles !)

You bring all your Object oriented skills to the table, create a separate class for the comparator as the sorting is done using 2 different fields, the age field and then the phoneBrand field. The comparator class looks like this:

public class TeenageSorter implements Comparator<Teenager>{
@Override
public int compare(Teenager t1, Teenager t2)
{
if( t1.getAge() == t2.getAge())
{
return (t1.getPhoneBrand().compareTo(t2.getPhoneBrand()));
}
return t1.getAge() > t2.getAge()? 1 : 1;
}
}

The input to your sort function is a List of Teenagers. You call the sort method like this:

Collections.sort(teens, new TeenageSorter());

Everything works fine.Given an input like this:

List<Teenager> teens = new ArrayList<>();
teens.add(new Teenager(15,"Apple"));
teens.add(new Teenager(16,"Samsung"));
teens.add(new Teenager(14,"Xiaomi"));
teens.add(new Teenager(16,"Apple"));

view raw
TeenagerInput.java
hosted with ❤ by GitHub

Output:

[Age:14 |Phone:Xiaomi, Age:15 |Phone:Apple, Age:16 |Phone:Apple, Age:16 |Phone:Samsung]

Sorted by increasing age and as there are 2 teenagers aged 16, the one with the Apple phone is followed by Samsung using alphabetical order. You check in the code and inform your manager that you are done with your task.

A few hours later, at around 6 pm when you are just about to leave, the QA guy comes to your desk.

QA : Your code is not working .

Developer : That is not possible ![Unpleasant stare]

QA : I am not sure, I added a teenager but did not give him a phone !

You think about it for a second, your coding skills (spelt  “ego”) are at stake, you have to come up with a reply.You know you left that part in the code but…

Developer: How can a teenager not have a phone ! [ You know in your heart that you never had a cell phone as a teenager]

QA : Well, yes, all teenagers do but currently our system allows me to do that, I followed the steps, kept the brand name of the phone empty, it crashed !

Well, you take a deep breath.You know you assumed that the data will never be null. You got what you deserved , a null pointer exception !

You go to your manager and tell him that it is possible that a teenager might not have a phone, so the requirement has to be discussed now. You involve the business analyst as well and explain the situation. The analyst says, well, if a teenager does not have a cell phone and two teenagers have the same age, we need to put them at the end of the list.

You have been asked to fix this in the next few hours.You start thinking about this problem. If the teenager does not have a phone, it means the phoneBrand field is null. Hence the sorting has to now deal with nulls.You look at your Comparator and this is what you come up with:

public class TeenageSorter implements Comparator<Teenager>{
@Override
public int compare(Teenager t1, Teenager t2) {
if( t1.getAge() == t2.getAge()){
return sortByPhoneBrand(t1.getPhoneBrand(),t2.getPhoneBrand());
}
return t1.getAge() > t2.getAge()? 1 : 1;
}
private int sortByPhoneBrand(String phoneBrand1,String phoneBrand2){
if(phoneBrand1 == null && phoneBrand2 == null){
return 0;
}
if(phoneBrand1 == null && phoneBrand2 != null){
return 1;
}
if(phoneBrand1 != null && phoneBrand2 == null){
return 1;
}
return phoneBrand1.compareTo(phoneBrand2);
}
}

A shiny new method to sort the phoneBrand. This time when you test your code, you supply the following input:

List<Teenager> teens = new ArrayList<>();
teens.add(new Teenager(15,"Apple"));
teens.add(new Teenager(16,"Samsung"));
teens.add(new Teenager(14,"Xiaomi"));
teens.add(new Teenager(14,"Samsung"));
teens.add(new Teenager(16,"Apple"));
teens.add(new Teenager(16,null));

Output:

[Age:14 |Phone:Samsung, Age:14 |Phone:Xiaomi, Age:15 |Phone:Apple, Age:16 |Phone:Apple, Age:16 |Phone:Samsung, Age:16 |Phone:null]

The list above is sorted by increasing age. When the age is the same,16, the teenager without the phone gets pushed to the end of the list as if the teenager is not wanted. You check in the code and leave for the day (night?)  and you are still feeling bad that the teenager does not have a phone.

The next morning, the QA does his testing, things are working fine!

QA : Everything is fine now.

Developer : Yes, I know !  [Arrogance is the key, isn’t it ! ]

Is there a chance to refactor your code ? You look at your code and ask yourself if you can refactor that code somehow. The comparator methods do not really look that clean. It is kind of buggy. It is not that clear when nulls starting popping up, should you return a 1 or a -1 ? It is more like trial and error at times. Imagine what happens if you are asked to sort by another field in addition to the 2 fields above? What if we add another field, earphoneBrand. If two teens are aged the same, have the same phone then sort by the brand of the ear phones. What if someone does not have earphones ?

You remember reading about Lambdas in Java 8 and start exploring if they made any changes to the Comparator interface. On further reading, you actually discover a lot and come up with the following analysis:

  1. Comparator present in the java.util.package is a functional interface and hence..
  2. Usage of the anonymous inner class can be replaced with usage of Lambdas.
  3. There are default methods in the Comparator interface now which can also sort objects if they are null.

So let us revisit the problem statement:

Given a list of Teenagers:

a) sort the list by age first

b) if age is the same then sort by phone brand name and

c) if the teenager does not have a phone(null), then it should be kept at the end for that particular age.

Declarative style:

Collections.sort(teens,
Comparator.comparing(Teenager::getAge)
.thenComparing(Teenager::getPhoneBrand,Comparator.nullsLast(Comparator.naturalOrder())));

What ? What is that ? Let us break that down a little bit, following a, b and c above.

Comparator<Teenager> phoneBrandPossibleNulls =
Comparator.comparing(Teenager::getPhoneBrand,Comparator.nullsLast(Comparator.naturalOrder()));
Collections.sort(teens, Comparator.comparing(Teenager::getAge)
.thenComparing(phoneBrandPossibleNulls));

The code on lines 1 and 2 creates a comparator to sort by the phone brand and instructs how to deal with null values. Line 4 combines the two, if the age is the same then compare with the phone brand. The part, Comparator.nullsLast(Comparator.naturalOrder()) means that when we get 2 phoneBrand objects, we can have various combinations. Both null, one of them null and none of them null. The code says, use natural order for sorting phoneBrand fields and if you encounter a null, then put them at the end of the list.

I must admit the code does need a little bit of explanation this time, but it is still expressive enough and probably takes getting used to the function composition part. But the code flows well, any kind of changes to it are quite easy rather than the imperative style. The focus is more on what you want. If you get a new requirement of putting the teenager without a phone at the beginning of the list, all we need to do is modify the code to use Comparator.nullsFirst above. If we need to sort by another field, add another thenComparing().

You are so enthusiastic about Java 8 but then it dawns upon you that your manager has not yet approved that you switch to Java 8 and you are still coding with an old version. Well, send him an email and tell him that you feel like this:

outdated1

 

Well, if you are still stuck with prior versions of Java , there are other libraries to do that. One solution is to use the Google Guava libraries. The Google Guava library has something similar to the Java 8 style of composing functions.It uses chaining of comparators like above.

Collections.sort(teens,new Comparator<Teenager>() {
@Override
public int compare(Teenager t1, Teenager t2)
{
return
ComparisonChain.start()
.compare(t1.getAge(), t2.getAge())
.compare(t1.getPhoneBrand(), t2.getPhoneBrand(),Ordering.natural().nullsLast())
.result();
}
});

This chaining is not very different from the Java 8 style, in fact a few things in the Google Guava library have served as an inspiration to the designers of the JDK library. So if you cannot switch to Java 8 yet, you could use the google guava version, it is much cleaner that writing a comparator of your own with all null checks and other checks.

It has been a couple of days since you checked in the code, things seem quiet, the QA, Business Analyst and the Manager, nobody has turned up at your desk. It means everything is working fine !

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Google photo

You are commenting using your Google account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s