web-dev-qa-db-fra.com

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

J'implémente la méthode compareTo() 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 veux que le 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, donc compareTo ne doit pas être rompu dans ces cas. 

La solution qui me vient à l’esprit est la suivante (j’utilise les «clauses de garde» ici alors que d’autres préfèreraient peut-être un point de retour unique, mais c’est à côté du 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 satisfait de ce code. Certes, ce n’est pas un complexe {très}, mais il est assez prolixe et fastidieux.

La question est: 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. 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?

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

A 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 préférée. Je n'ai jamais réussi à changer cela jusqu'à maintenant (2017).

Il existe également des solutions de bibliothèques tierces - une version 2009 d'Apache Commons Collections et une version 2013 de Guava, toutes deux postées par moi, que je préférais à un moment donné.

Je fais maintenant la solution propre Java 8 de Lukasz Wiktor la réponse acceptée. Cela devrait certainement être préféré sur Java 8, et de nos jours Java 8 devrait être disponible pour presque tous les projets.

130
Jonik

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);
}
145
Lukasz Wiktor

Vous pouvez simplement utiliser Apache Commons Lang :

result = ObjectUtils.compare(firstComparable, secondComparable)
183
Dag

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 tu veux.

Je mettrais cela en œuvre 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 le tester d'abord!

EDIT: promotion de nullSafeStringComparator en statique.

88
Eddie

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


C'est ce que je suis finalement allé avec. 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);
}

Voici comment l'assistant est défini (il est surchargé 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) { ... }

C'est donc essentiellement la même chose que la réponse de 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 je pense que c'est une bonne pratique d'utiliser les bibliothèques établies 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.

Edit (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 la classe String:

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 commun NullComparator ne prend 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 5 ans plus tard, voici comment je répondrais à ma question initiale. Si je codais en Java, j'utiliserais (bien sûr) Guava . (Et très 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()

Ensuite, 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, cela est presque identique à la version d'Apache Commons (les deux utilisent le CASE_INSENSITIVE_ORDER de JDK, l'utilisation de nullsLast() é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. (Comme tout le monde est d'accord .)

Si vous vous interrogiez sur Ordering , notez qu'il implémente Comparator. C'est très pratique, en particulier pour les besoins de tri plus complexes, vous permettant par exemple de chaîner plusieurs commandes en utilisant compound(). Lire Ordre expliqué pour plus!

21
Jonik

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 faire des valeurs nulles haut ou bas. 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 fait simplement référence à cela.

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.

13
Patrick

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 compareTo décrit dans official 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 ou simplement le laisser être lancé pour la première fois lorsqu'un argument nul est déréférencé.

7
Piotr Sobczyk

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
Yoni Roit

Vous pouvez concevoir votre classe comme étant immuable (Effective Java 2nd Ed. Contient une excellente section à ce sujet, élément 15: minimiser la mutabilité) et vous assurer, 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.

3
Fabian Steeg

nous pouvons utiliser Java 8 pour faire une comparaison null-friendly entre object .

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
Leo Ng

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);
    }
}
2
Dustin
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
Nikhil Kumar K

L’un des moyens simples de d’utiliser NullSafe Comparator consiste à utiliser son implémentation Spring. 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;
    }
1
Amandeep Singh

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 avec certitude qu'il s'agit de votre case , 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);
0
kisna

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

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

la méthode "compareIfNull ()" est utilisée pour un tri sûr nul. Les autres méthodes sont complètes, 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
Angel Koh

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