いくつかのフィールドを持つオブジェクトがあり、それらを比較することができるとします。
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()
など)を追加することは雑然としています。それで、これについて最善の方法は何ですか?
2つのComparator
オブジェクトを比較するPerson
を実装すると、好きなだけフィールドを調べることができます。どのフィールドと比較するかを指示する変数をコンパレータに入れることができます。ただし、複数のコンパレータを記述するほうが簡単です。
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);
}
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);
}
(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);
}
});
これは多くのタイピング、メンテナンスを必要とし、エラーを起こしやすいです。
ComparatorChain chain = new ComparatorChain(Arrays.asList(
new BeanComparator("size"),
new BeanComparator("nrOfToppings"),
new BeanComparator("name")));
Collections.sort(pizzas, chain);
明らかにこれはより簡潔ですが、代わりに文字列を使用してフィールドへの直接参照を失うため、さらにエラーが発生しやすくなります。フィールドの名前が変更された場合、コンパイラは問題を報告することすらありません。さらに、このソリューションではリフレクションを使用しているため、ソートがはるかに遅くなります。
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)が欲しいです。
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)の必要性に帰着します。
私はMultiComparator
name__の中で、優先度の順に/ 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));
@Patrick複数のフィールドを連続してソートするには、 ComparatorChain を試してください。
ComparatorChainは、1つ以上のコンパレータを順番にラップするコンパレータです。 ComparatorChainは、1)単一のComparatorがゼロ以外の結果を返す(そしてその結果が返される)、または2)ComparatorChainが使い果たされる(そして0が返される)まで、各Comparatorを順番に呼び出します。このタイプのソートは、SQLの複数列ソートと非常によく似ています。このクラスを使用すると、リストをソートするときにJavaクラスでそのような動作をエミュレートできます。
SQL風のソートをさらに容易にするために、リスト内の任意の単一のコンパレータの順序を逆にすることができます。
Compare(Object、Object)が呼び出された後で新しいComparatorsを追加するメソッドまたは昇順または降順のソートを変更するメソッドを呼び出すと、UnsupportedOperationExceptionが発生します。ただし、基になるコンパレータのリストまたは並べ替え順序を定義するBitSetを変更しないように注意してください。
ComparatorChainのインスタンスは同期されません。このクラスは構築時にはスレッドセーフではありませんが、すべてのセットアップ操作が完了した後で複数の比較を実行することはスレッドセーフです。
あなたがいつも考えることができるもう一つの選択肢は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();
}
また、Comparatorを実装しているEnumを見ることもできます。
http://tobega.blogspot.com/2008/05/beautiful-enums.html
例えば.
Collections.sort(myChildren, Child.Order.ByAge.descending());
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);
}
}
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();
}
}
そのようなユースケースのために手動でComparator
を書くことはひどい解決策IMOです。このようなアドホックアプローチには多くの欠点があります。
それでは、解決策は何ですか?
まずいくつかの理論。
命名式 "A
は比較をサポートする"をOrd A
で表すことにしましょう。 (プログラムの観点からは、Ord A
は2つのA
を比較するためのロジックを含むオブジェクトと考えることができます。はい、Comparator
と同じです。)
さて、Ord A
とOrd B
の場合、それらの複合(A, B)
も比較をサポートするはずです。すなわちOrd (A, B)
。 Ord A
、Ord B
、およびOrd C
の場合は、Ord (A, B, C)
です。
この議論を任意のアリティに拡張することができ、そしてこう言う:
Ord A, Ord B, Ord C, ..., Ord Z
⇒Ord (A, B, C, .., Z)
これを1と呼びましょう。
コンポジットの比較は、あなたがあなたの質問で説明したのと同じようにうまくいくでしょう。最初の比較が最初に試みられ、それから次のもの、そして次に次のように続きます。
それが私たちのソリューションの最初の部分です。今度は第二部。
Ord A
を知っていて、B
をA
に変換する方法を知っている場合(その変換関数f
を呼び出す)、Ord B
を持つこともできます。どうやって? 2つのB
インスタンスを比較するときは、まずA
を使用してそれらをf
に変換し、次にOrd A
を適用します。
ここでは、変換B → A
をOrd 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());
}
}
);
説明:
stringOrd
はOrd<String>
型のオブジェクトです。これは私たちのオリジナルの「サポート比較」命題に対応します。p3Ord
はOrd<A>
、Ord<B>
、Ord<C>
を取り、Ord<P3<A, B, C>>
を返すメソッドです。これは、ステートメント1に対応します。( P3
は、3つの要素を持つ積を表します。積は、コンポジットの代数的な用語です。)comap
はよく対応しています、comap
。F<A, B>
は変換関数A → B
を表します。p
は製品を作成するためのファクトリメソッドです。それが役立つことを願っています。
比較メソッドの代わりに、Personクラスの中にいくつかのタイプの "Comparator"サブクラスを定義したいだけかもしれません。そうすれば、それらを標準のCollectionsソート方法に渡すことができます。
あなたの比較アルゴリズムが「賢い」のであれば、もっと混乱するだろうと思います。私はあなたが提案した多数の比較方法と行きたいと思います。
私にとって唯一の例外は平等でしょう。単体テストの場合、2つのオブジェクト間で複数のフィールドが等しいかどうかを判断するために(参照が同じではない)、.Equals(.net内)をオーバーライドすると便利です。
ユーザーが人を注文する方法が複数ある場合は、複数の Comparator を定数として設定することもできます。ソート操作とソートされたコレクションのほとんどは、パラメータとして比較子を取ります。
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()));
}
//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);
//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());
});
}
}
通常、マルチレベルソートをしなければならないときはいつでも、私はこのように私の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です。あなたはただそれらの乗数が互いの境界を越えないように十分に離れていることを確認する必要があります。
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);
}
Comparable インターフェースを実装する場合は、並べ替えるための単純なプロパティを1つ選択します。これは自然順序付けとして知られています。デフォルトと考えてください。特定のコンパレータが指定されていない場合は常に使用されます。通常これは名前ですが、あなたのユースケースは別の何かを要求するかもしれません。自然な順序を上書きするために、さまざまなコレクションAPIに提供できる他のコンパレータをいくつでも自由に使用できます。
また、通常、a.compareTo(b)== 0の場合、a.equals(b)== trueになります。そうでない場合は問題ありませんが、注意が必要な副作用があります。 Comparableインターフェースの優れたjavadocを見れば、これに関するたくさんの素晴らしい情報が見つかるでしょう。
次のブログは良い連鎖比較器の例を与えられました
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())
);