Smart-cast problem


#1

The following code compiles with no warnings but throws a java.lang.UnsupportedOperationException

fun main(args: Array<String>) {
    val alist = listOf("a", "b")
    foo(alist)
}

fun foo(alist: List<String>) {
    if (alist is MutableList) {
        alist.add("s")
    }
}

Shouldn’t the compiler generate a warning/error for this case, since the smart-cast (eventually) fails?

Thanks


#2

No, because you are using the “unsafe” cast operator which will throw an exception at runtime if the cast fails.
So if there is a chance of an exception being thrown you should use the “safe” (nullable) cast operator


#3

Thanks for your answer @SackCastellon.
The exception is thrown on add method, not the cast.
If I change the code to safe cast operator

fun foo(alist: List<String>) {
    (alist as? MutableList)?.add("s")
}

I still get the same error


#4

Oh, sorry I misread the is as as in the first post, so my previous answer doesn’t make much sense :sweat:


#5

listOf(...) probably produces an unmodifiable list in Java. An unmodifiable list implements java.util.List, which is the Java equivalent of MutableList. So you can cast it, but as it is an unmodifiable list, it will not allow addition.

There are lots of instances where an implementation violates the contract of the interfaces it implements.


#6

@jstuyts is right, listOf(...) returns an java.util.Arrays.ArrayList (not to be confused with java.util.ArrayList) which implements java.util.List and is unmodifiable


#7

Hmm, I would expect subclasses of MutableList to be also mutable by contract as Kotlin makes the distinction between mutable and immutable collections. I am a bit surprised that java.util.List is equivalent to MutableList as @jstuyts says. I guess that this LSP violation comes from java interop, right?

So, is there a “safe” way to check if a list is actually mutable?


#8

In this case, yes. But LSP violations are common.

Not that I know of. No lie detector for classes :wink:

But your approach is probably wrong. If you want a MutableList, change the parameter to it. If you cannot change the parameter, make a copy of the list inside the method, and add an element to the copy. Yes, the latter option loses the side effect, but that is probably for the best.


#9

@jstuyts thanks for your answer. Obviously, the code of the first more like a puzzler than an approach I would follow. I always tend to prefer immutability, so I would probably return a new list in the foo function, as you recommended.

Regarding the first approach you suggested (changing the parameter type to MutableList), I believe it is still problematic as cast alist as MutableList<String> will succeed, but I think that you probably mean that the caller side should construct the mutable list using something like alist.toMutableList(), right?


#10

Yes, if a method expects a MutableList, it is the responsibility of the caller to ensure that the list that is passed to the method is actually mutable.

Kotlin is more expressive than Java here. In Kotlin you can clearly state that a list that you can’t modify yourself is good enough: List. If you do modify the list, the client knows that too: MutableList.


#11

You can modify java.util.Arrays.asList() return values as long as the requested modification can apply to the array. As such, modifications that change the size of the list are not supported. On the contrary, set(index, element) is. Same goes for listIterator().set(element) method.