CLOVER🍀

That was when it all began.

JCacheのjavax.cache.Cacheが、size/keySet/values/entrySetを定義していないよという話

JJUG CCC 2015 FallでJCache(JSR-107)についての発表があり、その時にMapとの差異についてのスライドが掲載されています。

JCache Using JCache

これを見ると、java.util.Mapに対してExpire(有効期限)やEviction(キャッシュからの追い出し)、Store-By-Valueシリアライズ保存)、CacheLoader/CacheWriterのサポート、Listener、Entry Processor、統計情報などを追加したものが、javax.cache.Cacheのように見えます。

ですが、Mapからなくなったものもあります。

Mapにおける、size、keySet、values、entrySetといった、"全体を取得する"という類のメソッドがありません。Iterableではあるので、イテレーション自体は可能です。
Cache#getAllというMapを返すメソッドもありますが、引数として取得対象のキーのSetを渡す必要があります。

javax.cache.CacheインターフェースのJavadocは、こちらです。
※JCacheのJavadocは、現在オンラインだとApache Igniteのところのものを見るのがよい気がします…
Interface Cache

参考)
Apache Ignite側にある、JCacheのJavadoc
Cache (JSR107 API and SPI 1.0.0 API)

これについて、結構前ですがIssueで質問が飛んでいたので、メモ的に。

Cache keys/values and size · Issue #302 · jsr107/jsr107spec · GitHub

このIssueでは、なんでsizeやkeys、valuesをサポートしていないの?Mapに似ているのに、という感じのことが聞かれています。

これに対して、Oracleの方?が回答しています。以下、荒くイメージを。

ばっくり意訳

※注記)以下、インターフェース的な意味でのキャッシュは、"Cache"と表記しています。

最初の回答

1)
有効期限(Expire)の設定をしたキャッシュは、結果が揮発的になります。例えば、以下のコードの例は、たとえシングルスレッド上での実行であっても、size1とsize2が同じになることを保証できません。

int size1 = cache.size();
int size2 = cache.size();

これは、キャッシュの有効期限が切れてしまい、バックグラウンドでキャッシュから追い出されてしまう(Eviction)ケースがあるからです。

2)
Eviction(キャッシュからの追い出し)が怠惰に(Lazyに)実行されるキャッシュの実装の場合、Cache#sizeの呼び出し結果として正しい"size"を返すために、キャッシュの全エントリへのアクセスが発生する(touch)可能性があります。Cache#sizeメソッドの持つこれらの実装の事情から、LazyなEvictionを最適化するのは容易ではありませんでした。

3)
データセットをすべてメモリ上に持つのではなく、ディスクに保存しているキャッシュの実装もあります。この場合、Cache#sizeの呼び出しは(ディスク上のエントリを含めた)エントリ全体のロード/参照を行う可能性があります。これはとても非効率です。

4)
Cacheは、Mapではありません。これらはよく似たAPIを持っていますが、その意味は非常に異なっています。これらの重要な違い(なぜCacheがMapを拡張しないのか)の概要は、仕様書に載っているので見ることをお勧めします。

ほとんどのキャッシュのベンダーは、Mapを拡張した(もしくはsizeを含む、Mapと同様のメソッドを持つ)Cacheは良くないアイデアだと考えています。キャッシュの利用シーンで、滅多に使われないはずの機能に誘導してしまうからです。

CacheインターフェースはIterableを実装しているので、キーと値を含めたエントリの反復処理は可能です。

回答に対する、Issue起票者の質問

CacheがIterableを実装していることには気付いていましたが、私の質問はどうしてkeysとvaluesを分けて取得できないのだろうか?ということです。現在提供されているのは、Cache.Entryに対するIterableです。
これらが分かれていれば、キャッシュが持つ(いくつかの階層での)異なる種類のキー集合に対して、(残りの値にtouchすることなく)特定のキーの種類に関連付けられた値をフィルタリングすることとができます。

※注記)質問者がなにしたいか、よくわかりません…ひとつのキャッシュに異なる型のキーを入れたり、複雑なキーを入れようとしてる??

再度、回答

APIを利用するとCache.Entryを反復処理することができますが、背後にあるエントリを"具体化する(materialized)"、またはすでに利用可能であることを要件とはしていません。

例えば、ある実装ではIteratorからCache.Entryを返しますが、値がフェッチされるのはEntry#getValueが呼び出された時です。同じことが、Entry#getKeyにも言えます。

Cache.Entryを反復処理しているということだけで、コストの高い、もしくは非効率な処理を行っていると仮定すべきではありません。

実際、(仮に可能であったとしても)以下のようなことは非効率でしょう。

Cache<String, ShoppingCart> cache = ...
for(String key : cache.keySet())
{
    ShoppingCart cart = cache.get(key);   // <--- Cacheに2回目の呼び出しを行っている!
}

(キャッシュの実装がLazyかつ/またはEagerに値をフェッチするように最適化することができているなら)次のように操作がするのが、より良いでしょう。

Cache<String, ShoppingCart> cache = ...
for(Cache.Entry<String, ShoppingCart> entry : cache)
{
    ShoppingCart cart = entry.getValue();   // <--- Cacheへの呼び出しは、1度だけ(値はプリフェッチされている)
}

思い出してください。
Map API(とそれが意味するもの)は、基本的にすべてがメモリ上(in-memory)にあり、keys/valuesへのアクセスは比較的効率的、もしくはコストがゼロだと仮定しています。
キャッシュの場合、これがいつも真ではありません。キャッシュ内のエントリは、メモリ、ディスク、その他の場所に保持されています。

仕様について

回答にある「仕様書を読もう」ってところですが、JSR-107の「2.1. Core Concepts」、「2.2. Caches and Maps」、「2.3. Consistency」、「2.4. Cache Topologies」あたりを読みましょう。

The Java Community Process(SM) Program - JSRs: Java Specification Requests - detail JSR# 107

私見

これは個人的なこれらのメソッドに対するイメージですが、現状JCache(JSR-107)の実装は、スタンドアロンのキャッシュではなくてIn Memory Data Gridが大半です。

ちょっと、実装寄りの話になりますね。

キャッシュがクラスタ構成されている場合、これらのsizeやkeySet、valuesなどのメソッドはクラスタ全体にまたがる操作となり、けっこう高コストになる可能性があるイメージです。

例えば、InfinispanのCache#sizeのJavadocには以下のような記載があります。

This method should only be used for debugging purposes such as to verify that the cache contains all the keys entered. Any other use involving execution of this method on a production system is not recommended.

https://docs.jboss.org/infinispan/8.2/apidocs/org/infinispan/Cache.html#size--

全キーに対しての操作になるので、デバッグ目的で利用すること、かつプロダクションで使うことはお勧めしない、と。keySetやvaluesなどについては、Viewを返す形で実装されているようですが。
https://github.com/infinispan/infinispan/blob/8.2.0.Final/core/src/main/java/org/infinispan/commands/read/KeySetCommand.java
https://github.com/infinispan/infinispan/blob/8.2.0.Final/core/src/main/java/org/infinispan/util/DataContainerRemoveIterator.java

以前は、keySetもvalues、entrySetもsizeと同じような注意書きが書かれていました。

Hazelcastの場合も、Cache#sizeはやっぱり全Partitionに対してのカウントを実行しようとします。
https://github.com/hazelcast/hazelcast/blob/v3.6.1/hazelcast/src/main/java/com/hazelcast/cache/impl/AbstractCacheProxy.java#L345-L361

こういう分散してデータを保持しているもので、裏の事情を知らないまま気軽にこういう操作はしない方がいいですよーという印象です。