Java
AdventCalendar
log
lambda
JavaDay 25

lambda式の遅延実行を利用したログ出力

More than 3 years have passed since last update.

この記事はJava Advent Calendar 2015の25日目の記事です。


この世の中には何十億という人間が住んでいる、だがその中で実際に知り合い言葉を交わすのはごくごく僅かな人々だ、だからなるべくそれらの人々とは仲良くした方がいい、その場合大切なのは挨拶だ、だがお前は正真正銘の化け物だからハウ・アー・ユーとかグッド・モーニンとか言っても誰も相手にしないだろう、クリスマスは祈りと祝福の日だから、人々の心はなごんでいる、死刑囚でさえも特赦を与えられる日なのだから、メリー・クリスマス、とそう挨拶すれば、お前も迎え入れて貰えるかも知れない。


『だいじょうぶマイ・フレンド』- 村上龍 -

せっかくクリスマスに投稿させてもらったのでクリスマスにちなんだ一節を引用してみました。

記事の内容とは関係ありません。


Java8になってlambda式が導入され、関数型プログラミングっぽく記述できることがフィーチャーされがちですがJavaでのlambda式の特徴はそれだけではありません。その特徴の一つに遅延実行1があります。


lambda式の遅延実行

lambda式の遅延実行については多くの人がご存知だと思いますが、例えば



method1(method2());



のような場合、method2が先に実行されそのreturn値がmethod1の引数に渡されmethod1が実行されます。つまり、メソッドが実行される順番はmethod2 → method1の順番になります。

これがlambda式の場合、



method3(() -> method4());



このようなコードがあったとすると、まずmethod3が実行されそのmethod3の処理の中で引数で渡されたlambda式(関数型インターフェイス2オブジェクト)が実行されます3

通常のメソッドと異なり、lambda式の実行は遅延して実行することが可能なのですね。


ログ出力ライブラリ

ここで一旦lambda式の話は置いておいてJavaのログ出力について考えてみたいと思います。

Javaでログ出力を行う時に使用するもので有名なものは以下のような感じでしょうか。


  • Java標準のロギング機能(java.util.logging)

  • Log4j

  • Log4j2

  • Logback

  • Apache Commons Logging

  • SLF4J

これらの中から選択して利用することが多いと思います。

最初の4つは実際にログを出力するロギングライブラリで、後の2つはアダプタになります。

流行りはSLF4J + Logbackの組み合わせのように思いますが、ここではApache Commons Logging + Log4j2で説明したいと思います。


ログ出力時の文字列生成コスト

Javaのログ出力でよくあるのが

if (LOG.isDebugEnabled()) {

LOG.debug("波動拳");
}

のようにログレベルを検査するif文で囲んでログを出力するコードです。

これは、例えばログレベルがERRORの場合、ログには出力されないのに文字列を生成するコストがかかってしまうのを避けるためです。

上の例はコストのかかるような文字列ではありませんが、例えばサイズの大きなListオブジェクトだったり、そもそもログに出力する内容自体をコストのかかる計算で求める場合なんかは役に立つイディオムだと思います。

しかしこのイディオム、何かイヤな感じしませんか?

このif文のせいでJUnitのカバレッジ(C1)が下がってしまうし、かといってそのためだけにログレベルを動的に変更して無駄に2回テストを実行するのもイヤだし、ポリモーフィズムを駆使してせっかくif文のないコードを書いてるのにこのif文のせいで汚された感じだし、第一このためだけにif文書くのがうっとうしい。

そんなことを思っていたのですが、lambda式の遅延実行によってこの問題を回避することができます。


lambda式による無駄な文字列生成の回避

ここで最初に述べたlambda式の遅延実行を使うと、無駄な文字列生成を回避することができます。

LOG.debug(() -> "昇竜拳");

もしこのように書けたら、そしてdebugメソッドの内部でログレベルを判断し、ログを出力する時だけlambda式を実行し文字列を生成するのであれば、無駄な文字列生成を回避することができます。

実際、Java8のjava.util.logging.Loggerクラスには、Supplier<String>4を引数にとるメソッドが定義されています。

例えばinfoメソッドは文字列を引数にとる

public void info(String msg)

以外に

public void info(Supplier<String> msgSupplier)

というものが追加されています。

同様に、Log4j2にも引数にlambda式を指定できるメソッドが存在します。

これらは、実際にログ出力しない場合に無駄な文字列生成を回避するために追加されたメソッドだと言えます。


lambda-logging-jcl

それではログアダプタ、例えばApache Commons Loggingを使っている場合はどうでしょうか?

残念ながらorg.apache.commons.logging.Logインターフェイスにはlambda式を引数に取ることができるメソッドはありません。


「じゃあ、使えへんやん!」

 


確かにそうですね。

ここまで説明してきてそれではあんまりなんで、Apache Commons Loggingと互換性のあるLog4j2用のブリッジを作りました。

https://github.com/kazsharp/lambda-logging-jcl

Mavenのセントラルリポジトリにも登録しましたので、pom.xmlに以下のように定義すれば使うことができます。5

<dependency>

<groupId>jp.gr.java_conf.kazsharp</groupId>
<artifactId>lambda-logging-jcl</artifactId>
<version>0.0.1</version>
</dependency>

使い方はApache Commons Loggingとほぼ同じですが、オブジェクト生成時に

private static LambdaLog4jLog LOG = (LambdaLog4jLog)LogFactory.getLog(Hoge.class);

のようにLambdaLog4jLog型にキャストする必要があります。

何ともダサいですが、キャストしない場合は今まで通りのApache Commons Loggingとして使うことができます。つまり、既にApache Commons Loggingを使ってる場合や、参照ライブラリがApache Commons Loggingを使用している場合でも何の問題もなく使うことができます。

上記の方法でオブジェクトを取得した後は

LOG.info(() -> "波動拳" + "昇竜拳" + "めくり大キックアッパーキャンセル昇竜拳");

LOG.error(() -> "大ゴスしゃがみ中キックキャンセル真空波動拳", e);

のようにラムダ式を使ってログ出力のコードを書くことができます。

もちろん、ログ出力しない場合は不要な文字列の生成コストはかかりません。

以上、lambda式の遅延実行を利用したログ出力でした。

長い文章にお付き合いいただきありがとうございました。m(_ _)m

皆さんが素敵なクリスマスを過ごしますように、そしてフォースと共にあらんことを!





  1. Javaのlambda式の遅延機構についての用語は色々議論されていますが、ここでは多くの人がイメージしやすいであろう「遅延実行」という用語を使うことにします。 



  2. 関数型インターフェイスとは抽象メソッドが一つだけ定義されたインターフェイスのことです。 



  3. もちろん、method3内で関数型インターフェイスオブジェクトを実行するように記述していた場合です。 



  4. Supplierは文字列を返す関数インターフェイスで、lambda式を記述することができます。 



  5. log4jを実行するにはlog4j-coreも必要です(lambda-logging-jclは直接依存していないのでmavenは自動で解決しません)