skip to content
everything.basics.strings101

strings, the basics

string literals, immutability, concatenation and text blocks.

chapter
chapter 02.05
reading time
8 min read

a String is an object holding text, and it's immutable: every method that seems to change it returns a new string instead. string literals are shared through the string pool, which is why == sometimes seems to work. compare strings with .equals(), always. and """ text blocks make multi-line strings painless.

the picture

a String is not a primitive. it's an object on the heap, and your variable holds a reference to it, exactly like the Point in stack and heap. it just gets special treatment: literals in quotes, + to glue, and a shared pool for literals.

java
String a = "hi";
String b = "hi";
String c = new String("hi");
stack
main
a
b
c
heap
Stringstring pool
"hi"
String
"hi"
two literals with the same text share one object from the string pool. new String always makes a separate object, even with the same characters. three references, two objects.

== vs equals

you know how == on two strings sometimes works and sometimes doesn't? kinda cursed. here's why: == on references asks "same object?", not "same text?". literals share a pooled object, so == happens to say yes. strings built at runtime are new objects, so it says no, even when the text is identical.

Main.java
void main() {
    String a = "hi";
    String b = "hi";
    String c = new String("hi");
    String d = "h" + "i";
    String h = "h";
    String e = h + "i";
    IO.println((a == b) + " " + (a == c) + " " + a.equals(c) + " " + (a == d) + " " + (a == e) + " " + (a == e.intern()));
}
output
true false true true false true

"h" + "i" is folded into "hi" by the compiler, so it's a literal and lands in the pool. h + "i" happens at runtime and makes a new object. intern() looks a string up in the pool and returns the pooled copy. none of that is something you want your program's correctness to depend on.

predict the output
java
String name = IO.readln("name? ");   // you type: duke
if (name == "duke") IO.println("same");
else IO.println("different");

immutable means new objects

no String method ever changes the string you call it on. toUpperCase, replace, trim, +: each one builds a new string and returns it. if you don't store the result, it's gone.

Main.java next to run
  1. 1public class Main {
  2. 2 public static void main(String[] args) {
  3. 3 String s = "duke";
  4. 4 String t = s;
  5. 5 s = s + "!";
  6. 6 s.toUpperCase();
  7. 7 String u = s.toUpperCase();
  8. 8 System.out.println(t + " " + s + " " + u);
  9. 9 }
  10. 10}
stdout
·
stack
main
no locals yet
heap
nothing allocated yet.

main starts.

step 1/7
step through. watch the arrows move and the old strings stay exactly as they were. the red one at step 5 is a result nobody kept.

so s.toUpperCase(); on its own line is a bug: it does the work and throws it away. you meant s = s.toUpperCase();.

the methods you'll use all the time (the full tour is in the string api):

Main.java
void main() {
    String s = "  Hello, Duke!  ";
    String t = s.strip();
    IO.println(t.length() + " " + t.charAt(0) + " " + t.indexOf("Duke") + " " + t.substring(7, 11));
    IO.println(t.toUpperCase() + " " + t.replace('l', 'L') + " " + t.contains("Duke") + " " + t.startsWith("Hell"));
    IO.println(String.join("|", "a", "b", "c") + " " + "ab".repeat(3) + " " + "   ".isBlank() + " " + "".isEmpty());
}
output
12 H 7 Duke
HELLO, DUKE! HeLLo, Duke! true true
a|b|c ababab true true

indexes start at 0, and substring(7, 11) means "from index 7 up to, but not including, 11". that half-open range is everywhere in java.

gluing strings together

+ with a String on either side means concatenation, and it reads left to right: 1 + 2 + "3" is "33" but "1" + 2 + 3 is "123" (more in operators). careful with chars: 'a' + 'b' is the int 195, because two chars add as numbers. "" + 'a' + 'b' is "ab".

in a loop, + gets expensive, because every round copies the whole string so far into a new one:

don't+ in a loop: a new string every time
java
String csv = "";
for (int i = 0; i < 10_000; i++) {
    csv += i + ",";
}

10,000 rounds, each copying everything built so far. quadratic.

doStringBuilder: one growing buffer
java
var sb = new StringBuilder();
for (int i = 0; i < 10_000; i++) {
    sb.append(i).append(',');
}
String csv = sb.toString();

one mutable buffer that grows as needed, one final string.

outside loops, just use +. the compiler makes it fast.

text blocks

for multi-line text (json, sql, html), a text block opens with """ and a line break, and closes with """ java 15+. no \n, no escaping quotes. the clever part is the indentation: java strips the spaces that are only there because your code is indented, and keeps the rest.

closing """ =
in the source
void main() {
String html = """
<ul>
··<li>tea</li>
</ul>
""";
}
incidental: stripped·· kept
the String you getlength() = 26
<ul>⏎
··<li>tea</li>⏎
</ul>⏎

the closing quotes line up with the content, so all 8 leading spaces are incidental. the string starts at <ul>, and ends with a newline.

move the closing """ and watch the result. the shaded spaces are incidental (just code indentation) and get stripped; the amber dots are spaces that end up in the string. lengths are from a real run.

the rule: java finds the smallest indentation among the content lines (counting the closing """ line if it's on its own), and removes exactly that much from every line. trailing spaces on each line are stripped too. two escapes help: \s is a space that won't be stripped, and a \ at the end of a line joins it with the next one.

one level deeper:what a String looks like inside+

since java 9 (JEP 254, "compact strings"), a String holds a byte[] plus a one-byte coder. if every character fits in Latin-1, it uses one byte per character; otherwise two (UTF-16). most strings in most programs are Latin-1, so this roughly halved the memory strings use. also since java 9 (JEP 280), a + b + c compiles to a single invokedynamic call that sizes the result once, instead of a chain of StringBuilder.append calls. that's why plain + is fine outside loops. and the hashCode is computed once and cached in the object, which is only safe because the string can never change.

keylines
  • a String is an object. the variable holds a reference.
  • strings never change. every "modifying" method returns a new string, so keep the result.
  • == asks "same object?". .equals() asks "same text?". you almost always mean equals.
  • literals are pooled, which is why == sometimes looks like it works. don't rely on it.
  • + is fine, except in loops: use StringBuilder.
  • text blocks strip the indentation your code added, decided by the least indented line.