kagamihogeの日記

kagamihogeの日記です。

Lombokのチュートリアル記事Reducing Boilerplate Code with Project Lombokをテキトーに訳した

Lombokのチュートリアルhttp://jnb.ociweb.com/jnb/jnbJan2010.html をテキトーに訳した。Lombok勉強の副産物に過ぎないんで、全くといっていいほど推敲してないし、品質についてはお察し。これ読まなくても使えるし、日本語で解説してる人いるし、まぁ多少はね?

なお、本文中のソースコードや画像は原文のものをそのままコピペしています。





Project Lombokによるボイラープレートの削減

はじめに

ボイラープレート("Boilerplate")とは、アプリケーションの様々な箇所に繰り返し出現する上にちょっとずつ異なるコードを説明するときに使われる用語です。Java言語に対する批判としてしょっちゅう聞かれるものの一つがコードの量であり、そうしたコードはたいていのプロジェクトで似通っているものです。この問題は各種ライブラリの設計方針の結果ではあるのですが、言語自身の限界によって事態は悪化してしまっています。Project Lombokは、getter/setter等お約束コードでグチャグチャなのを削減することを目指しています。具体的には、いくらかのアノテーションでそれらのコードの肩代わりをさせます。

使い方を示すためにアノテーションが用いられるのは良くあることですが、バインディングを実装するとかフレームワークが使用するコードを生成するとか、そういう類の操作は、アプリケーションが参照するコード生成にはほとんどの場合に使われません。その理由の一つは、たいていは、開発時に先んじて実行されるアノテーションの付与だけだからです。Project LombokはIDEに統合されているので、開発者はすぐLombokによってインジェクトされたコードを利用できます。たとえば、単に@Dataアノテーションをデータ保持クラスに付け加えると、下図のように、IDEから新しいメソッドの幾らかが見えるようになります。
http://jnb.ociweb.com/jnb/jnbJan2010_files/jnbJan2010-DataAnnotation.png

インストール

Project Lombokは、Project LombokのWebサイトからダウンロードできる単体のJARファイルによって提供されています。このJARファイルはIDE統合のためのインストーラー操作用のAPIを含んでいます。たいていの場合は、単にJARファイルをダブルクリックすればインストーラーが起動します。もしお使いのシステムでJARファイルを正しく起動できない場合、コマンドラインから下記のように実行します。

java -jar lombok.jar

インストーラーはサポートされているIDEの検出を試みます。もしインストールされているIDEが正しく決定できない場合、ディレクトリを手動で設定します。あとは単に"Install/Update"ボタンをクリックすればIDE統合に必要な操作は完了です。この記事*1を書いている段階では、EclipseとNetBeasnのみサポートされています。しかしながら、IntelliJ IDEAソースコードのリリースは、将来のリリースの可能性として、IDEAサポートを置いているし*2、限定的ながらもJDeveloperで成功した事例が報告されています。(※最新版が何をどこまで対応してるかは、公式サイトを要確認と思われる)
http://jnb.ociweb.com/jnb/jnbJan2010_files/jnbJan2010-LombokInstaller.png

JARファイルに含まれるProject Lombokのアノテーションを使うには、プロジェクト内のクラスパスにJARファイルを追加します。MavenでLombokを使う場合にはpom.xmlファイルに下記のような依存性を記述します。(※現在は http://mvnrepository.com/artifact/org.projectlombok/lombok で良いと思われる)

<dependencies>
    <dependency>
        <groupId>org.projectlombok</groupId>
        <artifactId>lombok</artifactId>
        <version>0.9.2</version>
    </dependency>
</dependencies>
<repositories>
    <repository>
        <id>projectlombok.org</id>
        <url>http://projectlombok.org/mavenrepo</url>
    </repository>
</repositories>

Lombokアノテーション

単純なデータクラスを定義するためにボイラープレートのコードを何百行もタイピングさせられるのは一般的なJavaプロジェクトでは稀に良くあることです。これらのクラスは一般的に、幾らかのフィールド、それらフィールド用のgetterとsetter、同様にequalsとhashCodeの実装、を含んでいます。最も単純なシナリオでは、Project Lombokはそれらのデータクラスをフィールド定義と@Dataアノテーションのコードだけにします。

もちろん、単純なシナリオでは開発者が日々直面するそれぞれの課題には不十分です。そういたった理由から、クラスの振る舞いと構造に対してよりきめの細かい制御を施すために、Project Lombokにはいくつかのアノテーションが用意されています。

@Getterと@Setter

@Getterと@Setterアノテーションは、フィールド一つ一つに対してgetterとsetterを生成します。booleanプロパティに対して生成されるgetterはJavaの一般的な命名規則に従うので、fooというbooleanフィールドに対してはgetFooではなくisFooという名前のgetterが生成されます。もし、アノテーションを付与したフィールドを持つクラスが、生成されるgetter/setterと同一のメソッドを持つ場合には注意が必要で、引数と戻り値型にかかわらずメソッドは生成されません。

@Getterと@Setterアノテーションは、生成されるメソッドのアクセス修飾子を指定するためのオプションを取ることが出来ます。

Lombokアノテーションでのサンプルコードは、

@Getter @Setter private boolean employed = true;
@Setter(AccessLevel.PROTECTED) private String name;

Javaでの同等のコードは、

private boolean employed = true;
private String name;

public boolean isEmployed() {
    return employed;
}

public void setEmployed(final boolean employed) {
    this.employed = employed;
}

protected void setName(final String name) {
    this.name = name;
}
@NonNull

@NonNullアノテーションは、そのフィールドにfast-fail*3のnullチェックが必要なことを指定するために使用します。このアノテーションが、Lombokが生成するsetterに対応するフィールドに付与されているとき、null値をNullPointerExceptionにするnullチェックのコードを生成します。付け加えると、もしLombokがクラスのコンストラクタを生成するとき、コンストラクタシグネチャにフィールドが追加されて、生成されたコンストラクタのコードにnullチェックも付与されます。

IntelliJ IDEAやFindBugsには@NotNullと@NonNullというアノテーションが存在し、こうしたバリエーションは他にも見られます。Lombokアノテーションはこのようなバリエーションに関しては関知しません*4。もしLombokが@NotNullや@NonNullという名前のアノテーションを付与されたメンバに遭遇したとき、適切なコードを生成することでそれらのアノテーションが有効だと見なせるようにします。また、Project Lombokの著者のコメントによると、Java言語にLombokアノテーションと同一名の型が追加された場合、Lombokはそれらを削除対象とする、とのことです。

FamilyクラスにLombokアノテーションを付与するとこんな感じです。

@Getter @Setter @NonNull
private List<Person> members;

Javaでの同等のコードはこんな感じです。

@NonNull
private List<Person> members;

public Family(@NonNull final List<Person> members) {
    if (members == null) throw new java.lang.NullPointerException("members");
    this.members = members;
}
    
@NonNull
public List<Person> getMembers() {
    return members;
}

public void setMembers(@NonNull final List<Person> members) {
    if (members == null) throw new java.lang.NullPointerException("members");
    this.members = members;
}
@ToString

このアノテーションはtoStringメソッドの実装を生成します。デフォルトでは、生成されるメソッドには非staticなフィールドがname-valueのペアとして含まれます。必要に応じて、生成されるコードにフィールド名を含めるかどうかをアノテーションパラメータincludeFieldNamesをfalseにすることで切り替えられます。

あるフィールドを生成の対象外にするには、excludeパラメーターにフィールド名を指定します。もしくは、ofパラメータに生成したいフィールドのみを指定することで同様の動作が可能です。親クラスのtoStringの出力も含めるには、callSuperパラメータをtrueに設定します。

Lombokのアノテーションを付与するとこんな感じです。

@ToString(callSuper=true,exclude="someExcludedField")
public class Foo extends Bar {
    private boolean someBoolean = true;
    private String someStringField;
    private float someExcludedField;
}

Javaでの同等のコードはこんな感じです。

public class Foo extends Bar {
    private boolean someBoolean = true;
    private String someStringField;
    private float someExcludedField;
    
    @java.lang.Override
    public java.lang.String toString() {
        return "Foo(super=" + super.toString() +
            ", someBoolean=" + someBoolean +
            ", someStringField=" + someStringField + ")";
    }
}
@EqualsAndHashCode

このクラスレベルアノテーションを付与するとLomnokはequalsとhashCodeメソッドを生成します。両者はhashCodeが従うべき規約により、互いに強い関係があります。デフォルトでは、非staticかtransientなクラスのフィールドが二つのメソッドに含まれます。@ToStringと同様に、excludeパラメータを、生成されるロジックに含めたくないフィールドを指定するために使用可能です。また、ofパラメータで生成対象としたいフィールドを限定させられます。

さらに@ToStringと同様に、@EqualsAndHashCodeにはcallSuperパラメータが用意されています。trueに設定することで、クラスの生成対象フィールドの等価性チェックの前に、親クラスのequalsを呼び出す等価性チェックを実行します。hashCodeメソッドでは、ハッシュの計算結果には親クラスのhashCodeの結果が組み込まれます。callSuperをtrueにするときの注意点として、親クラスのプロパティでequalsメソッドインスタンス型チェックをしていることを確認してください。親クラスのequalsのロジックが、クラスの型チェックとただ単に二つのオブジェクトのクラスが同一なことの場合、子クラスのequalsの結果は望まぬ結果となります。親クラスがLombokの生成したequalsメソッドを持っている場合は問題になりませんが、他の実装がこの状況を正しく処理できない可能性はあります。また、子クラスがObjectのみ拡張している場合もcallSuperをtrueに出来ないことにも注意が必要で*5、その理由はインスタンスの等価性チェックがフィールド比較を短絡評価するためです。生成したメソッドがObjectのequals実装を呼び出したとすると、二つのインスタンスを比較するとき同一インスタンスなときだけtrueを返すことになってしまいます*6。そのため、Lombokはこのケースではコンパイルエラーになります。

Lombokのアノテーションを付与するとこんな感じです。

@EqualsAndHashCode(callSuper=true,exclude={"address","city","state","zip"})
public class Person extends SentientBeing {
    enum Gender { Male, Female }

    @NonNull private String name;
    @NonNull private Gender gender;
    
    private String ssn;
    private String address;
    private String city;
    private String state;
    private String zip;
}

Javaでの同等のコードはこんな感じです。

public class Person extends SentientBeing {
    
    enum Gender {
        /*public static final*/ Male /* = new Gender() */,
        /*public static final*/ Female /* = new Gender() */;
    }
    @NonNull
    private String name;
    @NonNull
    private Gender gender;
    private String ssn;
    private String address;
    private String city;
    private String state;
    private String zip;
    
    @java.lang.Override
    public boolean equals(final java.lang.Object o) {
        if (o == this) return true;
        if (o == null) return false;
        if (o.getClass() != this.getClass()) return false;
        if (!super.equals(o)) return false;
        final Person other = (Person)o;
        if (this.name == null ? other.name != null : !this.name.equals(other.name)) return false;
        if (this.gender == null ? other.gender != null : !this.gender.equals(other.gender)) return false;
        if (this.ssn == null ? other.ssn != null : !this.ssn.equals(other.ssn)) return false;
        return true;
    }
    
    @java.lang.Override
    public int hashCode() {
        final int PRIME = 31;
        int result = 1;
        result = result * PRIME + super.hashCode();
        result = result * PRIME + (this.name == null ? 0 : this.name.hashCode());
        result = result * PRIME + (this.gender == null ? 0 : this.gender.hashCode());
        result = result * PRIME + (this.ssn == null ? 0 : this.ssn.hashCode());
        return result;
    }
}



○○○○○訳注ここから○○○○○
ビミョーに分かりにくいのが「親クラスのequalsのロジックが、クラスの型チェックとただ単に二つのオブジェクトのクラスが同一なことの場合、子クラスのequalsの結果は望まぬ結果となります」なので、サンプルのコードを書く。

親クラスがこんなんだとして。

public class Parent {
    @Override
    public boolean equals(Object obj) {
        if (obj.getClass() != this.getClass()) return false;//こっちが「クラスの型チェック」
        if (obj == this) return true;//こっちが「ただ単に二つのオブジェクトのクラスが同一なこと」
        return false;
    }
    
}

子クラスを下記のように作る。mainを実行すると、s1とs2のequalsはfalseになる。もちろん、callSuper=trueを外せばtruenになる。

@EqualsAndHashCode(callSuper=true)
public class Sub extends Parent {
    private String name = "hoge";
    private Integer age = 10;
    
    public Sub(String name, Integer age) {
        super();
        this.name = name;
        this.age = age;
    }
    
    public static void main(String[] args) {
        Sub s1 = new Sub("AAAA", 10);
        Sub s2 = new Sub("AAAA", 10);
        
        System.out.println(s1.equals(s2));//falseになる!
    }
}

これは、Lombokが生成するequalsが短絡評価するため。super.equalsがfalseを返したら、そこから後続のフィールドの等価性チェックはしない、という意味合い。そして、親クラスのequalsがインスタンスの参照が同一かどうかのチェックしかしていないと、equalsはそのロジックだけに支配されてしまう。

Objectだけを拡張してる子クラスでcallSuper=trueが出来ないのも同様な理由による。Object#equalsの実装はreturn (this == obj);だけなので、もしこの状況下でcallSuper=trueが出来るとすると、サブクラスでのフィールドを用いた同値チェックは何の意味も持たなくなってしまう。

○○○○○訳注ここまで○○○○○



@Data

@Dataは、Project Lombokのツールセットにおいて、おそらく最も良く使われるアノテーションです。このアノテーションは、@ToString、@EqualsAndHashCode、@Getter、@Setterを一緒くたにした機能を持っています。基本的に、@Dataをクラスに付与することと、デフォルト状態の@ToStringに@EqualsAndHashCodeおよびフィールドそれぞれに@Getterと@Setterアノテーションを付与すること、は同じになります。また、クラスに@Dataアノテーションを付与すると、Lombokはコンストラクタを生成します。publicなコンストラクタを生成し、引数は@NonNullもしくはfinalなフィールド*7を取ります。これらはPlain Old Java Object (POJO)を作るために必要です。

たいていは@Dataは便利ですが、他のLombokアノテーションと同程度に細かい制御が利くわけではありません。デフォルトのメソッド生成の振る舞いをオーバーライドするために、クラスにアノテーションを追加したり、他のLombokアノテーションをフィールドやメソッドに追加したり、お好みに応じて必要なパラメータを設定することも出来ます。

@Dataはオプションで一つのパラメータを持ち、staticファクトリーメソッドを生成させることが出来ます。staticConstructorパラメータに任意のメソッド名を設定することで、コンストラクタはprivateになり、設定したメソッド名でstaticファクトリーメソッドを生成します。

Lombokのアノテーションを付与するとこんな感じです。

@Data(staticConstructor="of")
public class Company {
    private final Person founder;
    private String name;
    private List<Person> employees;
}

Javaでの同等のコードはこんな感じです。

public class Company {
    private final Person founder;
    private String name;
    private List<Person> employees;
    
    private Company(final Person founder) {
        this.founder = founder;
    }
    
    public static Company of(final Person founder) {
        return new Company(founder);
    }
    
    public Person getFounder() {
        return founder;
    }
    
    public String getName() {
        return name;
    }
    
    public void setName(final String name) {
        this.name = name;
    }
    
    public List<Person> getEmployees() {
        return employees;
    }
    
    public void setEmployees(final List<Person> employees) {
        this.employees = employees;
    }
    
    @java.lang.Override
    public boolean equals(final java.lang.Object o) {
        if (o == this) return true;
        if (o == null) return false;
        if (o.getClass() != this.getClass()) return false;
        final Company other = (Company)o;
        if (this.founder == null ? other.founder != null : !this.founder.equals(other.founder)) return false;
        if (this.name == null ? other.name != null : !this.name.equals(other.name)) return false;
        if (this.employees == null ? other.employees != null : !this.employees.equals(other.employees)) return false;
        return true;
    }
    
    @java.lang.Override
    public int hashCode() {
        final int PRIME = 31;
        int result = 1;
        result = result * PRIME + (this.founder == null ? 0 : this.founder.hashCode());
        result = result * PRIME + (this.name == null ? 0 : this.name.hashCode());
        result = result * PRIME + (this.employees == null ? 0 : this.employees.hashCode());
        return result;
    }
    
    @java.lang.Override
    public java.lang.String toString() {
        return "Company(founder=" + founder + ", name=" + name + ", employees=" + employees + ")";
    }
}
@Cleanup

@Cleanupアノテーションは、取得したリソースの解放を保証するために使います。ローカル変数に@Cleanupが付与されたとき、現在のスコープの最後でクリーンナップ・メソッドが呼ばれることを保証するtry/finallyブロックで後続のコードを囲みます。デフォルトでは、@Cleanupはクリーンナップ・メソッドの名前が"close"との仮定で動作します―Javaのinput/outputストリームAPIがそうであるように。しかしながら、異なるメソッド名をアノテーションのvalueパラメータに設定できます。引数を一つも取らないクリーンナップ・メソッドだけがこのアノテーションで利用可能です。

@Cleanupアノテーションを使うときの考慮点がまだあります。クリーンナップ・メソッドから例外がスローされた場合、メソッド本体でスローされた例外に取って代わってしまいます。このことは問題が起きたときの本来の原因を埋もれさせてしまうので、Project Lombokのリソース管理を選択できるように考慮すべきでしょう。その上、Java 7の自動リソース管理により、このちょっとしたアノテーションは比較的短命になるでしょう*8

Lombokのアノテーションを付与するとこんな感じです。

public void testCleanUp() {
    try {
        @Cleanup ByteArrayOutputStream baos = new ByteArrayOutputStream();
        baos.write(new byte[] {'Y','e','s'});
        System.out.println(baos.toString());
    } catch (IOException e) {
        e.printStackTrace();
    }
}

Javaでの同等のコードはこんな感じです。

public void testCleanUp() {
    try {
        ByteArrayOutputStream baos = new ByteArrayOutputStream();
        try {
            baos.write(new byte[]{'Y', 'e', 's'});
            System.out.println(baos.toString());
        } finally {
            baos.close();
        }
    } catch (IOException e) {
        e.printStackTrace();
    }
}
@Synchronized

メソッドにsynchronizedキーワードを用いると不幸な出来事に見舞われる、とマルチスレッドで動作するソフトウェア開発に取り組む開発者は証言するでしょう。synchronizedキーワードは、インスタンスメソッドでのthisオブジェクトもしくはstaticメソッドでのclassオブジェクトでロックを取得します。これが意味するのは、開発者が知り得るコードの外側で同一オブジェクトをロックしようとする可能性があり、その結果はデッドロックになります。一般的なアドバイスとして、別のオブジェクトで明示的なロックをするようにし、そのオブジェクトはロックのためだけに使用し、また、異なるロックで使用されないようにします。Project Lombokはこの目的のために@Synchronizedアノテーションを用意しています。

@Synchronizedアノテーションインスタンスメソッドに付与されると、Lombokはprivateなロック変数($lock)を生成し、メソッド実行に先立ってロックをかけます。同様に、staticメソッドアノテーションを付与すると、staticメソッドではprivateなstaticオブジェクト($LOCK)を生成します。ロックオブジェクトに別のフィールド変数を使うためには、アノテーションのvalueパラメータにフィールド名を指定します。valueパラメータにフィールド名が指定されている時、Lombokはそのプロパティを生成しないので開発者は自前で定義する必要があります。

Lombokのアノテーションを付与するとこんな感じです。

private DateFormat format = new SimpleDateFormat("MM-dd-YYYY");

@Synchronized
public String synchronizedFormat(Date date) {
    return format.format(date);
}

Javaでの同等のコードはこんな感じです。

private final java.lang.Object $lock = new java.lang.Object[0];
private DateFormat format = new SimpleDateFormat("MM-dd-YYYY");

public String synchronizedFormat(Date date) {
    synchronized ($lock) {
        return format.format(date);
    }
}
@SneakyThrows*9

@SneakyThrowsは、おそらくProject Lombokの中では疑問を持つ人が多いと思われますが、その理由はチェック例外に対し真っ向から挑戦しているためです。チェック例外の使用方法には様々な意見・論争があり、大多数の開発者は失敗経験をそのままにしています。そうした開発者は@SneakyThrowsを歓迎するでしょう。チェック/非チェック例外という壁向こうに居る開発者は、隠された潜在的な問題にしょっちゅう出くわすことになります*10

IllegalAccessExceptionをスローするコードを書くと、もしそのメソッドか親クラスのthrows節*11に記述が無い場合、"Unhandled exception"コンパイルエラーになります。もしIllegalAccessExceptionかその親クラスがthrows節に記述されていない場合、IllegalAccessExceptionのスローは通常"Unhandled exception"エラーとなります。*12

http://jnb.ociweb.com/jnb/jnbJan2010_files/jnbJan2010-UnhandledException.png

@SneakyThrowsアノテーションを付与すると、コンパイルエラーはどっかに消えます。

デフォルトでは、@SneakyThrowsはthrows節での宣言をすること無しにチェック例外をスロー可能にします。許容する例外の組み合わせを制限することができ、その場合はアノテーションのvalueパラメータにClassの配列を設定します。

Lombokのアノテーションを付与するとこんな感じです。

@SneakyThrows
public void testSneakyThrows() {
    throw new IllegalAccessException();
}

Javaでの同等のコードはこんな感じです。

public void testSneakyThrows() {
    try {
        throw new IllegalAccessException();
    } catch (java.lang.Throwable $ex) {
        throw lombok.Lombok.sneakyThrow($ex);
    }
}

上記のコード例にあるとおり、Lombok.sneakyThrow(Throwable)がRuntimeExceptionでラップした例外を再スローするように仕向けます*13*14。sneakyThrowメソッドは決してリターンせず、代わりに与えられた例外を変更せずにスローします。

コストとメリット

採用する技術を選ぶ時と同様、Project Lombokにもポジティブ・ネガティブ両方の効果があります。プロジェクトにおいてLombokアノテーションを組み込みと、多くのIDEによる生成や手作りなボイラープレートを削減できます。コード削減の結果として、メンテナンスコストを下げ、バグを少なくし、クラスのコードを読みやすくします

ただし、プロジェクトにProject Lombokアノテーションを導入することでシステムの規模が小さくなる*15と言っているわけではありません。Project Lombokは主としてJava言語のギャップを埋めることを目的としています。言語に対する変更によってLombokのアノテーションを使用しなくてもよくなる可能性があり、例えばfirst class property*16のサポートが上げられます。加えて、アノテーションベースのO/Rマッパーと組み合わせて使うとき、データクラスに多数のアノテーションを付与しまくると手が付けられなくなりがちです。ただ、Lombokアノテーションによって抑制されるコードの量がその状況を相殺してはいます。しかし、アノテーションの多用を避ける勢力は見なかったフリをして別の方法を探すでしょう。

何が生成されているのか?*17

Project Lombokは、Lombokアノテーションを同等なソースコードで置換するdelombokユーティリティーを用意しています。このユーティリティーはコマンドラインを通してソースコードのディレクトリに対して実行できます。

java -jar lombok.jar delombok src -d src-delomboked

もしくは、Antタスクでビルドに組み込めます。

<target name="delombok">
    <taskdef classname="lombok.delombok.ant.DelombokTask"
        classpath="WebRoot/WEB-INF/lib/lombok.jar" name="delombok" />
    <mkdir dir="src-delomboked" />
    <delombok verbose="true" encoding="UTF-8" to="src-delomboked"
        from="src" />
</target>

delombokとAntタスクは、lomcbok.jarのコアパッケージに入れられています。LombokアノテーションGWTやその他の互換性の無いフレームワークでアプリケーションを作成するときに役に立つのに加えて、Lombokアノテーションで書かれたクラスと、それと同等なボイラープレートをインラインで持つコードとの比較は、Personクラスに対しdelombokすれば簡単に分かります。

package com.ociweb.jnb.lombok;

import java.util.Date;

import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.NonNull;

@Data
@EqualsAndHashCode(exclude={"address","city","state","zip"})
public class Person {
    enum Gender { Male, Female }

    @NonNull private String firstName;
    @NonNull private String lastName;
    @NonNull private final Gender gender;
    @NonNull private final Date dateOfBirth;
    
    private String ssn;
    private String address;
    private String city;
    private String state;
    private String zip;
}

Project Lombokのアノテーションで書かれたコードは、ボイラープレートで同等な記述をしたコードよりもはるかに簡潔です。

package com.ociweb.jnb.lombok;

import java.util.Date;
import lombok.NonNull;

public class Person {
    
    enum Gender {
        /*public static final*/ Male /* = new Gender() */,
        /*public static final*/ Female /* = new Gender() */;
    }
    @NonNull
    private String firstName;
    @NonNull
    private String lastName;
    @NonNull
    private final Gender gender;
    @NonNull
    private final Date dateOfBirth;
    private String ssn;
    private String address;
    private String city;
    private String state;
    private String zip;
    
    public Person(@NonNull final String firstName, @NonNull final String lastName,
            @NonNull final Gender gender, @NonNull final Date dateOfBirth) {
        if (firstName == null)
            throw new java.lang.NullPointerException("firstName");
        if (lastName == null)
            throw new java.lang.NullPointerException("lastName");
        if (gender == null)
            throw new java.lang.NullPointerException("gender");
        if (dateOfBirth == null)
            throw new java.lang.NullPointerException("dateOfBirth");
        this.firstName = firstName;
        this.lastName = lastName;
        this.gender = gender;
        this.dateOfBirth = dateOfBirth;
    }
    
    @NonNull
    public String getFirstName() {
        return firstName;
    }
    
    public void setFirstName(@NonNull final String firstName) {
        if (firstName == null)
            throw new java.lang.NullPointerException("firstName");
        this.firstName = firstName;
    }
    
    @NonNull
    public String getLastName() {
        return lastName;
    }
    
    public void setLastName(@NonNull final String lastName) {
        if (lastName == null)
            throw new java.lang.NullPointerException("lastName");
        this.lastName = lastName;
    }
    
    @NonNull
    public Gender getGender() {
        return gender;
    }
    
    @NonNull
    public Date getDateOfBirth() {
        return dateOfBirth;
    }
    
    public String getSsn() {
        return ssn;
    }
    
    public void setSsn(final String ssn) {
        this.ssn = ssn;
    }
    
    public String getAddress() {
        return address;
    }
    
    public void setAddress(final String address) {
        this.address = address;
    }
    
    public String getCity() {
        return city;
    }
    
    public void setCity(final String city) {
        this.city = city;
    }
    
    public String getState() {
        return state;
    }
    
    public void setState(final String state) {
        this.state = state;
    }
    
    public String getZip() {
        return zip;
    }
    
    public void setZip(final String zip) {
        this.zip = zip;
    }
    
    @java.lang.Override
    public java.lang.String toString() {
        return "Person(firstName=" + firstName + ", lastName=" + lastName + 
            ", gender=" + gender + ", dateOfBirth=" + dateOfBirth +
            ", ssn=" + ssn + ", address=" + address + ", city=" + city +
            ", state=" + state + ", zip=" + zip + ")";
    }
    
    @java.lang.Override
    public boolean equals(final java.lang.Object o) {
        if (o == this) return true;
        if (o == null) return false;
        if (o.getClass() != this.getClass()) return false;
        final Person other = (Person)o;
        if (this.firstName == null
                ? other.firstName != null
                : !this.firstName.equals(other.firstName))
            return false;
        if (this.lastName == null
                ? other.lastName != null
                : !this.lastName.equals(other.lastName))
            return false;
        if (this.gender == null
                ? other.gender != null
                : !this.gender.equals(other.gender))
            return false;
        if (this.dateOfBirth == null
                ? other.dateOfBirth != null
                : !this.dateOfBirth.equals(other.dateOfBirth))
            return false;
        if (this.ssn == null
                ? other.ssn != null
                : !this.ssn.equals(other.ssn))
            return false;
        return true;
    }
    
    @java.lang.Override
    public int hashCode() {
        final int PRIME = 31;
        int result = 1;
        result = result * PRIME +
            (this.firstName == null ? 0 : this.firstName.hashCode());
        result = result * PRIME +
            (this.lastName == null ? 0 : this.lastName.hashCode());
        result = result * PRIME +
            (this.gender == null ? 0 : this.gender.hashCode());
        result = result * PRIME +
            (this.dateOfBirth == null ? 0 : this.dateOfBirth.hashCode());
        result = result * PRIME +
            (this.ssn == null ? 0 : this.ssn.hashCode());
        return result;
    }
}

コードは書くだけでなくメンテナンスのために開発者が読むという点にも気を配るべきです。この意味するところは、Project Lombokのアノテーションで書かれているとき、もし問題となっているクラスが単純なデータクラスかもっと邪悪な何かなとき、開発者は原因究明のために大量のコードをスクロールしてまわる必要が無くなります。

制限事項

時にProject Lombokは開発者のコーディングライフを劇的に改善しますが、制限事項が存在します。issues listを閲覧すると、現時点の欠点についてすぐに理解できます―ほとんどが軽微なものですが。重要な問題の一つは、スーパークラスコンストラクタを検出できないことです。もしスーパークラスにデフォルトコンストラクタが無い場合、スーパークラスコンストラクタを呼び出すコンストラクタを明示的に定義しなければ@Dataアノテーションは利用できません。Project Lombokは生成されるメソッド名と同じメソッドが既にある場合にはそちらを優先するため、たいていの欠点はこのアプローチによって切り抜けられます。

議論

Project Lombokの使用法に対して多くのイシューが提起されています。最も良くある議論は、アノテーションはメタ情報を記述するものであり、コードベースがコンパイル出来ないような使い方をしてはいけない、というものです*18。これは確かにLombokアノテーションを使用した際に生じる状況を示しています。Lombokのアノテーションは、フレームワークだけでなくアプリケーションの他の部分でも使用できるように作られています。Project Lombokは開発ライフサイクルの基本的な部分に位置づけられるものであり、その帰結として、少なくともサポートするIDEは限定されません。

前述したように、@SneakyThrowsはチェック例外と非チェック例外に関する昔ながらの議論をかき回すことでしょう。このディベートにおける意見はしばしば凶暴で宗教的になります。@SneakyThrowsの使用に対する議論もまた人々をエキサイトさせるでしょう。

論争のもう一つ重要な点は、javacのアノテーション処理とIDE統合によるコード支援のサポート両方の実装についてです。これらのProject Lombokの魔術的機能は非publicなAPIを活用して作られています。これはProject Lombokが将来のIDEJDKのリリースにより使えなくなるリスクを意味しています。プロジェクト創設者の一人Reinier Zwitserloot氏この状況を下記のように表現しています。

*19

これはハックとしか言いようがありません。非publicなAPIの使用。強制キャスト
(javacでのアノテーション・プロセッサ実行時には、ライブなASTを取得するため
に使われる拡張メソッドをたまたま持っているAnnotationProcessorインタフェース
の内部的な実装であるJavacAnnotationProcessorのインスタンスを取得すると
知っている、からキャストできる)。

Eclipseでは、間違いなく最悪です*20
java agentは、非publicなAPIと使用禁止領域であるEclipseの文法とパーサー
クラスにコードを注入するために使用されます。

もしあなたが標準APIでlombokがやることを実装出来たなら、私はそうしたでしょう
が、そうしなくても良いです。これが正しいというつもりはありませんが、
Java 1.6 + Eclipse v3.5用のEclipseプラグインを作成し、何も変更することなく
Java 1.5 + Eclipse v3.4で同様に動作したので、すべてが脆いというわけでは
ありません。

要約

Project Lombokは、プログラマにとってパワフルなツールで、Javaクラスから大量のボイラープレートを除去するアノテーションを提供します。最良のケースでは、わずか5文字が何百行のコードを置換します。その結果、Javaクラスはクリーンになり、簡潔でメンテナンスしやすくなります。これらのメリットにはコストがかかりません。IntelliJ IDEAでProject Lombokを使用することはまだオプションです。リスクとしては、IDEJDKのアップグレードによる動作不良とプロジェクトの実装とゴールに関する議論が挙げられます。どんな技術を採用するにせよ考慮が必要である、と言い換えられます。得るものと失うものの二者択一が常に存在します。つまり、Project Lombokを自分のプロジェクトに採用するとコストよりも得られるものが大きいかどうか、という質問です。少なくとも、Project Lombokは、これまで実を結ばなかった言語機能の議論に新しい息吹を吹き込み、そうした事態こそが勝利と言えるでしょう。

*1:kagamihogeが書いたこの訳文ではなく、原文の方を指す

*2:IntelliJ IDEA source code has placed IDEA supportだが上手く訳せない。IDEAサポートを受けられればLombokのIDEA対応もするよ、って感じなのか?

*3:日本語的にはフェイルファーストになるだろうか?

*4:原文はLombok is annotationとなっているが、Lombok annotation isと思われる

*5:lombok-1.12.4.jarではコンパイルエラー Generating equals/hashCode with a supercall to java.lang.Object is pointless. となる

*6:Object#equalsの実装がreturn (this == obj);だから

*7:つまりprivate int id;みたいなのはコンストラクタの引数にならない

*8:言うまでも無くtry-with-finallyが導入されたため

*9:Sneakyの意味は、コッソリとか内緒でとかなので、SneakyThrowsは、神隠しスローとかニンジャスローとでも言うべきか?

*10:ビミョーな日本語だが、まぁタブンJavaの例外機構をdisってるんだと思う

*11:原文はif IllegalAccessException, or some parent class is not listed...で、ちと意味が分からず上手く訳せなかった

*12:https://twitter.com/kis/status/545499043518885890 を参考に修正しました。

*13:「仕向けます」はlead most to believe辺りと対応するのだが、イマイチどう訳せばいいか分からなかった。

*14:原文ではこの後にhowever this is not the caseと続くのだけど、何がどうnot the caseか分からなかったので省略した

*15:downsidesって書いてあるので、ダウンサイジングでもいいかなぁと思ったけど、それだと概念が広すぎるんで、こういう訳にした

*16:今のところJava言語にこういう仕組みが無いんでなんて訳せばいいのか分からんけど、要はC#の自動プロパティみたいなのを想像すれば良いと思われる

*17:原文はWhat is missing?で「消えたコードは何か?」の方が良いかもしれない。ただ、この節はdelombokで実際に「生成」されるコードを出力する方法が書いてあるので、こーいう訳の方が日本語的には自然かと思ってこうした

*18:原文では文末にwere they removedとあるのだが、何に掛かっているのかよーわからんので訳文に含められなかった

*19:下記は、文脈的にも他者の発言を引用していると思われるので、該当部分は引用記法で囲むこととした

*20:原文ではこの後ろに(and yet more robust)とあるのだが、何がどうrobustか分からないので訳さなかった。これに続く節では、非公開API使ってバージョン変わっても動くときは動くんだからいいじゃん?的なことを書いているので「ソコソコ堅牢」という意味合いなのだろうか?

*21:下記はすべて原文からのコピペ

*22:原文ではココに謝辞が入るが省略した