【问题标题】:How can I compare null values using Comparator?如何使用 Comparator 比较空值?
【发布时间】:2009-11-05 18:56:00
【问题描述】:

我有几个Comparators - 一个用于Dates,一个用于小数,一个用于百分比,等等。

起初我的十进制比较器是这样的:

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

生活变得更好了。测试感觉更可靠。然而,我的代码审查员指出,“nulls 呢?”

太好了,所以现在我必须使用 NullPointerException 重复上述操作,或者在方法主体前添加:

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

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

我不是 Java 专家。有没有比 -- gasp -- 复制和粘贴更清洁、更整洁的解决方案?只要记录在案,我是否应该用正确性来换取不复杂性?


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

【问题讨论】:

  • 我想知道为什么您需要比较器来处理所有可能的情况而不会出错。在我看来,您所做的似乎是错误屏蔽,这在大多数情况下都没有帮助。
  • 如果我误解了代码,请原谅我,如果它们都无法解析,你是说 s1 和 s2 相等吗?这似乎有点奇怪......
  • 关于错误屏蔽,这些类用于对 GWT 表中的列进行排序。从用户的角度来看,它需要“足够好”。关于相等性,是的,如果两个字符串都不可解析,则两个字符串都不可比较。因此,它们的不可解析性是相同的。
  • 啊,所以所有具有不可解析值的行都将被组合在一起。明白了。

标签: java refactoring comparator


【解决方案1】:

您正在实施Comparator&lt;String&gt;String 的方法,包括 compareTo 抛出一个 NullPointerException 如果 null 被交给他们,所以你也应该这样做。同样,如果参数的类型阻止比较它们,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;
  }
}

如果您不特别关注异常消息,则可能会丢失“名称”参数。我相信你可以通过应用一些小技巧在这里少写一行或在那里少写一个单词。

你说你需要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 的Comparator

【讨论】:

  • 我喜欢投掷ClassCastException 的建议。但是,这样做会使错误通过我正在执行的Collections.sort() 向上冒泡,并且整个排序失败。这不是向用户显示“排序”结果的理想行为。 (我应该在问题中提到后者。)
  • 您可以在任意点上放任自流。看起来代码的这个特定部分需要复杂性控制,所以也许许可可以转移到其他地方。也许您可以实现 santize(Collection c) 以从集合中删除失败的项目。如果您希望大多数输入格式错误(由比较器实现定义),那么您可以在排序之前对每个集合运行 sanitize。否则,如果您认为格式错误的集合很少见,您可以try 排序,如果遇到异常则运行sanitizesort
【解决方案2】:

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

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

其次,在我看来,如果其中一个不能被视为双精度的表示,那么将两个字符串作为双精度进行比较应该是一个错误。 (对于空值等也是如此。)因此,您应该允许异常传播!我预计这将是一个有争议的观点。

【讨论】:

  • 我很困惑为什么你会认为“一流的功能”会有所帮助。
  • 我同意例外情况,也可能是空值,具体取决于您的场景/用法
  • 如果第一类函数可以在这里提供帮助,你能证明使用单一方法接口和匿名内部类型吗?因为我想知道如何使用它们来解决这个问题:-)
【解决方案3】:

这是我改进比较器的方法:

首先,提取一个转换值的方法。它被重复,多次尝试......捕获总是丑陋 -> 最好尽可能少。

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:s(或者像其他人喜欢说的那样,“实现比较器”):

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

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

【讨论】:

  • 这似乎最适合我的情况。
【解决方案4】:

您可以创建一个实用方法来处理解析并在出现空值或解析异常时返回特定值。

【讨论】:

  • 那些特定的值是什么?
  • 取决于您希望如何进行比较。如果您希望比较仍然有效,或者您只想检查一种情况,则可以返回 Double.MIN_VALUE 之类的东西,返回 null 并检查它,这将表明任何错误。
【解决方案5】:

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

【讨论】:

    【解决方案6】:

    试试这个:

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

    如果你这么认为的话,唯一的问题是空字符串和其他不可解析的字符串都会混合在一起。考虑到好处,这可能没什么大不了的——这为您提供了一个保证正确的比较器,而使用手动编码的比较器,即使是相对简单的比较器,令人惊讶的是,很容易犯下一个破坏的细微错误传递性,或者,嗯,反对称。

    http://google-collections.googlecode.com

    【讨论】:

      【解决方案7】:

      这里似乎混合了两个问题,也许应该将其分解为单独的组件。考虑以下几点:

      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 的使用替换为一些处理 null 的逻辑并使用 Comparable 而不是 Object 用于 c1 和 c2。

      【讨论】:

        【解决方案8】:

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

        据我所知,这是一个用户输入验证的案例。例如,如果您从对话框中获取输入,则确保您拥有一个可解析的字符串的正确位置是 Double、Date 或输入处理程序中的任何内容。确保在用户可以使用 Tab 键、点击“Okay”或同等功能之前它是好的。

        这就是我这么认为的原因:

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

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

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

        这正是我认为应该在 Comparator 之外进行解析的原因,并在它到达您的代码之前处理异常处理。

        【讨论】:

        • 关于第一个,不,我不是想将1.0Two 进行比较——字符串的格式保证是相同的。关于第二个,只要结果一致,哪个更大并不重要。
        • @a 付费书呆子,我知道这不是您要解决的问题。我指出这就是您的代码本质上在做的事情。如果我在示例中使用“1.0”和“茄子”,可能会更明显。我会将我的摘要评论添加到答案的顶部。
        【解决方案9】:

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

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

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

        【讨论】:

        • 恐怕你脱离了上下文,他的问题是比较 2 个字符串,但期望它们代表双打。
        【解决方案10】:

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

        那么你的比较器只需要比较获得的 Double 实例。

        换句话说,重构……

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

        【讨论】:

          【解决方案11】:

          根据您的需求和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;
              }
          
          }
          

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

          干杯。

          【讨论】:

            【解决方案12】:

            所以我改进了 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;  
             }
            }
            

            似乎您可能会发现将 test 重命名为 t1 并将 retVal 重命名为 q 更清晰。

            至于重复模式……嗯。您也许可以使用带有反射的泛型来调用适当的 parseX 方法。似乎那不值得。

            【讨论】:

            • 为什么不用Double的compareTo呢?您的测试值可能会溢出,这对于比较而不是减法来说是不必要的。
            • 出现这种情况是什么情况?该测试 Double.MAX_VALUE - Double.MIN_VALUE 不是最坏的情况吗?解析为大于 0 的 Double.POSITIVE_INFINITY,因此您得到正确答案。授予,它不处理 Double.NaN。但鉴于我们正在抛出 NumberFormatExceptions,我不知道您将如何生成 Double.NaN。
            猜你喜欢
            • 2021-04-29
            • 1970-01-01
            • 1970-01-01
            • 1970-01-01
            • 1970-01-01
            • 1970-01-01
            • 2017-06-26
            • 2011-04-11
            • 2018-08-13
            相关资源
            最近更新 更多