In reality, performance often matters. Then there's no way to avoid needing to understand what the implementation does. The java6 implementation was a big, somewhat well-known gotcha. Read in mb of data in a String, substring a couple kb out, and tada! you're still holding all those mb.
You inescapably either:
1 - substring introduces a new string, creating lots of garbage since it's an incredibly common operation,
or
2 - substring saves garbage in many cases by having substring return sub-chunks of the full char array, but you will be unable to gc the full string as long as any substring remains reachable