Ссылки на конструктор
Ссылки на конструктор такие же, как ссылки на метод, за исключением того, что именем метода является new. Например, Button::new является ссылкой на конструктор класса Button. На какой именно конструктор? Это зависит от контекста. Предположим, у вас есть список строк. Затем, вы можете превратить его в массив кнопок, путем вызова конструктора для каждой из строк, с помощью следующего вызова:
List strs = ...;
Stream stream = strs.stream().map(Button::new);
List buttons = stream.collect(Collectors.toList());
Подробная информация о stream, map и методах collect выходит за рамки этой статьи. На данный момент, важно то, что метод map вызывает конструктор Button(String) для каждого элемента списка. Есть несколько конструкторов Button, но компилятор выбирает тот, что с параметром строкового типа, потому что из контекста ясно, что конструктор вызывается со строкой.
Вы можете сформировать ссылки на конструкторы с типом массивов. Например, int[]::new является ссылкой на конструктор с одним параметром: длиной массива. Это равносильно лямбда-выражению x -> new int[x].
Ссылки на конструкторы массива полезны для преодоления ограничений Java. Невозможно создать массив универсального типа T. Выражение new T[n] является ошибкой, так как оно будет заменено new Object[n]. Это является проблемой для авторов библиотек. Например, предположим, мы хотим иметь массив кнопок. Интерфейс Stream имеет метод toArray, возвращающее массив Object:
Object[] buttons = stream.toArray();
Но это неудовлетворительно. Пользователь хочет массив кнопок, а не объектов. Библиотека потоков решает эту проблему за счет ссылок на конструкторы. Передайте Button[]::new методу toArray:
Button[] buttons = stream.toArray(Button[]::new);
Метод toArray вызывает этот конструктор для получения массива нужного типа. Затем он заполняет и возвращает массив.
Область действия переменной
Часто вы хотели бы иметь возможность получить доступ к переменным из охватывающего метода или класса в лямбда-выражении. Рассмотрим следующий пример:
public static void repeatText(String text, int count) {
Runnable r = () -> {
for (int i = 0; i < count; i++) {
System.out.println(text);
Thread.yield();
}
};
new Thread(r).start();
}
Рассмотрим вызов:
repeatText("Hi!", 2000); // Prints Hi 2000 times in a separate thread
Теперь посмотрим на переменные count и text внутри лямбда-выражения. Обратите внимание, что эти переменные не определены в лямбда-выражении. Вместо этого, они являются параметрами метода repeatText.
Если подумать хорошенько, то не очевидно, что здесь происходит. Код лямбда-выражения может выполниться гораздо позже вызова repeatText и переменные параметров уже будут потеряны. Как же переменные text и count остаются доступными?
Чтобы понять, что происходит, мы должны уточнить наши представления о лямбда-выражениях. Лямбда-выражение имеет три компонента:
Значения для свободных переменных; то есть, переменных, которые не являются параметрами и не определены в коде
В нашем примере лямбда-выражение имеет две свободных переменных, text и count. Структура данных, представляющая лямбда-выражение, должна сохранять значения для этих переменных, в нашем случае , "Hi!" и 2000. Будем говорить, что эти значения были захвачены лямбда-выражением. (Это деталь реализации , как это делается. Например, можно преобразовать лямбда-выражение в объект с одним методом, так что значения свободных переменных копируются в переменные экземпляра этого объекта.)
Техническим термином для блока кода вместе со значениями свободных переменных является замыкание. Если кто-то злорадствует, что их язык поддерживает замыкания, будьте уверены, что Java также их поддерживает. В Java лямбда-выражения являются замыканиями. На самом деле, внутренние классы были замыканиями все это время. Java 8 предоставляет нам замыкания с привлекательным синтаксисом.
Как вы видели, лямбда-выражение может захватить значение переменной в охватывающей области. В Java, чтобы убедиться, что захватили значение корректно, есть важное ограничение. В лямбда-выражении можно ссылаться только на переменные, значения которых не меняются. Например, следующий код является неправильным:
public static void repeatText(String text, int count) {
Runnable r = () -> {
while (count > 0) {
count--; // Error: Can't mutate captured variable
System.out.println(text);
Thread.yield();
}
};
new Thread(r).start();
}
Существует причина для этого ограничения. Изменяющиеся переменные в лямбда-выражениях не потокобезопасны. Рассмотрим последовательность параллельных задач, каждая из которых обновляет общий счетчик.
int matchCount = 0;
for (Path p : files)
new Thread(() -> { if (p has some property) matchCount++; }).start();
// Illegal to mutate matchCount
Если бы этот код был правомерным, это было бы не слишком хорошо. Приращение matchCount++ неатомарно, и нет никакого способа узнать, что произойдет, если несколько потоков выполнят этот код одновременно.
Внутренние классы могут также захватывать значения из охватывающей области. До Java 8 внутренние классы могли иметь доступ только к локальным final переменным. Это правило теперь ослаблено для соответствия правилу для лямбда-выражений. Внутренний класс может получить доступ к любой эффективно final локальной переменной; то есть, к любой переменной, значение которой не изменяется.
Не рассчитывайте, что компилятор выявит все параллельные ошибки доступа. Запрет на модификацию имеет место только для локальных переменных. Если matchCount – переменная экземпляра или статическая переменная из охватывающего класса, то никакой ошибки не будет, хотя результат так же не определен.
Кроме того, совершенно законно изменять разделяемый объект, хоть это и не очень надежно. Например,
List
matchedObjs = new ArrayList<>();
for (Path p : files)
new Thread(() -> { if (p has some property) matchedObjs.add(p); }).start();
// Legal to mutate matchedObjs, but unsafe
Обратите внимание, что переменная matchedObjs эффективно final. (Эффективно final переменная является переменной, которой никогда не присваивается новое значение после ее инициализации.) В нашем случае matchedObjs всегда ссылается на один и тот же объект ArrayList. Однако объект изменяется, и это не потокобезопасно. Если несколько потоков вызовут метод add, результат может быть непредсказуемым.
Существуют безопасные механизмы подсчета и сбора значений одновременно. Вы можете использовать потоки для сбора значений с определенными свойствами. В других ситуациях вы можете использовать потокобезопасные счетчики и коллекции.
Как и с внутренними классами, есть обходное решение, которое позволяет лямбда-выражению обновить счетчик в локальной охватывающей области видимости. Используйте массив длиной 1, вроде этого:
int[] counts = new int[1];
button.setOnAction(event -> counts[0]++);
Конечно, такой код не потокобезопасный. Для обратного вызова кнопки это не имеет значения, но в целом, вы должны подумать дважды, прежде чем использовать этот трюк.
Тело лямбда-выражения имеет ту же область видимости, что и вложенный блок. Здесь применяются те же самые правила для конфликтов имен. Нельзя объявить параметр или локальную переменную в лямбде, которые имеют то же имя, что и локальная переменная.
Path first = Paths.get("/usr/local");
Comparator comp =
(first, second) -> Integer.compare(first.length(), second.length());
// Error: Variable first already defined
Внутри метода вы не можете иметь две локальные переменные с тем же именем. Таким образом, вы не можете объявить такие переменные также и в лямбда-выражении. При использовании ключевого слова this в лямбда-выражении вы ссылаетесь на параметр this метода, который создает лямбду. Рассмотрим, например, следующий код
public class Application() {
public void doWork() {
Runnable r = () -> { ...; System.out.println(this.toString()); ... };
...
}
}
Выражение this.toString() вызывает метод toString объекта Application, а не экземпляра Runnable. Нет ничего особенного в использовании this в лямбда-выражении. Область видимости лямбда-выражения вложена внутрь метода doWork, и this имеет такое же значение в любой точке этого метода.
Do'stlaringiz bilan baham: