Intelligent operation and maintenance platform
概要:この記事では、Manifold を使用して Java で拡張メソッドを実装し、開発効率とコードの可読性を向上させる方法を紹介します。
拡張メソッドとは
拡張メソッドとは、新しい派生型を作成したり、再コンパイルしたり、既存の型をその他の方法で変更したりすることなく、既存の型に直接メソッドを「追加」できる機能です。拡張メソッドを呼び出す際、その型に実際に定義されたメソッドを呼び出す場合と比べて目立った違いはありません。
なぜ拡張メソッドが必要なのか
次のような関数を実装する場合を考えてみましょう。Redis から複数のプロダクト ID を含む文字列を取得し(各プロダクト ID はカンマで区切り)、まずプロダクト ID の重複を排除し(要素の順序を維持できること)、最後に英語のカンマでプロダクト ID を結合します。
// "123,456,123,789"
String str = redisService. get(someKey)
従来の書き方:
String itemIdStrs = String.join(",", new LinkedHashSet<>(Arrays.asList(str.split(","))));
Stream 記法を使う場合:
String itemIdStrs = Arrays. stream(str. split(",")). distinct(). collect(Collectors. joining(","));
Java で拡張メソッドが実装できると想定し、配列に toList 拡張メソッド(配列を List に変換)を追加し、List に toSet 拡張メソッド(List を LinkedHashSet に変換)を追加し、Collection に join 拡張メソッド(コレクション内の要素の文字列形式を指定された結合子で結合)を追加すると、次のようにコードを書けるようになります。
String itemIdStrs = str.split(",").toList().toSet().join(",");
ここまで読めば、なぜ拡張メソッドが必要なのか、既に答えがお分かりでしょう。
ユーティリティクラスを使わずに既存のクラスライブラリを直接強化できる
ユーティリティクラスを使うのと比べて、型自体のメソッドでコードを書く方がより滑らかで快適である
静的メソッドのネストではなくメソッドチェーンになるため、コードが読みやすくなる
Java で拡張メソッドを実装する方法
まず、最近話題の ChatGPT に聞いてみましょう。
なるほど、ChatGPT は Java の拡張メソッドをユーティリティクラスが提供する静的メソッドだと考えているようです。
そこで次に、Manifold という全く新しい技術を紹介していきます。
事前準備
Manifold の仕組みは Lombok と同じで、コンパイル時にアノテーションプロセッサを通じて処理されます。そのため、IDEA で Manifold を正しく使用するには、Manifold IDEA プラグインをインストールする必要があります。
次に、プロジェクトの pom の Maven の maven-compiler-plugin に annotationProcessorPaths を追加します。
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
...
2022.1.34
systems.manifold
manifold-ext
${manifold.version}
...
org.apache.maven.plugins
maven-compiler-plugin
3.8.1
8
8
UTF-8
-Xplugin:Manifold no-bootstrap
systems.manifold
manifold-ext
${manifold.version}
プロジェクトで Lombok を使用している場合は、annotationProcessorPaths に Lombok も追加する必要があります。
org.projectlombok
lombok
${lombok.version}
systems.manifold
manifold-ext
${manifold.version}
拡張メソッドを書く
JDK では、String の split メソッドは文字列をパラメーターとして受け取ります。つまり String[] split(String) です。ここで、String に拡張メソッド String[] split(char)(指定された文字で分割)を追加してみましょう。
Manifold を使った拡張メソッドの書き方:
package com.alibaba.zhiye.extensions.java.lang.String;
import manifold.ext.rt.api.Extension;
import manifold.ext.rt.api.This;
import org.apache.commons.lang3.StringUtils;
/**
* Extension method for String
*/
@Extension
public final class StringExt {
public static String[] split(@This String str, char separator) {
return StringUtils. split(str, separator);
}
}
本質的にはユーティリティクラスの静的メソッドですが、いくつかの要件があります。
ユーティリティクラスには Manifold の @Extension アノテーションを使用する必要がある
静的メソッド内で、拡張対象の型のパラメーターに @This アノテーションを付ける必要がある
ユーティリティクラスのパッケージ名は、extensions.拡張対象の型の完全修飾クラス名 で終わる必要がある
C# を使ったことがある方ならピンと来るでしょう。これは C# の拡張メソッドを模倣したものです。
3 番目の要件について、Manifold がこのようなパッケージ配置を要求する理由は、プロジェクト内の拡張メソッドを素早く見つけられるようにするためです。プロジェクト内のすべてのクラスに対してアノテーションをスキャンするのを避け、処理効率を向上させています。
拡張メソッドの機能により、次のように呼び出せるようになります。
Xnip2023-01-11_10-54-36.png
素晴らしいですね。そして、System.out.println(numStrs.toString()) が実際に配列オブジェクトの文字列表現を出力していることにもご注目ください。配列オブジェクトのアドレスではありません。App.class を逆コンパイルして確認すると、拡張メソッドの呼び出しが静的メソッドの呼び出しに置き換えられていることがわかります。
image.png
配列の toString メソッドは、Manifold が配列用に定義した拡張メソッド ManArrayExt.toString(@This Object array) を使用しています。
image.png
[Ljava.lang.String;@511d50c0 なんて出力とは、さようなら。
拡張メソッドの呼び出しはコンパイル時に静的メソッドの呼び出しに置き換えられるため、Manifold の拡張メソッドを使えば、呼び出し対象のオブジェクトが null でも問題ありません。変換後のコードでは null がパラメーターとして対応する静的メソッドに渡されるだけです。たとえば、Collection に拡張メソッドを追加してみましょう。
package com.alibaba.zhiye.extensions.java.util.Collection;
import manifold.ext.rt.api.Extension;
import manifold.ext.rt.api.This;
import java.util.Collection;
/**
* Extension method of Collection
*/
@Extension
public final class CollectionExt {
public static boolean isNullOrEmpty(@This Collection coll) {
return coll == null || coll.isEmpty();
}
}
呼び出し時は次のようになります。
List list = getSomeNullableList();
// list が null の場合、null ポインタ例外を発生させずに if ブロックに入ります
if (list. isNullOrEmpty()) {
//TODO
}
java.lang.NullPointerException とも、さようなら。
配列の拡張メソッド
JDK では、配列には特定の対応する型がないため、配列用に定義する拡張クラスはどのパッケージに配置すればよいでしょうか。ManArrayExt のソースコードを確認すると、Manifold は配列を表現するために特別に manifold.rt.api.Array というクラスを提供していることがわかります。たとえば、ManArrayExt の toList メソッドは次のようになっています。
image.png
List<@Self(true) Object> という書き方になっています。@Self は、アノテーションが付けられた値がどの型であるべきかを示すために使用されます。@Self、つまり @Self(false) の場合、アノテーションが付けられた値は @This アノテーションの値と同じ型であることを示します。@Self(true) は配列内の要素の型を示します。
オブジェクト配列の場合、toList メソッドは対応する List を返します(T は配列要素の型)。
image.png
しかし、プリミティブ型の配列の場合、IDEA が示す戻り値は次のようになります。
image.png
でも使っているのは Java です。型消去のあるジェネリクスに List のような便利な機能があるわけがありません。プリミティブ型でこの戻り値を受け取るしかないのです。
image.png
願いを込めて:Project Valhalla が Java 21 で GA になりますように。
さまざまなプロジェクトで、オブジェクトをいったん Optional でラップしてから filter や map などの操作を行うパターンがよく見られます。@Self の型マッピングを使うと、Object に次のような実用的なメソッドを追加できます。
package com.alibaba.zhiye.extensions.java.lang.Object;
import manifold.ext.rt.api.Extension;
import manifold.ext.rt.api.Self;
import manifold.ext.rt.api.This;
import java.util.Optional;
/**
* Extension method for Object
*/
@Extension
public final class ObjectExt {
public static Optional<@Self Object> asOpt(@This Object obj) {
return Optional.ofNullable(obj);
}
}
これで任意のオブジェクトに asOpt() メソッドが使えるようになります。以前のように先にラップする不自然さと比べると:
Optional.ofNullable(someObj).filter(someFilter).map(someMapper).orElseGet(someSupplier);
次のように自然に Optional を使えるようになります。
someObj.asOpt().filter(someFilter).map(someMapper).orElseGet(someSupplier);
もちろん、Object はすべてのクラスの親クラスです。这种做法が適切かどうかは慎重に検討する必要があります。
静的メソッドの拡張
ご存知の通り、Java 9 でコレクションにファクトリメソッドが追加されました。
List list = List.of("a", "b", "c");
Set set = Set.of("a", "b", "c");
Map map = Map.of("a", 1, "b", 2, "c", 3);
うらやましいですか?Java 9 以上を使っていない場合(Java 8:もう私の ID を通報してください)、Guava などのライブラリを使う必要があります。しかし ImmutableList.of はやはり List.of ほど本格的ではありません。
大丈夫です。Manifold が「任せて」と言っています。Manifold で静的メソッドを拡張するには、拡張クラスの静的メソッドに @Extension を追加します。
package com.alibaba.aladdin.app.extensions.java.util.List;
import manifold.ext.rt.api.Extension;
import manifold.ext.rt.api.This;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
/**
* List extension method
*/
@Extension
public final class ListExt {
/**
* Returns an immutable List containing only one element
*/
@Extension
public static List of(E element) {
return Collections. singletonList(element);
}
/**
* Returns an immutable List containing multiple elements
*/
@Extension
@SafeVarargs
public static List of(E... elements) {
return Collections. unmodifiableList(Arrays. asList(elements));
}
}
これで Java 8 以降のバージョンを使っていると自分に言い聞かせられます。好きなだけ配布してください。私は Java 8 を使います。
ところで、Object はすべてのクラスの親クラスなので、Object に静的拡張メソッドを追加すると、どこからでも import なしでこの静的メソッドに直接アクセスできます。おめでとうございます。「トップレベル関数」をアンロックしました。
提案
Manifold について
私が Manifold に注目し始めたのは 2019 年です。当時、Manifold の IDEA プラグインはまだ有料だったため、簡単な試用にとどまりました。最近確認したところ、IDEA プラグインは完全に無料になったので、さっそく本格的に使い始めました。現在、あるプロジェクトで Manifold を使って拡張メソッド機能を実現しています。関係者の話では、非常に夢中になっていて、もう手放せないとのことです。使い方についてご提案やご質問があれば、お気軽にご議論ください。
拡張メソッドは慎重に追加する
プロジェクトで Manifold を使って拡張メソッドを実装すると決めた場合、必ず「自制心を持つ」必要があります。
まず、前述の通り、Object やプロジェクト内で広範に使用されているその他のクラスに拡張メソッドを追加する際は、非常に慎重である必要があります。プロジェクトチームのメンバーと議論し、皆で決めるのが最善です。さもないと、容易に混乱を招きます。
さらに、あるクラスに拡張メソッドを追加する前に、まず次の質問を真剣に考える必要があります。「このメソッドのロジックはこのクラスの責務範囲内か、業務固有のロジックが混在していないか」。たとえば、次のメソッド(指定された文字列が有効なパラメーターかどうかを判定する)を見てみましょう。
public static boolean isValidParam(String str) {
return StringUtils.isNotBlank(str) && 。"null".equalsIgnoreCase(str);
}
明らかに、isValidParam は String クラスの責務範囲外です。isValidParam は引き続き XxxBizUtils に配置すべきです。もちろん、メソッド名を isNotBlankAndNotEqualsIgnoreCaseNullLiteral に変更するなら問題ありませんが、それはやめておくことをお勧めします。殴られかねませんから。
拡張メソッドとは
拡張メソッドとは、新しい派生型を作成したり、再コンパイルしたり、既存の型をその他の方法で変更したりすることなく、既存の型に直接メソッドを「追加」できる機能です。拡張メソッドを呼び出す際、その型に実際に定義されたメソッドを呼び出す場合と比べて目立った違いはありません。
なぜ拡張メソッドが必要なのか
次のような関数を実装する場合を考えてみましょう。Redis から複数のプロダクト ID を含む文字列を取得し(各プロダクト ID はカンマで区切り)、まずプロダクト ID の重複を排除し(要素の順序を維持できること)、最後に英語のカンマでプロダクト ID を結合します。
// "123,456,123,789"
String str = redisService. get(someKey)
従来の書き方:
String itemIdStrs = String.join(",", new LinkedHashSet<>(Arrays.asList(str.split(","))));
Stream 記法を使う場合:
String itemIdStrs = Arrays. stream(str. split(",")). distinct(). collect(Collectors. joining(","));
Java で拡張メソッドが実装できると想定し、配列に toList 拡張メソッド(配列を List に変換)を追加し、List に toSet 拡張メソッド(List を LinkedHashSet に変換)を追加し、Collection に join 拡張メソッド(コレクション内の要素の文字列形式を指定された結合子で結合)を追加すると、次のようにコードを書けるようになります。
String itemIdStrs = str.split(",").toList().toSet().join(",");
ここまで読めば、なぜ拡張メソッドが必要なのか、既に答えがお分かりでしょう。
ユーティリティクラスを使わずに既存のクラスライブラリを直接強化できる
ユーティリティクラスを使うのと比べて、型自体のメソッドでコードを書く方がより滑らかで快適である
静的メソッドのネストではなくメソッドチェーンになるため、コードが読みやすくなる
Java で拡張メソッドを実装する方法
まず、最近話題の ChatGPT に聞いてみましょう。
なるほど、ChatGPT は Java の拡張メソッドをユーティリティクラスが提供する静的メソッドだと考えているようです。
そこで次に、Manifold という全く新しい技術を紹介していきます。
事前準備
Manifold の仕組みは Lombok と同じで、コンパイル時にアノテーションプロセッサを通じて処理されます。そのため、IDEA で Manifold を正しく使用するには、Manifold IDEA プラグインをインストールする必要があります。
次に、プロジェクトの pom の Maven の maven-compiler-plugin に annotationProcessorPaths を追加します。
...
...
プロジェクトで Lombok を使用している場合は、annotationProcessorPaths に Lombok も追加する必要があります。
拡張メソッドを書く
JDK では、String の split メソッドは文字列をパラメーターとして受け取ります。つまり String[] split(String) です。ここで、String に拡張メソッド String[] split(char)(指定された文字で分割)を追加してみましょう。
Manifold を使った拡張メソッドの書き方:
package com.alibaba.zhiye.extensions.java.lang.String;
import manifold.ext.rt.api.Extension;
import manifold.ext.rt.api.This;
import org.apache.commons.lang3.StringUtils;
/**
* Extension method for String
*/
@Extension
public final class StringExt {
public static String[] split(@This String str, char separator) {
return StringUtils. split(str, separator);
}
}
本質的にはユーティリティクラスの静的メソッドですが、いくつかの要件があります。
ユーティリティクラスには Manifold の @Extension アノテーションを使用する必要がある
静的メソッド内で、拡張対象の型のパラメーターに @This アノテーションを付ける必要がある
ユーティリティクラスのパッケージ名は、extensions.拡張対象の型の完全修飾クラス名 で終わる必要がある
C# を使ったことがある方ならピンと来るでしょう。これは C# の拡張メソッドを模倣したものです。
3 番目の要件について、Manifold がこのようなパッケージ配置を要求する理由は、プロジェクト内の拡張メソッドを素早く見つけられるようにするためです。プロジェクト内のすべてのクラスに対してアノテーションをスキャンするのを避け、処理効率を向上させています。
拡張メソッドの機能により、次のように呼び出せるようになります。
Xnip2023-01-11_10-54-36.png
素晴らしいですね。そして、System.out.println(numStrs.toString()) が実際に配列オブジェクトの文字列表現を出力していることにもご注目ください。配列オブジェクトのアドレスではありません。App.class を逆コンパイルして確認すると、拡張メソッドの呼び出しが静的メソッドの呼び出しに置き換えられていることがわかります。
image.png
配列の toString メソッドは、Manifold が配列用に定義した拡張メソッド ManArrayExt.toString(@This Object array) を使用しています。
image.png
[Ljava.lang.String;@511d50c0 なんて出力とは、さようなら。
拡張メソッドの呼び出しはコンパイル時に静的メソッドの呼び出しに置き換えられるため、Manifold の拡張メソッドを使えば、呼び出し対象のオブジェクトが null でも問題ありません。変換後のコードでは null がパラメーターとして対応する静的メソッドに渡されるだけです。たとえば、Collection に拡張メソッドを追加してみましょう。
package com.alibaba.zhiye.extensions.java.util.Collection;
import manifold.ext.rt.api.Extension;
import manifold.ext.rt.api.This;
import java.util.Collection;
/**
* Extension method of Collection
*/
@Extension
public final class CollectionExt {
public static boolean isNullOrEmpty(@This Collection coll) {
return coll == null || coll.isEmpty();
}
}
呼び出し時は次のようになります。
List
// list が null の場合、null ポインタ例外を発生させずに if ブロックに入ります
if (list. isNullOrEmpty()) {
//TODO
}
java.lang.NullPointerException とも、さようなら。
配列の拡張メソッド
JDK では、配列には特定の対応する型がないため、配列用に定義する拡張クラスはどのパッケージに配置すればよいでしょうか。ManArrayExt のソースコードを確認すると、Manifold は配列を表現するために特別に manifold.rt.api.Array というクラスを提供していることがわかります。たとえば、ManArrayExt の toList メソッドは次のようになっています。
image.png
List<@Self(true) Object> という書き方になっています。@Self は、アノテーションが付けられた値がどの型であるべきかを示すために使用されます。@Self、つまり @Self(false) の場合、アノテーションが付けられた値は @This アノテーションの値と同じ型であることを示します。@Self(true) は配列内の要素の型を示します。
オブジェクト配列の場合、toList メソッドは対応する List を返します(T は配列要素の型)。
image.png
しかし、プリミティブ型の配列の場合、IDEA が示す戻り値は次のようになります。
image.png
でも使っているのは Java です。型消去のあるジェネリクスに List のような便利な機能があるわけがありません。プリミティブ型でこの戻り値を受け取るしかないのです。
image.png
願いを込めて:Project Valhalla が Java 21 で GA になりますように。
さまざまなプロジェクトで、オブジェクトをいったん Optional でラップしてから filter や map などの操作を行うパターンがよく見られます。@Self の型マッピングを使うと、Object に次のような実用的なメソッドを追加できます。
package com.alibaba.zhiye.extensions.java.lang.Object;
import manifold.ext.rt.api.Extension;
import manifold.ext.rt.api.Self;
import manifold.ext.rt.api.This;
import java.util.Optional;
/**
* Extension method for Object
*/
@Extension
public final class ObjectExt {
public static Optional<@Self Object> asOpt(@This Object obj) {
return Optional.ofNullable(obj);
}
}
これで任意のオブジェクトに asOpt() メソッドが使えるようになります。以前のように先にラップする不自然さと比べると:
Optional.ofNullable(someObj).filter(someFilter).map(someMapper).orElseGet(someSupplier);
次のように自然に Optional を使えるようになります。
someObj.asOpt().filter(someFilter).map(someMapper).orElseGet(someSupplier);
もちろん、Object はすべてのクラスの親クラスです。这种做法が適切かどうかは慎重に検討する必要があります。
静的メソッドの拡張
ご存知の通り、Java 9 でコレクションにファクトリメソッドが追加されました。
List
Set
Map
うらやましいですか?Java 9 以上を使っていない場合(Java 8:もう私の ID を通報してください)、Guava などのライブラリを使う必要があります。しかし ImmutableList.of はやはり List.of ほど本格的ではありません。
大丈夫です。Manifold が「任せて」と言っています。Manifold で静的メソッドを拡張するには、拡張クラスの静的メソッドに @Extension を追加します。
package com.alibaba.aladdin.app.extensions.java.util.List;
import manifold.ext.rt.api.Extension;
import manifold.ext.rt.api.This;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
/**
* List extension method
*/
@Extension
public final class ListExt {
/**
* Returns an immutable List containing only one element
*/
@Extension
public static
return Collections. singletonList(element);
}
/**
* Returns an immutable List containing multiple elements
*/
@Extension
@SafeVarargs
public static
return Collections. unmodifiableList(Arrays. asList(elements));
}
}
これで Java 8 以降のバージョンを使っていると自分に言い聞かせられます。好きなだけ配布してください。私は Java 8 を使います。
ところで、Object はすべてのクラスの親クラスなので、Object に静的拡張メソッドを追加すると、どこからでも import なしでこの静的メソッドに直接アクセスできます。おめでとうございます。「トップレベル関数」をアンロックしました。
提案
Manifold について
私が Manifold に注目し始めたのは 2019 年です。当時、Manifold の IDEA プラグインはまだ有料だったため、簡単な試用にとどまりました。最近確認したところ、IDEA プラグインは完全に無料になったので、さっそく本格的に使い始めました。現在、あるプロジェクトで Manifold を使って拡張メソッド機能を実現しています。関係者の話では、非常に夢中になっていて、もう手放せないとのことです。使い方についてご提案やご質問があれば、お気軽にご議論ください。
拡張メソッドは慎重に追加する
プロジェクトで Manifold を使って拡張メソッドを実装すると決めた場合、必ず「自制心を持つ」必要があります。
まず、前述の通り、Object やプロジェクト内で広範に使用されているその他のクラスに拡張メソッドを追加する際は、非常に慎重である必要があります。プロジェクトチームのメンバーと議論し、皆で決めるのが最善です。さもないと、容易に混乱を招きます。
さらに、あるクラスに拡張メソッドを追加する前に、まず次の質問を真剣に考える必要があります。「このメソッドのロジックはこのクラスの責務範囲内か、業務固有のロジックが混在していないか」。たとえば、次のメソッド(指定された文字列が有効なパラメーターかどうかを判定する)を見てみましょう。
public static boolean isValidParam(String str) {
return StringUtils.isNotBlank(str) && 。"null".equalsIgnoreCase(str);
}
明らかに、isValidParam は String クラスの責務範囲外です。isValidParam は引き続き XxxBizUtils に配置すべきです。もちろん、メソッド名を isNotBlankAndNotEqualsIgnoreCaseNullLiteral に変更するなら問題ありませんが、それはやめておくことをお勧めします。殴られかねませんから。
Related Articles
-
A detailed explanation of Hadoop core architecture HDFS
Knowledge Base Team
-
What Does IOT Mean
Knowledge Base Team
-
6 Optional Technologies for Data Storage
Knowledge Base Team
-
What Is Blockchain Technology
Knowledge Base Team
Explore More Special Offers
-
Short Message Service(SMS) & Mail Service
50,000 email package starts as low as USD 1.99, 120 short messages start at only USD 1.00
