Освой Java играючи

Сайт Александра Климова

Шкодим

/* Моя кошка замечательно разбирается в программировании. Стоит мне объяснить проблему ей - и все становится ясно. */
John Robbins, Debugging Applications, Microsoft Press, 2000

Лямбда-выражения

В Java 8 появились лямбда-выражения. Поначалу они кажутся какой-то магией.

Magic

Давайте разбираться.

После того, как вы включили поддержку Java 8, вы можете использовать лямбда-выражения.

Сначала рассмотрим, как студия пытается помочь использовать новые возможности.

Допустим, у нас есть код для щелчка кнопки.


Button button = findViewById(R.id.button);
button.setOnClickListener(new View.OnClickListener() {
    @Override
    public void onClick(View v) {
        Toast.makeText(MainActivity.this, "Clicked", Toast.LENGTH_SHORT).show();
    }
});

Студия показывает имя анонимного класса серым цветом и выводит подсказку, что можно заменить код на лямбда-выражение.

Replace with lambda

Получится.


button.setOnClickListener(v ->
        Toast.makeText(MainActivity.this, "Clicked", Toast.LENGTH_SHORT).show()
);

Это подходит для старых проектов,когда код уже написан в старом стиле. Если вы пишете новый код, используя автозавершение, то студия сразу же предлагает использовать лямбда-выражение.

lambda

Когда код в студии использует лямбда-выражение, то слева на информационном желобе рядом с нумерацией строк выводится специальный значок с греческим символом лямбды λ. Щелчок по нему перенесёт в суперкласс.

Как же это работает? Нужно понимать, что лямбда-выражение является синтаксическим сахаром, который уменьшает количество кода. Понадобится некоторое время, чтобы начать понимать новый код.

Лямбда-выражение можно применить только к интерфейсу с единственным абстрактным методом (также могут быть другие методы, но не абстрактные). Интерфейс OnClickListener с единственным абстрактным методом onClick() как раз и является таким. Зная, что интерфейс имеет только один метод, мы можем опустить его имя и его модификатор. Вычёркиваем лишнее.


button.setOnClickListener(new View.OnClickListener() {
    @Override
    public void onClick(View v) {
        Toast.makeText(MainActivity.this, "Clicked", Toast.LENGTH_SHORT).show();
    }
});

У метода используется один параметр типа View. Раз мы знаем этот тип, то тоже опускаем его, оставляя только имя переменной view или v для совсем ленивых.


(View v)

То, что мы пишем в теле метода, пишется после комбинации символов ->.

Вот таким образом и трансформируется старый код в новый.

Возьмём теперь код щелчка на элементе списка.


listView.setOnItemClickListener(new AdapterView.OnItemClickListener() {
    @Override
    public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
        Log.d(TAG, "Position: " + position);
        Log.d(TAG, "Id: " + id);
    }
 });

У интерфейса AdapterView.OnItemClickListener есть единственный метод onItemClick() с четырьмя параметрами. Следуем такому алгоритму - убираем имя метода, а в параметрах убираем типы переменных. Получится следующее:


listView.setOnItemClickListener((parent, view, position, id) -> {
    Log.d(TAG, "Position: " + position);
    Log.d(TAG, "Id: " + id); 
}

Ещё один вариант для интерфейса Runnable с единственным методом run().


Runnable runnable = new Runnable() {
    @Override
    public void run() {
        Log.d(TAG, "From Runnable");
    }
};
new Thread(runnable).start();

У метода нет параметров, поэтому оставляем только скобки.


Runnable runnable = () -> Log.d(TAG, "From Runnable");
new Thread(runnable).start(); 

Общий формат лямбда-выражения: список параметров через запятую, далее ->, а затем тело выражения.


param1, param2, paramN -> { // body }

Если тело состоит из одной строчки, то фигурные скобки можно опустить. В примерах выше есть два варианта.

Мы рассмотрели случаи, когда метод имел возвращаемое значение void.

Если метод что-то возвращает, то здесь также доступна оптимизация. Например, если можно составить выражение в одну строку для возвращаемого значения, то return можно убрать.


// Создадим собственный интерфейс
public interface Calculator {
    int calculate(int a, int b);
}

Тогда можно написать так.


Calculator calculator = (a, b) -> a + b;

Выражение a + b соответствует выражению {return a + b;}.

Сокращённый синтаксис

Очень часто используется лямбда-выражение, которое вызывает метод от имени своего параметра. Например, мы хотим, чтобы лямбда-выражение получало имя кота:


cat -> cat.getName()

Учитывая популярность данного кода, решили ввести сокращённый синтаксис, который позволяет повторно использовать существующий метод. Синтаксис называется ссылкой на метод. Воспользовавшись ссылкой на метод, предыдущее лямбда-выражение можно переписать так:


Cat::getName

В общем случае ссылка на методы имеет вид ClassName::methodName. Хотя это и метод, указывать скобки не следует, так как мы ничего не вызываем. Это просто эквивалент лямбда-выражения, вызов которого приведёт к вызову метода. Ссылки на методы можно использовать всюду, где допустимы лямбда-выражения.

Сокращённый синтаксис применим и к конструктору.


Cat::new

Так можно создавать и массивы. Например, так создаётся массив строк:


String[]::new

Операция :: отделяет имя метода от имени класса или объекта, доступны три варианта.

  • Объект::МетодЭкземпляра
  • Класс::СтатическийМетод
  • Класс::МетодЭкземпляра

Выражение System.out::println равнозначно лямбда-выражению x -> System.out.println(x).

Выражение Math::pow равнозначно лямбда-выражению (x, y) -> Math.pow(x, y).

Выражение String::compareToIgnoreCase равнозначно лямбда-выражению (x, y) -> x.compareToIgnoreCase(y).

Интерфейс с одним абстрактным методом можно снабдить аннотацией @FunctionalInterface, чтобы уменьшить число ошибок. Компилятор заметит попытку добавить второй абстрактный метод и предупредит вас.

В стандартной библиотеке Java имеется целый ряд функциональных интерфейсов из пакета java.util.function.

BiFunction<T, U, R> - описывает функции с помощью параметров типа Т и U и возвращаемого типа R.

Predicate<T>

Работа с элементами списка

Есть список значений и их нужно перебрать. Старый способ.


List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5);

for (int number : numbers) {
    System.out.println(number);
}

Мы последовательно перебираем все числа по одному и выводим их на экран. С лямбда-выражением можно переписать код, который работает по другому. Мы сразу берём все числа и скармливаем методу.


numbers.forEach((Integer value) -> System.out.println(value)); // первый способ
numbers.forEach(value -> System.out.println(value)); // укорачиваем код, убирая тип
numbers.forEach(System.out::println); // ещё короче, используя оператор ::

Иногда нужно не только перебрать все числа в списке, но и что-то с ними сделать. Например, сложить их. Но задачи могут различаться, может вам нужно сложить все числа, может нужно сложить только чётные числа, а может только числа, которые больше определённого значения. Раньше нам пришлось бы создавать три отдельных метода для каждой задачи. Теперь можно создать один универсальный метод с предикатом.


public void onClick(View view) {
    List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5);

    int result;
    result = sumAll(numbers, n -> true); // суммируем все числа
    System.out.println(result);
    result = sumAll(numbers, n -> n % 2 == 0); // суммируем чётные числа
    System.out.println(result);
    result = sumAll(numbers, n -> n > 3); // суммируем числа, которые больше трёх
    System.out.println(result);
}

public int sumAll(List<Integer> numbers, Predicate<Integer> p) {
    int total = 0;
    for (int number : numbers) {
        if (p.test(number)) {
            total += number;
        }
    }
    return total;
}

Пример можно переписать с использованием нового Stream API, где есть специальные возможности для работы с коллекциями.

Реклама