Ви повинні розрізняти часті виконання одного і того ж сайту виклику , для лямбда або лямбди без стану, і часте використання методу-посилання на один і той же метод (різними сайтами викликів).
Подивіться на такі приклади:
Runnable r1=null;
for(int i=0; i<2; i++) {
Runnable r2=System::gc;
if(r1==null) r1=r2;
else System.out.println(r1==r2? "shared": "unshared");
}
Тут один і той же виклик виконується два рази, створюючи лямбду без стану, і поточна реалізація друкується "shared"
.
Runnable r1=null;
for(int i=0; i<2; i++) {
Runnable r2=Runtime.getRuntime()::gc;
if(r1==null) r1=r2;
else {
System.out.println(r1==r2? "shared": "unshared");
System.out.println(
r1.getClass()==r2.getClass()? "shared class": "unshared class");
}
}
У цьому другому прикладі один і той же сайт виклику виконується два рази, створюючи лямбда, що містить посилання на Runtime
екземпляр, і поточна реалізація надрукує, "unshared"
але "shared class"
.
Runnable r1=System::gc, r2=System::gc;
System.out.println(r1==r2? "shared": "unshared");
System.out.println(
r1.getClass()==r2.getClass()? "shared class": "unshared class");
На відміну від цього, в останньому прикладі є два різних сайти викликів, що виробляють еквівалентну посилання на метод, але станом на 1.8.0_05
нього буде надруковано "unshared"
і "unshared class"
.
Для кожного лямбда-виразу або посилання на метод компілятор видасть invokedynamic
інструкцію, яка посилається на метод завантаження JRE у класі LambdaMetafactory
та статичні аргументи, необхідні для створення бажаного класу реалізації лямбда-сигналу. Те, що виробляє мета-фабрика, залишається за фактичним JRE, але це вказана поведінка invokedynamic
інструкції запам'ятовувати та повторно використовувати CallSite
екземпляр, створений під час першого виклику.
Поточний JRE виробляє ConstantCallSite
вміщуючий a MethodHandle
до константного об'єкта для лямбд без громадянства (і немає жодної підстави робити це інакше). А посилання на static
методи завжди не мають стану. Отже, для лямбд без громадянства та окремих сайтів викликів відповідь повинна бути такою: не кешуйте, JVM зробить, а якщо цього не стане, вона повинна мати вагомі причини, проти яких вам не слід протидіяти.
Щодо лямбд, що мають параметри, і this::func
це лямбда, яка має посилання на this
екземпляр, справа йде дещо інакше. JRE дозволено кешувати їх, але це означатиме збереження певного роду Map
між фактичними значеннями параметрів та отриманою лямбда, що може бути дорожчим, ніж просто створення цього простого структурованого екземпляра лямбда знову. Поточний JRE не кешує лямбда-екземпляри, що мають стан.
Але це не означає, що клас лямбда створюється щоразу. Це просто означає, що вирішений виклик-сайт буде поводитися як звичайна конструкція об'єкта, що створює клас лямбда-класу, який був сформований при першому виклику.
Подібні речі застосовуються до посилань на методи до одного і того ж цільового методу, створеного різними сайтами викликів. JRE дозволено ділитися між собою одним лямбда-екземпляром, але в поточній версії цього немає, швидше за все, тому, що незрозуміло, чи окупиться обслуговування кеш-пам’яті. Тут навіть генеровані класи можуть відрізнятися.
Тож кешування, як у вашому прикладі, може змусити вашу програму робити інші речі, ніж без. Але не обов'язково ефективніше. Кешований об'єкт не завжди є більш ефективним, ніж тимчасовий об'єкт. Якщо ви дійсно не вимірюєте вплив на продуктивність, спричинений створенням лямбда, не слід додавати кешування.
Думаю, є лише деякі особливі випадки, коли кешування може бути корисним:
- ми говоримо про безліч різних сайтів викликів, що посилаються на один і той же метод
- лямбда створюється в конструкторі / класі, який ініціалізується, оскільки пізніше на сайті використання буде
- викликатись кількома потоками одночасно
- страждають від нижчої ефективності першого виклику