JavaRush/Java блог/Random/Лямбды и стримы, только практика, теории не будет

Лямбды и стримы, только практика, теории не будет

Статья из группы Random
участников
Всем привет. По случаю конкурса я решил написать тут не статью, но небольшой урок. Он будет про лямбды и стримы (stream) в Java. Если вы уже знакомы и используете их, то мотайте сразу к концу статьи, там будет небольшая подборка задач с JavaRush на которых можно потренироваться. Нужна java 8 и выше, закалка от JR, будет мало подробностей и много непонятного, сильное желание разобраться. Начнем с того, что я не буду объяснять историю появлений лямбд и стримов, просто сам её не знаю. Знаю только то, что пришли они из функционального стиля программирования, в наш ООП’шный. За мой короткий опыт обучения, просто показывал, как и что, некоторым людям сложновато понять идею, поэтому просто запоминайте, как пишется, поймете потом.

Лямбды

Лямбды и стримы, только практика, теории не будет - 1Лямбды и стримы, только практика, теории не будет - 2Если вы совсем не знаете что такое лямбды, то: Лямбда выглядит так:
(a, b) -> a.compareTo(b)
(переменные) -> действие
Этого пока достаточно. Почитать теорию можно тут: ссылка раз, ссылка два, но мне кажется практика гораздо веселее. Предлагаю вам решить такую задачку: Напишите калькулятор 1 методом. Метод должен принимать 2 цифровых значения и кое-что ещё. Код ваш будет выглядеть примерно так:
class Lambda{
    public static void main (String[] args) {
	}

    public static double calculate(){
       	return null;
    }
}
Вам надо вписать 3 параметра в сигнатуру метода calculate, дописать 1 команду в return и протестировать вызов этого метода в main. Что должен уметь этот метод
  • Складывать;
  • умножать;
  • делить;
  • вычитать;
  • вычислять корень;
  • возводить в степень;
  • возводить в степень сумму аргументов поделенную на первое число + 117;
  • и все любые другие операции, которые сможете придумать.
Что нельзя использовать:
  • if-else;
  • char как указатель операции;
  • switch-case;
  • и все остальное что вам придет в голову.
Что можно использовать:
  • Только лямбды, задание то на них.
— Что? И это все? — Да это все, потому что дописать надо буквально 3 строки, если я подскажу хоть одну, остальные будут написаны на автомате. А при желании вы сможете нагуглить примеры, попытаться понять. Проверять конечны вы будете себя сами и если схитрите никто не узнает, но тогда зачем? Решив такую простенькую задачку, все мои 1,5 ученика обрели примерное понимание, что такое лямбды и как ими пользоваться. Это будет очень нужно для стримов. Если есть желание похвастаться результатом и узнать правильно ли вы сделали, код кидайте в личку. В комментарии не надо, туда можете кинуть интересные подсказки (но так, чтобы не слишком облегчать задачу). Повторюсь, решив этот пример, вы должны уже примерно понять, как пользоваться лямбдами.
Лямбды и стримы, только практика, теории не будет - 3
Теперь перейдем к стримам (java streams). Это не те стримы о которых ты, читатель возможно подумал. Нет это не inputStream и не OutputStream. Это другое, это интереснее. Стримы пришли на замену циклам, не полностью, но все же. Подаются они с девизом «не объясняй, как делать, объясняй что делать». Небольшой пример стрима:
List<String> myList = Arrays.asList("a1", "a2", "b1", "c2", "c1");

myList.stream()
    .filter(s -> s.startsWith("c"))
    .map(String::toUpperCase)
    .sorted()
    .forEach(System.out::println);
Что тут происходит? Добавим комментарии:
myList.stream() // получить поток
    .filter(s -> s.startsWith("c")) //отфильтровать значения, оставить те, что начинаются с «с»
    .map(String::toUpperCase)  // преобразовать все значения, перевести в верхний регистр
    .sorted() // отсортировать по порядку (дефолтный порядо)
    .forEach(System.out::println); // вывести каждый элемент на экран
Сравним с обычным циклом:
List<String> toSort = new ArrayList<>();
for(String s : myList){
     if(s.startsWith("c")){
         toSort.add(s.toUpperCase());
     }
}

Collections.sort(toSort);

for(String s : toSort){
     System.ouy.println(s);
}
Когда читаешь код, все выглядит страшно, а с комментариями проще так? Это нормально, я их тоже долго не понимал. Ключ к пониманию – практика. Поэтому начинаем читать сторонние статьи и искать ответы на свои вопросы, можете их так же задавать тут в комментарии, полноценный ответ не дам, но укажу направление. Список задач от JavaRush, которые, я считаю, отлично подходят для практики стримов:
  • 2208 — можно решить 1 стримом и 1 return, т.е. тело метода будет начинаться с return и дальше будет 1 целый стрим. Требование StringBuilder опустим.

  • 1908 — так же можно решить 1 стримом и 1 return. Начиная с чтения файла. Запись в файл через стримы как сделать я не знаю (если это возможно), пока делаем ручками. Т.е. Открываем только 2 потока (консоль и запись в файл). Чтение файла производим через методы, которые вернут нам либо лист, либо сразу стрим (google и javadoc).

  • 1907 — по идее тоже можно решить в один стрим. На входе стрима имя файла, на выходе количество слов world.

На этом все. Если смогу напишу ещё уже просто рассказ. На мой взгляд, читать про что-то крутое без возможности это попробовать, как-то скучно, что ли. А после калькулятора и 3 задачек думаю вы уже неплохо ладите с лямбдами и стримами, чтобы почитать уже обо всех возможностях, если ещё не прочитали. UPD:
  • 1016 — немного извращенным способом можно решить в 1 стрим и 1 return;

  • 1821 — очень легко и в 1 стрим и 1 return.

    Эти 2 задачи познакомят вас с ещё одним методом стримов и ещё одним коллектором.

  • 1925 — можно одним стримом получить строку со словами и потом записать её в файл (можно ли писать в файл из стрима я не знаю)

Комментарии (24)
  • популярные
  • новые
  • старые
Для того, чтобы оставить комментарий Вы должны авторизоваться
Daniyar
Уровень 1
10 января 2021, 08:13
1925
try (BufferedReader reader = new BufferedReader(new FileReader(Path.of("...").toFile()))) {

    BufferedWriter writer = new BufferedWriter(new FileWriter(Path.of("...").toFile()));

    writer.write(reader.lines()
            .flatMap(p -> Arrays.stream(p.split("\\n")))
            .flatMap(p -> Arrays.stream(p.split("\\s+")))
            .filter(p -> p.length() > 6)
            .map(p -> p.replaceAll("\\s+", ", "))
            .collect(Collectors.joining(",\n")));

    writer.close();
}
Sergey Anisimov
Уровень 29
11 ноября 2019, 14:23
Помню в питоне впервые узнал про list compression.
list1 = [1,2,3,4,"az", "av", "d"]
print([i for i in list1 if str(i).startswith("a")])
В одну строчку можно из списка убрать все ненужные элементы. Да что хочешь можно с ними сделать. Можно даже ссылки парсить или вызывать дьявола для каждого элемента. Главное чтобы было понятно что ты делаешь в данный момент. Вот и в джаве смотрю есть нечто похожее? Или это по сути тоже самое? Я только месяц-полтора в программировании, так что если что-то не так сказал прошу ткнуть носом и разъяснить.
Сергеев Виктор
Уровень 40
Master
13 ноября 2019, 14:05
print([i for i in list1 if str(i).startswith("a")])
Это вроде и в одну строчку, но операторов больше одного, не нравятся мне такие штуки ) В java тоже так можно:
for(i : list1) if (newString(i).startWith("a")) System.out.println(i);
Но для меня такое написание дико. Удобнее читается по 1 оператору на строку и со скобками:
for(i : list1) {
    if (newString(i).startWith("a")) {
         System.out.println(i);
    }
}
Но т.к. пост про стримы то:
List myList = Arrays.asList(1,2,3,4,"az", "av", "d"); //без типизации

myList.stream()
     .map(String::new)
    .filter(s -> s.startsWith("a"))
    .foreach(System.out::println);
примерно так. Если нравится все в одну строчку, то переносы можно убрать, они только для удобочитаемости
Sergey Anisimov
Уровень 29
14 ноября 2019, 15:18
Подождите, в моём примере
list1 = [1,2,3,4,"az", "av", "d"]
print([i for i in list1 if str(i).startswith("a")])
>>> ["az", "av"]
будет выведен список элементов типа str, которые начинаются на "а". В вашем же примере
for(i : list1) {
    if (newString(i).startWith("a")) {
         System.out.println(i);
    }
}
будет выведен каждый элемент листа1 с новой строки. Я имею ввиду, что в моём примере создаётся новый список в памяти. Есть ещё генераторы (generator expressions), которые отлично справляются с большими объёмами данных. Можно отфильтровать огромные объемы и не нагружать оперативную память. Если интересует, то тут есть есть пояснения, чтобы понять разницу. Вот поэтому я интересуюсь, есть ли что-то подобное в java? Ваш пример как-то с этим связан? (пошёл гуглить заодно)
for(i : list1) if (newString(i).startWith("a")) System.out.println(i);
Сергеев Виктор
Уровень 40
Master
14 ноября 2019, 18:57
В вашем же примере
...
будет выведен каждый элемент листа1 с новой строки.
нет, if отфильтрует лишние элементы. Единственное, что там делается, это создание строки из элемента. Без каких либо оптимизаций. Выведены будут только элементы, строковое представление которых начинается с "а"
Можно отфильтровать огромные объемы и не нагружать оперативную память.
if или filter этим и занимаются. Тут if такой же как и в любом другом языке. По вашей ссылке я увидел описание потоковой обработки, когда элементы по одному обрабатываются. Но про "не нагружать оперативную память" я не совсем понял. Вам в любом случае надо загрузить данные в память, чтобы работать с ними как с объектами. Без оперативной памяти, работая исключительно на жестком диске ваша скорость скорее упадет.
Sergey
Уровень 35
27 октября 2019, 15:37
наводка по первому заданию. следуй за BiFunction<T,U,R>
Riccio
Уровень 35
Master
21 октября 2019, 21:15
UPD: Кому нужна подсказка с решением - можно посмотреть по ссылке. Буду рад, если предложите иной вариант решения.
Сергеев Виктор
Уровень 40
Master
21 октября 2019, 21:43
А нахрен решения то выкладывать, думаешь читатели глупые и сам не смогут решить? Никогда не понимал этого
Riccio
Уровень 35
Master
21 октября 2019, 22:28
1) Это не решения в обсуждениях задач. Это факультативная тема. 2) Могут предложить иное решение. 3) Кому-то может быть нужна подсказка.
Сергеев Виктор
Уровень 40
Master
22 октября 2019, 16:49
какая разница где? зачем выкладывать решение вообще? Люди не тупые и они умеют решать задачи, если кому-то нужна подсказка он попросит. Иные решения можно обсудить лично. Ок дело твое, но я никогда не пойму нахрена. Те кто могут решить - решат сами, те кто не могут спросят. Единственная причина выкладывать решения для всех, когда не просили - потешить самолюбие "смотрите какой я молодец"...
Riccio
Уровень 35
Master
22 октября 2019, 16:56
Знаешь, я только взялся за стримы и был бы рад более подробным материалам с разборам примеров. Ты оцениваешь поступок со своей позиции, какой мотив ближе тебе. Давай поступим следующим образом - чтобы ты не переживал, я уберу решения на Гит - кому надо - посмотрят ссылку, кому не надо - спойлера не будет.
Сергеев Виктор
Уровень 40
Master
22 октября 2019, 17:56
Видимо ты плохо читал статью: "Если есть желание похвастаться результатом и узнать правильно ли вы сделали, код кидайте в личку. В комментарии не надо, туда можете кинуть интересные подсказки (но так, чтобы не слишком облегчать задачу). Повторюсь, решив этот пример, вы должны уже примерно понять, как пользоваться лямбдами."
R2D2
Уровень 16
26 мая 2022, 03:43
Ничего не мешает ответ не открывать.
Riccio
Уровень 35
Master
21 октября 2019, 19:17
Копирование файла с помощью Stream API:
String inputFile = reader.readLine();
String outputFile = reader.readLine();

//1. копирование файла
Files.newOutputStream(Paths.get(outputFile))
 .write(Files.newInputStream(Paths.get(inputFile)).readAllBytes());

//2. копирование файла
Files.newInputStream(Paths.get(inputFile))
 .transferTo(new FileOutputStream(outputFile));
Vitaly Khan Java Developer в Onollo Master
3 июня 2019, 08:00
не уверен, что точно понял, какое решение предполагает автор, но я решил с использованием BiFunction... соответственно пришлось дописать только одну новую строку - для тестирования в main, плюс изменения в сигнатуру и после оператора return. тем, кто только начинает изучение лямбда, крайне рекомендую это видео в нем с самого нуля доходчиво объясняется, что такое лямбда, ссылки на метод (::). зачем это и откуда взялось.
21 февраля 2018, 15:39
Я думал, это как доп обучение... Ну "Напишите калькулятор 1 методом. " не в моих силах с даннми знаниями по лямдам. И даже на меков, где рыть...
Сергеев Виктор
Уровень 40
Master
21 февраля 2018, 21:22
Сложно подсказать, когда все решение это 2 строки =) можно дать слишком сильную подсказку и тогда решать уже на надо будет. Поэтому дам обычную. Чем в java является лямбда и как это можно использовать?
Viktor Kusliy
Уровень 2
Expert
22 февраля 2018, 07:59
Использовать можно только встроенные функциональные интерфейсы? Или написать свой функциональный интерфейс?
Сергеев Виктор
Уровень 40
Master
22 февраля 2018, 10:36
Можно написать свой интерфейс.
mb00mer
Уровень 35
20 февраля 2018, 19:40
На самом деле многие задачи, в которых - так или иначе - надо "бегать" по коллекциям, фильтровать их и т.д. можно решить через стримы. Только вот не везде это оправдано. Из последнего вспомнил задачу 2913, которую тоже попробовал решить через StreamAPI
public static String getAllNumbersBetween(int a, int b) {
    return IntStream
            .rangeClosed(a > b ? b : a, a > b ? a : b)
            .boxed()
            .sorted( (i1, i2)-> a > b ? i2.compareTo(i1) : i1.compareTo(i2) )
            .map(String::valueOf)
            .collect(Collectors.joining(" "));
}
, а затем сравнил по скорости с обычными циклами. Циклы в этом случае быстрее, иногда на порядок. Было бы интересно почитать - в каких случаях стоит использовать StreamAPI, а в каких нет.
Сергеев Виктор
Уровень 40
Master
20 февраля 2018, 19:53
стримы, тем более на примитивах, а тем более на java 8 работают довольно медленно. Стримы java 8 в принципе работают медленнее чем циклы, но читаются они проще. Я не считаю их соперниками. Где есть возможность красиво использовать стримы, использую. Если стримы тут выглядят как пятая лапа, то беру циклы. В приведенном вами примере думаю цикл и читается проще. Слишком много тренарных операторов =) + я не совсем понимаю зачем вам там сортировка, если вы итак заполняете поток значениями от a до b или от b до a. Или rangeClosed возвращает не отсортированную последовательность? Если отсортированную, то можно убрать boxed который забирает много ресурсов и сортировку.
mb00mer
Уровень 35
20 февраля 2018, 20:39
Ну, была просто идея фикс - проверить работу с IntStream. :) А т.к. rangeClosed требует, чтобы левая граница диапазона (1-й параметр) была <= чем правая (2-й параметр), потому и пришлось извращаться с тернарным оператором. Возвращает отсортированную, только вот - всегда по возрастанию. А по условию задачи надо, в зависимости от того какие a и b, либо возрастающую, либо убывающую последовательность. Что касается .boxed: в любом случае при преобразовании потока int в поток String придется использовать, например, .mapToObj, что тоже недешево. Другого способа не нашел. Спасибо за ответ!
Сергеев Виктор
Уровень 40
Master
20 февраля 2018, 22:07
ну при таких условиях цикл тут похоже более приемлемый и чтаемый =)
Иван Кирсанов Java Developer в ЮMoney
22 февраля 2018, 21:21
Вот очень хорошая статья о том когда использовать стрим, когда лучше отказаться. Используйте Stream API проще (или не используйте вообще)