JavaRush /Java блог /Архив info.javarush /Избавляемся от циклов в Java 8
KapChook
19 уровень
Volga

Избавляемся от циклов в Java 8

Статья из группы Архив info.javarush
Функциональный стиль, представленный в Java 8, — большая добавка к языку. Теперь Java — это не чистое ООП, теперь это гибрид ООП и функционального программирования. Это меняет правила игры и нам нужно изменить свои ООП-мозги, чтобы вобрать в себя эти изменения. Избавляемся от циклов в Java 8 - 1Но почему мы должны принимать эти изменения? Почему должны тратить время в попытках ужиться с функциональным стилем, когда мы можем решить проблему на чистом ООП?
  • Функциональный стиль, представленный в Java 8, помогает нам уменьшить пропасть между бизнес-логикой и кодом. Он позволяет нам рассказывать историю в естественном потоке на более высоком уровне. Вместо того, чтобы говорить как вы хотите это сделать, вы можете сказать что вы хотите сделать.

  • Код становится более чистым и кратким.

  • Функции высокого порядка позволяют нам:

    • Отправлять функции в другие функции
    • Создавать функции внутри других функций
    • Возвращать функции из других функций

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

  • Благодаря лямбдам мы можем делать ленивые вычисления. Когда лямбда-выражение отправляется как аргумент метода, компилятор вычислит его, когда оно вызывается в методе. Это отличается от обычных аргументов методов, которые вычисляются сразу же.

  • Лямбды делают написание unit-тестов весёлым. Они позволяют нам создавать легковесные тесты, которые чисты, малы по размеру и быстры в написании. Мы можем корчевать тестируемый код, используя лямбды. Это позволяет нам тестировать, как все виды сценариев повлияют на код.

  • Новые паттерны для изучения.

  • И многое другое!

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

Да начнётся кодинг!

Мы будем работать со статьями. У статьи есть название, автор и несколько тегов.

private class Article {
 
    private final String title;
    private final String author;
    private final List<String> tags;
 
    private Article(String title, String author, List<String> tags) {
        this.title = title;
        this.author = author;
        this.tags = tags;
    }
 
    public String getTitle() {
        return title;
    }
 
    public String getAuthor() {
        return author;
    }
 
    public List<String> getTags() {
        return tags;
    }
}
Каждый пример будет содержать традиционное решение с использованием циклов и решение, использующее новые фишки Java 8. В первом примере мы хотим найти в коллекции первую статью с тегом “Java”. Давайте взглянем на решение с использованием цикла.

public Article getFirstJavaArticle() {
 
    for (Article article : articles) {
        if (article.getTags().contains("Java")) {
            return article;
        }
    }
    return null;
}
Теперь давайте решим проблему, пользуясь операциями из Stream API.

public Optional<Article> getFirstJavaArticle() {
    return articles.stream()
        .filter(article -> article.getTags().contains("Java"))
        .findFirst();
    }
Довольно таки круто, не правда ли? Сначала мы используем операцию filter для нахождения всех статей с тегом “Java”, потом используем findFirst(), чтобы получить первое вхождение. Так как потоки (streams) ленивые и фильтр возвращает поток, этот подход будет обрабатывать элементы только пока не найдёт первое совпадение. Теперь давайте получим все статьи с тегом “Java”, вместо только первой. Сначала решение с помощью циклов.

public List<Article> getAllJavaArticles() {
 
    List<Article> result = new ArrayList<>();
 
    for (Article article : articles) {
        if (article.getTags().contains("Java")) {
            result.add(article);
        }
    }
    return result;
}
Решение с использованием потоковых операций.

public List<Article> getAllJavaArticles() {
    return articles.stream()
        .filter(article -> article.getTags().contains("Java"))
        .collect(Collectors.toList());
    }
В этом примере мы использовали операцию collect для сокращения результирующего потока, вместо объявления коллекции и явного добавления статей, которые подходят. Пока всё идёт хорошо. Время для примеров, которые заставят Stream API действительно блестать. Давайте сгруппируем все статьи по автору. Как обычно, начинаем с решения с помощью циклов:

public Map<String, List<Article>> groupByAuthor() {
 
    Map<String, List<Article>> result = new HashMap<>();
 
    for (Article article : articles) {
        if (result.containsKey(article.getAuthor())) {
            result.get(article.getAuthor()).add(article);
        } else {
            ArrayList<Article> articles = new ArrayList<>();
            articles.add(article);
            result.put(article.getAuthor(), articles);
        }
    }
    return result;
}
Сможем ли мы найти чистое решение этой проблемы, используя потоковые операции?

public Map<String, List<Article>> groupByAuthor() {
    return articles.stream()
        .collect(Collectors.groupingBy(Article::getAuthor));
}
Замечательно! Используя операцию groupingBy и ссылку на метод getAuthor(), мы получаем чистый и читабельный код. Теперь давайте найдём остальные теги, используемые в коллекции. Начнём с циклового примера:

public Set<String> getDistinctTags() {
 
    Set<String> result = new HashSet<>();
 
    for (Article article : articles) {
        result.addAll(article.getTags());
    } 
    return result;
}
Окей, давайте взглянем, как мы можем решить это с помощью потоковых операций:

public Set<String> getDistinctTags() {
    return articles.stream()
        .flatMap(article -> article.getTags().stream())
        .collect(Collectors.toSet());
}
Круто! flatmap помогает нам сгладить список тегов в один результирующий поток, а затем мы используем collect для создания возвращаемого сета.

Бесконечные возможности

Это были 4 примера, как можно заменить циклы более читабельным кодом. Обязательно ознакомьтесь с Stream API, так как эта статья только поскребла её поверхность. Освоение нового функционального стиля Java будем испытанием для ООП-разработчиков, но это испытание, которое должно быть хорошо принято. Я даже пойду дальше и скажу, что вам стоит выучить чистый функциональный язык программирования. Таким образом вы сможете полностью понять возможности и мощь, которые он предоставляет. Я думаю это поможет вам понять функциональное программирование на другом уровне. Так что осваивайте функциональное программирование, наряду со старым добрым ООП, и используйте их обоих для написания ещё более великого кода! Вольный микс из переводов двух статей — Why you should embrace functional programming in Java 8 и Swerving Away from Loops in Java 8
Комментарии (9)
ЧТОБЫ ПОСМОТРЕТЬ ВСЕ КОММЕНТАРИИ ИЛИ ОСТАВИТЬ КОММЕНТАРИЙ,
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ
S Уровень 22 Expert
7 апреля 2019
Эта Java хороша - начинай сначала.
PodoBulge Уровень 31
25 апреля 2015
есть одна интересная книжечка Functional programming in Java (можно скачать на it-ebooks)
Litle Уровень 9
26 марта 2015
хм, непонятен только один момент. По мне, так самый важный… ничего не сказано про распараллеливание, особенно с учетом того, что бесплатного супа больше не будет! :) Думаю легкого упоменания о strem api как то слишком мало.

Функциональная программа сразу готова к распараллеливанию без каких-либо изменений. Вам не придётся задумываться о deadlock-ах или состояниях гонки (race conditions) потому что вам не нужны блокировки! Ни один кусочек данных в функциональной программе не меняется дважды одним и тем же потоком или разными. Это означает, что вы можете легко добавить потоков к вашей программе даже не задумываясь при этом о проблемах, присущих императивным языкам.
Функциональное программирование для всех.
vladimirsencov Уровень 40
12 марта 2015
Java пошел по пути С++ где прикрутили все, что только можно что не очень положительно сказывается на языке увеличивая его сложность и порог вхождения. Хотя никто не заставляет использовать лямбда-исчисление и прочий матан в своих разработках. Но все равно придется понимать чужой.
Kishuomi Уровень 22
10 марта 2015
Что ж повышение чтабельности на лицо. Особено с гупировкой. Надеюсь с производительность дела обстоят так же.