如何使用Comparator比较空值?

时间:2009-11-05 18:56:01

标签: java refactoring comparator

我有一些Comparator s - 一个用于Date s,一个用于小数,一个用于百分比等。

起初我的小数比较器看起来像这样:

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);
  }

}

生活很简单。当然,这不处理字符串不可解析的情况。所以我改进了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);
  }
}

生活更美好。测试感觉更稳固。但是,我的代码审核员指出,“null s怎么样?”

很好,所以现在我必须用NullPointerException重复上述内容,或者在方法主体前加上:

if (s1 == null) {
  if (s2 == null) {
    return 0;
  } else {
    return -1;
  }
} else if (s2 == null) {
  return 1;
}

这种方法很庞大。最糟糕的是,我需要用三个其他类重复这个模式,它比较不同类型的字符串,并且可以在解析时引发三个其他异常

我不是Java专家。是否有一个更清洁,更整洁的解决方案 - gasp - 复制和粘贴?我是否应该因缺乏复杂性而交换正确性,只要有文件记录?


更新:有人建议处理Comparator值不是null的工作。由于排序结果显示给用户,我确实希望空值一致

12 个答案:

答案 0 :(得分:4)

您正在实施Comparator<String>String的方法,包括compareTo如果将null传递给NullPointerException,那么你也应该这样做。同样,如果参数的类型阻止它们被比较,Comparator会抛出ClassCastException。我建议你实现这些继承的行为。

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);
  }
}

您可以通过extracting a method 几乎恢复原始的优雅,进行类型检查和转换。

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;
  }
}

如果您不特别关注异常消息,则可能会丢失“name”参数。我相信你可以在这里输掉额外的一行,或者通过应用小技巧来说明。

你说你需要repeat this pattern with three other classes which compare different types of strings and could raise three other exceptions。在没有看到情况的情况下很难在那里提供细节,但是你可以在我的parseStringAsDouble版本上使用"Pull Up Method"NumericComparator的共同祖先,它本身实现了java {{1} }}

答案 1 :(得分:2)

这个问题有很多主观的答案。这是我自己的$ .02。

首先,您所描述的问题是缺乏一流功能的语言的规范症状,这将使您能够简洁地描述这些模式。

其次,在我看来,如果其中一个字符串不能被视为double的表示,那么将两个字符串比作双打是错误的。 (对于null等也是如此)因此,您应该允许异常传播!我希望,这将是一个有争议的意见。

答案 2 :(得分:1)

您可以创建一个处理解析的实用程序方法,并在空值或解析异常的情况下返回某个值。

答案 3 :(得分:1)

退后一步。那些Strings来自哪里?这个Comparator的用途是什么?你有Collection Strings你想要排序吗?

答案 4 :(得分:1)

tl; dr:从JDK获取指导。 Double比较器未定义为非数字或空值。让人们为您提供有用的数据(双打,日期,恐龙,等等)并为此编写比较器。

尽我所知,这是一个用户输入验证的案例。例如,如果您从对话框中获取输入,则确保您具有可解析的字符串,该字符串是Double,Date或输入处理程序中的任何内容。在用户可以选择之前确定它是好的,点击“Okay”或等效。

这就是为什么我认为这个:

第一个问题:如果字符串不能作为数字解析,我认为你试图在错误的地方解决问题。比如说,我尝试将"1.0""Two"进行比较。第二个显然不能解析为Double,但它是否比第一个少?或者它更大。我认为用户应该在他们问你哪个更大之前将他们的Strings变成双打(例如你可以用Double.compareTo轻松回答)。

第二个问题:如果字符串是"1.0"null,哪个更大? JDK源代码不处理Comparator中的NullPointerExceptions:如果给它一个null,则autoboxing将失败。

  

最糟糕的是,我需要重复一遍   这个模式与其他三个类   比较不同类型的   字符串,可以提出另外三个   解析时的异常。

正是为什么我认为解析应该在比较器之外进行,异常处理在它到达你的代码之前处理。

答案 5 :(得分:1)

试试这个:

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;
      }
    })

唯一的问题,如果您认为是这样的话,那么null字符串和其他不可解析的字符串将全部混合在一起。这可能不是什么大不了的,考虑到它的好处 - 这给你一个保证正确的比较器,而使用手动编码的比较器,即使是相对简单的比较器,令人惊讶的是提交一个细微的错误是多么容易传递性,或者,反对称性。

http://google-collections.googlecode.com

答案 6 :(得分:1)

以下是我改进比较器的方法:

首先,提取转换值的方法。这是重复的,多次尝试......捕获总是很难看 - &gt;最好尽量少用它们。

private Double getDouble(String number) {
 try {
  return Double.parseDouble(number);
 } catch(NumberFormatException e) {
  return null;
 }
}

接下来,写下简单的规则来说明你希望比较器的流程如何。

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

最后对实际的比较器进行了可怕的混淆,以便在代码审查中引发一些WTF:或像其他人喜欢说的那样,“实施比较器”

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;
          }
     }
}

...是的,这是一个分支嵌套的三元组。如果有人抱怨它,请说出其他人在说什么:处理空值不是比较者的工作。

答案 7 :(得分:1)

似乎有两个问题在这里混合,也许应该分解成单独的组件。请考虑以下事项:

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();
  }
}

Parser接口可以实现数字,日期等。您可以使用java.text.Format类作为Parser接口。如果你不想使用commons-lang,你可以用一些逻辑来替换CompareToBuilder来处理空值,并使用Comparable而不是Object来表示c1和c2。

答案 8 :(得分:0)

如果您能够更改签名,我建议您编写该方法,以便它可以接受任何受支持的对象。

  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");
  }

您甚至可以递归地将字符串转换为双打,然后返回使用这些对象调用自身的结果。

答案 9 :(得分:0)

恕我直言你应该首先创建一个从String返回Double的方法,嵌入null并解析失败的情况(但你必须定义在这种情况下要做什么:抛出异常?返回默认值?)。 / p>

然后你的比较器只需比较获得的Double实例。

换句话说,重构......

但我仍然想知道为什么你需要比较字符串,虽然期望它们代表双打。我的意思是,什么阻止你在实际使用这个比较器的代码中操纵双打?

答案 10 :(得分:0)

根据您的需求和Ewan的帖子,我认为有一种方法可以提取您可以重复使用的结构:

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;
    }

}

我将该方法提取为抽象类,在其他情况下可以重用(尽管类名很糟糕)。

欢呼声。

答案 11 :(得分:-3)

  

所以我改进了compare()...

确定你做到了。

首先,Comparator接口没有指定null的内容。如果您的null检查if语句是否适用于您的用例,那很好,但一般的解决方案是抛出一个npe。

至于清洁......为什么最后?为什么所有的捕获/抛出?为什么使用compareTo作为原始包装器?

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;  
 }
}

您似乎可以更清楚地将测试重命名为 t1 ,将 retVal 重命名为 q

重复模式...呃。您可以使用带有反射的泛型来调用适当的 parseX 方法。虽然看起来不值得。