воскресенье, 9 апреля 2017 г.

Функциональное программирование в Java 8 (Часть 3): Stream

Сегодня мы будем разбирться со Stream, которые вы используете, как функциональную альтернативу работая с коллекциями. Некоторые методы мы уже видели, когда рассматривали Optionals.

Когда мы используем Stream

Вы можете спросить, чем плох текущий способ хранения набора объектов. Почему нельзя продалжить использовать List,Set и другие?
Я хотел бы пояснить: с ними все в порядке. Но когда вы хотите работать в функциональном стиле, вы должны рассматривать возможность работать с ними (со Stream). Стандартный способ работы - перевести структуру данных в Stream. Далее, вы работаете с ними в функциональном стиле и в конце переводите в структуру данных по своему выбору. По этому мы научимся переводить наиболее популярные структуры данных в Stream.

Почему мы используем Stream

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

Как перевести структуру данных в Stream

Перевод набор объектов в Stream
Если вы хотите перести набор в Stream - можно использовать метод Stream.of():
public void convertObjects() {
    Stream<String> objectStream = Stream.of("Hello", "World");
}
Перевод List, Set, Array и др.
К счастью в Оракл продумали реализацию Stream в Java8. В каждый класс, который реализует  java.util.Collection<T>, добавили метод stream(), который переводит коллекцию в Stream. Массивы, так же легко могут быть переведны в Stream, с помощью Arrays.stream(array). Все так просто. 
public void convertStuff() {
    String[] array = {"apple", "banana"};
    Set<String> emptySet = new HashSet<>();
    List<Integer> emptyList = new LinkedList<>();
    Stream<String> arrayStream = Arrays.stream(array);
    Stream<String> setStream = emptySet.stream();
    Stream<Integer> listStream = emptyList.stream();
}
Всеравно, обычно, вы не будете хранить Stream в объекте. Вы будете его использовать и после этого переводить в необходимую структуру.

Работа со Stream

Как я уже сказал, Stream - это способ работать со структурой данных в функциональном стиле. А сейчас мы рассмотрим самый часто встречаемые методы. 

Уже изученные методы

Map
Все довольно просто. Вместо того что работать с один элементом, мы работаем со всеми элементами в Stream. Если мы хотим возвести в квадрат каждое число, то мы можем использовать Map, вместо того, чтобы писать функцию для List:

public void showMap() {
    Stream.of(1, 2, 3)
        .map(num -> num * num)
        .forEach(System.out::println); // 1 4 9
}
flatMap
Мы используем flatMap, чтобы перейти от Stream<List<Integer>> к Stream<Integer>. В примере мы хотим сложить два List в один

public void showFlatMapLists() {
    List<Integer> numbers1 = Arrays.asList(1, 2, 3);
    List<Integer> numbers2 = Arrays.asList(4, 5, 6);
    Stream.of(numbers1, numbers2) //Stream<List<Integer>>
        .flatMap(List::stream)  //Stream<Integer>
        .forEach(System.out::println); // 1 2 3 4 5 6
}
Так же в примере используется foreEach, который я опишу ниже:
forEach
Метода forEach, как и ifPresent у Optional, у него есть побочный эффект. Данный метод можно использовать, чтобы вывести все элементы Stream. ForEach один из немногих методов, который не возвращает Stream (Примечание:это метод терминальный), так что он используется последним и только один раз. 
Вы должны быть осторожны при использовании forEach, потому что это вызывает побочные эффекты, которые мы не хотим получить. Поэтому подумайте дважды, если вы могли бы заменить его другим методом без побочных эффектов.(Примечание: возможно, автор намекает на то, что данный метод не подходит для использование в параллельном Stream)

public void showForEach() {
    Stream.of(0, 1, 2, 3)
        .forEach(System.out::println); // 0 1 2 3
}
Filter
Filter - это основной метод. Он принимате 'test' функцию, которая принимает значение и возвращает boolena. Т.о. она првоеряет каждый объект в Stream. Если он проходит тест - остается в Stream. Иначе - будет удален.
Тип 'test' функции - Function<T, Boolean>. В JavaDoc вы заметите, что тип test функции на самом деле - Predicate<T>. Но это короткая форма для всех функций, которые принимают один параметр и возвращают boolean. 

public void showFilter() {
    Stream.of(0, 1, 2, 3)
        .filter(num -> num < 2)
        .forEach(System.out::println); // 0 1
}
Функции могут сделать твою жизнь легче, особенно если использовать Predicate.negate() и Objects.nonNull()
Первый инверсирует результат test. Все объекты, которые не проходят оригинальный тест, проходят тест после инверсии и наоборот.
Второй метод используется как метод ссылка, чтобы избавиться от всех null элементов. Это поможет избежать NullPointerException, например, при применении функции map:
public void negateFilter() {
    Predicate<Integer> small = num -> num < 2;
    Stream.of(0, 1, 2, 3)
        .filter(small.negate()) // Все большие цифры проходят
        .forEach(System.out::println); // 2 3
}
public void filterNull() {
    Stream.of(0, 1, null, 3)
        .filter(Objects::nonNull)
        .map(num -> num * 2) // без фильтра получили бы NullPointerExeception
        .forEach(System.out::println); // 0 2 6
}
Collect
Как было сказано выше, вы можете перевести Stream в другую структура данных. Для этого и нужен Collect. Чаще всего вы будете приводить к List или Stream.

public void showCollect() {
    List<Integer> filtered = Stream.of(0, 1, 2, 3)
        .filter(num -> num < 2)
        .collect(Collectors.toList());
}
Но Collect можно использовать для большего. Например, чтобы собрать все в String. Так же в конце строки не будет раздражающего разделителя в конце строки.

public void showJoining() {
    String sentence = Stream.of("Who", "are", "you?")
        .collect(Collectors.joining(" "));
    System.out.println(sentence); // Who are you?
}
Шорткаты
Эти методы могут быть заменине комбинацией filter, map и collect, но на то они и шорткаты.
Reduce
Отличная функция! Она принимает начальный параметр T  и функцию типа BiFunction<T, T, T>. Если у вас BiFuction, у которой все  параметры типа Т, то шорткат для нее BinaryOperator<T>. Фактически она (функция Reduce) приводит все объекты Stream к одному. Вы можете сложить все строки в одну или просуммировать все числа и т.д. В данных примерах стартовым параметром будет пустая строка или 0. Данная функция поможет сделать ваш код более читаемым, если вы знаете, что она делает.

public void showReduceSum() {
    Integer sum = Stream.of(1, 2, 3)
        .reduce(0, Integer::sum);
    System.out.println(sum); // 6
}
Расмотрим, как reduce работает:

  • суммируем первое число...
  • со вторым ...
  • с третьим...
  • со входным параметром
Как вы заметили появляется длинная цепочка функций. В конце мы получим sum(1, sum 2,(sum 3,0))). Они будут вычеслены с права налево или  из нутри к наружу. По этой причине нам нужен начальный параметр.
Sorted
Вы можете использовать Stream, чтоб отсортировать. Объекту в Streamб необязательно даже реализовывать Comperable<T>, т.к. можно написать свой собственный  Comperator<T>. Это обычная BiFunction<T, T, int>, но Comperator это шорткат для всех BiFunctional, которые принимают 2 паарметра и возвращают int.И этот int как в compareTo(), говорит нам, что первый объект меньше, когда int < 0, равны, когда int == 0, и больше, когда int > 0. Функция сортировки будет оперировать этими int и отсортирует Stream.

public void showSort() {
    Stream.of(3, 2, 4, 0)
        .sorted((c1, c2) -> c1 - c2)
        .forEach(System.out::println); // 0 2 3 4
}
Другие виды Stream
Есть специальные виды Stream, которые содержать только цифры, у них есть свой набор методов. Расмотрим IntStream и Sum, но так же есть DoubleStream, LongStream  и д.р. Подробности в JavaDoc. Чтобы перевести обычный Stream в IntStream, необходимо использовать mapToInt. Она делает то же самое, что обычная map, но возвращает IntStream, конечно, можно передать mapToInt функцию, которая будет возвращать int. В данном примере будет рассмотрено, как суммровать числа без reduce:

public void sumWithIntStream() {
    Integer sum = Stream.of(0, 1, 2, 3)
        .mapToInt(num -> num)
        .sum();
}
Использование Stream в тестах
Рассмотрим методы anyMatch, но тоак же есть методы count, max и др. которые могут пригодиться при тестировании. anyMatch работает, как filter, но он сообщает, прошел ли какой-либо объект фильтрацию. Его можно использовать в assertTrue, чтобы проверить, есть ли у какого-нибудь обхект специфическое свойство. В следующем примере проверим, было ли определенное имя сохранено в БД.

@Test
public void testIfNameIsStored() {
    String testName = "Albert Einstein";
    Datebase names = new Datebase();
    names.drop();
    db.put(testName);
    assertTrue(db.getData()
        .stream()
        .anyMatch(name -> name.equals(testName)));
}
Большой пример
В этом примере мы хотим отправить сообщение каждому пользователю, у которого сегодня день рождения.
класс User
User определяется именем и датой рождения. День рождения будет в формате "день.месяц.год". В данном примере не будем производить никаких проверок.

public class User {
    private String username;
    private String birthday;
    public User(String username, String birthday) {
        this.username = username;
        this.birthday = birthday;
    }
    public String getUsername() {
        return username;
    }
    public String getBirthday() {
        return birthday;
    }
}
Чтобы хранить всех пользователей используем List. В настоящей программе List будет заменен БД.

public class MainClass {
    public static void main() {
        List<User> users = new LinkedList<>();
        User birthdayChild = new User("peter", "20.02.1990");
        User otherUser = new User("kid", "23.02.2008");
        User birthdayChild2 = new User("bruce", "20.02.1980");
        users.addAll(Arrays.asList(birthdayChild, otherUser, birthdayChild2));
        greetAllBirthdayChildren(users);
    }
    private static void greetAllBirthdayChildren(List<User> users) {
        // Next Section
    }
}
Поздравление
Теперь мы хотим поздравить именинников. Прежде всего необходимо отфильтровать всех пользователей, у которых сегодня день рождения. После этого мы должны сообщить об этом. Итак, давайте сделаем это. Я не буду реализовывть sendMessage(String message, User receiver), он просто должен отправлять поздравления

public static void greetAllBirthdayChildren(List<User> users) {
    String today = "20.02"; //Чтобы облегчить пример. В реальности необходимо использовать LocalDateTime.
    users.stream()
        .filter(user -> user.getBirthday().startsWith(today))
        .forEach(user -> sendMessage("Happy birthday, ".concat(user.getUsername()).concat("!"), user));
}
private static void sendMessage(String message, User receiver) {
    //...
}
Параллелизм
Stream могут выполняться параллельно! По умолчанию каждый Stream не параллельный. Чтобы его сделать таковым необходимо использовать parallelStream(). Это поможет выполняться вашим Stream быстрее, но необходимо быть аккуратнее с этим. Как рассказано здесь параллелизм, например, может испортить сортировку. Поэтому будьте готовы столкнуться с неприятными ошибками с parralelStream, хотя это может сделать вашу программу значительно быстрее.
Выводы
Вот и все на сегодня! Мы много узнали о Stream на Java. Мы узнали, как преобразовать структуру данных в поток, как работать со Stream и как преобразовать ваш поток обратно в структуру данных. Я представил наиболее распространенные методы и когда вы должны их использовать. В конце урока, мы проверили наши знания на более крупном примере, где мы поприветствовали всех детей в день рождения. В следующей части этой серии у нас будет большой пример, когда мы будем использовать Stream. Но я пока не буду рассказывать вам пример, так что надеюсь, вы будете удивлены.
PS это мой перевод данной статьи.

Комментариев нет :

Отправить комментарий