覚えたら書く

IT関係のデベロッパとして日々覚えたことを書き残したいです。twitter: @yyoshikaw

Lombok - @ExtensionMethod

Lombokの@ExtensionMethod(lombok.experimental.ExtensionMethod)アノテーションの利用サンプルです。

JavaにおいてString(java.lnag.String)クラスに対して、メソッドを追加することはできません。
Stringに対する新しい操作がほしい場合は、Apache CommonsのStringUtilsクラスやGuavaのStringsクラスなど、Stringクラスの外側で提供されているAPIを使用することになります。

しかし、@ExtensionMethodを利用すると、あたかもStringクラス内にStringUtilsクラスやStringsクラスで提供されてるAPIが追加(拡張)されたような状態にすることができます。

@ExtensionMethodは所属パッケージが"experimental"になっており、その名の通り実験的なアノテーションです。
そのため、今後Lombokのバージョンが上がった際などに仕様が変更される可能性もあると思われます。


java.lang.StringをApache Commons LangのStringUtilsで拡張する

クラスに@ExtensionMethodアノテーションを付与し、パラメータにStringUtils.classを指定して拡張を行います

■@ExtensionMethodでApache Commons LangのStringUtilsを追加

Stringの値に対して、StringUtilsで定義されているメソッド(repeat, isNumeric, wrap 等)が使用できていることが分かります

import org.apache.commons.lang3.StringUtils;

import lombok.experimental.ExtensionMethod;

@ExtensionMethod({StringUtils.class})
public class StringClient {

    public static void main(String[] args) {

        String str1 = "Hello World.";
        String str2 = str1.repeat(5);
        System.out.println(str1 + ": repeat 5 -> " + str2);

        String str3 = "123456";
        String str4 = "A123456C";
        boolean isNumeric1 = str3.isNumeric();
        boolean isNumeric2 = str4.isNumeric();
        System.out.println(str3 + " is numeric? -> " + isNumeric1);
        System.out.println(str4 + " is numeric? -> " + isNumeric2);

        String wrappedStr1 = "hello".wrap("###");
        System.out.println("hello wrap ### -> " + wrappedStr1);
    }
}

■実行結果

StringUtilsの各APIの結果が得られていることが分かります

Hello World.: repeat 5 -> Hello World.Hello World.Hello World.Hello World.Hello World.
123456 is numeric? -> true
A123456C is numeric? -> false
hello wrap ### -> ###hello###

■実際に生成されるソースコード

文字列操作がStringUtilsのメソッドで実行されていることが分かります

import org.apache.commons.lang3.StringUtils;

public class StringClient {
    public static void main(String[] args) {
        String str1 = "Hello World.";
        String str2 = org.apache.commons.lang3.StringUtils.repeat(str1, 5);
        System.out.println(str1 + ": repeat 5 -> " + str2);
        String str3 = "123456";
        String str4 = "A123456C";
        boolean isNumeric1 = org.apache.commons.lang3.StringUtils.isNumeric(str3);
        boolean isNumeric2 = org.apache.commons.lang3.StringUtils.isNumeric(str4);
        System.out.println(str3 + " is numeric? -> " + isNumeric1);
        System.out.println(str4 + " is numeric? -> " + isNumeric2);
        String wrappedStr1 = org.apache.commons.lang3.StringUtils.wrap("hello", "###");
        System.out.println("hello wrap ### -> " + wrappedStr1);
    }
}


java.lang.StringをGuavaのStringsで拡張する

クラスに@ExtensionMethodアノテーションを付与し、パラメータにStrings.classを指定して拡張を行います

■@ExtensionMethodでGuavaのStringsを追加

Stringの値に対して、GuavaのStringsで定義されているメソッド(repeat, padStart, padEnd, isNullOrEmpty 等)が使用できていることが分かります

import com.google.common.base.Strings;

import lombok.experimental.ExtensionMethod;

@ExtensionMethod({Strings.class})
public class StringClient2 {

    public static void main(String[] args) {

        String str1 = "Hello World.";
        String str2 = str1.repeat(5);
        System.out.println(str1 + ": repeat 5 -> " + str2);

        String str3 = "abc";
        String paddedStr1 = str3.padStart(10, '*');
        String paddedStr2 = str3.padEnd(10, '/');
        System.out.println(str3 + " pad start. -> " + paddedStr1);
        System.out.println(str3 + " pad end.   -> " + paddedStr2);

        String str4 = "not empty";
        String str5 = "";
        String str6 = null;
        System.out.println("[" + str4 + "] is null or empty? -> " + str4.isNullOrEmpty());
        System.out.println("[" + str5 + "] is null or empty? -> " + str5.isNullOrEmpty());
        System.out.println("[" + str6 + "] is null or empty? -> " + str6.isNullOrEmpty());
    }
}

■実行結果

Stringsの各APIの結果が得られていることが分かります

Hello World.: repeat 5 -> Hello World.Hello World.Hello World.Hello World.Hello World.
abc pad start. -> *******abc
abc pad end.   -> abc///////
[not empty] is null or empty? -> false
[] is null or empty? -> true
[null] is null or empty? -> true

■実際に生成されるソースコード

文字列操作がStringsのメソッドで実行されていることが分かります

import com.google.common.base.Strings;

public class StringClient2 {
    public static void main(String[] args) {
        String str1 = "Hello World.";
        String str2 = com.google.common.base.Strings.repeat(str1, 5);
        System.out.println(str1 + ": repeat 5 -> " + str2);
        String str3 = "abc";
        String paddedStr1 = com.google.common.base.Strings.padStart(str3, 10, '*');
        String paddedStr2 = com.google.common.base.Strings.padEnd(str3, 10, '/');
        System.out.println(str3 + " pad start. -> " + paddedStr1);
        System.out.println(str3 + " pad end.   -> " + paddedStr2);
        String str4 = "not empty";
        String str5 = "";
        String str6 = null;
        System.out.println("[" + str4 + "] is null or empty? -> " + com.google.common.base.Strings.isNullOrEmpty(str4));
        System.out.println("[" + str5 + "] is null or empty? -> " + com.google.common.base.Strings.isNullOrEmpty(str5));
        System.out.println("[" + str6 + "] is null or empty? -> " + com.google.common.base.Strings.isNullOrEmpty(str6));
    }
}


java.lang.Stringを独自クラスで拡張する

自分で拡張用のクラスを定義して、そのクラスを@ExtensionMethodアノテーションのパラメータに指定することもできます。
(本サンプルではStringExtensionsという独自クラスを定義しています)

■Stringを拡張するための独自クラスを定義

public class StringExtensions {

    public static String toTitleCase(String in) {
        return "[" + in + "]";
    }

    public static boolean isHello(String in) {
        return "Hello".equals(in);
    }

    public static int toInt(String in) {
        return Integer.valueOf(in);
    }
}

■@ExtensionMethodでStringExtensionsを指定する

Stringの値に対して、StringExtensionsクラスで定義されているメソッド(toTitleCase, isHello, toInt)が使用できていることが分かります

import lombok.experimental.ExtensionMethod;

@ExtensionMethod({StringExtensions.class})
public class StringClient3 {

    public static void main(String[] args) {

        String str1 = "Hello World.";
        String str2 = "title";
        System.out.println(str1 + "#toTitleCase -> " + str1.toTitleCase());
        System.out.println(str2 + "#toTitleCase -> " + str2.toTitleCase());

        String str3 = "Hello";
        String str4 = "Dummy";
        System.out.println(str3 + " is Hello? -> " + str3.isHello());
        System.out.println(str3 + " is Hello? -> " + str4.isHello());

        int val1 = "12345".toInt();
        int val2 = "0".toInt();
        System.out.println("val1: " + val1);
        System.out.println("val2: " + val2);
    }
}

■実行結果

StringExtensionsの各APIの結果が得られていることが分かります

Hello World.#toTitleCase -> [Hello World.]
title#toTitleCase -> [title]
Hello is Hello? -> true
Hello is Hello? -> false
val1: 12345
val2: 0

■実際に生成されるソースコード

文字列操作がStringExtensionsクラスのメソッドで実行されていることが分かります

import sample.extension.StringExtensions;

public class StringClient3 {
    public static void main(String[] args) {
        String str1 = "Hello World.";
        String str2 = "title";
        System.out.println(str1 + "#toTitleCase -> " + sample.extension.StringExtensions.toTitleCase(str1));
        System.out.println(str2 + "#toTitleCase -> " + sample.extension.StringExtensions.toTitleCase(str2));
        String str3 = "Hello";
        String str4 = "Dummy";
        System.out.println(str3 + " is Hello? -> " + sample.extension.StringExtensions.isHello(str3));
        System.out.println(str3 + " is Hello? -> " + sample.extension.StringExtensions.isHello(str4));
        int val1 = sample.extension.StringExtensions.toInt("12345");
        int val2 = sample.extension.StringExtensions.toInt("0");
        System.out.println("val1: " + val1);
        System.out.println("val2: " + val2);
    }
}


intの操作をGuavaのIntsで拡張する

クラスに@ExtensionMethodアノテーションを付与し、パラメータにInts.classを指定して拡張を行います

■@ExtensionMethodでGuavaのIntsを追加

int配列に対して、GuavaのIntsで定義されているメソッド(asList, max, min, contains 等)が使用できていることが分かります

import java.util.List;

import com.google.common.primitives.Ints;

import lombok.experimental.ExtensionMethod;

@ExtensionMethod({Ints.class})
public class IntClient {

    public static void main(String[] args) {

        int[] srcArray = new int[] { 1, 2, 3, 10, 5 };
        List<Integer> intList = srcArray.asList();
        System.out.println("intList: " + intList);

        System.out.println("srcArray#max: " + srcArray.max());
        System.out.println("srcArray#min: " + srcArray.min());

        System.out.println("srcArray contains 3? -> " + srcArray.contains(3));
        System.out.println("srcArray contains 4? -> " + srcArray.contains(4));
    }

}

■実行結果

Intsの各APIの結果が得られていることが分かります

intList: [1, 2, 3, 10, 5]
srcArray#max: 10
srcArray#min: 1
srcArray contains 3? -> true
srcArray contains 4? -> false

■実際に生成されるソースコード

int配列に対する操作がIntsのメソッドで実行されていることが分かります

import java.util.List;
import com.google.common.primitives.Ints;

public class IntClient {
    public static void main(String[] args) {
        int[] srcArray = new int[] {1, 2, 3, 10, 5};
        List<Integer> intList = com.google.common.primitives.Ints.asList(srcArray);
        System.out.println("intList: " + intList);
        System.out.println("srcArray#max: " + com.google.common.primitives.Ints.max(srcArray));
        System.out.println("srcArray#min: " + com.google.common.primitives.Ints.min(srcArray));
        System.out.println("srcArray contains 3? -> " + com.google.common.primitives.Ints.contains(srcArray, 3));
        System.out.println("srcArray contains 4? -> " + com.google.common.primitives.Ints.contains(srcArray, 4));
    }
}


汎用的な拡張用クラスを定義して使用する

Genericsを用いて汎用的な拡張用のクラスを定義して、そのクラスを@ExtensionMethodアノテーションのパラメータに指定することもできます

■@汎用的な拡張用クラスを定義

値がnullならデフォルト値を返すメソッドを持つクラス

public class Extensions {
    
    public static <T> T or(T obj, T defaultValue) {
        if (obj == null) {
            return defaultValue;
        }

        return obj;
    }
}

■@ExtensionMethodでExtensionsを指定する

Extensionsで定義されているメソッド(or)が使用できていることが分かります

import java.util.Arrays;
import java.util.List;

import lombok.experimental.ExtensionMethod;

@ExtensionMethod({Extensions.class})
public class Client1 {

    public static void main(String[] args) {

        String[] strArray = new String[] { "abc", null, "000", null, "AAA" };

        for (int i = 0; i < strArray.length; i++) {
            System.out.println("strArray ellement[ "+ i + "]: " + strArray[i].or("default"));
        }

        List<Long> longList = Arrays.asList(100L, null, null, 999L);

        for (Long l : longList) {
            System.out.println("longList ellement: " + l.or(-1L));
        }
    }
}

■実行結果

Extensionsの各APIの結果が得られていることが分かります

strArray ellement[ 0]: abc
strArray ellement[ 1]: default
strArray ellement[ 2]: 000
strArray ellement[ 3]: default
strArray ellement[ 4]: AAA
longList ellement: 100
longList ellement: -1
longList ellement: -1
longList ellement: 999

■実際に生成されるソースコード

Extensionsのメソッドで実行されていることが分かります

import java.util.Arrays;
import java.util.List;
import sample.extension.Extensions;

public class Client1 {

    public static void main(String[] args) {
        String[] strArray = new String[] {"abc", null, "000", null, "AAA"};
        for (int i = 0; i < strArray.length; i++) {
            System.out.println("strArray ellement[ " + i + "]: " + sample.extension.Extensions.or(strArray[i], "default"));
        }
        List<Long> longList = Arrays.asList(100L, null, null, 999L);
        for (Long l : longList) {
            System.out.println("longList ellement: " + sample.extension.Extensions.or(l, -1L));
        }
    }
}


補足

  • @ExtensionMethodで既存クラスを拡張する場合の弱点として、追加されたメソッドはEclipseのコンテンツ・アシスト(コード補完)機能が使えない点があげられます。
  • (私の設定が悪い可能性もありますが)IntelliJ IDEA上だと@ExtensionMethodはそもそも正常に動作しないように見えます



関連エントリ