it-swarm-eu.dev

Wie vereinfacht man eine nullsichere compareTo () -Implementierung?

Ich implementiere die Methode compareTo() für eine einfache Klasse wie diese (um Collections.sort() und andere von der Java-Plattform angebotene Extras verwenden zu können):

public class Metadata implements Comparable<Metadata> {
    private String name;
    private String value;

// Imagine basic constructor and accessors here
// Irrelevant parts omitted
}

Ich möchte die natürliche Reihenfolge für diese Objekte: 1) sortiert nach Name und 2) sortiert nach Wert, wenn der Name derselbe ist; Bei beiden Vergleichen sollte die Groß- und Kleinschreibung nicht berücksichtigt werden. Für beide Felder sind Nullwerte durchaus akzeptabel, daher darf compareTo in diesen Fällen nicht abbrechen.

Die Lösung, die mir einfällt, sieht folgendermaßen aus (ich verwende hier "Schutzklauseln", während andere einen einzelnen Rückkehrpunkt bevorzugen, aber das ist nicht der Punkt):

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

Das macht den Job, aber ich bin mit diesem Code nicht ganz zufrieden. Zugegeben, es ist nicht sehr komplex, aber ziemlich ausführlich und langweilig.

Die Frage ist,wie würden Sie dies weniger ausführlich machen(unter Beibehaltung der Funktionalität)? Sie können auch auf Java-Standardbibliotheken oder Apache Commons zurückgreifen, wenn diese hilfreich sind. Wäre die einzige Möglichkeit, dies (ein wenig) einfacher zu machen, die Implementierung meines eigenen "NullSafeStringComparator" und dessen Anwendung für den Vergleich beider Felder?

Bearbeitet 1-3: Eddies Recht; behebt den "beide namen sind null" fall oben

Über die akzeptierte Antwort

Ich habe diese Frage 2009 gestellt, natürlich auf Java 1.6, und damals wardie reine JDK-Lösung von Eddiemeine bevorzugte akzeptierte Antwort. Ich bin bis jetzt (2017) nie dazu gekommen, das zu ändern.

Es gibt auch Bibliothekslösungen von Drittanbietern - eine von mir gepostete Apache Commons Collection 2009 und eine von Guava 2013 - die ich irgendwann vorgezogen habe.

Ich habe jetzt die saubereJava 8-Lösung von Lukasz Wiktor gemachtdie akzeptierte Antwort. Dies sollte auf jeden Fall unter Java 8 bevorzugt werden, und heutzutage sollte Java 8 für fast alle Projekte verfügbar sein.

135
Jonik

Verwenden von 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);
}
145
Lukasz Wiktor

Sie können einfach Apache Commons Lang verwenden:

result = ObjectUtils.compare(firstComparable, secondComparable)
183
Dag

Ich würde einen nullsicheren Vergleicher implementieren. Es gibt zwar eine Implementierung, aber die Implementierung ist so unkompliziert, dass ich immer meine eigene gewürfelt habe. 

Hinweis: Ihr Vergleicher oben, wenn beide Namen null sind, vergleicht nicht einmal die Wertfelder. Ich glaube nicht, dass Sie das wollen.

Ich würde dies mit etwas wie dem folgenden implementieren:

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

BEARBEITEN: Fehler im Codebeispiel behoben. Das ist es, was ich bekomme, wenn ich es nicht erst getestet habe!

BEARBEITEN: NullSafeStringComparator wurde in statisch gesetzt.

88
Eddie

Eine aktualisierte (2013) -Lösung mit Guava finden Sie am Ende dieser Antwort.


Damit bin ich letztendlich gegangen. Es stellte sich heraus, dass wir bereits eine Utility-Methode für den nullsicheren String-Vergleich hatten. Die einfachste Lösung bestand darin, diese zu nutzen. (Es ist eine große Codebasis; so etwas kann man leicht übersehen :)

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

So wird der Helper definiert (er ist überladen, sodass Sie auch festlegen können, ob Nullen zuerst oder zuletzt kommen, wenn Sie möchten):

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

Das ist also im Wesentlichen dasselbe wie Eddies Antwort (obwohl ich eine statische Hilfsmethode nicht Komparator) und die von Uzhin nennen würde.

Jedenfalls hätte ich Patricks Lösung generell stark favorisiert, da es meiner Meinung nach eine gute Praxis ist, etablierte Bibliotheken zu verwenden, wann immer dies möglich ist. (Kenne und nutze die Bibliotheken wie Josh Bloch sagt.) Aber in diesem Fall hätte das nicht den saubersten, einfachsten Code ergeben.

Edit (2009): Version von Apache Commons Collections

Hier ist eine Möglichkeit, die Lösung basierend auf Apache Commons NullComparator einfacher zu gestalten. Kombinieren Sie es mit der case-insensitive Comparator , die in der String-Klasse bereitgestellt wird:

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

Nun, das ist ziemlich elegant, denke ich. (Es bleibt nur ein kleines Problem: Die Commons NullComparator unterstützt keine Generika, daher gibt es eine ungeprüfte Zuordnung.)

Update (2013): Guavenversion

Fast fünf Jahre später habe ich meine ursprüngliche Frage in Angriff genommen. Wenn Sie in Java programmieren, würde ich (natürlich) Guava verwenden. (Und ganz bestimmt nicht Apache Commons.) 

Setzen Sie diese Konstante irgendwo, z. in der Klasse "StringUtils":

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

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

Natürlich ist dies nahezu identisch mit der Apache Commons-Version (beide verwenden JDKs CASE_INSENSITIVE_ORDER ), wobei die Verwendung von nullsLast() die einzige Guava-spezifische Sache ist. Diese Version ist einfach vorzuziehen, da Guava als Abhängigkeit von Commons Collections bevorzugt wird. (Wie alle sind einverstanden .)

Wenn Sie sich über Ordering wundern, beachten Sie, dass Comparator implementiert wird. Dies ist vor allem für komplexere Sortieranforderungen sehr praktisch, da Sie beispielsweise mehrere Bestellungen mit compound() verketten können. Lesen Sie Ordering Explained für mehr!

21
Jonik

Ich empfehle immer die Verwendung von Apache Commons, da dies höchstwahrscheinlich besser ist als einer, den Sie selbst schreiben können. Außerdem können Sie dann „echte“ Arbeit erledigen, anstatt sie neu zu erfinden.

Die Klasse, an der Sie interessiert sind, ist der Null-Komparator . Sie können Nullen hoch oder niedrig machen. Sie können auch einen eigenen Vergleicher verwenden, wenn die beiden Werte nicht Null sind.

In Ihrem Fall können Sie eine statische Member-Variable haben, die den Vergleich durchführt, und Ihre compareTo-Methode verweist darauf.

So etwas

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

}

Auch wenn Sie sich entscheiden, Ihre eigene Rolle zu erstellen, sollten Sie sich diese Klasse merken, da sie sehr nützlich ist, wenn Sie Listen bestellen, die Nullelemente enthalten.

13
Patrick

Ich weiß, dass es möglicherweise keine direkte Antwort auf Ihre Frage gibt, weil Sie gesagt haben, dass Nullwerte unterstützt werden müssen.

Ich möchte jedoch nur anmerken, dass die Unterstützung von Nullen in compareTo nicht mit dem compareTo-Vertrag übereinstimmt, der in offiziellen javadocs für Comparable beschrieben ist:

Beachten Sie, dass null keine Instanz einer Klasse ist und e.compareTo (null) sollte eine NullPointerException auslösen, obwohl e.equals (null) .__ zurückgibt. falsch.

Also würde ich entweder NullPointerException explizit auslösen oder einfach das erste Mal werfen, wenn das Null-Argument dereferenziert wird.

7
Piotr Sobczyk

Sie können die Methode extrahieren:

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

}

4
Yoni Roit

Sie können Ihre Klasse als unveränderlich gestalten (Effective Java 2nd Ed. Enthält einen guten Abschnitt dazu, Punkt 15: Veränderlichkeit minimieren) und bei der Konstruktion sicherstellen, dass keine Nullen möglich sind (und bei Bedarf das null-Objektmuster verwenden ). Dann können Sie all diese Prüfungen überspringen und sicher annehmen, dass die Werte nicht null sind.

3
Fabian Steeg

wir können Java 8 verwenden, um einen null-freundlichen Vergleich zwischen object ..__ durchzuführen. Ich habe eine Boy-Klasse mit 2 Feldern: Stringname und Integer-Alter, und ich möchte zuerst Namen und dann Alter vergleichen, wenn beide gleich sind.

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

und das Ergebnis:

    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
2
Leo Ng

Ich suchte nach etwas Ähnlichem und dies schien etwas kompliziert zu sein, also tat ich das. Ich denke, es ist ein bisschen leichter zu verstehen. Sie können es als Komparator oder als Einlage verwenden. Für diese Frage würden Sie ändern, umToIgnoreCase () zu vergleichen. Wie es ist, schweben Nullen auf. Sie können die 1, -1 umdrehen, wenn Sie möchten, dass sie sinken.

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
Dustin

Falls jemand Spring verwendet, gibt es eine Klasse org.springframework.util.comparator.NullSafeComparator, die dies auch für Sie erledigt. Gestalte einfach deine eigenen damit vergleichbaren so

new NullSafeComparator<YourObject>(new YourComparable(), true)

https://docs.spring.io/spring/docs/current/javadoc-api/org/springframework/util/comparator/NullSafeComparator.html

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


    }

}

ausgabe ist 

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
Nikhil Kumar K

Eine der einfachen Möglichkeiten von mit NullSafe Comparator ist die Verwendung der Spring-Implementierung. Nachfolgend finden Sie eines der einfachen Beispiele, auf die verwiesen werden soll:

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;
    }
1
Amandeep Singh

Für den speziellen Fall, dass Sie wissen, dass die Daten keine Nullen haben (immer eine gute Idee für Zeichenfolgen) und die Daten sehr groß sind, machen Sie immer noch drei Vergleiche, bevor Sie die Werte tatsächlich vergleichen, wenn Sie sicher sind, dass dies der Fall ist Ihr Fall, können Sie ein wenig Bit optimieren. YMMV als lesbarer Code übertrifft geringfügige Optimierung:

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

Dies ist meine Implementierung, mit der ich meine ArrayList sortiere. Die Null-Klassen werden bis zum letzten sortiert.

in meinem Fall erweitert EntityPhone EntityAbstract, und mein Container ist List <EntityAbstract>.

die "compareIfNull ()" -Methode wird für die sichere Sortierung verwendet. Die anderen Methoden dienen der Vollständigkeit und zeigen, wie compareIfNull verwendet werden kann.

@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
Angel Koh

Ein weiteres Apache ObjectUtils-Beispiel. Kann andere Arten von Objekten sortieren.

@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
snp0k