Java's Missing Feature: Extension Methods

Summary: この記事では、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-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 に静的拡張メソッドを追加すると、どこからでもインポートなしでこの静的メソッドに直接アクセスできるということです。おめでとうございます、「トップレベル関数」をアンロックしました。


アドバイス


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.