Strings in Java — equals, substring and split if you come from Python (and why == sometimes fools you)
When I moved from Python to Java, I wrote something like if (name == "Sergio") and it seemed perfectly natural: that’s how you do it in Python. Sometimes it worked and sometimes it didn’t. The most baffling part was that two identical texts gave false, and I didn’t understand why. It took me a good while to discover that in Java == doesn’t compare the contents of the texts, it compares whether they are the same object. This article is so you don’t lose that afternoon.
We’ll see how Strings are created, how they’re compared, how to access their characters and pieces, how to search inside them and how to split them. As in other articles, we’ll compare with Python along the way, because almost everything you know carries over, but with different names and different traps. If you haven’t yet seen arrays, go through the arrays exercises first: split is going to hand you one.
Table of Contents
What a String is in Java
A String is a sequence of characters. It’s written between double quotes:
String greeting = "Hello world";
There’s an important difference from Python: single quotes don’t work for texts. In Java they’re reserved for a single character, which is a different type called char:
String text = "a"; // a String that contains one letter char letter = 'a'; // a char: a single character
"a" and 'a' look the same, but they aren’t: one is an object and the other a primitive value. You’ll notice right away with charAt.
An empty String is written with two quotes and nothing in between (or with new String(), which is the same but longer):
String empty1 = ""; String empty2 = new String();
And to turn another value into text there are several ways, all equivalent:
int number = 10; String text1 = String.valueOf(number); // "10" String text2 = Integer.toString(number); // "10" String text3 = "" + number; // "10" (the quick trick)
Escape sequences
Just like in Python, the backslash \ lets you write special characters inside a text, and the sequences are the same:
| Sequence | Meaning |
|---|---|
\n | New line |
\t | Tab |
\" | Double quote |
\\ | Backslash |
System.out.println("Pedro said \"hello\""); // Pedro said "hello"
System.out.println("Line 1\nLine 2"); // two lines
System.out.println("Col1\tCol2"); // Col1 Col2
System.out.println("C:\\Users\\ana"); // C:\Users\ana
The golden rule: a String doesn’t change
In Java (just like in Python), Strings are immutable: once created, they don’t change. What look like “modifications” are really methods that return a new String and leave the original as it was.
String name = "ana"; name.toUpperCase(); // creates "ANA"... and throws it away System.out.println(name); // ana (it hasn't changed!) name = name.toUpperCase(); // now we do keep the result System.out.println(name); // ANA
Calling a String method and not storing the result. name.toUpperCase(); on its own line does nothing useful: it gives no error, it doesn’t change name, and it’s one of the hardest bugs to spot. Remember: name = name.toUpperCase();.
Length: length() with parentheses
String s1 = "this is a string"; System.out.println(s1.length()); // 16
In Python you wrote len(s1); here it’s s1.length().
With arrays, length has no parentheses (array.length), and with Strings, it does (text.length()). It’s the most typical mix-up when starting out: both mean “how many are there”, but one is an attribute and the other a method. If the compiler says “cannot find symbol”, look at this first.
Comparing Strings: equals, not ==
This is the most important section of the article, and the one from my anecdote.
String s1 = "this is a string"; String s2 = "this is another string"; String s3 = "this is a string"; System.out.println(s1.equals(s2)); // false System.out.println(s1.equals(s3)); // true
To know whether two texts are equal, you use equals. If you don’t care about upper and lower case, equalsIgnoreCase:
System.out.println("Hello".equalsIgnoreCase("hello")); // true
And what about ==?
Let’s see why my comparison “sometimes worked”:
String a = "Sergio";
String b = "Sergio";
String c = new String("Sergio");
String part = "Ser";
String d = part + "gio";
System.out.println(a == b); // true (!)
System.out.println(a == c); // false (!)
System.out.println(a == d); // false (!)
System.out.println(a.equals(c)); // true
System.out.println(a.equals(d)); // true
== compares addresses, not contents. And here’s the trap: when you write two identical literal texts (a and b), Java reuses them and makes them point to the same object, so a == b gives true by coincidence. But as soon as a text is built in some other way (new String(...), a concatenation at runtime, something you read with Scanner…) it becomes a different object, and == gives false even if the content is identical.
That’s why for me it “sometimes worked”: with texts typed by hand they coincided, and with texts read from the keyboard they didn’t.
The rule is the same as with arrays: == compares references, and to compare contents you have to use a method. With arrays, Arrays.equals; with String, equals. Once you understand this, you’ll also understand how objects work in general.
Ordering: compareTo
To know which one comes first alphabetically, you use compareTo. It returns 0 if they’re equal, a negative number if the first comes before, and a positive one if it comes after:
System.out.println(s1.compareTo(s2)); // -78
System.out.println(s2.compareTo(s1)); // 78
System.out.println("a".compareTo("b")); // -1
System.out.println("hello".compareTo("hello world")); // -6
The number comes from subtracting the codes of the first pair of different characters (in the first example, ' ' − 'n' = 32 − 110 = −78, at index 9), or, if one is the beginning of the other, from the difference in lengths. What matters in practice is only the sign. There’s also compareToIgnoreCase.
If you’re going to compare a variable with a fixed text, write it the other way round: "Sergio".equals(name) instead of name.equals("Sergio"). If name is null, the first one calmly gives false and the second throws a NullPointerException.
Concatenating
Just like in Python, with +:
String s4 = "Hello" + " " + "world"; // "Hello world"
String s5 = "Hello".concat(" ").concat("world"); // the same with concat
But with a trap that doesn’t exist in Python, because Python won’t let you add numbers and texts without converting:
System.out.println("Name: " + 1 + 2); // Name: 12
System.out.println(1 + 2 + " total"); // 3 total
System.out.println("Name: " + (1 + 2)); // Name: 3
The + is evaluated from left to right. In the first case, "Name: " + 1 is already a text, and you glue the 2 onto a text: you get “12”. In the second, 1 + 2 is added first because they’re still numbers. If you want to add inside a text, use parentheses.
Accessing characters: charAt
In Python you wrote s[4]. In Java there are no square brackets for Strings; you use charAt:
String h = "Hello world"; char c1 = h.charAt(4); // 'o' System.out.println(c1 == 'o'); // true
Notice two things. First, charAt returns a char, not a String (in Python the result would be a one-character text). Second, since char is a primitive type, with chars you can use == with no problem.
Indexes start at 0 as always, and negative ones don’t exist: charAt(-1) doesn’t give the last character (that was Python), it gives an error.
h.charAt(11); // StringIndexOutOfBoundsException: Index 11 out of bounds for length 11
The exact text of the error message depends on the Java version (this one is from Java 21), but the exception is always the same: StringIndexOutOfBoundsException. And the rule, as always: the last index is length() - 1.
Looping over a String
Since there are no square brackets, you loop with charAt and a classic for, or with a for-each over toCharArray(), which turns the text into an array of char:
String h = "Hello world";
int vowels = 0;
for (int i = 0; i < h.length(); i++) {
char ch = h.charAt(i);
if (ch == 'a' || ch == 'e' || ch == 'i' || ch == 'o' || ch == 'u') {
vowels++;
}
}
System.out.println(vowels); // 3
// The same with a for-each
int vowels2 = 0;
for (char ch : h.toCharArray()) {
if ("aeiou".indexOf(ch) != -1) { // is ch inside "aeiou"?
vowels2++;
}
}
System.out.println(vowels2); // 3
Pieces of a String: substring
Python’s slicing (s[1:4]) becomes substring(start, end). Just like in Python, the end index is not included:
String h = "Hello world"; System.out.println(h.substring(1, 4)); // ell (positions 1, 2 and 3) System.out.println(h.substring(6)); // world (from 6 to the end) System.out.println(h.substring(6, 6)); // (empty: from 6 to 6 there's nothing)
With a single number, it copies to the end. And what doesn’t forgive, unlike Python, are indexes out of range:
h.substring(4, 2); // error: the start can't be greater than the end h.substring(0, 100); // error: the end goes past the text
Both throw StringIndexOutOfBoundsException. In Python, s[0:100] would simply give you the whole text; Java has no such generosity.
Forgetting that the end is not included. To keep the first 3 letters it’s not substring(0, 2), it’s substring(0, 3). And for the whole text, substring(0, h.length()).
Searching inside a String
The following table gathers the equivalents with Python:
| What I want | Python | Java |
|---|---|---|
| Does it contain a piece? | "lo wo" in s | s.contains("lo wo") |
| Position of the first occurrence | s.find("x") | s.indexOf("x") |
| Position of the last occurrence | s.rfind("x") | s.lastIndexOf("x") |
| Does it start with…? | s.startswith("x") | s.startsWith("x") |
| Does it end with…? | s.endswith("x") | s.endsWith("x") |
String h = "Hello world";
System.out.println(h.contains("lo wo")); // true
System.out.println(h.contains("house")); // false
String w = "Hello, welcome to my world";
System.out.println(w.indexOf("welcome")); // 7
System.out.println(w.indexOf("welcome", 9)); // -1 (searching from 9, it's no longer there)
System.out.println(w.indexOf('o')); // 4
System.out.println(w.indexOf('o', 5)); // 11 (the next 'o')
System.out.println(w.indexOf("xyz")); // -1
String b = "A banana in a banana tree";
System.out.println(b.lastIndexOf("banana")); // 14
System.out.println(b.lastIndexOf("banana", 9)); // 2 (searching backwards from 9)
String email = "ana.perez@alu.ulpgc.es";
System.out.println(email.startsWith("ana")); // true
System.out.println(email.endsWith("@alu.ulpgc.es")); // true
System.out.println(email.startsWith("Ana")); // false (it's case-sensitive)
What’s worth remembering:
indexOfreturns-1if it finds nothing. It’s the “not there” signal, exactly likefindin Python. (It’s the same-1we saw in the arrays practice.)- It works with a
Stringor with achar. - The second parameter is where to start searching. In
lastIndexOfit searches backwards from that position.
Using the result of indexOf without checking it isn’t -1. If you do email.substring(0, email.indexOf('@')) with a text that has no @, indexOf returns -1 and substring(0, -1) throws an exception. Check first.
Cleaning and transforming
All of these methods return a new String (remember the golden rule):
System.out.println("[" + " hello ".trim() + "]"); // [hello]
System.out.println("Hello".toLowerCase()); // hello
System.out.println("Hello".toUpperCase()); // HELLO
System.out.println("banana".replace('a', 'o')); // bonono
System.out.println("banana".replace("an", "AN")); // bANANa
System.out.println("hello world, hello java".replace("hello", "bye")); // bye world, bye java
trim()removes the spaces from both ends (like Python’sstrip()), but not the ones in the middle.replacesubstitutes all the occurrences, not just the first.
Splitting a String: split
split cuts a text into pieces and returns them in a String array (in Python, a list):
String[] parts = "one,two,three".split(",");
System.out.println(Arrays.toString(parts)); // [one, two, three]
System.out.println(parts[1]); // two
System.out.println(parts.length); // 3
There are three details that aren’t like Python and that cause trouble:
1. You always have to write the separator
In Python, s.split() with no arguments splits on spaces. In Java there’s no split() without parameters: you have to say the separator, and also two spaces in a row leave an empty text in the middle:
System.out.println(Arrays.toString("a b".split(" "))); // [a, , b]
System.out.println(Arrays.toString("a b".split("\\s+"))); // [a, b]
The second form ("\\s+", which means “one or more spaces”) uses a regular expression; you’ll see them calmly in their own topic. For now, copy it if you need to split on spaces.
2. The separator is a regular expression
This is what surprises people most. The parameter of split is not a literal text, it’s a regular expression, and some characters have a special meaning, like . and |:
System.out.println(Arrays.toString("1.2.3".split("."))); // [] (empty array!)
System.out.println(Arrays.toString("1.2.3".split("\\."))); // [1, 2, 3]
System.out.println(Arrays.toString("a|b".split("|"))); // [a, |, b]
System.out.println(Arrays.toString("a|b".split("\\|"))); // [a, b]
"1.2.3".split(".") returns an empty array, because in a regular expression the dot means “any character” and the whole text is made of separators. To split on a dot (or on |, +, *, ?…) you have to “escape” it with two backslashes: "\\.". If your split returns odd things, suspect the separator.
3. Trailing empty pieces disappear
System.out.println(Arrays.toString("a,b,,".split(","))); // [a, b]
System.out.println(Arrays.toString("a,b,,".split(",", -1))); // [a, b, , ]
By default, split discards the empty pieces at the end. With a second parameter of -1, it keeps them. And with a positive number, it limits how many pieces at most:
System.out.println(Arrays.toString("one,two,three,four".split(",", 2))); // [one, two,three,four]
Other utilities
A few more you’ll use a lot:
System.out.println("hello".isEmpty()); // false
System.out.println("".isEmpty()); // true
System.out.println(" ".isBlank()); // true (only spaces)
System.out.println(String.join("-", "a", "b", "c")); // a-b-c
System.out.println("ab".repeat(3)); // ababab (Python's "ab" * 3)
System.out.println(Integer.parseInt("42") + 1); // 43 (from text to number)
And if the text isn’t a valid number, Integer.parseInt("4x2") throws a NumberFormatException.
To learn more: StringBuilder
Since Strings don’t change, a loop that concatenates many times (result += ...) creates a new String on every pass. For a little data it doesn’t matter; for a lot, there’s StringBuilder, which is modifiable:
StringBuilder sb = new StringBuilder();
for (int i = 1; i <= 5; i++) {
sb.append(i).append(", ");
}
sb.setLength(sb.length() - 2); // we remove the last ", "
System.out.println(sb.toString()); // 1, 2, 3, 4, 5
System.out.println(new StringBuilder("hello").reverse()); // olleh
You don’t need it to get started, but you’ll run into it soon, and reverse() is a quick way to reverse a text.
Summary of equivalences with Python
| Python | Java |
|---|---|
len(s) | s.length() |
s == t | s.equals(t) |
s[3] | s.charAt(3) (returns a char) |
s[1:4] | s.substring(1, 4) |
s[5:] | s.substring(5) |
"x" in s | s.contains("x") |
s.find("x") | s.indexOf("x") |
s.upper() / s.lower() | s.toUpperCase() / s.toLowerCase() |
s.strip() | s.trim() |
s.split(",") | s.split(",") (returns an array; the separator is a regex) |
s * 3 | s.repeat(3) |
A complete example: student records
Let’s put several pieces together in a small program. We receive lines of text with mixed-up data (with extra spaces) and we want to extract the name, the initials, the username from the university email and the subject in capital letters.
public class StudentRecord {
// " Ana Pérez ; email ; fp2 " -> {"Ana Pérez", "email", "fp2"} without extra spaces
static String[] splitFields(String line) {
String[] fields = line.split(";");
for (int i = 0; i < fields.length; i++) {
fields[i] = fields[i].trim(); // trim returns a NEW String: we must store it
}
return fields;
}
// "Ana Pérez Suárez" -> "APS"
static String initials(String fullName) {
String result = "";
String[] words = fullName.split(" ");
for (String word : words) {
if (!word.isEmpty()) { // with double spaces, split leaves empty Strings
result += word.substring(0, 1).toUpperCase();
}
}
return result;
}
static boolean isUniversityEmail(String email) {
int at = email.indexOf('@'); // -1 if there is no @
return at > 0 && email.endsWith("ulpgc.es");
}
// What comes before the @ (only call it if isUniversityEmail is true)
static String username(String email) {
return email.substring(0, email.indexOf('@'));
}
public static void main(String[] args) {
String[] lines = {
" Ana Pérez Suárez ; ana.perez@alu.ulpgc.es ; fp2 ",
"Luis Gómez;luis.gomez@gmail.com;ic2",
"Marta Ruiz ; marta.ruiz.ulpgc.es ; FP1"
};
for (String line : lines) {
String[] fields = splitFields(line);
String name = fields[0];
String email = fields[1];
String subject = fields[2].toUpperCase();
System.out.println("Student: " + name + " (" + initials(name) + ")");
if (isUniversityEmail(email)) {
System.out.println("Username: " + username(email));
} else {
System.out.println("Not a university email: " + email);
}
System.out.println("Subject: " + subject);
System.out.println();
}
}
}
Output:
Student: Ana Pérez Suárez (APS) Username: ana.perez Subject: FP2 Student: Luis Gómez (LG) Not a university email: luis.gomez@gmail.com Subject: IC2 Student: Marta Ruiz (MR) Not a university email: marta.ruiz.ulpgc.es Subject: FP1
Notice how the pieces combine: split returns an array that we loop over with a for; trim returns a new String and that’s why we store it in fields[i]; indexOf returns -1 if there’s no @ and that’s why we check at > 0 before calling substring; and in initials, the second line (“Luis Gómez”, with three spaces) leaves empty Strings when splitting, so we skip them with isEmpty().
See it with Python Tutor
Immutability and == are much easier to understand by seeing them. Go to pythontutor.com/java.html, paste this code, and step through it with “Next”:
public class Main {
public static void main(String[] args) {
String name = "ana";
name.toUpperCase();
String big = name.toUpperCase();
System.out.println(name);
System.out.println(big);
String a = "Sergio";
String b = "Ser" + "gio";
String c = new String("Sergio");
System.out.println(a == b);
System.out.println(a == c);
System.out.println(a.equals(c));
}
}
What to look for:
- After
name.toUpperCase();, a new String"ANA"appears in memory, but nobody keeps it:namestill points to"ana". - With
big = name.toUpperCase(), now there is a variable pointing to"ANA". The two texts live side by side as different objects. - With
aandb, the two identical literal texts point to the same object anda == bgivestrue. Withc, created withnew, there’s a different object anda == cgivesfalse, even thougha.equals(c)givestrue.
Questions you might be asking
Can I change one character of a String, like s[0] = 'X' in other languages?
No: Strings are immutable and nothing like that exists. What you do is build a new String (for example, "X" + s.substring(1)) or use a StringBuilder, which does let you modify characters.
What’s the difference between "" and null?
"" is a real String with zero characters: you can call its methods ("".length() gives 0). null means the variable doesn’t point to any String: calling any method on it throws a NullPointerException. It’s the same idea as with arrays.
When do I use char and when String?
Use char when you work with one character (the result of charAt, or when looping over a text) and String for any text, even one with a single letter. A char is written with single quotes and can be compared with ==; a String, with double quotes and is compared with equals.
How do I turn a text into a number?
With Integer.parseInt("42") for integers and Double.parseDouble("3.5") for decimals. If the text isn’t valid, they throw NumberFormatException, an error you’ll meet when you cover exceptions.
Summary
- A
Stringis written with double quotes; single quotes are for achar. Strings are immutable: methods return a new one, and you have to store it (s = s.trim();).- Length:
s.length()(with parentheses, unlikearray.length). - To compare contents,
equals(orequalsIgnoreCase);==compares references. To order,compareTo. +concatenates, but it’s evaluated left to right:"a" + 1 + 2gives"a12".charAt(i)returns achar;substring(start, end)doesn’t include the end; out of range throwsStringIndexOutOfBoundsException.indexOf/lastIndexOfreturn-1if they find nothing;contains,startsWithandendsWithreturn a boolean.trim,toLowerCase,toUpperCaseandreplacereturn new Strings.splitreturns an array, its separator is a regular expression ("\\."for the dot) and it discards trailing empty pieces.
Checklist: do you have it clear?
- Do you know why
name.toUpperCase();on its own does nothing? - Can you explain why
==sometimes givestrueand sometimesfalsewith equal texts? - Do you remember when
lengthtakes parentheses and when it doesn’t? - Do you know what
charAtreturns and why it can be compared with==? - Can you extract a piece with
substringwithout forgetting that the end isn’t included? - Do you know why
"1.2.3".split(".")returns an empty array?
If any of them left you with doubts, go back to its section before moving on. In the next article we’ll put all this into practice with complete programs.
Next step: practiceStrings in Java — practice with a student importer, a username generator and a contact finder (and why you must normalize before comparing)Did this article help you? Here is how you can support SergioLearns →
