Рубрики
Без рубрики

Функциональные интерфейсы в Java: Что такое функциональные интерфейсы?

Ключевым понятием в функциональном программировании является понятие функций более высокого порядка. Более высокого порядка… Помеченный java, функциональный, функциональные интерфейсы, функции более высокого порядка.

Ключевым понятием в функциональном программировании является понятие функций более высокого порядка. Функции более высокого порядка – это функции, которые могут использоваться в качестве входных параметров и выходных значений для других функций или методов. Однако в типизированном языке, таком как Java, мы должны иметь возможность определять наши параметры и возвращаемые значения как типы.

При определении целочисленного параметра мы используем тип int . Для набора строковых значений мы можем использовать Collection<Строка> или мы можем быть более конкретными с такими типами, как Список<Строка> или Установите <Строка> . Чтобы извлечь выгоду из использования функций более высокого порядка, мы должны быть в состоянии описать лямбда-выражение или ссылку на метод аналогичным образом.

Функциональные интерфейсы – это языковая функция, добавленная в Java 8 вместе с лямбда-выражениями, которые предоставляют средства для выражения лямбда-выражения или метода в виде определенного типа с помощью интерфейса. Любой интерфейс с одним абстрактным методом может рассматриваться как функциональный интерфейс и может использоваться в сигнатуре типа метода, который ожидает лямбда-выражение в качестве параметра или выходного значения.

Управление Запасами Видеопроката

Чтобы проиллюстрировать, как работают функциональные интерфейсы и полезность функций более высокого порядка, давайте создадим простую систему управления запасами. Магазины видеопроката в основном ушли в прошлое, но для тех, кто все еще существует, отслеживание доступных запасов фильмов является обязательным. Требования к этому приложению следующие:

  1. Он должен иметь возможность подсчитать доступный инвентарь всех фильмов.
  2. Он должен иметь возможность подсчитывать фильмы по типу носителя — либо VHS, либо DVD.
  3. Он должен уметь считать фильмы по названию.
  4. Он должен быть расширяемым для включения дополнительных методов подсчета запасов в будущем.

Сначала мы определим пару моделей предметной области для Фильма и Инвентарь использование Ломбока для уменьшения количества требуемых шаблонов.

public enum MediaType {
    VHS, DVD
}

@Data
public class Movie {
    private final String title;
    private final MediaType mediaType;
    private final int quantity;
}

@RequiredArgsConstructor
public class Inventory {
    private final List movies;
}

Мы могли бы добавить метод в класс Инвентаризация с логикой подсчета доступных фильмов, однако для каждого требования к подсчету запасов нам потребуется либо написать отдельные методы, либо использовать композицию объектов.

Написание отдельных методов означало бы, что наш класс не закрыт для модификации, нарушая один из SOLID принципы. Состав объектов был бы предпочтительнее, но для этого потребуется создать новый интерфейс и добавить его в качестве поля в класс Инвентаризация , а также создать новые классы, реализующие интерфейс для каждого известного и будущего требования.

// adding new methods for each requirement modifies the class's public contract with each new use case
@RequiredArgsConstructor
public class Inventory {
    private final List movies;

    public int countAll() {
        return movies.stream().mapToInt(Movie::getQuantity).sum();
    }

    public int countAllVhs() {
        return movies.stream()
                .filter(movie -> Objects.equals(movie.getMediaType(), MediaType.VHS))
                .mapToInt(Movie::getQuantity).sum();
    }

    public int countAllByTitle(String title) {
        return movies.stream()
                .filter(movie -> Objects.equals(movie.getTitle(), title))
                .mapToInt(Movie::getQuantity).sum();
    }
}

// object composition is preferable but requires new classes to be made for each use case
public interface InventoryCounter {
    int count(List inventory);
}

@RequiredArgsConstructor
public class Inventory {
    private final List movies;
    private final InventoryCounter counter;

    public int count() {
        return counter.count(movies);
    }
}

Использование Функций Более Высокого Порядка

Функции более высокого порядка предоставляют нам третий вариант. Мы можем определить интерфейс функции, которая принимает список фильмов и возвращает целочисленное значение, которое на высоком уровне учитывает функциональность всех наших вариантов использования подсчета фильмов. Мы используем аннотацию @FunctionalInterface для документирования нашего целевого назначения для интерфейса Счетчика запасов .

@FunctionalInterface
public interface InventoryCounter {
    int apply(List inventory);
}

За исключением изменения имени единственной функции в этом интерфейсе, он идентичен интерфейсу в приведенном выше примере композиции объектов. Однако, хотя сам интерфейс не изменился, то, как мы его используем, изменилось.

Затем мы добавим метод count в класс Inventory . В качестве единственного параметра этот метод принимает ссылку на лямбда-функцию или метод с сигнатурой того же типа, что и метод apply в интерфейсе; любой метод или лямбда-код, который имеет Список<Фильм> в качестве входного параметра и int в качестве выходного значения.

@RequiredArgsConstructor
public class Inventory {
    private final List movies;

    public int count(InventoryCounter counter) {
        return counter.apply(movies);
    }
}

Тестирование реализации

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

public class InventoryCounterTest {
    private static Inventory inventory;

    @BeforeAll
    public static void setup() {
        inventory = new Inventory(getMovies());
    }

    private static List getMovies() {
        return asList(
                new Movie("Aquaman", MediaType.DVD, 15),
                new Movie("Aquaman", MediaType.VHS, 5),
                new Movie("Batman v. Superman", MediaType.DVD,  25),
                new Movie("Batman v. Superman", MediaType.VHS,  10),
                new Movie("Justice League", MediaType.DVD, 30),
                new Movie("Justice League", MediaType.VHS, 12),
                new Movie("Man of Steel", MediaType.DVD, 12),
                new Movie("Man of Steel", MediaType.VHS, 3),
                new Movie("Wonder Woman", MediaType.DVD, 35),
                new Movie("Wonder Woman", MediaType.VHS, 10)
        );
    }
}

Как им удалось приобрести видеозаписи этих фильмов, остается загадкой.

Наш первый вариант использования – подсчитать общее количество доступных фильмов. Для этого мы можем использовать анонимное лямбда-выражение, лямбда-выражение, присвоенное переменной, или ссылку на метод.

// anonymous lambda expression
@Test
public void itShouldCountAllMoviesInInventory() {
    var inventoryCount = inventory.count(movies -> movies.stream()
            .mapToInt(Movie::getQuantity)
            .sum());

    assertEquals(157, inventoryCount);
}


// lambda expression assigned to a variable
@Test
public void itShouldCountAllMoviesInInventory() {
    InventoryCounter counter = movies -> movies.stream()
            .mapToInt(Movie::getQuantity)
            .sum();

    var inventoryCount = inventory.count(counter);

    assertEquals(157, inventoryCount);
}


// method reference
@Test
public void itShouldCountAllMoviesInInventory() {
    var inventoryCount = inventory.count(this::countMovieInventory);

    assertEquals(157, inventoryCount);
}

private int countMovieInventory(List movies) {
    return movies.stream().mapToInt(Movie::getQuantity).sum();
}

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

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

@Test
public void itShouldCountAllVhsMoviesInInventory() {
    var vhsInventoryCount = inventory.count(this::countVhsMovieInventory);
    assertEquals(40, vhsInventoryCount);
}

private int countVhsMovieInventory(List movies) {
    return movies.stream()
            .filter(movie -> Objects.equals(movie.getMediaType(), MediaType.VHS))
            .mapToInt(Movie::getQuantity).sum();
}

Нам нужно выполнить последнее требование — подсчитать доступные фильмы по названиям. Для выполнения этого требования нам необходимо использовать Счетчик запасов функциональные интерфейсы по-новому, как возвращаемый тип метода.

@Test
public void itShouldCountAllMoviesInInventoryWithSpecifiedTitle() {
    var aquamanInventoryCount = inventory.count(countMovieInventoryWithTitle("Aquaman"));
    var wonderWomanInventoryCount = inventory.count(countMovieInventoryWithTitle("Wonder Woman"));

    assertEquals(20, aquamanInventoryCount);
    assertEquals(45, wonderWomanInventoryCount);
}

private InventoryCounter countMovieInventoryWithTitle(String title) {
    return movies -> movies.stream()
            .filter(movie -> Objects.equals(movie.getTitle(), title))
            .mapToInt(Movie::getQuantity).sum();
}

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

С помощью этого последнего теста мы выполнили все наши требования. Рассматриваются три известных варианта использования, и с помощью функциональных интерфейсов мы обеспечили расширяемость нашего кода для будущих вариантов использования.

Стандартные Функциональные Интерфейсы

Мы устранили много ненужного кода, используя функциональные интерфейсы поверх композиции объектов, однако есть одна возможность для рефакторинга, которая позволяет нам удалить интерфейс InventoryCounter . Большинство сигнатур типов функций могут быть выражены небольшим количеством интерфейсов с одним или несколькими параметрами универсального типа. Пакет function использует этот факт для предоставления нескольких стандартных функциональных интерфейсов, чтобы вам не нужно было определять свой собственный.

import java.util.function.Function;
import java.util.function.ToIntFunction;

public class Inventory {
    private final List movies;

    // the Function interface accepts two type parameters, one for the input parameter and one for the output
    public int count(Function, Integer> counter) {
        return counter.apply(movies);
    }

    // to avoid unnecessary boxing, each functional interface has several alternatives for working with primitives
    public int count(ToIntFunction> counter) {
        return counter.applyAsInt(movies);
    }
}

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

Больше для изучения

Хотя это введение и обзор функциональных интерфейсов подошли к концу, еще многое предстоит изучить. В будущих сообщениях в блоге я рассмотрю каждый из основных функциональных типов интерфейсов, опишу их варианты использования, приведу примеры кода того, как они работают, и изучу дополнительные функции, предоставляемые их отдельными API.

Оригинал: “https://dev.to/jakewitcher/functional-interfaces-in-java-what-are-functional-interfaces-1f3l”