はじめに
大量のデータを処理するときなどに、Iteratorは便利です。
特に計算時間やメモリ使用量にコストがかかるデータを大量に処理する場合、あらかじめすべてのデータを変換してListなどのコレクションに変換してしまうと、メモリが足りずにOutOfMemoryしてしまったり、GCの頻発で大幅に処理が遅くなってしまうこともあります。
イテレータを使わない場合
例えば、以下のようにデータストアからリストでデータを受け取るインタフェースがあったとします。(レコードの登録順でプライマリキーは並んでいるものとします)
interface MyRepository<E>{
/**
* プライマリキーの値が [fromInclusive, untilInclusive]の範囲のレコードをListで返します
*/
List<E> findByIdRange(long fromInclusive,long untilInclusive);
}
ある日(あるプライマリキーの範囲)に書き込まれたレコードをすべて変換して、他のデータストアに突っ込みたいときなど、例えばStreamAPIを活用して以下のように書くことができます。。
MyRepository<MyRecord> myRepository;
void extractTransferAndLoad(long fromInclusive,long untilInclusive){
// 並列で負荷のかかる処理を流していくイメージ
myRepository.findByIdRange(fromInclusive,untilInclusive).parallelStream().map(this::transfer).foreach(this::load);
}
しかしデータが大量にある場合などは、myRepository.findByIdRange(long,long)
でOutOfMemoryを引き起こしてしまったり、メモリを大量に消費しパフォーマンスに影響を与えてしまう場合があります。
イテレータを使用してみる
そこで、イテレータを自作します。
イテレータを作るために覚えておくべきインターフェースは2つです。
package java.util;
interface Iterator<A>{
boolean hasNext();
A next();
}
package java.lang;
interface Iterable<A>{
Iterator<A> iterator();
}
上記のfindByIdRange
をイテレータを使って分割取得するようなクラスを書いてみましょう。
class MyIterableRepository<A> implements Iterable<List<A>>{
final private MyRepository<A> myRepository;
final private long fromInclusive;
final private long untilInclusive;
final private int splitSize; // splitSize ごとに分割してListを返すようにする
public MyIterableRepository(MyRepository<A> myRepository,long fromInclusive,long untilInclusive,int splitSize){
// DIするだけのコンストラクタ…省略
}
public Iterator<List<A>> iterator() {
return new Iterator<List<A>>() {
long currentFromInclusive = fromInclusive;
public boolean hasNext() {
return untilInclusive >= currentFromInclusive;
}
public List<A> next() {
long currentUntilInclusive = Math.min(untilInclusive, nextFromInclusive+splitSize-1);
List<A> ret = myRepository.findById(currentFromInclusive,currentUntilInclusive);
currentFromInclusive = currentUntilInclusive+1;
return ret;
}
};
}
}
これでイテレータが作れました。シンプルなパターンなので慣れて覚えれば簡単です。
Iterableを実装すると、呼び出し側では拡張for文を使って以下のように書けます。
MyRepository<MyRecord> myRepository;
void extractTransferAndLoad(long fromInclusive,long untilInclusive){
MyIterableRepository<MyData> myIterableRepository = new MyIterableRepository(myRepository,fromInclusive,untilInclusive, MagicNumbers.SPLIT_SIZE);
for(List<MyData> myDataList : myIterableRepository){
// 並列で負荷のかかる処理を流していくイメージ
myDataList.parallelStream().map(this::transfer).forEach(this::load);
}
}
このように変更することで、一度に生成するオブジェクトを少なく抑えるとともに、for文の終わりでmyDataList
(とその参照先)はGCから改修可能になり、OoMに陥ることを回避することができます。
番外編:超便利!Iteratorの合成
複数のデータソースからデータを取得しなくてはいけない場合など、イテレータを合成したいときがあります。
例えばユーザ情報とユーザに紐づく購入履歴とユーザに紐づく投稿履歴があって、統合した静的ページをバッチ処理の中で作っている場合などに、内側の実装までかければ2重の拡張for文で済むのですが、(以下、実現したい処理の内容のイメージ)
Iterable<MyRepository2> repositories;
void extractTransferAndLoad(MyTime fromInclusive,MyTime untilInclusive){
for(MyRepository2 repository: repositories){
Iterable<List<UserId>> ittrable = new MyIterableRepository2(repository,fromInclusive,untilInclusive,42).getUserIdsIterable();
for(List<UserId> userIds : ittrable){
userIds.parallelStream().filter( /*既に処理したIDを無視して*/ ).map( /*変換して*/ ).forEach( /* 書き出す */ );
}
}
}
イテレータを受け取って変換する処理が与えられていて、それを呼び出さなくてはいけない場合はそのままだと困ってしまいます。(以下呼び出し先の例)
// 呼び出し先のメソッド
void extractTransferAndLoad2(Iterable<List<UserId>> userIdsListIterable){
for(List<UserId> userIds : userIdsListIterable){
userIds.parallelStream().filter( /*既に処理したIDを無視して*/ ).map( /*変換して*/ ).forEach( /* 書き出す */ );
}
}
// 呼び出し元がやりたいことのイメージ
extractTransferAndLoad2(repositories.flatMap(repository -> new MyIterableRepository2(repository,fromInclusive,untilInclusive,42).getUserIdsIterable()));
// IteratorにはflatMapはないので、こういうことはできない。
そこで今回私の考えた超すごいイテレータを合成する方法を使えば
public class IteratorUtils {
public static <A,B> Iterator<B> composedIterator(Iterator<A> aittr, Function<A,Iterator<B>> func){
return new Iterator<B>(){
Iterator<B> bittr = Collections.emptyIterator();
public boolean hasNext() {
while(!bittr.hasNext() && aittr.hasNext()){
bittr = func.apply(aittr.next());
}
return bittr.hasNext();
}
public B next() {
while(!bittr.hasNext() && aittr.hasNext()){
bittr = func.apply(aittr.next());
}
return bittr.next();
}
};
}
}
すると、
extractTransferAndLoad2(IteratorUtils.composedIterator(repositories, repository -> new MyIterableRepository2(repository,fromInclusive,untilInclusive,42).getUserIdsIterable()));
としてイテレータを合成して渡すことができるのです!!
ちなみにIteratorをやめてStreamで全部返すようにすればflatMapで一発なのでは、と思って考えましたが、Javaで有限のStreamを作る方法はIteratorを元にSpliteratorを作る方法以外にないので、残念ながら実現できませんでした。