How I fucked up with enum's hashCode
A short story about a newbie’s mistake I’ve made recently.
I was implementing an HTTP caching for one of our APIs, based on "ETag" and "If-None-Match" headers.
The entity I was trying to cache — Feed
— was simple:
data class Feed(
val deviceId: String,
val platform: Platform,
val items: List<FeedItem>
)
enum class Platform {
IOS, ANDROID
}
data class FeedItem(
val id: Int
// Integers and strings
)
I decided to use hashCode()
for ETags.
I knew that both Integer
and String
have a well-defined and stable hash code: Integer#hashCode()
is equal to the primitive value represented by that Integer
object, and String#hashCode()
uses a known formula.
List#hashCode()
is stable as well as long as it’s elements have stable hash codes.
Of course not, we’re not happy!
I forgot to check the details about Enum#hashCode()
method.
And here be dragons: enum’s hash code is not stable across different JVM instances.
I assumed wrongly that it is calculated as value’s ordinal()
, which is Integer
, which, we know, has a stable hash code, equal to itself.
But it’s not.
Enum’s hash code is defined in Java (Kotlin is virtually the same) as:
public final int hashCode() {
return super.hashCode();
}
Here, super
is an Object
, and, as we know, Object
's hash code is stable unless the object itself is changed.
It’s generally the case with enums: there is only one instance of each of enum’s values per JVM and they are stable.
But it’s important to emphasize, that it applies to a single JVM.
Enum’s on different JVMs will have different hash codes.
And that’s how our ETags were broken: every time the request landed on a new server, the ETag was different, even if the objects stored in the database were the same. The situation was worsened by the fact that we were using short-lived JVMs (AWS Lambda functions), so the caching was completely broken for a while until I tracked down this issue.
RTFM!