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.

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:

SequenceMeaning
\nNew line
\tTab
\"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 wantPythonJava
Does it contain a piece?"lo wo" in ss.contains("lo wo")
Position of the first occurrences.find("x")s.indexOf("x")
Position of the last occurrences.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:

  • indexOf returns -1 if it finds nothing. It’s the “not there” signal, exactly like find in Python. (It’s the same -1 we saw in the arrays practice.)
  • It works with a String or with a char.
  • The second parameter is where to start searching. In lastIndexOf it 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’s strip()), but not the ones in the middle.
  • replace substitutes 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

PythonJava
len(s)s.length()
s == ts.equals(t)
s[3]s.charAt(3) (returns a char)
s[1:4]s.substring(1, 4)
s[5:]s.substring(5)
"x" in ss.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 * 3s.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: name still 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 a and b, the two identical literal texts point to the same object and a == b gives true. With c, created with new, there’s a different object and a == c gives false, even though a.equals(c) gives true.

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 String is written with double quotes; single quotes are for a char.
  • Strings are immutable: methods return a new one, and you have to store it (s = s.trim();).
  • Length: s.length() (with parentheses, unlike array.length).
  • To compare contents, equals (or equalsIgnoreCase); == compares references. To order, compareTo.
  • + concatenates, but it’s evaluated left to right: "a" + 1 + 2 gives "a12".
  • charAt(i) returns a char; substring(start, end) doesn’t include the end; out of range throws StringIndexOutOfBoundsException.
  • indexOf / lastIndexOf return -1 if they find nothing; contains, startsWith and endsWith return a boolean.
  • trim, toLowerCase, toUpperCase and replace return new Strings.
  • split returns 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 gives true and sometimes false with equal texts?
  • Do you remember when length takes parentheses and when it doesn’t?
  • Do you know what charAt returns and why it can be compared with ==?
  • Can you extract a piece with substring without 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 →

Sigue aprendiendo

Leave a Reply

Your email address will not be published. Required fields are marked *