Part II: Structuring Data
Chapter 8: Strings as Data
You have used strings since Chapter 1 as labels and messages. This chapter treats them as data to be examined, taken apart and rebuilt—which is what most real programs do with text.
A string is a sequence of characters, and much of what Chapter 7 did with arrays works on text too, through a String's own methods.
4 J a Jav ava avaJ
Positions count from zero, as an array's indexes do, and there are no negative positions: the last character is at length() - 1. substring(from, to) is a string's slice: the characters from position from up to, but not including, position to, given back as a new String, exactly as Arrays.copyOfRange slices an array. With only one number, substring(1), the slice runs to the end. A position past the end stops the program with a StringIndexOutOfBoundsException, as it did for Chapter 5's charAt.
A String has no method that reverses it, so the loop does the job: it walks the positions backward, from the last to 0, and adds each character to the end of a new string. += joins a char onto a String just as it joins text.
Strings are immutable
Here is the one large difference from arrays: a string cannot be changed after it is created. An array is mutable, in Chapter 7's word; a String is immutable. Java will not even accept a line that tries to change one character:
One character of a String is assigned to, which javac refuses: a String cannot be changed. The error message is the lesson.
Press Run, and javac refuses:
Main.java:3: error: unexpected type
word.charAt(0) = 'J';
^
required: variable
found: value
1 error
charAt gives back a value, a copy of the character, and only a variable can be assigned to: required: variable, found: value.
This is not a limitation so much as a design decision, and it has a practical consequence: every String method that seems to change text gives back a new string and leaves the original as it was.
java JAVA
The first word.toUpperCase() worked out "JAVA" and threw it away, because nothing stored the result. This is the single most common string mistake, and the fix is always the same: store the result.
Essential string methods
| Method | Gives back |
|---|---|
toUpperCase() | All uppercase |
toLowerCase() | All lowercase |
strip() | Spaces, tabs and line breaks removed from both ends |
replace(a, b) | Every a replaced with b |
split(",") | An array of the pieces between the commas |
indexOf(x) | Position of the first x, or -1 |
contains(x) | true if x appears in it |
startsWith(x) | true if it begins with x |
endsWith(x) | true if it ends with x |
Here they are at work:
ABC abc [hi] bonono 2 -1 true true
Some jobs have no ready-made method. Nothing counts how many times a letter appears, and nothing capitalizes the first letter of each word: exercises 2 and 4 of Try It build those from loops. Whether a whole piece of text is a number is still Chapter 4's job, Integer.parseInt inside a try. And whether one character is a letter or a digit is a question for that character alone, which this chapter comes to when it walks text character by character.
Methods chain left to right, each working on the result of the last:
[ada lovelace]
strip() gives back a new string, and toLowerCase() is called on that one. That combination—strip the whitespace, normalize the case—is what you want on almost any input a human typed.
Comparing text safely
String comparison is case-sensitive and whitespace-sensitive, so raw user input rarely compares the way you want:
false true
Normalize before comparing. Every time.
And compare with .equals, as Chapter 3 taught. Here strip() and toLowerCase() each gave back a new String object, and == asks Chapter 7's question, whether two variables hold the same object, so it can say false when every letter matches.
contains tests whether one string appears inside another:
true false false true
contains is case-sensitive too, so the The at the start is not a the until the sentence is lowercased.
split and String.join
These two are opposites, and between them they handle most text processing.
split cuts a string into an array of strings, at every place its argument appears:
[the, quick, brown, fox] [2026, 09, 17] Ada scored 88 for a B
That last pattern—split a comma-separated line, then give each piece a name—is how nearly every program reads data files. Every piece is a String, the score included: to do arithmetic with it, convert it with Chapter 2's Integer.parseInt.
Text a person typed is rarely so tidy. Splitting at every single space gives an empty piece wherever two spaces meet, or the text starts with one:
[, the, quick, , brown, fox] [the, quick, brown, fox]
The second line is the fix. split's argument is a pattern, a short description of what to split at. Most characters in a pattern stand for themselves, which is why "," and "-" meant what they said. A few have special meanings: \s stands for any space, tab or line break, and + after something means "one or more of it in a row," so the pattern \s+ matches every run of spaces, however long. Written in a Java string, its backslash is doubled, "\\s+", for a reason the section on escape sequences explains. strip() goes first because a space at the very start would still leave an empty first piece. strip().split("\\s+") is the way to cut typed text into words.
Two more things about split are worth knowing before they surprise you. Empty pieces at the very end are dropped, so "a,b,,".split(",") gives two pieces, not four. And splitting an empty string gives back one piece, the empty string itself, so a program that counts words checks isEmpty() first.
. and | are special in a pattern as well, and splitting at either one goes wrong quietly: . stands for any character at all, and split("|") cuts the text apart at every character. Put \\ in front of either to make it an ordinary character:
[] [25, 0, 4] [Ada, 88]
split(".") found a separator at every character, so every piece was empty, and the empty pieces at the end were dropped: all of them.
String.join does the reverse, gluing strings together with a separator between each one. It is called on String itself, as Arrays.toString is called on Arrays, and takes the separator first, then the pieces: an array of Strings, or an ArrayList<String>.
the quick brown fox the-quick-brown-fox the, quick, brown, fox Ada and Grace and Alan
String.join takes only text. Given an int[], it does not compile, so numbers are joined with a loop, Chapter 5's accumulator:
88, 92, 79
The separator goes in front of every number but the first, so none is left dangling at the end.
Processing character by character
Chapter 5's counting loop with charAt hands you one character at a time, which is how you examine text in detail:
6
Note "aeiou".indexOf(letter) >= 0. indexOf takes a char as well as a String, and asks where it appears in "aeiou": any position at all, 0 or more, means the letter is a vowel, and -1 means it is not. That one test does the work of five comparisons joined with ||, letter == 'a' || letter == 'e' and so on, which makes checking against a set of characters very compact.
Building a new string works by accumulation, as Chapter 7 built lists, with += doing the adding:
Prgrmmng n Jv
Each += makes a new string one character longer, which is fine for text of this size. Programs that build very long text use a tool called StringBuilder instead, which Appendix C names.
Three questions about one character have ready-made answers, from Character, the object twin of char that Chapter 7 named: Character.isLetter(c), Character.isDigit(c) and Character.isLetterOrDigit(c), each true or false.
9 letters, 4 digits, 4 others
The spaces and the comma are neither letters nor digits, so they land in others.
Characters are numbers
Every char is stored as a number, its character code, and Chapter 3's character-code order is simply the order of those numbers: 'a' is 97, 'b' is 98, and so on to 'z', 122, with the capitals earlier, from 'A', 65, to 'Z', 90. Arithmetic on a char works on its number, and gives back an int:
121 y 23 c true
letter + 1 is the int 121, one more than the code of 'x', and IO.println prints it as a number. The cast (char), like Chapter 2's (int), turns a number back into the character it is the code of: y. letter - 'a' is how far a letter is from a, which is its position in the alphabet counting from 0: 0 for a, 23 for x. And since chars compare as numbers, letter >= 'a' && letter <= 'z' asks whether a character is a lowercase letter.
The parentheses around letter + 1 matter. A cast applies to the value right after it, so (char) letter + 1 casts first, adds after, and gives the int 121 again.
A worked example: palindromes
A palindrome reads the same forward and backward, ignoring case, spaces and punctuation. Checking for one combines most of this chapter.
true racecar true A man, a plan, a canal: Panama true Was it a car or a cat I saw? false hello world
The structure is worth studying. cleanText does one job—strip the text down to comparable characters—reverse does one job, and isPalindrome does one job, given clean text. Splitting the work that way makes the actual test a single readable line, and it compares with .equals, because the two sides are different String objects. Character.isLetterOrDigit is true for letters and digits, which is exactly the filter needed.
Escape sequences
Some characters cannot be typed directly inside a string, so they are written with a backslash. These pairs are called escape sequences:
| Sequence | Means |
|---|---|
\n | New line |
\t | Tab |
\" | A double quote |
\' | A single quote |
\\ | A backslash |
Name: Ada Role: Mathematician Ada said, "Hello."
A single quote needs no backslash inside a string, as Chapter 1 found with its apostrophe; \' is for a char, where '\'' is the single quote itself. And \\ is why split's pattern for spaces was written "\\s+": the pattern needs one backslash before the s, and in a Java string one backslash is written \\.
A text block, opened and closed with three double quotes, spans several lines without needing \n:
1. Add a student 2. View the roster 3. Quit
The text starts on the line after the opening """, and Java removes the indentation its lines share, so a text block can be indented along with the code around it. The closing """ on a line of its own ends the text with a line break, which is why IO.print is enough here: IO.println would add a blank line after the menu.
Common mistakes
| Mistake | What happens |
|---|---|
word.toUpperCase(); on its own | Nothing changes; store the result |
word.charAt(0) = 'J' | unexpected type—build a new string, "J" + word.substring(1) |
answer.equals("yes") on raw input | false for "Yes" or " yes"; strip and lowercase first |
"a,b".split(", ") | One piece, not two: the separator must match exactly |
String.join(", ", scores) on an int[] | Does not compile: join takes text, so use a loop |
indexOf(x) returning 0 read as "not found" | 0 is the first position; not found is -1 |
split(".") to split at dots | No pieces at all: . is special in a pattern, so write split("\\.") |
'a' + 'b' to join two letters | 195: two chars add as numbers |
(char) letter + 1 | An int: the cast applies to letter alone; write (char) (letter + 1) |
Try It
Exercises 2, 4 and 6 name their methods exactly, as Chapter 6's did, and Check calls each one with the cases listed under "What it should do."
1. Ask for a word and print it forward, backward, in uppercase, and its length, each on its own line.
Show a solution
// A word forward, backward, in uppercase, and its length.
void main() {
String word = IO.readln("Enter a word: ").strip();
String reversed = "";
for (int i = word.length() - 1; i >= 0; i--) {
reversed += word.charAt(i);
}
IO.println(word);
IO.println(reversed);
IO.println(word.toUpperCase());
IO.println(word.length());
}
The backward loop is the one from the start of the chapter: from the last position, length() - 1, down to 0, adding each character to the end of a new string. toUpperCase() gives back a new string, so its result is printed straight away; called on a line of its own, it would be lost.
2. Write int countLetter(String text, String letter), returning how many times the letter (a one-letter String) appears in the text, ignoring case.
What it should do:
countLetter("Hello World", "l") -> 3countLetter("Banana", "A") -> 3countLetter("xyz", "q") -> 0countLetter("Anna", "a") -> 2
Show a solution
int countLetter(String text, String letter) {
String lowerText = text.toLowerCase();
char target = letter.toLowerCase().charAt(0);
int count = 0;
for (int i = 0; i < lowerText.length(); i++) {
if (lowerText.charAt(i) == target) {
count++;
}
}
return count;
}
void main() {
IO.println(countLetter("Hello World", "l"));
}
Lowercase both the text and the letter, so neither one's case matters, then count with Chapter 5's loop. charAt(0) takes the letter's one character as a char, which compares with ==.
3. Ask for a full name and print its initials in capitals, as AL for Ada Lovelace. The name may be typed in any case, with extra spaces.
Show a solution
// Prints the initials of a full name.
void main() {
String fullName = IO.readln("Enter your full name: ");
String[] words = fullName.strip().split("\\s+");
String initials = "";
for (String word : words) {
initials += word.substring(0, 1).toUpperCase();
}
IO.println(initials);
}
strip().split("\\s+") cuts the name into words however many spaces were typed. Each word's first letter is its substring(0, 1), a one-letter String, which toUpperCase() turns into a capital, and += joins the initials into one string.
4. Write String titleCase(String sentence), returning the sentence with the first letter of each word in uppercase. Java has no ready-made method for this, so build it. The words are lowercase, with one space between them.
What it should do:
titleCase("hello world") -> "Hello World"titleCase("java") -> "Java"titleCase("the quick brown fox") -> "The Quick Brown Fox"
Show a solution
String titleCase(String sentence) {
ArrayList<String> words = new ArrayList<>();
for (String word : sentence.split(" ")) {
words.add(word.substring(0, 1).toUpperCase() + word.substring(1));
}
return String.join(" ", words);
}
void main() {
IO.println(titleCase("the quick brown fox"));
}
Split at the spaces, capitalize each word's first letter with substring(0, 1).toUpperCase(), keep the rest with substring(1), and String.join puts the spaces back between the words, with none left over at the end.
5. Count the words in a sentence the user enters, however many spaces are typed before, between and after them.
Show a solution
// Counts the words in a sentence.
void main() {
String sentence = IO.readln("Enter a sentence: ").strip();
if (sentence.isEmpty()) {
IO.println("Words: 0");
} else {
String[] words = sentence.split("\\s+");
IO.println("Words: " + words.length);
}
}
strip() and then split("\\s+") give exactly one piece per word, however many spaces surround them, so the array's length is the count. An empty sentence would still split into one empty piece, which is why it is checked first.
6. Write String caesarShift(String text, int n), returning the text with each lowercase letter shifted n places along the alphabet, wrapping around from z to a, and every other character left as it is. n is from 0 to 25.
What it should do:
caesarShift("abc", 1) -> "bcd"caesarShift("xyz", 3) -> "abc"caesarShift("hello", 0) -> "hello"caesarShift("hello world", 1) -> "ifmmp xpsme"
Show a solution
String caesarShift(String text, int n) {
String result = "";
for (int i = 0; i < text.length(); i++) {
char c = text.charAt(i);
if (c >= 'a' && c <= 'z') {
result += (char) ('a' + (c - 'a' + n) % 26);
} else {
result += c;
}
}
return result;
}
void main() {
IO.println(caesarShift("hello world", 3));
}
For a lowercase letter, c - 'a' is its place in the alphabet, from 0 to 25. Adding n and taking the remainder after dividing by 26 wraps anything past z back to the start, and 'a' plus that place, cast to char, is the shifted letter. Anything else, such as the space, is added unchanged.
7. Given the text "Ada,88\nGrace,92\nAlan,45", print each name with its score on its own line, as Ada: 88.
Show a solution
// Prints each name and score from lines of comma-separated text.
void main() {
String data = "Ada,88\nGrace,92\nAlan,45";
String[] lines = data.split("\n");
for (String line : lines) {
String[] parts = line.split(",");
IO.println(parts[0] + ": " + parts[1]);
}
}
split("\n") cuts the text into its three lines, and each line splits at its comma into a name and a score, taken from the array by index.
Chapter project: a text analyzer
Write a program that reports statistics about a passage of text. Its report lines up its columns with Chapter 7's String.format widths.
================================== Characters: 151 Characters (no spaces): 124 Words: 28 Sentences: 3 Vowels: 42 Longest word: programming Average word length: 4.3 ==================================
Two notes.
The sample text is written as several strings joined with + across lines, which is the usual way to write a long piece of text without an enormous line. A text block would not do here: it keeps its line breaks as part of the text, and every count would change.
The sentence count is a heuristic, not a fact. It counts ., ! and ?, which means Dr. Smith would count as a sentence ending and ... as three. Real sentence detection is a hard problem. A program that is approximately right is often the correct engineering answer, but you should know which of your numbers are exact and which are estimates—and say so in a comment when it matters.
longestWord also quietly picks the first longest word when several tie. That is a reasonable choice, and it is a choice; someone reading the output has no way to know a tie occurred.
- Chapter 8 quiz (plain text: fill it in, then print it or show it)
- Chapter 8's programs as .java files, to run with
javaon a computer