Maps in a
Bundle is harder than it looks.
[This post was written with the help of Eugenio Marletti]
⚠️ Warning — this is a long post.
Assume that you need to pass a map of values as an extra in an
Intent. This might not be a common scenario, admittedly, but it can happen. It definitely happened to Eugenio.
If you are using a
HashMap, the most common type of
Map, and you didn’t create a custom class that contains “extra” information, you’re lucky. You can put in:
And in your receiving
Activity, you’ll get your nice map back out of the
HashMap map = (HashMap) getIntent().getSerializableExtra("map");
But what if you need to pass another kind of
Map in the intent extras — say, a
TreeMap (or any custom implementation)? Well, when you retrieve it:
TreeMap map = (TreeMap) getIntent().getSerializableExtra("map");
Then you get this:
java.lang.ClassCastException: java.util.HashMap cannot be cast to java.util.TreeMap
Yep, a nice
ClassCastException, because your map has turned into… a
Note: we’ll see why we’re using
getSerializableExtra() later on, suffice for now saying it’s because all the default
Map implementations are
Serializable and there’s no narrower scope of
get*Extra() that can accept them.
Before we move on, let’s get to know the actors involved in this drama.
[tl:dr; skip to “Workaround” at the end if you just want a solution!]
Many of you will know (but maybe some don’t) that all the IPC communication in the Android framework is based upon the concept of
Binders. And hopefully many of you know that the main mechanism to allow data marshalling between those processes is based on
Parcel is an optimised, non-general purpose serialisation mechanism that Android employs for IPC. Contrary to
Serializable objects, you should never use
Parcels for any kind of persistence, as it does not provision for handling different versions of the data. Whenever you see a
Bundle, you’re dealing with a
Parcel under the hood.
Adding extras to an
Parcel. Setting arguments on a
And so on.
Parcels know how to handle a bunch of types out of the box, including native types, strings, arrays, maps, sparse arrays, parcelables and serializables.
Parcelables are the mechanism that you (should) use to write and read arbitrary data to a
Parcel, unless you really, really need to use
The advantages of
Serializable are mostly about performances, and that should be enough of a reason to prefer the former in most cases, as
Serializable comes with a certain overhead.
Down into the rabbit hole
So, let’s try to understand what makes us get a
ClassCastException. Starting from the code we are using, we can see that our call to
Intent#putExtras() resolves to the overload that takes a
String and a
Serializable. As we’ve said before, this is expected, as
Map implementations are
Serializable, and they aren’t
Parcelable. There also isn’t a
putExtras() that explicitly takes a
Step one: finding the first weak link
Let’s look at what happens in
In here, mExtras is clearly a
Bundle. So ok,
Intent delegates all the extras to a bundle, just as we expected, and calls
Bundle#putSerializable(). Let’s see what that method does:
As it turns out, this just in turn delegates to the
super implementation, which is:
Good, we got to some meat, at last.
First of all, let’s ignore
unparcel(). We can notice that
mMap is an
ArrayMap<String, Object>. This tells us we’re losing any kind of type information we might have had before — i.e., at this point, everything ends up in one big map that contains
Objects as values, no matter how strongly typed the method we used to put the value in the
Our spider sense starts to tingle…
Step two: writing the map
The really interesting stuff starts to happen when we get to actually writing the contents of the
Bundle to a
Parcel. Until then, if we check the type of our extra, we’re still getting the correct type:
Intent intent = new Intent(this, ReceiverActivity.class); intent.putExtra("map", treeMap); Serializable map = intent.getSerializableExtra("map"); Log.i("MAP TYPE", map.getClass().getSimpleName());
That prints, as we’d expect,
TreeMap to the LogCat. So the transformation must happen between the time the
Bundle gets written into the
Parcel, and when it’s read again.
If we look at how writing to a
Parcel happens, we see that the nitty gritty goes down in
Skipping all the code that is irrelevant for us, we see that the bulk of the work is performed by
Parcel#writeArrayMapInternal() (remember that
mMap is an
What this basically does is it writes every key-value pair in the
mMap sequentially as a
String (the keys are all strings here) followed by the value. The latter seems not to be considering the value type so far.
Let’s go one level deeper!
Step three: writing maps’ values
So how does
Parcel#writeValue() look like, you ask? Here it is, in its if-elseif-else glory:
Aha! Gotcha! Even though we put our
TreeMap in the bundle as a
writeValue() method does in fact catch it in its
v instanceOf Map branch, which (for obvious reasons) comes before the
else … if (v instanceOf Serializable) branch.
At this point, the smell is getting really strong.
I now wonder, are they using some totally undocumented shortcut for
Maps, that somehow turns them into
Step four: writing a
Map to the
Well, as it turns out,
writeMap() doesn’t do an awful lot in and by itself, apart from enforcing the type of
Map we’ll be handling later on:
The JavaDoc for this method is pretty clear:
“The Map keys must be String objects.”
Type erasure makes sure we’ll actually never have a runtime error here, though, even if we might be passing a
Map with keys that aren’t
Strings (again, this is totally undocumented at higher level…).
In fact, as soon as we take a look at
writeMapInternal(), this hits us:
Again, type erasure here makes all those casts pretty much worthless at runtime. The fact is that we’re relying on our old type-checking friend
writeValue() for both the keys and the values as we “unpack” the map and just dump everything in the
Parcel. And as we’ve seen,
writeValue() is perfectly able to handle non-
Maybe the documentation got out of sync with the code at some point here, but as a matter of fact, putting and retrieving a
TreeMap<Integer, Object> in a
Bundle works perfectly.
Well, with the exception of the
TreeMap becoming an
HashMap, of course.
Ok, the picture here is pretty clear by now. Maps completely lose their type when they’re written to a
Parcel, so there’s no way to recover that information when they get read back.
Step five: reading back the
As a last quick check of our theory, let’s go and check
readValue(), which is
Parcel works when writing data is, for each item it contains:
- it writes an
intthat defines the data type (one of the
- dumps the data itself (optionally including other metadata such as the data length for non-fixed size types, e.g.
- recursively repeat for nested (non-primitive) data types
Here we see that
readValue() reads that data type int, that for our
TreeMap was set to
writeValue(), and then the corresponding switch case simply calls
readHashMap() to retrieve the data itself:
Note: the C#-style opening curly brace is actually in AOSP, it’s not my fault.
You can pretty much imagine that
readMapInternal() simply repacks all map items it reads from the
Parcel into the map that we pass to it.
And yes. This is the reason why you get always a
HashMap back from a
Bundle. The same goes if you create a custom
Map that implements
Parcelable. Definitely not what we’d expect!
It’s hard to say if this is an intended effect or simply an oversight. It’s admittedly an edge case, since you have really few valid reasons to pass a
Map into an
Intent, and you should have just as little good reasons to pass
Serializables instead of
Parcelables. But the lack of documentation makes me think it might actually be an oversight rather than a design decision.
Workaround (aka tl;dr)
Ok, we’ve understood our issue in depth, and now we’ve identified the critical path that messes with us. We need to make sure our
TreeMap doesn’t get caught into the
v instanceOf Map check in
The first solution that came to my mind when talking to Eugenio was ugly but effective: wrap the map into a
Serializable container. Eugenio quickly whipped up this generic wrapper and confirmed it solves the issue.
Please note the gist is using the Android’s
@NonNull annotation to enforce its contracts. If you want to use this in pure Java modules, you can replace it with JetBrains’
@NotNull, or you could strip those annotations altogether.
Another possible workaround
Another solution could be to pre-serialize the
Map yourself into a byte array before putting it as an
Intent extra, and then retrieving it with
getByteArrayExtra(), but you’d then have to handle serialisation and deserialisation manually.
In case you masochistically wanted to opt for this other solution instead, Eugenio has provided a separate Gist with the code.
When you don’t control upstream Intents
Lastly, maybe for some reason you can’t control the
Bundle creation code — e.g., because it’s in some third-party library.
In that case, remember that many
Map implementations have a constructor that takes a
Map as input, like new
TreeMap(Map). You can use that constructor, if needed, to “change back” the
HashMap you retrieve from the Bundle into your preferred
Keep in mind that in this case any “extra” properties on that map will be lost and only the key/value pairs will be preserved.
Being an Android developer means juggling your way around pretty much everything, especially the small, seemingly insignificant things.
What can we learn from this?
When things don’t work as you’d expect them,
don’t just stare at the JavaDoc.
Because that might be outdated.
Or because the authors of the JavaDoc
didn’t know about your specific case.
The answer might be in the AOSP code.
We have the huge luxury (and curse) of having access to the AOSP code. That’s something almost unique in the mobile landscape. We can know to a certain extent exactly what goes on. And we should.
Because even though it might look like it’s WTF-land sometimes, you can only become a better developer when you get to know the inner workings of the platform you work on.
And remember: what doesn’t kill you makes you stronger. Or crazier.