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

Реальная Java с предикатами и потоками

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

Java сегодня гораздо более выразительный язык, чем в прошлом. Если вы все еще работаете с идиомами Java 7 (независимо от того, действительно ли вы компилируете в 8+), стоит ознакомиться с некоторыми мощными языковыми функциями современной Java.

В этой статье я расскажу об одном примере, просто чтобы подогреть ваш аппетит: предикаты.

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

Вас наняли в Candy Corp, крупнейшего производителя конфет в стране. Как довольно новая компания, наша фабрика имеет множество потребностей в программном обеспечении. Добро пожаловать на борт!

Чтобы вы сориентировались, первое, что вам следует знать, это то, что вы можете позвонить Candy Factory.bagOfCandy() для изготовления пакета конфет:

Collection bagOfCandy = CandyFactory.bagOfCandy();

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

Давайте уберем нашу первую историю из списка невыполненных работ.

Подсчитайте, сколько конфет заданного цвета находится в пакете

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

Одна из моих задач – подсчитать, сколько конфет в каждом пакете определенного цвета.

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

Хорошо, это достаточно просто. Как классический Java-программист, вы можете это сделать. Вы просто обобщите эту идею на метод фильтр по цвету , который возьмет пакет конфет и заданный цвет, а затем выделит все кусочки, соответствующие этому цвету, в свою собственную новую коллекцию. С помощью этого метода менеджер по контролю качества может выполнять множество задач, которые у него есть. Чтобы соответствовать приведенному примеру, они могут вызвать метод size() для новой коллекции, чтобы узнать, сколько их было.

Collection bagOfCandy = CandyFactory.bagOfCandy();
Collection redCandies = filterByColor(bagOfCandy, Color.RED);
int numberOfReds = redCandies.size();

Вот классическая, императивная, до Java-8 реализация метода фильтр по цвету .

Collection filterByColor(Collection assortedCandy, Candy.Color color) {
    Collection results = new ArrayList<>();
    for(Candy candyPiece : assortedCandy) {
        if(candyPiece.getColor().equals(color)) {
            results.add(candyPiece);
        }
    }
    return results;
}

Для выполнения своей задачи метод фильтрации по цвету выполняет следующие действия.

Оно:

  1. Создает новую коллекцию результаты для хранения конфет, которые соответствуют заданному цвету.
  2. Перебирает основную коллекцию конфет, которая находится в переменной с именем ассорти конфет .
  3. Проверяет, соответствует ли данный кусочек конфеты заданному цвету.
  4. Добавляет изделие в новую коллекцию, если оно заданного цвета.
  5. Возвращает новую коллекцию.

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

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

Расширение линейки

Через некоторое время наша компания задумывается о расширении линейки выпускаемой продукции. Мы решили поэкспериментировать с двумя новыми видами конфет:

  1. Арахис
  2. Крендель

На данный момент эти два новых вида конфет будут предлагаться в упаковке “grab bag”, что означает, что голодные покупатели получат пакет конфет со всеми тремя видами конфет (обычными, кренделями и арахисом) в рамках специальной акции. Если продвижение пройдет хорошо, мы знаем, что спрос хороший, и мы можем начать выпускать отдельные пакеты с арахисом или конфетами с кренделями.

Ваша команда уже добавила новый метод GetType() в класс Candy . Когда Кондитерская фабрика изготавливает пакет для захвата, мы можем получить цвет и тип каждой конфеты с помощью кода, такого как:

Collection bagOfCandy = CandyFactory.grabBag();
for(Candy candyPiece : bagOfCandy) {
    Candy.Color color = candyPiece.getColor();
    Candy.Type type = candyPiece.getType();
    // now use the color and/or type in some way
}

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

Вы копируете/вставляете предыдущий метод и меняете пару вещей… хорошо, на самом деле вы просто изменили везде, где было написано “цвет” на “тип:”

Collection filterByType(Collection assortedCandy, Candy.Type type) {
    Collection results = new ArrayList<>();
    for(Candy candyPiece : assortedCandy) {
        if(candyPiece.getType().equals(type)) {
            results.add(candyPiece);
        }
    }
    return results;
}

И вы приводите пример такого использования:

Collection bagOfCandy = CandyFactory.grabBag();
Collection pretzelCandies = filterByType(bagOfCandy, Candy.Type.PRETZEL);
int numberOfPretzels = pretzelCandies.size();

… и менеджер по контролю качества снова счастлив!

Ноющее чувство

На следующий день после отправки нового кода вы думаете о том, как вы просто скопировали/вставили новый метод из старого метода. Это кажется… как-то неправильно. Рассматривая эти два метода бок о бок, становится ясно, что должен быть какой-то способ разделить функциональность между ними, поскольку они, по сути, почти одинаковы.

Кажется естественным подумать о том, как вы могли бы написать единый метод, который мог бы учитывать оба варианта использования. (Или даже дополнительные, которые обязательно появятся.)

Что-то вроде этого:

Collection filter(Collection candies, Object attribute) {
    Collection results = new ArrayList<>();
    for(Candy candyPiece : candies) {
        if(/* condition matching the corresponding attribute of the candy to the attribute variable */) {
            results.add(candyPiece);
        }
    }
    return results;
}

Но вы не можете придумать простого способа сделать это, потому что то, что вам нужно обобщить здесь, – это не то, что вы можете сохранить в переменной. Это код! Логическое условие в операторе if должно фактически сравнивать разные атрибуты (например, цвет или тип) объекта Candy каждый раз.

Обычно вы пишете метод для совместного использования кода. Есть ли способ передать другой метод в предлагаемый новый метод filter и вместо этого вызвать метод в операторе if?

Классическая Java: S.A.M. интерфейсы

До Java 8 функциональность могла быть только в методах, а методы всегда были членами класса.

Для совместного использования функциональности для подобных случаев использования использовался специальный шаблон, интерфейс единого абстрактного метода или S.A.M. Как бы это ни звучало, это просто интерфейс с одним методом. Он постоянно использовался в классической Java. Одним из хорошо известных примеров является Интерфейс компаратора , используемый для предоставления критериев упорядочения алгоритмов сортировки.

Мы можем реорганизовать наши два метода, фильтр По Типу и фильтр по цвету , в один метод с помощью AS.A.M. У S.A.M. может быть логический метод, а цикл for в методе фильтра может вызывать логический метод S.A.M. при повторении коллекции конфет.

Конфетные спички будут нашим S.A.M. Это выглядит так:

interface CandyMatcher {
    boolean matches(Candy candy);
}

Используя этот подход, мы пишем новый более обобщенный фильтр метод:

Collection filter(Collection candies, CandyMatcher matcher) {
    Collection results = new ArrayList<>();
    for(Candy candyPiece : candies) {
        if(matcher.matches(candyPiece)) {
            results.add(candyPiece);
        }
    }
    return results;
}

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

Для фильтрации по цвету мы создаем новый класс, реализующий Конфеты совпадают и обеспечивают конкретную функциональность сопоставления по заданному цвету:

class ColorMatcher implements CandyMatcher {
    private final Candy.Color color;

    ColorMatcher(Candy.Color color) {
        this.color = color;
    }

    @Override
    public boolean matches(Candy c) {
        return c.getColor().equals(this.color);
    }
}

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

Collection filterByColor(Collection candies, Candy.Color color) {
    ColorMatcher matcher = new ColorMatcher(color);
    return filter(candies, matcher);
}

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

Недостаток подхода S.A.M.

Если вы реализовали метод, как я, он выглядит так:

    class TypeMatcher implements CandyMatcher {
        private final Candy.Type type;

        TypeMatcher(Candy.Type type) {
            this.type = type;
        }

        @Override
        public boolean matches(Candy c) {
            return c.getType().equals(this.type);
        }
    }

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

Один из способов решить эту проблему в классической Java – использовать вместо этого анонимный класс. Вместо того, чтобы писать целый Введите сопоставитель класс, просто создайте его, когда вам это нужно:

Collection filterByType(Collection candies, String type) {
    return filter(candies,
                  new CandyMatcher() {
                      @Override
                      public boolean matches(Candy c) {
                          return c.getType().equals(type);
                      }
                  });
}

Это довольно неприятно. Анонимный класс занимает 5-6 строк кода в зависимости от того, как вы это делаете. Тем не менее, это, возможно, лучше, чем иметь целый класс только для одного использования.

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

Современная Java: лямбды

И то, и другое – решаемые проблемы в современной Java. Решением является лямбды . С помощью лямбды вам нужна всего одна строка кода, чтобы выразить концепцию соответствия!

Синтаксис лямбды

Это лямбда:

c -> c.getColor().equals(color)

И это лямбда:

c -> c.getType().equals(type)

Грубо говоря, лямбды имеют следующий синтаксис:

  • Заключенный в скобки набор переменных, которые соответствуют параметрам метода интерфейса AS.A.M.. Если имеется только одна переменная, круглые скобки можно опустить.
  • “Стрелка”, представляющая собой тире, за которым следует знак “больше, чем”: ->
  • (необязательно) открывающая фигурная скобка (используется только в том случае, если будет следовать несколько строк)
  • Код для реализации
  • (необязательно) закрывающая фигурная скобка

Вы можете найти формальное определение лямбда-выражений в разделе 15.27 спецификации языка Java .

Использование лямбды

Используя лямбду вместо анонимного класса, фильтр по типу теперь становится:

Collection filterByType(Collection candies, Candy.Type type) {
    return filter(candies, c -> c.getType().equals(type));
}

Следует отметить, что во многих Java Ide теперь есть рефакторинг для этого изменения. Чтобы перейти от анонимного класса, ранее упомянутого здесь, к лямбде, я просто применил рефакторинг в IntelliJ IDEA, вместо того, чтобы самостоятельно переписывать метод.

Лямбды и функциональные интерфейсы в стандартной библиотеке

Наш код стал намного чище теперь, когда мы используем лямбды, но что-то новое начинает нас беспокоить. От нашей реализации S.A.M. остался артефакт: интерфейс Candy Matcher . Это все еще похоже на небольшой фрагмент шаблона, который нам нужен для использования лямбд.

Тоже решенная проблема!

Стандартная библиотека Java фактически предоставляет ряд интерфейсов именно для этой цели, называемых функциональными интерфейсами . Функциональные интерфейсы описаны в разделе 9.8 спецификации языка Java и определены таким образом:

Функциональный интерфейс – это интерфейс, который имеет только один абстрактный метод (помимо методов объекта) и, таким образом, представляет собой единый функциональный контракт.

Иногда я думаю о функциональном интерфейсе как об интерфейсе, который можно использовать как тип лямбды. Например, для того, чтобы передать лямбда методу, вам необходимо создать параметр метода, чтобы принять его. К какому типу относится этот параметр? Функциональный интерфейс!

Интерфейс Candy Matcher на самом деле соответствует техническому определению функционального интерфейса, и именно поэтому мы смогли оставить сигнатуру метода метода filter в покое, когда мы выполнили наш рефакторинг.

Сигнатура этого метода:

Collection filter(Collection candies, CandyMatcher matcher)

И он все еще мог передавать лямбду в качестве переменной соответствует .

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

В Предикате javadoc говорится, что он “представляет собой предикат (функцию с логическим значением) одного аргумента”.

Именно наш вариант использования! Таким образом, теперь мы можем изменить сигнатуру метода фильтра и его реализацию на использование предиката:

Collection filter(Collection candies, Predicate predicate) {
    Collection results = new ArrayList<>();
    for(Candy candyPiece : candies) {
        if(predicate.test(candyPiece)) {
            results.add(candyPiece);
        }
    }
    return results;
}

У Предиката есть тестовый метод, тело которого мы предоставляем, передавая лямбду.

Конечно, мы не хотим называть переменную здесь предикатом , как это говорит нам как код выполняет свою работу, но не говорит нам, что он делает и почему. Лучшее имя – это то, которое мы использовали, сопоставитель конфет . Возможно, Селектор конфет или что-то в этом роде тоже будет хорошо работать. Но я выбрал предикат в приведенном выше примере, чтобы вы могли точно видеть, где новая концепция применяется на практике.

Код вызова теперь выглядит так:

Collection candies = CandyFactory.grabBag();
Collection redCandies = filter(candies, c -> Color.RED.equals(c.getColor()));
int numberOfRedCandies = redCandies.size();
Collection pretzelCandies = filter(candies, c -> Type.PRETZEL.equals(c.getType());

Наш код теперь стал современным. Это проще. Это более выразительно. Возможно, самое главное, что это идиоматично. Это то, что легко поймут другие разработчики Java.

Теперь нам даже не нужен фильтр По цвету и фильтр По Типу методы. Мы удаляем их. Метод filter достаточно прост с лямбдой, поэтому нам не нужен этот дополнительный код.

Небольшая боковая панель о переменных в лямбдах

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

Тем не менее, аналогично тому, как имена односимвольных переменных, такие как i и j обычно используются в циклах for, лямбды (особенно однострочные с одной входной переменной) являются особым случаем. Тип и значение переменной очень ясны, а область действия очень мала. Поэтому программисты, использующие лямбды, обычно используют односимвольные имена, как здесь, где мы используем c для представления candy .

Современная Java: Потоки

Говоря об идиоматической Java, на самом деле есть еще лучший способ реализовать метод фильтрации: использование потока.

На самом деле, одним из методов класса Stream является фильтр и он делает именно то, что делает наш метод filter , хотя и немного по-другому.

Однако, прежде чем мы перейдем прямо к делу, давайте поговорим о потоках.

Определение потока

Поток похож на коллекцию объектов, но Javadoc отмечает, что он “отличается от коллекций несколькими способами. ” Вы можете прочитать javadoc Stream , чтобы увидеть эти различия, если хотите, но я думаю, что проще (хотя и неполно) думать о Потоке как о данных, к которым может быть применен ряд операций, завершающийся некоторым результатом. Это делается в свободном стиле, когда вы буквально просто вызываете один потоковый метод за другим. Например, если бы у вас был поток, вы могли бы начать с запроса только 100 фрагментов из этого потока, затем получить цвет этих фрагментов, в результате чего поток получит цвет каждого из них, а затем, наконец, распечатает все цвета, как это:

candyStream.limit(100)
           .map(c -> c.getColor())
           .forEach(c -> System.out.println(c));

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

Мы можем использовать наш пример использования конфет для создания практического примера. Конфеты начинаются в “сумке для захвата”, где есть смесь цветов и типов. Мы применяем операцию |/| чтобы отфильтровать конфеты только до красных кусочков. Затем мы применяем терминальную операцию для создания новой коллекции только из этих фрагментов. Мы даже можем просто подсчитать количество деталей в качестве конечной операции для нашего конвейера, чтобы соответствовать данному варианту использования.

В Java это выглядит так:

bagOfCandy.stream()
       .filter(c -> Color.RED.equals(c.getColor()))
       .count();

Нам нужен только вызов .stream() здесь, потому что мы начинаем с коллекции ( пакет Конфет ). Если бы это уже был Поток, то в этом не было бы необходимости.

Рефакторинг нашего последнего примера для потокового использования

Императивная версия filter выглядит так прямо сейчас:

static Collection filter(Collection candies, Predicate candyMatcher) {
        Collection results = new ArrayList<>();
        for(Candy candyPiece : candies) {
            if(candyMatcher.test(candyPiece)) {
                results.add(candyPiece);
            }
        }
        return results;
    }

Мы можем удалить этот метод. Давайте просто использовать потоки сейчас.

Collection bagOfCandy = CandyFactory.grabBag();

long numberOfRedCandies = candies.stream()
                         .filter(c -> Color.RED.equals(c.getColor()))
                         .count();
long numberOfPretzelCandies = candies.stream()
                            .filter(c -> Type.PRETZEL.equals(c.getType())
                            .count();

Мы понимаем, что современная Java из стандартной библиотеки Java была всем, что нам было нужно для использования QC manager с самого начала!

Почему поток лучше, чем его императивный двоюродный брат

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

Другой способ выразить это состоит в том, что поток является декларативным , а не императив . То есть мы инструктируем компьютер, что делать с данными, а не выполняем шаги более низкого уровня.

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

Вывод

Надеюсь, вам понравилась эта статья. Вы узнали о лямбдах, предикатах и потоках на примере реального мира. Уделите этой статье внимание, и если их будет достаточно, я добавлю больше статей с практическими примерами из реального мира, где потоки в Java могут упростить и улучшить ваш код.

Оригинал: “https://dev.to/scottshipp/real-world-java-with-predicates-and-streams-2jlo”