متد هندل (Method Handle) یک مکانیسم سطح پایین برای جستجو و فراخوانی متدهاست. متد هندلها اغلب با ریفلکشن (Reflection) مقایسه میشوند، زیرا هر دو ابزار (API ریفلکشن و Method Handle) امکان فراخوانی متدها، سازندهها (Constructors) و دسترسی به فیلدها را فراهم میکنند.
اما دقیقاً متد هندل چیست؟ متد هندل یک ارجاع مستقیم و قابلفراخوانی به یک متد، سازنده یا فیلدِ زیرین است. API متد هندل اجازه میدهد علاوه بر داشتن یک اشارهگر ساده به متد، دستکاریهایی روی آن انجام دهیم؛ مثلاً آرگومانها را جابهجا یا وارد کنیم، مقادیر بازگشتی را تغییر دهیم و مواردی از این دست.
در ادامه نگاه دقیقتری میاندازیم به اینکه متد هندلها چه امکاناتی در اختیارمان میگذارند و چگونه میتوانیم از آنها به شکلی موثر استفاده کنیم.
بررسی سطح دسترسی در فراخوانی متد هندلها با API ریفلکشن متفاوت است. در ریفلکشن، با هر بار فراخوانی، سطح دسترسی کدِ فراخواننده بررسی میشود. اما در متد هندل، بررسی دسترسی فقط در زمان ساخت (Creation) آن انجام میشود.
نکته مهمی که باید به خاطر داشت این است: اگر یک متد هندل در کانتکستی (محیطی) ساخته شود که به اعضای غیرعمومی (non-public) دسترسی دارد، اگر آن را به بیرون منتقل کنید، همچنان میتواند به آن اعضای غیرعمومی دسترسی داشته باشد. در نتیجه، اعضای غیرعمومی ممکن است از کدهایی دسترسیپذیر شوند که نباید دسترسی داشته باشند. مسئولیت نگهداری خصوصی (Private) این متد هندلها در محیط خودش بر عهده توسعهدهنده است. به عنوان جایگزین، میتوان متد هندل را از همان ابتدا با محدودیتهای دسترسی و با استفاده از شیء Lookup مناسب ساخت.
برای ساخت یک متد هندل، ابتدا باید یک شیء Lookup بسازیم که مثل یک کارخانه (Factory) برای تولید متد هندل عمل میکند. بسته به اینکه قرار است شیء Lookup یا خود متد هندلها چگونه استفاده شوند، میتوانیم تصمیم بگیریم که سطح دسترسی آن را محدود کنیم یا خیر.
برای مثال، اگر متد هندلی بسازیم که به یک متد Private اشاره میکند و آن متد هندل از بیرون قابل دسترسی باشد، متد Private هم قابل دسترسی خواهد بود. معمولاً میخواهیم از این اتفاق جلوگیری کنیم. یک راه این است که خود شیء Lookup و متد هندل را هم Private نگه داریم. راه دیگر این است که شیء Lookup را با استفاده از متد MethodHandles.publicLookup بسازیم؛ در این صورت فقط میتواند اعضای Public را در کلاسهای Public که در پکیجها به صورت غیرمشروط اکسپورت شدهاند جستجو کند:
MethodHandles.Lookup publicLookup = MethodHandles.publicLookup();
اگر قرار است شیء Lookup و متد هندلها را Private نگه داریم، امن است که به تمام اعضا (از جمله اعضای Private و Protected) به آنها دسترسی بدهیم:
MethodHandles.Lookup lookup = MethodHandles.lookup();
برای جستجوی یک متد هندل، باید اطلاعات تایپ (Type) متد یا فیلد را هم ارائه دهیم. این اطلاعات در قالبی به نام MethodType بازنمایی میشوند. برای ساخت یک نمونه از MethodType، باید نوع مقدار بازگشتی را به عنوان پارامتر اول و به دنبال آن، نوع تمام آرگومانها را وارد کنیم:
MethodType methodType = MethodType.methodType(int.class /* متد خروجی عددی بازمیگرداند */,
String.class /* و یک آرگومان از نوع String میپذیرد */);
با داشتن نمونههای Lookup و MethodType میتوانیم متد هندل را جستجو کنیم. برای متدهای نمونه (Instance Methods) باید از Lookup.findVirtual و برای متدهای ایستا (Static Methods) از Lookup.findStatic استفاده کنیم. هر دوی این متدها آرگومانهای زیر را میپذیرند:
کلاسی که متد در آن قرار دارد (Class)
نام متد به صورت رشته (String)
یک نمونه از MethodType
در مثال زیر، از متد Lookup.findVirtual برای جستجوی متد نمونهی String.replace استفاده میکنیم که دو آرگومان char میگیرد و یک String برمیگرداند:
MethodHandles.Lookup lookup = MethodHandles.lookup();
MethodType replaceMethodType = MethodType.methodType(String.class, char.class, char.class);
MethodHandle replaceMethodHandle = lookup.findVirtual(String.class, "replace", replaceMethodType);
در مثال بعدی، از Lookup.findStatic برای پیدا کردن متد استاتیک String.valueOf استفاده میکنیم که یک Object میگیرد و یک String برمیگرداند:
MethodType valueOfMethodType = MethodType.methodType(String.class, Object.class);
MethodHandle valueOfMethodHandle = lookup.findStatic(String.class, "valueOf", valueOfMethodType);
به همین ترتیب، میتوانیم از متد Lookup.findConstructor برای پیدا کردن متد هندلی که به یک سازنده (Constructor) اشاره میکند استفاده کنیم.
در نهایت، وقتی متد هندل را به دست آوردیم، میتوانیم متد اصلی را فراخوانی کنیم.
فراخوانی را هم میتوان به چند روش انجام داد.
تمام متدهایی که فراخوانی را تسهیل میکنند، در نهایت به یک متد واحد ختم میشوند: MethodHandle.invokeExact. همانطور که از نامش پیداست، آرگومانهای دادهشده به invokeExact باید دقیقاً با تایپ متد هندل مطابقت داشته باشند.
برای مثال، اگر متد String.replace را فراخوانی کنیم، آرگومانها باید دقیقاً مطابق با خروجی String و دو آرگومان char باشند:
MethodType replaceMethodType = MethodType.methodType(String.class, char.class, char.class);
MethodHandle replaceMethodHandle = lookup.findVirtual(String.class, "replace", replaceMethodType);
String result = (String) replaceMethodHandle.invokeExact("dummy", 'd', 'm');
متد MethodHandle.invoke انعطافپذیرتر است. این متد تلاش میکند متد هندل جدیدی با تایپهای تنظیمشده به دست آورد که دقیقاً با تایپ آرگومانهای دادهشده مطابقت داشته باشد؛ سپس آن متد هندلِ تنظیمشده را با استفاده از invokeExact اجرا میکند:
// این کد با invokeExact شکست میخورد ولی با invoke کار میکند:
String result = (String) replaceMethodHandle.invoke((Object)"dummy", (Object)'d', (Object)'m');
یک جایگزین دیگر برای فراخوانی متد هندل، استفاده از MethodHandle.invokeWithArguments است. نتیجهی اجرای این متد معادل invoke است، با این تفاوت که تمام آرگومانها میتوانند به صورت یک آرایه یا لیستی از شیءها (Objects) ارائه شوند.
یک ویژگی جالب این متد این است که اگر تعداد آرگومانهای ارائهشده بیشتر از تعداد مورد انتظار باشد، تمام آرگومانهای اضافی در آرگومان آخر فشرده (Squash) میشوند و به عنوان یک آرایه با آنها رفتار خواهد شد.
امکان ساخت متد هندلهایی که دسترسی خواندن (Read) یا نوشتن (Write) روی فیلدها داشته باشند وجود دارد. برای فیلدهای نمونه (Instance Fields)، این کار از طریق متدهای findGetter و findSetter و برای فیلدهای ایستا (Static Fields)، از طریق متدهای findStaticGetter و findStaticSetter انجام میشود. در اینجا نیازی به ارائه نمونهای از MethodType نیست؛ به جای آن، فقط کافی است یک تایپ واحد که همان نوعِ (Type) فیلد است را مشخص کنیم.
برای مثال، فرض کنید یک فیلد استاتیک به نام magic در کلاس Example داریم:
private static String magic = "initial value static field";
با فرض اینکه یک شیء Lookup ساختهایم:
MethodHandles.Lookup lookup = MethodHandles.lookup();
میتوانیم به راحتی متد هندلهای Setter و Getter را بسازیم و آنها را جداگانه فراخوانی کنیم:
MethodHandle setterStaticMethodHandle = lookup.findStaticSetter(Example.class, "magic", String.class);
MethodHandle getterStaticMethodHandle = lookup.findStaticGetter(Example.class, "magic", String.class);
setterStaticMethodHandle.invoke("new value static field");
String staticFieldResult = (String) getterStaticMethodHandle.invoke(); // staticFieldResult == `new value static field`
در ادامه، یک فیلد نمونه به نام abc در کلاس Example داریم:
private String abc = "initial value";
به همین ترتیب میتوانیم متد هندلهایی برای خواندن و نوشتن در این فیلد نمونه بسازیم:
MethodHandle setterMethodHandle = lookup.findSetter(Example.class, "abc", String.class);
MethodHandle getterMethodHandle = lookup.findGetter(Example.class, "abc", String.class);
برای استفاده از این متد هندلها روی یک فیلد نمونه، ابتدا باید یک نمونه (Instance) از کلاسی که فیلد به آن تعلق دارد به دست آوریم:
Example example = new Example();
سپس باید نمونهی ساختهشده از Example را هنگام فراخوانی Setter و Getter به عنوان پارامتر پاس دهیم:
setterMethodHandle.invoke(example, "new value");
String result = (String) getterMethodHandle.invoke(example); // result == `new value`
اگرچه خواندن و نوشتن مقادیر فیلدها با استفاده از متد هندلها امکانپذیر است، اما این یک روش رایج نیست. برای کار با فیلدها، استفاده از VarHandles مناسبتر است که میتوان آنها را با متدهای findVarHandle و findStaticVarHandle ایجاد کرد.
کلاس MethodHandles حاوی متدهایی است که متد هندلهای پیشفرضی را در اختیار ما میگذارند. اینها شامل متد هندلهایی میشوند که امکان دستکاری و کار روی آرایهها را فراهم میکنند. ساخت این متد هندلها نیازی به بررسی سطح دسترسی (Access Checking) ندارد، بنابراین به شیء Lookup نیازی نخواهیم داشت.
بیایید با استفاده از arrayConstructor یک آرایه از نوع String با ۵ عضو بسازیم:
MethodHandle arrayConstructor = MethodHandles.arrayConstructor(String[].class);
String[] arr = (String[]) arrayConstructor.invoke(5);
برای تغییر دادن یک عنصر خاص، میتوانیم از arrayElementSetter استفاده کنیم که ارجاع به آرایه هدف، اندیس عنصر و مقدار جدید را به آن میدهیم:
MethodHandle elementSetter = MethodHandles.arrayElementSetter(String[].class);
elementSetter.invoke(arr, 4, "test");
برای خواندن مقدار یک عنصر، باید از متد هندل arrayElementGetter استفاده کنیم که ارجاع به آرایه و اندیس عنصر را به آن پاس میدهیم:
MethodHandle elementGetter = MethodHandles.arrayElementGetter(String[].class);
String element = (String) elementGetter.invoke(arr, 4); // element == "test"
همچنین میتوانیم از متد هندلی که توسط arrayLength ارائه میشود استفاده کنیم تا طول آرایه را به عنوان یک عدد صحیح (Integer) دریافت کنیم:
MethodHandle arrayLength = MethodHandles.arrayLength(String[].class);
int length = (int) arrayLength.invoke(arr); // length == 5
هر دو متد invokeExact و invoke خطای Throwable پرتاب میکنند، بنابراین هیچ محدودیتی برای آنچه متد زیرین میتواند پرتاب کند وجود ندارد. متدی که یک متد هندل را فراخوانی میکند باید استثنای Throwable را صریحاً پرتاب کند (با throws) یا آن را مدیریت کند (با try-catch).
متدهای خاصی در API مربوط به MethodHandles وجود دارند که میتوانند مدیریت استثناها را آسانتر کنند. در ادامه به چند مثال نگاهی میاندازیم.
متد MethodHandles.catchException میتواند یک متد هندل مشخص را درون یک متد هندل دیگر که نقش handler (مدیریتکننده استثنا) دارد، بپوشاند (Wrap کند).
فرض کنید متدی به نام problematicMethod داریم که برخی منطقهای تجاری (Business Logic) را انجام میدهد، و متدی به نام exceptionHandler که یک استثنای خاص مانند IllegalArgumentException را مدیریت میکند. متد handler باید نوع خروجی یکسانی با متد اصلی داشته باشد. اولین آرگومانی که میپذیرد همان Throwable مورد نظر ماست و بعد از آن بقیه آرگومانهایی میآیند که متد اصلی میپذیرفت:
public static int problematicMethod(String argument) throws IllegalArgumentException {
if ("invalid".equals(argument)) {
throw new IllegalArgumentException();
}
return 1;
}
public static int exceptionHandler(IllegalArgumentException e, String argument) {
// ثبت و لوگ کردن استثنا
return 0;
}
میتوانیم متد هندلهای هر دو متد را پیدا کرده و problematicMethod را درون exceptionHandler بپوشانیم. متد هندل به دست آمده (wrapped) هنگام فراخوانی، استثنای IllegalArgumentException را به درستی مدیریت میکند و در صورت بروز استثناهای دیگر، آنها را کماکان پرتاب خواهد کرد:
MethodHandles.Lookup lookup = MethodHandles.lookup();
MethodHandle methodHandle = lookup.findStatic(Example.class, "problematicMethod", MethodType.methodType(int.class, String.class));
MethodHandle handler = lookup.findStatic(Example.class, "exceptionHandler",
MethodType.methodType(int.class, IllegalArgumentException.class, String.class));
MethodHandle wrapped = MethodHandles.catchException(methodHandle, IllegalArgumentException.class, handler);
IO.println(wrapped.invoke("valid")); // خروجی: "1"
IO.println(wrapped.invoke("invalid")); // خروجی: "0"
متد MethodHandles.tryFinally به شکلی مشابه عمل میکند، اما به جای یک handler برای استثنا، متد هدف را با اضافه کردن یک بلوک try-finally میپوشاند.
فرض کنید یک متد جداگانه به نام cleanupMethod داریم که حاوی منطق پاکسازی (Cleanup) است. نوع خروجی این متد باید با نوع خروجی متد هدف یکسان باشد. این متد باید یک Throwable بپذیرد، به دنبال آن مقدار خروجی حاصل از متد هدف، و سپس تمام آرگومانها:
public static int cleanupMethod(Throwable e, int result, String argument) {
IO.println("inside finally block");
return result;
}
میتوانیم متد هندلِ مثال قبل را درون بلوک try-finally به شکل زیر بپوشانیم:
MethodHandle cleanupMethod = lookup.findStatic(Example.class, "cleanupMethod",
MethodType.methodType(int.class, Throwable.class, int.class, String.class));
MethodHandle wrappedWithFinally = MethodHandles.tryFinally(methodHandle, cleanupMethod);
IO.println(wrappedWithFinally.invoke("valid")); // چاپ میکند: "inside finally block" و سپس "1"
IO.println(wrappedWithFinally.invoke("invalid")); // چاپ میکند: "inside finally block" و خطای java.lang.IllegalArgumentException پرتاب میکند
همانطور که در مثالهای قبلی دیدیم، متد هندلها میتوانند رفتاری فراتر از صرفاً اشاره به یک متد زیرین را درون خود جای دهند. ما میتوانیم متد هندلهای آداپتور (Adapter) را به دست آوریم؛ این آداپتورها متد هندلِ هدف را میپوشانند (Wrap میکنند) تا رفتارهای خاصی مثل تغییر ترتیب آرگومانها، مقداردهی اولیه به برخی آرگومانها (Pre-inserting) یا فیلتر کردن مقادیر بازگشتی را به آن اضافه کنند.
بیایید نگاهی به چند مورد از این تغییر شکلها بیندازیم.
نوعِ (Type) یک متد هندل را میتوان با استفاده از متد asType به یک نوع جدید تبدیل کرد. اگر چنین تبدیلی غیرممکن باشد، استثنای WrongMethodTypeException دریافت خواهیم کرد. به یاد داشته باشید وقتی از تغییر شکلها استفاده میکنیم، در واقع دو متد هندل داریم که در آن، متد هندل اصلی درون منطقِ اضافی دیگری پیچیده شده است. در این حالت، پوششدهنده (Wrapper) آرگومانها را دریافت کرده و تلاش میکند آنها را با آرگومانهای متد هندل اصلی منطبق کند. به محض اینکه متد هندل اصلی کار خود را انجام داد و نتیجه را بازگرداند، این Wrapper تلاش میکند تا نتیجه را به نوع دادهشده تبدیل (Cast) کند.
فرض کنید یک متد تست داریم که یک Object میپذیرد و یک String برمیگرداند. میتوانیم این متد را طوری تغییر دهیم که نوع آرگومانِ مشخصتری مانند String را بپذیرد:
MethodHandle targetMethodHandle = lookup.findStatic(Example.class, "test",
MethodType.methodType(String.class, Object.class));
MethodHandle adapter = targetMethodHandle.asType(
MethodType.methodType(String.class, String.class));
String originalResult = (String) targetMethodHandle.invoke(111); // کار میکند
String adapterResult = (String) adapter.invoke("aaaaaa"); // کار میکند
adapterResult = (String) adapter.invoke(111); // شکست میخورد
در واقع، هر بار که از متد invoke روی یک MethodHandle استفاده میکنیم، اولین اتفاقی که میافتد فراخوانی asType است. متد invoke آرگومانهای خود را به صورت Object میگیرد و برمیگرداند، که سپس تلاش میشود آنها به انواع مشخصتر تبدیل شوند. این انواع مشخص از کد ما استخراج میشوند (یعنی مقادیر دقیقی که به عنوان آرگومان پاس میدهیم و نوعی که مقدار بازگشتی را به آن تبدیل میکنیم). به محض اینکه نوعها با موفقیت تبدیل شدند، متد invokeExact برای این نوعهای مشخص فراخوانی میشود.
برای به دست آوردن یک متد هندل آداپتور با آرگومانهای جابهجاشده، میتوانیم از MethodHandles.permuteArguments استفاده کنیم.
برای مثال، بیایید یک متد تست بسازیم که مجموعهای از آرگومانها با انواع مختلف را میپذیرد:
public static void test(int v1, String v2, long v3, boolean v4) {
IO.println(v1 + v2 + v3 + v4);
}
و یک متد هندل برای آن پیدا کنیم:
MethodHandle targetMethodHandle = lookup.findStatic(Example.class, "test",
MethodType.methodType(void.class, int.class, String.class, long.class, boolean.class));
متد permuteArguments ورودیهای زیر را میپذیرد:
متد هندل هدف: در مثال ما، متدی که به متد test اشاره میکند.
یک MethodType جدید: همراه با تمام آرگومانها که به شکل مورد نظر تغییر ترتیب یافتهاند.
یک آرایه از اندیسها: که نشاندهنده ترتیب جدید آرگومانهاست.
MethodHandle reversedArguments = MethodHandles.permuteArguments(targetMethodHandle,
MethodType.methodType(void.class, boolean.class, long.class, String.class, int.class), 3, 2, 1, 0);
reversedArguments.invoke(false, 1L, "str", 123); // خروجی: "123str1false"
متد MethodHandles.insertArguments یک متد هندل ارائه میدهد که یک یا چند آرگومانِ آن از قبل مقداردهی و تثبیت شدهاند (Bound شدهاند).
برای مثال، بیایید دوباره به متد هندل مثال قبل نگاه کنیم:
MethodHandle targetMethodHandle = lookup.findStatic(Example.class, "test",
MethodType.methodType(void.class, int.class, String.class, long.class, boolean.class));
میتوانیم به راحتی یک متد هندل آداپتور دریافت کنیم که آرگومانهای String و long آن از قبل مقداردهی شدهاند:
MethodHandle boundArguments = MethodHandles.insertArguments(targetMethodHandle, 1, "new", 3L);
برای فراخوانی متد هندلِ حاصل، فقط باید آرگومانهایی را ارائه دهیم که از قبل پر نشدهاند:
boundArguments.invoke(1, true); // خروجی: "1new3true"
اگر تلاش کنیم آرگومانهایی که از قبل مقداردهی شدهاند را دوباره پاس دهیم، کد ما با استثنای WrongMethodTypeException شکست خواهد خورد.
میتوانیم از MethodHandles.filterArguments برای اعمال تغییرات روی آرگومانها قبل از فراخوانی متد هندل هدف استفاده کنیم. برای اینکه این کار انجام شود، باید موارد زیر را ارائه دهیم:
متد هندل هدف
موقعیت (اندیس) اولین آرگومانی که قرار است تغییر کند
متد هندلهای مربوط به تغییرِ هر آرگومان
اگر برخی آرگومانها نیازی به تغییر ندارند، میتوانیم با پاس دادن مقدار null از آنها صرفنظر کنیم. همچنین اگر فقط نیاز به تغییر بخشی از آرگومانها داریم، میتوانیم کلاً از بقیه آرگومانها بگذریم.
بیایید از متد هندل بخش قبل دوباره استفاده کنیم و برخی از آرگومانهای آن را قبل از فراخوانی فیلتر کنیم:
MethodHandle targetMethodHandle = lookup.findStatic(Example.class, "test",
MethodType.methodType(void.class, int.class, String.class, long.class, boolean.class));
سپس متدی میسازیم که هر مقدار boolean را نقیض (معکوس) میکند:
private static boolean negate(boolean original) {
return !original;
}
و متد دیگری میسازیم که یک واحد به هر عدد صحیح میافزاید:
private static int increment(int original) {
return ++original;
}
میتوانیم متد هندلهای مربوط به این متدهای تغییردهنده را دریافت کنیم:
MethodHandle negate = lookup.findStatic(Example.class, "negate", MethodType.methodType(boolean.class, boolean.class));
MethodHandle increment = lookup.findStatic(Example.class, "increment", MethodType.methodType(int.class, int.class));
و از آنها برای دریافت یک متد هندل جدید با آرگومانهای فیلترشده استفاده کنیم:
// فیلتر 'increment' را روی آرگومان اندیس 0 و فیلتر 'negate' را روی آرگومان آخر اعمال میکند
// و نتیجه را به 'targetMethodHandle' پاس میدهد
MethodHandle withFilters = MethodHandles.filterArguments(targetMethodHandle, 0, increment, null, null, negate);
withFilters.invoke(3, "abc", 5L, false); // خروجی: "4abc5true"
وقتی میخواهیم قبل از فراخوانی یک متد هندل، عملیات پیشپردازش (Pre-processing) روی یک یا چند آرگومان انجام دهیم، میتوانیم از MethodHandles.foldArguments استفاده کنیم و متد هندلِ یک متد ترکیبکننده (Combiner) را به آن بدهیم تا آرگومانها را از یک موقعیت مشخص پردازش کند.
فرض کنید متد هدف ما این است:
private static void target(int ignored, int sum, int a, int b) {
System.out.printf("%d + %d equals %d and %d is ignored%n", a, b, sum, ignored);
}
با استفاده از foldArguments میتوانیم بخشی از آرگومانها را پیشپردازش کرده، مقدار حاصل را به عنوان یک آرگومان دیگر وارد کنیم و سپس به سراغ اجرای متد هدف برویم.
در مثال ما، آرگومانهای int a و int b در انتهای پارامترها قرار دارند. ما میتوانیم هر تعداد از آرگومانها را پیشپردازش کنیم، اما همه آنها باید در انتهای لیست باشند. فرض کنید میخواهیم مجموع این دو مقدار a و b را محاسبه کنیم، پس متدی برای آن میسازیم:
private static int sum(int a, int b) {
return a + b;
}
مقدار حاصل دقیقاً کجا قرار میگیرد؟ این مقدار در یکی از آرگومانهای متد هدف ما قرار داده میشود. این آرگومان باید دقیقاً قبل از آرگومانهایی باشد که قرار است ادغام (Fold) شوند؛ بنابراین در مثال ما، همان آرگومان int sum است. آرگومانی که برای نتیجهی ترکیب اختصاص داده شده نمیتواند در جای دیگری باشد. اگر متد هدف نیاز به پذیرش آرگومانهای بیشتری داشته باشد که ربطی به این منطق ترکیب ندارند، همه آنها باید در ابتدا قرار بگیرند.
بیایید متد هندلها را بسازیم و ببینیم چگونه باید آنها را ترکیب کنیم:
MethodHandle targetMethodHandle = lookup.findStatic(Example.class, "target",
MethodType.methodType(void.class, int.class, int.class, int.class, int.class));
MethodHandle combinerMethodHandle = lookup.findStatic(Example.class, "sum",
MethodType.methodType(int.class, int.class, int.class));
MethodHandle preProcessedArguments = MethodHandles.foldArguments(targetMethodHandle, 1, combinerMethodHandle);
متد foldArguments ورودیهای زیر را میپذیرد:
MethodHandle target: متد هندل هدف (در اینجا متد target).
int pos: یک عدد صحیح که موقعیت شروع آرگومانهای مربوط به ترکیب را مشخص میکند. در مثال ما، آرگومان sum در موقعیت اندیس 1 قرار دارد، بنابراین عدد 1 را پاس دادیم. اگر این مقدار را حذف کنیم، پیشفرض آن 0 خواهد بود.
MethodHandle combiner: متد هندل ترکیبکننده (در اینجا متد sum).
در نهایت، میتوانیم متد هندل حاصل را فراخوانی کرده و تمام آرگومانها را به جز sum (که به صورت خودکار محاسبه میشود) پاس دهیم:
preProcessedArguments.invokeExact(10000, 1, 2); // خروجی: "1 + 2 equals 3 and 10000 is ignored"
این امکان وجود دارد که متد ترکیبکننده (Combiner) مقادیر را پردازش کند اما هیچ خروجی بازنگرداند (void). در این حالت، نیازی به قرار دادن متغیری برای ذخیره نتیجه در لیست آرگومانهای متد هدف نیست.
مشابه آرگومانها، میتوانیم از آداپتوری استفاده کنیم که تغییرات را روی مقدار بازگشتی (Return Value) اعمال کند.
موقعیتی را تصور کنید که متدی داریم که یک String برمیگرداند و میخواهیم هر مقدار بازگشتی از این متد را هدایت کنیم به متد دیگری که کاراکتر d را با m جایگزین کرده و حروف را بزرگ (Uppercase) میکند.
در اینجا متد هندل برای متد getSomeString آمده است که همیشه مقدار "dummy" را برمیگرداند:
MethodHandle getSomeString = lookup.findStatic(Example.class, "getSomeString", MethodType.methodType(String.class));
این هم متد resultTransform که تغییرات را انجام میدهد:
private static String resultTransform(String value) {
return value.replace('d', 'm').toUpperCase();
}
و این متد هندلِ مربوط به متد تغییردهنده ماست:
MethodHandle resultTransform = lookup.findStatic(Example.class, "resultTransform", MethodType.methodType(String.class, String.class));
در نهایت، این ترکیبِ دو متد هندل است که در آن، نتیجهی بازگشتی از getSomeString به متد resultTransform داده شده و طبق آن تغییر میکند:
MethodHandle getSomeUppercaseString = MethodHandles.filterReturnValue(getSomeString, resultTransform);
IO.println(getSomeUppercaseString.invoke()); // خروجی: "MUMMY"
متد هندلها در JDK 7 به عنوان ابزاری برای کمک به توسعهدهندگان کامپایلر و محیطهای اجرا (Runtime) زبان معرفی شدند. آنها هرگز برای جایگزینی کامل ریفلکشن ساخته نشدهاند.
API ریفلکشن چیزی ارائه میدهد که متد هندلها نمیتوانند: امکان لیست کردن اعضای کلاس (Class Members) و بررسی ویژگیها و مشخصات آنها (Inspection). در مقابل، متد هندلها میتوانند به گونهای تغییر شکل یافته و دستکاری شوند که در API ریفلکشن امکانپذیر نیست.
وقتی صحبت از فراخوانی متدها میشود، تفاوتهایی در زمینه بررسی سطح دسترسی و ملاحظات امنیتی وجود دارد. API ریفلکشن در هر بار فراخوانی، سطح دسترسی کد فراخواننده را بررسی میکند؛ اما در متد هندلها، بررسی دسترسی تنها در زمان ساخت انجام میشود. همین امر فراخوانی از طریق متد هندلها را سریعتر از ریفلکشن میکند. با این حال، باید احتیاطهای لازم صورت گیرد تا متد هندل به کدهایی که نباید به آن دسترسی داشته باشند، منتقل نشود.
از شیء Lookup میتوان برای تبدیل اشیای API ریفلکشن به متد هندلهای معادل (از نظر رفتاری) استفاده کرد که دسترسی مستقیمتر و کارآمدتری به اعضای کلاس فراهم میکنند.
برای ساخت یک متد هندل اشارهکننده به یک Method مشخص (با فرض اینکه کلاس lookup مجوز آن را داشته باشد)، میتوانیم از unreflect استفاده کنیم.
فرض کنید یک متد تست در کلاس Example داریم که یک آرگومان String گرفته و یک String برمیگرداند. با استفاده از API ریفلکشن، میتوانیم شیء Method را به دست آوریم:
Method method = Example.class.getMethod("test", String.class);
به کمک شیء lookup، میتوانیم شیء Method را unreflect کنیم تا یک MethodHandle به دست آوریم:
MethodHandles.Lookup lookup = MethodHandles.lookup();
MethodHandle methodHandle = lookup.unreflect(method);
String result = (String) methodHandle.invoke("something");
به همین ترتیب، با داشتن شیء Field میتوانیم متد هندلهای getter و setter را به دست آوریم:
Field field = Example.class.getField("magic");
MethodHandle setterMethodHandle = lookup.unreflectSetter(field);
MethodHandle getterMethodHandle = lookup.unreflectGetter(field);
setterStaticMethodHandle.invoke("something"); // توجه: متغیر setterMethodHandle
String result = (String) getterMethodHandle.invoke(); // result == "something"
تبدیل از MethodHandle به یک Member (مثل Method یا Field) نیز امکانپذیر است؛ به شرطی که هیچ تغییر شکلی (Transformation) روی آن MethodHandle انجام نشده باشد.
فرض کنید یک متد هندل داریم که مستقیماً به یک متد اشاره میکند. میتوانیم از متد MethodHandles.reflectAs برای به دست آوردن شیء Method استفاده کنیم:
Method method = MethodHandles.reflectAs(Method.class, methodHandle);
این روند برای شیء Field نیز به همین شکل کار میکند:
Field field = MethodHandles.reflectAs(Field.class, getterMethodHandle); // نتیجه مشابهی با reflect کردن setterMethodHandle به دست میآید
در این آموزش، مکانیسم متد هندل (Method Handle) را بررسی کردیم و یاد گرفتیم که چگونه به شکل کارآمد از آن استفاده کنیم. اکنون میدانیم که متد هندلها راهکاری برای فراخوانی سریع و کارآمد متدها ارائه میدهند، اما قرار نیست جایگزین API ریفلکشن شوند.
متد هندلها به دلیل روش متفاوت در بررسی سطح دسترسی، عملکرد (Performance) بهتری در فراخوانی متدها دارند. با این حال، از آنجا که دسترسی فقط هنگام ساخت متد هندل بررسی میشود، باید در انتقال و اشتراکگذاری آنها احتیاط کرد.
برخلاف API ریفلکشن، متد هندلها ابزاری برای مشاهده اعضای کلاس و بررسی ویژگیهای آنها ارائه نمیدهند. از طرف دیگر، API متد هندل به ما اجازه میدهد اشارهگرهای مستقیم به متدها و فیلدها را درون منطقهای پیچیدهتر — مانند دستکاری آرگومانها و مقادیر بازگشتی — بپوشانیم و ترکیب کنیم.
این محتوا کاملا رایگان توسط تیم کدلپر ترجمه شده و در اختیار شما کاربران عزیز قرار گرفته است، هر گونه کپی برداری برای مقاصد غیر رایگان و بدون ذکر منبع، مورد پیگیری قانونی قرار میگیرد.
ترجمه شده از منبع: https://dev.java/learn/