Comment simplifier une implémentation de compareTo () à sécurité nulle?


157

J'implémente une compareTo()méthode pour une classe simple comme celle-ci (pour pouvoir utiliser Collections.sort()et d'autres goodies offerts 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 veux que l' ordre naturel de ces objets soit: 1) trié par nom et 2) trié par valeur si le nom est le même; les deux comparaisons doivent être insensibles à la casse. Pour les deux champs, les valeurs nulles sont parfaitement acceptables et compareTone doivent donc pas être interrompues dans ces cas.

La solution qui me vient à l'esprit est la suivante (j'utilise ici des «clauses de garde», tandis que d'autres préfèrent peut-être un seul point de retour, mais ce n'est pas 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 assez verbeux et fastidieux.

La question est, comment feriez-vous cela moins verbeux (tout en conservant la fonctionnalité)? N'hésitez pas à vous référer aux bibliothèques standard Java ou à Apache Commons si elles vous aident. Est-ce que la seule option pour rendre cela (un peu) plus simple serait d'implémenter mon propre "NullSafeStringComparator" et de l'appliquer pour comparer les deux champs?

Modifications 1 à 3 : à droite d'Eddie; correction du cas "les deux noms sont nuls" 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 eu l'occasion de changer cela jusqu'à maintenant (2017).

Il existe également des solutions de bibliothèque tierces - une Apache Commons Collections de 2009 et une 2013 Guava, toutes deux publiées par moi - que j'ai préférées à un moment donné.

J'ai maintenant fait le nettoyage solution Java 8 de Lukasz Wiktor la réponse acceptée. Cela devrait certainement être préféré si sur Java 8, et ces jours-ci, Java 8 devrait être disponible pour presque tous les projets.


Réponses:


173

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

7
Je supporte l'utilisation de trucs intégrés Java 8 en faveur d'Apache Commons Lang, mais ce code Java 8 est assez laid et toujours verbeux. Je m'en tiendrai à org.apache.commons.lang3.builder.CompareToBuilder pour le moment.
jschreiner

1
Cela ne fonctionne pas pour Collections.sort (Arrays.asList (null, val1, null, val2, null)) car il essaiera d'appeler compareTo () sur un objet nul. Pour être honnête, cela ressemble à un problème avec le cadre des collections, essayer de trouver comment résoudre ce problème.
Pedro Borges

3
@PedroBorges L'auteur a posé des questions sur le tri des objets conteneurs qui possèdent des champs triables (où ces champs peuvent être nuls), pas sur le tri des références de conteneurs nulles. Ainsi, bien que votre commentaire soit correct, cela Collections.sort(List)ne fonctionne pas lorsque la liste contient des valeurs nulles, le commentaire n'est pas pertinent pour la question.
Scrubbie

211

Vous pouvez simplement utiliser Apache Commons Lang :

result = ObjectUtils.compare(firstComparable, secondComparable)

4
(@Kong: Cela prend en charge la sécurité nulle mais pas l'insensibilité à la casse, qui était un autre aspect de la question d'origine. Ainsi, ne change pas la réponse acceptée.)
Jonik

3
De plus, dans mon esprit, Apache Commons ne devrait pas être la réponse acceptée en 2013. (Même si certains sous-projets sont mieux entretenus que d'autres.) Guava peut être utilisé pour réaliser la même chose ; voir nullsFirst()/ nullsLast().
Jonik

8
@Jonik Pourquoi pensez-vous qu'Apache Commons ne devrait pas être la réponse acceptée en 2013?
reallynice

1
Une grande partie d'Apache Commons est héritée / mal entretenue / de mauvaise qualité. Il existe de meilleures alternatives pour la plupart des choses qu'il fournit, par exemple dans Guava qui est une bibliothèque de très haute qualité partout, et de plus en plus dans JDK lui-même. Vers 2005 oui, Apache Commons était la merde, mais ces jours-ci, il n'y en a pas besoin dans la plupart des projets. (Bien sûr, il y a des exceptions; par exemple, si j'avais besoin d'un client FTP pour une raison quelconque, j'utiliserais probablement celui d'Apache Commons Net, et ainsi de suite.)
Jonik

6
@Jonik, comment répondriez-vous à la question en utilisant Guava? Votre affirmation selon laquelle Apache Commons Lang (package org.apache.commons.lang3) est «hérité / mal entretenu / de mauvaise qualité» est fausse ou au mieux sans fondement. Commons Lang3 est facile à comprendre et à utiliser, et il est activement maintenu. C'est probablement ma bibliothèque la plus souvent utilisée (à part Spring Framework et Spring Security) - la classe StringUtils avec ses méthodes de sécurité null rend la normalisation d'entrée triviale, par exemple.
Paul

93

J'implémenterais un comparateur sûr nul. Il peut y avoir une implémentation là-bas, mais c'est tellement simple à implémenter que j'ai toujours roulé la mienne.

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 ce soit ce que tu veux.

Je mettrais en œuvre cela 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 j'obtiens si je ne l'ai pas testé en premier!

EDIT: promotion de nullSafeStringComparator en statique.


2
Concernant le "if" imbriqué ... je trouve le if imbriqué moins lisible dans ce cas, donc je l'évite. Oui, il y aura parfois une comparaison inutile à cause de cela. final pour les paramètres n'est pas nécessaire, mais c'est une bonne idée.
Eddie

31
Sweet use of XOR
James McMahon

9
@phihag - Je sais que cela fait plus de 3 ans, MAIS ... le finalmot-clé n'est pas vraiment nécessaire (le code Java est déjà verbeux tel quel.) Cependant, il empêche la réutilisation des paramètres en tant que variables locales (une pratique de codage terrible.) notre compréhension collective des logiciels s'améliore avec le temps, nous savons que les choses doivent être finales / const / inmuables par défaut. Je préfère donc un peu plus de verbosité dans l'utilisation des finaldéclarations de paramètres (aussi triviale que soit la fonction) pour obtenir inmutability-by-quasi-default.) La surcharge de compréhensibilité / maintenabilité est négligeable dans le grand schéma des choses.
luis.espinal

24
@James McMahon Je ne suis pas d'accord. Xor (^) pourrait simplement être remplacé par différent de (! =). Il compile même avec le même code d'octet. L'utilisation de! = Vs ^ est juste une question de goût et de lisibilité. Donc, à en juger par le fait que vous avez été surpris, je dirais que cela n'a pas sa place ici. Utilisez xor lorsque vous essayez de calculer une somme de contrôle. Dans la plupart des autres cas (comme celui-ci), restons fidèles à! =.
bvdb

5
Il est facile d'étendre cette réponse en remplaçant String par T, T déclaré comme <T étend Comparable <T>> ... et nous pouvons ensuite comparer en toute sécurité tous les objets comparables nullables
Thierry

20

Voir le bas de cette réponse pour une 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 à valeur nulle, la solution la plus simple était donc de l'utiliser. (C'est une grosse base de code; il est 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);
}

Voici comment l'assistant est défini (il est surchargé afin que vous puissiez également définir si les valeurs nulles viennent en premier ou en dernier, si vous le souhaitez):

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

C'est donc essentiellement la même chose que la réponse d'Eddie (même si je n'appellerais pas une méthode d'assistance statique un comparateur ) et celle d'uzhin aussi.

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

Edit (2009): version Apache Commons Collections

En fait, voici un moyen de NullComparatorsimplifier la solution basée sur Apache Commons . Combinez-le avec le non sensible à la casseComparator fourni en 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);
}

Maintenant, c'est assez élégant, je pense. (Il reste un petit problème: les communes NullComparatorne prennent pas en charge les génériques, il y a donc une affectation non vérifiée.)

Mise à jour (2013): version Guava

Près de 5 ans plus tard, voici comment j'aborderai ma question initiale. Si je codais en Java, j'utiliserais (bien sûr) Guava . (Et certainement pas Apache Commons.)

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

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

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

Bien sûr, c'est presque identique à la version Apache Commons (les deux utilisent CASE_INSENSITIVE_ORDER de JDK ), l'utilisation nullsLast()étant la seule chose spécifique à Guava. Cette version est préférable simplement parce que Guava est préférable, en tant que dépendance, aux collections communes. (Comme tout le monde est d'accord .)

Si vous vous posez des questions Ordering, notez qu'il met en œuvre Comparator. C'est assez pratique, en particulier pour les besoins de tri plus complexes, vous permettant par exemple d'enchaîner plusieurs commandes à l'aide de compound(). Lisez la commande expliquée pour en savoir plus!


2
String.CASE_INSENSITIVE_ORDER rend vraiment la solution beaucoup plus propre. Bonne mise à jour.
Patrick

2
Si vous utilisez quand même Apache Commons, ComparatorChainvous n'avez donc pas besoin d'une propre compareTométhode.
amoebe

13

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 un «vrai» travail plutôt que réinventer.

La classe qui vous intéresse est le comparateur nul . Il vous permet de rendre les valeurs nulles 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 compareTométhode y fait simplement référence.

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 la vôtre, gardez cette classe à l'esprit car elle est très utile lors de l'ordre des listes qui contiennent des éléments nuls.


Merci, j'espérais qu'il y aurait quelque chose comme ça à Commons! Dans ce cas, cependant, je n'ai pas fini par l'utiliser: stackoverflow.com/questions/481813/…
Jonik

Je viens de réaliser que votre approche peut être simplifiée en utilisant String.CASE_INSENSITIVE_ORDER; voir ma réponse de suivi modifiée.
Jonik

C'est bien mais la vérification "if (other == null) {" ne devrait pas être là. Le Javadoc pour Comparable indique que compareTo doit lancer une NullPointerException si other est null.
Daniel Alexiuc

7

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 veux juste noter que la prise en charge des nulls dans compareTo n'est pas conforme au contrat compareTo décrit dans les javadocs officiels de Comparable :

Notez que null n'est une instance d'aucune classe et que e.compareTo (null) doit lever une exception NullPointerException même si e.equals (null) renvoie false.

Donc, je lancerais NullPointerException explicitement ou je le laisserais simplement être lancé la première fois lorsqu'un argument nul est déréférencé.


4

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

}


2
Le "0: 1" ne devrait-il pas être "0: -1"?
Rolf Kristensen

3

Vous pouvez concevoir votre classe pour qu'elle soit immuable (Effective Java 2nd Ed. A une grande section à ce sujet, Point 15: Minimiser la mutabilité) et vous assurer lors de la construction qu'aucune valeur nulle n'est possible (et utiliser le modèle d'objet nul si nécessaire). Ensuite, vous pouvez ignorer toutes ces vérifications et supposer en toute sécurité que les valeurs ne sont pas nulles.


Oui, c'est généralement une bonne solution, et cela simplifie beaucoup de choses - mais ici j'étais plus intéressé par le cas où les valeurs nulles sont autorisées, pour une raison ou une autre, et doivent être prises en compte :)
Jonik

2

Je cherchais quelque chose de similaire et cela me 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 unique. Pour cette question, vous changeriez pour compareToIgnoreCase (). En l'état, les valeurs nulles flottent. Vous pouvez inverser 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);
    }
}

2

nous pouvons utiliser java 8 pour faire une comparaison amicale entre les objets. supposé que j'ai une classe Boy 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


1

Pour le cas spécifique où vous savez que les données n'auront pas de valeurs nulles (toujours une bonne idée pour les chaînes) et que les données sont vraiment volumineuses, vous faites toujours trois comparaisons avant de comparer les valeurs, si vous savez avec certitude que c'est votre cas , vous pouvez optimiser un peu. 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);

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


    }

}

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]

1

L'un des moyens simples d' utiliser NullSafe Comparator est d'utiliser l'implémentation Spring de celui-ci, ci-dessous est l'un des exemples simples à consulter:

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 autre exemple d'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());
}

0

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

pour mon cas, EntityPhone étend EntityAbstract et mon conteneur est List <EntityAbstract>.

la méthode "compareIfNull ()" est utilisée pour le tri sûr nul. Les autres méthodes sont pour l'exhaustivité, montrant comment compareIfNull peut être utilisé.

@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

Si vous voulez un simple Hack:

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

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

si vous voulez mettre des valeurs nulles à la fin de la liste, changez simplement cela dans le metod ci-dessus

return o2.getName().compareTo(o1.getName());
En utilisant notre site, vous reconnaissez avoir lu et compris notre politique liée aux cookies et notre politique de confidentialité.
Licensed under cc by-sa 3.0 with attribution required.