Domanda

Sto implementando il metodo compareTo() 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 ordinamento 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 nulli sono perfettamente accettabili, pertanto in questi casi compareTo non deve essere interrotto.

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 è 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 suo lavoro, ma non sono perfettamente soddisfatto di questo codice. Certo, non è molto complesso, ma è piuttosto prolisso e noioso.

La domanda è: come renderesti questo meno prolisso (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 & Quot; NullSafeStringComparator & Quot; e applicarlo per confrontare entrambi i campi?

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

Informazioni sulla risposta accettata

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

Ci sono anche soluzioni di libreria di terze parti & # 8212; una Apache Commons Collections 2009 e una Guava 2013 uno, entrambi pubblicati da me & # 8212; che preferivo ad un certo punto nel tempo.

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

È stato utile?

Soluzione

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

Altri suggerimenti

Puoi semplicemente usare Apache Commons Lang :

result = ObjectUtils.compare(firstComparable, secondComparable)

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 sono nulli, non confronterà nemmeno i campi del valore. Non credo 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 corretti nell'esempio di codice. Questo è quello che ottengo per non averlo provato prima!

EDIT: promosso nullSafeStringComparator su statico.

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 genere 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 questo è essenzialmente lo stesso di La risposta di Eddie (anche se non definirei un metodo di supporto statico un comparatore ) e anche quello di Uzhin .

Comunque, in generale, avrei favorito fortemente La soluzione di Patrick , poiché ritengo sia una buona pratica utilizzare 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 delle collezioni Apache Commons

In realtà, ecco un modo per rendere la soluzione basata su Apache Commons NullComparator più semplice. Abbinalo al insensibile al maiuscolo Comparator fornito nella String classe:

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: Commons public class Metadata implements Comparable<Metadata> non supporta 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 es. tra " StringUtils " classe:

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

Quindi, in nullsLast():

@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 di Ordering essere l'unica cosa specifica di Guava. Questa versione è preferibile semplicemente perché Guava è preferibile, come dipendenza, alle Collezioni Commons. (Come tutti sono d'accordo .)

Se ti chiedevi compound() , nota che implementa <=>. È piuttosto utile soprattutto per esigenze di ordinamento più complesse, che consente ad esempio di concatenare diversi ordini usando <=>. Leggi Spiegazione degli ordini per ulteriori informazioni!

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" piuttosto che reinventare.

La classe che ti interessa è Null Comparator . 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 compareTo metodo fa solo riferimento a questo.

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 farne uno tuo, tieni a mente questa classe poiché è molto utile quando ordini liste che contengono elementi null.

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 ufficiale javadocs per Comparable :

  

Nota che null non è un'istanza di nessuna classe ed e.compareTo (null)   dovrebbe generare una NullPointerException anche se restituisce e.equals (null)   falso.

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

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

}

Potresti progettare la tua classe in modo che sia immutabile (l'Efficace Java 2nd Ed. ha un'ottima sezione su questo, Item 15: Riduci al minimo la mutabilità) e assicurati sulla costruzione che non siano possibili null (e usa il modello di oggetti null se necessario). Quindi puoi saltare tutti quei controlli e presumere che i valori non siano nulli.

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

possiamo usare java 8 per fare una comparazione amichevole tra oggetti. suppongo di avere una classe Boy con 2 campi: nome stringa ed età intero e voglio prima confrontare i nomi e poi l'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

Nel caso in cui qualcuno utilizzi Spring, esiste una classe org.springframework.util.comparator.NullSafeComparator che lo fa anche per te. Decora il tuo paragonabile con esso in questo modo

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

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

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'output è

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]

Uno dei modi semplici di utilizzando NullSafe Comparator è quello di utilizzare la sua implementazione primaverile, di seguito è riportato un semplice esempio di 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;
    }

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

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

per il mio caso, EntityPhone estende EntityAbstract e il mio contenitore è List < EntityAbstract gt &;.

il " compareIfNull () " Il metodo viene utilizzato per l'ordinamento sicuro null. Gli altri metodi sono per completezza, mostrando come si può usare 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;
}

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 davvero grandi, stai ancora facendo tre confronti prima di confrontare effettivamente i valori, se sai per sicuramente 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);
Autorizzato sotto: CC-BY-SA insieme a attribuzione
Non affiliato a StackOverflow
scroll top