Help us understand the problem. What is going on with this article?

Java8のSupplierで簡単キャッシュ

More than 3 years have passed since last update.

約2年前に Java - GuavaのSupplierで簡単キャッシュ というネタを書きましたが、それを Java8 っぽく書き直してみよう、という話です。

(おさらい) 重い処理だからキャッシュ化したい

例えば大量の int に対して平均を求めたいけど、計算が重いので1回計算したら使いまわしたい、といった状況です。

LargetObject.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;
    }

}

Supplier を使って見る

Java8 で追加された java.util.function.Supplier<T> を使ってみます。今回は平均を求めたいだけなので Supplier<Double> ではなく DoubleSupplier を使いました。

AverageHelper.java
import java.util.List;
import java.util.function.DoubleSupplier;

public class AverageHelper {

    private final DoubleSupplier averageSupplier;

    public AverageHelper(final LargeObject largeObject) {
        averageSupplier = new DoubleSupplier() {
            private final double average = average(largeObject.getSeq());

            @Override
            public double getAsDouble() {
                return average;
            }

            private double average(List<Integer> seq) {
                System.out.println("calculating...");
                return seq.stream().mapToInt(x -> x).average().getAsDouble();
            }
        };
    }

    public double calcAverage() {
        System.out.println("average calculation");
        return averageSupplier.getAsDouble();
    }
}

実行してみます。

AverageHelperTest.java
import static org.junit.Assert.assertEquals;
import java.util.ArrayList;
import java.util.List;
import org.junit.Test;

public class AverageHelperTest {

    @Test
    public void testCalcAverage() {
        List<Integer> seq = Arrays.asList(20, 12, 12);
        LargeObject largeObject = new LargeObject();
        largeObject.setSeq(seq);

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

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

実行結果は以下の通り。"calculating..." という文字列が1回しか出力されていません。

calculating...
average calculation
average calculation

欲しいタイミングで計算させる

上記ログを見ると、キャッシュ化はされていますが、実際に平均が欲しい target.calcAverage() よりも前に平均が計算されてしまっていることが分かります。なので、実際に target.calcAverage() 改め target.getAverage() が呼ばれたら初めて平均を計算するように修正します。

単純に、averageSupplier をインスタンス化するタイミングを AverageHelper のコンストラクタから、メソッドに移せばいいだけです。

AverageHelper2.java
import java.util.List;
import java.util.function.DoubleSupplier;

public class AverageHelper2 {

    private final LargeObject largeObject;
    private DoubleSupplier averageSupplier = () -> calculateAndCache();

    public AverageHelper2(final LargeObject largeObject) {
        this.largeObject = largeObject;
    }

    public double getAverage() {
        System.out.println("get average");
        return averageSupplier.getAsDouble();
    }

    private double calculateAndCache() {
        if (!AverageFactory.class.isInstance(averageSupplier)) {
            averageSupplier = new AverageFactory();
        }
        return averageSupplier.getAsDouble();
    }

    private class AverageFactory implements DoubleSupplier {
        private final double average = average(largeObject.getSeq());

        @Override
        public double getAsDouble() {
            return average;
        }

        private double average(List<Integer> seq) {
            System.out.println("calculating...");
            return seq.stream().mapToInt(x -> x).average().getAsDouble();
        }
    }
}

テストコードもちょっと修正して、実行してみます。

    @Test
    public void testCalcAverage2() {
        List<Integer> seq = Arrays.asList(20, 12, 12);
        LargeObject largeObject = new LargeObject();
        largeObject.setSeq(seq);

        AverageHelper2 target = new AverageHelper2(largeObject);
        Double actual = target.getAverage();
        Double expected = 44.0 / 3.0;

        assertEquals(expected, actual);
        target.getAverage();
    }
get average
calculating...
get average

上記ログから、平均が欲しいタイミングで計算され、かつキャッシュ化されていることが分かります。!AverageFactory.class.isInstance(averageSupplier) がダサい感じもしますが、まぁいいってことで。

参考文献

Venkat Subramaniam『Javaによる関数型プログラミング』の6章『「遅延させる」ということ』を参考にさせていただきました。

Why do not you register as a user and use Qiita more conveniently?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away