C# がデリゲートをサポートするのと同様に、Java 言語にはデリゲート機能がありますか?
15 に答える
そうではありません。
リフレクションを使用して Method オブジェクトを取得することで同じ効果を達成できる場合があります。もう 1 つの方法は、単一の「invoke」または「execute」メソッドを使用してインターフェイスを作成し、それらをインスタンス化してメソッドを呼び出すことです。興味があります(つまり、匿名の内部クラスを使用します)。
この記事も興味深い/役に立つかもしれません: A Java Programmer Looks at C# Delegates (@blueskyprojects.com)
正確に何を意味するかに応じて、ストラテジーパターンを使用して同様の効果(メソッドを渡す)を実現できます。
名前付きメソッドのシグネチャを宣言するこのような行の代わりに:
// C#
public delegate void SomeFunction();
インターフェイスを宣言します。
// Java
public interface ISomeBehaviour {
void SomeFunction();
}
メソッドの具体的な実装については、動作を実装するクラスを定義します。
// Java
public class TypeABehaviour implements ISomeBehaviour {
public void SomeFunction() {
// TypeA behaviour
}
}
public class TypeBBehaviour implements ISomeBehaviour {
public void SomeFunction() {
// TypeB behaviour
}
}
次にSomeFunction
、C#でデリゲートがあった場合は、ISomeBehaviour
代わりに参照を使用します。
// C#
SomeFunction doSomething = SomeMethod;
doSomething();
doSomething = SomeOtherMethod;
doSomething();
// Java
ISomeBehaviour someBehaviour = new TypeABehaviour();
someBehaviour.SomeFunction();
someBehaviour = new TypeBBehaviour();
someBehaviour.SomeFunction();
匿名の内部クラスを使用すると、個別の名前付きクラスを宣言することを回避し、それらを実際のデリゲート関数のように扱うこともできます。
// Java
public void SomeMethod(ISomeBehaviour pSomeBehaviour) {
...
}
...
SomeMethod(new ISomeBehaviour() {
@Override
public void SomeFunction() {
// your implementation
}
});
これはおそらく、実装が現在のコンテキストに非常に固有であり、再利用してもメリットがない場合にのみ使用する必要があります。
そしてもちろん、Java 8では、これらは基本的にラムダ式になります。
// Java 8
SomeMethod(() -> { /* your implementation */ });
序章
Microsoft Visual J++ 開発環境の最新バージョンは、デリゲートまたはバインドされたメソッド参照と呼ばれる言語構造をサポートしています。この構造、およびそれをサポートするために導入された新しいキーワード
delegate
はmulticast
、Java TM プログラミング言語の一部ではありません。Java TM プログラミング言語は、Java 言語仕様によって指定され、JDKTM 1.1 ソフトウェアのドキュメントに含まれる内部クラス仕様によって修正されています。Java プログラミング言語にこの構造が含まれる可能性はほとんどありません。Sun は 1996 年に、動作するプロトタイプを構築して破棄する程度まで、すでに慎重に採用を検討していました。私たちの結論は、バインドされたメソッド参照は不要であり、言語に有害であるというものでした。この決定は、Delphi Object Pascal でバインドされたメソッド参照の経験がある Borland International と相談して行われました。
バインドされたメソッド参照は不要であると考えています。これは、別の設計の代替手段である内部クラスが同等または優れた機能を提供するためです。特に、内部クラスは、ユーザー インターフェイス イベント処理の要件を完全にサポートし、少なくとも Windows Foundation Classes と同じくらい包括的なユーザー インターフェイス API を実装するために使用されています。
バインドされたメソッド参照は、Java プログラミング言語の単純さと API の広く普及しているオブジェクト指向の特徴を損なうため、有害であると私たちは考えています。バインドされたメソッド参照は、言語の構文とスコープ規則にも不規則性をもたらします。最後に、VM テクノロジへの投資が希薄化されます。これは、VM が追加の異なるタイプの参照とメソッド リンクを効率的に処理する必要があるためです。
これを読んだことがありますか:
デリゲートは、イベントベースのシステムで便利な構造です。基本的に、デリゲートは、指定されたオブジェクトのメソッド ディスパッチをエンコードするオブジェクトです。このドキュメントでは、Java 内部クラスがこのような問題に対してより一般的なソリューションを提供する方法を示します。
デリゲートとは何ですか? 実際には、C++ で使用されるメンバー関数へのポインターに非常に似ています。ただし、デリゲートには、呼び出されるメソッドとともにターゲット オブジェクトが含まれます。理想的には、次のように言えるとよいでしょう。
obj.registerHandler(ano.methodOne);
..そして、メソッド methodOne は、特定のイベントが受信されたときに ano で呼び出されます。
これは、デリゲート構造が実現するものです。
Java 内部クラス
Java は匿名の内部クラスを介してこの機能を提供するため、追加の Delegate コンストラクトは必要ないと主張されてきました。
obj.registerHandler(new Handler() {
public void handleIt(Event ev) {
methodOne(ev);
}
} );
一見、これは正しいように見えますが、同時に厄介なことでもあります。多くのイベント処理の例では、デリゲート構文の単純さが非常に魅力的です。
総合ハンドラー
ただし、イベントベースのプログラミングが、たとえば一般的な非同期プログラミング環境の一部として使用されるなど、より普及した方法で使用される場合、問題はさらに大きくなります。
このような一般的な状況では、ターゲット メソッドとターゲット オブジェクト インスタンスだけを含めるだけでは十分ではありません。一般に、イベント ハンドラーの登録時にコンテキスト内で決定される他のパラメーターが必要になる場合があります。
このより一般的な状況では、特に final 変数の使用と組み合わせると、Java アプローチは非常に洗練されたソリューションを提供できます。
void processState(final T1 p1, final T2 dispatch) {
final int a1 = someCalculation();
m_obj.registerHandler(new Handler() {
public void handleIt(Event ev) {
dispatch.methodOne(a1, ev, p1);
}
} );
}
ファイナル * ファイナル * ファイナル
あなたの注意を引きましたか?
final 変数は、無名クラス メソッド定義内からアクセスできることに注意してください。このコードを注意深く調べて、影響を理解してください。これは潜在的に非常に強力なテクニックです。たとえば、MiniDOM でハンドラーを登録するときや、より一般的な状況で効果的に使用できます。
対照的に、Delegate コンストラクトは、このより一般的な要件に対する解決策を提供しないため、設計の基になるイディオムとして拒否する必要があります。
いいえ。ただし、プロキシとリフレクションを使用して偽造できます。
public static class TestClass {
public String knockKnock() {
return "who's there?";
}
}
private final TestClass testInstance = new TestClass();
@Test public void
can_delegate_a_single_method_interface_to_an_instance() throws Exception {
Delegator<TestClass, Callable<String>> knockKnockDelegator = Delegator.ofMethod("knockKnock")
.of(TestClass.class)
.to(Callable.class);
Callable<String> callable = knockKnockDelegator.delegateTo(testInstance);
assertThat(callable.call(), is("who's there?"));
}
このイディオムの良い点は、デリゲータを作成する時点で、デリゲート先のメソッドが存在し、必要な署名があることを確認できることです (残念ながら、コンパイル時ではありませんが、FindBugs プラグインはここでヘルプ)、安全に使用してさまざまなインスタンスに委任します。
より多くのテストと実装については、github の karg コードを参照してください。
リフレクションを使用して、Java でコールバック/デリゲートのサポートを実装しました。詳細と作業ソースは、私の Web サイトで入手できます。
使い方
WithParms というネストされたクラスを持つ Callback という名前のプリンシパル クラスがあります。コールバックを必要とする API は Callback オブジェクトをパラメーターとして取り、必要に応じてメソッド変数として Callback.WithParms を作成します。このオブジェクトのアプリケーションの多くは再帰的であるため、これは非常にきれいに機能します。
パフォーマンスは依然として優先度が高いため、すべての呼び出しのパラメーターを保持するために使い捨てのオブジェクト配列を作成する必要はありませんでした。結局のところ、大規模なデータ構造には何千もの要素が存在する可能性があり、メッセージ処理ではシナリオでは、1 秒間に数千のデータ構造を処理することになる可能性があります。
スレッドセーフにするために、パラメーター配列は API メソッドの呼び出しごとに一意に存在する必要があり、効率のために、コールバックの呼び出しごとに同じ配列を使用する必要があります。コールバックを呼び出し用のパラメーター配列にバインドするために、安価に作成できる 2 つ目のオブジェクトが必要でした。ただし、シナリオによっては、他の理由で呼び出し元が既にパラメーター配列を持っている場合があります。これら 2 つの理由から、パラメーター配列は Callback オブジェクトに属しません。また、呼び出しの選択 (パラメーターを配列として渡すか、個々のオブジェクトとして渡すか) は、コールバックを使用して API の手に委ねられ、内部の動作に最も適した呼び出しを使用できるようにします。
したがって、WithParms のネストされたクラスはオプションであり、2 つの目的を果たします。コールバックの呼び出しに必要なパラメーター オブジェクト配列を含み、パラメーター配列をロードしてから 10 個のオーバーロードされた invoke() メソッド (1 ~ 10 個のパラメーターを持つ) を提供します。コールバック ターゲットを呼び出します。
以下は、コールバックを使用してディレクトリ ツリー内のファイルを処理する例です。これは、処理するファイルをカウントするだけの初期検証パスであり、事前に定義された最大サイズを超えるファイルがないことを確認します。この場合、API 呼び出しとインラインでコールバックを作成するだけです。ただし、毎回リフレクションが行われないように、ターゲット メソッドを静的な値としてリフレクトします。
static private final Method COUNT =Callback.getMethod(Xxx.class,"callback_count",true,File.class,File.class);
...
IoUtil.processDirectory(root,new Callback(this,COUNT),selector);
...
private void callback_count(File dir, File fil) {
if(fil!=null) { // file is null for processing a directory
fileTotal++;
if(fil.length()>fileSizeLimit) {
throw new Abort("Failed","File size exceeds maximum of "+TextUtil.formatNumber(fileSizeLimit)+" bytes: "+fil);
}
}
progress("Counting",dir,fileTotal);
}
IoUtil.processDirectory():
/**
* Process a directory using callbacks. To interrupt, the callback must throw an (unchecked) exception.
* Subdirectories are processed only if the selector is null or selects the directories, and are done
* after the files in any given directory. When the callback is invoked for a directory, the file
* argument is null;
* <p>
* The callback signature is:
* <pre> void callback(File dir, File ent);</pre>
* <p>
* @return The number of files processed.
*/
static public int processDirectory(File dir, Callback cbk, FileSelector sel) {
return _processDirectory(dir,new Callback.WithParms(cbk,2),sel);
}
static private int _processDirectory(File dir, Callback.WithParms cbk, FileSelector sel) {
int cnt=0;
if(!dir.isDirectory()) {
if(sel==null || sel.accept(dir)) { cbk.invoke(dir.getParent(),dir); cnt++; }
}
else {
cbk.invoke(dir,(Object[])null);
File[] lst=(sel==null ? dir.listFiles() : dir.listFiles(sel));
if(lst!=null) {
for(int xa=0; xa<lst.length; xa++) {
File ent=lst[xa];
if(!ent.isDirectory()) {
cbk.invoke(dir,ent);
lst[xa]=null;
cnt++;
}
}
for(int xa=0; xa<lst.length; xa++) {
File ent=lst[xa];
if(ent!=null) { cnt+=_processDirectory(ent,cbk,sel); }
}
}
}
return cnt;
}
この例は、このアプローチの美しさを示しています。アプリケーション固有のロジックはコールバックに抽象化されており、ディレクトリ ツリーを再帰的にたどる単調な作業は、完全に再利用可能な静的ユーティリティ メソッドにうまく隠されています。また、新しい用途ごとにインターフェースを定義して実装するという代償を繰り返し支払う必要はありません。もちろん、インターフェイスの議論は、何を実装するかについてはるかに明示的であるということです(単に文書化されているのではなく、強制されています)が、実際には、コールバックを正しく定義することが問題になることはありません。
インターフェイスを定義して実装することはそれほど悪くはありません (余分なクラスの作成を避けることが実際に重要な私のように、アプレットを配布している場合を除きます) が、これが本当に優れているのは、1 つのクラスに複数のコールバックがある場合です。それらを個別の内部クラスにプッシュすることを余儀なくされるだけでなく、展開されたアプリケーションのオーバーヘッドが追加されますが、プログラミングは実に面倒であり、定型コードはすべて実際には単なる「ノイズ」です。
はい、いいえ。ただし、Java のデリゲート パターンはこのように考えることができます。このビデオ チュートリアルは、アクティビティ - フラグメント間のデータ交換に関するものであり、インターフェイスを使用したデリゲート ソート パターンの優れたエッセンスを含んでいます。
それほどクリーンではありませんが、Javaプロキシを使用してC#デリゲートのようなものを実装できます。
いいえ、内部的には同様の動作をしています。
C# では、デリゲートを使用して別のエントリ ポイントを作成し、関数ポインターのように機能します。
Java には関数ポインタのようなものはありませんが (上から見れば)、Java はこれらの目的を達成するために内部的に同じことを行う必要があります。
たとえば、Java でスレッドを作成するには、Thread を拡張するクラスまたは Runnable を実装するクラスが必要です。これは、クラス オブジェクト変数をメモリ ロケーション ポインターとして使用できるためです。
説明されているコードは、C# デリゲートの多くの利点を提供します。静的または動的なメソッドは、統一された方法で処理できます。リフレクションを通じてメソッドを呼び出す際の複雑さが軽減され、ユーザー コードにクラスを追加する必要がないという意味で、コードが再利用可能になります。ここでは、オブジェクト配列を作成せずに 1 つのパラメーターを持つメソッドを呼び出すことができる、別の便利なバージョンの invoke を呼び出していることに注意してください。以下の Java コード:
class Class1 {
public void show(String s) { System.out.println(s); }
}
class Class2 {
public void display(String s) { System.out.println(s); }
}
// allows static method as well
class Class3 {
public static void staticDisplay(String s) { System.out.println(s); }
}
public class TestDelegate {
public static final Class[] OUTPUT_ARGS = { String.class };
public final Delegator DO_SHOW = new Delegator(OUTPUT_ARGS,Void.TYPE);
public void main(String[] args) {
Delegate[] items = new Delegate[3];
items[0] = DO_SHOW .build(new Class1(),"show,);
items[1] = DO_SHOW.build (new Class2(),"display");
items[2] = DO_SHOW.build(Class3.class, "staticDisplay");
for(int i = 0; i < items.length; i++) {
items[i].invoke("Hello World");
}
}
}
Javaにはデリゲートがなく、それを誇りに思っています:)。ここで読んだことから、デリゲートを偽造する本質的に 2 つの方法を見つけました。2. 内部クラス
反射が遅い!内部クラスは、最も単純なユースケースであるソート機能をカバーしていません。詳細は省きますが、基本的に内部クラスを使用した解決策は、整数の配列を昇順でソートするためのラッパー クラスと、整数の配列を降順でソートするためのクラスを作成することです。