Learn Java

Lesson 20Phase 2: Objects

Strings, StringBuilder, StringBuffer

Why strings are immutable, the most useful String methods, and when to reach for a builder.

By Updated 60 min

  • String
  • immutability
  • length
  • charAt
  • substring
  • StringBuilder

You have used String since your first program, the moment you printed a line of text. It has felt like any other value, sitting in a variable the way an int does. Underneath, a String is an object with an unusual rule: once it exists, its text never changes. This lesson shows how the string methods live with that rule, why comparing two strings with == is one of the most common mistakes in Java, and which class to use when you need text that does change.

By the end of this lesson
  • Explain what it means that a String is immutable, and predict why a call such as toUpperCase() leaves the original alone.
  • Pick the right method by name: length, charAt, substring, indexOf, contains, replace, trim, the case methods and a few newer helpers.
  • Compare strings with equals, and predict when == says true or false for strings made in different ways.
  • Put strings in dictionary order with compareTo.
  • Build text in a loop with a StringBuilder, and say what StringBuffer adds.
From before
You have everything this page needs. Loops walk across text, arrays taught you to count positions from zero, and methods take arguments and hand a value back. A string method is a method you call on a piece of text. From the this keyword and object references comes the idea that a variable holds a reference to an object, and that == asks whether two references point at the same object. And the Scanner recipe from making decisions reads what someone types on the keyboard.

The one surprising thing: a string never changes#

Start with a question that sounds as if it has an obvious answer. A string holds text in lower case, you want it in upper case, so you call toUpperCase() on it. What does the string hold afterwards? Decide, then press Run.

Immutable.java
1public class Main {
2 public static void main(String[] args) {
3 String greeting = "hello";
4
5 greeting.toUpperCase();
6
7 System.out.println(greeting);
8 }
9}
Prints
›hello

It still prints hello. The call did run, but the upper-case text it made went nowhere, because nothing kept it. A String method never rewrites the original text: once a Java string exists, the characters inside it never change. A String is immutable, which means it cannot be changed after it is made.

So toUpperCase() gives back a new string, separate from the original. To use it, store it in a variable.

Immutable.java
1public class Main {
2 public static void main(String[] args) {
3 String greeting = "hello";
4 String shout = greeting.toUpperCase();
5
6 System.out.println(greeting);
7 System.out.println(shout);
8 }
9}
Prints
›hello
›HELLO

Now both are there. greeting still holds hello, and shout holds the new string HELLO that toUpperCase() built and returned. Nothing was edited: there is the old string and the new one.

Key idea

A String method that seems to change a string returns a new string and leaves the original as it was. If you do not store the return value, the work is lost.

Coming from Python or JavaScript?
Strings are immutable there too. Python's "hi".upper() and JavaScript's "hi".toUpperCase() also return a new string and leave the original alone.
Quick check
After these three lines run, what does name hold? String name = "ada"; then name.toUpperCase(); then name = name + "!";

The methods you will use most#

A string carries a set of methods. None of them change the string. Each one reads it and hands back a number, a character, a boolean or a new string.

Length and one character at a time

length() tells you how many characters a string has. charAt(i) hands you the single character at position i, counting from zero, like an array index. The first character is at 0 and the last is at length() - 1.

Reaching.java
1public class Main {
2 public static void main(String[] args) {
3 String word = "Hello, World";
4
5 System.out.println(word.length());
6 System.out.println(word.charAt(0));
7 System.out.println(word.charAt(4));
8 System.out.println(word.charAt(word.length() - 1));
9 }
10}
Prints
›12
›H
›o
›d

That prints 12, then H, then o, then d. The comma and the space both count toward the length of 12. A space is a character like any other.

charAt past the end stops the program
On a 12-character string, word.charAt(12) stops the program with StringIndexOutOfBoundsException: Index 12 out of bounds for length 12, because the last index is 11. It is the same off-by-one mistake as walking off the end of an array. A loop over a string runs while i < word.length(), never i <= word.length().

Pulling out a piece with substring

substring(start, end) returns the part of the text from index start up to, but not including, index end. With one argument, substring(start) runs from start to the end of the string.

Slice.java
1public class Main {
2 public static void main(String[] args) {
3 String word = "abcdef";
4
5 System.out.println(word.substring(2, 5));
6 System.out.println(word.substring(3));
7 }
8}
Prints
›cde
›def

The first line prints cde: the characters at indexes 2, 3 and 4, stopping just before 5. The second prints def, from index 3 to the end. Because the character at end is left out, substring(a, b) always has exactly b - a characters: substring(2, 5) has 3.

Searching: indexOf and contains

indexOf(text) finds where a smaller piece of text first appears and returns the index where it starts, or -1 when it is not there at all. contains(text) answers the plain yes-or-no question with a boolean.

Search.java
1public class Main {
2 public static void main(String[] args) {
3 String sentence = "Hello, World";
4
5 System.out.println(sentence.indexOf("World"));
6 System.out.println(sentence.indexOf("zebra"));
7 System.out.println(sentence.contains("World"));
8 System.out.println(sentence.contains("zebra"));
9 }
10}
Prints
›7
›-1
›true
›false

This prints 7, then -1, then true, then false. World starts at index 7. zebra is not in the sentence, so indexOf answers -1 and contains answers false.

-1 means not found
No character sits at index -1; the number is a code for “not found”. Check for it before you use the result: if (sentence.indexOf("World") != -1)means “if the text is there”. Passing a -1 straight to charAt or substring stops the program with an exception.

Case, trimming and replacing

toUpperCase() and toLowerCase() return a new string in that case. trim() returns a copy without the spaces at the start and the end, which is handy for text someone typed. replace(a, b) returns a copy with every a swapped for b. All of them leave the original alone.

Reshape.java
1public class Main {
2 public static void main(String[] args) {
3 String messy = " Hello ";
4
5 System.out.println(messy.trim());
6 System.out.println(messy.toUpperCase());
7 System.out.println("banana".replace("a", "o"));
8
9 System.out.println(messy);
10 }
11}
Prints
›Hello
› HELLO
›bonono
› Hello

It prints Hello with the padding gone, then HELLO in upper case with its spaces still around it, then bonono. The last line prints the original, padding and all: messy never changed.

Method
Returns
length()
an int, the number of characters
charAt(i)
the single char at index i
substring(a, b)
a new String, from index a up to but not including b
indexOf(text)
an int, the index where text starts, or -1
contains(text)
a boolean, whether text is there
trim()
a new String without the spaces at both ends
replace(a, b)
a new String with every a turned into b
toUpperCase(), toLowerCase()
a new String in that case
Quick check
A file name is stored in String file = "report.pdf";. Which expression gives just pdf?

A few more you will meet#

The String class has dozens of methods. A few more come up often enough to know by name. Several of them arrived in Java 11, so older tutorials may not show them.

MoreMethods.java
1public class Main {
2 public static void main(String[] args) {
3 String file = "holiday-photo.JPG";
4
5 System.out.println(file.startsWith("holiday"));
6 System.out.println(file.toLowerCase().endsWith(".jpg"));
7 System.out.println(file.lastIndexOf("o"));
8 System.out.println(" ".isEmpty() + " " + " ".isBlank());
9 System.out.println("ha".repeat(3));
10
11 String[] colors = "red,green,blue".split(",");
12 System.out.println(colors.length + " colors, the first is " + colors[0]);
13 System.out.println(String.join(" + ", colors));
14 }
15}
Prints
›true
›true
›12
›false true
›hahaha
›3 colors, the first is red
›red + green + blue

The name starts with holiday, so the first line is true. The second line calls one method on the result of another: toLowerCase() returns a new string, and endsWith runs on that string, so the capital JPG still counts and the answer is true. The last o is at index 12. A string of three spaces is not empty, since its length is 3, but it is blank, because it holds only spaces: false true. repeat(3) gives hahaha. Finally split(",") cuts the text at every comma and hands back the pieces in an array, and String.join glues them back together with a plus sign between them.

MethodWhat it gives back
startsWith(text), endsWith(text)true when the string begins, or ends, with text
lastIndexOf(text)the index where text starts for the last time, or -1
isEmpty()true when the length is 0
isBlank()true when the string is empty or holds only white space such as spaces and tabs (Java 11)
strip()like trim(), and it also removes the less common kinds of white space that Unicode defines (Java 11)
repeat(n)a new string with the text n times over (Java 11)
split(separator)an array of the pieces between the separators
String.join(between, pieces)one string with between placed between the pieces
Every method here leaves the original string unchanged.
split takes a pattern
The separator you give splitis a pattern, and a few characters mean something special in a pattern. A dot means “any character”, so "a.b".split(".") does not split at the dot. Regular expressions explains patterns and how to split on a dot.

Putting a few together

A lot of string work is a loop plus charAt. This program counts how many times the letter a appears in a word.

CountLetter.java
1public class Main {
2 public static void main(String[] args) {
3 String word = "banana";
4 int count = 0;
5
6 for (int i = 0; i < word.length(); i++) {
7 if (word.charAt(i) == 'a') {
8 count++;
9 }
10 }
11
12 System.out.println("a appears " + count + " times");
13 }
14}
Prints
›a appears 3 times

Step through it and watch count climb. banana has an a at indexes 1, 3 and 5, so the last line is a appears 3 times. The test word.charAt(i) == 'a' uses == on two char values, and that is correct: a char is a primitive, one number underneath, so == compares the values. A whole string is an object, and that changes what == means, as the next section shows.

Comparing strings: equals, not ==#

To ask whether two strings hold the same text, use the equals method.

Equals.java
1public class Main {
2 public static void main(String[] args) {
3 String a = "cat";
4 String b = "Cat";
5
6 System.out.println(a.equals(b));
7 System.out.println(a.equals("cat"));
8 System.out.println(a.equalsIgnoreCase(b));
9 }
10}
Prints
›false
›true
›true

This prints false, then true, then true. equals compares the characters one by one, and it cares about case: cat and Cat differ in the first letter, so the answer is false. When case should not matter, equalsIgnoreCase compares as if both were in the same case, so cat and Cat come out equal.

Why not ==, which worked for char values a moment ago? Because a String is an object, and for objects == answers a different question. It does not compare the text; it checks whether two variables point at the very same object. You met this in object references: a String variable holds a reference to a string object in memory. Two separate string objects can hold the same letters, and then == says false even though the text matches.

Whether two strings with the same letters are one object or two depends on how each one was made. Decide what this prints before you run it.

Predict the outputSameText.java
1public class Main {
2 public static void main(String[] args) {
3 String a = "cat";
4 String b = "cat";
5 String c = new String("cat");
6
7 System.out.println(a == b);
8 System.out.println(a == c);
9 System.out.println(a.equals(c));
10 }
11}

All three variables hold the letters c, a, t. What do the three lines print?

It prints true, false, true. A string literal is text written in quotes in your code, such as "cat". Java keeps one shared object for each different literal, in a table called the string pool. a and b were both written as the literal "cat", so they point at the same pooled object and a == b is true. new String("cat") always makes a new object holding a copy of the letters, so a == c is false. equals compares the letters, so a.equals(c) is true.

The bug: comparing what someone typed#

With only literals in a program, == can look as if it works. The trouble starts with text that only exists once the program is running. This program asks a yes-or-no question and checks the answer both ways. The Keyboard input box holds yes, as if someone typed it and pressed Enter. One method is new: in.nextLine() reads everything typed up to the Enter key and hands it back as a String.

SaveCheck.java
1import java.util.Scanner;
2
3public class Main {
4 public static void main(String[] args) {
5 Scanner in = new Scanner(System.in);
6 System.out.println("Save your changes before closing? (yes/no)");
7 String answer = in.nextLine();
8
9 if (answer == "yes") {
10 System.out.println("== says: saving");
11 } else {
12 System.out.println("== says: not saving");
13 }
14
15 if (answer.equals("yes")) {
16 System.out.println("equals says: saving");
17 } else {
18 System.out.println("equals says: not saving");
19 }
20 }
21}
Prints
›Save your changes before closing? (yes/no)
›yes== says: not saving
›equals says: saving

Someone typed exactly yes, and the == check still decides not to save. nextLine() built a new String object from the keystrokes, while the "yes" in the code is the pooled literal: the same letters in two objects, so == is false. equals compares the letters and gets it right. Step through it and read the narration on the first if, which shows both sides holding yes and the answer false. Then change the input to no and run it again: both checks now agree.

== on strings fails without a sound
== often gives the right answer while every string in a program is a literal, so it can pass a quick test. Then the same check meets text read from the keyboard, a file or a calculation, and it quietly answers false, with no error message to tell you. For text, use a.equals(b), or a.equalsIgnoreCase(b) when case should not matter.

Which strings are the same object?#

Here is the whole rule. Every literal is the pooled object for its text. So is any text the compiler can work out before the program runs, such as "ca" + "t": the compiler joins the two literals into "cat" and uses the pooled one. Text made while the program runs is a new object each time, whether it comes from new String(...), from the keyboard, or from + with a variable in it. The method intern()looks a string's text up in the pool and hands back the pooled object.

Pool.java
1public class Main {
2 public static void main(String[] args) {
3 String word = "cat";
4 String joined = "ca" + "t";
5 String start = "ca";
6 String built = start + "t";
7
8 System.out.println(word == joined);
9 System.out.println(word == built);
10 System.out.println(word.equals(built));
11 System.out.println(word == built.intern());
12 }
13}
Prints
›true
›false
›true
›true

It prints true, false, true, true. joined was worked out by the compiler, so it is the pooled cat, the same object as word. built has a variable in it, start, so it is joined while the program runs and becomes a new object: == says false while equals says true. built.intern() hands back the pooled cat. You will rarely call intern() yourself; it exists so that a program holding many copies of the same text can keep just one.

The lab below draws the rule. Pick how a and b are made. Every choice holds the letters c, a, t, yet the answer from == changes with the choice, while equals always says true.

String pool lab== and equals
String a =
String b =
Try
String a = "cat"; // a literal: the pooled object
String b = new String("cat"); // always a new object
VariablesNew objectsString poolone per textString aString bString"cat"String"cat"
a == b
false
asks: the same object?
a.equals(b)
true
compares the letters

One variable points at the pooled "cat" and the other at a new object holding the same letters. == says false because they are different objects; equals compares the letters and says true.

Quick check
A program reads a city with String city = in.nextLine();. Which condition is true exactly when the person typed Paris?
ExerciseCapital.java
This quiz never accepts the right answer, because it compares with ==. Fix the check so that Paris is accepted, and also paris, and the answer with stray spaces around it, as people often type.
Key idea

== asks “is it the same object?”. equalsasks “is it the same text?”. For strings you almost always mean the second, so compare them with equals.

When the string might be null
answer.equals("yes") throws a NullPointerException when answer is null. Writing the literal first, "yes".equals(answer), gives false instead, because a literal is never null.

Ordering with compareTo

Sometimes you need before-or-after instead of equal-or-not, to put words in dictionary order. a.compareTo(b) returns a negative number when a comes before b, zero when they are equal, and a positive number when a comes after b.

Order.java
1public class Main {
2 public static void main(String[] args) {
3 System.out.println("apple".compareTo("banana"));
4 System.out.println("banana".compareTo("apple"));
5 System.out.println("apple".compareTo("apple"));
6 }
7}
Prints
›-1
›1
›0

This prints -1, then 1, then 0. apple comes before banana, so the first result is negative; swap the two and it turns positive; identical strings give 0. Test the sign, not an exact number: if (x.compareTo(y) < 0)reads “if x comes before y”.

Why make strings immutable at all?#

Immutability can feel like a nuisance the first time it surprises you. It was a deliberate choice, and it buys safety. A string can never change, so it is always safe to share: when two parts of a program hold the same string, neither can alter it behind the other's back. When you pass a name to a method that keeps it, you never have to worry that the method will change your text later, or that you will change its.

Safe to use as keys and labels
Later you will file values under a string, such as a price list where “apple” leads to its price. The HashMapthat does this files each entry by its key's text, which only works because a string key cannot change after it is filed.

Building strings: StringBuilder#

Immutability has one real cost, and it shows when you build a long string piece by piece in a loop. Each += on a string has to make a whole new string, copy everything built so far into it, and add the next piece. Join a hundred pieces that way and you make and throw away ninety-nine strings along the way, copying the early pieces again and again. For a few pieces it does not matter. For many thousands in a loop it gets slow.

A pattern to recognise
String s = ""; for (...) s += piece; looks harmless and works for small inputs. When you see a string grown with += inside a loop that may run thousands of times, switch to a StringBuilder.

A StringBuilder holds text you can change in place: it is a mutable partner to String. You append to it as often as you like, and at the end you call toString() once to get the finished, immutable String. Step through this and watch the memory view: sb points at one builder, drawn as a row of characters, and each append adds two characters to the end of that same row.

Build.java
1public class Main {
2 public static void main(String[] args) {
3 StringBuilder sb = new StringBuilder();
4
5 for (int i = 0; i < 3; i++) {
6 sb.append("ab");
7 }
8
9 String result = sb.toString();
10 System.out.println(result);
11 }
12}
Prints
›ababab

It prints ababab. There was one builder from start to finish, and no String was made until toString() copied the finished text out.

A builder changes in place

Changing in place makes a difference when two variables share one object. Decide what this prints.

Predict the outputTeapot.java
1public class Main {
2 public static void main(String[] args) {
3 String s = "tea";
4 String t = s;
5 t = t + "pot";
6
7 StringBuilder a = new StringBuilder("tea");
8 StringBuilder b = a;
9 b.append("pot");
10
11 System.out.println(s);
12 System.out.println(a);
13 }
14}

One string and one builder, each shared by two variables. What do the two lines print?

It prints tea, then teapot. t = t + "pot" made a new string and pointed t at it, so s still points at the unchanged tea. b = a copied the reference to the one builder, and append changed that builder in place, so a sees the change. In the memory view the builder appears once, labelled with both names.

More builder methods

Besides append, a builder can insert, delete, replace and reverse. These methods change the builder and hand back that same builder.

BuilderMethods.java
1public class Main {
2 public static void main(String[] args) {
3 StringBuilder word = new StringBuilder("stressed");
4 word.reverse();
5 System.out.println(word);
6
7 StringBuilder sb = new StringBuilder("Hello World");
8 sb.insert(5, ",");
9 System.out.println(sb);
10 sb.deleteCharAt(sb.length() - 1);
11 sb.setCharAt(0, 'J');
12 System.out.println(sb + " has " + sb.length() + " characters");
13
14 StringBuilder room = new StringBuilder();
15 room.append("Room ").append(12).append('B');
16 System.out.println(room);
17 }
18}
Prints
›desserts
›Hello, World
›Jello, Worl has 11 characters
›Room 12B
  • reverse() turns stressed into desserts.
  • insert(5, ",") puts a comma at index 5, just before the space: Hello, World.
  • deleteCharAt removes the last character and setCharAt replaces the first, which leaves Jello, Worl, 11 characters long.
  • The last part chains three calls. Each append returns the same builder, so the next append runs on it. append also takes numbers and single characters and adds them as text, so the builder ends up holding Room 12B.

Printing a builder, or joining it to a string with +, uses the text it holds at that moment.

String
StringBuilder
Immutable: it never changes after it is made.
Mutable: made to be changed in place.
Methods return a new string.
Most methods change the builder and return that same builder.
Growing one with += in a loop copies the text again and again.
append in a loop adds to the end with little copying.
equals compares the text.
equals only checks for the same object. Compare sb.toString() instead.
The everyday type for finished text.
Temporary, while you put text together. toString() hands you the result.
Put the lines in orderInitials.java
Put the lines in order so the program builds the initials of portable network graphics and prints PNG.

Tap a line, then tap where it should go. Or use the arrows.

ExerciseNumberList.java
The program reads a whole number n. Finish it so it prints the numbers from 1 to n separated by a comma and a space, with nothing after the last one. For 5 it prints 1, 2, 3, 4, 5. Build the line in the StringBuilder.
Key idea

Use String for finished text. Use StringBuilder while you are still putting text together piece by piece, especially in a loop, then call toString() to get a String back.

The sibling: StringBuffer

StringBuffer is an older near-twin of StringBuilder. It has been in Java since the first version, and StringBuilder arrived in Java 5. Their methods are the same: append, insert, reverse, toString and the rest. The difference is that StringBuffer is thread-safe. A program can run several jobs at the same moment, each one called a thread, and several threads can share one StringBuffer without scrambling its text.

Threads come much later
Running several jobs at once is the subject of introduction to threads, and why shared data needs guarding is the subject of thread creation and synchronization, both near the end of the course. Until then, every program you write runs on a single thread.
StringBuilder
StringBuffer
The same building methods.
The same building methods.
Not safe to share between threads.
Safe to share between threads.
Faster in most Java versions, with no guarding.
Slower, because it guards every call.
The default choice.
For a buffer that several threads really do share.

So build with StringBuilder by default. Its own documentation recommends it wherever a single thread uses the builder, which is nearly always. You will mostly see StringBuffer in older code.

Try it

Answer each one in your head or on paper first, then open the answer.

1
After String s = "quiet"; s.toUpperCase(); runs, what does System.out.println(s) print, and why?
2
For String w = "programming";, what do w.length(), w.charAt(0) and w.substring(0, 4) each give?
3
Two strings a and b both hold the text java, but b was made with new String("java"). What do a == b and a.equals(b) give? Explain the gap.
4
What does "cat".compareTo("dog") tell you, just from its sign? And "dog".compareTo("dog")?
5
You need to join 10,000 small pieces of text in a loop. Why is String result = ""; result += piece; a poor choice, and what should you use instead?

Practice

Work through these on your own. Each one builds on this page.

1
Write a program that reads a full name such as ada lovelace with nextLine() and prints its initials as capitals with dots: A.L. Use indexOf to find the space. Test it with a few names, including one with capitals already.
2
Read an email address and check that it has exactly one @, that the @ is neither the first nor the last character, and that the part after it contains a dot. If the address passes, print the name part and the domain part on separate lines. If not, say which check failed.
3
Write a program that reads a name that may have stray spaces, like " Ada ", and prints a greeting with the spaces gone and the name in capitals. Use trim and one case method, then explain in a sentence why the string you read is unchanged after both calls.
4
Reverse a word two ways. First, with a loop that starts at word.length() - 1, counts down to 0 and appends each charAt(i) to a StringBuilder. Second, with new StringBuilder(word).reverse(). Print both for hello and check that they agree on olleh.
5
In one short paragraph, explain the string pool to a friend who has not read this page. Use the fact that strings are immutable to explain why sharing one copy of "cat" is safe, and why the same sharing would be dangerous if strings could change.

Further reading#

Spotted a mistake, or a part that did not make sense? Report it by email