Java 7 changed the structure of String
javaspecialists.eu
javaspecialists.eu
If changes to the implementation of String could cause issues for you, you really should be using a custom solution anyway. A quick and dirty option would be to write a wrapper class which uses all of your preferred String hacks in a central location, and using that in place of String throughout your code. When String's implementation changes, you can update your hacks all in one place.
Here is some relevant advice from The Pragmatic Programmer: https://pragprog.com/the-pragmatic-programmer/extracts/coinc...
That's a bit naive. Let's say you want to code something which heavily depends on a String class. You test the standard one, it's fast enough. You decide you don't need to roll your own because of it.
Then it changes and it's no longer fast and changing it at this point might be a big pain. I mean, it's hard to predict that fundamental class in a language is going to change its performance characteristics.
If you are designing something that makes heavy use of String and String operations, and you know it needs to be as optimized as possible, you should consider whether or not you need a custom String solution as part of the design process. Of course there is a hypothetical scenario where a developer might run into this even when "doing everything right", but generally it can be avoided by being aware of these potential interactions and "program deliberately."
I am not saying there will never be times when you need to "just make it work" and reasonably can't account for everything like this. The real world does not always provide the opportunity to do everything "the right way." My point is just that, when you run into this issue, it should be understood that any issues caused by implementation changes aren't really the fault of the maintainers if they haven't broken the contract/interface, and it performs well under the use cases it is designed for.
And how do you do that? By writing a test case and seeing what the standard string does. Clearly, we expect things to change over time, so Oracle didn't necessarily do something underhanded here. But Oracle did change the behavior of String enough that people need to hear about it, and perhaps reconsider whether the standard string still does what they need to do.
So, if you are implementing something that deals with a large input string, and creates a large number of substrings, you know there could potentially be a performance impact depending on the way the implementation works with substrings. The API makes no performance or memory guarantees about substrings. If your application has strict performance requirements, you know you either need to find another solution, or "proceed with caution."
(We'll ignore the fact that every article I see about high performance Java talks about using memory buffers or some kind of "off heap" scheme specifically to get around the garbage collector).
I need a random number. I see that Java has a class for this ( http://docs.oracle.com/javase/8/docs/api/java/util/Random.ht... ). It doesn't guarantee anything about performance. It doesn't even really guarantee anything about the algorithm it will use in the future, but does say that it currently uses a linear congruential algorithm. Should I (1) write my own random number generator? (2) "proceed with caution"? or (3) write a test case, see if the thing is fast enough for what I want, and pay attention to any changes to java.util.Random in future releases? Personally, I generally pick (3).
It is similar to sorting implementations. If you have a small list of items in a collection, you might not care about how fast they can be sorted as long as it is "reasonably fast," so you can just use a built-in sort() function. If you know your collection will likely be larger than most, or you need them sorted as fast as possible, you might choose to implement your own sorting function, or use a different library which specifies a given performance level.
You could just use this hypothetical, built-in sort() for now, and deal with it later if any issues arise, or you could plan ahead if it is important and future proof the code now. The former would be "proceeding with caution" while the latter would be the more maintainable, "best practices" approach when you know performance is higher priority than normal.
If you "proceed with caution" you might run into a situation where the maintainers switch from insertion sort to heap sort, because in many (most?) use cases, heap sort is faster. However, your collection is often already sorted or mostly sorted, which is a case where heap sort performs much worse than insertion sort. Now your specific application has worse performance, but other applications have improved performance. If your collection needs to be sorted at a specific performance level, it is better (but not required) for you to implement a custom sort function, or use a library that is designed for your use case. This way you can have appropriate control over the performance.
For the record, C++'s standard library has algorithmic complexity as a part of the API. See the analogous std::basic_string::substr: http://en.cppreference.com/w/cpp/string/basic_string/substr
I've been bitten by std::list::size in pre C++11 code, as it was allowed to be O(n). But they were clear about it, the fault was mine. Now, it is guaranteed to be O(1): http://en.cppreference.com/w/cpp/container/list/size
I think it's likely that the change to String.substring was actually a good one. But I also agree with others that it was rolled out poorly, and should probably not have been in a fixpack release, as from how I see it, they changed the API.
The end-game for what you're suggesting is that all Java developers who care about performance, and do heavy string manipulation in their performance critical code, should re-implement the String class. I do not find that realistic.
That means that if there's no explicit running time guarantee given, your alternatives are to either not use that API at all, or depend on the running time you can observe or guess at.
Since people write APIs to be used, the former is obviously not what's intended. So it's pretty reasonable to consider any API without an explicit running time guarantee to have an implicit one along the lines of "this won't get vastly worse" because your only other choice is not to use it at all.
If your argument is that the String implementation change constitutes "unreasonably poor performance," as in...for most normal use cases, then that makes sense. I disagree with it, but it makes sense to argue that if you think that is what's happening. That goes beyond the implementation vs. contract discussion, and maybe that is the real issue many people have. Maybe some feel like the performance hit will affect so many use cases that are common for String, that it is an unreasonable performance change.
"Returns a new string that is a substring of this string..."
Both implementations satisfy the API description so the API didn't change, the implementation did.
Optimizing your code based on the internals of a supposedly opaque data structure is a bad practice and if you get burned you only have yourself to blame.
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
In short, the previous way of keeping the same underlying character array and just updating the {offset, count} indexes has a drawback in that if the original string is large, it is prevented from being GC'd if one keeps a reference to even a single substring generated from it.
So, it's a trade-off between the original and new behaviour; the original way more or less caps the memory usage at the size of the original string, but at the expense of not being able to GC it if even a single substring exists, while the new way increases memory usage for each substring generated but does not prevent any of the strings from being GC'd.
This is why the article's code example yields such a huge difference in memory usage in Java 6 vs Java 7; it is effectively a sort of "anti-pattern" when used against the new `substring()` method. (i.e. iterating through a large string and generating lots of sub-strings)
The article I linked to, which came out in late 2012, basically had the same advice:
"If you are writing parsers and such, you can not rely any more on the implicit caching provided by String. You will need to implement a similar mechanism based on buffering and a custom implementation of CharSequence"
0. http://www.javaadvent.com/2012/12/changes-to-stringsubstring...
And it being done in a "bugfix" release? That's unacceptable.
Conversely, for people new or somewhat-new to the language, the change probably makes sense from a principle of least surprise. From the start, you're taught that Strings are immutable objects, so you probably understand that `.substring()` produces a new instance object. Not having the original memory freed when you remove all references to the original string would likely be puzzling at first.
In this respect, the Java/Oracle folks likely decided that optimizing for the "parsing/tokenization" use case (where you make lots of substrings from a large original string and thus it makes sense to use the same underlying character array) was more novel and less frequent than the use case of "just pulling a small substring from a much larger one and then discarding the large one."
You can roll your own for the standard String too, through the bootstrap class loader.
Although I don't know how many assumptions about the internals of the string class are baked into the JVM. But I think you could replace substring() relatively safely.
I wanted to show in my article how one of String's strengths is that String efficiently shares memory with substrings. But then when I looked into Java 8's String source code, I was surprised to find that this was not the case. It seemed I had been working with false assumptions for years.
Now, thanks to Heinz's article (OP), I discover that this was a change in the String class.
Long ago, a man who had been programming for 30 years told me about his theory of the half-life of programming knowledge being roughly 18 months. Today, Java has made me believe his theory a little bit more.
I think Java is sort of an exceptional case here in that the language stays relatively stable and evolves very little over time. Also they tend to not change internal details of the standard library without reason, so even lots of things programmers shouldn't rely on, such as behaviour of substring() will stay unchanged for quite long.
Now JavaScript on the other hand probably has programmign language and framework knowledge half life of considerably less than 18 months ...
http://stackoverflow.com/questions/1969442/whats-wrong-with-...
The substring change was done to address common real scenarios, as the author of the change described here: http://www.reddit.com/r/programming/comments/1qw73v/til_orac...
I can see why they made the change, but it absolutely did cause problems for real-world use-cases.
If the rate has increased significantly under Oracle's stewardship, this is something I'd like to write about in my Java newsletter.
Do you have some statistics to demonstrate that regressions have become substantially worse since Oracle took over Java? I'd happily acknowledge you as a source!
The author's observation is interesting and useful to know, but this is an edge case that is easily fixable and actually kind of the fault of user code, not the implementation or the spec.
Its not so simple to just say "I can't believe they changed the performance of xyz"... the author's article is about just one usage pattern, and there is no evidence presented to show it is very common compared to others. Developers needing specific behavior for this pattern could easily have used java.nio.CharBuffer which makes allocation/copying/access to the underlying array/etc explicit, and just happens to implement CharSequence. No reimplementation of String necessary.
In general, Java is not C or C++, and has never been billed as a platform where you could rely on the internal representation of anything unless it is specified as such. Even the same bytecode running on the same platform on the same machine can run differently if the hotspot compiler says Make It So. That of course assumes you are even using Oracle's JVM or OpenJDK... there are in fact other implementations of both the JVM and the standard library that are free to implement things however they want.
Even within the Oracle/OpenJDK sphere - there are 4 different garbage collectors which all could affect this pattern differently. There is even a new String dedup feature that would have an impact on the internal behavior of Strings:
https://blog.codecentric.de/en/2014/08/string-deduplication-...
tl;dr - don't assume and pick the right tool for the job
I think it is an excellent idea, but in practice too awkward to implement seamlessly.
https://reddit.com/r/programming/comments/1qw73v/til_oracle_...
https://www.reddit.com/r/programming/comments/1qw73v/til_ora...
seems to bring up persuasive arguments on the other side.
You can't just deprecate substring() which is a function that is used in what 90% of the millions of Java applications around the world. All because life is difficult for a tiny few edge cases (and for which workarounds exist i.e. checking Java version numbers). Sure it wasn't great that it was done during a bug fix but we need to be mindful that Java releases do span multiple years.
this is fairly easy to spot with a profiler though
The description in the Java 7 API for substring is "Returns a new string that is a substring of this string." That does not suggest any sharing with the parent string; it says "new string".
Anyway, I have moderately sized strings and am trying to analyze parts of them. And I don't want to make copies for the sake of analysis. This has to be quite common as well.
I wrote Map<String,String> which does this transparently, and it consumes about 5x less memory compared to HashMap<String,String>.
[1] https://bugs.openjdk.java.net/browse/JDK-8054307 [2] http://shipilev.net/blog/2015/black-magic-method-dispatch/
In theory they could have implemented both. Substring optimization for small but not larger strings.
For example, back when I was at a JVM company, we had two string implementations, one for ASCII strings and one for UCS-2 strings; the JVM uses a lot of ASCII strings, and frequently you could figure this out at code load time. Having an implementation based on an array of bytes saved quite a lot of space, and was completely transparent.
For large strings, it would work exactly like it did before with fast views. And since the existing code already knew about memory sharing this wouldn't be a new problem for any code.
For small strings, it would now be faster and have smaller memory footprint. Existing programs would be faster and better performing.
I think the main reason this is a bad idea is that String is final. The way to do this properly is with two classes, SmallString and LargeString, and then using hotspot to optimize the program for how they are used. But that is not possible without changing the API.
There's also a catch : if the hashcode of the string is 0, the hashcode will be recalculated every time (since the code assumes it has not been cached yet).
At least that part should be easy to fix by defining a hash function that returns numbers != 0 - even the article says they did it for JVM7 but it's gone in JVM8 - with no explanation ?
It was made to decrease the number of hash value collisions in large data structures (hash maps and such). Its replacement is described here: http://openjdk.java.net/jeps/180 . Given that most Strings never end up in a large collection, allocating an extra 4 bytes for every one of them was a waste.
This post has been edited once for factual correctness.
You can easily find good & bad cases for all of these string implementations. They all have tradeoffs.
However other data structures (notably the ones in guava), don't do this caching - which does hurt performance for any sort of heavy writes and/or complex object.