Skip to content
wannacode

Text up close

Problem

The report that drags

You already know that title.toUpperCase(); does nothing on its own: a string is immutable, and the method returns a new string. This has a cost, and you can see it in a loop.

The online store's program builds the daily report line by line:

String report = "";
for (int i = 1; i <= 3; i++) {
report += "Order " + i + ": €12.50\n";
}
System.out.print(report);
Output
Order 1: €12.50
Order 2: €12.50
Order 3: €12.50

With three orders it is instant. On Saturday there are 50,000 orders, and the same loop takes about 2 seconds. We measured it on the machine where we check the code of the lessons.

Why? Each report += ... does not add to the old string. It makes a new string, copies the whole report so far into it, and only then glues on the new line. Near the end, every iteration copies almost a million characters.

The idea

StringBuilder: text that changes

To build text from many pieces, there is StringBuilder. It is an object that keeps the text in its own buffer and adds in place, without copying everything again:

StringBuilder report = new StringBuilder();
for (int i = 1; i <= 3; i++) {
report.append("Order ").append(i).append(": €12.50\n");
}
System.out.print(report.toString());
Output
Order 1: €12.50
Order 2: €12.50
Order 3: €12.50
  • new StringBuilder() creates an empty "builder".
  • append(...) adds to the end. It takes text, a number or a character, and returns the same object, so you can chain the calls one after another.
  • toString() makes a plain String from it, once, at the end.

With 50,000 orders this version takes about 7 milliseconds instead of 2 seconds.

Here is what happens in memory with each approach. To show that the buffer is not copied, here it is created with room for 64 characters.

String report = "";
for (int i = 1; i <= 3; i++) {
report += "Order " + i + ": €12.50\n";
}
StringBuilder builder = new StringBuilder(64);
for (int i = 1; i <= 3; i++) {
builder.append("Order ").append(i).append(": €12.50\n");
}
System.out.println(report.length() + " " + builder.length());
System.out.println(report.equals(builder.toString()));
Step by step, in words
  1. report points to an empty string.
  2. The first += makes a new String with one line: 16 characters.
  3. The second += makes one more String. It copies the old line into it (16 characters) and only then glues on the new one. Nobody needs the old string anymore.
  4. The third copies 32 characters. Every line adds to the copying: with 50,000 lines it is almost a million characters per iteration.
  5. StringBuilder is one object with a buffer. Here the buffer holds 64 characters.
  6. append writes into the buffer in place. Nothing is copied, and the object stays the same.
  7. The text is the same, 48 characters. With += another 48 were copied, and with StringBuilder not a single one.
More precisely, if you are curious
  • The buffer grows on its own: when it fills up, StringBuilder allocates one about twice as big and copies once. That is why copying is rare.
  • The compiler turns a single expression like "Order " + i + ": " + price into efficient code by itself. The problem is only += in a loop, where every iteration is a separate new string.
  • There is also StringBuffer with the same methods. It is safe when several threads write to one object at the same time, and that makes it a bit slower. Threads come in the module "Real-world programs". Until then, use StringBuilder.

See it

Changing in place

StringBuilder can do more than add. Here is a card number for printing on a receipt:

StringBuilder card = new StringBuilder("4532015112830366");
card.insert(12, ' ').insert(8, ' ').insert(4, ' ');
System.out.println(card);
card.replace(0, 14, "**** **** ****");
System.out.println(card);
StringBuilder phone = new StringBuilder("0888-123-456");
phone.deleteCharAt(8).deleteCharAt(4);
System.out.println(phone);
System.out.println(new StringBuilder("stressed").reverse());
Output
4532 0151 1283 0366
**** **** **** 0366
0888123456
desserts
Method What it does
insert(i, x) inserts x before the character at index i
replace(a, b, x) replaces the characters from a to b (without b) with x
deleteCharAt(i) removes the character at index i
delete(a, b) removes the characters from a to b (without b)
reverse() reverses the text, back to front
length(), charAt(i) the same as for String

The spaces in the card number go in from the back: first at 12, then at 8, then at 4. If you start at 4, the inserted space shifts everything after it, and 8 is no longer the place you want. The dashes in the phone number are removed from the back first for the same reason.

Try it

Two names, one object

What will the program print?

StringBuilder note = new StringBuilder("Table 4");
StringBuilder copy = note;
copy.append(", 2 coffees");
System.out.println(note);
String text = "Table 4";
String other = text;
other += ", 2 coffees";
System.out.println(text);
Answer

The idea

In alphabetical order: compareTo

equals only tells you "are they the same". To sort, you need more: which string comes before the other. That is what compareTo tells you:

System.out.println("Andreev".compareTo("Borisov"));
System.out.println("Borisov".compareTo("Borisov"));
System.out.println("ivanova".compareTo("Ivanova"));
System.out.println("ivanova".compareToIgnoreCase("Ivanova"));
Output
-1
0
32
0
  • a negative number: the first one comes before the second;
  • 0: they are the same;
  • a positive number: the first one comes after the second.

The sign matters, not the number itself. With it you sort a list of names, for example with the bubble sort from the lesson on arrays:

String[] names = { "Petrova", "Andreev", "Ivanova", "Borisov" };
for (int i = 0; i < names.length - 1; i++) {
for (int j = 0; j < names.length - 1 - i; j++) {
if (names[j].compareTo(names[j + 1]) > 0) {
String tmp = names[j];
names[j] = names[j + 1];
names[j + 1] = tmp;
}
}
}
System.out.println(String.join(", ", names));
Output
Andreev, Borisov, Ivanova, Petrova

String.join(", ", names) glues the elements of the array with a separator between them.

More precisely, if you are curious
  • compareTo compares character by character, by their codes in the character table. Capital letters come before lowercase ones, so "ivanova".compareTo("Ivanova") is positive. compareToIgnoreCase ignores upper and lower case.
  • The number is the difference between the codes of the first characters that differ (here "i" and "I" differ by 32), or between the lengths, if one string is the start of the other.
  • To sort by the rules of a language, for example with accented letters like "é", there is java.text.Collator.getInstance(Locale.of("en")).
  • java.util.Arrays.sort(names) sorts an array of strings by the same rule, without writing the bubble sort.

Try it

Invoice 10 or 9?

The invoice numbers are text. Which one is earlier?

What will the program print?

String a = "INV-10";
String b = "INV-9";
System.out.println(a.compareTo(b) < 0);
System.out.println(Integer.parseInt(a.substring(4)) < Integer.parseInt(b.substring(4)));
Answer

The idea

Search, cut, put together

Here is a line from a contacts file: name, email and city, separated with " ; ", with extra spaces at the ends:

String line = " Elena Koleva ; elena.koleva@company.com ; Plovdiv ";
String[] parts = line.strip().split(" ; ");
String email = parts[1];
int at = email.indexOf('@');
System.out.println(email.substring(0, at));
System.out.println(email.substring(at + 1));
System.out.println(email.indexOf('.', at));
System.out.println(String.join(" | ", parts));
System.out.println("=".repeat(20));
Output
elena.koleva
company.com
20
Elena Koleva | elena.koleva@company.com | Plovdiv
====================
  • strip() removes the spaces at the front and at the back, like trim(), but it also knows the "wide" spaces from other languages. Use it in new code.
  • indexOf('@') gives the index of the first occurrence, and indexOf('.', at) starts searching only from index at on. lastIndexOf searches from the back.
  • You already know substring and split. String.join is the opposite of split.
  • "=".repeat(20) repeats the text 20 times.

Empty and "blank" are not the same thing:

String empty = "";
String spaces = " ";
System.out.println(empty.isEmpty() + " " + spaces.isEmpty());
System.out.println(empty.isBlank() + " " + spaces.isBlank());
Output
true false
true true

isEmpty() is true only for text of length 0. isBlank() is also true when there are only spaces. To check a field someone filled in, you usually want isBlank().

The idea

Numbers and text

Numbers go into text and come out of text:

int code = Integer.parseInt("0042");
double price = Double.parseDouble("2.40");
String label = String.valueOf(code) + "-" + String.valueOf(price);
System.out.println(label);
Output
42-2.4

You already know Integer.parseInt and Double.parseDouble. String.valueOf is the opposite: it makes text from a number, a character or a boolean.

For a table with columns you need String.format from the lesson on input, with a width for each column. Here is a receipt from a café:

String[] names = { "Coffee", "Croissant", "Mineral water" };
double[] prices = { 2.40, 3.10, 1.50 };
int[] qty = { 2, 1, 3 };
double total = 0;
for (int i = 0; i < names.length; i++) {
double sum = prices[i] * qty[i];
total += sum;
System.out.println(String.format("%-16s%2d x %5.2f = %6.2f", names[i], qty[i], prices[i], sum));
}
System.out.println("-".repeat(35));
System.out.println("%-29s€%5.2f".formatted("Total", total));
Output
Coffee 2 x 2.40 = 4.80
Croissant 1 x 3.10 = 3.10
Mineral water 3 x 1.50 = 4.50
-----------------------------------
Total €12.40
  • %-16s is text in 16 characters, aligned left. %5.2f is a number in 5 characters with 2 after the point, aligned right. %2d is a whole number in 2 characters.
  • "template".formatted(...) does the same as String.format("template", ...). You call it directly on the template.
More precisely, if you are curious
  • a.concat(b) is the same as a + b. You will see it in older code.
  • The width is at least that many characters. Longer text is not cut. It pushes the column to the right.
  • Double.parseDouble("2,40") throws NumberFormatException: here the decimal mark is always a point. But String.format("%.2f", 2.4) on a computer with German or Bulgarian settings, for example, gives 2,40. For text that another program will read, use String.format(Locale.ROOT, ...).

Try it

Find the bug

"Kayak" reads the same backwards, but the program says "kayak is not a palindrome". Which line has the bug?

Pick the line with the bug.

Exercise

Text you put together

Solve the tasks in your editor. Do not print prompts.

1. Palindrome. Read a line and say whether it is a palindrome, ignoring capital letters, spaces, commas and dashes. This time reverse the text with StringBuilder.reverse() instead of a loop.

Input
Never odd or even
Output
Palindrome
Input
Never odd, never even
Output
Not a palindrome

2. A receipt with columns. The first line has the number of items. Each line after it has a name, a price and a quantity, separated with ;. Print a receipt with aligned columns and the total at the end.

Input
3
Bread;1.20;2
Cheese;9.80;1
Eggs;0.35;10
Output
Bread 2 x 1.20 = 2.40
Cheese 1 x 9.80 = 9.80
Eggs 10 x 0.35 = 3.50
------------------------------------
Total 15.70

3. Words in reverse. Read a sentence and print its words in reverse order. There may be more than one space between the words, and the result must have exactly one. Put it together with a StringBuilder.

Input
Tomorrow at 9 there is a meeting
Output
meeting a is there 9 at Tomorrow

More tasks

  • Initials. Read a full name ("Maria Ivanova Petrova") and print the initials with dots: "M. I. P." Put them together with a StringBuilder.
  • Hidden email. From elena.koleva@company.com make e***********@company.com: the first letter, then as many asterisks as there are other characters before the @. Use indexOf and repeat.
  • A tour of String. Create one string with quotes ("Practice text") and another with new String("Practice text"). Compare them with == and with equals, take substring(0, 8), split the second one on the space and print the parts, turn "100" into a number and print it with String.format("Value: %d", ...), and finally append " and more" to a StringBuilder with the first string. Predict every line before you run the program.
  • Sort ignoring case. Change the bubble sort from the compareTo card to use compareToIgnoreCase, and try it with "ivan", "Boris" and "ana".

Test

Check yourself

1. Why is report += line slow in a loop with 50,000 iterations?
2. What does "Borisov".compareTo("Andreev") return?
3. StringBuilder a = new StringBuilder("12"); StringBuilder b = a; b.append("3"); What is in a?
4. How do you turn a StringBuilder into a String?
5. What is the difference between String.format(...) and "...".formatted(...)?

Made by Ivo Gergov as part of Evo Tech's campaign for accessible programming education ·