Как я упоминал на прошлой неделе , 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 — лямбды и потоки .