Status Code 303 - See Other

サーバサイド、iOS・アンドロイドアプリ、インフラレベルの話まで幅広くやってます。情報の誤りや指摘・意見などは自由にどうぞ。

C言語 の long int データ型仕様

概要

今回、開発環境でテストすると正しいが、本番環境では正しい結果が出力できなかった。
あまりにバカらしい理由ではあるが、戒めのために記述する。

起こった事

環境

この案件では、C 言語で記述したプログラムを本番環境に送って実行する。
私は、Windows 7 64bit のデスクトップで cygwingcc (4.9.3) を使って開発。

問題箇所

ある計算結果を出力するのだが、その結果は結構大きな値になってしまうので、
結果を格納するデータの型に long int を使用。そして、結果の表示において最初以下のように記述していた。

	long int result = (計算部);
	printf("%d", result); // 588491482334(期待値)

すると、出力結果が明らかに小さい。

80962782

そして、自分が フォーマッタを『%d』にしていたミスに気付く。
『%d』→ 『%ld』に修正して、テスト。

	long int result = (計算部);
	printf("%ld", result); // 588491482334(期待値)

出力結果が期待されるものになった。

588491482334

これで問題ないと思い、本番環境に送信して実行。その出力結果。

80962782

なぜか出力が戻った。送信したプログラムを確認したが問題ない。

問題解決フェーズ

計算結果を小さくしてみると正しい値が表示されるため、この現象は計算結果が大きくなった時のみ起こる。
しかし、フォーマッタは『%ld』を指定しているので、問題ないはずだが・・。
試しに『%lu』に変更してみたが効果なし。

今は C 言語の仕様が変わったのか?と思い、C 言語の仕様書を漁るもどこも変更なさそう。

仕方がないので本番環境で試しに、『LONG_MAX』(limits.h)を出力してみた。

2147483647

この瞬間、計算期待値がlong int の表現範囲を超えてることに気づく。
そこで、下記のように修正。

	long long int result = (計算部);
	printf("%lld", result); // 588491482334(期待値)

本番環境でも動作を確認。こんな単純な問題に2時間掛けてしまった。

C 言語のデータ型仕様

この問題の要因を作った、long int の表現範囲を調べる。
まず、一般的には(データ型を参照)

int → 2byte(-32768~32767)を記憶できる
long int → 4byte (-2147483648~2147483647)を記憶できる
long long int → 8byte (-9223372036854775808~9223372036854775807)を記憶できる

まず、ここで既に勘違い。
long int が 8byte だと思ってた。(Java の long 型が 8byte だから)
(付け加えるなら long long int は 16byte だと思ってた)

そして、仕様は結構アバウトであり、基本的に下限だけ決まっている。
つまり、byte 数が大きい分には構わない。実際、私の開発環境では

#include<stdio.h>
#include<limits.h>

int main() {
	printf("INT_MAX:\t%d\n", INT_MAX);
	printf("LONG_MAX:\t%ld\n", LONG_MAX);
	printf("LLONG_MAX:\t%lld\n", LLONG_MAX);
	return 0;
}

その結果。

INT_MAX:        2147483647
LONG_MAX:       9223372036854775807
LLONG_MAX:      9223372036854775807

long int は 8byte だった。しかも、 long long int とデータサイズ同じだった。
普段そんなに大きな値を使わないから、全く意識できていなかった。
反省。

結論

基本的なことではあるけれど、データ仕様は正しく覚える。
環境によってデータサイズが異なる事にも留意する。

各ケースのデータ選択には分岐より規約に基づいた処理を適用する

概要

分岐処理は、手軽に記述できるため、多くの場面で利用される。

しかし、あるデータの値によって、途中のデータを選択する処理を分岐を用いて
実装してしまうと、全てのケースをカバーしなければならないため、
新たなケースの追加や既存のケースの修正によって、プログラムを修正する必要がある。

もし、これら各ケースをプログラムでは同じ処理だと認識できれば、
ケースの追加やケースの修正を感知しないため、もっと保守性を向上できる。
今回はこれについて説明する。

詳細説明

今回は、会員のカテゴリ(VIP, 会員, 無料会員)があり、
これらのカテゴリ毎にメッセージを別々に表示したいとしよう。
まずは、改善の余地がある処理を提示する。

package sample;

import resources.properties.Property;

public class Main {

	public static void main(String[] args) {
		
		Property property = new Property("message");
		String title, message;
		
		// ここで会員カテゴリを取得するとする.
		String category = getMemberCategory();
		// ここでデータを選択する.
		if(category.equals("A")) {
			title = property.getString("vip.title");
			message = property.getString("vip.message");
		}else if(category.equals("B")) {
			title = property.getString("member.title");
			message = property.getString("member.message");
		}else if(category.equals("C")) {
			title = property.getString("normal.title");
			message = property.getString("normal.message");
		}else {
			throw new IllegalStateException("カテゴリ名が不正です");
		}
		// データを用いた処理.
		System.out.println("title: " +  title);
		System.out.println("message: " +  message);
	}
	// コンパイラを通すために仮の値を作成.
	private static String getMemberCategory() {
		return "B";
	}
}

なお、property クラスは、message.properties からキーに対応した値を取得するクラスである。
以下の過去記事にその実装例がある。
プロパティファイルの使用方法 - Status Code 303 - See Other
コンストラクタでリソースファイル名を指定できる実装にしているが、基本的に動作は同じ。

そして、タイトルやメッセージを格納した message.properties はこちら。

vip.title = VIP会員様へ
vip.message = 今お買い物頂くと 40% のポイントが溜まります。
member.title = 会員様へ
member.message = 今お買い物頂くと 20% のポイントが溜まります!
normal.title = 無料会員様へ
normal.message = 正会員になれば、ポイントが溜まり、1p=1円でご利用できます!

タイトルとメッセージを外出しファイルにすることで、内容の変更は properties ファイルの
更新だけになり、プログラムを修正する必要がないようにしている。

しかし、もうちょっと保守性を向上できる。
今回、ネックとなっているのは分岐の部分だ。

分岐を用いた、このような実装は自然に映るかもしれないが、
会員カテゴリが追加、変更された場合は、以下を変更しなければならない。

  • プログラム(if 文の分岐部分)
  • リソースファイルの xxx.title, xxx.message キーの追加

本来、プログラムは処理を記述するものであるから、データ追加の影響は受けたくない。
タイトルとメッセージだけを追加すれば動作するようにしたい。

これを実現するには、下記のように message.properties のキー名を
カテゴリに対応したキー名にし、それを処理で取得するようにする。
今回の例では、次のようにする。

  • [カテゴリ名] + ".title" の値をタイトルに表示する
  • [カテゴリ名] + ".message" の値を本文に表示する

このように、リソースデータの記述方法を規約化してしまうことによって、
各ケースに対する処理をまとめてしまう。

package sample;

import java.util.MissingResourceException;

import resources.properties.Property;

public class Main {

	public static void main(String[] args) {
		
		Property property = new Property("message");
		String title, message;
		
		// ここで会員カテゴリを取得するとする.
		String category = getMemberCategory();
		try {
			// ここでデータを選択する.
			title = property.getString(category + ".title");
			message = property.getString(category + ".message");

		} catch(MissingResourceException e) {
			throw new IllegalStateException("カテゴリ名が不正です");
		}
		// データを用いた処理.
		System.out.println("title: " +  title);
		System.out.println("message: " +  message);
	}
	// コンパイラを通すために仮の値を作成.
	private static String getMemberCategory() {
		return "B";
	}
}

message.properties は以下のようにキー名を変更する。

# 存在するカテゴリ名 + .title, + .message をデータとして取得する
A.title = VIP会員様へ
A.message = 今お買い物頂くと 40% のポイントが溜まります。
B.title = 会員様へ
B.message = 今お買い物頂くと 20% のポイントが溜まります!
C.title = 無料会員様へ
C.message = 正会員になれば、ポイントが溜まり、1p=1円でご利用できます!

このようにすれば、もし、カテゴリが追加されたとしても、message.properties に
新しいカテゴリ "D" に対する title と message を追加すれば、処理は変更しなくて良くなる。

# 存在するカテゴリ名 + .title, + .message をデータとして取得する
A.title = VIP会員様へ
A.message = 今お買い物頂くと 40% のポイントが溜まります。
B.title = 会員様へ
B.message = 今お買い物頂くと 20% のポイントが溜まります!
C.title = 無料会員様へ
C.message = 正会員になれば、ポイントが溜まり、1p=1円でご利用できます!
# 追加
D.title = お試し会員様へ
D.message = お試し期間中は全サービスが使えますが、有効期間は1週間です!

その出力結果。(getMemberCategory() で "D" を返すように変更)

title: お試し会員様へ
message: お試し期間中は全サービスが使えますが、有効期間は1週間です!

今回は、簡単のため、同一 properties ファイル内にデータを記述しているが、
カテゴリによって変更されるデータの数が多くなればなるほど、大量のデータで混沌とする。

この場合は、読み込むリソースファイル自体を変更する。
例えば カテゴリが "A" の場合、message_A.properties を読み込むようにする。

注意点として、この方法は各ケースの異なる部分が処理データだけだということ。
もし、各ケースの処理自体が異なっている場合は、単純にこの方法は適用できない。

もし、データ選択とそのデータの処理がコード的に分割できるのであれば、
まずそれを行ってから、上記変形を行う。

結論

データの値によって、処理されるデータを変更したい場合は、
リソースファイルに規約を適用した処理を記述することによって、
ケースの追加や変更にリソースファイルだけで対応できるようになる。

しかし、各ケースによって処理自体が異なってしまう場合は、単純に適用できない。
もし、これらからデータ選択部と処理部が分割できるなら、それをまず行う。

テンプレートによるロジックとデザイン切り分け

概要

デザインとロジックを切り分ける内容。
本来これらは全く別個に管理すべきなものであるため、分離させたい場合が多くある。
今回は、Java でこの方法を実現する手順を記述。

デザインとロジックの混在する悪い例

public class Main {

	public static void main(String[] args){
		String user = "hoshi-kouki";
		boolean isMember = true;
		Cart cart = new Cart();
		cart.add(new Item("タマネギ", 100));
		cart.add(new Item("じゃがいも", 150));
		cart.add(new Item("人参", 100));
		cart.add(new Item("肉", 500));

		System.out.println(user + " 様");
		System.out.println();
		System.out.println("以下商品を購入されたことを通知します。");
		System.out.println("===========================");
		for(Item item : cart.getItems()) {
			System.out.println(item.getName() + "		"  + item.getPrice() + " 円");
		}
		System.out.println("--------------------------");
		System.out.println("合計		" + cart.sum() + " 円");
		System.out.println("--------------------------");		
		if (isMember) {
			System.out.println("いつもお買い上げ頂きありがとうございます!");		
		}else {
			System.out.println("会員登録するとポイントがもらえますよ!");		
		}
	}	
}

まずい理由

  • ロジックとデザインが混在するため、どちらも内容が分かり辛い。
  • デザイン変更→ロジック破壊、ロジック変更→デザイン破壊が起こる可能性
  • デザインを管理すべき人が自由に変更できない
    • デザイン担当する人がプログラマと同じことができるとは限らない

本来あるべき姿

  • デザインを担当する人は、その人の好きなツールで作成
  • そのデザインを内部に取り込む仕組みはプログラマが作成

テンプレートの仕組み作成

Velocity - Velocity User Guide を用いたサンプルを記述する。
インストール手順は、ここ参照(http://hack.aipo.com/archives/8360/)。

事前設定
  1. プロジェクト作成
  2. velocity.zip ダウンロード
  3. 展開 → velocity-x.x-dep.jar (x.xはバージョン) プロジェクト内にコピー
  4. ビルドパスに velocity-x.x-dep.jar 設定
詳細説明

今回実装したテンプレート利用クラス(OutputBuilderクラス).

import java.io.StringWriter;

import org.apache.velocity.Template;
import org.apache.velocity.VelocityContext;
import org.apache.velocity.app.Velocity;

/**
 * 既に定義されたテンプレートファイルからプログラム上で様々な値を
 * セットして処理に応じた結果を作成する。
 */
public class OutputBuilder {

	/**
	 * ファイル名.
	 */
	private String fileName;
	
	/**
	 * Velocity コンテキスト.
	 */
	private VelocityContext context;

	/**
	 * コンストラクタ.
	 * @param fileName ファイル名
	 */
	public OutputBuilder(String fileName) {
		this.fileName = fileName;
		context = new VelocityContext();
	}

	/**
	 * Velocityコンテキストにパラメータを設定する.
	 * @param key キー名
	 * @param value
	 * @return 呼び出したテンプレートビルダー自身
	 */
	public OutputBuilder set(String key, Object value) {
		context.put(key, value);
		return this;
	}

	/**
	 * 格納した結果から文字列を作成する.
	 * @return 文字列
	 */
	public String build() {
		Velocity.init();
		Template template = Velocity.getTemplate(fileName, "UTF-8");
		StringWriter writer = new StringWriter();
		template.merge(context, writer);
		return writer.toString();
	}
}

今回は最小構成で作るため最低限のみ。機能追加は本家ドキュメントを参考に。

使い方

請求書を作成するとして記述する。

  1. 必要データ洗い出し
    請求書に必要なデータは、今回は以下とする。

    • 購入者
    • 購入商品・その値段
    • 合計金額
    • 購入者が会員/非会員かどうか


  2. Value Object(VO)用意
    基本データ(文字・数値)のみで構成しても動作するが、モデル化クラスを利用した方が分かりやすくなる。

    • 商品の属性情報を管理するクラス。(Itemクラス)
    • 商品自体を管理するクラス (Cartクラス)


  3. 各クラス実装
    Itemクラス

    /**
     * 商品情報を管理する.
     */
    public class Item {
    	/**
    	 * 商品名.
    	 */
    	private String name;
    	/**
    	 * 単価.
    	 */
    	private int price;
    	/**
    	 * コンストラクタ.
    	 * @param name 商品名
    	 * @param price 単価
    	 */
    	public Item(String name, int price) {
    		this.name = name;
    		this.price = price;
    	}
    	public String getName() {
    		return name;
    	}
    	public int getPrice() {
    		return price;
    	}	
    }
    

    Cartクラス

    import java.util.ArrayList;
    import java.util.List;
    
    /**
     * 購入商品を管理する.
     */
    public class Cart {
    	/**
    	 * 購入商品.
    	 */
    	private  List<Item> items;
    	/**
    	 * コンストラクタ.
    	 */
    	public Cart() {
    		items = new ArrayList<>();
    	}
    	/**
    	 * 商品リストを取得する.
    	 * @return 商品リスト
    	 */
    	public List<Item> getItems() {
    		return items;
    	}
    	/**
    	 * 商品を items に追加する.
    	 * @param item 追加する商品
    	 */
    	public void add(Item item) {
    		items.add(item);
    	}
    	/**
    	 * 商品の合計金額を取得する.
    	 * @return 金額
    	 */
    	public int sum() {
    		int sum = 0;
    		for(Item item : items) {
    			sum += item.getPrice();
    		}
    		return sum;
    	}
    }
    


  4. テンプレート(bill.vm)作成

    $name 様
    
    以下商品を購入されたことを通知します。
    ===========================
    #foreach ($item in $cart.getItems())
    $item.getName()		$item.getPrice() 円
    #end
    --------------------------
    合計		$cart.sum() 円
    ===========================
    #if($isMember)
    いつもお買い上げ頂きありがとうございます!
    #else
    会員登録するとポイントがもらえますよ!
    #end

    今回は紹介のため様々なものを使っているが、
    デザインの全体像が分かり辛くなるため分岐(#if ~ #else ~ #end)はあまり使わない方が良い。

    なお、上記にある特殊な意味を持つ文字について説明すると。

    $name

    setメソッドで渡したキー名'name'に対応した値を表示。

    $cart.getItems()

    setメソッドで渡したキー名'cart'に対応するデータに対して getItem メソッドを呼び出した結果を表示。

    #foreach ($item in $cart.getItems()) (処理) #end

    (処理)を集合個数($cartにはいったアイテム分)実行する。
    各繰り返し処理において、カートの中の各要素がループごとに $item にセットされ、処理に利用できる。

    #if($isMember) (処理1) #else (処理2) #end

    条件によって、(処理1)か(処理2)の結果が表示。


  5. ロジック記述
    ロジックにテンプレートに対する処理を記述。

    1. 使用するテンプレート指定 (OutputBuilder コンストラクタ)
    2. $name, $isMember, $cart に使用するデータ設定(setメソッド)
    3. テンプレートの指定箇所にロジックで得たデータを組み合わせた結果を得る(build メソッド)
    public class Main {
    
    	public static void main(String[] args) {
    		Cart cart = new Cart();
    		cart.add(new Item("タマネギ", 100));
    		cart.add(new Item("じゃがいも", 150));
    		cart.add(new Item("人参", 100));
    		cart.add(new Item("肉", 500));
    		
    		OutputBuilder template = new OutputBuilder ("bill.vm").set("name", "hoshi-kouki").set("isMember", true).set("cart", cart);
    		System.out.print(template.build());
    	}
    }
    

  6. 出力結果は以下。

    hoshi-kouki 様
    
    以下商品を購入されたことを通知します。
    ===========================
    タマネギ		100 円
    じゃがいも		150 円
    人参		100 円
    肉		500 円
    --------------------------
    合計		850 円
    ===========================
    いつもお買い上げ頂きありがとうございます!

    bill.vm の所定の箇所にデータが埋め込まれた形で出力できていることが分かる。

参考文献

Start up the Velocity Template Engine | JavaWorld
http://hack.aipo.com/archives/8360/

更新履歴

2016/01/20 変な表現多すぎたので、記事修正。
      TemplateBuilderクラス → OutpuBuilderクラス(テンプレート作るわけじゃないし)
2016/06/13 説明が冗長すぎて分からなかったので修正。

Lombok ライブラリ - フィールド関連

主記事
Lombok ライブラリ - Status Code 303 - See Other

この記事では、フィールドに定義可能なアノテーションを記述。
(記事執筆時のバージョン :v1.16.6)

(更新中)

アクセッサ自動生成

指定されたフィールドのアクセッサを提供する。

@Getter

どのようなフィールドに対しても、Lombok が標準的なゲッタを提供する。

標準的なゲッタとは、おそらく以下の内容だと思われる。

  • JavaBeansの命名規則を満たすゲッタ
  • 処理はフィールドをそのまま返す

例:

	private @Getter int foo;

上記を設定することにより、下記のコードが生成される。

	public int getFoo() {
		return this.foo;
	}
オプション

lazy(デフォルト:false)
明示的に書かれていない。遅延初期化?
onMethod
Lombok により生成されたゲッタに設定するアノテーションをリスト形式で記述する。
value(デフォルト:public)
アクセスレベルをゲッタに設定する。

@Setter

指定されたフィールドにセッタを提供する。

オプション

lazy(デフォルト:false)
明示的な説明がない。遅延初期化?
onMethod
Lombok により生成されたセッタに設定するアノテーションをリスト形式で記述する。
value(デフォルト:public)
アクセスレベルをセッタに設定する。

コレクションフィールドに対するデータ格納メソッドの自動生成

@Singular

@Builder を使用しているときに用いられ、コレクションデータのフィールドに指定すると、それに要素を追加する add メソッドに相当するメソッドを自動生成する。
生成するメソッド名は、指定するフィールド名の末尾が「s」であれば、末尾の「s」のないメソッド名になる。
もし、末尾が「s」で終わっていないフィールド名に指定する場合、value オプションで明示しなければコンパイルエラーが発生する。

メソッドパラメータにも指定できるみたいだが、その説明はされていない。

オプション

value
コレクションにデータを格納するメソッド名を指定する。

null チェックの自動生成

@NonNull

Lombok から生成された全てのメソッドにおいて、該当のフィールドに値を割り当てる処理をする前に nullチェックを行う。
もし、 割り当てる値が null だったなら NullPointerException をスローする。

オプション

なし。

注釈
  • たとえ異なるパッケージだったとして @NonNull が付属している生成メソッドには Lombok が null チェックを生成してしまう。
  • このアノテーションは、Java Communityでもサポートすることが決まっているため、Lombok パッケージからは今後削除される予定。
  • @NonNull 削除による影響があるなら、自身で @NonNull を作成し、代わりに使うこと

Lombok ライブラリ - その他

主記事
Lombok ライブラリ - Status Code 303 - See Other

この記事では、クラスとフィールド以外に関連するアノテーションを記述。
(記事執筆時のバージョン :v1.16.6)

(更新中)

データ同期

@Synchronized

このアノテーションメソッドに設定すると、メソッドに synchronized キーワードを設定したときとほとんど同等動きをする。
プライベートな内部インスタンスを除いては、管轄外のコードがスレッド操作に割り込まないように、そのインスタンスをロックする。

staticでないメソッドには $lock、static なメソッドには $Lock という名称のフィールド変数がロックのために使われる。
これらは、必要であったり、存在しなかった場合のみ作成され、このフィールドはシリアライズ可能である。

オプション

value
ロックに利用するフィールド名を異なる名前に設定したい場合に使用する。 注意事項として、コンパイル時までにその変数を作成しなければならず、存在していない場合はコンパイルエラーになる。 (デフォルトコンストラクタと同じように、自動生成されるのは何も指定しなかったときのみ)

型推論

val

ローカル変数にのみ使用可能であり、初期化の式から型を推測する。
また、val を定義したローカル変数は、final になる。
例:

val x = 10.0;
val y = new ArrayList<String>();

と記述すれば、 val は以下と同等とみなされる。

final double x = 10.0;
final ArrayList<String> y = new ArrayList<String>();

なお、あまりそうは見えないかもしれないが val はアノテーション型である。(正確にはシンタックスシュガーらしい)
なぜなら、 val x = 10.0; と記述すれば、Lombok は @val final int x = 10; にコードを変換するから。
詳細はこちら(val)

オプション

なし。

ツールに対する適用外設定

@Generated

いずれは、Lombok が生成したメソッドやクラスに自動的に付加するアノテーション
全てのコードスタイルチェックツールやバグ発見ツールに対して、これらのコードを無視させるために使用する。

オプション

なし。

注釈

このアノテーションが導入されたバージョン 1.16.2 では、実際は Lombok はこのアノテーションを付加できていない。
このため、広く普及している Lombok.jar がこの機能を持つように準備中であり、これらは近い将来使用できるようになるだろう。

メソッドパラメータの null チェック自動生成

@NonNull

詳細は、メソッド @NonNull 参照。
注意点として、メソッドのパラメータにこれを指定すると、Lombok によって生成メソッドでなくても適用される。

メソッドやローカル変数にも設定できるらしいが、その場合の記載はない。

リソース解放処理の自動生成

@Cleanup

何が起こったかに関係なく、ローカル変数がそれらの close メソッド呼び出しによってリソースの解放を保証する宣言。
ローカル変数の有効範囲にある全ての命令文を包括した try 文を実装することにより、リソースを解放する。
詳細なドキュメントこちら(@Cleanup)。

例:

 public void copyFile(String in, String out) throws IOException {
     @Cleanup FileInputStream inStream = new FileInputStream(in);
     @Cleanup FileOutputStream outStream = new FileOutputStream(out);
     byte[] b = new byte[65536];
     while (true) {
         int r = inStream.read(b);
         if (r == -1) break;
         outStream.write(b, 0, r);
     }
 }

これらの宣言が Lombok によって以下の処理に変換される。

 public void copyFile(String in, String out) throws IOException {
     @Cleanup FileInputStream inStream = new FileInputStream(in);
     try {
         @Cleanup FileOutputStream outStream = new FileOutputStream(out);
         try {
             byte[] b = new byte[65536];
             while (true) {
                 int r = inStream.read(b);
                 if (r == -1) break;
                 outStream.write(b, 0, r);
             }
         } finally {
             if (out != null) out.close();
         }
     } finally {
         if (in != null) in.close();
     }
 }
オプション

value(デフォルト:close)
リソース解放メソッドが close ではないときにそのメソッド名を指定する。 ただし、指定するメソッドは1つもパラメータを持たないものでなければならない。

チェック例外 → 実行時例外変換

@SneakyThrow

Java では、メソッド処理内にチェック例外を生成し得る命令が記述された場合、その例外に対処しなければならない。
(try-catch 文で例外をキャッチするか、メソッド呼び出し元に伝播させるなら throws 宣言を使ったりする)
しかし、このアノテーションメソッドに記述すると、本来行うはずのチェック例外に対処しなくても良くなる。

このアノテーションは、内部的にオプション(value)に指定されたチェック例外(複数可)を
RuntimeException もしくは他の実行時例外にラップしている。こうして、チェック例外が実行時例外内に隠れるため、
本来 JVM が行うはずだった、チェック例外における一貫性チェックを回避している。

例:
下記のバイト列を特定の文字コードから String インスタンスを生成するコンストラクタはチェック例外である
UnsupportedEncodingException をスローするため、本来は何かしら対処をしなければならない。
しかし、以下の例では、これを省略してもプログラムは正常に動作する。

 @SneakyThrows(UnsupportedEncodingException.class)
 public void utf8ToString(byte[] bytes) {
     return new String(bytes, "UTF-8");
 }

これは、Lombok が上記コードを内部的に下記のように変換しているから。

public void utf8ToString(byte[] bytes) {
     try {
         return new String(bytes, "UTF-8");
     } catch (UnsupportedEncodingException $uniqueName) {
         throw useMagicTrickeryToHideThisFromTheCompiler($uniqueName);
         // This trickery involves a bytecode transformer run automatically during the final stages of compilation;
         // there is no runtime dependency on lombok.
     }
}

なお、元のコードと同じ動きを保証するために発生する条件では、本来スローされるべき例外がスローされる。

import java.text.SimpleDateFormat;
import lombok.SneakyThrows;

public class Main {
	@SneakyThrows
	public static void main(String[] args) {
		System.out.println(new SimpleDateFormat("yyyyMMdd").parse("aaaaa")); // 意図的に例外(ParseException)を発生させる
	}
}

出力結果。

Exception in thread "main" java.text.ParseException: Unparseable date: "aaaaa"
	at java.text.DateFormat.parse(DateFormat.java:366)
	at Main.main(Main.java:8)

詳細はこちら(@SneakyThrows)

オプション

value(デフォルト:java.lang.Throwable.class)
メソッド呼び出し元にこっそりスローしたい例外型を指定する(複数可能)。

Lombok ライブラリ - クラス関連

主記事
Lombok ライブラリ - Status Code 303 - See Other

この記事では、クラスに定義可能なアノテーションを記述。
(記事執筆時のバージョン :v1.16.6)

クラス性質

ある性質を有するクラス構造を提供する。

@Data

全てのフィールドに対するゲッタ、全ての transient ではないフィールドを利用した便利な toString, equals, hashCodeメソッドを提供する。
また、全ての final ではないフィールドに対するセッタと必要パラメータ用コンストラクタを提供する。

オプション

staticConstructor
生成インスタンスを指定した名前のstaticメソッドでのみ提供できる。

import lombok.Data;
import lombok.NonNull;

@Data(staticConstructor="of")
public class Book {

	private final String id; // final なので引数に追加される
	@NonNull
	private String title;    // @NonNull なので引数に追加される
	private String author;
	private int page;

	public static void main(String[] args) {
		// 使用方法
		Book b = Book.of("ID", "Title");
	}
}

注釈

コンストラクタアノテーションを一緒に記述すると public なコンストラクタが優先して提供され、static メソッドが提供されなかった。

@Value

不変クラスの表現に合致したコードを生成する。具体的には、下記がクラスに設定される。

オプション

staticConstructor
(@Data参照)

注釈

フィールド宣言に final がなくても、Lombok による自動生成コード上では
final が付与されるため以下のコードは、コンパイルエラーとなる。

import lombok.Value;

@Value
public class Book {

	private String id; // 宣言は final ついていない
	private String title;
	private String author;
	private int page;

	public static void main(String[] args) {
		Book b = new Book("ID", "Title", "Author", 5);
		b.author = "AAA"; //コンパイルエラー finalなので変更できない
	}
}

また、不変クラスとして扱うとあるが、フィールドに可変インスタンスが存在していた場合、
ゲッタはその参照を返してしまうので、そのフィールドの内部は変更されることに注意する。

@Builder

「指定したクラス」もしくは「指定されたメンバーメソッドを含むクラス」に対して、
いわゆる Builder パターン機能を付加する。

指定可能な要素

なお、既にクラスに private な全フィールドのパラメータを持つコンストラクタが存在しても、上記が作成される。

指定クラスの影響

private コンストラクタをメンバーに持つ「(指定クラス名)Builder」という
インナークラス(以後 Builder クラス)が生成される。
Builder クラスのインスタンス(以後 Builder インスタンス)を生成するメソッドとして、
static の builder メソッドが生成される。

なお、Builderクラスのインスタンスメソッドに似たものがあるが、
こちらは「 build 」メソッド( er がない)で全くの別もの。(詳細は下記)

Builder クラスの構成

なお、上記 builder メソッドの返す型は、static メソッドに指定されていなければ、指定クラスの型が返る。

詳細な説明はこちら。(@Builder)
例:

@Builder
class Example {
	private int foo;
	private final String bar;
}

このように設定したクラスは Lombok によって次のように変換される。

class Example<T> {
	private T foo;
	private final String bar;

	private Example(T foo, String bar) {
        	this.foo = foo;
		this.bar = bar;
	}
	public static <T> ExampleBuilder<T> builder() {
		return new ExampleBuilder<T>();
	}

	public static class ExampleBuilder<T> {
		private T foo;
		private String bar;

		private ExampleBuilder() {}
		public ExampleBuilder foo(T foo) {
			this.foo = foo;
			return this;
		}
		public ExampleBuilder bar(String bar) {
			this.bar = bar;
			return this;
		}
		@java.lang.Override public String toString() {
			return "ExampleBuilder(foo = " + foo + ", bar = " + bar + ")";
		}
		public Example build() {
			return new Example(foo, bar);
		}
	}
}
オプション

builderClassName(デフォルト:builder)
ビルダークラス名を変更する。デフォルトでは、@Builder 指定クラスとコンストラクタから「(指定クラス)Builder」という名前になる。
builderMethodName
Builder インスタンスを作成する static のメソッド名を変更する。
buildMethodName(デフォルト:build)
@Builder が指定されたクラスのインスタンスを作成する Builder クラスのメソッド名を変更する。
toBuilder(デフォルト:false)
@Builder が指定されたクラスのフィールドに Builder インスタンスの初期化に関わるデータがあり、そのデータから Builder インスタンスを生成するインスタンスメソッドを生成する必要がある場合は true を指定する。 なお、このオプションが指定可能なのは、

の場合である。

コンストラクタ自動生成

コンストラクタを自動生成するアノテーションについて記述する。

@AllArgsConstructor

全てのフィールドに対する初期化データを必要とするコンストラクタを提供する。

オプション

access(デフォルト: public)
コンストラクタのアクセスレベルを設定できる。
onConstructor
Lombokにより生成されたコンストラクタに設定するアノテーションのリストを記述する。
staticName
生成インスタンスを指定した名前のstaticメソッドでのみ提供可能にする。型パラメータをクラスに渡すときに便利らしい。

@RequiredArgsConstructor

初期化が必要なフィールドに対する引数データを持ったコンストラクタの提供。
初期化が必要なフィールドとは、finalもしくは@NonNullが付属したフィールドを指す。

オプション

access, onConstructor, staticName
@AllArgsConstructorと同等。

@NoArgsConstructor

引数のないコンストラクタを提供する。もし、このアノテーションを指定しているにも関わらず
クラス定義に final フィールドが存在し、かつ、オプション forceが true でない場合、コンパイルエラーとなる。
(@NonNullは無視されるため、記述されていても問題ない)

オプション

access, onConstructor, staticName
@AllArgsConstructorと同等。
force(デフォルト:false)
trueなら、全てのfinalフィールドを 0 / null/ falseに初期化する。

ユーティリティメソッド自動生成

特定のメソッドを提供するアノテーションについて記述する。

@ToString

関連フィールドから構成される、継承クラスによって構成される toString メソッドを実装する。
詳細ドキュメント(@ToString)。

オプション

callSuper(デフォルト:false)
生成された toString メソッドに、スーパークラスメソッド出力を含める場合。
doNotUseGetters(デフォルト:false)
生成された toString メソッドにフィールドを直接参照させる場合。 デフォルトでは、ゲッタが利用可能であれば、それを通じてアクセスする。
exclude
生成される toString メソッドの出力に特定フィールドを除外したい場合。of とは一緒に記述できない。
includeFiledNames(デフォルト:true)
生成される toString メソッドの出力にフィールド名を含める場合。
of
生成される toString メソッドの出力に含めるフィールドを明示的に設定したい場合。exclude とは一緒に記述できない。

@EqualsAndHashCode

関連フィールドに基づいた、全ての継承クラスに対する equals と hashCode メソッドを生成する。

オプション

callSuper(デフォルト:false)
このクラスのメソッドを実行する前に、スーパークラスのequals/hashCodeメソッドを呼び出す。
doNotUseGetters(デフォルト:false)
生成メソッドにフィールドを直接参照させる場合には true を設定する。 デフォルトでは、ゲッタが利用可能であればそれを通じてアクセスする。
exclude
生成される equals/hashCodeメソッドの同値条件から除外したいフィールドを指定する。of とは一緒に記述できない。
onParam
生成される equals/hashCodeメソッドアノテーションを設定したい場合指定する。
of
生成された equals/hashCodeメソッドの同値条件に含めるフィールドを明示的に指定する。 デフォルトでは、全てのstatic, transient でないフィールドが使われ、exclude とは一緒に記述できない。

アクセッサ自動生成

@Getter

クラス内の @Getter が既に設定されていない static でないフィールドに対して、ゲッタを提供する。
詳細は、フィールド関連 - @Getterを参照。

オプション

フィールドと異なる点として onMethod は使えない。

lazy
フィールド関連 - @Getterを参照
value
フィールド関連 - @Getterを参照

@Setter

クラス内の static でなく @Getter も設定されていないフィールドに対してセッタを提供する。

オプション

フィールドと異なる点として onMethod は使えない。

lazy
フィールド関連 - @Setterを参照
value
フィールド関連 - @Setterを参照

ツールに対する適用外設定

@Generated

全てのメソッドに@Generatedを適用する。詳細は、その他 - @Generatedを参照。

Lombok ライブラリ

概要

Javaでは、クラスにフィールドを追加することによって、
既存のロジックに影響が出たり、新しく追加する必要があるメソッドが存在する。

また、これらはフィールドが増えるごとにコードの大部分を占めるようになるにも関わらず、
実装は極めて機械的であり、そのロジックはクラスにとって本質的でないことが多い。
このようなコードをボイラープレートコードという。

今回は、アノテーション記述によって、ボイラープレートコードを自動生成する
Javaライブラリの Lombok を調査してみた。

導入

インストール

個人的に分かりやすかったのは、以下のサイト。
【Java】Lombokで冗長コードを削減しよう | キャスレーコンサルティング 技術ブログ

なお、lombok.jar を Eclipse へのインストールを実行した後は、
eclipse.ini に以下2行が入っているか確認した方が良い。
=============================
-javaagent:lombok.jar
-Xbootclasspath/a:lombok.jar
=============================
私が起動したときは、-Xbootclasspath/a:lombok.jar が記述されて
いなかったため、動かなかった。(Eclipseバージョン: Mars.1 Release (4.5.1))

導入効果

可読性向上

ボイラープレートコードが減ることによって、コードの記述量がはっきりと減る。
同時に、本質的なコードが大部分を占めるようになるため、コードが分かりやすくなる。

保守性向上

フィールドを追加/削除したい場合は、そのフィールドを追加/削除するだけ。
また、関連ロジック (コンストラクタや hashcode/equals/toString メソッド)も
自動で対応するため、そのロジックの整合性を保証できる。

導入例

Before
public class Book {
	private String id;
	private String title;
	private String author;
	private int page;

	public Book(String id, String title, String author, int page) {
		this.id = id;
		this.title = title;
		this.author = author;
		this.page = page;
	}
	public String getId() {
		return id;
	}
	public void setId(String id) {
		this.id = id;
	}
	public String getTitle() {
		return title;
	}
	public void setTitle(String title) {
		this.title = title;
	}
	public String getAuthor() {
		return author;
	}
	public void setAuthor(String author) {
		this.author = author;
	}
	public int getPage() {
		return page;
	}
	public void setPage(int page) {
		this.page = page;
	}
}
After
import lombok.AllArgsConstructor;
import lombok.Data;

@Data
@AllArgsConstructor
public class Book {
	private String id;
	private String title;
	private String author;
	private int page;
}
Use Case
public class Main {
	public static void main(String[] args) {
		Book book1 = new Book("10", "EFFECTIVE JAVA 第2版", "Joshua Bloch", 327);
		Book book2 = new Book("10", "EFFECTIVE JAVA 第2版", "Joshua Bloch", 327);
		Book book3 = new Book("15", "星の王子さま", "サン=テグジュペリ", 245);
		Book book4 = new Book("15", "星の王子さま", "サン=テグジュペリ", 246);

		// toStringメソッド
		System.out.println(book1);
		// equalsメソッド
		System.out.println("book1.equals(book2): " + book1.equals(book2));
		System.out.println("book1.equals(book3): " + book1.equals(book3));
		System.out.println("book3.equals(book4): " + book3.equals(book4));
		// hashCodeメソッド
		System.out.println("book1 hash:" + book1.hashCode() + ", book2 hash: " + book2.hashCode());
		System.out.println("book1 hash:" + book1.hashCode() + ", book3 hash: " + book3.hashCode());
		System.out.println("book3 hash:" + book3.hashCode() + ", book4 hash: " + book4.hashCode());
	}
}
Console Output
Book(id=10, title=EFFECTIVE JAVA 第2版, author=Joshua Bloch, page=327)
book1.equals(book2): true
book1.equals(book3): false
book3.equals(book4): false
book1 hash:712838778, book2 hash: 712838778
book1 hash:712838778, book3 hash: 1107997353
book3 hash:1107997353, book4 hash: 1107997354

Lombok ライブラリ

この他、どのような記述が可能か本家(Lombok)で調査。
(記事執筆時のバージョン :v1.16.6)

規模が大きくなったため、別ページに分割記述。
Lombok ライブラリ - クラス関連 - Status Code 303 - See Other
Lombok ライブラリ - フィールド関連 - Status Code 303 - See Other
Lombok ライブラリ - その他 - Status Code 303 - See Other

対象パッケージ


lombok
lombok のユーザとして必要なクラスや全てのアノテーションを含むパッケージ。
lombok.experimental
長期サポート対象となる前に、その仕様が変更される可能性のある新しいアノテーションを含むパッケージ。
lombok.extern.*
JRE コード以外のライブラリから作成された、ボイラープレートコード問題を解決するアノテーションを含むパッケージ。

今後更新予定


lombok.exprimental
@Accessor, @Delegate, @ExtensionMethod, @FieldDefaults, @Helper, @NonFinal, @PackagePrivate, @Torerate, @UtilityClass, @Wither