Question

J'implémente la compareTo() méthode pour une classe simple comme celle-ci (pour pouvoir utiliser Collections.sort() et d'autres fonctionnalités offertes par la plate-forme Java):

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

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

Je souhaite que l'ordre naturel de ces objets soit: 1) trié par nom et 2) trié par valeur si name est identique; les deux comparaisons doivent être insensibles à la casse. Pour les deux champs, les valeurs NULL sont parfaitement acceptables. Par conséquent, compareTo ne doit pas être interrompu dans ces cas.

La solution qui nous vient à l’esprit est la suivante (j’utilise des & "clauses de garde &" ici, alors que d’autres préféreraient peut-être un point de retour unique, mais c’est à côté de la question):

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

Cela fait le travail, mais je ne suis pas parfaitement satisfait de ce code. Certes, ce n'est pas très complexe, mais il est assez prolixe et fastidieux.

La question qui se pose est la suivante: comment rendriez-vous cela moins verbeux (tout en conservant la fonctionnalité)? N'hésitez pas à consulter les bibliothèques standard Java ou Apache Commons si elles vous aident. La seule option pour rendre cela (un peu) plus simple serait-elle de mettre en œuvre mon propre & "NullSafeStringComparator &"; Et de l'appliquer pour comparer les deux champs?

Modifications 1 à 3 : Eddie a raison; correction du & "; les deux noms sont nuls &"; cas ci-dessus

À propos de la réponse acceptée

J'ai posé cette question en 2009, sur Java 1.6 bien sûr, et à l'époque la solution JDK pure d'Eddie était ma réponse acceptée préférée. Je n'ai jamais réussi à changer cela jusqu'à maintenant (2017).

Il existe également des solutions de bibliothèques tierces & # 8212; un Apache Commons Collections One 2009 et un Guava 2013. un, tous deux postés par moi & # 8212; ce que je préférais à un moment donné.

Je viens maintenant de donner la solution Java 8 propre de Lukasz Wiktor à la réponse acceptée. Cela devrait définitivement être préféré sur Java 8, et Java 8 devrait actuellement être disponible pour presque tous les projets.

Était-ce utile?

La solution

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

Autres conseils

Vous pouvez simplement utiliser Apache Commons Lang :

result = ObjectUtils.compare(firstComparable, secondComparable)

Je mettrais en œuvre un comparateur null safe. Il existe peut-être une implémentation, mais cette implémentation est si simple que je me suis toujours lancée.

Remarque: Votre comparateur ci-dessus, si les deux noms sont nuls, ne comparera même pas les champs de valeur. Je ne pense pas que c'est ce que vous voulez.

Je voudrais implémenter ceci avec quelque chose comme ce qui suit:

// 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: Correction des fautes de frappe dans l'exemple de code. C'est ce que je reçois pour ne pas l'avoir d'abord testé!

EDIT: promotion de nullSafeStringComparator en statique.

Voir au bas de cette réponse la solution mise à jour (2013) utilisant Guava.

C'est ce que j'ai finalement choisi. Il s'est avéré que nous disposions déjà d'une méthode utilitaire pour la comparaison de chaînes null-safe. La solution la plus simple consistait donc à l'utiliser. (C'est un gros code; facile de rater ce genre de chose:)

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

C’est ainsi que l’aide est définie (elle est surchargée de sorte que vous puissiez également définir si les valeurs NULL viennent en premier ou en dernier, si vous le souhaitez):

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

Il s’agit donc en gros de de la réponse d'Eddie (bien que je n'appellerais pas une méthode d'assistance statique un comparateur ) et celle de uzhin aussi.

Quoi qu'il en soit, en général, j'aurais fortement préféré La solution de Patrick , car j'estime que c'est une bonne pratique d'utiliser les bibliothèques existantes autant que possible. ( Connaissez et utilisez les bibliothèques comme dit Josh Bloch.) Mais dans ce cas, cela n’aurait pas donné le code le plus propre et le plus simple.

Éditer (2009): version d'Apache Commons Collections

En fait, voici un moyen de rendre la solution basée sur Apache Commons NullComparator plus simple. Combinez-le avec le insensible à la casse Comparator fourni dans String la 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);
}

Maintenant, c'est assez élégant, je pense. (Il ne reste qu'un petit problème: les Communs public class Metadata implements Comparable<Metadata> ne prennent pas en charge les génériques, il y a donc une affectation non vérifiée.)

Mise à jour (2013): version goyave

Près de cinq ans plus tard, voici comment je répondrais à ma question initiale. Bien sûr, si je codais en Java, j'utiliserais Guava . (Et très certainement pas Apache Commons.)

Mettez cette constante quelque part, par exemple dans " StringUtils " classe:

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

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

Bien sûr, cela est presque identique à la version d’Apache Commons (les deux utilisent JDK CASE_INSENSITIVE_ORDER ), son utilisation de Ordering étant la seule chose spécifique à la goyave. Cette version est préférable simplement parce que Guava est préférable, en tant que dépendance, à Commons Collections. (En tant que tout le monde est d'accord ).

Si vous vous interrogiez sur compound() , notez qu'il implémente <=>. C'est très pratique, en particulier pour les besoins de tri plus complexes, vous permettant par exemple de chaîner plusieurs commandes à l'aide de <=>. Lisez L'explication de la commande pour plus d'informations!

Je recommande toujours d'utiliser Apache commons car ce sera probablement mieux que celui que vous pouvez écrire vous-même. De plus, vous pouvez alors faire du "vrai" travail plutôt que de réinventer.

La classe qui vous intéresse est le Comparateur null . Il vous permet de rendre les valeurs NULL hautes ou basses. Vous lui donnez également votre propre comparateur à utiliser lorsque les deux valeurs ne sont pas nulles.

Dans votre cas, vous pouvez avoir une variable membre statique qui effectue la comparaison, puis votre méthode compareTo référence simplement celle-là.

Quelque chose comme

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

}

Même si vous décidez de lancer le vôtre, gardez cette classe à l'esprit car elle est très utile pour commander des listes contenant des éléments nuls.

Je sais que cela ne répond peut-être pas directement à votre question, car vous avez dit que les valeurs nulles doivent être prises en charge.

Mais je tiens simplement à noter que la prise en charge des valeurs NULL dans compareTo n'est pas conforme au contrat de compareTo décrit dans le document officiel javadocs pour Comparable :

  

Notez que null n'est une instance d'une classe et e.compareTo (null)   devrait lancer une exception NullPointerException même si e.equals (null) renvoie   faux.

Donc, je voudrais soit lancer explicitement NullPointerException, soit le laisser être lancé pour la première fois lorsqu'un argument nul est déréférencé.

Vous pouvez extraire la méthode:

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

}

Vous pouvez concevoir votre classe comme étant immuable (Effective Java 2nd Ed. contient une excellente section à ce sujet, article 15: minimisation de la mutabilité) et assurez-vous, lors de la construction, qu'aucune valeur NULL n'est possible (et utilisez le modèle d'objet null si nécessaire). Ensuite, vous pouvez ignorer toutes ces vérifications et supposer en toute sécurité que les valeurs ne sont pas nulles.

Je cherchais quelque chose de similaire et cela semblait un peu compliqué, alors je l’ai fait. Je pense que c'est un peu plus facile à comprendre. Vous pouvez l'utiliser comme comparateur ou comme doublure. Pour cette question, vous devez changer to compareToIgnoreCase (). En l'état, les valeurs nuls flottent. Vous pouvez retourner le 1, -1 si vous voulez qu’ils coulent.

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

nous pouvons utiliser java 8 pour faire une comparaison null-friendly entre objets. suppose que je possède une classe de garçon avec 2 champs: nom de chaîne et âge entier et je veux d’abord comparer les noms, puis les âges si les deux sont égaux.

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

et le résultat:

    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

Si quelqu'un utilise Spring, il existe une classe org.springframework.util.comparator.NullSafeComparator qui le fait également pour vous. Il suffit de décorer votre propre comparable avec ça comme ça

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


    }

}

la sortie est

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]

Un des moyens simples de d'utiliser NullSafe Comparator consiste à utiliser l'implémentation Spring de celle-ci. Vous trouverez ci-dessous l'un des exemples simples à suivre:

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 autre exemple Apache ObjectUtils. Capable de trier d'autres types d'objets.

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

C’est mon implémentation que j’utilise pour trier mon ArrayList. les classes nulles sont triées au dernier rang.

pour mon cas, EntityPhone étend EntityAbstract et mon conteneur est List < EntityAbstract & Gt;.

le " compareIfNull () & "; La méthode est utilisée pour un tri sûr nul. Les autres méthodes sont complètes et montrent comment utiliser 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;
}

Pour le cas spécifique où vous savez que les données n'auront pas de valeur NULL (toujours une bonne idée pour les chaînes) et que les données sont vraiment volumineuses, vous effectuez encore trois comparaisons avant de comparer les valeurs, si vous savez pour Bien sûr que ce soit votre cas , vous pouvez optimiser un bit. YMMV en tant que code lisible l'emporte sur l'optimisation mineure:

        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);
Licencié sous: CC-BY-SA avec attribution
Non affilié à StackOverflow
scroll top