{"id":"streams-loop-or-stream","slug":"loop-or-stream","title":"A loop or a stream?","text":"You now have two ways to go through a collection. Neither is the \"right\" one: both are everyday Java, and one program often uses both. This step compares them, so you can pick the one that reads better for each job.\n\n### Where a stream reads well\n\nA question of the form \"from these values, keep some, change them, and count, add up or collect them\" is what streams were made for. A bookshop's list has one entry for each copy sold. Which titles sold today, each once, in alphabetical order? With a loop:\n\n```java\nArrayList<String> sold = new ArrayList<>();\nsold.add(\"Kindred\");\nsold.add(\"Beloved\");\nsold.add(\"Kindred\");\nsold.add(\"Emma\");\nArrayList<String> titles = new ArrayList<>();\nfor (String title : sold) {\n    if (!titles.contains(title)) {\n        titles.add(title);\n    }\n}\nCollections.sort(titles);\nSystem.out.println(titles);\n```\n\n```output\n[Beloved, Emma, Kindred]\n```\n\nWith a stream, the same steps have names:\n\n```java\nimport java.util.ArrayList;\nimport java.util.List;\nimport java.util.stream.Collectors;\n\npublic class Main {\n    public static void main(String[] args) {\n        ArrayList<String> sold = new ArrayList<>();\n        sold.add(\"Kindred\");\n        sold.add(\"Beloved\");\n        sold.add(\"Kindred\");\n        sold.add(\"Emma\");\n        List<String> titles = sold.stream()\n                .distinct()\n                .sorted()\n                .collect(Collectors.toList());\n        System.out.println(titles);\n    }\n}\n```\n\n```output\n[Beloved, Emma, Kindred]\n```\n\nThe loop version needs a new list, a loop, an `if` and a sort, and the reader has to put them together to see the idea. The stream says the idea in its step names: distinct, sorted, collect.\n\n### Where a loop reads well\n\n- **Stopping early.** A loop can `return` or `break` as soon as it has its answer, such as the first day the shop sold more than 100 books. A `return` inside a lambda only ends that one call of the lambda, not the stream. (Java has stream operations that stop at the first match, such as `findFirst`, but this course doesn't use them, and a loop that stops says so plainly.)\n- **Several results in one go.** One loop can update a count, a sum and a largest value at the same time. A lambda can't change the variables of its method (step 2), so with streams, each result is a stream of its own. For the lists in this course, going through a list three times is no problem: choose by what's easier to read.\n- **Reading input.** A Scanner gives one line at a time, and a `while` loop reads them. Step 5's programs read with a loop and then answer with streams.\n- **When the position matters.** Comparing each value with the one before it is natural in a loop with an index. `IntStream.range` can do it too, but the loop is often plainer.\n- **Changing the list while going through it.** The stream steps of this module never change the list they read. To remove values while going through a list, use an index loop that goes backwards (module 14) or module 39's iterator. (Module 39 also shows `removeIf`, which does that loop for you.)\n\nHere's a question of the \"stopping early\" kind: was there a day when the shop sold no books at all? As soon as the loop finds one such day, the answer is known, and the method returns it without looking at the rest:\n\n```java\nimport java.util.ArrayList;\nimport java.util.List;\n\npublic class Main {\n    public static void main(String[] args) {\n        List<Integer> booksSold = new ArrayList<>();\n        booksSold.add(64);\n        booksSold.add(0);\n        booksSold.add(89);\n        booksSold.add(130);\n        System.out.println(hadEmptyDay(booksSold));\n    }\n\n    public static boolean hadEmptyDay(List<Integer> booksSold) {\n        for (int sold : booksSold) {\n            if (sold == 0) {\n                return true;\n            }\n        }\n        return false;\n    }\n}\n```\n\n```output\ntrue\n```\n\n`filter(sold -> sold == 0).count() > 0` gives the same answer, but it counts every empty day to answer a yes-or-no question, and the reader has to work out why it compares with 0.\n\n### A rule of thumb\n\n- If you can say the question as \"keep these, turn them into that, then count, add up or collect\", try a stream.\n- If you need to stop early, remember something from the round before, or change several variables as you go, a loop is usually plainer.\n- If a lambda grows to many lines, the code is telling you something: move that work into a method of its own, and call it from the lambda (or with a method reference), or write a loop.\n","task":"The bookshop's app has a method with a loop. `BookSale.java` is ready: each sale has a title, the number of copies and the price of one copy. `bestSellers` returns the titles that had a sale of at least `minCopies` copies, each once, in alphabetical order. Rewrite its body with a stream, with no loop and no `Collections.sort`, and keep its header as it is. The check runs:\n\n```java\nList<BookSale> sales = new ArrayList<>();\nsales.add(new BookSale(\"Kindred\", 3, 14));\nsales.add(new BookSale(\"Emma\", 1, 12));\nsales.add(new BookSale(\"Beloved\", 2, 15));\nsales.add(new BookSale(\"Kindred\", 4, 14));\nSystem.out.println(bestSellers(sales, 2));\n```\n\nand expects:\n\n```\n[Beloved, Kindred]\n```\n","kind":"code","seed":"// ==== Main.java ====\nimport java.util.ArrayList;\nimport java.util.Collections;\nimport java.util.List;\nimport java.util.stream.Collectors;\n\npublic class Main {\n    public static void main(String[] args) {\n        List<BookSale> sales = new ArrayList<>();\n        sales.add(new BookSale(\"Kindred\", 3, 14));\n        sales.add(new BookSale(\"Emma\", 1, 12));\n        sales.add(new BookSale(\"Beloved\", 2, 15));\n        sales.add(new BookSale(\"Kindred\", 4, 14));\n        System.out.println(bestSellers(sales, 2));\n    }\n\n    public static List<String> bestSellers(List<BookSale> sales, int minCopies) {\n        List<String> titles = new ArrayList<>();\n        for (BookSale sale : sales) {\n            if (sale.getCopies() >= minCopies && !titles.contains(sale.getTitle())) {\n                titles.add(sale.getTitle());\n            }\n        }\n        Collections.sort(titles);\n        return titles;\n    }\n}\n\n// ==== BookSale.java ====\npublic class BookSale {\n    private String title;\n    private int copies;\n    private int price;\n\n    public BookSale(String title, int copies, int price) {\n        this.title = title;\n        this.copies = copies;\n        this.price = price;\n    }\n\n    public String getTitle() {\n        return this.title;\n    }\n\n    public int getCopies() {\n        return this.copies;\n    }\n\n    public int getPrice() {\n        return this.price;\n    }\n\n    public int total() {\n        return this.copies * this.price;\n    }\n}\n","solution":"// ==== Main.java ====\nimport java.util.ArrayList;\nimport java.util.List;\nimport java.util.stream.Collectors;\n\npublic class Main {\n    public static void main(String[] args) {\n        List<BookSale> sales = new ArrayList<>();\n        sales.add(new BookSale(\"Kindred\", 3, 14));\n        sales.add(new BookSale(\"Emma\", 1, 12));\n        sales.add(new BookSale(\"Beloved\", 2, 15));\n        sales.add(new BookSale(\"Kindred\", 4, 14));\n        System.out.println(bestSellers(sales, 2));\n    }\n\n    public static List<String> bestSellers(List<BookSale> sales, int minCopies) {\n        return sales.stream()\n                .filter(sale -> sale.getCopies() >= minCopies)\n                .map(BookSale::getTitle)\n                .distinct()\n                .sorted()\n                .collect(Collectors.toList());\n    }\n}\n\n// ==== BookSale.java ====\npublic class BookSale {\n    private String title;\n    private int copies;\n    private int price;\n\n    public BookSale(String title, int copies, int price) {\n        this.title = title;\n        this.copies = copies;\n        this.price = price;\n    }\n\n    public String getTitle() {\n        return this.title;\n    }\n\n    public int getCopies() {\n        return this.copies;\n    }\n\n    public int getPrice() {\n        return this.price;\n    }\n\n    public int total() {\n        return this.copies * this.price;\n    }\n}\n","hints":["Name each part of the loop as a stream step: the `if` on the copies is a `filter`, taking the title is a `map`, the `contains` check is `distinct()`, and `Collections.sort` is `sorted()`.","`return sales.stream().filter(sale -> sale.getCopies() >= minCopies).map(BookSale::getTitle).distinct().sorted().collect(Collectors.toList());`"],"tests":[{"name":"Test 1","stdin":"","call":"List<BookSale> sales = new ArrayList<>();\nsales.add(new BookSale(\"Kindred\", 3, 14));\nsales.add(new BookSale(\"Emma\", 1, 12));\nsales.add(new BookSale(\"Beloved\", 2, 15));\nsales.add(new BookSale(\"Kindred\", 4, 14));\nSystem.out.println(bestSellers(sales, 2));\n","expect":"[Beloved, Kindred]","hidden":false},{"name":"Test 2","stdin":"","call":"List<BookSale> sales = new ArrayList<>();\nsales.add(new BookSale(\"Walden\", 5, 10));\nsales.add(new BookSale(\"Dune\", 5, 16));\nsales.add(new BookSale(\"Candide\", 1, 9));\nsales.add(new BookSale(\"Walden\", 1, 10));\nSystem.out.println(bestSellers(sales, 5));\nSystem.out.println(bestSellers(sales, 1));\nSystem.out.println(bestSellers(sales, 6) + \" \" + sales.size());\n","expect":"[Dune, Walden]\n[Candide, Dune, Walden]\n[] 4","hidden":true}],"require":[{"pattern":"\\.stream\\s*\\(\\s*\\)","message":"Use a stream: `sales.stream()`."},{"pattern":"\\.collect\\s*\\(\\s*Collectors\\s*\\.\\s*toList\\s*\\(\\s*\\)\\s*\\)","message":"Gather the titles with `collect(Collectors.toList())`."}],"forbid":[{"pattern":"\\b(for|while)\\b","message":"Rewrite the loop as a stream: no `for` or `while`."},{"pattern":"\\bCollections\\s*\\.\\s*sort\\b","message":"Let the stream sort with `sorted()`."}],"more":[{"task":"Some questions suit a loop better. Write the method `firstDayOver(List<Integer> booksSold, int target)`, which returns the number of the first day whose sales were over `target` (1 for the first day in the list), or 0 if no day was. Use a loop that returns as soon as it finds the day. The check runs:\n\n```java\nList<Integer> booksSold = new ArrayList<>();\nbooksSold.add(64);\nbooksSold.add(112);\nbooksSold.add(89);\nbooksSold.add(130);\nSystem.out.println(firstDayOver(booksSold, 100));\nSystem.out.println(firstDayOver(booksSold, 120));\n```\n\nand expects:\n\n```\n2\n4\n```\n","kind":"code","seed":"import java.util.ArrayList;\nimport java.util.List;\n\npublic class Main {\n    public static void main(String[] args) {\n        List<Integer> booksSold = new ArrayList<>();\n        booksSold.add(64);\n        booksSold.add(112);\n        booksSold.add(89);\n        booksSold.add(130);\n        System.out.println(firstDayOver(booksSold, 100));\n        System.out.println(firstDayOver(booksSold, 120));\n    }\n\n    // Write the method firstDayOver here.\n}\n","solution":"import java.util.ArrayList;\nimport java.util.List;\n\npublic class Main {\n    public static void main(String[] args) {\n        List<Integer> booksSold = new ArrayList<>();\n        booksSold.add(64);\n        booksSold.add(112);\n        booksSold.add(89);\n        booksSold.add(130);\n        System.out.println(firstDayOver(booksSold, 100));\n        System.out.println(firstDayOver(booksSold, 120));\n    }\n\n    public static int firstDayOver(List<Integer> booksSold, int target) {\n        for (int i = 0; i < booksSold.size(); i++) {\n            if (booksSold.get(i) > target) {\n                return i + 1;\n            }\n        }\n        return 0;\n    }\n}\n","hints":["The header is `public static int firstDayOver(List<Integer> booksSold, int target)`. The day's number is its index plus one, so go through the indexes with a `for` loop.","Inside the loop, `if (booksSold.get(i) > target) { return i + 1; }`. After the loop, no day was over the target: `return 0;`."],"tests":[{"name":"Test 1","stdin":"","call":"List<Integer> booksSold = new ArrayList<>();\nbooksSold.add(64);\nbooksSold.add(112);\nbooksSold.add(89);\nbooksSold.add(130);\nSystem.out.println(firstDayOver(booksSold, 100));\nSystem.out.println(firstDayOver(booksSold, 120));\n","expect":"2\n4","hidden":false},{"name":"Test 2","stdin":"","call":"List<Integer> booksSold = new ArrayList<>();\nbooksSold.add(100);\nbooksSold.add(101);\nSystem.out.println(firstDayOver(booksSold, 100) + \" \" + firstDayOver(booksSold, 99) + \" \" + firstDayOver(booksSold, 101));\nSystem.out.println(firstDayOver(new ArrayList<>(), 0));\n","expect":"2 1 0\n0","hidden":true}],"require":[{"pattern":"\\bpublic\\s+static\\s+int\\s+firstDayOver\\s*\\(\\s*List<Integer>\\s+\\w+\\s*,\\s*int\\s+\\w+\\s*\\)","message":"Write the method as `public static int firstDayOver(List<Integer> booksSold, int target)`."},{"pattern":"\\b(for|while)\\b","message":"Use a loop that returns as soon as it finds the day."}],"forbid":[]},{"task":"The closing challenge puts the module together. The bookshop's till writes each sale to `sales.csv`: the title, the number of copies and the price of one copy, separated by commas. `BookSale.java` is ready. Write `main` so that it reads the file into a `List<BookSale>` with `Files.lines`, and then uses streams to print:\n\n- the number of sales\n- the number of copies sold, all sales together\n- the takings: each sale's copies times its price (`total()`), added up\n- the titles sold, each once, in alphabetical order\n\nIf the file can't be read, print only `Error: ` and the exception's message. Use no loops. For the file shown, the program prints:\n\n```\nSales: 6\nCopies: 12\nTakings: 155 dollars\nTitles: [Beloved, Dune, Emma, Kindred]\n```\n","kind":"code","seed":"// ==== Main.java ====\nimport java.nio.file.Files;\nimport java.nio.file.Paths;\nimport java.util.List;\nimport java.util.stream.Collectors;\n\npublic class Main {\n    public static void main(String[] args) {\n\n    }\n}\n\n// ==== BookSale.java ====\npublic class BookSale {\n    private String title;\n    private int copies;\n    private int price;\n\n    public BookSale(String title, int copies, int price) {\n        this.title = title;\n        this.copies = copies;\n        this.price = price;\n    }\n\n    public String getTitle() {\n        return this.title;\n    }\n\n    public int getCopies() {\n        return this.copies;\n    }\n\n    public int getPrice() {\n        return this.price;\n    }\n\n    public int total() {\n        return this.copies * this.price;\n    }\n}\n","solution":"// ==== Main.java ====\nimport java.nio.file.Files;\nimport java.nio.file.Paths;\nimport java.util.List;\nimport java.util.stream.Collectors;\n\npublic class Main {\n    public static void main(String[] args) {\n        try {\n            List<BookSale> sales = Files.lines(Paths.get(\"sales.csv\"))\n                    .map(line -> line.split(\",\"))\n                    .map(parts -> new BookSale(parts[0], Integer.valueOf(parts[1]), Integer.valueOf(parts[2])))\n                    .collect(Collectors.toList());\n            int copies = sales.stream()\n                    .mapToInt(BookSale::getCopies)\n                    .sum();\n            int takings = sales.stream()\n                    .mapToInt(BookSale::total)\n                    .sum();\n            List<String> titles = sales.stream()\n                    .map(BookSale::getTitle)\n                    .distinct()\n                    .sorted()\n                    .collect(Collectors.toList());\n            System.out.println(\"Sales: \" + sales.size());\n            System.out.println(\"Copies: \" + copies);\n            System.out.println(\"Takings: \" + takings + \" dollars\");\n            System.out.println(\"Titles: \" + titles);\n        } catch (Exception e) {\n            System.out.println(\"Error: \" + e.getMessage());\n        }\n    }\n}\n\n// ==== BookSale.java ====\npublic class BookSale {\n    private String title;\n    private int copies;\n    private int price;\n\n    public BookSale(String title, int copies, int price) {\n        this.title = title;\n        this.copies = copies;\n        this.price = price;\n    }\n\n    public String getTitle() {\n        return this.title;\n    }\n\n    public int getCopies() {\n        return this.copies;\n    }\n\n    public int getPrice() {\n        return this.price;\n    }\n\n    public int total() {\n        return this.copies * this.price;\n    }\n}\n","hints":["Read the file as in step 8: `Files.lines(Paths.get(\"sales.csv\"))`, a `map` that splits each line, a `map` that makes a `new BookSale(...)` with the two numbers turned into ints, and `collect(Collectors.toList())`. Do it all inside the `try` block.","Then one stream per answer: the list's `size()` for the sales, `mapToInt(BookSale::getCopies).sum()`, `mapToInt(BookSale::total).sum()`, and `map(BookSale::getTitle)` with `distinct()`, `sorted()` and `collect`."],"tests":[{"name":"Test 1","stdin":"","files":{"sales.csv":"Dune,2,15\nEmma,1,12\nBeloved,3,14\nDune,1,15\nKindred,4,11\nEmma,1,12\n"},"expect":"Sales: 6\nCopies: 12\nTakings: 155 dollars\nTitles: [Beloved, Dune, Emma, Kindred]","hidden":false},{"name":"Test 2","stdin":"","files":{"sales.csv":"Walden,1,10\n"},"expect":"Sales: 1\nCopies: 1\nTakings: 10 dollars\nTitles: [Walden]","hidden":true},{"name":"Test 3","stdin":"","files":{"sales.csv":"Candide,2,9\nUlysses,1,20\nCandide,5,9\nBeloved,10,14\n"},"expect":"Sales: 4\nCopies: 18\nTakings: 223 dollars\nTitles: [Beloved, Candide, Ulysses]","hidden":true},{"name":"Test 4","stdin":"","files":{"till.csv":"Dune,2,15\n"},"expect":"Error: sales.csv","hidden":true}],"require":[{"pattern":"\\bFiles\\s*\\.\\s*lines\\s*\\(","message":"Read the file with `Files.lines`."},{"pattern":"\\.distinct\\s*\\(\\s*\\)","message":"Keep each title once with `distinct()`."},{"pattern":"\\.sorted\\s*\\(\\s*\\)","message":"Sort the titles with `sorted()`."}],"forbid":[{"pattern":"\\b(for|while)\\b","message":"Use streams for this one: no `for` or `while`."},{"pattern":"\\bScanner\\b","message":"Read the file with `Files.lines`, not a Scanner."}]}]}