Phase 0 · Foundations
Text is not what you think
By the end you can pull any letter out of any word, chop a sentence into pieces, compare text honestly, and you'll know the one rule about Java text that costs beginners a whole afternoon.
Think about the last time your phone changed a word you didn't want changed. Or a website that said your password was too short. Or a game that turned your guess green, one letter at a time. Every one of those is a program taking text apart and looking at the pieces. Text feels like the easy bit of programming, because you've been reading since you were four. It isn't. Java's text hides a rule so odd that most people lose an afternoon to it the first time. You'll meet that rule in about twenty minutes, and you'll see it happen in front of you.
What you'll be able to do
- Pull any single letter out of any word, and glue letters back into text.
- Chop up, search inside and compare pieces of text — the exact toolkit you need to build Wordle next chapter.
- Say why
.lengthand.length()are different, and read the compiler error when you swap them. - Explain why
toUpperCase()on a line of its own does absolutely nothing.
Section 1A String is a row of letters
You've just spent a whole chapter on arrays. One name, a row of numbered slots, counting from zero. Now the good news. A String is that same idea wearing different clothes. It's a row of letters, numbered from 0, with a size you can ask for. So you already know most of how text works. Nobody has told you yet.
Since 0.2 you've used String as if it were one single thing, like a number. It isn't. A String is a row of characters, in order. And in 0.2 you learned a char is a number in disguise. So the first letter of a word sits at index 0, exactly like the first slot of an array.
That's why word.charAt(3) needs no explaining. It means what slots[3] meant in 0.5. Four steps along, counting the first letter as nought. One idea, two spellings.
An array is a street of houses with door numbers. A word is a street of letters. "crane" has five houses. c at number 0, r at 1, a at 2, n at 3, e at 4. Ask for number 5 and nothing is there. Same as the street. Same as the crash you already know how to read.
The toolkit, live
A list of nine methods is just a list. So here they are as a machine instead. The box starts with the word Wordle. Each row shows what one method really hands back for whatever the box holds.
Type your own name in and watch every row change at once. Then try a single letter, like a. One row turns red: substring(1, 3) cannot reach slots that don't exist. The same nine methods in real Java are in src/StringTour.java.
s.length()6how many letterss.charAt(0)"W"the letter at slot 0s.toUpperCase()"WORDLE"a NEW word, all capitalss.toLowerCase()"wordle"a NEW word, all smalls.substring(1, 3)"or"slots 1 and 2 — stops BEFORE 3s.indexOf("e")5slot of the first e, or -1s.contains("or")trueis that inside it?s.trim()"Wordle"same word, outer spaces gones.isEmpty()falseno letters at all?That's your reference — poke at it whenever you forget one. Three of those rows carry a real idea, so let's do those properly.
substring stops before the second number
Two numbers go in. The first says where to start. The second says where to stop before. So substring(1, 3) hands you slots 1 and 2, and slot 3 is left out.
That feels like a mistake until you spot the payoff. The number of characters you get back is exactly end - start. Three minus one is two. With one number, substring(7) means "from slot 7 to the end".
The text is "Wordle". What does word.substring(2, 4) give you? Count the letters on your fingers before you answer — the off-by-one feeling is the whole point of this one.
"rd"If you said "rdl", you fell into the trap almost everybody falls into, and it's a useful fall. Index the word: W=0, o=1, r=2, d=3, l=4, e=5. Start at 2, stop before 4 — so index 2 and index 3, "r" and "d". Two characters, because 4 − 2 = 2. When unsure, do that subtraction.
indexOf says "not found" with -1
indexOf("e") hands back the slot number where it found your text. Type a word with no e into the widget. That row reads -1.
Why a negative? Because 0 is a real answer. In "egg" the e sits at slot 0. So "not found" needs a number no real slot could have. Every index is 0 or bigger, which leaves -1 free.
So the test you want is if (s.indexOf("e") >= 0) — "found it somewhere". The tempting version, > 0, is wrong. It quietly throws away every match sitting at slot 0. It would tell you "egg" has no e.
One method hands you an array back
split(" ") chops your text wherever it sees a space, and hands back an array of the pieces. Text in, array out. That's how a sentence becomes a list of words, and you'll use it in the project.
One catch. split doesn't read what you give it as plain text. It reads it as a pattern — a tiny search language. A space means a space, so split(" ") behaves. But a full stop means "any character at all", so split(".") hands back nothing. Patterns get a proper chapter in Phase 1. Until then, I'll say in words what any pattern I show you does.
From String to char and back
Next chapter you build Wordle, and Wordle is entirely about single letters. So you need to move between text and characters comfortably. Watch the quotes as you read: single quotes mean a char, double quotes mean a String.
String word = "crane";
char[] letters = word.toCharArray(); // text -> array of chars
for (char c : letters) {
System.out.print(c + "-");
}
char first = word.charAt(0);
String asText = "" + first; // char -> text, the quick way
String alsoText = String.valueOf(first); // char -> text, the clear way
System.out.println(first + 1); // ?
System.out.println((char) (first + 1)); // ?toCharArray() takes your text apart into a real char[] — an array, with a .length and everything. From there the for-each loop walks the letters. Going the other way: stick a char onto an empty String with + and it turns into text. String.valueOf does the same job with a clearer name.
The last two lines are the trick from 0.2, where 'A' + 1 came out as 66. Do arithmetic on a char and you get a plain number back. Put (char) in front — a cast — to read that number as a letter again.
Here's what this chapter adds. charAt hands you a char, so you can do sums with it. c - 'a' gives 0 for 'a', 1 for 'b', up to 25 for 'z'. Twenty-six letters, twenty-six slots — perfect for an int[26]. That's how you walk a word letter by letter and tally what you find. You'll do exactly that in the project.
Section 2Two kinds of length, one letter apart
Chapter 0.5 told you an array's size is .length, with no round brackets, and promised text was different. Here's the payoff. It's a trap that catches people for months, and it can only trap you now that you know both forms.
int[] marks = {10, 20, 30, 40};
String word = "banana";
System.out.println("array -> " + marks.length); // no brackets
System.out.println("String -> " + word.length()); // bracketsLine one makes an array of four numbers. Line two makes a piece of text. Line three asks the array its size with .length and no round brackets. Line four asks the text its size with .length() and round brackets.
array -> 4
String -> 6Four slots. Six letters. Both right, asked two different ways.
Why the difference? An array's size is a field — a value stored on the thing itself. Think of a label stuck on a box. You just read it. A String's size comes from a method — a small piece of work Java runs for you. They are exactly like the methods you wrote in 0.4. Round brackets always mean "run this". No brackets means "read this". Arrays are built into the language at a very low level. String is an ordinary thing with methods. Not elegant. Just true.
Getting this backwards is a compile error, not a crash — Java refuses to build the program at all. There's a deliberately broken file in your src/ to prove it. Run javac LengthMistake.java:
LengthMistake.java:9: error: cannot find symbol
System.out.println(marks.length()); // wrong: arrays have no length() method
^
symbol: method length()
location: variable marks of type int[]
LengthMistake.java:10: error: cannot find symbol
System.out.println(word.length); // wrong: String has no length field
^
symbol: variable length
location: variable word of type String
2 errors"Cannot find symbol" means "you used a name I don't have". The symbol: line says what you asked for. The location: line says what you asked it of. The compiler is handing you the fix.
Read the second error again. It doesn't say "you forgot the brackets". It says variable length. Without brackets you asked for a stored value, and String hasn't got one by that name. The compiler is describing your request back to you. That's the habit worth building: read the symbol: line as a sentence about what you typed.
- An array's size is
arr.length— no round brackets. It's a field: a label you read. - Text's size is
s.length()— with round brackets. It's a method: a job Java runs. - Round brackets mean "run this". No brackets means "read this".
- Swap them and nothing compiles.
cannot find symbolplusmethodorvariabletells you which way round you got it.
You have String[] names = {"Ada", "Linus"};. You want the number of letters in the first name — the answer 3. Which do you write?
Section 3Nothing you do to a String ever changes it
This is the big one. If you take one thing from this chapter, take this. Beginners lose whole afternoons to it, and the code that causes it looks completely reasonable.
String s = "wordle";
s.toUpperCase();
System.out.println("after calling toUpperCase(): " + s);Three lines. Make a piece of text. Call toUpperCase() on it. Print it. The file carries on for a few more lines after these, and you'll see their output in a second.
What does that print? Really commit to an answer before you click. If you get this wrong you're in enormous company, and you'll save yourself an afternoon later.
after calling toUpperCase(): wordle
s is still: wordle
shouty is now: WORDLE
after s = s.toUpperCase(): WORDLELower case. Unchanged. Nothing happened.
toUpperCase() does not make s shouty. It builds a brand new piece of text that is the shouty version, and hands it back. If you don't catch what it hands back, it's thrown away instantly. Line 2 of that program is completely pointless, and Java doesn't warn you.
The fix is to catch the result. Put it in a new name (String shouty = s.toUpperCase();). Or put it straight back into the old one (s = s.toUpperCase();). Both are in the output above. The rule: every String method returns a new String, and none of them change the one you called it on. Ever.
The proper word for this is immutable — "cannot be changed". Once a piece of text exists in memory, those exact characters stay that way. trim(), substring(), toLowerCase(), replace() — every one hands you back something new and leaves the original alone.
A String is not a whiteboard you can wipe and rewrite. It's a printed photograph. toUpperCase() isn't editing the photo — it's taking it to a shop and having a new print made, brighter. Now you have two photos, and the original is exactly as it was. Walk out without picking up the new one and you still just have the old photo.
Watch it happen slot by slot:
String s = "wordle"; — the name s is a label that points at some text sitting in memory.
Names
Memory
s.toUpperCase(); — Java builds a second, separate piece of text. It does not touch ①. Nobody is holding on to the new one.
Names
Memory
Nothing points at ② any more, so Java is free to reclaim that space later. Meanwhile s still points at ①, so printing it gives wordle. That's the whole mystery.
Names
Memory
s = s.toUpperCase(); — same new text is built, but now we move the label. s points at the new one. The text ① never changed. The label did.
Names
Memory
The hidden cost of gluing text in a loop
Immutability has a price, and you can measure it. Both loops below end up with 60,000 x's.
String glued = "";
for (int i = 0; i < 60000; i++) {
glued = glued + "x";
}
StringBuilder box = new StringBuilder();
for (int i = 0; i < 60000; i++) {
box.append("x");
}
String built = box.toString();The first loop starts with empty text and glues an x on 60,000 times. But Strings can't change — so glued + "x" can't add to glued. It has to copy the whole thing into a new, one-longer piece of text. On round 50,000, Java copies 50,000 characters to add one. Over a billion character copies in total.
The second loop uses StringBuilder — a thing that is a whiteboard. append writes at the end of what's there, no copying. At the end, toString() takes one photograph of the result.
Same 60,000 x's, both ways. Roughly how much slower is the gluing loop? Same speed? Twice? Ten times? Guess a factor.
glued length: 60000
built length: 60000
String += took 144 ms
StringBuilder took 2 msRoughly seventy times slower on the machine this was measured on. Your numbers will differ. They differ from run to run on one machine too. Runs here gave anywhere from 73 to 144 milliseconds for the gluing loop. StringBuilder took 1 or 2 ms. The exact figure doesn't matter. The shape does. The gap grows as the text gets longer, because each extra character makes every later copy bigger.
Don't go rewriting everything, though. Gluing text with + a handful of times is fine and much easier to read. The rule is narrow: building text inside a loop that runs many times? Use a StringBuilder. Make one, append in the loop, toString() at the end. More in Phase 1.
Why == lies about text
One more, and it's the nastiest, because it works right up until it doesn't. In 0.3 you compared numbers with ==. It's natural to reach for it with text too.
String a = "java";
String b = "java";
System.out.println(a == b);
String half = "ja";
String built = half + "va"; // same letters, made while running
System.out.println(built == a);
System.out.println(built.equals(a));
String copy = new String("java"); // a deliberate second copy
System.out.println(copy == a);
System.out.println(copy.equals(a));The first three lines make two pieces of text, both typed out as "java". Then they compare them with ==. The next three build the same word a different way: take "ja", glue "va" on the end. Identical letters — j, a, v, a. Then compare with ==, and again with .equals. The last three do it a third way, with new String. That forces Java to make a genuinely separate copy.
Five true-or-false answers, and every one of those strings spells "java". Write your guesses down. This is the trap that gets professionals.
two plain literals with == : true
built at run time, with == : false
built at run time, with .equals : true
new String("java") with == : false
new String("java") with .equals : true
printed side by side: java javaLook at that second line. Two pieces of text, both spelling "java", and == says they're not equal. The last line of output prints them side by side and they are plainly identical: java java.
Here's just enough of the reason. == does not ask "do these spell the same word". It asks "are these the very same object in memory" — one photograph, or two? Type "java" twice and Java is clever: it reuses one copy, so == happens to say true. Build the word while the program runs and you get a second, separate copy, so == says false. Same letters, different objects.
.equals() asks what you actually meant: "do these contain the same characters". It walks the letters and compares them, so it said true both times. The full story is Phase 1: objects, memory, why Java reuses text you typed. I'm not going to fake it here. What you need now is a rule with no exceptions: compare text with .equals(), never with ==. Or .equalsIgnoreCase() when capitals shouldn't matter.
This bug is vicious because == often works in tiny test programs. In those, all your text is typed out as literals. It starts failing the moment real text arrives — from a Scanner, from a file, glued together from pieces. Which is to say: it fails in the real program and passes in the test. Use .equals() from today and you never meet it.
Going deeper Why two typed-out "java"s are the same object
Every piece of text you type between quote marks is stored once. Java keeps a shared table for exactly this, called the string pool. See "java" a second time and the compiler hands you the entry that's already there. It doesn't make a new one. That's why == said true on the first line.
Two consequences worth knowing. First, the compiler glues text it can work out for itself. Both halves of "ja" + "va" are typed out, so it becomes the single literal "java" before your program runs. == says true there too. Our built is different, because half is a variable. Its value isn't known until the program runs. Second, new String("java") means "make me a fresh one anyway". That's a good demonstration and a bad habit.
None of this changes the rule. Pool membership is an optimisation, not a promise about your data. Compare text with .equals().
You're checking whether the player typed the right word. Which line do you write?
Watch the second string appear
This is the part that costs beginners hours, so here it is as a picture you can drive. On the left are the names in your program. On the right are the actual pieces of text sitting in memory. Press the button and watch carefully — count the objects before and after.
Names
Objects in memory
One name, one object. Now press the button and watch what toUpperCase() actually does.
One object before, two after — and ① reads the same in both.
- Every String method hands back a new String. None of them change the one you called it on.
s.toUpperCase();on its own line does nothing. You must catch the result.- Gluing text with
+inside a big loop copies everything, every time. Use aStringBuilder. - Compare text with
.equals(), or.equalsIgnoreCase(). Never==.
Section 4Project: count the letters and words in a real file
Time to put arrays and Strings together on real text. Your src/ has a small file called sample.txt — eight lines about a boy counting waves. You'll count every letter in it, draw a bar chart out of # characters, and find the most-used word.
One genuinely new tool first. Until now all your text came from your own code or a Scanner. Reading a file needs this:
import java.nio.file.Files;
import java.nio.file.Path;
String text = Files.readString(Path.of("sample.txt"));The two import lines tell Java where to find these tools. They're not switched on by default, so you name them at the top of the file. Path.of("sample.txt") turns a filename into something Java can use to find the file. Files.readString(...) reads the whole file and hands it back as one String, newlines and all. It looks in the folder you ran java from, so run it from inside src/.
main in that file ends with throws IOException. Reading a file can fail — the file might be missing. That phrase is you telling Java "I know, and if it happens, just stop the program". Handling it properly is Phase 1. For now, copy the phrase.
Part one: counting letters with 26 slots
String lower = text.toLowerCase();
int[] letterCounts = new int[26];
for (int i = 0; i < lower.length(); i++) {
char c = lower.charAt(i);
if (c >= 'a' && c <= 'z') {
int slot = c - 'a';
letterCounts[slot] = letterCounts[slot] + 1;
}
}Line 1 makes an all-lower-case copy, so T and t get counted together. It's a copy — text is untouched, because Strings never change. Line 2 makes 26 counters in one go, one per letter. All of them start at 0, because that's the default a fresh int[] gets.
The loop walks every character position from 0 up to length() - 1. Note the round brackets on length() — this is text, not an array. charAt(i) pulls out the character there. The if checks it's really a letter between 'a' and 'z'. That comparison works because chars are numbers, so spaces, commas and full stops fall outside the range and get skipped. Then the clever line. c - 'a' gives 0 for 'a', 1 for 'b', up to 25 for 'z'. That is exactly the 26 valid indexes. The last line adds one to that counter. Twenty-six named variables would be unbearable. One array makes it four lines.
Part two: counting words without a single new idea
lower.split("[^a-z]+") means "chop wherever you see one or more things that are not letters a-z". So spaces, commas, full stops and newlines all become cut points. Out comes a String[] of words.
Now count how often each word appears, using only what you know. The trick is two arrays that travel together. uniqueWords holds each different word once. wordCounts holds its tally at the same index. Word at index 4, its count at index 4. For each word from split, look through the words already seen. If it's there, add one to its count. If not, add it at the end and start its count at 1. That's the whole program, and there's nothing in it you haven't met.
One detail worth pointing at. The search uses uniqueWords[i].equals(word), not ==. Every one of these words was built while the program ran, by split. Use == here and the program finds nothing and reports every word as new.
Characters in the file: 327
-- letters --
a 27 ###########################
b 7 #######
c 9 #########
d 7 #######
e 32 ################################
...
s 27 ###########################
t 31 ###############################
u 8 ########
v 4 ####
w 1 #
y 4 ####
-- words --
Different words: 30
Most common word: "the" appears 11 times
-- words used more than twice --
the -> 11
sea -> 5
does -> 3
not -> 3
it -> 3
boy -> 4
counts -> 3
and -> 4
is -> 3e wins at 32, then t at 31, then a and s tied at 27. That is roughly the order of letter frequency in English generally. That's the sort of thing you only notice once a program counts it for you.
The letter counter meets a capital C in the text. Which slot of letterCounts goes up by one?
All in 06-strings/src/. Compile with javac Name.java, run with java Name — no .java on the run.
Section 5Two more questions
You want to replace every name in a String[] with its upper-case version. Which loop?
A friend writes String name = " Ada "; name.trim(); if (name.equals("Ada")) { ... } and the if never runs. What's wrong?
Section 6What broke, and what surprised you
Open a plain text file and note anything from this chapter where your prediction and reality parted ways. Especially:
- Did
substring(2, 4)give you three characters in your head? - Did you reach for
.lengthon a piece of text, or.length()on an array? - Did you believe, even for a second, that
toUpperCase()changes the String? Write that one in capitals. - Did the
==result feel wrong when you sawjava javaprinted side by side?
Those four aren't trivia. They're the four things that will bite you next chapter. That is when you build Wordle and compare a typed guess against a secret word, letter by letter. You now have every tool that needs. An array to hold the letters. charAt to pull them out. equals to compare them honestly. And the knowledge that none of your Strings will change behind your back.
- 12 Oct 2026 — The trimmed
WordCountletter table stopped att. The real table carries on withu,v,wandy, which are shown now.
Nice. Chapter marked complete.