views:

1549

answers:

5

I'm implementing compareTo() method for a simple class such as this (to be able to use Collections.sort() and other goodies offered by the Java platform):

public class Metadata implements Comparable<Metadata> {
    private String name;
    private String value;

// Imagine basic constructor and accessors here
// Irrelevant parts omitted
}

I want the natural ordering for these objects to be: 1) sorted by name and 2) sorted by value if name is the same; both comparisons should be case-insensitive. For both fields null values are perfectly acceptable, so compareTo must not break in these cases.

The solution that springs to mind is along the lines of the following (I'm using "guard clauses" here while others might prefer a single return point, but that's beside the point):

// primarily by name, secondarily by value; null-safe; case-insensitive
public int compareTo(Metadata other) {
    if (this.name == null && other.name != null){
        return -1;
    }
    else if (this.name != null && other.name == null){
        return 1;
    }
    else if (this.name != null && other.name != null) {
        int result = this.name.compareToIgnoreCase(other.name);
        if (result != 0){
            return result;
        }
    }

    if (this.value == null) {
        return other.value == null ? 0 : -1;
    }
    if (other.value == null){
        return 1;
    }

    return this.value.compareToIgnoreCase(other.value);
}

This does the job, but I'm not perfectly happy with this code. Admittedly it isn't very complex, but is quite verbose and tedious.

The question is, how would you make this less verbose (while retaining the functionality)? Feel free to refer to Java standard libraries or Apache Commons if they help. Would the only option to make this (a little) simpler be to implement my own "NullSafeStringComparator", and apply it for comparing both fields?

Edits 1-3: Eddie's right; fixed the "both names are null" case above

A: 

You could design your class to be immutable (Effective Java 2nd Ed. has a great section on this, Item 15: Minimize mutability) and make sure upon construction that no nulls are possible (and use the null object pattern if needed). Then you can skip all those checks and safely assume the values are not null.

Fabian Steeg
Yes, that's generally a good solution, and simplifies many things - but here I was more interested in the case where null values are allowed, for one reason or another, and must be taken into account :)
Jonik
+5  A: 
Eddie
Your code is a little bit botched: final is not necessary here, the first character of the String class is capitalized in Java. Furthermore, return 1:-1 is not valid Java. I'd suggest a nested if (one == null) {if two == null) return 0; else return -1;}else{if (two == null) return 0; else return 1;}
phihag
Yup. I should have tested it first. Now it's tested. :)
Eddie
@Eddie: sorry about my edit. I decided to fix the issues @phihag mentioned, but our edits conflicted. I've rolled back to your revision.
A. Rex
Regarding nested "if" ... I find the nested if to be less readable for this case, so I avoid it. Yes, sometimes there will be an unnecessary comparison due to this. final for parameters is not necessary, but is a good idea.
Eddie
@A. Rex, I noticed. No worries. Thanks for trying to help out.
Eddie
Or return one==null ? (two==null ? 0 :-1) : two==null ? -1 : one.compareToIgnoreCase(two);
Peter Lawrey
@Peter Lawrey: Yes, but that is less readable. And with a good optimizer, there should be no real speed difference between the optimized and the readable version.
Eddie
+2  A: 

You can extract method:

public int cmp(String txt, String otherTxt)
{
    if ( txt == null )
        return otjerTxt == null ? 0 : 1;

    if ( otherTxt == null )
          return 1;

    return txt.compareToIgnoreCase(otherTxt);
}

public int compareTo(Metadata other) {
   int result = cmp( name, other.name); 
   if ( result != 0 )  return result;
   return cmp( value, other.value);

}

Yoni Roit
+3  A: 

I always recommend using Apache commons since it will most likely be better than one you can write on your own. Plus you can then do 'real' work rather then reinventing.

The class you are interested in is the Null Comparator. It allows you to make nulls high or low. You also give it your own comparator to use when the two values are not null.

In your case you can have a static member variable that does the comparison and then your compareTo method just references that.

Somthing like

class Metadata implements Comparable<Metadata> {
private String name;
private String value;

static NullComparator nullAndCaseInsensitveComparator = new NullComparator(
  new Comparator<String>() {

   @Override
   public int compare(String o1, String o2) {
    // inputs can't be null
    return o1.compareToIgnoreCase(o2);
   }

  });

@Override
public int compareTo(Metadata other) {
 if (other == null) {
  return 1;
 }
 int res = nullAndCaseInsensitveComparator.compare(name, other.name);
 if (res != 0)
  return res;

 return nullAndCaseInsensitveComparator.compare(value, other.value);
}

}

Even if you decide to roll your own, keep this class in mind since it is very useful when ordering lists thatcontain null elements.

Patrick
Thanks, I was sort of hoping there would be something like this in Commons! In this case, however, I didn't end up using it: http://stackoverflow.com/questions/481813/how-to-simplify-a-null-safe-compareto-implementation/500643#500643
Jonik
Just realised your approach can be simplified by using String.CASE_INSENSITIVE_ORDER; see my edited follow-up answer.
Jonik
+1  A: 

This is what I ultimately went with. It turned out we already had a utility method for null-safe String comparison, so the simplest solution was to make use of that. (It's a big codebase; easy to miss this kind of thing :)

public int compareTo(Metadata other) {
    int result = StringUtils.compare(this.getName(), other.getName(), true);
    if (result != 0) {
        return result;
    }
    return StringUtils.compare(this.getValue(), other.getValue(), true);
}

This is how the helper is defined (it's overloaded so that you can also define whether nulls come first or last, if you want):

public static int compare(String s1, String s2, boolean ignoreCase)

So this is essentially the same as Eddie's answer (although I wouldn't call a static helper method a comparator) and that of uzhin too.

Anyway, in general, I would have strongly favoured Patrick's solution, as I think it's a good practice to use established libraries whenever possible. (Know and use the libraries as Josh Bloch says.) But in this case that would not have yielded the cleanest, simplest code.


Edit: Actually, here's a way to make the solution based on Apache Commons NullComparator simpler. Combine it with the case-insensitive Comparator provided in String class:

static Comparator<String> nullSafeComparator 
    = new NullComparator(String.CASE_INSENSITIVE_ORDER);

private int compareTo(Metadata other) {
    int result = nullSafeComparator.compare(this.name, other.name);
    if (result != 0){
        return result;
    }
    return nullSafeComparator.compare(this.value, other.value);
}

Now this is pretty elegant, I think. (Just one small issue remains: the Commons NullComparator doesn't support generics, so there's an unchecked assignment.)

Jonik
String.CASE_INSENSITIVE_ORDER really does make the solution much cleaner. Nice update.
Patrick