Closed15

Guavaメモ

ayumukobayumukob

忘れてしまっていちいちググらなくても良いようにGuavaで利用したクラス、メソッドのまとめて行こうと思う。
新たに利用したものがあれば追記していく。

Guavaについて

天下のGoogle様が開発しているOSSのJavaユーティリティライブラリで、Java1.5以上で使用できる。
Apatch CommonsライブラリのGoogle版って感じで、手で実装するとめんどくさい処理を簡略化してくれるイメージ。
特にコレクションに関連したユーティリティはかなり便利。

使い方

Maven Dependency

<dependency>
    <groupId>com.google.guava</groupId>
    <artifactId>guava</artifactId>
    <version>31.0.1-jre</version>
</dependency>
ayumukobayumukob

Lists

Listに関連したユーティリティクラス。

インスタンス生成

空のリストで初期化

古いJavaだったらGuavaの方が簡潔だったけど、今はもうどちらも変わらん。

//Java
List<String> list = new ArrayList<>();

//Guava
List<String> list = Lists.newArrayList();

要素有りで初期化

入れるべき要素が事前にある場合(UTでよくあるパターン)に、空リストにaddしていくのはコードが長くなるし、めんどい。
そこでGuavaを利用するとスッキリ記述できる。

//Java
List<String> list = new ArrayList<>();
list.add("a");
list.add("b");
list.add("c");

//Guava
List<String> list = Lists.newArrayList("a", "b", "c");
ayumukobayumukob

Maps

Mapに関したユーティリティクラス。
こちらは初期化時点で値を入れられない。

//Guava
Map<String, String> map = Maps.newHashMap()
ayumukobayumukob

ImmutableList

変更不可のListを生成できる。
よく使うArrays.asListは、実はjava.util.ArrayListのインスタンスではなく変更不可のList。
なのでImmutableListを利用する旨味はそんなにないかも。
Java9以降だとListにofというstaticメソッドができたのだが、こいつも変更不可List。

//Java
List<String> list = Arrays.asList("a", "b", "c");

//Guava
List<String> list = ImmutableList.of("a", "b", "c");

//Java9
List<String> list = List.of("a", "b", "c");
ayumukobayumukob

ImmutableSet

変更不可のSetを生成できる。
こいつはJava9以降のSet.ofが同じ挙動となる。

//Guava
Set<String> set = ImmutableSet.of("a", "b", "c");

//Java9
Set<String> set = Set.of("a", "b", "c");
ayumukobayumukob

ImmutableMap

変更不可のMapを生成できる。
利用したことある内容はImmutableSetと同じ感じ

//Guava
Map<String, String> map = ImmutableMap.of("1", "data1", "2", "data2", "3", "data3")

//Java9
Map<String, String> map = Map.of("1", "data1", "2", "data2", "3", "data3")
ayumukobayumukob

Multimap

keyに対して複数のデータを持つMap。
結構便利でそれなりに使ってる。
イメージ的にはMap<K, List<V>>みたいな感じ。
この形式のMapを扱うと以下の例のようにかなり煩雑になってしまう。
ただ、Java8からは簡潔に書けるようになってはいる。

//Java 超雑な作り
Map<String, List<String>> map = new HashMap<>();
if(!map.containsKey("1")) {
  map.put("1", new ArrayList<>());
}
List<String> list1 = map.get("1");
list1.add("a");

if(!map.containsKey("1")) {
  map.put("1", new ArrayList<>());
}
List<String> list2 = map.get("1");
list2.add("b");

if(!map.containsKey("1")) {
  map.put("1", new ArrayList<>());
}
List<String> list3 = map.get("1");
list3.add("c");

if(!map.containsKey("2")) {
  map.put("2", new ArrayList<>());
}
List<String> list4 = map.get("2");
list4.add("a");

if(!map.containsKey("3")) {
  map.put("3", new ArrayList<>());
}
List<String> list5 = map.get("3");
list5.add("a");

//Java8
Map<String, List<String>> map = new HashMap<>();

List<String> list1 = map.computeIfAbsent("1", k -> new ArrayList<>());
list1.add("a");

List<String> list2 = map.computeIfAbsent("1", k -> new ArrayList<>());
list2.add("b");

List<String> list3 = map.computeIfAbsent("1", k -> new ArrayList<>());
list3.add("c");

List<String> list4 = map.computeIfAbsent("2", k -> new ArrayList<>());
list4.add("a");

List<String> list5 = map.computeIfAbsent("3", k -> new ArrayList<>());
list5.add("a");

では、Guava。

//Guava
Multimap<String, String> multiMap = ArrayListMultimap.create();
multiMap.put("1", "a");
multiMap.put("1", "b");
multiMap.put("1", "c");
multiMap.put("2", "a");
multiMap.put("3", "a");

単純にMapにputする感覚でListの要素を追加できてかなり楽。
Java8パターンは1行で記述できるのでこれだけだとあまり変わりがないが、MultimapはvalueのCollectionへの操作が非常に楽に記述できることがすごく強力。

インスタンス生成

valueの性質によっていろいろ種類がある。
他にもある。

//HashMapのvalueがArrayListのMultiMap
Multimap<String, String> multiMap = ArrayListMultimap.create();

//ハッシュテーブルを利用したMultiMap
Multimap<String, String> multiMap = HashMultimap.create();

//LinkedHashMap的なMultiMap
Multimap<String, String> multiMap = LinkedHashMultimap.create();

//TreeMap/Set的なMultiMap
Multimap<String, String> multiMap = TreeMultimap.create();

valueへのアクセス

普通にvalueを取得する方法のほかに、所謂entrySetのようなentries()がある。
普通のMap場合はkeyとvalueの組み合わせのまんまアクセスできるが、Multimapの場合はvalue側も分解され1つの要素となる。

Collection<String> collection = multiMap.get("key");

//Java
Set<Entry<String, Collection<String>>> set = map.entrySet();
//中身としてはこんな感じ
//[{key:1, value:[a, b, c]}]

//Guava
Collection<Map.Entry<K,V>> collection = multiMap.entries();
//中身としてはこんな感じ
//[{key:1, value:a}, {key:1, value:b}, {key:1, value:c}]

要素の削除

普通のMapだとremoveするとkeyに紐付くvalueが削除されるが、Multimapのremoveの場合はkeyとvalueの要素を指定するので1Entryだけ削除される。Mapのremoveと同等はなのはremoveAllとなる。

multiMap.remove("1", "b");

multiMap.removeAll("1");

Mapへの変換

MultimapからMapへの変換はめっちゃ楽。

Map<String, Collection<String>> map = multiMap.asMap();

multiMapに含まれているかの確認

containsKeyはMapと同様だが、containsValueは違いvalueの全要素と比較してくれる。またcontainsEntryというものも存在し、keyとvalueのペアでチェックしてくれる。

multiMap.containsKey("1");

multiMap.containsValue("b");

multiMap.containsEntry("2", "b")

keyに紐づくvalueの要素数を取得

いちいちvalueを取得してsize確認しなくても良い

Multiset<String> keys = multiMap.keys();
//中身としてはこんな感じ
//1件しか入っていないキーについてはキー情報しか出力されない
//[1 x 3, 2, 3]
ayumukobayumukob

Table

2次元配列的な2つのキーを持つデータ構造を表現したクラス。
row、column、valueで構成される。
イメージはMap<row, Map<column, value>>のような感じ。
compositeなキーをMapのkeyに使ってるような場合は置き換えれる。
Multimapと同様にJavaだとかなり煩雑なロジックになるところをカバーしてくれる。
ちなみに3つ以上のキーを持つデータ構造についてはサポートされていない。

インスタンス生成

Tableも性質によっていろいろ種類がある。
他にもある。

//LinkedHashMap的なTable
Table<String, String, String> table = HashBasedTable.create();

//TreeMap的なTable
Table<String, String, String> table = TreeBasedTable.create();

valueへのアクセス

row指定、column指定もできる。

Table<String, String, String> table = HashBasedTable.create();
table.put("1", "a", "data1");
table.put("1", "b", "data2");
table.put("2", "c", "data3");
table.put("2", "a", "data1");

//valueを取得
String value = table.get("1", "a");

//columnに紐付くrowとvalueのMapを取得
Map<String, String> rowValueMap = table.column("a");

//rowに紐付くcolumnとvalueのMapを取得
Map<String, String> columnValueMap = table.row("1");

要素の削除

1要素の削除になるので、rowとcolumnを指定する。

table.remove("1", "a");

Mapへの変換

TableからMapへの変換もめっちゃ楽

Map<String, Map<String, String>> map =  table.columnMap();

Tableに含まれているかの確認

rowとcolumnの組み合わせ、columnのみ、rowのみ、valueのみという4つのチェックがある。

table.contains("1", "a");

table.containsColumn("a");

table.containsRow("1");

table.containsValue("data1");
ayumukobayumukob

@VisibleForTesting

実はこいつが一番使ってるかも。
テストのためにアクセスレベル下げてるよって意味を付与するマーカー的な役割。
例えばprivateメソッドをUTしようとするときに、そのまま修飾子をprivateのままにしているとめちゃくちゃテストしづらい。そこでpackage privateにしてアクセスできるようにすることで、テスタビリティを向上させことがよくある。このアノテーションをそういった場合に修飾子を緩めたメソッドや変数に付与するもの。

@VisibleForTesting
void privateMethod() {
  // 処理
}
ayumukobayumukob

Guava Cache

Guavaでは簡単にインメモリキャッシュを利用できる。
スレッドセーフなのでマルチスレッドでも問題なし。

キャッシュの登録

  • 自動的にキャッシュに登録
    LoadingCacheインターフェースでキャッシュを操作していく。
    Guava Cacheのドキュメントを見ていくとこちらが一般的なように見えるが、自分としてはどのようなユースケースでこれを利用していくのかあまり想像できていない。ランダム値とかをvalueとして入れるみたいな時は使えそうだけど。。。
    詳しい人誰か教えて(苦笑
    LoadingCache<String, String> loadingCache = CacheBuilder.newBuilder()
      .build(new CacheLoader<String, String>() {
        @Override
        public String load(String key) throws Exception {
          return key.toUpperCase();
        }
      });
    
    //cacheに存在しないため、ここでvalueが自動生成
    String value = loadingCache.get("key");
    //cacheから値を取得
    value = loadingCache.get("key");
    
    この例ではkeyを大文字にしたものが自動的に登録される。
    コメントに書いてあるとおりファーストアクセスではデータが存在しないため、CacheLoaderのloadメソッドに記述した計算をし、結果をキャッシュに登録する。
    loadメソッドはkeyが引数となり、別のデータを付与できないため、keyから計算される値か、外からのデータが不要な値しか登録できない(ここがそんなに用途がないと思える部分)。
  • 手動でキャッシュに挿入
    Cacheインターフェースでキャッシュを操作していく。
    ただ、CacheインターフェースはLoadingCacheインターフェースの親なのでLoadingCacheでも以下の操作は可能。
    自分はこちらの方を多用するが、一般的なのかは不明。
    Cache<String, String> cache = CacheBuilder.newBuilder().build();
    
    //キャッシュの登録
    cache.put("key", "cacheData");
    //cacheからvalueを取得
    String value = cache.getIfPresent("key");
    
    シンプルにキャッシュの登録、取得ができる。
    また、一気に複数の値も登録できる。
    cache.putAll(ImmutableMap.of("key1", "data1", "key2", "data2", "key3", "data3"));
    

取得について

取得するメソッドは3つ存在する。

  • get
    LoadingCacheでkeyに対するvalueを取得する。
    load時にエラーが発生すると、ExecutionExceptionがthrowされる
  • getUnchecked
    LoadingCacheでkeyに対するvalueを取得する。
    getとの違いはExecutionExceptionがthrowされないこと
    CacheLoaderをlambdaを利用して生成もでき、その場合Exceptionは吐かれないのでgetメソッドを使う必要がないという感じ(使えないわけではない)。
    //lambdaを利用したインスタンス生成
    CacheLoader.from((key) -> key.toUpperCase())
    
  • getIfPresent
    Cacheでkeyに対するvalueを取得する。
    紐づくvalueがあればその値を返却するし、なければnullを返却する

削除について

あまり利用しないと思うが、明示的に削除も可能

//一つのエントリーを削除
cache.invalidate("key");

//複数のエントリーを削除
cache.invalidateAll(Arrays.asList("key1", "key2", "key3"));

//全てのエントリーを削除
cache.invalidateAll();

nullの扱い

CacheLoaderパターンで、nullをloadしようとするとExceptionがthrowされてしまう。
あまりないのかもしれないが、nullをloadする必要がある場合はOptionalを活用する。

Eviction

Guava Cacheでは大きく分けて3つのEvictionを設定できる。
CacheBuilderにEviction設定を付与することで利用できる。

  • 保持数による破棄
    登録されたエントリー数が指定した数値を超えた場合に古いものから破棄される。
    Cache<String, String> cache = CacheBuilder.newBuilder().maximumSize(10).build();
    
  • 重みによる破棄
    重みという表現が正しいのか不明だが、メソッドを見る限りWeightということなので重みとしておく。
    任意の何かを超えた場合に古いものから破棄される。
    任意の何かというのはWeigherクラスで定義する。
    //この例はキャッシュに登録されたvalueのバイト数が10バイトを超えた場合に古いものから破棄
    Cache<String, String> cache = CacheBuilder.newBuilder()
      .maximumWeight(10)
      .weigher(new Weigher<String, String>() {
        @Override
        public int weigh(String key, String value) {
          return value.getBytes().length();
        }
      }).build();
    
  • 時間による破棄
    判断基準となる時間が2つ存在し、それぞれその時間から有効期限が過ぎたものから破棄される。
    //最後にアクセスしたタイミング
    Cache<String, String> cache = CacheBuilder.newBuilder()
      .expireAfterAccess(3, TimeUnit.HOURS)
      .build();
    
    //作成したタイミング
    Cache<String, String> cache = CacheBuilder.newBuilder()
      .expireAfterWrite(1, TimeUnit.DAYS)
      .build();
    

GCに関連した設定

キャッシュデータを強参照じゃなくすことができる。
これによりGCされた時に破棄されやすくできる。
正直そこまで厳密にGC制御しなきゃいけないアプリ作ったことはないので、あまり使う機会はなさそう。

//keyをWeakReferenceにすることで強参照が存在しなくなり、GCが発生したタイミングでkeyも破棄される
Cache<String, String> cache = CacheBuilder.newBuilder().weakKeys().build();

//valueをWeakReferenceにすることで強参照が存在しなくなり、GCが発生したタイミングでvalueも破棄される
Cache<String, String> cache = CacheBuilder.newBuilder().weakValues().build();

//valueをSoftReferenceにすることで弱参照よりは強い参照となり、メモリに余裕があればGCが発生しても破棄されない可能性がある
Cache<String, String> cache = CacheBuilder.newBuilder().softValues().build();

キャッシュの更新

LoadingCacheパターンの場合のリフレッシュ方法は手動更新と自動更新の2種類存在する。

  • 手動でキャッシュを更新
    refreshメソッドを使うことで更新できる。
    loadingCache.refresh("key");
    
  • 自動でキャッシュを更新
    refreshAfterWriteを事前に設定しておくことで自動更新が可能。 また、Evictionのexpire系に似ているが、あちらはデータが破棄されるが、こちらは古いデータを保持し続けている。
    LoadingCache<String, String> loadingCache = CacheBuilder.newBuilder()
      .refreshAfterWrite(1, TimeUnit.MINUTES)
      .build(new CacheLoader<String, String>() {
        @Override
        public String load(String key) throws Exception {
          return key.toUpperCase();
        }
      });
    

削除時の追加処理

キャッシュからデータが削除されたときに、何らかのアクションを起こす必要がある場合に、RemovalListenerを利用して追加処理を行うことができる。
以下の例はEvictionで破棄された時にコンソールにキーを出力する処理。

Cache<String, String> cache = CacheBuilder.newBuilder()
  .maximumSize(10)
  .removalListener(new RemovalListener<String, String>() {
    @Override
    public void onRemoval(RemovalNotification<String, String> notification){
        if (notification.wasEvicted()) {
          System.out.println(notification.getKey());
        }
    }
  }).build();

削除された原因はgetCauseメソッドで取得でき、

  • COLLECTED(GCにて破棄)
  • EXPIRED(有効期限切れ)
  • EXPLICIT(削除した時)
  • REPLACED(更新した時)
  • SIZE(サイズ超過)

の5種類存在する。

パフォーマンス計測

各種統計データを簡単に取得できる。
recodeStatsを設定しておき、statsメソッドで統計データを取得する。

Cache<String, String> cache = CacheBuilder.newBuilder().recordStats().build();

//CacheStatusに情報が格納されている
CacheStats cacheStats = cache.stats();

取れるデータとしては以下。
それなりに使えるデータが多いと思うがパフォーマンス問題がどれほど影響するのかな?

  • 平均ロード時間
  • Eviction数
  • ヒット数
  • ヒットレート
  • ロード数
  • ロード時のエラー発生回数
  • ロード時のエラーレート
  • ロードに成功した回数
  • ミスヒット数
  • ミスヒットレート
  • リクエスト数
  • 総ロード時間
ayumukobayumukob

Resources

クラスパスにあるリソースを操作するためのユーティリティ。
ファイル生成のためのURI生成に利用したりしている。

そのリソースネームのURLを返却

Resources.getResource(/sample/json.conf)

URLから全ての文字列を読み込む

String resourceString = Resources.toString(url, Charsets.UTF_8);

URLから全てのバイトを読み込む

byte[] resourceBytes = Resources.toByteArray(url);

URLから全ての行を読み込む

List<String> resourceLines = Resources.readLines(url, Charsets.UTF_8);

Guavaでのファイル操作のためのオブジェクトByteSourceを取得

ByteSource byteSource = Resources.asByteSource(url);

Guavaでのファイル操作のためのオブジェクトCharSourceを取得

CharSource textSource = Resources.asCharSource(url, UTF_8);

ファイル操作に関してはいつか書くかも。

ayumukobayumukob

TypeToken

Javaのジェネリクスはコンパイル時にしか利用できず、実行時にはもう利用できない仕組みになっている。
よく安全ではない警告出るけど、あれは実行時に型情報が消去されるので安全ではないということ。

型を維持できないので、例えば以下のコードだと成功しちゃう。

List<String> stringList = new ArrayList<>();
List<Integer> intList = new ArrayList<>();

boolean actual = stringList.getClass().isAssignableFrom(intList.getClass());

assertThat(actual, is(true));

で、Javaの標準的なリフレクションAPIを使えばメソッドやクラスの型の検出はできて、例えば、List<String>を返却するメソッドがあればリフレクションAPIで戻り値の型であるList<String>を表すParameterizedTypeを取得できる。

TypeTokenはこの仕組みを利用して、ジェネリクスの操作を簡単にしてくれるクラス。

具体的にいうとジェネリクスを持つTypeClassしか利用できなかった操作もできるようにするもの。より便利な追加機能もある。
FW利用のために使っていたが改めて何者か調べると全然わかってなかった。
正直リフレクションをあまり利用してなくて、よくわかってないことが多いのでリフレクションについてはいつか勉強してまとめたい。

TypeTokenで型が何か判断できるので先程の例の処理もちゃんと判断できるようになる。

TypeToken<List<String>> stringListTypeToken = new TypeToken<List<String>>() {}
TypeToken<List<Integer>> integerListTypeToken = new TypeToken<List<String>>(){}

boolean actual = stringListToken.isSubtypeOf(integerListTypeToken);

assertThat(actual, is(false));

isSubtypeOfはこの型が指定された型のサブタイプかどうかを判断してくれるのでfalseとなる。

もうすでに出てしまっているがJavaのリフレクションAPIを知ってないと読めないので、とりあえず今回の記事内でわからないメソッドたちはまとめておく。

  • isAssignableFrom
    このクラスまたはインタフェースが、指定されたクラスまたはインタフェース(親含む)と等しいかどうか
  • getGenericReturnType
    このメソッドの仮の戻り値の型を表すTypeを取得
  • getTypeParameters
    ジェネリック宣言で宣言された型変数を表すTypeVariableオブジェクトの配列を宣言順に取得

インスタンス取得

TypeTokenのインスタンスを取得する方法は3つ

  • Typeをラップする方法
    TypeToken.of(method.getGenericReturnType()) 
    
  • サブクラス(普通は匿名状態)でジェネリクスをキャプチャする方法
    new TypeToken<List<String>>() {}
    
    正直何言ってんだ?って感じだが恐らくStringがサブクラス(どんなクラスでもObjectのサブクラス)って解釈してるけど合ってんのかな??自信ねぇ〜
    例えば以下のコードだとT型をキャプチャしてるだけでStringは取り出せない。
    public class Util {
      public static <T> TypeToken<List<T>> listTypeToken() {
        return new TypeToken<List<T>>() {};
      }
    }
    
    TypeToken<List<String>> stringListTypeToke = Util.<String>listTypeToken();
    
  • 型変数を知っているコンテキストクラスで解決する方法
    protected abstract class IKnowMyType<T> {
      TypeToken<T> typeToken = new TypeToken<T>(getClass()) {};
    }
    
    new IKnowMyType<String>() {}.typeToken
    
    このパターンは先ほどできなかったものの代替みたいな感じかな?

発展版

複数のジェネリクスを持つ複雑なTypeTokenを作成し、実行時にそれらの情報を取得することもできる。

TypeToken<Function<Integer, String>> functionTypeToken
  = new TypeToken<Function<Integer, String>>() {};

TypeToken<?> actual = functionResultTypeToken
  .resolveType(Function.class.getTypeParameters()[1]);

assertThat(actual, is(TypeToken.of(String.class));

リフレクションがわからなすぎて辛すぎるが、getTypeParametersで取得したTypeVariableの2要素目はいわゆるRとなるので、それをresolveTypeに指定してあげるとそれに対応した部分のTypeTokenが取得できるみたい。
他にもMapのEntryもわかったりする。

TypeToken<Map<String, Integer>> mapTypeToken
  = new TypeToken<Map<String, Integer>>() {};

TypeToken<?> actual = mapTypeToken
  .resolveType(Map.class.getMethod("entrySet")
  .getGenericReturnType());

assertThat(actual, is(new TypeToken<Set<Map.Entry<String, Integer>>>() {});
ayumukobayumukob

DoubleMath

整数を判断したいなぁと思ったときに見つけたユーティリティ。
Double型に関する計算を簡単にしてくれるもの。IntMathというユーティリティがあるのだが、内容はそれに類似するらしい。

数学的な整数かどうか判断する

boolean actual1 = DoubleMath.isMathematicalInteger(5);
boolean actual2 = DoubleMath.isMathematicalInteger(5.2);

assertThat(actual1, is(true));
assertThat(actual2, is(false));

で、数学的なという部分が引っかかり色々調べたら、「非数 、無限大」という概念に関しても考慮した形らしい。正直システム要件的には問題なさそうにも思えたが、そんなに厳密は話でなくても問題なさそうだったし、非数とか何言ってんのかよくわからなくなった(笑)ので使用は見送った。

階乗計算

double result = DoubleMath.factorial(5);

2の累乗かどうか

boolean actual1 = DoubleMath.isPowerOfTwo(16);
boolean actual2 = DoubleMath.isPowerOfTwo(10);

assertThat(actual1, is(true));
assertThat(actual2, is(false));

log2計算

double result = DoubleMath.log2(4);

アベレージ計算

double result = DoubleMath.mean(30.1, 20.9, 10);
このスクラップは2023/01/03にクローズされました