Come semplificare un'implementazione compareTo () nulla di sicura?


157

Sto implementando un compareTo()metodo per una classe semplice come questa (per poter usare Collections.sort()e altri gadget offerti dalla piattaforma Java):

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

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

Voglio che l' ordine naturale per questi oggetti sia: 1) ordinati per nome e 2) ordinati per valore se il nome è lo stesso; entrambi i confronti dovrebbero essere case-sensitive. Per entrambi i campi i valori null sono perfettamente accettabili, quindi compareToin questi casi non devono rompersi.

La soluzione che mi viene in mente è in linea con quanto segue (sto usando "clausole di guardia" qui mentre altri potrebbero preferire un singolo punto di ritorno, ma questo è accanto al punto):

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

Questo fa il lavoro, ma non sono perfettamente soddisfatto di questo codice. Certo, non è molto complesso, ma è piuttosto prolisso e noioso.

La domanda è: come lo renderesti meno prolisso (pur mantenendo la funzionalità)? Sentiti libero di fare riferimento alle librerie standard Java o Apache Commons se ti aiutano. L'unica opzione per rendere questo (un po ') più semplice sarebbe implementare il mio "NullSafeStringComparator" e applicarlo per confrontare entrambi i campi?

Modifiche 1-3 : Eddie ha ragione; risolto il caso "entrambi i nomi sono null" sopra

Informazioni sulla risposta accettata

Ho fatto questa domanda nel 2009, ovviamente su Java 1.6, e all'epoca la soluzione JDK pura di Eddie era la mia risposta preferita accettata. Non ho mai avuto modo di cambiarlo fino ad ora (2017).

Ci sono anche soluzioni per biblioteche di terze parti - una Apache Commons Collections del 2009 e una Guava del 2013, entrambe pubblicate da me - che preferivo ad un certo punto nel tempo.

Ora ho reso la soluzione Java 8 pulita di Lukasz Wiktor la risposta accettata. Questo dovrebbe sicuramente essere preferito se su Java 8, e in questi giorni Java 8 dovrebbe essere disponibile per quasi tutti i progetti.


Risposte:


173

Utilizzando 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
Sostengo l'utilizzo di roba incorporata in Java 8 a favore di Apache Commons Lang, ma quel codice Java 8 è abbastanza brutto e ancora prolisso. Continuerò con org.apache.commons.lang3.builder.CompareToBuilder per il momento.
Jschreiner,

1
Questo non funziona per Collections.sort (Arrays.asList (null, val1, null, val2, null)) in quanto tenterà di chiamare compareTo () su un oggetto null. Ad essere onesti sembra un problema con il framework delle collezioni, cercando di capire come risolverlo.
Pedro Borges,

3
@PedroBorges L'autore ha chiesto informazioni sull'ordinamento di oggetti contenitore che possiedono campi ordinabili (dove tali campi possono essere nulli), non sull'ordinamento di riferimenti contenitore nulli. Quindi, mentre il tuo commento è corretto, in quanto Collections.sort(List)non funziona quando l'elenco contiene valori nulli, il commento non è rilevante per la domanda.
Scrubbie,

211

Puoi semplicemente usare Apache Commons Lang :

result = ObjectUtils.compare(firstComparable, secondComparable)

4
(@Kong: questo si occupa della sicurezza nulla ma non dell'insensibilità al maiuscolo, che era un altro aspetto della domanda originale. Non cambia quindi la risposta accettata.)
Jonik,

3
Inoltre, secondo me Apache Commons non dovrebbe essere la risposta accettata nel 2013. (Anche se alcuni sottoprogetti sono meglio mantenuti di altri.) Guava può essere usato per ottenere la stessa cosa ; vedi nullsFirst()/ nullsLast().
Jonik,

8
@Jonik Perché pensi che Apache Commons non dovrebbe essere la risposta accettata nel 2013?
reallynice

1
Grandi parti di Apache Commons sono roba legacy / mal mantenuta / di bassa qualità. Ci sono alternative migliori per la maggior parte delle cose che fornisce, ad esempio in Guava che è una lib di qualità molto alta in tutto e sempre più nello stesso JDK. Intorno al 2005 sì, Apache Commons era la merda, ma oggigiorno non ce n'è bisogno nella maggior parte dei progetti. (Certo, ci sono delle eccezioni; per esempio se per qualche motivo avessi bisogno di un client FTP, probabilmente
userei

6
@Jonik, come risponderesti alla domanda usando Guava? La tua affermazione che Apache Commons Lang (pacchetto org.apache.commons.lang3) è "legacy / scarsamente mantenuta / di bassa qualità" è falsa o nella migliore delle ipotesi. Commons Lang3 è facile da capire e da usare ed è attivamente mantenuto. È probabilmente la mia libreria più usata (a parte Spring Framework e Spring Security) - la classe StringUtils con i suoi metodi null-safe rende la normalizzazione dell'input banale, per esempio.
Paul,

93

Vorrei implementare un comparatore sicuro null. Potrebbe esserci un'implementazione là fuori, ma questo è così semplice da implementare che ho sempre fatto il mio.

Nota: il tuo comparatore sopra, se entrambi i nomi sono nulli, non confronterà nemmeno i campi del valore. Non penso sia questo ciò che vuoi.

Lo implementerei con qualcosa di simile al seguente:

// 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: errori di battitura risolti nell'esempio di codice. Questo è quello che ottengo per non averlo provato prima!

EDIT: promosso nullSafeStringComparator su statico.


2
Riguardo al nidificato "if" ... Trovo il nidificato se deve essere meno leggibile per questo caso, quindi lo evito. Sì, a volte ci sarà un confronto non necessario a causa di questo. final per i parametri non è necessario, ma è una buona idea.
Eddie,

31
Dolce uso di XOR
James McMahon

9
@phihag - So che sono passati più di 3 anni, MA ... la finalparola chiave non è veramente necessaria (il codice Java è già prolisso così com'è). Tuttavia, impedisce il riutilizzo dei parametri come variabili locali (una pratica di codifica terribile). la nostra comprensione collettiva del software migliora nel tempo, sappiamo che le cose dovrebbero essere definitive / const / inutilizzabili di default. Quindi preferisco un po 'più di verbosità nell'usare finalnelle dichiarazioni dei parametri (per quanto banale possa essere la funzione) da ottenere inmutability-by-quasi-default.) La comprensibilità / la manutenibilità di cui sopra è trascurabile nel grande schema delle cose.
luis.espinal,

24
@James McMahon Non sono d'accordo. Xor (^) potrebbe semplicemente essere sostituito con non-uguale (! =). Si compila anche con lo stesso codice byte. L'uso di! = Vs ^ è solo una questione di gusti e leggibilità. Quindi, a giudicare dal fatto che sei rimasto sorpreso, direi che non appartiene qui. Usa xor quando stai provando a calcolare un checksum. Nella maggior parte degli altri casi (come questo) atteniamoci a! =.
bvdb,

5
È facile estendere questa risposta sostituendo String con T, T dichiarata come <T estende Comparable <T>> ... e quindi possiamo tranquillamente confrontare qualsiasi oggetto comparabile nulla annullabile
Thierry

20

Vedi il fondo di questa risposta per una soluzione aggiornata (2013) usando Guava.


Questo è quello che alla fine sono andato con. Si è scoperto che disponevamo già di un metodo di utilità per il confronto di stringhe null-safe, quindi la soluzione più semplice era quella di utilizzarlo. (È una grande base di codice; facile perdere questo tipo di cose :)

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

Ecco come viene definito l'helper (è sovraccarico in modo che tu possa anche definire se i null arrivano prima o alla fine, se vuoi):

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

Quindi questa è essenzialmente la stessa della risposta di Eddie (anche se non definirei un metodo di supporto statico un comparatore ) e anche quella di uzhin .

Comunque, in generale, avrei favorito fortemente la soluzione di Patrick , poiché penso che sia una buona pratica usare librerie consolidate quando possibile. ( Conosci e usa le librerie come dice Josh Bloch.) Ma in questo caso ciò non avrebbe prodotto il codice più pulito e semplice.

Modifica (2009): versione di Apache Commons Collections

In realtà, ecco un modo per semplificare la soluzione basata su Apache Commons NullComparator. Combinalo con la distinzione tra maiuscole e minuscoleComparator fornita in Stringclasse:

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

Ora questo è piuttosto elegante, penso. (Rimane solo un piccolo problema: i Commons NullComparatornon supportano i generici, quindi c'è un compito non controllato.)

Aggiornamento (2013): versione di Guava

Quasi 5 anni dopo, ecco come affrontare la mia domanda originale. Se codifico in Java, utilizzerei (ovviamente) Guava . (E sicuramente non Apache Commons.)

Metti questa costante da qualche parte, ad esempio nella classe "StringUtils":

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

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

Naturalmente, questo è quasi identico alla versione di Apache Commons (entrambi usano CASE_INSENSITIVE_ORDER di JDK ), l'uso di nullsLast()essere l'unica cosa specifica di Guava. Questa versione è preferibile semplicemente perché Guava è preferibile, come dipendenza, alle Collezioni Commons. (Come tutti concordano .)

Se ti stavi chiedendo Ordering, nota che implementa Comparator. È piuttosto utile soprattutto per esigenze di ordinamento più complesse, che consente ad esempio di concatenare diversi ordini utilizzando compound(). Leggi gli ordini spiegati per ulteriori informazioni!


2
String.CASE_INSENSITIVE_ORDER rende davvero la soluzione molto più pulita. Bel aggiornamento.
Patrick,

2
Se usi comunque Apache Commons, ce n'è un ComparatorChainquindi non hai bisogno di un compareTometodo personale.
amebe,

13

Consiglio sempre di usare i comuni Apache poiché molto probabilmente sarà meglio di uno che puoi scrivere da solo. Inoltre, puoi fare un lavoro "reale" anziché reinventarlo.

La classe che ti interessa è il comparatore null . Ti permette di fare null alti o bassi. Gli dai anche il tuo comparatore da usare quando i due valori non sono nulli.

Nel tuo caso puoi avere una variabile membro statica che fa il confronto e quindi il tuo compareTometodo fa solo riferimento a quello.

Qualcosa del genere

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

}

Anche se decidi di creare il tuo, tieni a mente questa classe poiché è molto utile quando ordini liste che contengono elementi null.


Grazie, speravo che ci fosse qualcosa di simile in Commons! In questo caso, però, non mi finisce di usarlo: stackoverflow.com/questions/481813/...
Jonik

Ho appena realizzato che il tuo approccio può essere semplificato usando String.CASE_INSENSITIVE_ORDER; vedere la mia risposta di follow-up modificata.
Jonik,

Questo è buono ma il controllo "if (other == null) {" non dovrebbe essere presente. Javadoc for Comparable afferma che compareTo dovrebbe generare un NullPointerException se altro è nullo.
Daniel Alexiuc,

7

So che potrebbe non essere la risposta diretta alla tua domanda, perché hai detto che i valori null devono essere supportati.

Ma voglio solo notare che il supporto di null in compareTo non è in linea con compareTo contratto descritto in javadocs ufficiali per Comparable :

Si noti che null non è un'istanza di nessuna classe e e.compareTo (null) dovrebbe generare una NullPointerException anche se e.equals (null) restituisce false.

Quindi vorrei lanciare NullPointerException in modo esplicito o semplicemente lasciarlo essere lanciato la prima volta quando viene negato l'argomento null.


4

Puoi estrarre il metodo:

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
Lo "0: 1" non dovrebbe essere "0: -1"?
Rolf Kristensen,

3

Puoi progettare la tua classe in modo che sia immutabile (l'efficace 2a edizione di Java ha una grande sezione su questo, elemento 15: minimizza la mutabilità) e assicurati sulla costruzione che non siano possibili null (e usa il modello di oggetti null se necessario). Quindi è possibile saltare tutti quei controlli e presumere che i valori non siano nulli.


Sì, questa è generalmente una buona soluzione e semplifica molte cose, ma qui ero più interessato al caso in cui sono ammessi valori null, per un motivo o per l'altro, e devono essere presi in considerazione :)
Jonik,

2

Stavo cercando qualcosa di simile e questo mi è sembrato un po 'complicato, quindi l'ho fatto. Penso che sia un po 'più facile da capire. Puoi usarlo come un comparatore o come una fodera. Per questa domanda cambieresti in compareToIgnoreCase (). Così com'è, i null galleggiano. Puoi capovolgere 1, -1 se vuoi che affondino.

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

possiamo usare java 8 per fare una comparazione "null-friendly" tra oggetti. suppongo di avere una classe Boy con 2 campi: nome stringa ed età intera e voglio prima confrontare i nomi e poi le età se entrambi sono uguali.

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

e il risultato:

    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

Per il caso specifico in cui sai che i dati non avranno valori null (sempre una buona idea per le stringhe) e che i dati sono molto grandi, stai ancora facendo tre confronti prima di confrontare effettivamente i valori, se sei sicuro che questo è il tuo caso , puoi ottimizzare un pochino. YMMV come codice leggibile supera l'ottimizzazione minore:

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


    }

}

l'uscita è

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

Uno dei modi più semplici di utilizzare NullSafe Comparator è quello di utilizzare l'implementazione Spring di esso, di seguito è riportato uno dei semplici esempi a cui fare riferimento:

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

Un altro esempio di ObjectUtils di Apache. In grado di ordinare altri tipi di oggetti.

@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

Questa è la mia implementazione che uso per ordinare la mia ArrayList. le classi null sono ordinate per l'ultima.

nel mio caso, EntityPhone estende EntityAbstract e il mio contenitore è List <EntityAbstract>.

il metodo "compareIfNull ()" viene utilizzato per l'ordinamento sicuro null. Gli altri metodi sono per completezza, mostrando come confrontareIfNull può essere usato.

@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

Se vuoi un semplice hack:

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

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

se vuoi mettere null alla fine della lista, basta cambiarlo nel metodo sopra

return o2.getName().compareTo(o1.getName());
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.