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

Рефакторинг кода в потоковый API Java 8

Возможно, вам интересно, почему в сообщении в блоге в 2021 году упоминается Java 8, поскольку оно вышло более 7 лет назад…. Помеченный java, рефакторинг, поток.

Возможно, вам интересно, почему в сообщении в блоге в 2021 году упоминается Java 8, поскольку оно вышло более 7 лет назад. Причина в том, что я сталкивался со многими кодовыми базами Java, которые действительно используют Java 8 или более позднюю версию, однако они не всегда используют API Java 8. Я часто нахожу блоки кода, которые можно было бы переработать . Что я имею в виду здесь под рефакторингом , так это то, что эти блоки кода могут использовать API Java 8, чтобы стать более удобочитаемыми, более краткими (т.Е. менее подробными), а также менее устаревшими.

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

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

Фильтрация

Одним из очень распространенных действий в программировании является фильтрация набора элементов. Чтобы сделать это до Java 8, вам необходимо выполнить следующее:

List longWords(final List words, final int threshold) {
    List longWords = new ArrayList<>();
    for (String word : words) {
        if (word.length() > threshold) {
            longWords.add(word);
        }
    }
    return longWords;
}

Описанный выше метод может быть заменен следующим:

List longWords(final List words, final int threshold) {
    return words.stream()
            .filter(word -> word.length() > threshold)
            .collect(toList());
}

Это более лаконично и более читабельно.

Подсчет

Вместо фильтрации вы можете просто посчитать длинные слова. До Java 8 вы могли бы сделать:

List longWords(final List words, final int threshold) {
     List longWords = new ArrayList<>();
     for (String word : words) {
        if (word.length() > threshold) {
            longWords.add(word);
        }
     }
     return longWords;
}

С помощью Java 8 Stream API вы можете сделать:

long longWordsCount(final List words, final int threshold) {
    return words.stream()
            .filter(word -> word.length() > threshold)
            .count();
}

Группировка

Другим распространенным действием является группирование элементов из списка на основе определенного условия или предиката. Например, вы можете захотеть сгруппировать слова по длине. Чтобы сделать это до Java 8, вам необходимо выполнить следующее:

Map> groupByWordLength(final List words) {
    Map> groups = new HashMap<>();
    for (String word : words) {
        int length = word.length();
        if (groups.containsKey(length)) {
            List group = groups.get(length);
            group.add(word);
        } else {
            List group = new ArrayList<>();
            group.add(word);
            groups.put(length, group);
        }
    }
    return groups;
}

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

Map> groupByWordLength(final List words) {
    return words.stream()
            .collect(groupingBy(word -> word.length()));
}

Этот один лайнер также намного более удобочитаем.

Разделение на разделы

Одним из несколько менее распространенных, но все же частых действий является разделение списка на основе определенного условия или предиката. Продолжая наш пример “слова”, мы можем разделить слова на основе их длины. Чтобы сделать это до Java 8, вам необходимо выполнить следующее:

List> partitionByWordLength(final List words, final int threshold) {
    List shortWords = new ArrayList<>();
    List longWords = new ArrayList<>();
    for (String word : words) {
        if (word.length() > threshold) {
            longWords.add(word);
        } else {
            shortWords.add(word);
        }
    }
    return Arrays.asList(shortWords, longWords);
}

С помощью Java 8 Stream API вы можете реорганизовать метод, который будет:

List> partitionByWordLength(final List words, final int threshold) {
    Collection> partition = words.stream()
        .collect(partitioningBy(word -> word.length() > threshold))
        .values();
    return new ArrayList<>(partition);
}

Здесь нам нужно преобразовать Коллекцию в Список массивов , потому что тип, возвращаемый Разделением коллектором, является Картой . Мы могли бы просто вернуть Карту из Списка s, который содержит два ключа, истина и ложь .

Преобразующий

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

static List fooTransform(final List words) {
    List fooStuff = new ArrayList<>();
    for (String word : words) {
        String foo1 = word.repeat(3);
        String foo2 = foo1.stripTrailing();
        String foo3 = foo2.toUpperCase();
        fooStuff.add(foo3);
    }
    return fooStuff;
}

Используя потоки, вы можете выполнить следующие действия:

List fooTransform(final List words) {
    return words.stream()
        .map(word -> fooThing(word))
        .collect(toList());
}

private String fooThing(final String word) {
    String foo1 = word.repeat(3);
    String foo2 = foo1.stripTrailing();
    return foo2.toUpperCase();
}

Извлечение вычисления внутреннего блока цикла for во вспомогательном методе позволяет затем выполнить чистое и читаемое преобразование с помощью map .

Сцепление

Другой распространенной практикой является извлечение данных из двух или более разных мест, а затем объединение элементов в окончательный список. Чтобы сделать это до Java 8, вы могли бы выполнить следующее:

List concat(final List words1, final List words2) {
    List concat = new ArrayList<>();

    for (String word : words1) {
        concat.add(word);
    }

    for (String word : words2) {
        concat.add(word);
    }

    return concat;
}

Это может быть преобразовано в (довольно длинный) один лайнер :

List concat(final List words1, final List words2) {
    return Stream.concat(words1.stream(), words2.stream()).collect(toList());
}

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

Суммирование

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

int characterSum(final List words, final int threshold) {
    int sum = 0;
    for (String word : words) {
        int length = word.length();
        if (length > threshold) {
            sum += length;
        }
    }
    return sum;
}

Этого можно достичь, используя метод уменьшить из потока с. Этот метод выполняет то, что называется сокращением для элементов потока. Он принимает два параметра: начальное значение для уменьшения и накопитель . Простейшим примером является сумма:

int characterSum(final List words, final int threshold) {
    return words.stream()
            .map(word -> word.length())
            .filter(length -> length > threshold)
            .reduce(0, (sum, l) -> sum + l);
}

Функция накопления применяется на каждой итерации с текущей временной суммой на данной итерации. Начальное значение здесь равно 0 .

Из того, что я прочитал, использование потоков не снижает производительность. Однако я не хотел просто угадывать, скорее я хотел проверить это сам, используя приведенные выше примеры. Я сделал очень простые и приблизительные (или “быстрые и грязные”) тесты таймера, используя System.nanoTime() до и после выполнения метода на моем ноутбуке.

Для всех примеров я сгенерировал либо 5 сотен тысяч, либо 5 миллионов случайных слов и выполнил метод 100 раз. Результаты, которыми я поделюсь ниже, – это среднее время выполнения в миллисекундах.

Я также буду использовать оба stream() и метод parallelStream() для сравнения. При использовании метода parallelStream() он создает несколько подпотоков, которые обрабатываются параллельно разными потоками. Промежуточные результаты каждого подпотока затем объединяются при сборе. Однако обратите внимание, что вы должны быть осторожны при использовании Параллельный поток() :

  • операция итерации не должна зависеть/мешать/друг другу
  • операция итерации не должна изменять общую изменяемую переменную

Фильтрация

При повторном использовании моего примера фильтрации результаты будут следующими:

  • При повторном использовании моего примера
  • фильтрации
  • результаты будут следующими:

При повторном использовании моего примера || фильтрации || результаты будут следующими:

При повторном использовании моего примера фильтрации результаты будут следующими:

  • При повторном использовании моего примера
  • фильтрации
  • результаты будут следующими:

Группировка

При повторном использовании моего примера группировки результаты будут следующими:

  • Java 7: 17,24 мс
  • Последовательный поток Java 8: 17,98 мс
  • Параллельный поток Java 8: 13,36 мс

Разделение на разделы

При повторном использовании моего примера разбиения результаты будут следующими:

  • Java 7: 8,71 мс
  • Последовательный поток Java 8: 10,40 мс
  • Параллельный поток Java 8: 7,40 мс

Преобразующий

Повторное использование моего преобразующего примера приводит к следующим результатам:

  • Java 7: 92,78 мс
  • Последовательный поток Java 8: 77,86 мс
  • Параллельный поток Java 8: 67,22 мс

Сцепление

Повторное использование моего примера конкатенации приводит к следующим результатам:

  • Java 7: 26,72 мс
  • Последовательный поток Java 8: 22,54 мс
  • Параллельный поток Java 8: 26,52 мс

Суммирование

При повторном использовании моего примера суммирования результаты будут следующими:

  • Java 7: 22,13 мс
  • Последовательный поток Java 8: 22,52 мс
  • Параллельный поток Java 8: 11,64 мс

Результаты

Отказ от ответственности за то, что мое тестирование было “быстрым и грязным” и что время выполнения зависит от многих факторов, особенно в Java, таких как время запуска, количество доступных потоков, количество ресурсов, доступных для JVM, и т.д. Так что принимайте результаты в качестве приближений.

Последовательные потоки

Похоже, что последовательные потоки в большинстве случаев работают немного хуже, чем для/цикла. Однако, с моей точки зрения — если вы не оптимизируете производительность — компромисс между наличием кода, который является более читабельным и кратким, что означает более проверяемым и ремонтопригодным по сравнению с немного худшей производительностью, делает использование Java 8 Stream полностью оправданным.

Параллельные потоки

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

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

Оригинал: “https://dev.to/ericlloyd/refactoring-code-to-java-8-stream-api-3jpk”