Java 8, самой заметной фичей которой стало появление лямбд, вышла два года назад, а в этом году мы даже начали её использовать в продакшен коде Идеи. Такое заметное нововведение в языке вызывает множество вопросов. Какие возможности перед нами открываются и какие проблемы при неаккуратном использовании лямбд могут возникнуть, как лямбды устроены внутри, во что они компилируются и как исполняются — вот темы, которые мы обсудим на докладе.
6. Специализации для примитивов
43 интерфейса в java.util.function: *Supplier, *Consumer, *Predicate, *Operator,
*Function.
Есть все варианты
(void|int|long|double|T) -> (void|boolean|int|long|double|T)
И ещё несколько для функций с двумя параметрами.
6
14. Обычно легко исправить
Добавление ? extends и ? super в параметры вызываемых методов не
ломает ни binary-compatibility, ни source-compatibility. (Если у них нет
наследников.)
14
16. Когда-то мы писали так
void print(List<String> strings) {
for (int i = 0; i < strings.size(); i++) {
String s = strings.get(i);
System.out.println(s);
}
}
16
17. Теперь пишем так
void print(List<String> strings) {
for (String s : strings) {
System.out.println(s);
}
}
17
18. А если что-то более сложное?
void move(List<PsiClass> classes) {
boolean containsAnonymous = false;
for (PsiClass aClass : classes) {
if (aClass instanceof PsiAnonymousClass) {
containsAnonymous = true;
break;
}
}
...
}
18
19. Теперь можно написать так
void move(List<PsiClass> classes) {
boolean containsAnonymous =
classes.stream().anyMatch(
c -> c instanceof PsiAnonymousClass
);
...
}
19
20. Код может быть и более сложным
void move(List<PsiClass> classes) {
int numberOfAnonymouses = 0;
for (PsiClass aClass : classes) {
if (aClass instanceof PsiAnonymousClass) {
numberOfAnonymouses++;
if (numberOfAnonymouses >= 2) {
break;
}
}
}
...
}
20
21. И его все равно можно упростить
void move(List<PsiClass> classes) {
long numberOfAnonymouses = classes.stream()
.filter(c -> c instanceof PsiAnonymousClass)
.limit(2)
.count();
...
}
21
22. Всегда ли Stream API упрощает?
void foo(List<PsiMethod> methods) {
List<PsiMethod> constructors =
methods.stream().filter(PsiMethod::isConstructor)
.collect(Collectors.toList());
}
void foo(List<PsiMethod> methods) {
List<PsiMethod> constructors =
ContainerUtil.filter(methods, PsiMethod::isConstructor);
}
22
23. Всегда ли Stream API упрощает?
void foo(List<PsiMethod> methods) {
List<PsiMethod> constructors =
methods.stream().filter(PsiMethod::isConstructor)
.collect(Collectors.toList());
}
void foo(List<PsiMethod> methods) {
List<PsiMethod> constructors =
ContainerUtil.filter(methods, PsiMethod::isConstructor);
}
23
24. Что делает этот метод?
Object bar(PsiElement element) {
return
Optional.ofNullable(element.getContainingFile())
.map(PsiFile::getVirtualFile)
.map(f -> ModuleUtilCore.findModuleForFile(f,
element.getProject()))
.orElse(null);
}
24
25. Это может быть не сразу очевидно
Module findModule(PsiElement element) {
return
Optional.ofNullable(element.getContainingFile())
.map(PsiFile::getVirtualFile)
.map(f -> ModuleUtilCore.findModuleForFile(f,
element.getProject()))
.orElse(null);
}
25
26. Тот же результат
Module findModule(PsiElement element) {
PsiFile file = element.getContainingFile();
if (file == null) return null;
VirtualFile virtualFile = file.getVirtualFile();
if (virtualFile == null) return null;
return ModuleUtilCore.findModuleForFile(virtualFile,
element.getProject());
}
26
28. Сокращается ещё больше
return element instanceof PySubscriptionExpression
&& keywordContainerName.equals(
((PySubscriptionExpression)element).getOperand().getText());
28
30. Часто есть несколько вариантов
void foo(List<VirtualFile> files) {
...
files.stream().map(VirtualFile::getFileType)
.anyMatch(StdFileTypes.XML::equals)
files.stream().map(VirtualFile::getFileType)
.anyMatch(Predicate.isEqual(StdFileTypes.XML));
files.stream().anyMatch(file ->
file.getFileType().equals(StdFileTypes.XML));
...
}
30
31. Легко получить запутанный код
Arrays.stream(PhpLibraryRoot. EP_NAME.getExtensions()).
map(PhpLibraryRoot::getProvider).
filter(PhpLibraryRootProvider::isRuntime).
map(provider -> provider.getLibraryRoot(getProject())).
filter(Optional::isPresent).
map(Optional::get).
map(VirtualFile::getChildren).
map(Arrays:: asList).
flatMap(Collection::stream).
filter(VirtualFile::isDirectory).
filter(module -> ! ".idea".equals(module.getName())).
...
31
42. Таких случаев много
public abstract class NotNullLazyValue<T> {
private T myValue;
@NotNull
protected abstract T compute();
...
}
42
43. Теперь можно создать без наследования
public abstract class NotNullLazyValue<T> {
...
@NotNull
public static <T> NotNullLazyValue<T>
createValue(@NotNull NotNullFactory<T> value) {
...
};
}
43
44. Есть более сложные случаи
Класс FileReferenceSet, больше 100 наследников, половина из них –
анонимные классы, 23 метода, которые переопределяются в наследниках.
44
45. Выглядят вот так
return new FileReferenceSet(...) {
protected boolean isSoft() { return soft; }
public boolean isAbsolutePathReference() { return true; }
public boolean couldBeConvertedTo(boolean relative) {...}
public boolean absoluteUrlNeedsStartSlash() {
String s = getPathString();
return s != null && !s.isEmpty() && s.charAt(0) == '/';
}
public Collection<PsiFileSystemItem>
computeDefaultContexts() {...}
}.getAllReferences();
45
47. Есть ли здесь утечка памяти?
public class Foo {
void foo() {
Runnable r = new Runnable() {
public void run() {
System.out.println("Bye!");
}
};
Runtime.getRuntime()
.addShutdownHook(new Thread(r));
}
...
}
47
48. Да, есть ссылка на внешний объект
class Foo$1 implements Runnable {
final Foo this$0;
public Foo$1(Foo foo) {
this$0 = foo;
}
...
}
48
49. Экземпляр создаётся каждый раз
ContainerUtil.filter(methods,
new Condition<PsiMethod>() {
public boolean value(PsiMethod psiMethod) {
return psiMethod.isConstructor();
}
}
);
49
50. Во что компилировать лябмды?
public class Simple {
public static void main(String[] args) {
Runnable r =
() -> System.out.println("Hello");
new Thread(r).start();
}
}
50
51. Самая простая реализация – анонимус
public class Simple {
public static void main(String[] args) {
Runnable r = new Runnable() {
public void run() {
System.out.println("Hello");
}
}
new Thread(r).start();
}
}
51
52. У такой реализации есть недостатки
● лишний class-файл на диске
● каждый раз создаётся новый экземпляр
● Class не будет собран в мусор, пока достижим его ClassLoader
52
54. Нужен ли вообще класс для лямбды?
Да, ведь можно вызвать r.getClass().
r.getClass().getName()вернёт что-то вроде
"Simple$$Lambda$1/1149319664"
Unsafe.getUnsafe().defineAnonymousClass(...)
54
56. Кто будет генерировать код, создающий лямбду?
Записывать этот код в каждый класс неэффективно. Требуется общий метод
в стандартной библиотеке, который будет создавать все классы лямбд.
Назовём его LambdaMetafactory.metafactory.
Но в него надо как-то передавать информацию про то, какой метод какого
интерфейса должна реализовывать лямбда, и код её тела.
56
57. Для начала сделаем desugaring
public class Simple {
public static void main(String[] args) {
Runnable r = Simple::lambda$main$0;
new Thread(r).start();
}
private static void lambda$main$0() {
System.out.println("Hello");
}
}
57
58. MethodHandle
public abstract class MethodHandle {
@PolymorphicSignature
public final native Object invoke(Object... args)
throws Throwable;
public MethodType type() { ... }
...
}
public class MethodType {
public static MethodType methodType(Class<?> rtype) {...}
public static MethodType methodType(Class<?> rtype,
Class<?> ptype0) {...}
...
}
58
59. Отличия от java.lang.reflect.Method
● проверка доступа в момент создания, а не вызова
● работают не только с методами, но и с полями, конструкторами, …
● могут делать преобразования параметров и возвращаемого значения
● работают с примитивными типами напрямую, без заворачивания в
объекты
● не создаётся массив для передачи аргументов
59
60. Реализация лямбды, попытка 1
public class LambdaMetafactory {
public static Object metafactory0(
String samMethodName,
Class<?> samType,
MethodType samMethodType,
MethodHandle implMethod) {
...
Class cls = Unsafe.getUnsafe()
.defineAnonymousClass(...);
return cls.newInstance();
}
} 60
61. Реализация лямбды, попытка 1
public class Simple {
public static void main(String[] args) throws Throwable {
MethodHandle implMethod =
MethodHandles.lookup().findStatic(Simple.class,
"lambda$main$0", MethodType.methodType(void.class));
MethodType samMethodType = MethodType.methodType(void.class);
Runnable r = (Runnable) LambdaMetafactory.metafactory0(
"run", Runnable.class, samMethodType, implMethod);
new Thread(r).start();
}
private static void lambda$main$0() {
System.out.println("Hello");
}
}
61
62. Попытка 2: кэшируем лямбду
public class Simple {
private static Object[] lambdas = new Object[1];
public static void main(String[] args) throws Throwable {
if (lambdas[0] == null) {
...
lambdas[0] = LambdaMetafactory.metafactory0(
"run", Runnable.class, samMethodType, implMethod);
}
Runnable r = (Runnable) lambdas[0];
new Thread(r).start();
}
...
}
62
63. CallSite
public abstract class CallSite {
public abstract MethodHandle getTarget();
...
}
public class ConstantCallSite extends CallSite {
public ConstantCallSite(MethodHandle target) {...}
}
63
64. Попытка 3: invokeDynamic
public class Simple {
private static CallSite[] callSites = new CallSite[1];
public static void main(String[] args) throws Throwable {
if (callSites[0] == null) {
MethodHandle implMethod = MethodHandles. lookup().findStatic(
Simple. class, "lambda$main$0", MethodType.methodType(void.class));
MethodType samMethodType = MethodType. methodType(void.class);
callSites[0] = LambdaMetafactory. metafactory(
MethodHandles. lookup(),
"run", MethodType.methodType(Runnable.class),
samMethodType, implMethod, samMethodType);
}
Runnable r = (Runnable) callSites[0].getTarget().invoke();
...
}
...
64
65. Попытка 3: invokeDynamic
public class Simple {
private static CallSite[] callSites = new CallSite[1];
public static void main(String[] args) throws Throwable {
if (callSites[0] == null) {
MethodHandle implMethod = MethodHandles. lookup().findStatic(
Simple.class, "lambda$main$0", MethodType.methodType(void.class));
MethodType samMethodType = MethodType.methodType(void.class);
callSites[0] = LambdaMetafactory. metafactory(
MethodHandles. lookup(),
"run", MethodType.methodType(Runnable.class),
samMethodType, implMethod, samMethodType);
}
Runnable r = (Runnable) callSites[0].getTarget().invoke() ;
...
}
...
65
73. Утечки памяти нет
public class Foo {
void foo() {
Runnable r =() -> System.out.println("Bye!");
Runtime.getRuntime()
.addShutdownHook(new Thread(r));
}
...
}
73
75. Это может приводить к проблемам
public interface Disposable {
void dispose();
}
public class Disposer {
public static void register(Disposable parent,
Disposable child) {
...
map.put(parent, child);
}
}
75
76. Если лямбды сравниваются как объекты
private final Disposable parent = new Disposable() {
public void dispose() { }
};
void foo() {
Disposer.register(parent, ...);
}
private final Disposable parent = () -> { };
void foo() {
Disposer.register(parent, ...);
}
76
78. Ссылки
Refactoring to Functional Style with Java 8 by Venkat Subramaniam
Stream API, часть 1, Сергей Куксенко
Stream API: Tagir Valeev
Translation of Lambda Expressions by Brian Goetz
Глубокое погружение в invokedynamic, Владимир Иванов
78
79. Итоги
● используйте лямбды и стримы, чтобы писать более высокоуровневый и
понятный код
● но не увлекайтесь, функциональный стиль – не самоцель, а лишь
средство
● изучайте, как это устроено; особенно если натыкаетесь на непонятное
поведение
79