How to handle null compare method arguments in Comparator? - java

I have created an implementation of Comparator<Entity>, but when I use this comparator to sort an Array<Entity>. I will receive an java.lang.NullPointerException, because when I map the entity to a static collections which is already removed. Now my problem is I don't know what to return to skip the compare method.
public class CustomComparator implements Comparator<Entity> {
public int compare(Entity e1, Entity e2) {
if( e1== null || e2 == null) {
return // don't know what to return to skip this method;
}
Vector2 e1Pos = Mapper.transform.get(e1).position;
Vector2 e2Pos = Mapper.transform.get(e2).position;
}
}

You can't "skip" the comparison. What would you expect the sorting code to do? You've got to provide it with a result.
Two options are common:
Throw a NullPointerException to indicate that you just don't support comparing null values. That's explicitly an option in the compare documentation
Decide that null comes before everything else, but is equal to itself
The latter implementation would be something like:
public int compare(Entity e1, Entity e2) {
if (e1 == e2) {
return 0;
}
if (e1 == null) {
return -1;
}
if (e2 == null) {
return 1;
}
Vector2 e1Pos = Mapper.transform.get(e1).position;
Vector2 e2Pos = Mapper.transform.get(e2).position;
return ...;
}

To elaborate on Jon's answer, and answer Ron's question, one should always look at the spec before deciding what to do. In this case it says "Unlike Comparable, a comparator may optionally permit comparison of null arguments, while maintaining the requirements for an equivalence relation." See the comparator API. It elaborates on what is meant. I can't see any other reasonable solution.

Related

Collections.sort() Comparison method violates its general contract in Java [duplicate]

This question already has answers here:
"Comparison method violates its general contract!"
(13 answers)
Closed 4 years ago.
I know that this kind of question has been asked millions of times if not billions, however I couldn't find my answer yet :)
This compare() method doesn't have Long, Double, Float, ..., it only has Date, boolean, and Null checker, however it shows me that contract violation error, can any one help plz?
Collections.sort(users, new Comparator<MiniUser>() {
#Override
public int compare(MiniUser u1, MiniUser u2) {
boolean resComing = checkMatchConditions(u1,user);
boolean resExists = checkMatchConditions(u2,user);
if(Boolean.valueOf(resComing) && Boolean.valueOf(resExists)) {
if(u1.getLastMatchDate() == null){
return -1;
}else if(u2.getLastMatchDate() ==null ){
return 1;
}else if (u1.getLastMatchDate().toInstant().isBefore(u2.getLastMatchDate().toInstant())){
return -1;
}else {
return 1;
}
}
else if (Boolean.valueOf(resComing)) {
return -1;
}
return 1;
}
});
MiniUser.class
public class MiniUser implements Serializable {
String id;
String name;
Date lastMatchDate;
boolean showCompleteName;
//getters, setters
}
checkMatchConditions return boolean based on some calculations
You should start by reading the JavaDoc of Comparator.compare() to understand what that "contract" is:
The implementor must ensure that sgn(compare(x, y)) == -sgn(compare(y,
x)) for all x and y.
In normal terms it says that if "x is greater than y, y must be smaller than x". Sounds obvious, but is not the case in your comparator:
In your case you violate it when two Users have checkMatchConditions false, in which case compare(u1, u2) and compare(u2, u1) both return 1. Hence, there are cases where u1 is greater than u2, and u2 is greater than u1, which is a violation.
Similarely, if both Users have checkMatchConditions true, and their lastMatchDates are both null, they will also violate the contract.
In addition, because you manually try to compare the dates with isBefore, you also return -1 in both cases when two Users have checkMatchConditions true and their lastMatchDates are both equal.
In order to fix this, you should first add a natural language description of how you want the Users to be ordered. Then you can work out the comparator logic.
The error has nothing to do with the Boolean.valueOf() by the way.
Now that you explained how you want to order, have a look at this comparator:
public int compare(MiniUser u1, MiniUser u2)
{
// order by match
boolean u1Matches = checkMatchConditions(u1, user);
boolean u2Matches = checkMatchConditions(u2, user);
if (u1Matches != u2Matches)
{
// put matching ones first
return u1Matches ? -1 : 1;
}
else if (u1Matches)
{
// order by dates
boolean u1HasDate = u1.getLastMatchDate() != null;
boolean u2HasDate = u2.getLastMatchDate() != null;
if (u1HasDate != u2HasDate)
{
// put the ones without date first
return u1HasDate ? 1 : -1;
}
else if (u1HasDate)
{
// order chronologically
return u1.getLastMatchDate().compareTo(u2.getLastMatchDate());
}
else
{
// no dates, no order possible
return 0;
}
}
else
{
// both don't match, no order possible
return 0;
}
}
If I understood your requirements correctly, this should impose a consistent order to your elements. Note how I use Date's compareTo for the date ordering instead of doing it myself, and how I return 0 in case they are "equal" in regards to the order instead of "randomly" returning 1 or -1.
You need to find where sgn(compare(x, y)) == -sgn(compare(y, x)) doesn't hold. I suggest you use brute force to find examples.
Comparator<MiniUser> comp = ...
for (MiniUser a : users) {
for (MiniUser b: users) {
if (a == b) continue;
if (comp.compare(a, b) != -comp.compare(b, a)) {
// print an error message with these two.
}
}
}

Java: HashSet what is the Compare concept?

Coming from a c++ world, I find reading of the HashSet documentation somewhat hard:
https://docs.oracle.com/javase/7/docs/api/java/util/HashSet.html
In c++, you would have:
http://en.cppreference.com/w/cpp/container/set
which in turns points to:
http://en.cppreference.com/w/cpp/concept/Compare
Which makes it obvious the requirement for the type of element handled by a std::set. My question is: What are the requirements for the type (E) of elements maintained by a Set in Java ?
Here is a short example which I fail to understand:
import gdcm.Tag;
import java.util.Set;
import java.util.HashSet;
public class TestTag
{
public static void main(String[] args) throws Exception
{
Tag t1 = new Tag(0x8,0x8);
Tag t2 = new Tag(0x8,0x8);
if( t1 == t2 )
throw new Exception("Instances are identical" );
if( !t1.equals(t2) )
throw new Exception("Instances are different" );
if( t1.hashCode() != t2.hashCode() )
throw new Exception("hashCodes are different" );
Set<Tag> s = new HashSet<Tag>();
s.add(t1);
s.add(t2);
if( s.size() != 1 )
throw new Exception("Invalid size: " + s.size() );
}
}
The above simple code fails with:
Exception in thread "main" java.lang.Exception: Invalid size: 2 at TestTag.main(TestTag.java:42)
From my reading of the documentation only the equals operator needs to be implemented for Set:
https://docs.oracle.com/javase/7/docs/api/java/util/Set.html
What am I missing from the documentation ?
I just tried to reproduce your issue, and maybe you just didn't override equals and/or hashSet correctly.
Take a look at my incorrect implemenation of Tag:
public class Tag {
private int x, y;
public Tag(int x, int y) {
this.x = x;
this.y = y;
}
public boolean equals(Tag tag) {
if (x != tag.x) return false;
return y == tag.y;
}
#Override
public int hashCode() {
int result = x;
result = 31 * result + y;
return result;
}
}
Looks quite ok doesn't it? But the problem is, I actually do not override the correct equals method, I overloaded it with my own implementation.
To work correctly, equals has to look like this:
#Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Tag tag = (Tag) o;
if (x != tag.x) return false;
return y == tag.y;
}
What am I missing from the documentation ?
You are looking at the wrong part of the documentation.
The C++ set is an "sorted set of unique objects", and are "usually implemented as red-black trees."
In Java, Set is a more abstract concept (it's an interface, not a class) with multiple implementations, most notably the HashSet and the TreeSet (ignoring concurrent implementations).
As you can probably guess from the name alone, the Java TreeSet is the equivalent of the C++ set.
As for requirements, HashSet uses the hashCode() and equals() methods. They are defined on the Object class, and needs to be overridden on classes that needs to be in a HashSet or as keys in a HashMap.
For TreeSet and keys of TreeMap, you have two options: Provide a Comparator when creating the TreeSet (similar to C++), or have the objects implement the Comparable interface.
I guess this was simply a combination of bad luck and misunderstanding of HashSet requirement. Thanks to #christophe for help, I realized the issue when I tried adding in my swig generated Tag.java class:
#Override
public boolean equals(Object o) {
}
I got the following error message:
gdcm/Tag.java:78: error: method does not override or implement a method from a supertype
#Override
^
1 error
1 warning
Which meant my error was simply:
I had the wrong signature in the first place: boolean equals(Object o) != boolean equals(Tag t)
The hint was simply to use the #Override keyword.
For those asking for the upstream code, the Java code is generated by swig. The original c++ code is here:
https://github.com/malaterre/GDCM/blob/master/Source/DataStructureAndEncodingDefinition/gdcmTag.h

sort list of objects based on field which can be null

How to to sort list of objects based on field which can be null?
I am trying to do it in following way using Comparator interface and collections sort method.
The class is CustomClass and the field on which sorting is to be done is createDate
Comparator comparator=new Comparator<CustomClass>(){
public int compare(CustomClass o1, CustomClass o2) {
if(o1.getCreateDate()==null && o2.getCreateDate()==null){
return 0;
}
else if(o1.getCreateDate()==null && o2.getCreateDate()!=null){
return 1;
}
else if(o1.getCreateDate()!=null && o2.getCreateDate()==null){
return -1;
}
else{
if(o1.getCreateDate().equals(o2.getCreateDate())){
return 0;
}
else if(o1.getCreateDate().after(o2.getCreateDate())){
return 1;
}
else{
return -1;
}
}
}
};
Is there a better way to do it?
If you're willing to use Google Guava, then you can use ComparisonChain and Ordering to make things more succinct.
public int compare(CustomClass o1, CustomClass o2)
{
return ComparisonChain.start()
.compare(o1.getCreateDate(), o2.getCreateDate(), Ordering.natural().nullsLast())
.result();
}
Assuming getCreateDate() returns an instance of java.util.Date, you can clean up the code a bit. The compareTo's method's contract specifies that it throws a NullPointerException if a date is compared to null (like any Comparable class should do), so you'll have to handle those directly. However, if both are non-null, you shouldn't reimplement the comparing logic, but rely on Date's implementation:
Comparator<CustomClass> comparator = new Comparator<CustomClass>(){
public int compare(CustomClass o1, CustomClass o2) {
if (o1.getCreateDate() == null && o2.getCreateDate() == null) {
return 0;
}
else if (o1.getCreateDate() == null && o2.getCreateDate() != null) {
return 1;
}
else if (o1.getCreateDate() != null && o2.getCreateDate() == null) {
return -1;
}
else{
// Just use java.util.Date's logic:
return o1.getCreateDate().compareTo(o2.getCreateDate());
}
}
};
EDIT:
This is quite an old question, but just to complete the picture, in Java 8, the Comparator class itself can do the heavy lifting for you:
Comparator<CustomClass> comparator =
Comparator.comparing(CustomClass::getCreateDate,
Comparator.nullsLast(Comparator.naturalOrder()));
You do need all of that to ensure that the null values always sort to the end of the list and otherwise you sort by date. You could simplify your code a bit by delegating to the built-in compareTo or by using a declarative Guava Ordering.

Treeset.contains() problem

So I've been struggling with a problem for a while now, figured I might as well ask for help here.
I'm adding Ticket objects to a TreeSet, Ticket implements Comparable and has overridden equals(), hashCode() and CompareTo() methods. I need to check if an object is already in the TreeSet using contains(). Now after adding 2 elements to the set it all checks out fine, yet after adding a third it gets messed up.
running this little piece of code after adding a third element to the TreeSet, Ticket temp2 is the object I'm checking for(verkoopLijst).
Ticket temp2 = new Ticket(boeking, TicketType.STANDAARD, 1,1);
System.out.println(verkoop.getVerkoopLijst().first().hashCode());
System.out.println(temp2.hashCode());
System.out.println(verkoop.getVerkoopLijst().first().equals(temp2));
System.out.println(verkoop.getVerkoopLijst().first().compareTo(temp2));
System.out.println(verkoop.getVerkoopLijst().contains(temp2));
returns this:
22106622
22106622
true
0
false
Now my question would be how this is even possible?
Edit:
public class Ticket implements Comparable{
private int rijNr, stoelNr;
private TicketType ticketType;
private Boeking boeking;
public Ticket(Boeking boeking, TicketType ticketType, int rijNr, int stoelNr){
//setters
}
#Override
public int hashCode(){
return boeking.getBoekingDatum().hashCode();
}
#Override
#SuppressWarnings("EqualsWhichDoesntCheckParameterClass")
public boolean equals(Object o){
Ticket t = (Ticket) o;
if(this.boeking.equals(t.getBoeking())
&&
this.rijNr == t.getRijNr() && this.stoelNr == t.getStoelNr()
&&
this.ticketType.equals(t.getTicketType()))
{
return true;
}
else return false;
}
/*I adjusted compareTo this way because I need to make sure there are no duplicate Tickets in my treeset. Treeset seems to call CompareTo() to check for equality before adding an object to the set, instead of equals().
*/
#Override
public int compareTo(Object o) {
int output = 0;
if (boeking.compareTo(((Ticket) o).getBoeking())==0)
{
if(this.equals(o))
{
return output;
}
else return 1;
}
else output = boeking.compareTo(((Ticket) o).getBoeking());
return output;
}
//Getters & Setters
On compareTo contract
The problem is in your compareTo. Here's an excerpt from the documentation:
Implementor must ensure sgn(x.compareTo(y)) == -sgn(y.compareTo(x)) for all x and y.
Your original code is reproduced here for reference:
// original compareTo implementation with bug marked
#Override
public int compareTo(Object o) {
int output = 0;
if (boeking.compareTo(((Ticket) o).getBoeking())==0)
{
if(this.equals(o))
{
return output;
}
else return 1; // BUG!!!! See explanation below!
}
else output = boeking.compareTo(((Ticket) o).getBoeking());
return output;
}
Why is the return 1; a bug? Consider the following scenario:
Given Ticket t1, t2
Given t1.boeking.compareTo(t2.boeking) == 0
Given t1.equals(t2) return false
Now we have both of the following:
t1.compareTo(t2) returns 1
t2.compareTo(t1) returns 1
That last consequence is a violation of the compareTo contract.
Fixing the problem
First and foremost, you should have taken advantage of the fact that Comparable<T> is a parameterizable generic type. That is, instead of:
// original declaration; uses raw type!
public class Ticket implements Comparable
it'd be much more appropriate to instead declare something like this:
// improved declaration! uses parameterized Comparable<T>
public class Ticket implements Comparable<Ticket>
Now we can write our compareTo(Ticket) (no longer compareTo(Object)). There are many ways to rewrite this, but here's a rather simplistic one that works:
#Override public int compareTo(Ticket t) {
int v;
v = this.boeking.compareTo(t.boeking);
if (v != 0) return v;
v = compareInt(this.rijNr, t.rijNr);
if (v != 0) return v;
v = compareInt(this.stoelNr, t.stoelNr);
if (v != 0) return v;
v = compareInt(this.ticketType, t.ticketType);
if (v != 0) return v;
return 0;
}
private static int compareInt(int i1, int i2) {
if (i1 < i2) {
return -1;
} else if (i1 > i2) {
return +1;
} else {
return 0;
}
}
Now we can also define equals(Object) in terms of compareTo(Ticket) instead of the other way around:
#Override public boolean equals(Object o) {
return (o instanceof Ticket) && (this.compareTo((Ticket) o) == 0);
}
Note the structure of the compareTo: it has multiple return statements, but in fact, the flow of logic is quite readable. Note also how the priority of the sorting criteria is explicit, and easily reorderable should you have different priorities in mind.
Related questions
What is a raw type and why shouldn't we use it?
How to sort an array or ArrayList ASC first by x and then by y?
Should a function have only one return statement?
This could happen if your compareTo method isn't consistent. I.e. if a.compareTo(b) > 0, then b.compareTo(a) must be < 0. And if a.compareTo(b) > 0 and b.compareTo(c) > 0, then a.compareTo(c) must be > 0. If those aren't true, TreeSet can get all confused.
Firstly, if you are using a TreeSet, the actual behavior of your hashCode methods won't affect the results. TreeSet does not rely on hashing.
Really we need to see more code; e.g. the actual implementations of the equals and compareTo methods, and the code that instantiates the TreeSet.
However, if I was to guess, it would be that you have overloaded the equals method by declaring it with the signature boolean equals(Ticket other). That would lead to the behavior that you are seeing. To get the required behavior, you must override the method; e.g.
#Override
public boolean equals(Object other) { ...
(It is a good idea to put in the #Override annotation to make it clear that the method overrides a method in the superclass, or implements a method in an interface. If your method isn't actually an override, then you'll get a compilation error ... which would be a good thing.)
EDIT
Based on the code that you have added to the question, the problem is not overload vs override. (As I said, I was only guessing ...)
It is most likely that the compareTo and equals are incorrect. It is still not entirely clear exactly where the bug is because the semantics of both methods depends on the compareTo and equals methods of the Boeking class.
The first if statement of the Ticket.compareTo looks highly suspicious. It looks like the return 1; could cause t1.compareTo(t2) and t2.compareTo(t1) to both return 1 for some tickets t1 and t2 ... and that would definitely be wrong.

How can I compare null values using Comparator?

I've got a few Comparators -- one for Dates, one for decimals, one for percentages, etc.
At first my decimal comparator looked like this:
class NumericComparator implements Comparator<String> {
#Override
public int compare(String s1, String s2) {
final Double i1 = Double.parseDouble(s1);
final Double i2 = Double.parseDouble(s2);
return i1.compareTo(i2);
}
}
Life was simple. Of course, this doesn't handle the case where the strings aren't parseable. So I improved compare():
class NumericComparator implements Comparator<String> {
#Override
public int compare(String s1, String s2) {
final Double i1;
final Double i2;
try {
i1 = Double.parseDouble(s1);
} catch (NumberFormatException e) {
try {
i2 = Double.parseDouble(s2);
} catch (NumberFormatException e2) {
return 0;
}
return -1;
}
try {
i2 = Double.parseDouble(s2);
} catch (NumberFormatException e) {
return 1;
}
return i1.compareTo(i2);
}
}
Life was better. Tests felt more solid. However, my code reviewer pointed out, "What about nulls?"
Great, so now I have to repeat the above with NullPointerException or prepend the method body with:
if (s1 == null) {
if (s2 == null) {
return 0;
} else {
return -1;
}
} else if (s2 == null) {
return 1;
}
This method is huge. The worst part is, I need to repeat this pattern with three other classes which compare different types of strings and could raise three other exceptions while parsing.
I'm not a Java expert. Is there a cleaner, neater solution than -- gasp -- copying and pasting? Should I trade correctness for lack of complexity so as long as it is documented?
Update: Some have suggested that it's not the Comparator's job to handle null values. Since the sort results are displayed to users I indeed want nulls to be sorted consistently.
You are implementing a Comparator<String>. String's methods, including compareTo throw a NullPointerException if a null is handed in to them, so you should too. Similarly, Comparator throws a ClassCastException if the arguments' types prevent them from being compared. I would recommend you implement these inherited behaviors.
class NumericComparator implements Comparator<String> {
public int compare(String s1, String s2) {
final Double i1;
final Double i2;
if(s1 == null)
{
throw new NullPointerException("s1 is null"); // String behavior
}
try {
i1 = Double.parseDouble(s1)
} catch (NumberFormatException e) {
throw new ClassCastException("s1 incorrect format"); // Comparator behavior
}
if(s2 == null)
{
throw new NullPointerException("s2 is null"); // String behavior
}
try {
i2 = Double.parseDouble(s1)
} catch (NumberFormatException e) {
throw new ClassCastException("s2 incorrect format"); // Comparator behavior
}
return i1.compareTo(i2);
}
}
You can almost regain the original elegance by extracting a method to do the type checking and conversion.
class NumericComparator implements Comparator<String> {
public int compare(String s1, String s2) {
final Double i1;
final Double i2;
i1 = parseStringAsDouble(s1, "s1");
i2 = parseStringAsDouble(s2, "s2");
return i1.compareTo(i2);
}
private double parseStringAsDouble(String s, String name) {
Double i;
if(s == null) {
throw new NullPointerException(name + " is null"); // String behavior
}
try {
i = Double.parseDouble(s1)
} catch (NumberFormatException e) {
throw new ClassCastException(name + " incorrect format"); // Comparator behavior
}
return i;
}
}
If you are not particular about the Exception messages, you can lose the "name" parameter. I'm sure you can lose an extra line here or word there by applying little tricks.
You say you need to repeat this pattern with three other classes which compare different types of strings and could raise three other exceptions. It's difficult to offer specifics there without seeing the situation, but you may be able to use "Pull Up Method" on a version of my parseStringAsDouble into a common ancestor of NumericComparator that itself implements java's Comparator.
There are a lot of subjective answers to this question. Here's my own $.02.
First, the trouble you're describing is the canonical symptom of a language that lacks first-class functions, which would enable you to succinctly describe these patterns.
Second, in my opinion, it should be an error to compare two Strings as Doubles if one of them cannot be considered a representation of a double. (The same is true for nulls, etc.) Therefore, you should permit the exceptions to propagate! This will be a contentious opinion, I expect.
Here's how I'd improve the comparator:
First, exctract a method for converting the value. It's being repeated, multiple try...catches are always ugly -> better to have as few of them as possible.
private Double getDouble(String number) {
try {
return Double.parseDouble(number);
} catch(NumberFormatException e) {
return null;
}
}
Next, write down simple rules to show how you want the flow of the comparator to be.
if i1==null && i2!=null return -1
if i1==null && i2==null return 0
if i1!=null && i2==null return 1
if i1!=null && i2!=null return comparison
Finally do horrible obfuscation to the actual comparator to raise a few WTF:s in code review (or like others like to say it, "Implement the Comparator"):
class NumericComparator implements Comparator<String> {
public int compare(String s1, String s2) {
final Double i1 = getDouble(s1);
final Double i2 = getDouble(s2);
return (i1 == null) ? (i2 == null) ? 0 : -1 : (i2 == null) ? 1 : i1.compareTo(i2);
}
private Double getDouble(String number) {
try {
return Double.parseDouble(number);
} catch(NumberFormatException e) {
return null;
}
}
}
...yes, that's a branching nested ternary. If anyone complains about it, say what others here have been saying: Handling nulls isn't Comparator's job.
You could create a utility method that handles parsing and returns a certain value in the case of nulls or parse exceptions.
Take a step back. Where does those Strings come from? For what is this Comparator to be used? Do you have a Collection of Strings which you would like to sort or so?
Try this:
import com.google.common.base.Function;
import com.google.common.collect.Ordering;
Ordering.nullsFirst().onResultOf(
new Function<String, Double>() {
public Double apply(String s) {
try {
return Double.parseDouble(s);
} catch (NumberFormatException e) {
return null;
}
})
The only problem, if it you consider it that, is that null Strings and other non-parseable Strings will all be intermingled. That's probably not a big deal, considering the benefits -- this gives you a comparator that is guaranteed to be correct, whereas with a hand-coded comparator, even relatively simple ones, it's amazing how easy it is to commit a subtle error that breaks transitivity or, umm, antisymmetricity.
http://google-collections.googlecode.com
It seems that there are two concerns being mixed here and maybe should be broken up into separate components. Consider the following:
public class ParsingComparator implements Comparator<String> {
private Parser parser;
public int compare(String s1, String s2) {
Object c1 = parser.parse(s1);
Object c2 = parser.parse(s2);
new CompareToBuilder().append(c1, c2).toComparison();
}
}
The Parser interface would have implementations for numbers, dates, etc. You could potentially use the java.text.Format class for your Parser interface. If you don't want to use commons-lang, you could replace the use of CompareToBuilder with some logic to handle nulls and use Comparable instead of Object for c1 and c2.
tl;dr: Take guidance from the JDK. The Double comparator is not defined for either non-numbers or nulls. Make people give you useful data (Doubles, Dates, Dinosaurs, whatever) and write your comparators for that.
As near as I can tell, this is a case of user input validation. For example, if you are taking input from a dialog box, the correct place to ensure that you have a parseable String that is a Double, Date or whatever is in the input handler. Make sure it's good before the user can tab away, hit "Okay" or equivalent.
Here's why I think this:
First question: if the Strings aren't parseable as numbers, I think you're trying to solve the problem in the wrong place. Say, for instance, I try to compare "1.0" to "Two". The second is clearly not parseable as a Double but is it less than the first? Or is it greater. I would argue that the users should have to turn their Strings into Doubles before they ask your which is greater (which you can easily answer with Double.compareTo, for instance).
Second question: if the Strings are "1.0" and null, which is greater? The JDK source doesn't handle NullPointerExceptions in the Comparator: if you give it a null, autoboxing will fail.
The worst part is, I need to repeat
this pattern with three other classes
which compare different types of
strings and could raise three other
exceptions while parsing.
Exactly why I would argue that the parsing should happen outside your Comparator with exception-handling dealt with before it arrives at your code.
If you are able to change the signature I would suggest you write the method so that it can accept any supported Object.
public int compare(Object o1, Object o2) throws ClassNotFoundException {
String[] supportedClasses = {"String", "Double", "Integer"};
String j = "java.lang.";
for(String s : supportedClasses){
if(Class.forName(j+s).isInstance(o1) && Class.forName(j+s).isInstance(o1)){
// compare apples to apples
return ((Comparable)o1).compareTo((Comparable)o2);
}
}
throw new ClassNotFoundException("Not a supported Class");
}
You might even define it recursively where you cast your Strings to Doubles and then return the result of calling itself with those objects.
IMHO you should first create a method that returns a Double from a String, embedding the null and parsing failure cases (but you must define what to do in such cases : throw an exception ? return a default value ??).
Then your comparator just have to compare obtained Double instances.
In other words, refactoring...
But I still wonder why you need to compare strings though expecting they represent doubles. I mean, what prevents you from manipulating doubles in the code that would actually use this comparator ?
according to your needs and Ewan's post, I think there's a way to extract the structure that you can reuse:
class NumericComparator implements Comparator<String> {
private SafeAdaptor<Double> doubleAdaptor = new SafeAdaptor<Double>(){
public Double parse(String s) {
return Double.parseDouble(s);
}
};
public int compare(String s1, String s2) {
final Double i1 =doubleAdaptor.getValue(s1, "s1");
final Double i2 = doubleAdaptor.getValue(s2, "s2");
return i1.compareTo(i2);
}
}
abstract class SafeAdaptor<T>{
public abstract T parse(String s);
public T getValue(String str, String name) {
T i;
if (str == null) {
throw new NullPointerException(name + " is null"); // String
}
try {
i = parse(str);
} catch (NumberFormatException e) {
throw new ClassCastException(name + " incorrect format"); // Comparator
}
return i;
}
}
I extract the method as an abstract class which can be reuse in other cases(although the class name is suck).
cheers.
So I improved compare()...
sure you did.
first, the Comparator interface doesn't specify what happens with nulls. if your null checking if statement works for your use case, that's great, but the general solution is throwing an npe.
as to cleaner... why final? why all the catch/throws? why use compareTo for a primitive wrapper?
class NumericComparator implements Comparator<String> {
public int compare(String s1, String s2) throws NullPointerException, NumberFormatException {
double test = Double.parseDouble(s1) - Double.parseDouble(s2);
int retVal = 0;
if (test < 0) retVal = -1;
else if (test > 0) retVal = 1;
return retVal;
}
}
seems you might find it clearer renaming test to t1 and retVal to q.
as to repeating the pattern... eh. you might be able to use generics with reflection to invoke appropriate parseX methods. seems like that'd not be worth it though.

Categories

Resources