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
- Working with String Objects
- Manipulate Data Using the StringBuilder Class
- 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");
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:
- If both operands are numeric,
+means numeric addition. - If either operand is a
String,+means concatenation. - 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"
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
Stringobjects 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
Stringobjects are immutable, their hash codes can be cached without worrying about changes. This makesStringobjects suitable for use as keys inHashMapand 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"
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
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)
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
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 theStringobject (number of characters):
"animals".length(); // 7
"".length(); // 0
-
toLowerCase()/toUpperCase()return a newStringobject with all characters converted to lowercase or uppercase, respectively:
"AniMaLs".toLowerCase(); // "animals"
"AniMaLs".toUpperCase(); // "ANIMALS"
-
equals()/equalsIgnoreCase()compare the content of twoStringobjects. Theequals()is case-sensitive, whileequalsIgnoreCase()is not. Both methods return aboolean:
"animals".equals("animals"); // true
"animals".equals("ANIMALS"); // false
"animals".equalsIgnoreCase("ANIMALS"); // true
-
contains()checks if theStringobject contains a specified sequence of characters. Returns aboolean:
"animals".contains("ani"); // true
"animals".contains("als"); // true
"animals".contains("AlS"); // false (case-sensitive)
-
startsWith()/endsWith()check if theStringobject starts or ends with a specified prefix or suffix. Returns aboolean:
"animals".startsWith("ani"); // true
"animals".endsWith("als"); // true
"animals".startsWith("als"); // false
-
replace()returns a newStringobject 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"
-
charAt()returns thecharat the specified index:
"animals".charAt(0); // a
"animals".charAt(6); // s
"animals".charAt(7); // throws exception
-
indexOf()finds the first index that matches the specified character or substring. Returns-1if no match is found and doesn’t throw an exception. Returns anint:
"animals".indexOf('a'); // 0
"animals".indexOf("al"); // 4
"animals".indexOf('a', 4); // 4
"animals".indexOf("al", 5); // -1
-
substring()returns parts of theStringobject:
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
-
trim()/strip()remove whitespace from the beginning and end of aStringobject. Thetrim()works only with ASCII-spaces, thestrip()(new in Java 11) and supports Unicode:
String text = " abc\t ";
text.trim().length(); // 3
text.strip().length(); // 3
-
stripLeading()/stripTrailing()(new in Java 11). The first removes whitespace from the beginning and leaves it at the end. ThestripTrailing()does the opposite:
String text = " abc\t ";
text.stripLeading().length(); // 5
text.stripTrailing().length(); // 4
-
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
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"
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"
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"
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 currentStringBuilderobject:
StringBuilder sb = new StringBuilder().append(1).append('c');
sb.append("-").append(true); // sb -> "1c-true"
-
insert()adds characters at the requested index. Theoffsetis 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-"
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, asdeleteCharAt()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)
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"
-
replace()deletes the characters starting withstartIndexand ending beforeendIndex:
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"
-
reverse()reverses the characters in theStringBuilderobject:
StringBuilder sb = new StringBuilder("ABC");
sb.reverse(); // sb -> "CBA"
3. Understanding Equality: String vs. StringBuilder
Equality Comparison in Java: String
-
==: Can returntrueif bothStringliterals point to the same object in the string pool. Fornew String(), it typically returnsfalse. -
String.equals(): Compares the content (sequence of characters) of the strings. Returnstrueif 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
Equality Comparison in Java: StringBuilder
-
==: Always checks for reference equality, just like other non-pooled objects. It will returntrueonly 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
❗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
}
}
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
Top comments (0)