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.
String a = "hi";
String b = "hi";
String c = new String("hi");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.
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()));
}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.
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.
- 1public class Main {
- 2 public static void main(String[] args) {
- 3 String s = "duke";
- 4 String t = s;
- 5 s = s + "!";
- 6 s.toUpperCase();
- 7 String u = s.toUpperCase();
- 8 System.out.println(t + " " + s + " " + u);
- 9 }
- 10}
·main starts.
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):
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());
}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:
String csv = "";
for (int i = 0; i < 10_000; i++) {
csv += i + ",";
}10,000 rounds, each copying everything built so far. quadratic.
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.
void main() {String html = """<ul>··<li>tea</li></ul>""";}
<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.
""" 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.
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.
- 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: useStringBuilder.- text blocks strip the indentation your code added, decided by the least indented line.