RizTech Academy logo
RizTech Academy
Language BasicsLesson 2 of 625 min

Strings, text blocks and formatting

String is the type you will use more than any other, and it has two properties that surprise everybody exactly once. One produces code that looks like it works and does nothing. The other produces a comparison that is true in testing and false in production.

Both are worth meeting deliberately, here, rather than at 11pm before a demo.

Strings cannot be changed

A String object is immutable. Once created, its characters never change. Every method that looks like it modifies a string actually returns a new one.

String name = "priya";
name.toUpperCase();
System.out.println(name);
priya

Nothing happened. toUpperCase() built a new string containing PRIYA and threw it away, because nothing was assigned. The fix is to use the return value:

name = name.toUpperCase();

If a String method call is a statement on its own, it is almost certainly a bug. replace, trim, strip, substring, concat — all of them return rather than modify.

Immutability is not an accident. It makes strings safe to share between threads, safe to use as Map keys, and cacheable. The price is that building a string piece by piece creates a lot of garbage, which the StringBuilder section below is about.

The == trap

String a = "Pune";
String b = "Pune";
String c = new String("Pune");
String d = "Pu" + "ne";

String part = "Pu";
String e = part + "ne";

System.out.println("a == b : " + (a == b));
System.out.println("a == c : " + (a == c));
System.out.println("a == d : " + (a == d));
System.out.println("a == e : " + (a == e));
System.out.println("a.equals(e) : " + a.equals(e));
a == b : true
a == c : false
a == d : true
a == e : false
a.equals(e) : true

Four identical-looking strings, three different answers from ==.

The explanation is the string pool. String literals appearing in source are stored once in a shared pool, so a and b reference the same object. d is "Pu" + "ne" where both halves are literals, so the compiler folds it into the literal "Pune" before the program ever runs — also pooled. c was built with new, which forces a fresh object. And e was assembled at runtime from a variable, so it is a new object too.

== on reference types asks "is this the same object?". It is not asking about the characters.

Here is why this is not academic. Every string that arrives from a user, a file, a database or an HTTP request is built at runtime, exactly like e:

String typed = scanner.nextLine();
if (typed == "veg") { ... }        // never true
if (typed.equals("veg")) { ... }   // correct

The first version compiles, runs, and silently never matches. It is the single most common bug in beginner Java, and it survives testing whenever the tester uses a literal.

Three habits that make it go away:

  • Always .equals() for strings. Always.
  • For case-insensitive comparison, .equalsIgnoreCase().
  • When one side might be null, put the literal first: "veg".equals(typed) cannot throw, whereas typed.equals("veg") can. Or use Objects.equals(a, b), which handles null on both sides.

The methods you will actually use

Worth keeping on one page rather than looking up each time. s is "Dal Rice Tiffin" throughout.

Method Returns Example result
s.length() Number of characters 15
s.isEmpty() Length is zero false
s.isBlank() Empty or only whitespace false
s.charAt(0) One character 'D'
s.indexOf("Rice") First position, or -1 4
s.lastIndexOf("i") Last position, or -1 13
s.contains("Tiffin") Substring present true
s.startsWith("Dal") true
s.endsWith("n") true
s.substring(4) From index to the end "Rice Tiffin"
s.substring(4, 8) From, to — end exclusive "Rice"
s.replace("Dal", "Rajma") A new string "Rajma Rice Tiffin"
s.toUpperCase() A new string "DAL RICE TIFFIN"
s.trim() Removes characters up to U+0020
s.strip() Removes Unicode whitespace — prefer this
s.split(",") String[], split on a regex
s.repeat(3) Repeated
s.equals(t) Same characters
s.equalsIgnoreCase(t) Same ignoring case
s.compareTo(t) Negative, zero or positive "apple".compareTo("banana") is -1
s.formatted(x, y) Like String.format(s, x, y)
s.chars() A stream of character codes
String.join(", ", parts) Glues an array or list together
String.valueOf(42) Any value as text "42"
Integer.parseInt("42") Text as an int 42

trim() versus strip() is worth ten seconds. trim() predates Unicode awareness and removes only characters at or below U+0020. strip(), added in Java 11, removes anything Unicode calls whitespace:

String messy = "\u2003 Dal Rice \u2003";   // \u2003 is an em space
System.out.println(messy.length());
System.out.println(messy.trim().length());
System.out.println(messy.strip().length());
12
12
8

trim() removed nothing at all. Text pasted from Word, a PDF or a web form frequently contains exactly this sort of character. Use strip().

split takes a regular expression, not a string

This one costs people an hour.

System.out.println("10.20.30".split(".").length);
System.out.println("10.20.30".split("\\.").length);
0
3

In a regular expression, . means "any character", so splitting on it consumes everything and returns an empty array. To split on a literal dot, escape it: "\\." — two backslashes, because the first escapes the second for the Java compiler, leaving \. for the regex engine. The same applies to |, *, +, ?, (, ), [, ], ^, $ and {, }.

And a second surprise in the same method:

String trailing = "a,b,,,";
System.out.println(trailing.split(",").length);
System.out.println(trailing.split(",", -1).length);
2
5

By default split discards trailing empty strings. When you are parsing a CSV row where an empty final column is meaningful — a missing pincode, say — that silently loses data. Pass -1 as the limit when reading real data.

Building text

Four ways, in increasing order of how much you should like them.

Concatenation with +. Fine for two or three pieces.

String line = customer + " owes Rs " + rupees;

String.format or .formatted. Better when there is formatting to do. The specifier table from module 1 applies.

String line = "%s owes Rs %,d.%02d".formatted(customer, paise / 100, paise % 100);

Text blocks, since Java 15. Three double quotes, a newline, then your content:

String bill = """
        Tiffin bill
        ===========
        Customer : %s
        Total    : Rs %d.%02d
        """.formatted(customer, total / 100, total % 100);
Tiffin bill
===========
Customer : Priya
Total    : Rs 2141.10

The indentation in your source is not in the string. Java finds the common leading whitespace across all lines including the closing """ and removes it. That is why the closing delimiter's position matters: move it further left and every line keeps some indentation.

Text blocks are exactly what you want for SQL, JSON and anything multi-line:

String sql = """
        SELECT name, pincode
          FROM customers
         WHERE city = 'Pune'
        """;

Note that single quotes inside need no escaping, and neither do double quotes unless you write three in a row. Two extra escapes are worth knowing: a trailing \ joins a line to the next without a newline, and \s keeps a space that would otherwise be stripped from the end of a line.

StringBuilder, when you are building in a loop. Here is why it matters:

int n = 40_000;

String slow = "";
for (int i = 0; i < n; i++) {
    slow += "x";
}

StringBuilder sb = new StringBuilder();
for (int i = 0; i < n; i++) {
    sb.append("x");
}
String fast = sb.toString();
string +=       : 90 ms
StringBuilder   : 0 ms

Your numbers will differ; the ratio will not. Because strings are immutable, slow += "x" creates a whole new string every iteration and copies everything so far into it. Forty thousand iterations copy roughly eight hundred million characters. StringBuilder keeps one growable buffer and appends.

The rule: + outside a loop, StringBuilder inside one. Do not reach for StringBuilder to join three values — the compiler already uses one behind the scenes for a single + expression, and the explicit version is just noisier.

switch on a string

Legal since Java 7, and pleasant in the expression form:

String price = switch (plan) {
    case "veg" -> "Rs 82.35";
    case "jain" -> "Rs 91.00";
    default -> "unknown plan";
};

Control flow is the next lesson but one; this is here because switch on strings uses equals internally, so it does not suffer the == trap. It is case-sensitive, and it throws NullPointerException if the value is null — which is a good reason to validate input before switching on it.

What String is not for

  • Money. The previous lesson covered this. Store paise.
  • Building large text in a loop. StringBuilder.
  • A fixed set of options — "veg", "jain", "eggs". A typo in a string compiles; a typo in an enum does not. Module 3 gets to enums, and this is the argument for them.
  • Holding a password for a long time. Strings stay in memory until garbage collected and cannot be wiped. Security-sensitive code uses char[]. You will rarely need this, and you should know why the API exists.

Check your work

Why does name.toUpperCase(); on its own line do nothing? Strings are immutable. The method returns a new string and the result is discarded. Assign it: name = name.toUpperCase();.

Why is a == b true for two "Pune" literals but a == e false when e was built from a variable? Literals are interned in the string pool and share one object. A string assembled at runtime is a new object, and == compares object identity. Use .equals().

Why does comparing user input with == fail even when the text looks identical? Input is built at runtime, so it is never the pooled literal. The comparison asks the wrong question and answers false.

Which is safer, typed.equals("veg") or "veg".equals(typed)? The second. If typed is null the first throws NullPointerException; the second returns false. Objects.equals(a, b) handles null on either side.

What does "10.20.30".split(".") return, and why? An empty array. split takes a regular expression and . matches any character. Escape it as "\\.".

What does "a,b,,,".split(",") give, and how do you keep the empty columns? Length 2 — trailing empty strings are discarded. Pass a limit of -1 to keep them, giving length 5.

Why is trim() worse than strip()? trim() only removes characters up to U+0020, so Unicode spaces pasted from other applications survive it. strip() is Unicode-aware.

Why is += in a loop slow? Each iteration allocates a new string and copies all previous characters into it, so the total work grows with the square of the length. StringBuilder appends into one buffer.

Where does the indentation of a text block go? Java removes the common leading whitespace of all lines, including the line holding the closing """. Moving that line changes the result.

Practice 2, initials. The naive version splits on " " and breaks on a double space, because split returns an empty string between the two spaces and charAt(0) on it throws StringIndexOutOfBoundsException. Splitting on the regex \s+ — one or more whitespace characters — fixes it and handles tabs too:

static String initials(String fullName) {
    StringBuilder sb = new StringBuilder();
    for (String word : fullName.strip().split("\\s+")) {
        sb.append(Character.toUpperCase(word.charAt(0))).append('.');
    }
    return sb.toString();
}
"Priya Anil Deshmukh"   ->  P.A.D.
"Priya  Anil Deshmukh"  ->  P.A.D.

The strip() matters as well: a leading space would otherwise produce an empty first element even with \s+.

Practice 3, the CSV row. "Priya Deshmukh,Wagholi,411207,".split(",", -1):

0 : [Priya Deshmukh]
1 : [Wagholi]
2 : [411207]
3 : []

Without the -1 you get three fields and the empty one vanishes.

Practice 4, masking.

static String mask(String phone) {
    if (phone.length() < 4) {
        return "X".repeat(phone.length());
    }
    return phone.substring(0, 2)
         + "X".repeat(phone.length() - 4)
         + phone.substring(phone.length() - 2);
}
"9876543210" -> 98XXXXXX10
"98765432"   -> 98XXXX32

Written against length() rather than the literal positions 2 and 8, a nine-digit or eight-digit number masks sensibly instead of throwing. The short-input branch is a decision, not an accident — deciding what a method does with bad input, rather than letting an exception decide for you, is most of what module 7 is about.

Practice 6, the timings. On the machine these lessons were written on, += took about 60 ms for 50,000 characters and about 197 ms for 100,000 — three to four times the work for twice the length, not twice. StringBuilder was under a millisecond for both. Your absolute numbers will differ; the shape will not.

Practice

  1. Reproduce the == trap and then fix it. Read a line with new Scanner(System.in).nextLine(), compare it to a literal with ==, and confirm it never matches even when you type it exactly. Then switch to .equals().

  2. Write initials(String fullName). "Priya Anil Deshmukh" becomes "P.A.D.". Use split(" "), charAt(0) and a StringBuilder. Then test it with a name that has two spaces between words and fix what you find.

  3. Parse a CSV line properly. Given "Priya Deshmukh,Wagholi,411207," — note the trailing comma — print each field with its index. Make the empty fourth field appear. That is the -1 limit.

  4. Mask a phone number. "9876543210" becomes "98XXXXXX10", for any ten-digit input. Use substring and repeat. Then decide what your method should do with a nine-digit input, and make it do that deliberately rather than throwing StringIndexOutOfBoundsException.

  5. Build a bill with a text block. Customer name, three line items with right-aligned amounts, and a total. All amounts in paise, formatted at the point of printing. Move the closing """ four spaces to the left and look at what happens to every line.

  6. Measure it yourself. Concatenate 50,000 characters with += and time it with System.nanoTime(). Then do the same with StringBuilder. Then try 100,000 with += and confirm the time grows by three to four times rather than doubling. That is what "grows with the square" means, and seeing it is more convincing than reading it.

Next: operators, and the one where += quietly changes the answer.

Stuck on this lesson?

Being stuck is part of it — but being stuck alone for three days is not. Our internship programme pairs this curriculum with code review and one-to-one help from working developers, and it is free.

About the internship