Comment simplifier une implémentation de null-safe compareTo ()?

j'implémente 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 l'ordre naturel 151990920" pour que ces objets soient: 1) triés par nom et 2) triés 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, donc compareTo ne doit pas se casser dans ces cas.

la solution qui me vient à l'esprit est dans le sens suivant (j'utilise des "clauses de garde" ici alors que d'autres pourraient préférer un seul point de retour, mais ce n'est pas le point):

// 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 heureux avec ce code. Certes, il n'est pas très complexe, mais est tout à fait verbeux et fastidieux.

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

modifications 1-3 : droit D'Eddie; fixe le cas "les deux noms sont nuls" ci-dessus

à propos de la réponse acceptée

j'ai demandé cette question en 2009, sur Java 1.6 bien sûr, et à l'époque la solution pure JDK par 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 y a aussi 3rd party library solutions - a 2009 Apache Commons Collections one et a 2013 Guava one, tous deux postés par moi-que j'ai préféré à un moment donné.

j'ai maintenant fait le propre de la Java 8 solution par Lukasz Wiktor les accepté de répondre. Cela devrait certainement être préféré si sur Java 8, et ces jours Java 8 devrait être disponible pour presque tous les projets.

126
demandé sur Community 2009-01-27 02:25:14

16 réponses

Utilisant 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);
}
140
répondu Lukasz Wiktor 2014-06-23 14:54:14

vous pouvez simplement utiliser Apache Commons Lang :

result = ObjectUtils.compare(firstComparable, secondComparable)
171
répondu Dag 2017-06-05 14:56:47

je mettrais en place un comparateur de sécurité nul. Il peut y avoir une implémentation là-bas, mais c'est tellement simple à mettre en œuvre que j'ai toujours utilisé la mienne.

Note: 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 est ce que vous voulez.

je voudrais implémenter ceci avec quelque chose comme ceci:

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

modifier: fixe fautes de frappe dans le code de l'échantillon. C'est ce que je gagne à ne pas l'avoir testé en premier!

EDIT: promu nullSafeStringComparator à statique.

85
répondu Eddie 2016-12-16 22:33:54

Voir le bas de cette réponse pour la mise à jour (2013) de la solution à l'aide de la Goyave.


C'est ce que j'ai finalement suivi. Il s'est avéré que nous avions déjà une méthode d'utilité pour la comparaison de chaînes null-safe, donc la solution la plus simple était de faire usage de cela. (C'est une grosse base de code; facile de manquer 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 le helper est défini (il est surchargé de sorte que vous pouvez également définir si les valeurs null venir en premier ou en dernier, si vous voulez):

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

donc c'est essentiellement la même chose que la réponse D'Eddie (bien que je ne voudrais pas appeler une méthode d'aide statique un comparateur ) et celui d'uzhin aussi.

de toute façon, en général, j'aurais fortement favorisé solution de Patrick , car je pense que c'est une bonne pratique d'utiliser des bibliothèques établies chaque fois que possible. ( Connaître et utiliser les bibliothèques comme Josh Bloch dit.) Mais dans ce cas, cela n'aurait pas donné le code le plus propre et le plus simple.

Edit (2009): Apache Commons Collections version

en fait, voici une façon de rendre la solution basée sur Apache Commons NullComparator plus simple. Combinez-le avec le cas-insensible Comparator prévu dans 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);
}

maintenant c'est assez élégant, je pense. (Il ne reste qu'un petit problème: le Commons NullComparator ne supporte pas les génériques, il y a donc une mission non contrôlée.)

mise à Jour (2013): Goyave version

presque 5 ans plus tard, voici comment j'aborderais ma question originale. Si je codais en Java, j'utiliserais (bien sûr) Guava . (Et très certainement pas Apache Commons.)

mettez cette constante quelque part, p.ex. 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 JDK CASE_INSENSITIVE_ORDER ), l'utilisation de nullsLast() étant la seule chose spécifique à la goyave. Cette version est préférable simplement parce que la goyave est préférable, comme dépendance, aux Collections communes. (Comme tout le monde est d'accord .)

si vous vous demandiez Ordering , notez qu'il met en œuvre Comparator . C'est assez pratique surtout pour les besoins de tri plus complexes, vous permettant par exemple de enchaîner plusieurs commandes en utilisant compound() . Lire commande expliquée pour plus!

21
répondu Jonik 2017-05-23 11:47:20

je recommande toujours L'utilisation D'Apache commons car il sera probablement meilleur qu'un que vous pouvez écrire vous-même. De plus, vous pouvez alors faire du "vrai" travail plutôt que de le réinventer.

La classe qui vous intéresse est le Null Comparateur . Il vous permet de faire nulls haut ou bas. Vous lui donnez aussi votre propre comparateur à utiliser lorsque les deux valeurs ne sont pas nulles.

Dans votre cas, vous pouvez avoir une variable membre statique que la comparaison et puis votre compareTo méthode des références.

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 qui contiennent des éléments nuls.

13
répondu Patrick 2009-01-27 00:04:44

je sais que cela peut ne pas être une réponse directe à votre question, parce que vous avez dit que les valeurs nulles doivent être supportées.

mais je veux juste noter que supporter des nulls dans compareTo n'est pas en ligne avec le contrat compareTo décrit dans le" Officiel 151930920 "javadocs pour Comparable :

noter que null n'est une instance d'aucune classe, et E. compareTo (null) devrait jeter une NullPointerException même si e.equals(null) renvoie faux.

donc je lancerais NullPointerException explicitement ou juste la laisser lancer la première fois quand l'argument null est déréférencé.

7
répondu Piotr Sobczyk 2014-10-30 09:03:00

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

}

4
répondu Yoni Roit 2009-01-26 23:38:38

, Vous pouvez concevoir votre classe pour être immuable (à compter de Java 2nd Ed. a une grande section sur ce, point 15: minimiser la mutabilité) et s'assurer sur la construction qu'aucun nulls sont possibles (et utiliser le nul object pattern si nécessaire). Ensuite, vous pouvez sauter toutes ces vérifications et en toute sécurité supposer que les valeurs ne sont pas nulles.

3
répondu Fabian Steeg 2009-01-26 23:37:32

je cherchais quelque chose de similaire et cela semblait un peu compliqué alors j'ai fait ceci. Je pense que c'est un peu plus facile à comprendre. Vous pouvez L'utiliser comme un comparateur ou comme une doublure. Pour cette question, vous changeriez en compareToIgnoreCase (). Comme est, les valeurs null flotter vers le haut. Vous pouvez retourner le 1, -1 si vous voulez couler.

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
répondu Dustin 2014-10-07 16:38:53

nous pouvons utiliser java 8 pour faire une comparaison "null-friendly" entre les objets. supposez que j'ai une classe de garçon avec 2 champs: Nom de chaîne et L'âge entier et je veux d'abord comparer les noms et 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
2
répondu Leo Ng 2016-03-18 18:24:19

dans le cas où quelqu'un utilise le ressort, il ya une org de classe.springframework.util.comparateur.NullSafeComparator qui fait cela pour vous. Il suffit de décorer votre propre comparable avec elle comme cela

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

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

1
répondu Björn Bergenheim 2016-01-19 12:33:46
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);
        }


    }

}

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
répondu Nikhil Kumar K 2017-02-13 16:48:06

un autre exemple D'objet Apache. 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
répondu snp0k 2015-08-11 15:39:24

Utilisant Java 7 :

public int compareNullFirst(String str1, String str2) {
    if (Objects.equals(str1, str2)) {
        return 0;
    }
    else {
        return str1 == null ? -1 : (str2 == null ? 1 : str1.compareToIgnoreCase(str2));
    }
}
0
répondu Craigo 2016-01-20 23:34:39

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

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

la méthode "compareIfNull ()" est utilisée pour le tri 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
répondu Angel Koh 2016-03-15 18:43:38

pour le cas spécifique où vous savez que les données n'auront pas nulls (toujours une bonne idée pour les chaînes de caractères) et les données sont vraiment grandes, vous faites encore trois comparaisons avant de réellement comparer les valeurs, si vous êtes sûr que c'est votre cas , vous pouvez optimiser un bit tad. YMMV comme un code lisible atouts mineur d'optimisation:

        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
répondu kisna 2016-07-12 03:10:40