Как известно, java хранит строки в некотором кэше находящимся в памяти. Это называется интернирование. В целях экономии памяти все одинаковые строки встречающиеся в коде, ссылаются на один и тот же объект. Приведу пример кода, который не всегда может выводить true:

String s1 = "hi";
String s2 = new String("h")+"i";
System.out.println(s1.equals(s2));

Первая строка s1 известна компилятору, вторая — создается режиме работы программы, т.е. в runtime, и компилятор не смог провести оптимизацию, сделав так, чтобы обе строки ссылались на один и тот же объект.

Следовательно, все последующие строки встречающиеся в коде с содержимым «hi», созданные через new, либо явно заданные, будут ссылаться на один объект, что и первая строка. Немного поэкспериментируем и изменем объект на который ссылается первая строка s1:

Field field = s1.getClass().getDeclaredField("value");
field.setAccessible(true);

Field modifiers = Field.class.getDeclaredField("modifiers");
modifiers.setAccessible(true);
modifiers.setInt(field, field.getModifiers() & ~Modifier.FINAL);

field.set(s1, "bye".toCharArray());

Теперь все строки «hi» на самом деле ссылаются на строку с содержимым «bye» 😃