DEV Community

Cover image for Mastering String APIs in Java
Marta Kravchuk
Marta Kravchuk

Posted on

Mastering String APIs in Java

In Java, strings are fundamental for data manipulation. This article delves into the core concepts of Java's String and StringBuilder classes, essential components of the Java Standard Library. It will explore how to create and manipulate String objects, understand the nuances of immutability, and leverage the string pool for efficiency. Additionally, the mutable nature of the StringBuilder class and its methods for dynamic string construction will be discussed here, providing a comprehensive understanding of string handling in Java.


Table of Contents

  1. Working with String Objects
  2. Manipulate Data Using the StringBuilder Class
  3. Understanding Equality: String vs. StringBuilder

String APIs refer to the set of methods and functionalities provided by the String and StringBuilder classes, which are part of the Java Standard Library.

1. Working with String Objects

The String class is fundamental in Java, representing an immutable sequence of characters. It implements the CharSequence interface, a general way of representing character sequences, also implemented by classes like StringBuilder. In Java, a String object can be created as follows (the new keyword is optional):

String name = "Fluffy";
String name = new String("Fluffy");
Enter fullscreen mode Exit fullscreen mode

In both examples, a reference variable name is created, pointing to a String object with the value "Fluffy". However, the creation process differs subtly. The String class is unique in that it doesn’t require instantiation with new, it can be created directly from a literal.

Concatenation

Placing one String object before another String object and combining them (using the + operator) is called string concatenation.
The most important rules to know are:

  1. If both operands are numeric, + means numeric addition.
  2. If either operand is a String, + means concatenation.

  3. The expression is evaluated left to right.
System.out.println(1 + 2);               // 3
System.out.println("a" + "b");           // "ab"
System.out.println("a" + "b" + 3);       // "ab3"
System.out.println(1 + 2 + "c");         // "3c"
System.out.println("c" + 1 + 2);         // "c12" => "c1" + 2 => "c12"
int three = 3;
String four = "4";
System.out.println(1 + 2 + three + four); // "64" => 3 + 3 + "4" => 6 + “4"

Enter fullscreen mode Exit fullscreen mode

Immutability and the String Pool

In Java, the String class is immutable. Once a String object is created, its value cannot be changed. This design choice offers key benefits:

  • Immutable String objects are inherently thread-safe, which prevents data corruption in parallel environments.
  • Immutability increases security. For example, class names or arguments passed to methods are often represented as strings. By making strings immutable, you prevent attackers from changing these values after they are passed.
  • Since String objects are immutable, their hash codes can be cached without worrying about changes. This makes String objects suitable for use as keys in HashMap and other hash-based data structures, improving performance.

The immutability of String objects is demonstrated in the following example:

String s1 = "1";
String s2 = s1.concat("2");
s2.concat("3");            // This call is effectively ignored
System.out.println(s2);    // Output: "12"
Enter fullscreen mode Exit fullscreen mode

In this example, s1.concat("2") creates a new String object, "12", and assigns its reference to s2. Even though s2.concat("3") is called, it doesn't change the value of s2. Because strings are immutable, concat() always returns a new String object. Since the result of s2.concat("3") isn't assigned to anything, it's discarded, and s2 remains "12".

Since strings are everywhere in Java, they can consume significant memory, especially in production applications. To mitigate this, Java reuses common strings through the string pool (also known as the intern pool), a special memory area in the Java Virtual Machine (JVM) that stores String literals to optimize memory usage.

So, the string pool contains String literals and constants. For example, x or y are String literals and will be placed in the string pool. But, the myObject.toString() is a string but not a literal, so it typically doesn’t go into the string pool directly. And String objects created with the new keyword are not automatically added to the string pool:

String x = "Hello World";       // x goes into the string pool
String y = "Hello World";       // y goes into the string pool
System.out.println(x == y);     // true (x and y refer to the same object)

String z = "Hello World";
String myObject = new String("Hello World");     // this is object
System.out.println(z == myObject);               // false
Enter fullscreen mode Exit fullscreen mode

The String object created as a result of runtime calculations (e.g., using String methods), even if they have the same content, a new String object will be created:

String x = "Hello World";
String z = " Hello World".trim();   // a new String object is created
System.out.println(x == z);         // false (x and z are different objects)
Enter fullscreen mode Exit fullscreen mode

Concatenation with non-literals also results in a new String object:

String single = "hello world";
String concat = "hello ";
concat += "world";
System.out.println(single == concat);   // false
Enter fullscreen mode Exit fullscreen mode

When a String literal is created, the JVM first checks if an identical String already exists in the pool. If it does, the variable simply points to the existing String object in the pool, avoiding the creation of a duplicate. This mechanism offers significant memory savings, especially in applications that use many identical string values.

Essential String Methods and the Method Chaining

For all these methods, remember that a String is a sequence of characters, and Java indexing starts at 0.

  • length() returns the length of the String object (number of characters):
"animals".length();    // 7
"".length();           // 0
Enter fullscreen mode Exit fullscreen mode
  • toLowerCase() / toUpperCase() return a new String object with all characters converted to lowercase or uppercase, respectively:
"AniMaLs".toLowerCase();    // "animals"
"AniMaLs".toUpperCase();    // "ANIMALS"
Enter fullscreen mode Exit fullscreen mode
  • equals() / equalsIgnoreCase() compare the content of two String objects. The equals() is case-sensitive, while equalsIgnoreCase() is not. Both methods return a boolean:
"animals".equals("animals");             // true
"animals".equals("ANIMALS");             // false
"animals".equalsIgnoreCase("ANIMALS");   // true
Enter fullscreen mode Exit fullscreen mode
  • contains() checks if the String object contains a specified sequence of characters. Returns a boolean:
"animals".contains("ani");     // true
"animals".contains("als");     // true
"animals".contains("AlS");     // false (case-sensitive)
Enter fullscreen mode Exit fullscreen mode
  • startsWith() / endsWith() check if the String object starts or ends with a specified prefix or suffix. Returns a boolean:
"animals".startsWith("ani");    // true
"animals".endsWith("als");      // true
"animals".startsWith("als");    // false
Enter fullscreen mode Exit fullscreen mode
  • replace() returns a new String object in which all occurrences of a specified sequence of characters are replaced with another sequence:
"animals".replace("a", "o");       // "onimols"
"animals".replace("als", "zzz");   // "animzzz"
Enter fullscreen mode Exit fullscreen mode
  • charAt() returns the char at the specified index:
"animals".charAt(0);      // a
"animals".charAt(6);      // s
"animals".charAt(7);      // throws exception
Enter fullscreen mode Exit fullscreen mode
  • indexOf() finds the first index that matches the specified character or substring. Returns -1 if no match is found and doesn’t throw an exception. Returns an int:
"animals".indexOf('a');        // 0
"animals".indexOf("al");       // 4
"animals".indexOf('a', 4);     // 4
"animals".indexOf("al", 5);    // -1
Enter fullscreen mode Exit fullscreen mode
  • substring() returns parts of the String object:
String string = "animals";


"animals".substring(3);                     // "mals"
"animals".substring(string.indexOf('m'));   // "mals"
"animals".substring(3, 5);                  // "ma"
"animals".substring(3, 3);                  // empty string
"animals".substring(3, 2);                  // throws exception
"animals".substring(3, 8);                  // throws exception
Enter fullscreen mode Exit fullscreen mode
  • trim() / strip() remove whitespace from the beginning and end of a String object. The trim() works only with ASCII-spaces, the strip() (new in Java 11) and supports Unicode:
String text = " abc\t ";
text.trim().length();             // 3
text.strip().length();            // 3
Enter fullscreen mode Exit fullscreen mode
  • stripLeading() / stripTrailing() (new in Java 11). The first removes whitespace from the beginning and leaves it at the end. The stripTrailing() does the opposite:
String text = " abc\t ";
text.stripLeading().length();            // 5
text.stripTrailing().length();           // 4
Enter fullscreen mode Exit fullscreen mode
  • intern() uses an object in the string pool if one is present. If the literal is not yet in the string pool, Java will add it at this time:
String x = "Hello World";
String y = new String("Hello World").intern();
System.out.println(x == y);              // true

String first = "rat" + 1;
String second = "r" + "a" + "t" + "1";
String third = "r" + "a" + "t" + new String("1");
System.out.println(first == second);             // true
System.out.println(first == second.intern());    // true
System.out.println(first == third);              // false
System.out.println(first == third.intern());     // true
Enter fullscreen mode Exit fullscreen mode

So, String methods can be used sequentially, with each method call assigning its resulting String object to a new variable:

String start = "AniMaL   ";
String trimmed = start.trim();               // "AniMaL"
String lowercase = trimmed.toLowerCase();    // "animal"
String result = lowercase.replace('a', 'A'); // "AnimAl"
Enter fullscreen mode Exit fullscreen mode

However, this approach can become verbose and less readable. Instead, Java allows you to use a technique called method chaining, where multiple method calls are chained together in a single expression:

String result = "AniMaL   "
    .trim()
    .toLowerCase()
    .replace('a', 'A');   // "AnimAl"
Enter fullscreen mode Exit fullscreen mode

2. Manipulate Data Using the StringBuilder Class

The StringBuilder class represents mutable sequences of characters. Most of its methods return a reference to the current object, enabling method chaining.

StringBuilder Methods

The StringBuilder class has many methods: substring() does not change the value of a StringBuilder, whereas append(), delete(), and insert() does change it.

StringBuilder sb = new StringBuilder("start");
sb.append("+middle");                       // "start+middle"
StringBuilder same = sb.append("+end");     // "start+middle+end"

StringBuilder a = new StringBuilder("abc");
StringBuilder b = a.append("de");
b = b.append("f").append("g");
System.out.println(a);               // Output: "abcdefg"
System.out.println(b);               // Output: "abcdefg"
Enter fullscreen mode Exit fullscreen mode

charAt(), indexOf(), length(), and substring(): these four methods work the same as in the String class:

  • append() adds values (e.g., String, char, int, Object, etc.) to the end of the current StringBuilder object:
StringBuilder sb = new StringBuilder().append(1).append('c');
sb.append("-").append(true);                // sb -> "1c-true"  
Enter fullscreen mode Exit fullscreen mode
  • insert() adds characters at the requested index. The offset is the index where we want to insert the requested parameter:
StringBuilder sb = new StringBuilder("animals");
sb.insert(7, "-");                          // sb -> "animals-"
sb.insert(0, "-");                          // sb -> "-animals-"
sb.insert(4, "-");                          // sb -> "-ani-mals-"
Enter fullscreen mode Exit fullscreen mode

As we add and remove characters, their indexes change. Also, if offset is equal to length() + 1 a StringIndexOutOfBoundsException will be thrown.

  • delete() / deleteCharAt(), the first removes characters from sequence, as deleteCharAt() uses to remove only one character at the specified index:
StringBuilder sb = new StringBuilder("abcdef");
sb.delete(1, 3);       // sb -> "adef" (the length is 4)
sb.deleteCharAt(5);    // throws an exception (the index is out of range)
Enter fullscreen mode Exit fullscreen mode

Even if delete() is called with an end index beyond the length of the StringBuilder, it will not throw an exception but will simply delete up to the end of the StringBuilder:

StringBuilder sb = new StringBuilder("abcdef");
sb.delete(1, 100);                  // sb -> "a"
Enter fullscreen mode Exit fullscreen mode
  • replace() deletes the characters starting with startIndex and ending before endIndex:
StringBuilder builder = new StringBuilder("pigeon dirty");
builder.replace(3, 6, "sty");        // sb -> "pigsty dirty"
builder.replace(3, 100, "a");        // sb -> "piga"
builder.replace(2, 100, "");         // sb -> "pi"
Enter fullscreen mode Exit fullscreen mode
  • reverse() reverses the characters in the StringBuilder object:
StringBuilder sb = new StringBuilder("ABC");
sb.reverse();                        // sb -> "CBA"
Enter fullscreen mode Exit fullscreen mode

3. Understanding Equality: String vs. StringBuilder

Equality Comparison in Java: String

  • ==: Can return true if both String literals point to the same object in the string pool. For new String(), it typically returns false.
  • String.equals(): Compares the content (sequence of characters) of the strings. Returns true if the contents are identical.
String a = "Fluffy";
String b = new String("Fluffy");
a == b;                       // false
a.equals(b);                  // true

String x = "Hello World";
String z = " Hello World".trim();
x == y;                       // false
x.equals(z);                  // true
Enter fullscreen mode Exit fullscreen mode

Equality Comparison in Java: StringBuilder

  • ==: Always checks for reference equality, just like other non-pooled objects. It will return true only when both references point to the same StringBuilder object.
  • StringBuilder.equals(): Compares the object references. Returns true only if both references point to the same StringBuilder object.
StringBuilder one = new StringBuilder();
StringBuilder two = new StringBuilder();
StringBuilder three = one.append("a");

one == two;                       // false
one.equals(two);                  // false

one == three;                     // true
one.equals(three);                // true
Enter fullscreen mode Exit fullscreen mode

Note: If the class does not have an equals() method, Java uses the == operator to check equality. Remember that == is checking for object reference equality.

public class Cat {
    String name;

    public void main(String[] args) {
        Cat t1 = new Cat();
        Cat t2 = new Cat();
        Cat t3 = t1;

        t1 == t2;         // false
        t1 == t3;         // true

        t1.equals(t2);    // false: equals() is absent -> t1 == t2
    }
}
Enter fullscreen mode Exit fullscreen mode

The compiler is smart enough to know that two references can’t possibly point to the same object when they are completely different types.

String string = "a";
StringBuilder builder = new StringBuilder("a");
string == builder;          // DOES NOT COMPILE
builder.equals(string)      // false
Enter fullscreen mode Exit fullscreen mode

Thanks for reading! Hope you found some useful info in this article. Got any questions or ideas? Drop them in the comments below. Stay tuned here for a new Java series every week! And feel free to connect with me on LinkedIn 😊

Top comments (0)