Edited at

GuavaのSupplierで簡単キャッシュ

More than 5 years have passed since last update.

この記事はJava Advent Calendar 2012の第23日目の記事となります.昨日は@masudaKさんによるJavaウェブオペレーションエンジニアがトラブル切りわけ時に見ていること3つでした.


キャッシュ化したい

例えば,作成するのにかなり時間がかかる変数があるとします.そこで,毎回その変数を作成するのではなく一度作成したらそれはメンバ変数などにキャッシュし,2回目以降に呼び出された場合はそのメンバ変数を読むことにします.以下は簡単に平均を求めるサンプルです.


AverageSample.java

public class AverageSample {

private final LargeObject largeObject;
private Double average;

public AverageSample(LargeObject largeObject) {
this.largeObject = largeObject;
}

public Double calcAverage() {
if (average != null)
return average;
int sum = 0;
for (Integer num : largeObject.getSeq()) {
sum += num.intValue();
}
average = (double) sum / largeObject.getSeq().size();
System.out.println("calculation done");
return average;
}
}



LargeObject.java

import java.util.List;

public class LargeObject {
private List<Integer> seq;
public List<Integer> getSeq() {
return seq;
}
public void setSeq(List<Integer> seq) {
this.seq = seq;
}
}


このサンプルの場合,calcAverage()による平均を求める処理が重い(という想定な)ので,メンバ変数averageを用意してキャッシュ化したという寸法です.見方によっては,平均値は必要になるまで計算しないという遅延処理でもあります.単体テストは以下.


AverageSampleTest.java

import static junit.framework.Assert.assertEquals;

import java.util.List;
import org.junit.Test;
import com.google.common.collect.Lists;

public class AverageSampleTest {

@Test
public void testCalcAverage() {
List<Integer> seq = Lists.newArrayList();
seq.add(20);
seq.add(12);
seq.add(12);
LargeObject largeObject = new LargeObject();
largeObject.setSeq(seq);

AverageSample target = new AverageSample(largeObject);
Double actual = target.calcAverage();
Double expected = 44.0 / 3.0;

assertEquals(expected, actual);
target.calcAverage();
}
}


実行するともちろん緑になりますが,ポイントは,calcAverage()を2回呼び出しているにも関わらず,コンソールに"calculation done"が1回しか出ていないことです.


nullチェックがダサい

まぁこれでもいいのですが,なんとなくif (average != null)というnullチェックがダサいです.また,このcalcAverage()をスレッドセーフにしなければならないとき,色々とめんどくさいです.このサンプルの場合はメンバ変数averagevolatile付ければいいだけですが (補足:AverageSampleインスタンスをスレッドセーフにする場合はまた別).


そこでSupplierを使ってみる

Googlesの便利ライブラリGuavaSupplierを使ってみます.上のサンプルファイルは以下な感じになります.


SupplierSample.java

import com.google.common.base.Supplier;

import com.google.common.base.Suppliers;

public class SupplierSample {

private final Supplier<Double> average;

public SupplierSample(final LargeObject largeObject) {
Supplier<Double> supplier = new Supplier<Double>() {
@Override
public Double get() {
int sum = 0;
for (Integer num : largeObject.getSeq()) {
sum += num.intValue();
}
double average = (double) sum / largeObject.getSeq().size();
System.out.println("calculation done");
return average;
}
};
average = Suppliers.memoize(supplier);
}

public Double calcAverage() {
return average.get();
}
}


先ほどと同じように単体テストでcalcAverage()を2回呼び出しても,やっぱり"calculation done"は1回しかコンソールにでません.Suppliers.memoize(supplier)でよしなにやってくれています.果たして簡単になったのかどうかは甚だ疑問ですが,nullという単語が駆逐できたので満足したということにします.


まとめ

簡単なサンプルと共にSupplierクラスの紹介してみました.サンプルの場合は単純すぎてあまり効用が分かりにくいですが,Functionクラスなどと組み合わせることによって割りと柔軟に遅延処理&キャッシュ化ができてしまいます.Collectionパッケージの拡張としてGuavaを使用している場合は,ついでにこうした恩恵に預かることができます.

また,GuiceというDIフレームワークがあり,そちらのProviderクラスの便利ライブラリ版とも言えます.(参考:java - Google Guava Supplier Example - Stack Overflow)

超簡単ではありますが,Java Advent Calendar 2012 23rdの記事となります.「こんな簡単な記事書いているんじゃねーよ」というツッコミが聞こえないフリをしつつ,次の@irofさんにバトンを渡します.