Статьи

Java 8 MOOC – Сессия 2 Подведение итогов

Как я упоминал на прошлой неделе , Sevilla Java User Group работает над завершением Java 8 MOOC на лямбдах и потоках. Мы проводим три сессии, чтобы поделиться знаниями между людьми, которые проходят курс.

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

На встрече мы говорили о:
Необязательно : нам было довольно удобно, я думаю, использовать Optionalдля предотвращения a NullPointerException. То, что мы не были так ясны, были примеры filter()и map()— если вы получали свои Optionalзначения из потока, почему бы вам сначала не сделать карту и фильтр в потоке? Например, зачем это делать:

list.stream()
    .findFirst()
    .map(String::trim)
    .filter(s -> s.length() > 0)
    .ifPresent(System.out::println);

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

Я считаю Optionalболее полезным, когда другие API полностью поддерживают Java 8 и возвращают Optionalзначения, тогда вы можете выполнять дополнительные операции с возвращаемыми значениями.

Эта терминальная операция на самом деле не терминальная ?? Мы сталкивались с этим пару раз в наших примерах на сессии, один из примеров — приведенный выше код (давайте скопируем его сюда, чтобы мы могли рассмотреть его более внимательно):

list.stream()
    .findFirst()
    .map(String::trim)
    .filter(s1 -> s1.length() > 0)
    .ifPresent(System.out::println);

Не эксплуатации терминала ? Как вы можете продолжать делать больше операций на этом?findFirst()

Ответ, конечно, заключается в том, что тип возврата операции терминала также может привести к дальнейшим операциям. Выше на самом деле:

Optional<String> result = list.stream()
                              .findFirst();
result.map(String::trim)
      .filter(s1 -> s1.length() > 0)
      .ifPresent(System.out::println);

Наш терминал возвращает опциональное значение, которое позволяет вам выполнять дальнейшие операции. Еще один пример этой путаницы:

list.stream()
    .map(String::toLowerCase)
    .collect(toList())
    .forEach(System.out::println);

Здесь collect()есть терминальная операция, но она возвращает список, который также позволяет forEach():

List<String> results = list.stream()
                           .map(String::toLowerCase)
                           .collect(toList());
results.forEach(System.out::println);

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

Параллельно / последовательно / параллельно : на предыдущей неделе был вопрос о том, почему вы можете написать код, подобный этому:

list.stream()
    .parallel()
    .map(String::trim)
    .sequential()
    .filter(s1 -> s1.length() > 0)
    .parallel()
    .forEach(System.out::println);

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

Неупорядоченный : «Почему вы хотите, чтобы ваш поток был неупорядоченным?» — ответ заключается в том,unordered() что ваша отсортированная коллекция не превращается в одну без порядка , а просто говорит, что при выполнении этого кода порядок элементов не имеет значения. Это может ускорить обработку в параллельном потоке, но, как мы полагали, это будет бессмысленно в последовательном потоке.

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

list.stream()
    .map(String::toLowerCase)
    .filter(s -> s.length() % 2 == 1)
    .collect(toList());

Операция фильтра должна привести к меньшему количеству элементов для обработки в потоке. Учитывая, что map()операция не меняет ничего, на что она filter()опирается, будет ли этот код каким-то образом оптимизирован под прикрытием, чтобы фильтр фактически выполнялся первым? Или оптимизация все еще будет соответствовать порядку операций в потоке?

Наш случай на самом деле является очень специфическим случаем, потому что a) map()возвращает тот же тип, что и передаваемые параметры (т.е. он не отображает a Stringв an int) и b) the map()не изменяет характеристику, на которую filter()смотрит (то есть длину) ). Но, вообще говоря, вы не можете ожидать, что эти условия будут верными — на самом деле я держу пари, что в большинстве случаев они не соответствуют действительности. Таким образом, конвейерные операции выполняются в том порядке, в котором они записаны , то есть наши mapи filterне будут переупорядочены в более эффективный порядок.

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

list.stream()
    .filter(s -> s.length() % 2 == 1)
    .map(String::toLowerCase)
    .collect(toList());

Плоская карта : что …? flatMap()это один из тех методов, который имеет смысл, когда вы освоите его, и вы не поймете, почему это так запутанно. Но когда вы впервые сталкиваетесь с этим, это сбивает с толку — чем это flatMap()отличается map()?

Ну, flatMapиспользуется для сжатия (например) потока потоков в простой поток. Это похоже на превращение двумерного массива в одно измерение, так что вы можете перебирать все элементы, не используя вложенные циклы for. Есть пример на StackOverflow и еще несколько примеров в ответ на этот вопрос .

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

reader.lines()
      .sorted(new Comparator<String>() {
          @Override
          public int compare(String o1, String o2) {
              return ???;
          }
      })
      .collect(toList());

К сожалению, использование лямбды по-прежнему не дает ответа на вопрос «я минус o1 от o2 или o2 от o1?»:

reader.lines()
      .sorted((o1, o2) -> ??? )
      .collect(toList());

Но есть еще один новый метод в Java 8, который может нас спасти, и который не так широко известен, как следовало бы. Есть возможность, с помощью Comparator.comparing()которой вы можете легко определить, с чем сравнивать. JavaDoc и сигнатура выглядят довольно запутанно, но это одно из тех мест, где ссылки на методы неожиданно обретают смысл:

reader.lines()
      .sorted(comparingInt(String::length))
      .collect(toList());

(Здесь мы на самом деле используем comparingIntметод, поскольку мы собираемся сравнивать примитивное значение). Лично это одна из моих любимых новых функций в Java 8.

Присоединяйтесь к нам на следующей неделе для последней сессии по Java 8 — лямбды и потоки .