読者です 読者をやめる 読者になる 読者になる

Lombok Experimental features

LombokのExperimental featuresを試したメモです。

目次

Lombokは今や色んなところで使われているすごく便利なライブラリですが、
LombokのExperimental featuresも便利で少し使っていました。
使ったことない機能もあったので、試してみたメモです。

Lombok Experimental features

Lombokといえば、@Data、@Getter/@Setter、@NoArgsConstructor/@AllArgsConstructorなどコア機能はよく使われますが、
LombokにはExperimental featuresという機能もあります。
名前の通り、実験的な機能です。
コア機能よりまだ不安定だったり、致命的なバグがあったりする可能性があります。

Lombok feature overview

IntelliJ Lombok Pluginでサポートされていない下記機能は試してないです。
 ・@ExtensionMethod
 ・@Delegate
 ・@Helper


maven
maven dependencyは普通にLombok使うのと同じです。

<dependency>
    <groupId>org.projectlombok</groupId>
    <artifactId>lombok</artifactId>
    <version>1.16.10</version>
    <scope>provided</scope>
</dependency>

@Accessors

@Accessorsはgetterとsetterの生成と呼び出しを変更します。

chain

chain = trueの場合、setterはvoidの代わりにthisを返すようになります。
そのため、メソッドチェーンのように書けるようになります。

@Accessors(chain = true)
public static class AccessorsChain {
    @Setter
    String bar;

   void printBar() {
        System.out.println("|" + this.bar + "|");
   }
}

テスト。

@Test
public void AccessorsChainTest() {
    AccessorsChain chain = new AccessorsChain();
    chain.setBar("AAA").printBar();

    assertThat(chain.setBar("AAA").getClass()).isEqualTo(AccessorsChain.class);
}
fluent

@Getterと@Setterを使うと、getterとsetterはそれぞれgetFoo()、setFoo(T newValue)となりますが、
fluent = trueの場合、getterとsetterはそれぞれfoo()、foo(T newValue)となります。
chainを指定しない場合は、同時にchain = trueとなります。
これで、スクリプト言語のようなメソッドチェーンができるようになります。

@Accessors(fluent = true)
public static class AccessorsFluent {
    @Getter
    @Setter
    private String foo = "abc";
}

テスト。

@Test
public void accessorsFluentTest() {
    AccessorsFluent acc = new AccessorsFluent();

    // getter
    acc.foo();
    assertThat(acc.foo()).isEqualTo("abc");

    // setter
    acc.foo("ccc");
    assertThat(acc.foo()).isEqualTo("ccc");
}
prefix

prefix = "xxx"を指定すると、プレフィックスを取り除いた名前でフィールドにアクセスできます。

public static class AccessorsPrefix {
    @Getter
    @Setter
    @Accessors(prefix = "pre")
    String preZoo;
}

テスト。
setPreZoo()、getPreZoo()ではなく、setZoo()、getZoo()でアクセスできます。

@Test
public void AccessorsPrefixTest() {
    AccessorsPrefix p = new AccessorsPrefix();

    // getter
    p.setZoo("AAA");

    // setter
    p.getZoo();

    assertThat(p.getZoo()).isEqualTo("AAA");
}

@FieldDefaults

@FieldDefaultsはクラスやフィールドにアクセス修飾子を付与したり、finalを付与することができます。

level

level = AccessLevel.xxxxでアクセス修飾子を付与できます。
アクセス修飾子はlombok.AccessLevelのEnumを指定します。
PACKAGE、PRIVATE、PROTECTED、PUBLIC、MODULE、NONEを指定できます。

@Getter
@FieldDefaults(level = AccessLevel.PRIVATE)
public static class FieldLevelPrivate {
    String text = "ABC";
}

@FieldDefaults(level = AccessLevel.PUBLIC)
public static class FieldLevelPublic {
    int num = 100;
}

テスト。
privateのフィールドは直接アクセスできず、publicのフィールドは直接アクセスできています。

@Test
public void fieldPrivateTest() {
    FieldLevelPrivate pri = new FieldLevelPrivate();

    // error!!
    // pri.text;
    assertThat(pri.getText()).isEqualTo("ABC");
}

@Test
public void fieldPublicTest() {
    FieldLevelPublic pub = new FieldLevelPublic();

    assertThat(pub.num).isEqualTo(100);

    pub.num = 200;
    assertThat(pub.num).isEqualTo(200);
}
makeFinal

makeFinal = trueを指定すると、フィールドをfinalにできます。
finalにしないフィールドがある場合、@NonFinalを指定します。

@FieldDefaults(makeFinal = true, level = AccessLevel.PUBLIC)
public static class FieldFinal {
    int count = 200;

    @NonFinal
    String memo = "memorandum";
}

テスト。
finalなフィールドには再代入出来ていないことがわかります。
@NonFinalをつけたフィールドには再代入出来ています。

@Test
public void FieldFinalTest() {
    FieldFinal ff = new FieldFinal();

    // final
    // ff.num = 200; // error!!
    assertThat(ff.count).isEqualTo(200);
    
    // non final
    ff.memo = "new memo";
    assertThat(ff.memo).isEqualTo("new memo");
}

@Wither

@Witherはfinalなフィールドを持つオブジェクトにおいて、新しいフィールド値を持つクローンを作成するメソッドを提供します。
fooフィールドに対して、fooに新しい値を設定したオブジェクトを返すwithFoo(newValue)メソッドが生成されます。

@Getter
public class WitherExample {
    @Wither
    private final String name;
    @Wither
    private final int age;

    public WitherExample(String name, int age) {
        if (name == null) throw new NullPointerException();
        this.name = name;
        this.age = age;
    }
}

テスト。
withName("BBB")でnameにBBBを設定したクローンを生成します。
withAge(1_000)でageに1_000を設定したクローンを生成します。

@Test
public void WitherTest() {
    WitherExample origin = new WitherExample("abc", 123);

    WitherExample newNameByWith = origin.withName("BBB");

    assertThat(origin).isNotEqualTo(newNameByWith);
    assertThat(newNameByWith.getAge()).isEqualTo(123);
    assertThat(newNameByWith.getName()).isEqualTo("BBB");

    WitherExample newAgeByWith = origin.withAge(1_000);

    assertThat(origin).isNotEqualTo(newAgeByWith);
    assertThat(newAgeByWith.getAge()).isEqualTo(1_000);
    assertThat(newAgeByWith.getName()).isEqualTo("abc");
}

@XXX(onMethod= / onConstructor= / onParam=)

onMethod、onConstructor、onParamは、Lombokが生成したコンストラクタ、メソッドにアノテーションを付与することができます。

onMethod

@Setter、@Getter、@Witherで生成されたメソッドにアノテーションを付与する場合、onMethodを使用します。

onConstructor

@AllArgsConstructor、@NoArgsConstructor、@RequiredArgsConstructorで生成されたコンストラクタに
アノテーションを付与する場合、onConstructorを使用します。

onParam

@Setter、@Witherで生成されたメソッドのパラメータにアノテーションを付与する場合、onParamを使用します。

付与したいアノテーションを指定する方法は、
onXxxx = @__({@Annotation1, @Annotation2})
という形で指定します。

@AllArgsConstructor(onConstructor = @__(@OnXExample.ConstructorAnnotation))
public class OnXExample {
    @Setter(onParam = @__({@Min(10), @Max(20)}))
    public String name;

    @Getter(onMethod = @__({@MethodAnnotation}))
    private int num;

    @NotNull
    @Target(METHOD)
    @Retention(RUNTIME)
    @interface MethodAnnotation {
    }

    @Target(CONSTRUCTOR)
    @Retention(RUNTIME)
    @interface ConstructorAnnotation {
    }
}

テスト。
指定したアノテーションが付与されているかをリフレクションを使って確認。

@Test
public void OnXTest() throws NoSuchMethodException {

    // constructor
    Constructor con = OnXExample.class.getConstructor(String.class, int.class);
    Annotation anoCons = con.getAnnotation(OnXExample.ConstructorAnnotation.class);
    assertThat(anoCons).isInstanceOf(OnXExample.ConstructorAnnotation.class);

    // setter
    Method name = OnXExample.class.getMethod("setName", String.class);
    Parameter[] paraSetter = name.getParameters();
    Annotation anoSetter = paraSetter[0].getAnnotation(Min.class);
    assertThat(anoSetter).isInstanceOf(Min.class);

    // getter
    Method num = OnXExample.class.getMethod("getNum");
    Annotation anoGetter = num.getAnnotation(OnXExample.MethodAnnotation.class);
    assertThat(anoGetter).isInstanceOf(OnXExample.MethodAnnotation.class);
}

@UtilityClass

@UtilityClassはインスタンス化不可のユーティリティクラスを生成します。
@UtilityClassを付与すると、そのクラスはfinalになります。同時にすべてのメンバがstaticになります。

@UtilityClass
public class UtilityExample {
    int MAGIC_NUMBER = 10;

    public int doubleNum(int num) {
        return num + num;
    }
}

テスト。
インスタンス化不可。staticメンバーが呼び出せる。

@Test
public void UtilityTest() {
    // error
    // UtilityExample util = new UtilityExample();

    int magicNum = UtilityExample.MAGIC_NUMBER;
    assertThat(magicNum).isEqualTo(10);

    int doubleNum = UtilityExample.doubleNum(200);
    assertThat(doubleNum).isEqualTo(400);
}

テストコード

今回のテストコードの全体です。

AccessorsExample.java

package lombok;

import lombok.experimental.Accessors;

public class AccessorsExample {
    @Accessors(chain = true)
    public static class AccessorsChain {
        @Setter
        String bar;

        void printBar() {
            System.out.println("|" + this.bar + "|");
        }
    }

    @Accessors(fluent = true)
    public static class AccessorsFluent {
        @Getter
        @Setter
        private String foo = "abc";
    }

    public static class AccessorsPrefix {
        @Getter
        @Setter
        @Accessors(prefix = "pre")
        String preZoo;
    }

}

FieldDefaultsExample.java

package lombok;

import lombok.experimental.FieldDefaults;
import lombok.experimental.NonFinal;
import lombok.experimental.Wither;

class FieldDefaultsExample {

    @Getter
    @FieldDefaults(level = AccessLevel.PRIVATE)
    public static class FieldLevelPrivate {
        String text = "ABC";
    }

    @FieldDefaults(level = AccessLevel.PUBLIC)
    public static class FieldLevelPublic {
        int num = 100;
    }

    @FieldDefaults(makeFinal = true, level = AccessLevel.PUBLIC)
    public static class FieldFinal {
        int count = 200;

        @NonFinal
        String memo = "memorandum";
    }
}

WitherExample.java

package lombok;

import lombok.experimental.Wither;

@Getter
public class WitherExample {
    @Wither
    private final String name;
    @Wither
    private final int age;

    public WitherExample(String name, int age) {
        if (name == null) throw new NullPointerException();
        this.name = name;
        this.age = age;
    }
}

OnXExample.java

package lombok;

import javax.validation.constraints.Max;
import javax.validation.constraints.Min;
import javax.validation.constraints.NotNull;

import java.lang.annotation.Retention;
import java.lang.annotation.Target;

import static java.lang.annotation.ElementType.CONSTRUCTOR;
import static java.lang.annotation.ElementType.METHOD;
import static java.lang.annotation.RetentionPolicy.RUNTIME;

@AllArgsConstructor(onConstructor = @__(@OnXExample.ConstructorAnnotation))
public class OnXExample {
    @Setter(onParam = @__({@Min(10), @Max(20)}))
    public String name;

    @Getter(onMethod = @__({@MethodAnnotation}))
    private int num;

    @NotNull
    @Target(METHOD)
    @Retention(RUNTIME)
    @interface MethodAnnotation {
    }

    @Target(CONSTRUCTOR)
    @Retention(RUNTIME)
    @interface ConstructorAnnotation {
    }
}

UtilityExample.java

package lombok;

import lombok.experimental.UtilityClass;

@UtilityClass
public class UtilityExample {
    int MAGIC_NUMBER = 10;

    public int doubleNum(int num) {
        return num + num;
    }
}

LombokExampleTest.java

package lombok;

import org.junit.Test;
import org.junit.experimental.runners.Enclosed;
import org.junit.runner.RunWith;

import javax.validation.constraints.Min;
import java.lang.annotation.Annotation;
import java.lang.reflect.Constructor;
import java.lang.reflect.Method;
import java.lang.reflect.Parameter;

import lombok.FieldDefaultsExample.FieldLevelPrivate;
import lombok.FieldDefaultsExample.FieldLevelPublic;
import lombok.FieldDefaultsExample.FieldFinal;
import lombok.AccessorsExample.AccessorsChain;
import lombok.AccessorsExample.AccessorsFluent;
import lombok.AccessorsExample.AccessorsPrefix;

import static org.assertj.core.api.Assertions.assertThat;

@RunWith(Enclosed.class)
public class LombokExampleTest {

    public static class AccessorsExampleTest {

        @Test
        public void AccessorsChainTest() {
            AccessorsChain chain = new AccessorsChain();
            chain.setBar("AAA").printBar();

            assertThat(chain.setBar("AAA").getClass()).isEqualTo(AccessorsChain.class);
        }

        @Test
        public void accessorsFluentTest() {
            AccessorsFluent acc = new AccessorsFluent();

            // getter
            acc.foo();
            assertThat(acc.foo()).isEqualTo("abc");

            // setter
            acc.foo("ccc");
            assertThat(acc.foo()).isEqualTo("ccc");
        }

        @Test
        public void AccessorsPrefixTest() {
            AccessorsPrefix p = new AccessorsPrefix();

            // getter
            p.setZoo("AAA");

            // setter
            p.getZoo();

            assertThat(p.getZoo()).isEqualTo("AAA");
        }
    }

    public static class FieldDefaultsExampleTest {

        @Test
        public void fieldPrivateTest() {
            FieldLevelPrivate pri = new FieldLevelPrivate();

            // error!!
            // pri.text;
            assertThat(pri.getText()).isEqualTo("ABC");
        }

        @Test
        public void fieldPublicTest() {
            FieldLevelPublic pub = new FieldLevelPublic();

            assertThat(pub.num).isEqualTo(100);

            pub.num = 200;
            assertThat(pub.num).isEqualTo(200);
        }

        @Test
        public void FieldFinalTest() {
            FieldFinal ff = new FieldFinal();

            // final
            // ff.num = 200; // error!!
            assertThat(ff.count).isEqualTo(200);

            // non final
            ff.memo = "new memo";
            assertThat(ff.memo).isEqualTo("new memo");
        }
    }

    public static class WitherExampleTest {

        @Test
        public void WitherTest() {
            WitherExample origin = new WitherExample("abc", 123);

            WitherExample newNameByWith = origin.withName("BBB");

            assertThat(origin).isNotEqualTo(newNameByWith);
            assertThat(newNameByWith.getAge()).isEqualTo(123);
            assertThat(newNameByWith.getName()).isEqualTo("BBB");

            WitherExample newAgeByWith = origin.withAge(1_000);

            assertThat(origin).isNotEqualTo(newAgeByWith);
            assertThat(newAgeByWith.getAge()).isEqualTo(1_000);
            assertThat(newAgeByWith.getName()).isEqualTo("abc");
        }
    }

    public static class onXExampleTest {

        @Test
        public void OnXTest() throws NoSuchMethodException {

            // constructor
            Constructor con = OnXExample.class.getConstructor(String.class, int.class);
            Annotation anoCons = con.getAnnotation(OnXExample.ConstructorAnnotation.class);
            assertThat(anoCons).isInstanceOf(OnXExample.ConstructorAnnotation.class);

            // setter
            Method name = OnXExample.class.getMethod("setName", String.class);
            Parameter[] paraSetter = name.getParameters();
            Annotation anoSetter = paraSetter[0].getAnnotation(Min.class);
            assertThat(anoSetter).isInstanceOf(Min.class);

            // getter
            Method num = OnXExample.class.getMethod("getNum");
            Annotation anoGetter = num.getAnnotation(OnXExample.MethodAnnotation.class);
            assertThat(anoGetter).isInstanceOf(OnXExample.MethodAnnotation.class);
        }
    }

    public static class UtilityExampleTest {

        @Test
        public void UtilityTest() {
            // error
            // UtilityExample util = new UtilityExample();

            int magicNum = UtilityExample.MAGIC_NUMBER;
            assertThat(magicNum).isEqualTo(10);

            int doubleNum = UtilityExample.doubleNum(200);
            assertThat(doubleNum).isEqualTo(400);
        }
    }
}

終わり。

ソースは一応上げときました。

github.com