Sitelet https://howtodoinjava.com/java/string/java-string-intern-method-example/#comment-97072

Java String intern() Method: How the String Pool Works

The String intern() method returns the pooled copy of a string, either an equal string already in the pool or the string itself after adding it. Literals, constants, both cases of intern(), a memory use case and alternatives on Java 25.

Java String

The Java String intern() method returns the pooled copy of a string, which is either the equal string that the JVM’s string pool already holds or, when there is none, this string after intern() adds it to the pool. So two strings with the same text give back the same object after intern().

We use intern() to save memory when an app keeps many copies of the same text in memory, for example country codes or status values read from a file. Java interns all string literals for us, so intern() matters only for strings created at runtime.

The following example compares a literal, a new String object and the interned object, with the result of each line as a comment.

String literal = "apple";                       // from the string pool
String object = new String("apple");            // a new object on the heap
String interned = object.intern();              // the pooled "apple"

boolean sameObject = literal == object;         // false
boolean samePooled = literal == interned;       // true
boolean sameText = literal.equals(object);      // true

Notice that object and literal hold the same text but are two different objects, whereas object.intern() returns the pooled object that literal points to. In the next sections, we look at the string pool and the two cases of intern(). After that, we see when interning saves memory in a real app and which alternatives cost less.

1. What Is the String Pool?

The string pool is a table inside the JVM that holds one String object for each distinct text that was interned. The pool works because strings are immutable, so many variables can share one String object without any risk that one of them changes the text.

Java adds strings to the pool in two ways.

  • The JVM interns every string literal, such as “apple”, and every string-valued constant expression, such as “app” + “le”. The String Javadoc and JLS 3.10.5 state this rule.
  • Our code interns any other string by calling intern() on it.

A string created at runtime, such as the result of new String(“apple”) or a line read from a file, is a normal heap object and is not in the pool.

Heap memory with the string pool inside it. The variable literal points to the pooled "apple" object. The variable object points to a separate "apple" object on the heap outside the pool. The call object.intern() finds the equal text in the pool, so the variable interned points to the same pooled object as literal.
A literal and intern() both lead to the one pooled object, whereas new String() always creates a separate object.

Since Java 7, the pool is part of the normal Java heap and no longer of the permanent generation, as the JDK 7 release notes describe. So interned strings count toward the heap size, and the garbage collector removes pooled strings that no variable references anymore.

1.1. Literals and Constant Expressions

The compiler computes a constant expression, such as “app” + “le”, at compile time and stores the result as one literal. The same is true for static final String constants, because a final variable with a constant value is itself a constant. Text that is joined at runtime creates a new object, and only intern() brings it back to the pooled one.

static final String PREFIX = "app";

String a = "apple";
String b = "apple";
boolean sameLiteral = a == b;                   // true (one pooled object)

String joined = "app" + "le";                   // constant expression
boolean sameJoined = a == joined;               // true

String constant = PREFIX + "le";                // PREFIX is a static final String
boolean sameConstant = a == constant;           // true

String part = "app";
String runtime = part + "le";                   // built at runtime
boolean sameRuntime = a == runtime;             // false
boolean afterIntern = a == runtime.intern();    // true

The runtime line shows why comparing strings with == is a bug. The result depends on how the string was created, so we compare text with equals(), which compares the characters.

2. How String.intern() Works

The method intern() looks up the text in the pool with equals(). The result depends on whether the pool already holds an equal string.

  • When the pool holds an equal string, intern() returns the pooled object, and the object we called it on stays a separate object.
  • When the pool holds no equal string, intern() adds this object to the pool and returns a reference to the same object.
Decision flow of s.intern(). The method checks with equals() whether the pool contains a string equal to s. If yes, it returns the pooled string, s stays a separate object and s == s.intern() is false for a new String object such as new String("cherry"). If no, it adds s itself to the pool and returns s, so s == s.intern() is true, as for the built string "banana42".
intern() either returns the string that is already in the pool or puts this object into the pool.

The following example shows both cases. The text “banana42” is built with a StringBuilder, so it is not in the pool before the call. The text “cherry” is a literal in the same class, so the pool already holds it.

String built = new StringBuilder("ban").append("ana42").toString();  // "banana42", not in the pool yet
String pooled = built.intern();
boolean same = built == pooled;                 // true (built itself is now in the pool)

String copy = new String("cherry");             // "cherry" literal is already in the pool
String fromPool = copy.intern();
boolean sameCopy = copy == fromPool;            // false (the pool returns the literal)

For any two strings s and t, s.intern() == t.intern() is true if and only if s.equals(t) is true. This rule from the Javadoc is the reason why interned strings can be compared with ==.

Notice the first case. Before Java 7, intern() copied the text into the permanent generation and returned the copy, so built == pooled was false. Many older articles still describe that copy, but current JVMs add the object itself.

3. When to Use intern()

Say an order import reads one million rows from a CSV file, and each row has a country column with one of three values, “IN”, “US” or “DE”. The CSV parser creates a new String for every value it reads, so the app keeps one million country strings in memory, although only three different texts exist. After intern(), all rows share three objects, and the garbage collector can free the rest.

The following example simulates the import. The new String() call stands for the text a parser reads, and an IdentityHashMap, which compares keys with ==, counts the distinct objects.

String[] codes = {"IN", "US", "DE"};
List<String> countries = new ArrayList<>();
for (int i = 0; i < 1_000_000; i++) {
  countries.add(new String(codes[i % codes.length]));
}

Set<String> before = Collections.newSetFromMap(new IdentityHashMap<>());
before.addAll(countries);
int beforeCount = before.size();                // 1000000

countries.replaceAll(String::intern);
Set<String> after = Collections.newSetFromMap(new IdentityHashMap<>());
after.addAll(countries);
int afterCount = after.size();                  // 3

Interning fits values with few distinct texts and many copies, such as country codes, currency codes, status names or the keys of parsed JSON objects. It does not fit unique values such as user names or order IDs, because every call adds a new entry to the pool and saves nothing.

3.1. Costs and Alternatives

The pool is a hash table inside the JVM. Every intern() call computes the hash of the text and looks it up in that table, so the call costs more than a plain field access. On Java 25, the table starts with 65536 buckets (the default of the JVM option -XX:StringTableSize), and the java command option -XX:+PrintStringTableStatistics prints the number of pooled strings when the JVM exits.

A ConcurrentHashMap that maps each text to its first instance gives the same deduplication under our control. The map belongs to our code, so we can limit its size or clear it after an import, and the JVM pool stays small.

Map<String, String> canonical = new ConcurrentHashMap<>();
String first = canonical.computeIfAbsent(new String("IN"), k -> k);
String second = canonical.computeIfAbsent(new String("IN"), k -> k);
boolean same = first == second;                 // true
int size = canonical.size();                    // 1

The garbage collector can also deduplicate strings when we start the JVM with -XX:+UseStringDeduplication, which is off by default. The option came with G1 in JEP 192, and since Java 18 the Serial, Parallel and Z collectors support it too. The collector shares the character arrays of equal strings in the background, but the String objects stay separate, so == still returns false. We need no code change, which makes the option a good first step when a heap dump shows many duplicate strings.

4. String intern() FAQs

4.1. Should We Compare Strings With == After intern()?

Only when we know that both strings are interned. A single string that was not interned makes == return false for equal text, and the compiler cannot warn us. Code that compares text with equals() is correct in every case, so we keep equals() and use intern() only to save memory.

4.2. Can intern() Cause an OutOfMemoryError?

Not because of the pool itself. Before Java 7, the pool was in the permanent generation, a small memory area of fixed size, so interning many strings could fill it. Since Java 7, the pool is in the heap, and the garbage collector removes pooled strings that nothing references. A program that interns millions of unique strings and keeps them in a collection can still fill the heap, as any other program that keeps too many objects can.

4.3. Is intern() Thread-Safe?

Yes. The JVM synchronizes access to the pool, so two threads that intern equal text at the same time get back the same object.

5. Conclusion

The String.intern() method returns the one pooled object for a given text. When the pool already holds an equal string, we get that object back; otherwise, the string we called intern() on becomes the pooled object. Java interns literals and constant expressions for us, so intern() matters only for strings built at runtime.

Interning saves memory when an app holds many copies of a few texts, such as codes read from a file. For unique values, interning only adds lookups. A ConcurrentHashMap gives the same deduplication under our control, and the JVM option for string deduplication needs no code change at all. In both cases, we compare text with equals(), not ==.

6. References

Happy Learning !!

Source Code on Github

Leave a Comment

  1. Is it really true that “When we create string with new keyword, two objects will be created i.e. one in the Heap Area and another in the String constant pool.”
    There would seem to be little need for an exposed intern() method if ‘new’ also creates a pooled object.

  2. But we can get a reference from String pool by using String literal.. we can so String str = “hello world”; this will return reference from pool. Why do we even need intern() method to get literal String ?

Comments are closed.

About Us

HowToDoInJava provides tutorials and how-to guides on Java and related technologies.

It also shares the best practices, algorithms & solutions and frequently asked interview questions.