Pol
Pol

Reputation: 181

UnsupportedOperationException when adding to a map in kotlin

I have a crash, that only happened once in my app, due to an UnsupportedOperationException when I try to add an item to a map. For the stacktrace it seems that an AbstractMap is instantiated instead of a MutableMap. The code is in kotlin:

val productMap: MutableMap<ProductModel, Int> =
            binding.myView.getProductMap() as? MutableMap<ProductModel, Int>
                ?: mutableMapOf()

            presenter.getProduct()?.let {
                productMap.put(it, 0)
            }

Could it be that kotlin/java did something weird behind curtains or is there anything I am missing?

The stacktrace:

Fatal Exception: java.lang.UnsupportedOperationException
       at java.util.AbstractMap.put(AbstractMap.java:218)
       at com.package.MyView.method2(MyView.java:108)
       at com.package.MyParentView.method1(MyParentView.java:1278)

Upvotes: 0

Views: 2166

Answers (2)

gidds
gidds

Reputation: 18577

I think the issue here is that MutableMap is a mapped type: the Kotlin compiler knows about the difference between Map and MutableMap, but they both compile down to java.util.Map in the Java* bytecode. That's because the underlying Java classes don't distinguish mutable from immutable collections; they use the same interface for both, and immutable implementations simply throw UnsupportedOperationException.

So in this case, the as? safe cast doesn't do what you want: it's effectively checking only whether the object is a Map, not whether it's mutable. (I don't know Android, but if getProductMap() is defined to return a Map, then that boils down to a simple null check.)

There are several approaches to fixing this, depending on your needs, e.g.:

  • If you happen to know the concrete, mutable type you expect (such as ArrayList), then you could cast to that. Since ArrayLists are always mutable, the put() should then be safe; but it would do nothing any other type of map was provided.

  • You could trap the UnsupportedOperationException in a trycatch block. This would avoid the error, but do nothing if an immutable map was provided.

  • You could create a mutable map from the given map, using toMutableMap(). This would always update the map — but it might be a private map not used by the view.


(* The stack trace shows that this question is about Kotlin/JVM.  You might get different results on other platforms.)

Upvotes: 2

broot
broot

Reputation: 28362

I don't know if this is what happens here, but we should not really cast read-only types to mutable like this. If the return type of getProductMap() is Map, not MutableMap then this is probably for a reason and we should not try to use it as it is mutable.

For example, buildList(): List utility actually returns a list that implements MutableList, but is in fact read-only and throws exceptions when we try to modify it. Similarly, collections returned from utils like Collections.unmodifiableMap() can be cast to mutable.

I think what you really need to do here is to create a mutable copy of the data in the map:

binding.myView.getProductMap().toMutableMap()

Or, if getProductMap() returns nullable:

binding.myView.getProductMap()?.toMutableMap() ?: mutableMapOf()

As a side note, your code seems strange to me. It uses the contents of the source map if it is mutable, but it ignores its contents if it is not.

Upvotes: 1

Related Questions