9.1 String Basics
Prerequisites: Objects, References, Method Calls
Estimated time: 25 minutes
Runtime Environment: Java 17
You are ready to continue when: You can explain immutability, value comparison, interning, and UTF-16 indexing
First Separate References from String Values
toUpperCase() does not modify a string in place, while two String objects with equal content may still produce false under ==. Both observations require the same first question: what does the variable reference, and what value does the object hold?
String behaves like a built-in type but is a class. The first boundary to clarify: variables store references, string objects store values; variables can refer to a different object, but string values don't change in place.
1. String is a Reference Type
String name = "a developer";
String alias = name;name and alias can point to the same object. The Java specification does not require you to understand programs in terms of "references being on the stack and objects being on the heap." The JVM can also perform escape analysis and other optimizations. During the initial learning phase, focus on the observable semantics:
Stringvariables can benull;- Methods access objects through references;
==checks if two references point to the same object;equalschecks if two strings contain the same sequence of characters.
2. Objects Are Immutable, Not Variables
String message = "hello";
String upper = message.toUpperCase(java.util.Locale.ROOT);
System.out.println(message);
System.out.println(upper);Output:
hello
HELLOtoUpperCase() returns the converted string, the original "hello" remains unchanged. The variable itself can still be reassigned:
message = upper;This line modifies the reference saved in message, not the original object. String methods can also reuse existing objects when the result is unchanged. Immutability guarantees that "old values will not be rewritten," but it does not guarantee that "a new object is created on every call."
String is a final class, and its public API does not include methods for in-place modification of character content. How the JDK internally stores characters is an implementation detail; application code should not rely on a specific version using char[] or byte[].
Immutable values bring several direct benefits: the same object can safely be shared; strings are suitable as Map keys; the JVM can pool literals. However, immutability does not automatically make an entire object thread-safe if it contains a String field, and it does not imply that sensitive text will be promptly cleared from memory.
3. Literals, new String and String Pool
String first = "hello";
String second = "hello";
String copied = new String("hello");
System.out.println(first == second);
System.out.println(first == copied);
System.out.println(first.equals(copied));Output:
true
false
trueString literals and the results of string constant expressions are interned. Such constants with identical content are guaranteed to reference the same pooled object. Explicit new String("hello") creates a different object, so identity comparison is false, while content comparison remains true.
This demonstration is meant to illustrate identity, not to teach you to rely on pooling. Whether text from a file, network, database, or runtime concatenation refers to the same literal object should not be part of business logic.
String fromInput = new String("admin");
if ("admin".equals(fromInput)) {
System.out.println("Content matching");
}intern() returns a canonical reference to the pooled representation of the same content, but it is rarely the correct tool for ordinary business code. First, use analytical data to prove that a large number of duplicate strings indeed cause memory issues, then evaluate the pooling strategy and lifecycle.
4. Value Comparison, Identity Comparison, and null
| Requirement | Syntax | Explanation |
|---|---|---|
| Compare text content | left.equals(right) | left cannot be null |
Both sides may be null | Objects.equals(left, right) | Two null return true |
| Compare with known constants | "admin".equals(input) | Return false when input is null |
| Compare if the same object | left == right | Use only when identity itself is meaningful |
import java.util.Objects;
String input = null;
System.out.println("admin".equals(input));
System.out.println(Objects.equals(input, null));equalsIgnoreCase uses Java's Unicode case-comparison rules. It does not provide locale-sensitive sorting or Unicode normalization. If a protocol restricts identifiers to ASCII or defines different case rules, validate its allowed character set first instead of treating this method as a universal equivalence test. User-visible text requires more explicit rules, such as specifying Locale, using Collator, or normalizing within domain boundaries.
5. length() Counts UTF-16 Code Units
Java's String uses UTF-16 semantics for addressing. Common Chinese characters typically occupy one char, while certain characters require a pair of surrogate units:
String badge = "A😀B";
System.out.println(badge.length());
System.out.println(badge.codePointCount(0, badge.length()));
System.out.printf("U+%X%n", badge.codePointAt(1));Output:
4
3
U+1F600length() returns 4 because it counts UTF-16 code units; this particular example contains three code points and displays as three characters. charAt(1) accesses one half of the surrogate pair and is not suitable for processing characters from the supplementary plane individually.
To iterate through code points, you can write:
badge.codePoints().forEach(codePoint ->
System.out.printf("U+%X ", codePoint));Code points are not equivalent to what the user perceives as "one character." Combining accents, skin-tone modifiers, and family emoji can join multiple code points into one perceived character. Java's standard library can handle code points; to truncate by user-perceived characters, use Unicode grapheme-cluster segmentation and check the Unicode version supported by the library. For example, "e\u0301" has two code points but usually appears as one accented character. It still compares unequal to the single-code-point "é" with equals unless you first apply an explicit normalization policy.
6. Compile-Time and Runtime Concatenation
String constant expressions have values determined at compile time and follow the interning rules:
String folded = "hel" + "lo";
System.out.println(folded == "hello"); // trueRuntime concatenation strategies depend on the compiler and JVM version. Modern compilers may use invokedynamic. You cannot derive a fixed number or exact allocation count of StringBuilder from a single + in the source code.
When choosing a writing style, consider the intent: small, fixed fragments are clearest with +; loops or stepwise construction of long text use StringBuilder; joining collection elements can use either String.join or Collectors.joining. Performance conclusions must be based on the target JDK and real load measurements.
7. Complete Working Example
First, prepare a temporary directory:
chapter9_semantics_root=$(mktemp -d)
readonly chapter9_semantics_root
mkdir -p "$chapter9_semantics_root/out"
cd "$chapter9_semantics_root"Save as StringSemanticsDemo.java:
import java.util.Objects;
public class StringSemanticsDemo {
public static void main(String[] args) {
String original = "hello";
String upper = original.toUpperCase(java.util.Locale.ROOT);
checkEquals("hello", original);
checkEquals("HELLO", upper);
String literal = "hello";
String sameLiteral = "hel" + "lo";
String copied = new String("hello");
check(literal == sameLiteral, "Compile-time constants should reuse literals");
check(literal != copied, "new String should create a distinct object");
check(literal.equals(copied), "The content should be equal");
String nullable = null;
check(!"admin".equals(nullable), "Constants on the left can prevent NPE");
check(Objects.equals(nullable, null), "Objects.equals should accept two null values");
String badge = "A😀B";
checkEquals(4, badge.length());
checkEquals(3, badge.codePointCount(0, badge.length()));
checkEquals(0x1F600, badge.codePointAt(1));
String combined = "e\u0301";
checkEquals(2, combined.codePointCount(0, combined.length()));
check(!combined.equals("é"), "similar appearance does not imply equal code units");
System.out.println("String semantics check passed");
System.out.println("UTF-16 code unit=" + badge.length()
+ ", code point=" + badge.codePointCount(0, badge.length()));
}
static void check(boolean condition, String message) {
if (!condition) throw new AssertionError(message);
}
static void checkEquals(Object expected, Object actual) {
if (!Objects.equals(expected, actual)) {
throw new AssertionError("expected=" + expected + ", actual=" + actual);
}
}
}Run:
javac --release 17 -encoding UTF-8 -Xlint:all -d out StringSemanticsDemo.java
java -cp out StringSemanticsDemoExpected output:
String semantics check passed
UTF-16 code unit=4, code point=3Clean up after saving the required experiment records:
cd
ls -ld -- "$chapter9_semantics_root"
rm -r -- "$chapter9_semantics_root"Professional Lookup Entry
- Java Language Specification 3.10.5: String Literals;
- Java Language Specification 15.18.1: String Concatenation Operator;
StringAPI:equals,codePoints,offsetByCodePoints,intern;- Unicode Standard Annex #29: Segmentation Rules for Handling User-Visible Characters When Processing Grapheme Clusters.
Before Continuing
Draw the process of string variable reassignment and the process of keeping string objects unchanged. Compare literals, new String, and runtime input using both identity and content checks. For A😀B, also provide the number of UTF-16 code units and code points. If the requirement states "a character as seen by the user," ask whether it should be processed as a grapheme cluster. A single + in the source code cannot be directly inferred to have a fixed number of allocations.
→ Next: String Operations and Construction
References: JLS §3.10.5: String Literals, Java 17 String API, Unicode UAX #29.