{"id":"useful-techniques-stringbuilder","slug":"stringbuilder","title":"StringBuilder: building a text piece by piece","text":"Programs often build a text from many pieces: a receipt from its lines, or a list of names with commas between them. Module 24's bookshelf built its `toString` with `text = text + \"\\n\" + book` in a loop. Here's the same idea, smaller. A seaside town rents out six beach huts, B1 to B6, which stand in a row, and its booking page draws them as a row of boxes:\n\n```java\nString row = \"\";\nfor (int hut = 1; hut <= 6; hut++) {\n    row += \"|B\" + hut;\n}\nrow += \"|\";\nSystem.out.println(row);\n```\n\n```output\n|B1|B2|B3|B4|B5|B6|\n```\n\n### Why += in a loop is slow\n\nA String never changes after it's made. `row += \"|B\" + hut` doesn't add to the old String: it makes a new one, copies every character of the old one into it, then the new piece, and the old String is thrown away. Each hut's piece is 3 characters, so the first round copies 3 characters, the second 6, the third 9, and so on. Every round copies everything built so far, once more.\n\nFor six huts, that's nothing. For 1,000 pieces of 3 characters each, the rounds copy 3 + 6 + 9 + ... + 3,000 characters: 1,501,500 in all, to make a text of 3,000 characters. Twice as many pieces means about four times as much copying, so a loop that builds a long text this way gets slower and slower as the text grows.\n\n### StringBuilder\n\nJava's **StringBuilder** is a text that can change. It keeps its characters with room to spare, and `append` writes a new piece at the end, without copying what's already there. (When the room runs out, it makes a bigger one, again with room to spare.)\n\n```java\nStringBuilder row = new StringBuilder();\nfor (int hut = 1; hut <= 6; hut++) {\n    row.append(\"|B\");\n    row.append(hut);\n}\nrow.append(\"|\");\nString text = row.toString();\nSystem.out.println(text);\n```\n\n```output\n|B1|B2|B3|B4|B5|B6|\n```\n\n- `new StringBuilder()` makes an empty one, and `new StringBuilder(\"Total\")` one that starts with a text.\n- `new StringBuilder(5)` doesn't hold 5: a number in the parentheses only sets how much room it starts with, and the text is empty. Use `new StringBuilder().append(5)`.\n- `append` adds anything to the end: a String, an int, a double, a char or a boolean.\n- `toString()` returns the finished text as a String. Printing a StringBuilder, or joining it to a String with `+`, calls it for you, as for any object. A method that returns a `String` needs it: `return row.toString();`\n- StringBuilder is in the package `java.lang`, like String, so it needs no import.\n\n`append` returns the StringBuilder itself, so the calls can be chained: `row.append(\"|B\").append(hut);` does the two appends in the loop in one statement.\n\n### A receipt\n\nHere's a receipt, built line by line. Step 1's index loop goes through the two lists side by side, and `\\n` is a line break, as in module 24:\n\n```java\nimport java.util.ArrayList;\n\npublic class Main {\n    public static void main(String[] args) {\n        ArrayList<String> items = new ArrayList<>();\n        items.add(\"Hammer\");\n        items.add(\"Nails\");\n        items.add(\"Tape\");\n        ArrayList<Integer> prices = new ArrayList<>();\n        prices.add(12);\n        prices.add(4);\n        prices.add(3);\n        System.out.println(receipt(items, prices));\n    }\n\n    public static String receipt(ArrayList<String> items, ArrayList<Integer> prices) {\n        StringBuilder text = new StringBuilder();\n        int total = 0;\n        for (int i = 0; i < items.size(); i++) {\n            text.append(items.get(i)).append(\": \").append(prices.get(i)).append(\" dollars\\n\");\n            total += prices.get(i);\n        }\n        text.append(\"Total: \").append(total).append(\" dollars\");\n        return text.toString();\n    }\n}\n```\n\n```output\nHammer: 12 dollars\nNails: 4 dollars\nTape: 3 dollars\nTotal: 19 dollars\n```\n\n### insert, reverse and length\n\n- `insert(index, text)` puts a text at a position, and moves everything after it to the right. `insert(0, ...)` adds to the front.\n- `reverse()` turns the characters around.\n- `length()` and `charAt(index)` work as they do for a String.\n\n```java\nStringBuilder code = new StringBuilder(\"4821\");\ncode.insert(0, \"#\");\ncode.insert(3, \"-\");\nSystem.out.println(code);\ncode.reverse();\nSystem.out.println(code + \" has \" + code.length() + \" characters\");\n```\n\n```output\n#48-21\n12-84# has 6 characters\n```\n\nAfter the first insert, the text is `#4821`, and `insert(3, \"-\")` puts the dash in front of index 3, the 2.\n\n### Comparing the text\n\nA StringBuilder has no `equals` of its own. It has Object's, which asks whether two variables refer to the same object (modules 23 and 34), so a StringBuilder is never equal to a String. To compare the text, turn it into a String first:\n\n```java\nString word = \"level\";\nStringBuilder backwards = new StringBuilder(word).reverse();\nSystem.out.println(backwards.equals(word));\nSystem.out.println(backwards.toString().equals(word));\n```\n\n```output\nfalse\ntrue\n```\n\n\"level\" reads the same backwards, but only the second line says so.\n\n`+` is still the clearest way to join a few pieces in one expression, as in `return this.name + \" (\" + this.age + \")\";`. StringBuilder is for a text that grows in a loop.\n","task":"Write the method `guestList(ArrayList<String> names)`, which returns the text `Guests: ` followed by the names with a comma and a space between them. For an empty list it returns `Guests: none`. Build the text with a StringBuilder and `append`. The check runs:\n\n```java\nArrayList<String> names = new ArrayList<>();\nnames.add(\"Ada\");\nnames.add(\"Ben\");\nnames.add(\"Cy\");\nSystem.out.println(guestList(names));\n```\n\nand expects:\n\n```\nGuests: Ada, Ben, Cy\n```\n","kind":"code","seed":"import java.util.ArrayList;\n\npublic class Main {\n    public static void main(String[] args) {\n        ArrayList<String> names = new ArrayList<>();\n        names.add(\"Ada\");\n        names.add(\"Ben\");\n        names.add(\"Cy\");\n        System.out.println(guestList(names));\n    }\n\n    public static String guestList(ArrayList<String> names) {\n        return \"\";\n    }\n}\n","solution":"import java.util.ArrayList;\n\npublic class Main {\n    public static void main(String[] args) {\n        ArrayList<String> names = new ArrayList<>();\n        names.add(\"Ada\");\n        names.add(\"Ben\");\n        names.add(\"Cy\");\n        System.out.println(guestList(names));\n    }\n\n    public static String guestList(ArrayList<String> names) {\n        if (names.isEmpty()) {\n            return \"Guests: none\";\n        }\n        StringBuilder list = new StringBuilder(\"Guests: \");\n        for (int i = 0; i < names.size(); i++) {\n            if (i > 0) {\n                list.append(\", \");\n            }\n            list.append(names.get(i));\n        }\n        return list.toString();\n    }\n}\n","hints":["Handle the empty list first, with `isEmpty()`. Otherwise start with `new StringBuilder(\"Guests: \")` and append the names in a loop.","The comma goes *between* names: append `\", \"` before every name except the first, which an index loop can tell by `i > 0`. Return `list.toString()` at the end."],"tests":[{"name":"Test 1","stdin":"","call":"ArrayList<String> names = new ArrayList<>();\nnames.add(\"Ada\");\nnames.add(\"Ben\");\nnames.add(\"Cy\");\nSystem.out.println(guestList(names));\n","expect":"Guests: Ada, Ben, Cy","hidden":false},{"name":"Test 2","stdin":"","call":"ArrayList<String> names = new ArrayList<>();\nSystem.out.println(guestList(names));\nnames.add(\"Zoe\");\nSystem.out.println(guestList(names));\n","expect":"Guests: none\nGuests: Zoe","hidden":true},{"name":"Test 3","stdin":"","call":"ArrayList<String> names = new ArrayList<>();\nnames.add(\"Maria Lopez\");\nnames.add(\"Tom\");\nnames.add(\"Ines\");\nnames.add(\"Tom\");\nSystem.out.println(guestList(names));\nSystem.out.println(names.size());\n","expect":"Guests: Maria Lopez, Tom, Ines, Tom\n4","hidden":true}],"require":[{"pattern":"\\bnew\\s+StringBuilder\\s*\\(","message":"Build the text in a `StringBuilder`."},{"pattern":"\\.append\\s*\\(","message":"Add the pieces with `append`."}],"forbid":[{"pattern":"\\+=","message":"Build the text with `append`, not with `+=`."},{"pattern":"\\bString\\s*\\.\\s*join\\b","message":"`String.join` would do it for you: build the text with a StringBuilder this time."}],"more":[{"task":"What does this program print?\n","kind":"predict","seed":"public class Main {\n    public static void main(String[] args) {\n        StringBuilder text = new StringBuilder(\"cat\");\n        text.append(\"s\").insert(0, \"3 \");\n        System.out.println(text + \" \" + text.length());\n        text.reverse();\n        System.out.println(text);\n        StringBuilder same = text;\n        same.append(\"!\");\n        System.out.println(text);\n    }\n}\n","solution":"public class Main {\n    public static void main(String[] args) {\n        StringBuilder text = new StringBuilder(\"cat\");\n        text.append(\"s\").insert(0, \"3 \");\n        System.out.println(text + \" \" + text.length());\n        text.reverse();\n        System.out.println(text);\n        StringBuilder same = text;\n        same.append(\"!\");\n        System.out.println(text);\n    }\n}\n","hints":["`append(\"s\")` gives `cats`, and it returns the same StringBuilder, so `insert(0, \"3 \")` adds to its front: `3 cats`, which is 6 characters long, the space included.","`reverse()` turns the whole text around, the space too. `same = text` makes no copy: both variables refer to the one StringBuilder, so the `!` shows up when `text` is printed."],"tests":[{"name":"Output","stdin":"","expect":"3 cats 6\nstac 3\nstac 3!","hidden":false}],"require":[],"forbid":[],"lines":["3 cats 6","stac 3","stac 3!"]},{"task":"Big numbers are easier to read with a comma between every three digits, counted from the right: 1,234,567. Write the method `withCommas(int number)`, which returns the number written that way, for any number from 0 up. Put the digits in a StringBuilder, and insert the commas with a `for` loop that goes backwards from the end, three digits at a time. The check runs:\n\n```java\nSystem.out.println(withCommas(1234567));\nSystem.out.println(withCommas(950));\nSystem.out.println(withCommas(20000));\n```\n\nand expects:\n\n```\n1,234,567\n950\n20,000\n```\n","kind":"code","seed":"public class Main {\n    public static void main(String[] args) {\n        System.out.println(withCommas(1234567));\n        System.out.println(withCommas(950));\n        System.out.println(withCommas(20000));\n    }\n\n    public static String withCommas(int number) {\n        return \"\" + number;\n    }\n}\n","solution":"public class Main {\n    public static void main(String[] args) {\n        System.out.println(withCommas(1234567));\n        System.out.println(withCommas(950));\n        System.out.println(withCommas(20000));\n    }\n\n    public static String withCommas(int number) {\n        StringBuilder digits = new StringBuilder();\n        digits.append(number);\n        for (int i = digits.length() - 3; i > 0; i -= 3) {\n            digits.insert(i, \",\");\n        }\n        return digits.toString();\n    }\n}\n","hints":["`new StringBuilder().append(number)` holds the digits (`new StringBuilder(number)` would only set its starting room and stay empty). The first comma goes in front of the last three digits, at index `length() - 3`, and each comma after that three places further left.","Loop with `for (int i = digits.length() - 3; i > 0; i -= 3)` and `digits.insert(i, \",\")`. Going from the right, the commas already added are all to the right of `i`, so they don't move the digits still to come. `i > 0` stops a comma from going in front of the first digit."],"tests":[{"name":"Test 1","stdin":"","call":"System.out.println(withCommas(1234567));\nSystem.out.println(withCommas(950));\nSystem.out.println(withCommas(20000));\n","expect":"1,234,567\n950\n20,000","hidden":false},{"name":"Test 2","stdin":"","call":"System.out.println(withCommas(0));\nSystem.out.println(withCommas(999));\nSystem.out.println(withCommas(1000));\nSystem.out.println(withCommas(100000));\n","expect":"0\n999\n1,000\n100,000","hidden":true},{"name":"Test 3","stdin":"","call":"System.out.println(withCommas(2147483647));\nSystem.out.println(withCommas(12345));\nSystem.out.println(withCommas(7));\n","expect":"2,147,483,647\n12,345\n7","hidden":true}],"require":[{"pattern":"\\bnew\\s+StringBuilder\\s*\\(","message":"Put the digits in a `StringBuilder`."},{"pattern":"\\.insert\\s*\\(","message":"Add the commas with `insert`."},{"pattern":"\\bfor\\s*\\(","message":"Insert the commas in a `for` loop that goes backwards, three digits at a time."}],"forbid":[{"pattern":"\\bString\\s*\\.\\s*format\\b|%,","raw":true,"message":"Java can format numbers for you, but here the point is to insert the commas yourself."}]}]}