Why is Kotlin's cast more weakly typed than Java's?

With Java the following fails to compile:

    String s = "abc"; 
    Integer i = (Integer) s;  // fails to compile - "Inconvertable types" error

But Kotlin’s cast does not cause a compile error when used in this way:

    val s: String = "abc" 
    val i: Int = s as Int // compiler is happy with this but guaranteed to fail at runtime

Java’s casting mechanism seems superior - more errors are caught at runtime. I’m curious why Kotlin weakened the type of the “as” operator here?

3 Likes

It seems like it shows a warning, but I would also prefer compilation errors in such oblivious cases.

1 Like

In general type casting is one of the most dangerous operations in any programming language and in real life nobody will cast String to Integer in Java.
When we’ll change slightly Java example:

String s = "123";
Integer i = (Integer) s;

Compiler will still not compile it just simply because String is different type than Integer. Even though for human this is obvious that content of this string is an integer number, for compiler this doesn’t really matter.

But even in Java compiler is not that smart all the time. Here’s more realistic example (though it is an example of bad code practice):

public class Main {
    enum ResultType {
        STRING_ABC,
        STRING_123,
        INTEGER
    }

    static Object getResult(ResultType type) {
        return switch (type) {
            case STRING_ABC -> "abc";
            case STRING_123 -> "123";
            case INTEGER -> 123;
        };
    }

    public static void main(String[] args) {
        Object result1 = getResult(ResultType.STRING_ABC);
        Integer i1 = (Integer) result1; // class cast exception

        Object result2 = getResult(ResultType.STRING_123);
        Integer i2 = (Integer) result2; // class cast exception

        Object result3 = getResult(ResultType.INTEGER);
        Integer i3 = (Integer) result3; // Ok
    }
}

Imagine we have some external service, which returns a result of Object type, which maybe of several types. This code will compile without errors, but will fail in 2 out of 3 cases. Type case is unsafe operation and should be used with caution.

In Kotlin as operator is “Unsafe” cast operator. It will try to cast object into denoted type but this cast is not guaranteed and it will throw an exception in case of failure.

In Java is instanceof operator which can be used for safe type cast, in Kotlin there is is operator.

1 Like

Hi luhtonen. I may not have expressed myself clearly.

This isn’t about parsing integers from strings or about runtime class cast exceptions - it’s about compile time type safety.

More abstractly, in Java the following

    X x = ...;
    Y y = (Y) x;

will only be accepted by the compiler if Y is the same type as X or a sub-class of X (or visa versa although “casting” from a sub-class to a super-class does nothing). This makes sense because if Y is NOT a sub-class of X, then obviously you know at compile time that the cast will always fail at runtime regardless of the actual type of x.

The Kotlin equivalent:

    var x: X = ...
    var y = x as Y

is not flagged as a compile time error even if X and Y have no sub/super class relationship. By not catching this at compile time, makes Kotlin less safe than Java in the sense that more errors are caught at runtime rather than compile time.

What’s weird is that Kotlin’s type system is easily capable of capturing this constraint. For example I can define a better cast operation:

fun <X, Y: X>stricterCast(x: X) = x as Y

fun bar() {
   foo(12.34d)
}

fun foo(a: Number) {
    val b = stricterCast<Number, Double>(a)  // compiles and succeeds at runtime
    val c = stricterCast<Number, Int>(a)     // compiles but throws runtime class cast exception
    val d = stricterCast<Number, String>(a)  // does not compile
}

I’m curious why Kotlin’s behaviour is a regression in terms of (static) type safety compared to Java’s in this regard.

Hi @darag,

First I’d like to point out that I’m no expert, but I can offer my two cents as to why I think this might be preferable in a language like Kotlin.

TL;DR: I expect it’s because of smart casting and a preference for as?

Whereas in Java, there is only one method of casting (Class) myObj, in Kotlin there are explicitly two ways myObj as Class and myObj as? Class.

Whilst it might be intuitive to imagine that the “unsafe cast” as which is regularly treated as equivalent to the previous cast in Java might be expected to be used in the same ways, it might actually be used in very different ways.

Whereas in Java you may well have code that you do not want to throw an InvalidCastException that performs an unsafe cast, in Kotlin, if you are using as instead of as? you are almost invariably choosing to accept and handle any TypeCastException that might be thrown either by accepting responsibility for any that occur at runtime or because they might be explicitly preferred in your code for whatever reason.

Further, if Kotlin were to function as you suggest, you might get some irregularities in code due to “smart casting” objects where objects are of a stricter type than you expect, so for example if you were writing a function that purported to take an “Any” and you decided that the best way would be to handle all types the same way by performing a cast and handling any TypeCastException

@Throws(TypeCastException::Class)
fun castingExperiment(a: Any) {
    ...
    return a as TargetClass
    ...
    return a as TargetClass
}

Now, if the compiler initially allowed one of these, but then either

  • code was introduced that allowed the compiler to judge that a at the second return was never of TargetClass or
  • updates to Kotlin led to improvements to smart casting leading to as TargetClass not compiling

Then you might end up having to change that code to throw a TypeCastException. And importantly, you might not throw it yourself in the same way as the previous call to a as TargetClass might’ve been doing — leading to slightly different behaviour further down the line.

Also, it might be worth noting that Java code that has something like

if (a instanceof NotTargetClass) {
    ...
    (TargetClass) a
}

Without as would not be easily portable into Kotlin because of smart casting as it would have to work out to convert it to:

if (a is NotTargetClass) {
    ...
    throw TypeCastException(...)
}

which might both be a large ask on the conversion process and not the code that the programmer was expecting or wanting.

Those are just my thoughts, I’d love to hear what other people might think or if they think that Kotlin would be better with refusing to compile in the case of any such cases.

Thanks.

3 Likes

Thanks josephroffey - lots to think about in what you wrote. I suspect you’re on the right track that it has something to do with smart casting or some other Kotlin feature (but I will need to think about it).

Your message provoked me to try the experiment with the “is” operator:

sealed class Foo

fun bar(x: Foo) {
    val b = x is String  // compilation error - "incompatible types"
    val c = x as String  // no compilation error
}

This for me is even odder - if I had been asked to guess I would have assumed that “is” would be more loosely typed than “as” but it seems the other way around. While in Java, both “(String) x” and “x instanceof String” in the above context result in “incompatible type” compile errors.

2 Likes

It is hard to track why it is exactly so, but we can change it if we’ll find no compelling reason to keep it this way. I’ve created the corresponding YT issue: https://youtrack.jetbrains.com/issue/KT-44569

7 Likes