この記事の目標
- equals, hashCodeメソッドの意義、オブジェクトの等価性の理解
- 上記に基づく、Set,List,MapなどのコレクションAPIを通じた集合操作の理解
この記事を読んで頂きたい人
- 四則演算、文字列操作、コレクション(List,Map,Set)操作にある程度通じてきた業務システムプログラマーの方
この記事を読んで、生まれる知識の活用シーン
- マスター一括更新(登録、更新、削除)
- オブジェクトの等価性を定義し、重複する要素の除外をSetクラスで実現できる
例えば、CSVファイルの差分比較も容易にできる。表データの差分比較も同じ。
サンプルコードの実行環境
Java 11
具体的な集合演算の効果
以下に集合演算の例を示す。
- 2つのオブジェクト群の両方に存在する要素を抽出できる・・・論理積
- 2つのグループの片側にのみ存在する要素を抽出できる・・・排他的論理和
- 2つのグループのいずれかに存在する要素を抽出できる・・・論理和
これ以外にも差集合なども得られるようになる。
前知識
オブジェクトの等価性
Javaにおけるオブジェクトの等価性は、以下のとおり、2種類ある。
- オブジェクトの同一性
- オブジェクトの同値性
オブジェクトの同一性
Javaでは、オブジェクトを==で参照アドレスを比較することで、同一性を検証できる。
Object object = new Object();
Object object2 = new Object();
object == object; // true
object == object2; // false
オブジェクトの同値性
すべてのオブジェクトが継承しているObjectクラスのequalsを
Overrideすることで同値性を判定できる。
Object object = new Object();
Object object2 = new Object();
object.equals(object); // true
object.equals(object2); // false
そして、このequalsには、hashCodeというメソッドが深く関わっている。
Javaオブジェクトの等価性評価の仕組み
まず、equalsのJavaDocを見てみたい。
equals
JavaDoc equals (Java8 OracleHPより)
このオブジェクトと他のオブジェクトが等しいかどうかを示します。
equalsメソッドは、null以外のオブジェクト参照での同値関係を実装します
object.equals(null); // false
- 反射性(reflexive): null以外の参照値xについて、x.equals(x)はtrueを返します。
- 対称性(symmetric): null以外の参照値xおよびyについて、y.equals(x)がtrueを返す場合に限り、x.equals(y)はtrueを返します。
- 推移性(transitive): null以外の参照値x、y、およびzについて、x.equals(y)がtrueを返し、y.equals(z)がtrueを返す場合、x.equals(z)はtrueを返します。
- 一貫性(consistent): null以外の参照値xおよびyについて、x.equals(y)の複数の呼出しは、このオブジェクトに対するequalsによる比較で使われた情報が変更されていなければ、一貫してtrueを返すか、一貫してfalseを返します。
null以外の参照値xについて、x.equals(null)はfalseを返します。
1つ1つ見ていく。
equalsの要件:反射性
- 反射性(reflexive): null以外の参照値xについて、x.equals(x)はtrueを返します。
オブジェクトの同値性の項で例示したものと同じ。
object.equals(object); // true
equalsの要件:対称性
- 対称性(symmetric): null以外の参照値xおよびyについて、y.equals(x)がtrueを返す場合に限り、x.equals(y)はtrueを返します。
Object object = new Object();
Object object2 = object;
object.equals(object2); // true
object2.equals(object); // true
equalsの要件:推移性
- null以外の参照値x、y、およびzについて、x.equals(y)がtrueを返し、y.equals(z)がtrueを返す場合、x.equals(z)はtrueを返します。
A=BかつB=Cならば、A=Cである。
Object A = new Object();
Object B = A;
Object C = A;
object.equals(object2); // true
object2.equals(object3); // true
// 比較せずとも、object2.equals(object3)はtrueを返すことが分かる。
equalsの要件:一貫性
- 一貫性(consistent): null以外の参照値xおよびyについて、x.equals(y)の複数の呼出し
このオブジェクトに対するequalsによる比較で使われた情報が変更されていなければ、一貫してtrueを返すか、一貫してfalseを返す。
何度equalsを実行しても等価なオブジェクト同士なら、同じ結果を返すこと。
object.equals(object); // true
object.equals(object); // true
Object#equalsのJavadoc続き
Objectクラスのequalsメソッドは、もっとも比較しやすいオブジェクトの同値関係を実装します。つまり、null以外の参照値xとyについて、このメソッドはxとyが同じオブジェクトを参照する(x == yがtrue)場合にだけtrueを返します
つまり、先程の例示のObjectクラスそのものが持つequals
object.equals(object2);
は、
object == object2;
クラスでequalsをOverrideしない限りは、上記の同一性のみが検証される。
String#equalsの場合
例えば、java.lang.Stringクラスのequalsは同一性が確認できれば、trueをまず返し、そうでなければ、同値性の検証を行う。基本的にOverrideする際は、同一性の検証→同値性の検証という流れを踏む。
public boolean equals(Object anObject) {
if (this == anObject) { // 同一性の検証
return true;
}
// 同値性の検証
if (anObject instanceof String) {
String aString = (String)anObject;
if (coder() == aString.coder()) {
return isLatin1() ? StringLatin1.equals(value, aString.value)
: StringUTF16.equals(value, aString.value);
}
}
return false;
}
Object#equalsのJavadoc続き
通常、このメソッドをオーバーライドする場合は、hashCodeメソッドを常にオーバーライドして、「等価なオブジェクトは等価なハッシュ・コードを保持する必要がある」というhashCodeメソッドの汎用規約に従う必要があることに留意してください。
さて、hasCodeの汎用規約1とはなにか。
そもそも、equalsメソッドだけOverrideしてはいけないのか。
hashCode
JavaDoc hashCode(Java8 OracleHPより)
オブジェクトのハッシュ・コード値を返します。このメソッドは、HashMapによって提供されるハッシュ表などの、ハッシュ表の利点のためにサポートされています。
ハッシュは、Wikipedia:ハッシュ関数が詳しい。
というようにデータの識別キーを表現している。
Javadocを読み進めていく。
hashCodeの一般的な規則は次のとおりです。
- Javaアプリケーションの実行中に同じオブジェクトに対して複数回呼び出された場合は常に、このオブジェクトに対するequalsの比較で使用される情報が変更されていなければ、hashCodeメソッドは常に同じ整数を返す必要があります。ただし、この整数は同じアプリケーションの実行ごとに同じである必要はありません。
前段のequalsの一貫性と同じか。
- equals(Object)メソッドに従って2つのオブジェクトが等しい場合は、2つの各オブジェクトに対するhashCodeメソッドの呼出しによって同じ整数の結果が生成される必要があります。
- equals(java.lang.Object)メソッドに従って2つのオブジェクトが等しくない場合は、2つの
各オブジェクトに対するhashCodeメソッドの呼出しによって異なる整数の結果が生成される必要はありません。ただし、プログラマは、等しくないオブジェクトに対して異なる整数の結果を生成すると、ハッシュ表のパフォーマンスが向上する場合があることに気付くはずです
「等価なオブジェクトは等価なハッシュ・コードを保持する必要がある」
2つのオブジェクトの等価、非等価について、equalsとhashCodeの返却値の性質をまとめると、以下となる。
メソッド | 等価であるオブジェクト | 等価ではないオブジェクト |
---|---|---|
hashCode | 等価でなくてはならない | 非等価を推奨。 ※非等価である必要はないがパフォーマンス上、推奨 |
equals | trueを返すべき | falseを返すべき |
閑話休題
さて、ここまでで、オブジェクトの等価性はequalsとhashCodeの両方で成り立っていることがわかってきた。
私達が作るクラスのうち、等価性の判定をしたり、ListやMap、Setなどのコレクションで取り扱ったりする対象は、概ねPOJO(Plain Old Java Object)となると思われる。
POJOの中でもデータをモデル化したもの、つまりDTO(Data Transfer Object)であったり、VO(Value Object)であったり、Entity(テーブル構造を模したクラス)であったり、名前は違えど、何らかのデータを表現したものにおいて、equalsによる等価性の判定は行われる。
JavaにおけるequalsとhashCode実装の選択肢について
equalsとhashCodeをPOJOでOverrideすることになるわけだが、手段としては以下になると予想される。
- ライブラリlombokの@EqualsAndHashCodeアノテーションによる実装
- eclipse等のIDEのソース補完機能による実装
- 開発者による実装
1,2については、そのクラスおよびスーパークラスのフィールドがすべて同値となるかを検証するequalsの実装となる。
3については、同様に同値の検証を行うメソッドを作成するか、特殊事情に基づく実装になると思われるが、この記事では想定しないし、推奨しない。
1が代表格かと思われる。
equalsとhashCodeを意識したコレクションAPIの再確認
オブジェクトのequalsとhashCodeはコレクションAPIにおいて重要である。
改めて、CollectionAPIにおいて、equalsとhashCodeの関与を確認していく。
インタフェース側の紹介となるため、利用時は適宜実装クラスをJavadoc参照のうえ、選択されたい。
List
特徴
順番を持つコレクションインタフェース。
実装クラスは、標準実装では以下がある。他多くのライブラリで実装がある。
- ArrayList
- LinkedList
- CopyOnWriteArrayList
これらは、ListインタフェースのJavadocに満たされる性質が記載されているのでそれに基づいて実装されている。実装の詳細が各実装クラスで異なる。例えば、LinkedListはランダムアクセス(添え字を利用したアクセス)は遅く、先端・終端要素の操作は高速、ArrayListはランダムアクセスが高速。ここでは、各実装クラスの選択方法については触れない。
単一オブジェクト操作:contains
boolean contains(Object o)
指定の要素がこのリストに含まれている場合にtrueを返します。つまり、このリストに、(o==null ? e==null : o.equals(e))となる要素eが1つ以上含まれている場合にのみtrueを返します。
eはListが持つ各要素。
o.equalsにて、eとの等価性を検証した結果、一つでもtrueを返すものがあれば、trueを返却する。
つまり、Listのcontainsは、渡されたオブジェクトのequals実装を用いるということ。
コレクションAPIでは、このようにコレクションが保持する要素に対する操作において、equalsを利用するものがある。
単一オブジェクト操作:remove
boolean remove(Object o)
指定された要素がこのリストにあれば、その最初のものをリストから削除します(オプションの操作)。このリストにその要素がない場合は、変更されません。つまり、(o==null ? get(i)==null : o.equals(get(i)))となる、最小のインデックス値iを持つ要素を削除します(そのような要素が存在する場合)。指定された要素がこのリストに含まれていた場合(すなわち、呼出しの結果としてこのリストが変更された場合)はtrueを返します。
containsと同じでequalsの実装に依存している。
なお、equalsとなるオブジェクトが複数ある場合の考慮も記載されている。
集合オブジェクト操作:equals
boolean equals(Object o)
指定されたオブジェクトがこのリストと等しいかどうかを比較します。指定されたオブジェクトもリストであり、サイズが同じで、2つのリストの対応する要素がすべて等しい場合にだけtrueを返します。2つの要素e1とe2は、(e1==null ? e2==null : e1.equals(e2))である場合に等しくなります。つまり2つのリストは、同じ要素が同じ順序で含まれている場合に等しいものとして定義されます。この定義により、Listインタフェースの実装が異なっても、equalsメソッドが正しく動作することが保証されます。
Listのequalsは、List自身が持つ要素のクラスのequalsに処理を委譲していることがわかる。
例えば、以下のlistA.equals(listB)の箇所は、
List<String> listA = List.of("A", "B", "C");
List<String> listB = List.of("A", "B", "C");
listA.equals(listB); // true
- 引数が同じリストであり
- サイズが同じであり
- リストの対応する要素がすべて等しく(=String#equalsの結果、等価と判定されること)
- 同じ要素が同じ順序で含まれている場合に等しいもの
といえるため、trueが返却される。
集合オブジェクト操作:containsAll
boolean containsAll(Collection> c)
指定されたコレクションのすべての要素がこのリストに含まれている場合にtrueを返します。
containsの拡張で、呼び出した側のListが引数cの要素をすべて含んでいる(containsの結果がtrueとなる)場合にtrueを返却する。
言い換えれば、ある集合が指定された集合をすべて包含しているか、を検証している。
集合オブジェクト操作:retainAll
boolean retainAll(Collection> c)
このリスト内で、指定されたコレクションに含まれている要素だけを保持します(オプションの操作)。つまり、指定されたコレクションに含まれていないすべての要素をこのリストから削除します。
言い換えれば、ある集合と指定された集合の積を得る。
当然だが、一つも含まれていない場合は、呼び出し元のListは空になる。
集合オブジェクト操作:removeAll
boolean removeAll(Collection> c)
このリストから、指定されたコレクションに含まれる要素をすべて削除します(オプションの操作)。
removeの拡張。
Set
特徴
重複要素のないコレクションです。すなわち、セットは、**e1.equals(e2)**であるe1とe2の要素ペアは持たず、null要素を最大1つしか持ちません。その名前が示すように、このインタフェースは、数学で言う集合の抽象化をモデル化します。
保持する要素の重複を許さないコレクションクラス。
重複の判定は太字のとおり。
メソッドの考え方はListと同じ。
詳細はJavadocを参照。
SortedSet
特徴
その要素に対して全体順序付けを提供するSetです。要素の順序付けは、その自然順序付けに従って行われるか、セット構築時に通常提供されるComparatorを使って行われます。セットのイテレータは、セットを要素の昇順でトラバースします。その順序付けを利用するために、追加のオペレーションがいくつか提供されています。(このインタフェースはセットで、SortedMapに類似しています。)
特記事項としては以下2
あるソート・セットがSetインタフェースを正しく実装するには、明示的なコンパレータが提供されているかどうかにかかわらず、そのソート・セットによって維持される順序付けがequalsとの一貫性のあるものでなければいけないことに注意してください。(equalsとの一貫性の正確な定義については、ComparableインタフェースまたはComparatorインタフェースを参照してください。)これはSetインタフェースがequalsオペレーションに基づいて定義されるためですが、ソート・セットはそのcompareToメソッドまたはcompareメソッドを使用してすべての要素比較を実行するので、このメソッドによって等価と見なされる2つの要素は、ソート・セットの見地からすれば同じものです。ソート・セットの動作は、その順序付けがequalsと一貫性がない場合でも明確に定義されていますが、Setインタフェースの一般規約には準拠していません。
SortedSetは、Comparableインタフェースを実装したクラスを保持するコレクションクラスだが、
Comparableで実装されるcompareToは、equalsとの一貫性があるものでなければならない。
つまり、compareToで0(並び順が一致する)のものはequalsでtrueを返すべきである。
参考記事
TreeSetのComparatorではまったのでメモ(初心者向け)
Map
特徴
いわゆるkey,valueの辞書クラス。
集合オブジェクト操作:entrySet
public Set<Map.Entry<K,V>> entrySet()
このマップに含まれるマッピングのSetビューを返します。セットはマップと連動しているので、マップに対する変更はセットに反映され、また、セットに対する変更はマップに反映されます。セットの反復処理中にマップが変更された場合、反復処理の結果は定義されていません(イテレータ自身のremoveオペレーション、またはイテレータにより返されるマップ・エントリに対するsetValueオペレーションを除く)。セットは要素の削除をサポートします。Iterator.remove、Set.remove、removeAll、retainAll、およびclearオペレーションで対応するマッピングをマップから削除します。addまたはaddAllオペレーションはサポートしていません。
Set<Map.Entry<K,V>>はFor Eachループで利用できる。
Mapの全要素をSetで取得することで、集合単位で操作することができるようになることを示している。
集合オブジェクト操作:equals
boolean equals(Object o)
指定されたオブジェクトがこのマップと等しいかどうかを比較します。指定されたオブジェクトもマップであり、2つのマップが同じマッピングを表す場合にtrueを返します。つまり、m1.entrySet().equals(m2.entrySet())である場合、2つのマップm1とm2は同じマッピングを表します。これにより、Mapインタフェースの実装が異なる場合でも、equalsメソッドが正しく動作することが保証されます。
具体的な集合演算の実装例 基本編
お題
- A,B,Cという文字列集合とB,C,Dという文字列集合に関する集合演算の例
import java.util.HashSet;
import java.util.Set;
public class StringSetDiff {
public static void main(String[] args) {
Set<String> setA = Set.of("a","b","c");
Set<String> setB = Set.of("b","c","d");
// 和集合
System.out.println("和集合");
Set<String> setAcopy = new HashSet<>(setA);
Set<String> setBcopy = new HashSet<>(setB);
setAcopy.addAll(setBcopy); // setAの内容にsetBが足される
setAcopy.forEach(System.out::print);
System.out.println();
// 差集合
System.out.println("差集合");
setAcopy = new HashSet<>(setA);
setBcopy = new HashSet<>(setB);
setAcopy.removeAll(setBcopy); // setAの内容からsetBを差し引く。
setAcopy.forEach(System.out::print);
System.out.println();
// 積集合
System.out.println("積集合");
setAcopy = new HashSet<>(setA);
setBcopy = new HashSet<>(setB);
setAcopy.retainAll(setBcopy); // setAとsetBに存在する内容をsetAに残す
setAcopy.forEach(System.out::print);
System.out.println();
// 排他的論理和
System.out.println("排他的論理和集合");
setAcopy = new HashSet<>(setA);
setBcopy = new HashSet<>(setB);
Set<String> setAcopy2 = new HashSet<>(setA);
// まず積集合を得る
setAcopy.retainAll(setBcopy);
// 各集合から積集合を除く
setAcopy2.removeAll(setAcopy);
setBcopy.removeAll(setAcopy);
setAcopy2.addAll(setBcopy);
setAcopy2.forEach(System.out::print);
}
}
和集合
A集合にB集合を加える。
Set<String> setA = Set.of("a","b","c");
Set<String> setB = Set.of("b","c","d");
Set<String> setAcopy = new HashSet<>(setA);
Set<String> setBcopy = new HashSet<>(setB);
setAcopy.addAll(setBcopy); // 集合A + 集合B
実行結果
abcd
差集合
A集合からB集合を引く
Set<String> setA = Set.of("a","b","c");
Set<String> setB = Set.of("b","c","d");
Set<String> setAcopy = new HashSet<>(setA);
Set<String> setBcopy = new HashSet<>(setB);
setAcopy.removeAll(setBcopy); // 集合A - 集合B
実行結果
a
積集合
A集合とB集合の双方にある要素を得る。
Set<String> setA = Set.of("a","b","c");
Set<String> setB = Set.of("b","c","d");
Set<String> setAcopy = new HashSet<>(setA);
Set<String> setBcopy = new HashSet<>(setB);
setAcopy.retainAll(setBcopy); // 集合A * 集合B
実行結果
bc
排他的論理和集合
A集合ないしB集合のいずれかにのみ存在する要素を得る。
Set<String> setA = Set.of("a","b","c");
Set<String> setB = Set.of("b","c","d");
setAcopy = new HashSet<>(setA);
setBcopy = new HashSet<>(setB);
Set<String> setAcopy2 = new HashSet<>(setA);
// まず積集合を得る
setAcopy.retainAll(setBcopy); // 集合A * 集合B
// 各集合から積集合を除く
setAcopy2.removeAll(setAcopy); // 集合A - (集合A * 集合B)=集合Aのみ存在
setBcopy.removeAll(setAcopy); // 集合B - (集合A * 集合B)=集合Bのみ存在
setAcopy2.addAll(setBcopy); // 集合A,Bの片方にしか存在しない集合
実行結果
ad
具体的な集合演算の実装例 発展編
利用ライブラリ
dependencies {
compile 'org.projectlombok:lombok:1.18.8'
compile 'org.apache.commons:commons-lang3:3.7'
compile 'org.slf4j:slf4j-api:1.7.25'
compile 'ch.qos.logback:logback-classic:1.2.3'
}
オブジェクト単位の差分チェック
利用するクラス
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.NoArgsConstructor;
import lombok.ToString;
import org.apache.commons.beanutils.BeanUtils;
import java.lang.reflect.InvocationTargetException;
import java.util.Comparator;
import java.util.UUID;
@Data
@EqualsAndHashCode
@ToString
@NoArgsConstructor
public class Record implements Comparable {
private UUID primaryKey;
private String name;
private Integer age;
// 出力を見やすくするため
@Override
public int compareTo(Object o) {
if (o instanceof Record) {
// age 昇順, name 昇順
Comparator<Record> comparator =
Comparator.comparing(Record::getAge).
thenComparing(Record::getName).
thenComparing(Record::getPrimaryKey);
return comparator.compare(this, (Record) o);
}
return 0;
}
// 後述する変更検知用のレコードクラスよりレコードクラスに戻す
public Record(ModifiedRecord modifiedRecord) {
try {
BeanUtils.copyProperties(this, modifiedRecord);
} catch (IllegalAccessException | InvocationTargetException ex) {
ex.printStackTrace();
}
}
public Record(DeleteOrInsertRecord deleteOrInsertRecord) {
try {
BeanUtils.copyProperties(this, deleteOrInsertRecord);
} catch (IllegalAccessException | InvocationTargetException ex) {
ex.printStackTrace();
}
}
}
いわゆる通常のテーブルレコードを模したクラス。
Comparableを実装することで、compareToの実装が必要となった。
Comparable実装時の注意点
compareToの結果が0となるオブジェクトはTreeSet系の順序を持つコレクション上でも等価と判定される。
このため、今回のサンプルにあるようなTreeSetにcompareToで0を返すようなオブジェクトを複数追加すると片方は消える。
Recordクラスでは、各フィールドのnullチェックをしていないので、実運用する場合はnullチェックも必要。
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.NoArgsConstructor;
import lombok.ToString;
import org.apache.commons.beanutils.BeanUtils;
import java.lang.reflect.InvocationTargetException;
import java.util.UUID;
/**
* 新規・削除検知用レコード.
* <pre>
* このVOは、マスターの新規・削除を分類するための等価性を実装している。
* equalsメソッドは、等価性の判定に{@code age, name}を含まない。
* </pre>
*/
@Data
@NoArgsConstructor
@ToString
@EqualsAndHashCode(onlyExplicitlyIncluded = true)
public class DeleteOrInsertRecord {
@EqualsAndHashCode.Include
private UUID primaryKey;
private Integer age;
private String name;
public DeleteOrInsertRecord(Record record) {
try {
BeanUtils.copyProperties(this, record);
} catch (IllegalAccessException | InvocationTargetException e) {
e.printStackTrace();
}
}
}
IDのみで等価性判定を行うレコードクラス。
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.NoArgsConstructor;
import lombok.ToString;
import org.apache.commons.beanutils.BeanUtils;
import java.lang.reflect.InvocationTargetException;
import java.util.UUID;
/**
* マスター更新用レコード.
* <pre>
* このVOは、マスター更新を分類するための等価性を実装している。
* equalsメソッドは、等価性の判定に{@code primaryKey}を含まない。
* </pre>
*/
@Data
@EqualsAndHashCode
@NoArgsConstructor
@ToString
public class ModifiedRecord {
@EqualsAndHashCode.Exclude
private UUID primaryKey;
private Integer age;
private String name;
public ModifiedRecord(Record record) {
try {
BeanUtils.copyProperties(this, record);
} catch (IllegalAccessException | InvocationTargetException e) {
e.printStackTrace();
}
}
}
ID以外のフィールド(name, age)で等価性判定を行うレコードクラス。
import org.apache.commons.lang3.RandomUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.*;
import java.util.stream.Collectors;
/**
* レコードの集合操作サンプル
*/
public class RecordPatternSample {
private Logger logger = LoggerFactory.getLogger(RecordPatternSample.class);
enum ModifiedPattern {
NEW,
UPDATE,
DELETE,
NO_MODIFIED
}
// ソートして出力したいので、Recordに実装したComparableをTreeSetにて有効化する
// マスタレコード群
private SortedSet<Record> masterRecords = new TreeSet<>();
// リクエストレコード群(何らかの方法で送信されたマスタレコード更新データ)
private SortedSet<Record> requestRecords = new TreeSet<>();
public RecordPatternSample() {
int totalSize = 100;
while (masterRecords.size() < totalSize) {
masterRecords.add(createRandomRecord());
}
int createSize = masterRecords.size();
// リクエストデータのうち、既存データ件数は半分とする
int existsDataCount = createSize / 2;
List<Record> createdList = masterRecords.stream().collect(Collectors.toList());
while(requestRecords.size() < existsDataCount) {
Record requestData = createRandomRecord();
requestData.setPrimaryKey(createdList.get(requestRecords.size()).getPrimaryKey());
requestRecords.add(requestData);
}
// 残り半数は新規データとする(UUIDにより必ず新規)
while (requestRecords.size() < createSize) {
requestRecords.add(createRandomRecord());
}
}
public static void main(String[] args) {
new RecordPatternSample().exec();
}
private void exec() {
// print用の和集合を得る
// この時点でSetにより、等価なオブジェクト(更新なしデータなので本処理の興味の対象外)は一つにまとめられる。
Set<Record> aPlusBSet = getPlusSet(masterRecords, requestRecords);
// 事前の一覧出力
// 和集合をベースに、マスターレコード群、リクエストレコード群から等価なオブジェクトを取りだし出力する
logger.info("print Master , Request.");
aPlusBSet.stream().forEach(e -> {
Optional<Record> masterRecord = searchSameRecord(masterRecords, e);
Optional<Record> requestRecord = searchSameRecord(requestRecords, e);
ModifiedPattern modifiedPattern = getModifiedPattern(masterRecord, requestRecord);
logger.info("{}\t master:{},\t request:{}",
modifiedPattern,
toString(masterRecord),
toString(requestRecord));
});
// 集合演算により、新規、削除、更新、更新なしデータを洗い出す
// リクエストレコード群にのみ存在する集合を得る。
// リクエストレコード群 - マスタレコード群 = リクエストレコード群にのみ存在するデータ
// つまり、新規データ
Set<Record> newRecordSet = getSubtractSet(requestRecords, masterRecords);
logger.info("新規データの表示。");
newRecordSet.forEach(e -> logger.info("new :{}", toString(Optional.of(e))));
// マスタレコード群にのみ存在する集合を得る。
// マスターレコード群 - リクエストレコード群 = マスターレコード群にのみ存在するデータ
// つまり、削除データ
Set<Record> deleteRecordSet = getSubtractSet(masterRecords, requestRecords);
logger.info("削除データの表示。");
deleteRecordSet.forEach(e -> logger.info("delete :{}", toString(Optional.of(e))));
// マスターレコード群とリクエストレコード群の積集合を得る。
// マスタレコード群(ID) × リクエストレコード群(ID)= 双方に存在するID群
// マスタレコード群(IDで抽出) × リクエストレコード群(IDで抽出)= 双方で同じIDを持つデータ群
// つまり、更新データ
Set<Record> updateRecordSet = getUpdateSet(masterRecords, requestRecords);
logger.info("更新データの表示。");
updateRecordSet.forEach(e -> logger.info("update :{}", toString(Optional.of(e))));
// マスタレコード群とリクエストレコード群の双方に存在し、すべてが完全一致する
Set<Record> noModifiedSet = getSameSet(masterRecords, requestRecords);
logger.info("更新なしデータの表示。");
noModifiedSet.forEach(e -> logger.info("no modified :{}", toString(Optional.of(e))));
}
/**
* マスター更新パターンを得る
*
* @param masterRecord マスターレコード
* @param requestRecord リクエストレコード
* @return マスター更新パターン
*/
private ModifiedPattern getModifiedPattern(Optional<Record> masterRecord, Optional<Record> requestRecord) {
if (masterRecord.isPresent() && requestRecord.isPresent()) {
// ModifiedRecordに置き換えることで、
// PKを除いた同値性の検証を行い、更新有無を確定させる
ModifiedRecord masterModifiedRecord = new ModifiedRecord(masterRecord.get());
ModifiedRecord requestModifiedRecord = new ModifiedRecord(requestRecord.get());
if (masterModifiedRecord.equals(requestModifiedRecord)) {
return ModifiedPattern.NO_MODIFIED;
}
return ModifiedPattern.UPDATE;
}
if (!masterRecord.isPresent()) {
return ModifiedPattern.NEW;
}
if (!requestRecord.isPresent()) {
return ModifiedPattern.DELETE;
}
throw new IllegalStateException();
}
/**
* Setから同じPrimaryKeyのレコードを探す
*
* @param set 集合
* @param e レコード
* @return Optionalなレコード
*/
private Optional<Record> searchSameRecord(Set<Record> set, Record e) {
return set.stream().filter(t -> t.getPrimaryKey().equals(e.getPrimaryKey())).findFirst();
}
/**
* UUIDと年齢と名前の文字列を得る。
*
* @param e レコード
* @return UUIDと年齢と名前をコロンで挟んだ文字列
*/
private String toString(Optional<Record> e) {
if (!e.isPresent())
return "nothing data";
return e.map(e2 -> String.join(",", e2.getPrimaryKey().toString(), String.valueOf(e2.getAge()), e2.getName())).get();
}
/**
* 積集合を得る
*
* @param setA 集合A
* @param setB 集合B
* @return 積集合
*/
private Set<Record> getUpdateSet(final Set<Record> setA, final Set<Record> setB) {
SortedSet<Record> recordSetCopyA = new TreeSet<>(setA);
SortedSet<Record> recordSetCopyB = new TreeSet<>(setB);
// 積集合のPrimaryKeyを得る
// 集合A,集合Bの両方に存在するIDを抽出する
Set<UUID> samePrimarySet = recordSetCopyA.stream()
.filter(e -> recordSetCopyB.stream().anyMatch(b -> e.getPrimaryKey().equals(b.getPrimaryKey())))
.map(Record::getPrimaryKey)
.collect(Collectors.toSet());
// 集合Aのうち、集合Bの要素が持つIDと一致するレコードを抽出する。
Set<Record> filteredSetA = recordSetCopyA.stream()
.filter(e -> samePrimarySet.contains(e.getPrimaryKey()))
.collect(Collectors.toSet());
Set<Record> filteredSetB = recordSetCopyB.stream()
.filter(e-> samePrimarySet.contains(e.getPrimaryKey()))
.collect(Collectors.toSet());
// 集合A-集合B
filteredSetA.removeAll(filteredSetB);
return filteredSetA;
}
/**
* 和集合を得る。
*
* @param setA 集合A
* @param setB 集合B
* @return 集合Aと集合Bの和集合
*/
private Set<Record> getPlusSet(final SortedSet<Record> setA, final SortedSet<Record> setB) {
SortedSet<Record> recordSetAcopy = new TreeSet<>(setA);
SortedSet<Record> recordSetBcopy = new TreeSet<>(setB);
recordSetAcopy.addAll(recordSetBcopy);
return recordSetAcopy;
}
/**
* 差集合を得る。
*
* @param setA 集合A
* @param setB 集合B
* @return setAからsetBを引いた差集合
*/
private Set<Record> getSubtractSet(SortedSet<Record> setA, SortedSet<Record> setB) {
// equalsの仕様をPrimaryKeyの一致性にのみ依存させる
Set<DeleteOrInsertRecord> copyASet = setA.stream().map(DeleteOrInsertRecord::new).collect(Collectors.toSet());
Set<DeleteOrInsertRecord> copyBSet = setB.stream().map(DeleteOrInsertRecord::new).collect(Collectors.toSet());
// IDが一致するものを除外する
copyASet.removeAll(copyBSet);
// 元のレコードに戻す
Set<Record> aSetSubtractBSet = copyASet.stream().map(Record::new).collect(Collectors.toSet());
return aSetSubtractBSet;
}
/**
* すべての項目が一致する同値集合を得る。
*
* @param setA A集合
* @param setB B集合
* @return A集合とB集合のすべての項目が一致する同値集合
*/
private Set<Record> getSameSet(SortedSet<Record> setA, SortedSet<Record> setB) {
SortedSet<Record> recordSetAcopy = new TreeSet<>(setA);
SortedSet<Record> recordSetBcopy = new TreeSet<>(setB);
recordSetAcopy.retainAll(recordSetBcopy);
return recordSetAcopy;
}
/**
* ランダムな値を持つレコードを生成する。
* <ul>
* <li>id:UUID</li>
* <li>年齢:1~5のいずれか</li>
* <li>名前:ichiro, jiro, saburo, shiro, goro, rokuroのいずれかランダム</li>
* </ul>
*
* @return ランダムな値を持つレコード
*/
Record createRandomRecord() {
Record record = new Record();
record.setPrimaryKey(UUID.randomUUID());
record.setAge(RandomUtils.nextInt(1, 5));
String[] names = new String[]{"ichiro", "jiro", "saburo", "shiro", "goro", "rokuro"};
List<String> nameList = Arrays.asList(names);
record.setName(nameList.get(RandomUtils.nextInt(1, nameList.size())));
return record;
}
}
2つのレコード群(マスターレコード群、リクエストレコード群)を比較し、
新規、削除、更新、更新なしの4パターンに場合分けするクラス。
実行結果
このように、オブジェクトの等価性を制御し、コレクションAPIを活用すれば、集合演算の威力を生かした実装ができる。