web-dev-qa-db-ja.com

複数のフィールドでオブジェクトを比較する方法

いくつかのフィールドを持つオブジェクトがあり、それらを比較することができるとします。

public class Person {

    private String firstName;
    private String lastName;
    private String age;

    /* Constructors */

    /* Methods */

}

そのため、この例では、次のことを確認してください。

a.compareTo(b) > 0

あなたは、ある人の姓がbよりも前にあるのか、それともaがbよりも古いのかなどを尋ねるかもしれません。

不要な混乱やオーバーヘッドを追加することなくこれらの種類のオブジェクト間の多重比較を可能にするための最もクリーンな方法は何ですか?

  • Java.lang.Comparableインターフェースは1フィールドのみの比較を可能にします
  • 私の考えでは、多数の比較メソッド(compareByFirstName()compareByAge()など)を追加することは雑然としています。

それで、これについて最善の方法は何ですか?

194
Yuval Adam

2つのComparatorオブジェクトを比較するPersonを実装すると、好きなだけフィールドを調べることができます。どのフィールドと比較するかを指示する変数をコンパレータに入れることができます。ただし、複数のコンパレータを記述するほうが簡単です。

70
Elie

Java 8の場合

Comparator.comparing((Person p)->p.firstName)
          .thenComparing(p->p.lastName)
          .thenComparingInt(p->p.age);

アクセサメソッドがある場合:

Comparator.comparing(Person::getFirstName)
          .thenComparing(Person::getLastName)
          .thenComparingInt(Person::getAge);

クラスがComparableを実装している場合は、compareToメソッドでそのようなコンパレータを使用できます。

@Override
public int compareTo(Person o){
    return Comparator.comparing(Person::getFirstName)
              .thenComparing(Person::getLastName)
              .thenComparingInt(Person::getAge)
              .compare(this, o);
}
312
Display Name

Comparable <Person>を実装するべきです。 (単純にするために)すべてのフィールドがNULLにならないと仮定して、ageはintであり、compareのランク付けはfirst、last、ageであり、compareToメソッドは非常に単純です。

public int compareTo(Person other) {
    int i = firstName.compareTo(other.firstName);
    if (i != 0) return i;

    i = lastName.compareTo(other.lastName);
    if (i != 0) return i;

    return Integer.compare(age, other.age);
}
149
Steve Kuo

(from コードハウス

乱雑で複雑:手によるソート

Collections.sort(pizzas, new Comparator<Pizza>() {  
    @Override  
    public int compare(Pizza p1, Pizza p2) {  
        int sizeCmp = p1.size.compareTo(p2.size);  
        if (sizeCmp != 0) {  
            return sizeCmp;  
        }  
        int nrOfToppingsCmp = p1.nrOfToppings.compareTo(p2.nrOfToppings);  
        if (nrOfToppingsCmp != 0) {  
            return nrOfToppingsCmp;  
        }  
        return p1.name.compareTo(p2.name);  
    }  
});  

これは多くのタイピング、メンテナンスを必要とし、エラーを起こしやすいです。

反射的な方法:BeanComparatorによるソート

ComparatorChain chain = new ComparatorChain(Arrays.asList(
   new BeanComparator("size"), 
   new BeanComparator("nrOfToppings"), 
   new BeanComparator("name")));

Collections.sort(pizzas, chain);  

明らかにこれはより簡潔ですが、代わりに文字列を使用してフィールドへの直接参照を失うため、さらにエラーが発生しやすくなります。フィールドの名前が変更された場合、コンパイラは問題を報告することすらありません。さらに、このソリューションではリフレクションを使用しているため、ソートがはるかに遅くなります。

アクセス:Google GuavaのComparisonChainによる並べ替え

Collections.sort(pizzas, new Comparator<Pizza>() {  
    @Override  
    public int compare(Pizza p1, Pizza p2) {  
        return ComparisonChain.start().compare(p1.size, p2.size).compare(p1.nrOfToppings, p2.nrOfToppings).compare(p1.name, p2.name).result();  
        // or in case the fields can be null:  
        /* 
        return ComparisonChain.start() 
           .compare(p1.size, p2.size, Ordering.natural().nullsLast()) 
           .compare(p1.nrOfToppings, p2.nrOfToppings, Ordering.natural().nullsLast()) 
           .compare(p1.name, p2.name, Ordering.natural().nullsLast()) 
           .result(); 
        */  
    }  
});  

これははるかに優れていますが、最も一般的なユースケースではいくつかの定型コードを必要とします。デフォルトではnull値の値を小さくする必要があります。 nullフィールドについては、その場合の対処方法をGuavaに追加の指令を提供する必要があります。あなたが何か特別なことをしたいのであればこれは柔軟なメカニズムですが、多くの場合デフォルトのケース(すなわち1、a、b、z、null)が欲しいです。

Apache Commonsを使ったソートCompareToBuilder

Collections.sort(pizzas, new Comparator<Pizza>() {  
    @Override  
    public int compare(Pizza p1, Pizza p2) {  
        return new CompareToBuilder().append(p1.size, p2.size).append(p1.nrOfToppings, p2.nrOfToppings).append(p1.name, p2.name).toComparison();  
    }  
});  

GuavaのComparisonChainと同様に、このライブラリクラスは複数のフィールドで簡単にソートできますが、null値(つまり、1、a、b、z、null)に対する既定の動作も定義します。ただし、独自のコンパレータを指定しない限り、他に何も指定することはできません。

このように

結局のところ、それはフレーバーと柔軟性(GuavaのComparisonChain)の必要性、そして簡潔なコード(ApacheのCompareToBuilder)の必要性に帰着します。

ボーナス方式

私はMultiComparatorname__の中で、優先度の順に/ CodeReview の順に複数のコンパレータを組み合わせたNiceソリューションを見つけました。

class MultiComparator<T> implements Comparator<T> {
    private final List<Comparator<T>> comparators;

    public MultiComparator(List<Comparator<? super T>> comparators) {
        this.comparators = comparators;
    }

    public MultiComparator(Comparator<? super T>... comparators) {
        this(Arrays.asList(comparators));
    }

    public int compare(T o1, T o2) {
        for (Comparator<T> c : comparators) {
            int result = c.compare(o1, o2);
            if (result != 0) {
                return result;
            }
        }
        return 0;
    }

    public static <T> void sort(List<T> list, Comparator<? super T>... comparators) {
        Collections.sort(list, new MultiComparator<T>(comparators));
    }
}

もちろん、Apache Commons Collectionsにはすでにこの機能があります。

ComparatorUtils.chainedComparator(compilerCollection)

Collections.sort(list, ComparatorUtils.chainedComparator(comparators));
58
Benny Bottema

@Patrick複数のフィールドを連続してソートするには、 ComparatorChain を試してください。

ComparatorChainは、1つ以上のコンパレータを順番にラップするコンパレータです。 ComparatorChainは、1)単一のComparatorがゼロ以外の結果を返す(そしてその結果が返される)、または2)ComparatorChainが使い果たされる(そして0が返される)まで、各Comparatorを順番に呼び出します。このタイプのソートは、SQLの複数列ソートと非常によく似ています。このクラスを使用すると、リストをソートするときにJavaクラスでそのような動作をエミュレートできます。

SQL風のソートをさらに容易にするために、リスト内の任意の単一のコンパレータの順序を逆にすることができます。

Compare(Object、Object)が呼び出された後で新しいComparatorsを追加するメソッドまたは昇順または降順のソートを変更するメソッドを呼び出すと、UnsupportedOperationExceptionが発生します。ただし、基になるコンパレータのリストまたは並べ替え順序を定義するBitSetを変更しないように注意してください。

ComparatorChainのインスタンスは同期されません。このクラスは構築時にはスレッドセーフではありませんが、すべてのセットアップ操作が完了した後で複数の比較を実行することはスレッドセーフです。

21
Nigel_V_Thomas

あなたがいつも考えることができるもう一つの選択肢はApache Commonsです。それはたくさんのオプションを提供します。

import org.Apache.commons.lang3.builder.CompareToBuilder;

例:

public int compare(Person a, Person b){

   return new CompareToBuilder()
     .append(a.getName(), b.getName())
     .append(a.getAddress(), b.getAddress())
     .toComparison();
}
19
Xeroiris

また、Comparatorを実装しているEnumを見ることもできます。

http://tobega.blogspot.com/2008/05/beautiful-enums.html

例えば.

Collections.sort(myChildren, Child.Order.ByAge.descending());
11
Boune

Java 8ストリーミングAPIを使うことができる人のために、ここでよく文書化されているより良いアプローチがあります: ラムダとソート

私はC#LINQに相当するものを探していました:

.ThenBy(...)

私は、コンパレータ上のJava 8のメカニズムを見つけました:

.thenComparing(...)

だからここにアルゴリズムを示すスニペットです。

    Comparator<Person> comparator = Comparator.comparing(person -> person.name);
    comparator = comparator.thenComparing(Comparator.comparing(person -> person.age));

上のリンクをチェックして、Javaの型推論がLINQと比較して定義するのが少し不器用になる方法についての説明と説明を確認してください。

これが参考のための完全な単体テストです。

@Test
public void testChainedSorting()
{
    // Create the collection of people:
    ArrayList<Person> people = new ArrayList<>();
    people.add(new Person("Dan", 4));
    people.add(new Person("Andi", 2));
    people.add(new Person("Bob", 42));
    people.add(new Person("Debby", 3));
    people.add(new Person("Bob", 72));
    people.add(new Person("Barry", 20));
    people.add(new Person("Cathy", 40));
    people.add(new Person("Bob", 40));
    people.add(new Person("Barry", 50));

    // Define chained comparators:
    // Great article explaining this and how to make it even neater:
    // http://blog.jooq.org/2014/01/31/Java-8-friday-goodies-lambdas-and-sorting/
    Comparator<Person> comparator = Comparator.comparing(person -> person.name);
    comparator = comparator.thenComparing(Comparator.comparing(person -> person.age));

    // Sort the stream:
    Stream<Person> personStream = people.stream().sorted(comparator);

    // Make sure that the output is as expected:
    List<Person> sortedPeople = personStream.collect(Collectors.toList());
    Assert.assertEquals("Andi",  sortedPeople.get(0).name); Assert.assertEquals(2,  sortedPeople.get(0).age);
    Assert.assertEquals("Barry", sortedPeople.get(1).name); Assert.assertEquals(20, sortedPeople.get(1).age);
    Assert.assertEquals("Barry", sortedPeople.get(2).name); Assert.assertEquals(50, sortedPeople.get(2).age);
    Assert.assertEquals("Bob",   sortedPeople.get(3).name); Assert.assertEquals(40, sortedPeople.get(3).age);
    Assert.assertEquals("Bob",   sortedPeople.get(4).name); Assert.assertEquals(42, sortedPeople.get(4).age);
    Assert.assertEquals("Bob",   sortedPeople.get(5).name); Assert.assertEquals(72, sortedPeople.get(5).age);
    Assert.assertEquals("Cathy", sortedPeople.get(6).name); Assert.assertEquals(40, sortedPeople.get(6).age);
    Assert.assertEquals("Dan",   sortedPeople.get(7).name); Assert.assertEquals(4,  sortedPeople.get(7).age);
    Assert.assertEquals("Debby", sortedPeople.get(8).name); Assert.assertEquals(3,  sortedPeople.get(8).age);
    // Andi     : 2
    // Barry    : 20
    // Barry    : 50
    // Bob      : 40
    // Bob      : 42
    // Bob      : 72
    // Cathy    : 40
    // Dan      : 4
    // Debby    : 3
}

/**
 * A person in our system.
 */
public static class Person
{
    /**
     * Creates a new person.
     * @param name The name of the person.
     * @param age The age of the person.
     */
    public Person(String name, int age)
    {
        this.age = age;
        this.name = name;
    }

    /**
     * The name of the person.
     */
    public String name;

    /**
     * The age of the person.
     */
    public int age;

    @Override
    public String toString()
    {
        if (name == null) return super.toString();
        else return String.format("%s : %d", this.name, this.age);
    }
}
7
Luke Machowski
import com.google.common.collect.ComparisonChain;

/**
 * @author radler
 * Class Description ...
 */
public class Attribute implements Comparable<Attribute> {

    private String type;
    private String value;

    public String getType() { return type; }
    public void setType(String type) { this.type = type; }

    public String getValue() { return value; }
    public void setValue(String value) { this.value = value; }

    @Override
    public String toString() {
        return "Attribute [type=" + type + ", value=" + value + "]";
    }

    @Override
    public int compareTo(Attribute that) {
        return ComparisonChain.start()
            .compare(this.type, that.type)
            .compare(this.value, that.value)
            .result();
    }

}
6
Ran Adler

そのようなユースケースのために手動でComparatorを書くことはひどい解決策IMOです。このようなアドホックアプローチには多くの欠点があります。

  • コードを再利用しません。 DRYに違反します。
  • 定型文。
  • エラーの可能性が高まりました。

それでは、解決策は何ですか?

まずいくつかの理論。

命名式 "Aは比較をサポートする"をOrd Aで表すことにしましょう。 (プログラムの観点からは、Ord Aは2つのAを比較するためのロジックを含むオブジェクトと考えることができます。はい、Comparatorと同じです。)

さて、Ord AOrd Bの場合、それらの複合(A, B)も比較をサポートするはずです。すなわちOrd (A, B)Ord AOrd B、およびOrd Cの場合は、Ord (A, B, C)です。

この議論を任意のアリティに拡張することができ、そしてこう言う:

Ord A, Ord B, Ord C, ..., Ord ZOrd (A, B, C, .., Z)

これを1と呼びましょう。

コンポジットの比較は、あなたがあなたの質問で説明したのと同じようにうまくいくでしょう。最初の比較が最初に試みられ、それから次のもの、そして次に次のように続きます。

それが私たちのソリューションの最初の部分です。今度は第二部。

Ord Aを知っていて、BAに変換する方法を知っている場合(その変換関数fを呼び出す)、Ord Bを持つこともできます。どうやって? 2つのBインスタンスを比較するときは、まずAを使用してそれらをfに変換し、次にOrd Aを適用します。

ここでは、変換B → AOrd A → Ord Bにマッピングしています。これは逆変量マッピング(またはcomap)として知られています。

Ord A, (B → A)コマース Ord B

これを2と呼びましょう。


それではあなたの例にこれを適用しましょう。

タイプPersonの3つのフィールドを含むStringという名前のデータ型があります。

  • Ord Stringです。ステートメント1で、Ord (String, String, String)

  • Personから(String, String, String)までの関数を簡単に書くことができます。 (3つのフィールドを返すだけです。)Ord (String, String, String)Person → (String, String, String)を知っているので、ステートメント2で、comapを使ってOrd Personを取得できます。

QED.


これらの概念をすべて実装するにはどうすればよいですか?

良いニュースはあなたがする必要はないということです。この記事で説明しているすべてのアイデアを実装した ライブラリ は既に存在します。 (あなたがこれらがどのように実装されているかに興味があるなら、あなたは フードの下を見てください

これがコードの外観です。

Ord<Person> personOrd = 
 p3Ord(stringOrd, stringOrd, stringOrd).comap(
   new F<Person, P3<String, String, String>>() {
     public P3<String, String, String> f(Person x) {
       return p(x.getFirstName(), x.getLastname(), x.getAge());
     }
   }
 );

説明:

  • stringOrdOrd<String>型のオブジェクトです。これは私たちのオリジナルの「サポート比較」命題に対応します。
  • p3OrdOrd<A>Ord<B>Ord<C>を取り、Ord<P3<A, B, C>>を返すメソッドです。これは、ステートメント1に対応します。( P3 は、3つの要素を持つ積を表​​します。積は、コンポジットの代数的な用語です。)
  • comap はよく対応しています、comap
  • F<A, B> は変換関数A → Bを表します。
  • p は製品を作成するためのファクトリメソッドです。
  • 式全体がステートメント2に対応します。

それが役立つことを願っています。

6
missingfaktor

比較メソッドの代わりに、Personクラスの中にいくつかのタイプの "Comparator"サブクラスを定義したいだけかもしれません。そうすれば、それらを標準のCollectionsソート方法に渡すことができます。

4
Marc Novakowski

あなたの比較アルゴリズムが「賢い」のであれば、もっと混乱するだろうと思います。私はあなたが提案した多数の比較方法と行きたいと思います。

私にとって唯一の例外は平等でしょう。単体テストの場合、2つのオブジェクト間で複数のフィールドが等しいかどうかを判断するために(参照が同じではない)、.Equals(.net内)をオーバーライドすると便利です。

2
Michael Haren

ユーザーが人を注文する方法が複数ある場合は、複数の Comparator を定数として設定することもできます。ソート操作とソートされたコレクションのほとんどは、パラメータとして比較子を取ります。

2
sblundy

2つのオブジェクトをJavaのハッシュコード方式で比較するのは簡単です

public class Sample{

  String a=null;
  String b=null;

  public Sample(){
      a="s";
      b="a";
  }
  public Sample(String a,String b){
      this.a=a;
      this.b=b;
  }
  public static void main(String args[]){
      Sample f=new Sample("b","12");
      Sample s=new Sample("b","12");
      //will return true
      System.out.println((s.a.hashCode()+s.b.hashCode())==(f.a.hashCode()+f.b.hashCode()));

      //will return false
      Sample f=new Sample("b","12");
      Sample s=new Sample("b","13");
      System.out.println((s.a.hashCode()+s.b.hashCode())==(f.a.hashCode()+f.b.hashCode()));

}
1
Exodus
//here threshold,buyRange,targetPercentage are three keys on that i have sorted my arraylist 
final Comparator<BasicDBObject> 

    sortOrder = new Comparator<BasicDBObject>() {
                    public int compare(BasicDBObject e1, BasicDBObject e2) {
                        int threshold = new Double(e1.getDouble("threshold"))
                        .compareTo(new Double(e2.getDouble("threshold")));
                        if (threshold != 0)
                            return threshold;

                        int buyRange = new Double(e1.getDouble("buyRange"))
                        .compareTo(new Double(e2.getDouble("buyRange")));
                        if (buyRange != 0)
                            return buyRange;

                        return (new Double(e1.getDouble("targetPercentage")) < new Double(
                                e2.getDouble("targetPercentage")) ? -1 : (new Double(
                                        e1.getDouble("targetPercentage")) == new Double(
                                                e2.getDouble("targetPercentage")) ? 0 : 1));
                    }
                };
                Collections.sort(objectList, sortOrder);
1
Pradeep Singh
//Following is the example in jdk 1.8
package com;
import Java.util.ArrayList;
import Java.util.Comparator;
import Java.util.List;

class User {
    private String firstName;
    private String lastName;
    private Integer age;

    public Integer getAge() {
        return age;
    }

    public User setAge(Integer age) {
        this.age = age;
        return this;
    }

    public String getFirstName() {
        return firstName;
    }

    public User setFirstName(String firstName) {
        this.firstName = firstName;
        return this;
    }

    public String getLastName() {
        return lastName;
    }

    public User setLastName(String lastName) {
        this.lastName = lastName;
        return this;
    }

}

public class MultiFieldsComparision {

    public static void main(String[] args) {
        List<User> users = new ArrayList<User>();

        User u1 = new User().setFirstName("Pawan").setLastName("Singh").setAge(38);
        User u2 = new User().setFirstName("Pawan").setLastName("Payal").setAge(37);
        User u3 = new User().setFirstName("Anuj").setLastName("Kumar").setAge(60);
        User u4 = new User().setFirstName("Anuj").setLastName("Kumar").setAge(43);
        User u5 = new User().setFirstName("Pawan").setLastName("Chamoli").setAge(44);
        User u6 = new User().setFirstName("Pawan").setLastName("Singh").setAge(5);

        users.add(u1);
        users.add(u2);
        users.add(u3);
        users.add(u4);
        users.add(u5);
        users.add(u6);

        System.out.println("****** Before Sorting ******");

        users.forEach(user -> {
            System.out.println(user.getFirstName() + " , " + user.getLastName() + " , " + user.getAge());
        });

        System.out.println("****** Aftre Sorting ******");

        users.sort(
                Comparator.comparing(User::getFirstName).thenComparing(User::getLastName).thenComparing(User::getAge));

        users.forEach(user -> {
            System.out.println(user.getFirstName() + " , " + user.getLastName() + " , " + user.getAge());
        });

    }

}
0
Pawan

通常、マルチレベルソートをしなければならないときはいつでも、私はこのように私のcompareTo()メソッドをオーバーライドします。

public int compareTo(Song o) {
    // TODO Auto-generated method stub
    int comp1 = 10000000*(movie.compareTo(o.movie))+1000*(artist.compareTo(o.artist))+songLength;
    int comp2 = 10000000*(o.movie.compareTo(movie))+1000*(o.artist.compareTo(artist))+o.songLength;
    return comp1-comp2;
} 

ここで最初に優先されるのは映画の名前、次にアーティスト、そして最後にsongLengthです。あなたはただそれらの乗数が互いの境界を越えないように十分に離れていることを確認する必要があります。

0
jayanth

Steve's answer から始めると、三項演算子を使うことができます。

public int compareTo(Person other) {
    int f = firstName.compareTo(other.firstName);
    int l = lastName.compareTo(other.lastName);
    return f != 0 ? f : l != 0 ? l : Integer.compare(age, other.age);
}
0
Gerold Broser

Comparable インターフェースを実装する場合は、並べ替えるための単純なプロパティを1つ選択します。これは自然順序付けとして知られています。デフォルトと考えてください。特定のコンパレータが指定されていない場合は常に使用されます。通常これは名前ですが、あなたのユースケースは別の何かを要求するかもしれません。自然な順序を上書きするために、さまざまなコレクションAPIに提供できる他のコンパレータをいくつでも自由に使用できます。

また、通常、a.compareTo(b)== 0の場合、a.equals(b)== trueになります。そうでない場合は問題ありませんが、注意が必要な副作用があります。 Comparableインターフェースの優れたjavadocを見れば、これに関するたくさんの素晴らしい情報が見つかるでしょう。

0
Mark Renouf

次のブログは良い連鎖比較器の例を与えられました

http://www.codejava.net/Java-core/collections/sorting-a-list-by-multiple-attributes-example

import Java.util.Arrays;
import Java.util.Comparator;
import Java.util.List;

/**
 * This is a chained comparator that is used to sort a list by multiple
 * attributes by chaining a sequence of comparators of individual fields
 * together.
 *
 */
public class EmployeeChainedComparator implements Comparator<Employee> {

    private List<Comparator<Employee>> listComparators;

    @SafeVarargs
    public EmployeeChainedComparator(Comparator<Employee>... comparators) {
        this.listComparators = Arrays.asList(comparators);
    }

    @Override
    public int compare(Employee emp1, Employee emp2) {
        for (Comparator<Employee> comparator : listComparators) {
            int result = comparator.compare(emp1, emp2);
            if (result != 0) {
                return result;
            }
        }
        return 0;
    }
}

呼び出しコンパレータ

Collections.sort(listEmployees, new EmployeeChainedComparator(
                new EmployeeJobTitleComparator(),
                new EmployeeAgeComparator(),
                new EmployeeSalaryComparator())
        );
0
vaquar khan