nullセーフなcompareTo()実装を単純化する方法は?


157

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
}

私が欲しい自然順序付け名前が同じである場合の値でソート)の名前と2でソート)1;:これらのオブジェクトのためにあることを どちらの比較でも、大文字と小文字は区別されません。どちらのフィールドでも、null値は完全に許容されるためcompareTo、これらの場合に壊れてはなりません。

頭に浮かぶ解決策は、次のようなものです(ここでは「ガード句」を使用していますが、他の人は単一の戻り点を好むかもしれませんが、それは重要ではありません)。

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

これでうまくいきますが、私はこのコードに完全に満足していません。確かに、それほど複雑ではありませんが、かなり冗長で退屈です。

問題は、(機能を維持しながら)これを冗長にするにどうすればよいですか?役立つ場合は、Java標準ライブラリまたはApache Commonsを参照してください。これを(少し)簡単にする唯一のオプションは、独自の "NullSafeStringComparator"を実装して、両方のフィールドを比較するために適用することですか?

編集1〜3:エディの権利。上記の「両方の名前がnullである」ケースを修正

採用された回答について

私は2009年にJava 1.6でこの質問をしましたが、当時はEddieによる純粋なJDKソリューションが私の好まれた回答でした。今まで(2017年)までそれを変えることに慣れなかった。

サードパーティのライブラリソリューションもあります。2009年のApache Commons Collectionsと2013年のGuavaの2つで、どちらも私が投稿したものです。ある時点で私はそれを好みました。

私は今、Lukasz WiktorによるクリーンなJava 8ソリューションを受け入れられた答えにしました。Java 8を使用している場合は、これが間違いなく推奨され、最近では、ほとんどすべてのプロジェクトでJava 8を使用できます。


回答:


173

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

7
私は、Apache Commons Langを支持してJava 8組み込みのものを使用することをサポートしていますが、そのJava 8コードは非常に醜く、それでも冗長です。差し当たり、org.apache.commons.lang3.builder.CompareToBuilderを使用します。
jschreiner 2017年

1
これは、nullオブジェクトでcompareTo()を呼び出そうとするため、Collections.sort(Arrays.asList(null、val1、null、val2、null))では機能しません。正直に言うと、これを回避する方法を理解しようとするコレクションフレームワークに問題があるようです。
ペドロボルヘス

3
@PedroBorges著者は、nullコンテナー参照の並べ替えではなく、並べ替え可能なフィールド(これらのフィールドがnullの可能性がある)を持つコンテナーオブジェクトの並べ替えについて質問しました。したがって、コメントは正しいですCollections.sort(List)が、リストにnullが含まれている場合は機能しませんが、コメントは質問には関係ありません。
スクラビー

211

Apache Commons Langを使用するだけです。

result = ObjectUtils.compare(firstComparable, secondComparable)

4
(@Kong:これはnullの安全性を処理しますが、大文字と小文字を区別しません。元の質問の別の側面でした。したがって、受け入れられた回答は変更されません。)
Jonik

3
また、私の心の中で、Apache Commonsは2013年に受け入れられるべき回答ではありません。(一部のサブプロジェクトが他のサブプロジェクトよりも優れている場合でも)Guavaを使用して同じことを達成できますnullsFirst()/を参照してくださいnullsLast()
Jonik

8
@Jonikなぜ、Apache Commonsが2013年の回答として受け入れられるべきではないと思いますか?
15

1
Apache Commonsの大部分は、レガシー/メンテナンスが不十分/低品質のものです。たとえば、全体的に非常に高品質のlib であるGuavaや、JDK自体にはますます多くのものが提供されています。2005年頃、Apache Commonsはたわごとでしたが、最近ではほとんどのプロジェクトで必要はありません。(もちろん、例外があります。たとえば、何らかの理由でFTPクライアントが必要な場合は、Apache Commons NetのFTPクライアントなどを使用するでしょう。)
Jonik

6
@ジョニック、グアバを使ってどのように質問に答えますか?Apache Commons Lang(パッケージorg.apache.commons.lang3)が「レガシー/不十分に維持/低品質」であるというあなたの主張は、誤りであるか、せいぜい根拠のないものです。Commons Lang3は理解と使用が簡単で、積極的に維持されています。これはおそらく、(Spring FrameworkとSpring Securityは別として)私が最もよく使用するライブラリです。たとえば、nullセーフメソッドを持つStringUtilsクラスは、入力の正規化を簡単にします。
Paul

93

nullセーフコンパレータを実装します。そこに実装があるかもしれませんが、これは非常に簡単に実装できるので、私は常に自分で実装しています。

注:上記のコンパレータは、両方の名前がnullの場合、値フィールドを比較しません。これがあなたの望んでいることだとは思いません。

私はこれを次のようなもので実装します:

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

編集:コードサンプルのタイプミスを修正しました。それは私が最初にテストしないことで得られるものです!

編集:nullSafeStringComparatorをstaticに昇格しました。


2
ネストされた "if"について...この場合、ネストされたifは読みにくくなるので、避けます。はい、時々これにより不要な比較が行われます。パラメータのfinalは必要ありませんが、良い方法です。
Eddie、

31
XORの甘い使い方
James McMahon

9
@phihag-私はそれが3年以上であることを知っていますが、... finalキーワードは本当に必要ありません(Javaコードは既に冗長です)。しかし、それはローカル変数としてのパラメーターの再利用を防ぎます(ひどいコーディング慣行です)。ソフトウェアに対する私たちの集合的理解は時間とともに改善され、物事はデフォルトでfinal / const / inmutableになるはずです。そのためfinal、パラメーター宣言での使用には少し余分な冗長性(関数が取るに足らないことはありますが)を使用することを好みますinmutability-by-quasi-default。それの包括性/保守性のオーバーヘッドは、グランドスキームでは無視できます。
luis.espinal 2012

24
@ジェームズマクマホン私は同意しないでください。Xor(^)は単に等しくない(!=)に置き換えることができます。同じバイトコードにコンパイルされます。!= vs ^の使い方は、単に好みと読みやすさの問題です。驚いたことから判断すると、ここには属さないと思います。チェックサムを計算する場合は、xorを使用します。他のほとんどの場合(この例のように)は!=に固執しましょう。
bvdb 14

5
StringをTに置き換えることでこの回答を拡張するのは簡単で、Tは<T extends Comparable <T >>として宣言されています...そして、null可能なComparableオブジェクトを安全に比較できます
Thierry

20

Guavaを使用した更新された(2013)ソリューションについては、この回答の下部を参照してください。


これは私が最終的に行ったものです。nullセーフの文字列比較用のユーティリティメソッドが既にあることが判明したため、最も簡単な解決策はそれを利用することでした。(それは大きなコードベースです;この種のものを見逃すのは簡単です:)

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

これはヘルパーの定義方法です(必要に応じて、nullが最初に来るか最後に来るかを定義できるようにオーバーロードされています)。

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

したがって、これは基本的にエディの答え(静的ヘルパーメソッドをコンパレータとは呼ばないでしょう)とuzhin の答えと同じです。

とにかく、できる限り確立されたライブラリを使用することは良い習慣だと思うので、一般的に、パトリックの解決策を強く支持したでしょう。(Josh Blochが言うように、ライブラリー知って使用してください。)しかし、この場合、それは最もクリーンで最も単純なコードを生成しませんでした。

編集(2009):Apache Commons Collectionsバージョン

実際、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);
}

これはかなりエレガントだと思います。(小さな問題が1つ残っています。コモンズNullComparatorはジェネリックをサポートしていないため、チェックされていない割り当てがあります。)

アップデート(2013):Guavaバージョン

ほぼ5年後の、元の質問への取り組み方は次のとおりです。Javaでコーディングする場合、私は(もちろん)Guavaを使用します。(そして確かにApache Commonsではありませ。)

この定数をどこかに、たとえば「StringUtils」クラスに配置します。

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

その後、中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);
}    

もちろん、これはApache Commonsバージョン(どちらもJDKのCASE_INSENSITIVE_ORDERを使用します)とほぼ同じです。これはnullsLast()、Guava固有のものとしてのみ使用されます。このバージョンは、Commonsコレクションよりも依存関係としてGuavaの方が望ましいという理由で推奨されています。(誰もが同意するように。)

について疑問に思っている場合はOrdering、それが実装されていることに注意してくださいComparator。これは特に、より複雑な並べ替えのニーズに非常に便利で、たとえばを使用して複数のOrderingをチェーンすることができますcompound()。詳細については、注文の説明をご覧ください!


2
String.CASE_INSENSITIVE_ORDERを使用すると、ソリューションがよりクリーンになります。素敵なアップデート。
Patrick、

2
とにかくApache Commonsを使用してComparatorChainいる場合は、独自のcompareToメソッドは必要ないので、
アメーベ

13

私は、Apacheコモンズを使用することを常にお勧めします。自分で書けるものよりも優れているからです。さらに、再発明するのではなく、「実際の」作業を行うことができます。

興味のあるクラスはNull Comparatorです。ヌルを高くしたり低くしたりできます。また、2つの値がnullでない場合に使用する独自のコンパレータを指定します。

あなたのケースでは、比較を行う静的メンバー変数を持つことができ、compareToメソッドはそれを参照するだけです。

のようなもの

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

}

独自にロールすることにした場合でも、このクラスはnull要素を含むリストを注文するときに非常に役立つため、覚えておいてください。


おかげで、私はコモンズにこのようなものが存在することを望んでいました!この場合は、しかし、私はそれを使用して終了しませんでした:stackoverflow.com/questions/481813/...
Jonik

String.CASE_INSENSITIVE_ORDER;を使用することで、アプローチを簡略化できることがわかりました。編集したフォローアップの回答をご覧ください。
Jonik

これは良いことですが、「if(other == null){」チェックは存在しないはずです。ComparableのJavadocによると、otherがnullの場合、compareToはNullPointerExceptionをスローする必要があります。
Daniel Alexiuc

7

null値をサポートする必要があるとおっしゃっていたので、質問に直接回答できない場合があることを知っています。

ただし、compareToでのnullのサポートは、Comparableの公式javadocsに記載されているcompareToコントラクトと一致しないことに注意してください。

nullはどのクラスのインスタンスでもないことに注意してください。e.equals(null)がfalseを返しても、e.compareTo(null)はNullPointerExceptionをスローする必要があります。

したがって、私はNullPointerExceptionを明示的にスローするか、null引数が逆参照されているときに初めてスローするようにします。


4

メソッドを抽出できます:

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

}


2
「0:1」を「0:-1」にしないでください。
Rolf Kristensen、2011

3

クラスを不変に設計し(Effective Java 2nd Ed。にこれに関するすばらしいセクションがあります。項目15:可変性を最小限に抑える)、構築時にnullが不可能であることを確認します(必要に応じてnullオブジェクトパターンを使用します)。次に、これらのチェックをすべてスキップして、値がnullでないことを安全に想定できます。


はい、それは一般的に良い解決策であり、多くのことを簡素化します-しかしここでは、何らかの理由でnull値が許可され、考慮に入れられる必要がある場合にもっと興味を持っていました:)
Jonik

2

私は似たようなものを探していましたが、これは少し複雑に思えたので、これを行いました。少しわかりやすいと思います。コンパレータまたはワンライナーとして使用できます。この質問では、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);
    }
}

2

java 8を使用して、オブジェクト間のnullフレンドリーな比較を行うことができます。文字列名と整数の年齢の2つのフィールドを持つBoyクラスがあると仮定します。最初に名前を比較し、両方が等しい場合は年齢を比較します。

static void test2() {
    List<Boy> list = new ArrayList<>();
    list.add(new Boy("Peter", null));
    list.add(new Boy("Tom", 24));
    list.add(new Boy("Peter", 20));
    list.add(new Boy("Peter", 23));
    list.add(new Boy("Peter", 18));
    list.add(new Boy(null, 19));
    list.add(new Boy(null, 12));
    list.add(new Boy(null, 24));
    list.add(new Boy("Peter", null));
    list.add(new Boy(null, 21));
    list.add(new Boy("John", 30));

    List<Boy> list2 = list.stream()
            .sorted(comparing(Boy::getName, 
                        nullsLast(naturalOrder()))
                   .thenComparing(Boy::getAge, 
                        nullsLast(naturalOrder())))
            .collect(toList());
    list2.stream().forEach(System.out::println);

}

private static class Boy {
    private String name;
    private Integer age;
    public String getName() {
        return name;
    }
    public void setName(String name) {
        this.name = name;
    }
    public Integer getAge() {
        return age;
    }
    public void setAge(Integer age) {
        this.age = age;
    }
    public Boy(String name, Integer age) {
        this.name = name;
        this.age = age;
    }

    public String toString() {
        return "name: " + name + " age: " + age;
    }
}

そして結果:

    name: John age: 30
    name: Peter age: 18
    name: Peter age: 20
    name: Peter age: 23
    name: Peter age: null
    name: Peter age: null
    name: Tom age: 24
    name: null age: 12
    name: null age: 19
    name: null age: 21
    name: null age: 24


1

データにnullが含まれていないことがわかっており(常に文字列に適しています)、データが非常に大きい場合でも、実際に値を比較する前に3つの比較を行っています。少しだけ最適化できます。可読コードとしてのYMMVは、マイナーな最適化よりも優れています。

        if(o1.name != null && o2.name != null){
            return o1.name.compareToIgnoreCase(o2.name);
        }
        // at least one is null
        return (o1.name == o2.name) ? 0 : (o1.name != null ? 1 : -1);

1
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import java.util.Comparator;

public class TestClass {

    public static void main(String[] args) {

        Student s1 = new Student("1","Nikhil");
        Student s2 = new Student("1","*");
        Student s3 = new Student("1",null);
        Student s11 = new Student("2","Nikhil");
        Student s12 = new Student("2","*");
        Student s13 = new Student("2",null);
        List<Student> list = new ArrayList<Student>();
        list.add(s1);
        list.add(s2);
        list.add(s3);
        list.add(s11);
        list.add(s12);
        list.add(s13);

        list.sort(Comparator.comparing(Student::getName,Comparator.nullsLast(Comparator.naturalOrder())));

        for (Iterator iterator = list.iterator(); iterator.hasNext();) {
            Student student = (Student) iterator.next();
            System.out.println(student);
        }


    }

}

出力は

Student [name=*, id=1]
Student [name=*, id=2]
Student [name=Nikhil, id=1]
Student [name=Nikhil, id=2]
Student [name=null, id=1]
Student [name=null, id=2]

1

NullSafeコンパレータ使用する簡単な方法の1つは、Springの実装を使用することです。以下は、参照する簡単な例の1つです。

public int compare(Object o1, Object o2) {
        ValidationMessage m1 = (ValidationMessage) o1;
        ValidationMessage m2 = (ValidationMessage) o2;
        int c;
        if (m1.getTimestamp() == m2.getTimestamp()) {
            c = NullSafeComparator.NULLS_HIGH.compare(m1.getProperty(), m2.getProperty());
            if (c == 0) {
                c = m1.getSeverity().compareTo(m2.getSeverity());
                if (c == 0) {
                    c = m1.getMessage().compareTo(m2.getMessage());
                }
            }
        }
        else {
            c = (m1.getTimestamp() > m2.getTimestamp()) ? -1 : 1;
        }
        return c;
    }

0

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

0

これは、ArrayListの並べ替えに使用する実装です。nullクラスは最後にソートされます。

私の場合、EntityPhoneはEntityAbstractを拡張し、コンテナーはList <EntityAbstract>です。

「compareIfNull()」メソッドは、nullセーフソートに使用されます。他のメソッドは、compareIfNullの使用方法を示す完全性のためのものです。

@Nullable
private static Integer compareIfNull(EntityPhone ep1, EntityPhone ep2) {

    if (ep1 == null || ep2 == null) {
        if (ep1 == ep2) {
            return 0;
        }
        return ep1 == null ? -1 : 1;
    }
    return null;
}

private static final Comparator<EntityAbstract> AbsComparatorByName = = new Comparator<EntityAbstract>() {
    @Override
    public int compare(EntityAbstract ea1, EntityAbstract ea2) {

    //sort type Phone first.
    EntityPhone ep1 = getEntityPhone(ea1);
    EntityPhone ep2 = getEntityPhone(ea2);

    //null compare
    Integer x = compareIfNull(ep1, ep2);
    if (x != null) return x;

    String name1 = ep1.getName().toUpperCase();
    String name2 = ep2.getName().toUpperCase();

    return name1.compareTo(name2);
}
}


private static EntityPhone getEntityPhone(EntityAbstract ea) { 
    return (ea != null && ea.getClass() == EntityPhone.class) ?
            (EntityPhone) ea : null;
}

0

単純なハックが必要な場合:

arrlist.sort((o1, o2) -> {
    if (o1.getName() == null) o1.setName("");
    if (o2.getName() == null) o2.setName("");

    return o1.getName().compareTo(o2.getName());
})

リストの最後にヌルを入れたい場合は、上記の方法でこれを変更してください

return o2.getName().compareTo(o1.getName());
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.