Java 如何简化空安全 compareTo() 实现?

声明:本页面是StackOverFlow热门问题的中英对照翻译,遵循CC BY-SA 4.0协议,如果您需要使用它,必须同样遵循CC BY-SA许可,注明原文地址和作者信息,同时你必须将它归于原作者(不是我):StackOverFlow 原文地址: http://stackoverflow.com/questions/481813/
Warning: these are provided under cc-by-sa 4.0 license. You are free to use/share it, But you must attribute it to the original authors (not me): StackOverFlow

提示:将鼠标放在中文语句上可以显示对应的英文。显示中英文
时间:2020-08-11 15:13:08  来源:igfitidea点击:

How to simplify a null-safe compareTo() implementation?

javarefactoringcomparisonnullcompareto

提问by Jonik

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):

我正在compareTo()为这样的简单类实现方法(为了能够使用Collections.sort()Java 平台提供的其他好东西):

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 orderingfor 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 compareTomust not break in these cases.

我希望这些对象的自然顺序是:1)按名称排序,2)如果名称相同,则按值排序;两种比较都应该不区分大小写。对于这两个字段,空值是完全可以接受的,因此compareTo在这些情况下不能中断。

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 verycomplex, 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?

问题是,您将如何使其不那么冗长(同时保留功能)?如果有帮助,请随时参考 Java 标准库或 Apache Commons。使这个(稍微)更简单的唯一选择是实现我自己的“NullSafeStringComparator”,并将其应用于比较两个字段?

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

编辑 1-3:埃迪是对的;修复了上面的“两个名字都为空”的情况

About the accepted answer

关于接受的答案

I asked this question back in 2009, on Java 1.6 of course, and at the time the pure JDK solution by Eddiewas my preferred accepted answer. I never got round to changing that until now (2017).

我在 2009 年问过这个问题,当然是在 Java 1.6 上,当时Eddie 的纯 JDK 解决方案是我首选的公认答案。直到现在(2017 年),我才开始改变它。

There are also 3rd party library solutions—a 2009 Apache Commons Collections one and a 2013 Guava one, both posted by me—that I did prefer at some point in time.

还有3rd 方库解决方案——一个 2009 年的 Apache Commons Collections 一个和一个 2013 年的 Guava 一个,都是我发布的——我在某个时间点确实更喜欢。

I now made the clean Java 8 solution by Lukasz Wiktorthe accepted answer. That should definitely be preferred if on Java 8, and these days Java 8 should be available to nearly all projects.

我现在让Lukasz Wiktor提出的干净的Java 8 解决方案成为公认的答案。如果在 Java 8 上,这绝对是首选,现在几乎所有项目都可以使用 Java 8。

采纳答案by Lukasz Wiktor

Using Java 8:

使用Java 8

private static Comparator<String> nullSafeStringComparator = Comparator
        .nullsFirst(String::compareToIgnoreCase); 

private static Comparator<Metadata> metadataComparator = Comparator
        .comparing(Metadata::getName, nullSafeStringComparator)
        .thenComparing(Metadata::getValue, nullSafeStringComparator);

public int compareTo(Metadata that) {
    return metadataComparator.compare(this, that);
}

回答by Fabian Steeg

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 patternif needed). Then you can skip all those checks and safely assume the values are not null.

您可以将您的类设计为不可变的(Effective Java 2nd Ed. 对此有一个很好的部分,Item 15: Minimize mutability)并确保在构造时没有可能出现null(并在需要时使用null 对象模式)。然后您可以跳过所有这些检查并安全地假设这些值不为空。

回答by Eddie

I would implement a null safe comparator. There may be an implementation out there, but this is so straightforward to implement that I've always rolled my own.

我会实现一个空安全比较器。那里可能有一个实现,但这实现起来非常简单,我总是自己推出。

Note: Your comparator above, if bothnames are null, won't even compare the value fields. I don't think this is what you want.

注意:您上面的比较器,如果两个名称都为空,则甚至不会比较值字段。我不认为这是你想要的。

I would implement this with something like the following:

我会用类似下面的东西来实现这个:

// primarily by name, secondarily by value; null-safe; case-insensitive
public int compareTo(final Metadata other) {

    if (other == null) {
        throw new NullPointerException();
    }

    int result = nullSafeStringComparator(this.name, other.name);
    if (result != 0) {
        return result;
    }

    return nullSafeStringComparator(this.value, other.value);
}

public static int nullSafeStringComparator(final String one, final String two) {
    if (one == null ^ two == null) {
        return (one == null) ? -1 : 1;
    }

    if (one == null && two == null) {
        return 0;
    }

    return one.compareToIgnoreCase(two);
}

EDIT: Fixed typos in code sample. That's what I get for not testing it first!

编辑:修复了代码示例中的拼写错误。这就是我不先测试的结果!

EDIT: Promoted nullSafeStringComparator to static.

编辑:将 nullSafeStringComparator 提升为静态。

回答by Yoni Roit

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

}

}

回答by Patrick

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.

我总是建议使用 Apache commons,因为它很可能比您自己编写的更好。另外,你可以做“真正的”工作,而不是重新发明。

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.

您感兴趣的类是Null Comparator。它允许您将空值设置为高或低。当两个值不为空时,您还可以为其提供自己的比较器以供使用。

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

在你的情况下,你可以有一个静态成员变量来进行比较,然后你的compareTo方法只是引用它。

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.

即使您决定自己动手,也要记住这个类,因为它在对包含空元素的列表进行排序时非常有用。

回答by Jonik

See the bottom of this answer for updated (2013) solution using Guava.

有关使用 Guava 的更新 (2013) 解决方案,请参阅此答案的底部。



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 :)

这就是我最终选择的。事实证明,我们已经有了一个用于 null-safe String 比较的实用方法,所以最简单的解决方案就是利用它。(这是一个很大的代码库;很容易错过这种事情:)

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):

这是 helper 的定义方式(它被重载,因此您还可以定义空值是先出现还是最后出现,如果需要):

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 uzhintoo.

因此,这与Eddie 的答案(尽管我不会将静态辅助方法称为比较器)和uzhin的答案基本相同。

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 librariesas Josh Bloch says.) But in this case that would not have yielded the cleanest, simplest code.

无论如何,总的来说,我会强烈支持帕特里克的解决方案,因为我认为尽可能使用已建立的库是一种很好的做法。(了解并使用Josh Bloch 所说的库。)但在这种情况下,这不会产生最干净、最简单的代码。

Edit (2009): Apache Commons Collections version

编辑(2009 年):Apache Commons Collections 版本

Actually, here's a way to make the solution based on Apache Commons NullComparatorsimpler. Combine it with the case-insensitive Comparatorprovided in Stringclass:

实际上,这里有一种方法可以使基于 Apache Commons 的解决方案NullComparator更简单。将它与类中提供的不区分大小写Comparator结合起来String

public static final Comparator<String> NULL_SAFE_COMPARATOR 
    = new NullComparator(String.CASE_INSENSITIVE_ORDER);

@Override
public int compareTo(Metadata other) {
    int result = NULL_SAFE_COMPARATOR.compare(this.name, other.name);
    if (result != 0) {
        return result;
    }
    return NULL_SAFE_COMPARATOR.compare(this.value, other.value);
}

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

现在这很优雅,我想。(只剩下一个小问题:CommonsNullComparator不支持泛型,所以有一个未经检查的分配。)

Update (2013): Guava version

更新(2013):番石榴版

Nearly 5 years later, here's how I'd tackle my original question. If coding in Java, I would (of course) be using Guava. (And quite certainly notApache Commons.)

将近 5 年后,这就是我如何解决我最初的问题。如果用 Java 编码,我(当然)会使用Guava。(当然不是Apache Commons。)

Put this constant somewhere, e.g. in "StringUtils" class:

将此常量放在某处,例如在“StringUtils”类中:

public static final Ordering<String> CASE_INSENSITIVE_NULL_SAFE_ORDER =
    Ordering.from(String.CASE_INSENSITIVE_ORDER).nullsLast(); // or nullsFirst()

Then, in public class Metadata implements Comparable<Metadata>:

然后,在public class Metadata implements Comparable<Metadata>

@Override
public int compareTo(Metadata other) {
    int result = CASE_INSENSITIVE_NULL_SAFE_ORDER.compare(this.name, other.name);
    if (result != 0) {
        return result;
    }
    return CASE_INSENSITIVE_NULL_SAFE_ORDER.compare(this.value, other.value);
}    

Of course, this is nearly identical to the Apache Commons version (both use JDK's CASE_INSENSITIVE_ORDER), the use of nullsLast()being the only Guava-specific thing. This version is preferable simply because Guava is preferable, as a dependency, to Commons Collections. (As everyone agrees.)

当然,这与 Apache Commons 版本几乎相同(都使用 JDK 的CASE_INSENSITIVE_ORDER),使用nullsLast()是唯一特定于 Guava 的东西。这个版本更可取,因为 Guava 作为一个依赖项,比 Commons Collections 更可取。(正如每个人都同意的那样。)

If you were wondering about Ordering, note that it implements Comparator. It's pretty handy especially for more complex sorting needs, allowing you for example to chain several Orderings using compound(). Read Ordering Explainedfor more!

如果您想知道Ordering,请注意它实现了Comparator. 它非常方便,特别是对于更复杂的排序需求,例如,允许您使用compound(). 阅读订购说明了解更多!

回答by Dag

You can simply use Apache Commons Lang:

您可以简单地使用Apache Commons Lang

result = ObjectUtils.compare(firstComparable, secondComparable)

回答by Piotr Sobczyk

I know that it may be not directly answer to your question, because you said that null values have to be supported.

我知道这可能不是直接回答您的问题,因为您说必须支持空值。

But I just want to note that supporting nulls in compareTo is not in line with compareTo contract described in official javadocs for Comparable:

但我只想指出,在 compareTo 中支持空值不符合 Comparable 官方javadoc 中描述的 compareTo 契约:

Note that null is not an instance of any class, and e.compareTo(null) should throw a NullPointerException even though e.equals(null) returns false.

请注意,null 不是任何类的实例,即使 e.equals(null) 返回 false,e.compareTo(null) 也应该抛出 NullPointerException。

So I would either throw NullPointerException explicitly or just let it be thrown first time when null argument is being dereferenced.

因此,我要么明确抛出 NullPointerException,要么在取消引用 null 参数时第一次抛出它。

回答by Dustin

I was looking for something similar and this seemed a bit complicated so I did this. I think it's a little easier to understand. You can use it as a Comparator or as a one liner. For this question you would change to compareToIgnoreCase(). As is, nulls float up. You can flip the 1, -1 if you want them to sink.

我正在寻找类似的东西,这看起来有点复杂,所以我这样做了。我认为这更容易理解。您可以将其用作比较器或单衬。对于这个问题,您将更改为 compareToIgnoreCase()。原样,空值向上浮动。如果你想让它们下沉,你可以翻转 1, -1。

StringUtil.NULL_SAFE_COMPARATOR.compare(getName(), o.getName());

.

.

public class StringUtil {
    public static final Comparator<String> NULL_SAFE_COMPARATOR = new Comparator<String>() {

        @Override
        public int compare(final String s1, final String s2) {
            if (s1 == s2) {
                //Nulls or exact equality
                return 0;
            } else if (s1 == null) {
                //s1 null and s2 not null, so s1 less
                return -1;
            } else if (s2 == null) {
                //s2 null and s1 not null, so s1 greater
                return 1;
            } else {
                return s1.compareTo(s2);
            }
        }
    }; 

    public static void main(String args[]) {
        final ArrayList<String> list = new ArrayList<String>(Arrays.asList(new String[]{"qad", "bad", "sad", null, "had"}));
        Collections.sort(list, NULL_SAFE_COMPARATOR);

        System.out.println(list);
    }
}

回答by snp0k

Another Apache ObjectUtils example. Able to sort other types of objects.

另一个 Apache ObjectUtils 示例。能够对其他类型的对象进行排序。

@Override
public int compare(Object o1, Object o2) {
    String s1 = ObjectUtils.toString(o1);
    String s2 = ObjectUtils.toString(o2);
    return s1.toLowerCase().compareTo(s2.toLowerCase());
}