【Transform3D】Detailed Transformation

概要:この記事では、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.xml の 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 の場合、ヌルポインタ例外を発生させずに 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 に変更するなら問題ありませんが——そんなことはしないことをお勧めします。殴られますよ。

Related Articles

Explore More Special Offers

  1. 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

phone お問い合わせ
Hi, I'm Alibaba Cloud AI Assistant!
I can help with questions and solutions.