toUpperCase() returns a new String but doesn't modify the original. Since we don't assign the result to anything, the original String s remains unchanged. This demonstrates String immutability.
String literals are automatically interned in the String Pool. Both s1 and s2 point to the same object in the pool, so == returns true.
new String() always creates a new object in the heap, not in the String Pool. So s1 and s2 are different objects with different addresses, making == return false.
.equals() compares the content of the Strings, not the addresses. Both contain "Java", so it returns true. Always use .equals() for String content comparison!
substring(0, 5) returns characters from index 0 to index 4 (5 is exclusive). That's "Hello" (indices 0,1,2,3,4). The space is at index 5 and is not included.
indexOf() returns the index of the first occurrence. In "programming": p(0), r(1), o(2), g(3)... The first 'g' is at index 3.
StringBuilder is the best choice for frequent modifications in a single-threaded application. It's mutable (no new objects) and faster than StringBuffer (no synchronization overhead).
StringBuffer is thread-safe because its methods are synchronized. This means only one thread can access it at a time, preventing data corruption. However, this makes it slightly slower than StringBuilder.
First, append(" World") makes it "Hello World". Then reverse() reverses the entire string to "dlroW olleH". StringBuilder modifies in place!
Because String is immutable, each + operation creates a new String object. With 1000 iterations, that's ~1000 new objects created and discarded! Use StringBuilder instead for much better performance.