I'd like to better understand java/kotlin interop relative to inline value classes. I have some example code below with my attempts for interop, a few notes:
import kotlin.Result
Result.success
myResult.isSuccess
I created some workaround below. They work... but seem to have their own edge-cases and oddities. I have a few questions:
"boxing" seems to be necessary to preserve type information. Is a way to enforce boxing other than ?
(which has the negative of triggering unnecessary null checking). I tried something like fun <T, R: Result<T>> success(value: T): R = Result.success(value as T) as R
-- it compiled but threw a java.lang.NoSuchMethodError
error at runtime (!!)
is there some other way that this can be done? Are there any modules/libraries that help with kotlin/java inline value interop?
it seems to be a bug that the java <-> kotlin interop here causes the isFailure
methods to loose the success/failure status when called from Java (without explicit ?
boxing) but I'd appreciate any thoughts!
Boxing as defined here, from which I can see that you can box by using generic and interfaces, but I don't see how I can use those to force a box AND perform logic and type checking.
Kotlin:
@file:JvmName("Inline")
package play
// Note: doesn't compile without `?`:
// incompatible types: no instance(s) of type variable(s) T exist so that
// Object conforms to Result<String>
@JvmName("success") fun <T> success(value: T): Result<T>?
= Result.success(value)
@JvmName("failure") fun <T> failure(t: Throwable): Result<T>?
= Result.failure(t)
// Note: java compiles without `?` BUT "failures" report isSuccess=true (!?!)
@JvmName("isSuccess") fun <T> isSuccess(r: Result<T>?): Boolean
= r!!.isSuccess
@JvmName("isFailure") fun <T> isFailure(r: Result<T>?): Boolean
= r!!.isFailure
java test code:
package play;
import static org.junit.Assert.assertTrue;
import kotlin.Result;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.JUnit4;
@RunWith(JUnit4.class)
public class InlineTest {
@Test
public void testResult() {
Result<String> rString = Inline.success("hi");
assertTrue(Inline.isSuccess(rString));
assertSuccess(rString);
Result<Integer> rInt = Inline.success(42);
assertTrue(Inline.isSuccess(rInt));
Result<Throwable> fail = Inline.failure(new Exception("test"));
// Note: fails unless isFailure using `?` type
assertTrue(Inline.isFailure(fail));
}
// just proves you can pass them to java functions
<T>void assertSuccess(Result<T> r) {
assertTrue(Inline.isSuccess(r));
}
}
You cannot use their constructor methods in java, i.e.
Result.success
Normally, methods of the companion object of a value class
can be accessed from Java as usual. However, Result.success
and Result.failure
are marked @InlineOnly
.
You cannot use their methods once constructed in java, i.e.
myResult.isSuccess
This is expected. Properties and methods of inline classes are compiled to static methods taking a parameter of the wrapped type, to avoid boxing. They also have a mangled name, so you cannot access them from Java.
The NoSuchMethodError
is a bug of either the Java or Kotlin compiler (I'm not sure). It should either not compile your code, or compile it without throwing a NoSuchMethodError
at runtime. This is possibly related to KT-26131.
What happens here is that the success
you wrote gets compiled to
Object success(Object value)
instead of
Result success(Object value)
The Java code looks for a method that matches the latter, which doesn't exist.
isFailure
always returns false when using the unboxed Result<T>
, because isFailure
determines whether the result is a failure by checking if it is an instance of an internal Failure
type. Obviously, the instance of Result
you passed in from Java is not Failure
. If you use the boxed Result<T>?
, then an extra unbox operation will be generated.
Some Java pseudocode to illustrate the difference:
// for Result<T>
public static boolean isFailure(Object r) {
return r instanceof Failure;
}
// for Result<T>?
public static boolean isFailure(Result r) {
return r.unbox() instanceof Failure;
}
If you have control over the inline class, an alternative way to use it in Java is to have it implement an interface, like my answer demonstrates here.
Otherwise, I'd just write my own non-inline wrapper, that wraps the inline class.