Java is a language based on references. That's just another word for pointers.
When you write:
String foo = "hello";
That is just syntax sugar for:
String foo; // [1]
foo = "hello"; // [2]
Line 1 declares a variable (a pointer) named 'foo'.
Line 2 does two things: It creates an entirely new object. foo is then updated to refer to it. It's like "hello" is the treasure, and foo is a treasure map. foo = "Hello" creates new treasure, buries it in the sand, and then draws a map to the treasure on your piece of paper; the piece of paper you labelled foo. "foo" is not the treasure, and it is incorrect to say that this string is the "foo" string. It's not - it's just treasure, and treasure has no names. There can be no maps that lead to it (which means the garbage collector will eventually dig it up and get rid of it), there can be a thousand maps that lead to it, and anything in between. In the above snippet, there is one map that leads to it. Your map. The one you called foo.
But you can share that map with others. And then they can find your treasure too.
'locals are immutable'. Yes. They are. However, your local is that foo variable, and foo is not the treasure. It's the map. It's your map. Nobody can mess with it. The only way that map is ever going to look any different, is if you write foo = someplace in your own code. No amount of passing foo around to other methods is ever going to change that map.
What the text you read is referring to, is that the only thing that's immutable is your map. The treasure the map is pointing at? Who knows. If you hand your map to others, they can't change your map, but they can copy it. They can follow it. They can dig for that treasure, and smash it to smithereens. They can't affect your map, but if you follow your map, whatever you find there? Could be completely different by now, if you shared the location of that treasure with anything else.
Now, for strings, this is a moot point: String treasures are invincible. They cannot be smashed or modified in any way. They are immutable objects, with no methods that change it, and no public (non-final) fields. But not all objects are like that. Some treasures can be changed or smashed. For example, a simple list:
List<String> x = new ArrayList<String>();
x.add("Hello");
foo(x);
System.out.println(x);
In the above code you have no idea what it prints, because you don't know what foo does. You do know that your x cannot possibly print null. null refers to the idea that you have a blank treasure map, and nobody can mess with your treasure map. But foo CAN mess with the treasure:
public void foo(List<String> x) {
x = List.of();
}
this does not do anything. This method gets a copy of your treasure map. It then creates new treasure, buries it in the sand, takes an eraser to its x treasure map (which used to hold a copy of yours), and then paints an entirely new treasure map on it, where X marks the location of the newly created treasure. This does absolutely nothing whatsoever to your map, or to the treasure that your map leads to. Your code will continue to print [Hello].
However:
public void foo(List<String> x) {
x.add("World");
}
This is quite different. This takes the copy of your treasure map that you gave it, follows it and digs down (. is java-ese for: Follow the map and dig). It then opens the chest, and puts "World" in there (technically, it puts a map to the "World" treasure in there, Strings are objects too, thus, reference-based).
If you then later follow your map and dig, you see that. Your code would print [Hello, World].
That's what the text is talking about. Avoid it by making copies, or by working with immutable objects, or by being aware of the notion that any treasures that your maps point at may have been changed by other code if you've shared your maps.