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

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

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

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