Strings in Java
1. What is a String in Java
A String in Java is a sequence of characters represented by the java.lang.String class. It is an object, not a primitive type.
String greeting = "Hello, World!";
Internally, a String is backed by a char[] (character array), and provides many useful methods for manipulation.
2. How to Create Strings
a. Using String Literal
Definition:
When you use the new keyword to create a string, a new String object is explicitly created in the heap memory, even if the same string already exists in the String Constant Pool.
String str1 = "Java";
Explanation:
- Memory efficient due to reuse of existing objects in the pool.
- Automatically managed by the JVM.
- Most preferred way to create strings when immutability is acceptable and thread-safety is not an issue.
Example:
String str1 = "Java";
String str2 = "Java";
System.out.println(str1 == str2); // true (same reference)
b. Using the new Keyword
Definition:
When you use the new keyword to create a string, a new String object is explicitly created in the heap memory, even if the same string already exists in the String Constant Pool.
String str2 = new String("Java");
Explanation:
- This method does not check the String Pool for an existing string.
- It always creates a new instance, which consumes more memory.
- Used when you specifically want a new string object.
Example:
String str1 = "Java";
String str2 = new String("Java");
System.out.println(str1 == str2); // false (different objects)
System.out.println(str1.equals(str2)); // true (same content)
3. How Strings are Stored in Java Memory
In Java, strings can be stored in two different memory locations, depending on how they are created:
a. String Literals
When a string is created using a literal, such as:
String a = "Hello";
Java stores this string in a special part of heap memory called the String Constant Pool (also known as the String Intern Pool).
What is the String Constant Pool?
- It's a reserved area of memory in the Java heap.
- It stores unique string literals to avoid duplication.
- If the same literal is used again, Java reuses the existing object instead of creating a new one.
How it works:
String a = "Hello";
String b = "Hello";
System.out.println(a == b); // true
- Both a and b refer to the same object in the String Pool.
- So, a == b returns true because both references point to the same memory address.
b. Strings using new Keyword
When you create a string using the new keyword:
String b = new String("Hello");
Java does not check the String Pool first. Instead, it creates a new object in the heap memory, even if an identical string already exists in the pool.
How it behaves:
String a = "Hello";
String b = new String("Hello");
System.out.println(a == b); // false
System.out.println(a.equals(b)); // true
- a == b is false because:
- a points to a string in the String Pool
- b points to a new object in the heap
- a.equals(b) is true because the content is the same
Illustration (Memory Representation):
String Pool (part of Heap)
--------------------------
"Hello" <--- Reference held by 'a'
Heap Memory
--------------------------
New String object with content "Hello" <--- Reference held by 'b'
Key Differences:
Aspect | String Literal | new String("...") |
---|---|---|
Memory Location | String Constant Pool (Heap area) | Heap (outside the pool) |
Reuse Existing Object | Yes | No |
Duplicate Storage | Avoided | Duplicated if same value exists |
== Comparison | May return true | Always false (unless interned) |
Pro Tip:
If you want to ensure a string created with new gets added to the pool, you can use .intern():
String a = "Hello";
String b = new String("Hello").intern();
System.out.println(a == b); // true
4. String Pool in Java
What is the String Pool?
The String Pool (also known as the String Constant Pool) is a special memory area in the Java heap. It is designed to store unique string literals so that they can be reused instead of creating multiple identical objects in memory.
This mechanism helps optimize memory usage and improves performance in Java applications.
How it Works:
When a string is created using a literal, the JVM does the following:
- Checks the String Pool to see if a string with the same content already exists.
- If it does exist, the new variable will point to the existing object.
- If it does not exist, a new string object is created in the pool.
Example 1: Using String Literals
String s1 = "Java";
String s2 = "Java";
System.out.println(s1 == s2); // true
Explanation:
- Both s1 and s2 are created using string literals.
- The string "Java" is stored only once in the String Pool.
- Both s1 and s2 refer to the same memory location.
- Therefore, s1 == s2 returns true (same reference).
Example 2: Using new Keyword
String s1 = "Java";
String s3 = new String("Java");
System.out.println(s1 == s3); // false
Explanation:
- s1 is stored in the String Pool (because it's a literal).
- s3 creates a new object in the heap, even though "Java" already exists in the pool.
- So, s1 and s3 refer to different memory locations.
- Therefore, s1 == s3 returns false.
- However, s1.equals(s3) will return true because their contents are the same.
How to Force Pool Reference Using .intern()
You can explicitly move a string created with new into the String Pool using the .intern() method:
String s1 = "Java";
String s3 = new String("Java").intern();
System.out.println(s1 == s3); // true
Explanation:
- .intern() returns a reference from the String Pool.
- Now s1 and s3 both point to the same object in the pool.
- == comparison returns true.
Summary:
Case | Location | Reference Equal (==) | Content Equal (.equals()) |
---|---|---|---|
Two literals | String Pool | true | true |
Literal and new String() | Pool & Heap | false | true |
new String().intern() vs literal | Both in Pool | true | true |
5. Immutable Nature of Strings

What Does "Immutable" Mean?
In Java, Strings are immutable, which means once a String object is created, its value cannot be changed. Any operation that seems to modify the string will not change the original object, but instead create a new String object with the modified value.
Why Are Strings Immutable in Java?
There are several reasons for making String immutable:
- Security
Strings are widely used in Java for storing sensitive data like file paths, URLs, usernames, and passwords. Immutability ensures that this data cannot be altered after creation. - String Pool Optimization
Since strings are immutable, they can safely be stored and reused from the String Pool without any risk of one reference changing the value for others. - Thread Safety
Strings are inherently thread-safe because their values cannot be modified by one thread while being accessed by another. - Efficient Hashing
Strings are often used as keys in hash-based collections (like HashMap, HashSet). Immutability ensures that the hashcode remains consistent.
Example 1: Unchanged Original String
String str = "Hello";
str.concat(" World");
System.out.println(str); // Output: Hello
Explanation:
- str.concat(" World") creates a new string object with the value "Hello World".
- But the result is not stored or assigned to any variable.
- Therefore, the original str still refers to "Hello".
- The original object is not modified.
Example 2: Updating the Reference
String str = "Hello";
str = str.concat(" World");
System.out.println(str); // Output: Hello World
Explanation:
- str.concat(" World") again creates a new string object.
- This time, the result is assigned back to str.
- Now, str points to the new string object "Hello World".
- The original "Hello" string is still in memory (if no longer referenced, it becomes eligible for garbage collection).
Visual Representation:
// Original
str -> "Hello"
// After str.concat(" World");
[New String Object] -> "Hello World" (but not assigned)
// After str = str.concat(" World");
str -> "Hello World"
Important Note:
While you cannot change the contents of a String object, you can change the reference to point to a different String.
How to Create a Custom Immutable Class in Java
An immutable class is a class whose objects cannot be modified after creation. This is especially useful in multi-threaded environments, where immutability guarantees thread safety without synchronization.
Java’s built-in String class is a perfect example of an immutable class.
Why Create Immutable Classes?
- Simpler to design, test, and use.
- Thread-safe without synchronization.
- Makes caching and reuse easier.
- Prevents accidental changes to objects.
Key Rules to Make a Class Immutable
- Declare the class as final so it cannot be extended.
- Make all fields private and final.
- Do not provide setter methods.
- Initialize all fields in the constructor.
- If the class has mutable objects as fields, do not expose them directly. Return a deep copy instead.
- Do not allow subclasses to override methods (especially if they can alter state).
Step-by-Step Example: Immutable Person Class
Let’s say we want to create an immutable class Person with the following fields:
- String name
- int age
- List hobbies (mutable object)
Step 1: Declare class as final
This prevents subclasses from modifying behavior.
public final class Person {
Step 2: Make fields private and final
private final String name;
private final int age;
private final List hobbies;
Step 3: Initialize fields via constructor
We create a constructor that initializes all fields. For mutable fields like lists, we create a defensive copy.
public Person(String name, int age, List hobbies) {
this.name = name;
this.age = age;
// Defensive copy to prevent external modification
this.hobbies = new ArrayList<>(hobbies);
}
Step 4: Only provide getters. For mutable fields, return a copy
public String getName() {
return name;
}
public int getAge() {
return age;
}
public List getHobbies() {
// Return a copy to prevent modification of internal list
return new ArrayList<>(hobbies);
}
Complete Immutable Class Code
import java.util.List;
import java.util.ArrayList;
public final class Person {
private final String name;
private final int age;
private final List hobbies;
public Person(String name, int age, List hobbies) {
this.name = name;
this.age = age;
this.hobbies = new ArrayList<>(hobbies); // Defensive copy
}
public String getName() {
return name;
}
public int getAge() {
return age;
}
public List getHobbies() {
return new ArrayList<>(hobbies); // Return a copy
}
}
Test Case
import java.util.*;
public class Main {
public static void main(String[] args) {
List hobbies = new ArrayList<>();
hobbies.add("Reading");
hobbies.add("Swimming");
Person person = new Person("John", 25, hobbies);
// Try to modify the original list
hobbies.add("Cycling");
// Try to modify the list returned by getter
person.getHobbies().add("Running");
System.out.println("Person's hobbies: " + person.getHobbies());
}
}
Output:
Person's hobbies: [Reading, Swimming]
Explanation:
- The original list modification (hobbies.add("Cycling")) doesn’t affect the internal list because a copy was made.
- The returned list from getHobbies() is also a copy, so modifying it does not affect the internal state.
6. Commonly Used String Methods
The String class in Java provides many built-in methods for performing common string operations such as comparison, searching, modification, and formatting.
Below are the most commonly used methods, along with their purpose, syntax, explanation, and output:
a. length()
Returns the number of characters in the string (including spaces).
String s = "Java";
System.out.println(s.length()); // Output: 4
Explanation:
The string "Java" contains 4 characters: 'J', 'a', 'v', 'a'.
b. charAt(int index)
Returns the character at the given index (indexing starts from 0).
String s = "Java";
System.out.println(s.charAt(2)); // Output: v
Explanation:
Index 0 → 'J', Index 1 → 'a', Index 2 → 'v', Index 3 → 'a'
c. substring()
Returns a sub-part of the original string.
String s = "Programming";
System.out.println(s.substring(3)); // Output: gramming
System.out.println(s.substring(0, 6)); // Output: Progra
Explanation:
- substring(start) returns substring from start to end.
- substring(start, end) returns substring from start to end - 1.
d. concat(String str)
Concatenates (joins) two strings.
String s = "Hello";
System.out.println(s.concat(" World")); // Output: Hello World
Note:
Returns a new string since strings are immutable.
e. equals() and equalsIgnoreCase()
Compare content of two strings.
System.out.println("Java".equals("java")); // Output: false
System.out.println("Java".equalsIgnoreCase("java")); // Output: true
Explanation:
- equals() is case-sensitive
- equalsIgnoreCase() ignores case
f. compareTo(String anotherString)
Performs lexicographic (dictionary order) comparison.
System.out.println("Apple".compareTo("Banana")); // Output: -1 (or any negative value)
Explanation:
- Returns 0 if both strings are equal
- Returns negative if calling string is less
- Returns positive if calling string is greater
g. contains(CharSequence s)
Checks if the string contains a specific sequence of characters.
System.out.println("Hello World".contains("World")); // Output: true
h. indexOf() and lastIndexOf()
Return the position of a character or substring.
String s = "Hello World";
System.out.println(s.indexOf("o")); // Output: 4
System.out.println(s.lastIndexOf("o")); // Output: 7
Explanation:
- indexOf() gives the first occurrence
- lastIndexOf() gives the last occurrence
i. toUpperCase() and toLowerCase()
Convert a string to upper-case or lower-case.
System.out.println("hello".toUpperCase()); // Output: HELLO
System.out.println("HELLO".toLowerCase()); // Output: hello
j. trim()
Removes leading and trailing white spaces.
System.out.println(" hello ".trim()); // Output: hello
k. replace(char oldChar, char newChar)
Replaces all occurrences of a character with another character.
System.out.println("Java".replace('a', 'o')); // Output: Jovo
l. split(String regex)
Splits a string into an array using the given delimiter (regular expression).
String[] fruits = "apple,banana,grapes".split(",");
for (String f : fruits) {
System.out.println(f);
}
Output:
apple
banana
grapes
Explanation:
- The delimiter is a comma ,
- The result is an array of strings split at each comma
These methods form the foundation of string manipulation in Java and are frequently used in both simple and advanced programming tasks.
7. String Comparison: == vs .equals()
- == checks reference
- .equals() checks content
String a = "Test";
String b = new String("Test");
System.out.println(a == b); // false
System.out.println(a.equals(b)); // true
8. StringBuffer and StringBuilder
In Java, since String objects are immutable, you cannot modify their contents once they are created. For scenarios where you need to perform many modifications to a string (like in loops or real-time processing), Java provides two mutable alternatives:
- StringBuffer
- StringBuilder
Both allow you to modify string content without creating new objects each time.

StringBuffer
- A mutable class in Java that allows modifications to the string content.
- It is thread-safe because all its methods are synchronized (i.e., multiple threads can’t access it simultaneously).
- Slightly slower than StringBuilder due to synchronization overhead.
Example:
StringBuffer sb = new StringBuffer("Hello");
sb.append(" World");
System.out.println(sb); // Output: Hello World
Explanation:
- append(" World") adds the string " World" to the existing content "Hello".
- No new object is created — the original StringBuffer is modified.
- Efficient for multi-threaded environments.
StringBuilder
- Also a mutable class like StringBuffer.
- Not thread-safe (i.e., not synchronized), which makes it faster than StringBuffer in single-threaded applications.
Example:
StringBuilder sb = new StringBuilder("Hello");
sb.append(" World");
System.out.println(sb); // Output: Hello World
Why Use Them Over String?
Suppose you're concatenating strings in a loop:
String s = "";
for (int i = 0; i < 100; i++) {
s += i;
}
This creates 100+ string objects in memory, because String is immutable.
Using StringBuilder:
StringBuilder sb = new StringBuilder();
for (int i = 0; i < 100; i++) {
sb.append(i);
}
System.out.println(sb);
Now only one object is created and modified repeatedly — this is much more memory and CPU efficient.
9. String Formatting
In Java, you can format strings using the String.format() method. This method allows you to insert variables or values into a string in a structured and readable way, similar to formatting functions in other programming languages like C (printf) or Python (format()).
Syntax:
String.format(String format, Object... args)
- format: A format string that contains format specifiers (like %s, %d, etc.)
- args: The values to be inserted at the format specifiers
Example:
String name = "Alice";
int age = 25;
String result = String.format("Name: %s, Age: %d", name, age);
System.out.println(result);
Output:
Name: Alice, Age: 25
Explanation:
- %s → placeholder for a String (in this case, name)
- %d → placeholder for an integer (in this case, age)
- String.format() replaces these placeholders with the actual values provided in the correct order.
So:
String.format("Name: %s, Age: %d", name, age);
Becomes:
"Name: Alice, Age: 25"
Common Format Specifiers:
Format Specifier | Description |
---|---|
%s | String |
%d | Integer (decimal) |
%f | Floating point (decimal) |
%.2f | Floating point with 2 decimals |
%c | Character |
%n | Newline (platform-independent) |
More Examples:
Floating point formatting:
double price = 45.6789;
System.out.println(String.format("Price: %.2f", price));
// Output: Price: 45.68
Multiple values:
String lang = "Java";
int version = 17;
System.out.println(String.format("%s version %d", lang, version));
// Output: Java version 17
Use Cases:
- Building readable and dynamic output
- Creating formatted reports or logs
- Aligning data in tabular output
10. Converting Between Strings and Other Types
a. String to int
int x = Integer.parseInt("100"); // 100
b. int to String
String s = String.valueOf(123); // "123"
11. Escape Sequences in Strings
Escape Sequence | Description |
---|---|
\n | New Line |
\t | Tab |
\" | Double Quote |
\\ | Backslash |
System.out.println("Line1\nLine2");
12. Multiline Strings (Text Blocks)
Introduced in Java 13:
String text = """
This is line one
This is line two
""";
System.out.println(text);
13. String Joining
String joining refers to the process of combining multiple strings into a single string using a delimiter (such as a comma, space, or any character). Java provides two key ways to join strings in a clean and efficient manner:
- String.join() (introduced in Java 8)
- StringJoiner class (also introduced in Java 8)
These approaches help avoid manual concatenation using + or StringBuilder, making the code more readable and concise.
a. String.join()
Definition:
The String.join() method joins multiple strings with a specified delimiter. It is a static method of the String class.
Syntax:
String.join(CharSequence delimiter, CharSequence... elements)
- delimiter – The separator to be used between elements (e.g., comma, space).
- elements – Strings to be joined.
Example:
String result = String.join(", ", "Java", "Python", "C++");
System.out.println(result); // Output: Java, Python, C++
Use Case:
Best used when you need to quickly join a known set of strings using a separator.
b. StringJoiner
Definition:
StringJoiner is a utility class in java.util that constructs a sequence of characters separated by a delimiter and optionally starting/ending with a prefix and suffix.
Syntax:
StringJoiner joiner = new StringJoiner(delimiter);
joiner.add(string1);
joiner.add(string2);
Example:
import java.util.StringJoiner;
StringJoiner sj = new StringJoiner(", ");
sj.add("Apple");
sj.add("Banana");
System.out.println(sj); // Output: Apple, Banana
Use Case:
Useful when you are dynamically adding strings in a loop or conditionally, and want full control over the formatting (including optional prefix/suffix).
Method | Class | Introduced In | Suitable When |
---|---|---|---|
String.join() | String | Java 8 | You know all strings up front |
StringJoiner | StringJoiner | Java 8 | You’re building strings dynamically |
14. Regular Expressions in Strings
Regular expressions (regex) are patterns used to match character combinations in strings. In Java, the String class provides built-in methods like matches() and replaceAll() that support regex, making pattern-based operations easy and efficient.
They are commonly used for:
- Input validation (e.g., email or phone number format)
- Replacing or removing characters
- Pattern searching and matching
a. matches()
Definition:
The matches() method checks whether the entire string matches the given regular expression.
Syntax:
boolean result = str.matches(regex);
- Returns: true if the entire string matches the pattern; otherwise false.
Example:
System.out.println("abc123".matches("[a-z]+[0-9]+")); // true
Explanation:
- [a-z]+ → One or more lowercase letters
- [0-9]+ → One or more digits
- The whole string must match the pattern completely, so "abc123" matches this pattern.
Use Case:
- Validating format of strings such as usernames, passwords, or codes.
b. replaceAll()
Definition:
The replaceAll() method replaces all occurrences of substrings that match a regular expression with a given replacement.
Syntax:
String result = str.replaceAll(regex, replacement);
Example:
System.out.println("abc123".replaceAll("[0-9]", "*")); // Output: abc***
Explanation:
- [0-9] → Matches each digit individually
- * → Replaces each digit with an asterisk
- All digits in "abc123" are replaced with *, resulting in "abc***"
Use Case:
- Masking sensitive data (like replacing numbers or emails)
- Removing unwanted characters from user input
Method | Purpose | Returns | Example Output |
---|---|---|---|
matches() | Checks if full string matches regex | true/false | "abc123".matches(...) → true |
replaceAll() | Replaces parts of string matching regex | New String | "abc123".replaceAll(...) → abc*** |
15. String Interning
Definition:
String interning is a method of storing only one copy of each distinct string value in the String Constant Pool.
When you call .intern() on a String object, it checks if a string with the same content already exists in the pool:
- If it exists, it returns the reference to that pooled object.
- If it doesn’t exist, it adds the new string to the pool and returns its reference.
Example:
String s1 = new String("Hello");
String s2 = s1.intern();
String s3 = "Hello";
System.out.println(s2 == s3); // true
Explanation:
- s1 is created using new, so it's stored in heap memory.
- s1.intern() checks the String Pool and finds that "Hello" already exists (as it's a literal), so it returns that reference.
- s2 and s3 now both point to the same pooled string, hence s2 == s3 is true.
- However, s1 == s3 would still be false, since s1 is in the heap.
Use Case:
Interning is useful for:
- Memory optimization, especially in applications that handle a large number of repetitive strings.
- Faster comparisons using == instead of .equals() if interning is applied consistently.
16. StringTokenizer (Legacy)
Definition:
StringTokenizer is a legacy class from java.util used to break a string into tokens (substrings) based on specified delimiters.
It was widely used before String.split() and Scanner were introduced. It's not recommended for new development but may still be found in legacy code.
Example:
import java.util.StringTokenizer;
StringTokenizer st = new StringTokenizer("apple,banana,grape", ",");
while (st.hasMoreTokens()) {
System.out.println(st.nextToken());
}
Output:
apple
banana
grape
Explanation:
- "apple,banana,grape" is the input string.
- "," is the delimiter that separates the tokens.
- hasMoreTokens() checks if there are more parts to process.
- nextToken() returns the next available token.
Alternative (Modern):
String[] fruits = "apple,banana,grape".split(",");
for (String fruit : fruits) {
System.out.println(fruit);
}
- This is more readable and part of the String class, making StringTokenizer mostly obsolete.
17. Performance: String vs StringBuilder vs StringBuffer
Type | Mutable | Thread-safe | Use-case |
---|---|---|---|
String | No | Yes | Read-only data |
StringBuffer | Yes | Yes | Multi-threaded mutable string ops |
StringBuilder | Yes | No | Single-threaded mutable string ops |
18. Locale-based Case Conversion
String str = "TÜRKİYE";
System.out.println(str.toLowerCase(Locale.ENGLISH)); // türkiye
System.out.println(str.toLowerCase(new Locale("tr"))); // türkiye
19. Best Practices
- Use StringBuilder for concatenation in loops
- Always use .equals() for comparing content
- Avoid unnecessary use of new String()
- Use intern() only when necessary to optimize memory
Conclusion
Strings are one of the most fundamental and widely used components in Java programming. The String class offers a powerful, immutable, and flexible way to work with textual data. Understanding how strings are created, stored, and manipulated is essential for writing efficient and reliable Java applications.
Java provides two primary ways to create strings—using literals and using the new keyword—each with its own memory implications. The concept of the String Constant Pool plays a critical role in optimizing memory usage by reusing instances of string literals. Moreover, the immutability of strings ensures thread-safety and consistency, especially when used in collections and concurrent environments.
To handle scenarios that require frequent string modifications, Java offers StringBuilder and StringBuffer—both mutable, but differing in thread safety and performance. Additionally, methods like substring(), concat(), replace(), split(), and format() provide robust tools for working with strings effectively.
Advanced features such as string interning, string formatting, and legacy utilities like StringTokenizer deepen your control and understanding of string operations. Choosing the right tool—immutable String, mutable StringBuilder, or thread-safe StringBuffer—based on the use-case is key to writing optimized and clean code.
In conclusion, mastering Java Strings not only helps in handling text-based data but also lays the foundation for mastering more complex programming constructs and solving real-world problems efficiently.