Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Idiomatic Interop

Kevin Most
November 06, 2017

Idiomatic Interop

Kevin Most

November 06, 2017
Tweet

More Decks by Kevin Most

Other Decks in Technology

Transcript

  1. Doesn't Kotlin already have 100% interop? • Yes, but the

    interop can either be pleasant or clumsy • And some features from Kotlin won't work in Java
  2. Who should be thinking about this? • Java library developers

    • Kotlin library developers • Anyone working in a mixed codebase
  3. Don't get too carried away data class User @JvmOverloads constructor(

    val id: String? = null, val name: String? = null, val username: String? = null, val gender: String? = null, val points: Int = 0 )
  4. @JvmStatic • Only for use in companion and named objects

    • On fun and val: • Generates a static method in the bytecode that delegates through to that function/property • On var: • Also generates a static setter that delegates through
  5. @Throws interface Repository<T> { /** * @throws IOException if can't

    save */ fun save(obj: T) }D class UserRepository implements Repository<User> { @Override public void save(User obj) { throw new IOException(""); } }
  6. @Throws interface Repository<T> { /** * @throws IOException if can't

    save */ fun save(obj: T) }D class UserRepository implements Repository<User> {A @OverrideB public void save(User obj) {C throw new IOException("");D }E }F
  7. @Throws interface Repository<T> { /** * @throws IOException if can't

    save */ fun save(obj: T) }D class UserRepository implements Repository<User> {A @OverrideB public void save(User obj) throws IOException {C throw new IOException("");D }E }F
  8. @Throws interface Repository<T> { /** * @throws IOException if can't

    save */ fun save(obj: T) }D class UserRepository implements Repository<User> {A @OverrideB public void save(User obj) throws IOException {C throw new IOException("");D }E }F
  9. @Throws interface Repository<T> { /** * @throws IOException if can't

    save */ fun save(obj: T) }D class UserRepository implements Repository<User> {A @OverrideB public void save(User obj) throws IOException {C throw new IOException("");D }E }F
  10. @Throws interface Repository<T> { /** * @throws IOException if can't

    save */ @Throws(IOException::class) fun save(obj: T) }D class UserRepository implements Repository<User> {A @OverrideB public void save(User obj) throws IOException {C throw new IOException("");D }E }F
  11. Extension functions • You have a huge Java codebase with

    many Utils classes • You add Kotlin • You still have all these Utils • Awesome new Kotlin code has to call into old Java utils
  12. Extension functions • Can we convert our Utils classes to

    Kotlin extensions • While preserving their signatures in Java so existing call-sites can stay as they are?
  13. Extension functions class UserUtils { static boolean hasName(User user) {}

    static String getDisplayableName(User user) {} static boolean isAnonymous(User user) {} static boolean isFriendsWith(User subject, User other) {} } UserUtils.isAnonymous(aUser); UserUtils.hasName(aUser); UserUtils.isAnonymous(aUser) UserUtils.hasName(aUser)
  14. Extension functions val User.hasName: Boolean get() {} val User.displayableName: String

    get() {} val User.isAnonymous: Boolean get() {} fun User.isFriendsWith(other: User): Boolean {} UserUtils.isAnonymous(aUser); UserUtils.hasName(aUser); UserUtils.isAnonymous(aUser) UserUtils.hasName(aUser)
  15. Extension functions val User.hasName: Boolean get() {} val User.displayableName: String

    get() {} val User.isAnonymous: Boolean get() {} fun User.isFriendsWith(other: User): Boolean {} UserUtils.isAnonymous(aUser); UserUtils.hasName(aUser); UserUtils.isAnonymous(aUser) UserUtils.hasName(aUser)
  16. Extension functions val User.hasName: Boolean get() {} val User.displayableName: String

    get() {} val User.isAnonymous: Boolean get() {} fun User.isFriendsWith(other: User): Boolean {} UserUtils.isAnonymous(aUser); UserUtils.hasName(aUser); aUser.isAnonymous aUser.hasName
  17. Extension functions UserUtils.isAnonymous(aUser); UserUtils.hasName(aUser); aUser.isAnonymous aUser.hasName @file:JvmName("UserUtils") // default is

    UserUtilsKt val User.hasName: Boolean get() {} val User.displayableName: String get() {} val User.isAnonymous: Boolean get() {} fun User.isFriendsWith(other: User): Boolean {}
  18. Extension functions UserUtils.isAnonymous(aUser); UserUtils.hasName(aUser); aUser.isAnonymous aUser.hasName @file:JvmName("UserUtils") // default is

    UserUtilsKt val User.hasName: Boolean get() {} val User.displayableName: String get() {} val User.isAnonymous: Boolean get() {} fun User.isFriendsWith(other: User): Boolean {}
  19. Extension functions UserUtils.isAnonymous(aUser); UserUtils.hasName(aUser); aUser.isAnonymous aUser.hasName @file:JvmName("UserUtils") // default is

    UserUtilsKt val User.hasName: Boolean @JvmName("hasName") get() {} val User.displayableName: String get() {} val User.isAnonymous: Boolean get() {} fun User.isFriendsWith(other: User): Boolean {}
  20. Extension functions UserUtils.isAnonymous(aUser); UserUtils.hasName(aUser); aUser.isAnonymous aUser.hasName @file:JvmName("UserUtils") // default is

    UserUtilsKt val User.hasName: Boolean @JvmName("hasName") get() {} val User.displayableName: String get() {} val User.isAnonymous: Boolean get() {} fun User.isFriendsWith(other: User): Boolean {}
  21. Inline functions • Java compiler doesn't support inlining • Can

    still use inline functions, but they won't be inlined • Be mindful of performance • Cannot call inline functions with reified generics from Java
  22. Reified generics workaround inline fun <reified T> View.firstViewOfType(): T? =

    firstViewOfType(T::class.java) fun <T> View.firstViewOfType(type: Class<T>): T? {}
  23. Visibility • public, protected, private behave as expected • Java

    package-local has no equivalent in Kotlin • Kotlin internal has no equivalent in Java
  24. internal • Kotlin module-level visibility • Module: IDEA, Maven, Gradle,

    or Ant compilation unit • So external Java shouldn't be able to see it, right? • Unfortunately, internal -> public in bytecode
  25. Package-local • Java default modifier • Only visible within the

    same package • Kotlin doesn't (currently?) have a way to restrict members to the same package
  26. !

  27. ! • The dreaded platform type • Blows up when

    dereferenced • Most calls into Java will return a platform type • You should try to eliminate most/all of these in your own code • Solution: Nullability Annotations
  28. Nullability Annotations! interface Request<T> {A Response<T> execute(); }B val request:

    Request<User> = getUser(id) request.execute().value.displayableName interface Response<T> {C T getValue(); }D NPE
  29. Nullability Annotations! interface Request<T> {A Response<T> execute(); }B val request:

    Request<User> = getUser(id) request.execute().value.displayableName interface Response<T> {C @Nullable T getValue(); }D
  30. Nullability Annotations! kmost@kmost: ~/work/foursquare-android dev $ rg "@NonNull" --count --no-filename

    | paste -d+ -s - | bc 759 kmost@kmost: ~/work/foursquare-android dev $ rg "@Nullable" --count --no-filename | paste -d+ -s - | bc 475 • Annotating everything is super-tedious
  31. Nullability Annotations! interface Request<T> {A Response<T> execute(); }B val request:

    Request<User> = getUser(id) request.execute().value.displayableName interface Response<T> {C @Nullable T getValue(); }D
  32. Nullability Annotations! interface Request<T> {A Response< A @Nullable T> execute();

    }B val request: Request<User> = getUser(id) request.execute().value.displayableName interface Response<T> {C T getValue(); }D
  33. Nullability Annotations! interface Request<T> {A Response<T> execute(); }B val request:

    Request<User> = getUser(id) request.execute().value.displayableName interface Response<@Nullable T> {C T getValue(); }D
  34. Nullability Annotations! interface Request<T> {A Response<T> execute(); }B val request:

    Request<User> = getUser(id) request.execute().value.displayableName interface Response<T> { @Nullable T getValue(); }D
  35. Nullability Annotations! interface Request<T> {A Response<T> execute(); }B val request:

    Request<User> = getUser(id) request.execute().value.displayableName interface Response<T> { @Nullable T getValue(); @Nullable Error<T> getError(); @Nullable HttpResponse getRawResponse(); }D
  36. Nullability Annotations! interface Request<T> {A Response<T> execute(); }B val request:

    Request<User> = getUser(id) request.execute().value.displayableName interface Response<T> { @Nullable T getValue(); @Nullable Error<@Nullable T> getError(); @Nullable HttpResponse getRawResponse(); }D
  37. Default Nullability Annotations • Added in Kotlin 1.1.50 • Specify

    default annotations: • Per package • Per class • Per method
  38. Default Nullability Annotations • JSR-305 comes with: • @ParametersAreNonnullByDefault •

    @ParametersAreNullableByDefault • Annotate a package to make all parameters for all functions non-null or nullable by default
  39. DIY Default Nullability Annotations • You're not limited to @ParametersAreNonnullByDefault

    • You can make your own nullability annotations • Let's look at the source for @ParametersAreNonnullByDefault
  40. Nulls in libraries • These solutions only work if you

    control the code in question • How do you deal with Java libs that have null everywhere? • ex: Android
  41. Nulls in libraries • Submit your own PR • Annotations

    are easy and low-risk • Even if you "know" the nullability of members, letting the compiler enforce it for you is better
  42. Lambdas and SAMs • Kotlin lambdas compile to a functional

    interface in Java • () -> R becomes Function0<R> • (T) -> R becomes Function1<T, R> • Java SAMs compile to a special syntax in Kotlin • SAMName { ... }
  43. SAMs • Unfortunately, Kotlin SAMs currently don't offer SAM syntax

    in Kotlin • Right now, it's best to keep your SAM types in Java
  44. SAMs interface KotlinSAM {
 fun onClick(view: View)
 }B val sam

    = object : KotlinSAM {
 override fun onClick(view: View) { ...
 }
 }
  45. Lambda signatures • Lambdas with receivers exported with receiver as

    1st param • Nullability of types is lost in Java! • (Foo) -> Unit is equivalent to Foo?.() -> Unit from Java's perspective
  46. Special Types • Most types are mapped between Java and

    Kotlin • There are exceptions: • Unit • Nothing
  47. Unit • Unit can be mapped to void in most

    cases in Java • It cannot, however, if Unit is the selected type for a generic • Ex: Lambdas. Java signature FunctionN<Args, Unit> • Java has to: return Unit.INSTANCE;
  48. Nothing • Nothing is the subtype of all other types

    • No instances exist, not even null • So a Nothing function can never return; must throw/loop • No type exists like this in Java
  49. Nothing • Generics of Nothing become raw types • List<Nothing>

    in Kotlin becomes List in Java • Actual Nothings become Void • Consumers just have to know the method will never return
  50. Wildcards • In Java, all use-sites of a generic need

    to say if they are: • Covariant: ? extends Foo • Contravariant: ? super Foo • In Kotlin, you just put that declaration on the type param itself: • Covariant: out T • Contravariant: in T
  51. Wildcards class Box<T> { T foo; }B <T> Box<T> box(T

    unboxed) { return new Box<>(unboxed); }D <T> T unbox(Box<T> box) { return box.foo; }F Box<Integer> boxedInt = box(3); List<Box<Number>> boxes = new ArrayList<>(); boxes.add(boxedInt);
  52. Wildcards class Box<T> { T foo; }B <T> Box<T> box(T

    unboxed) { return new Box<>(unboxed); }D <T> T unbox(Box<T> box) { return box.foo; }F Box<Integer> boxedInt = box(3); List<Box<Number>> boxes = new ArrayList<>(); boxes.add(boxedInt);
  53. Wildcards class Box<T> { T foo; }B <T> Box<T> box(T

    unboxed) { return new Box<>(unboxed); }D <T> T unbox(Box<T> box) { return box.foo; }F Box<Integer> boxedInt = box(3); List<Box<? extends Number>> boxes = new ArrayList<>(); boxes.add(boxedInt);
  54. Wildcards class Box<T> { T foo; }B <T> Box<T> box(T

    unboxed) { return new Box<>(unboxed); }D <T> T unbox(Box<T> box) { return box.foo; }F Box<Integer> boxedInt = box(3); List<Box<? extends Number>> boxes = new ArrayList<>(); boxes.add(boxedInt);
  55. Wildcards class Box<T> { T foo; }B <T> Box<T> box(T

    unboxed) { return new Box<>(unboxed); }D <T> T unbox(Box<T> box) { return box.foo; }F Box<Integer> boxedInt = box(3); List<Box<? extends Number>> boxes; boxes.add(boxedInt);
  56. Wildcards class Box<T> { T foo; }B <T> Box<T> box(T

    unboxed) { return new Box<>(unboxed); }D <T> T unbox(Box<T> box) { return box.foo; }F Box<Integer> boxedInt = box(3); List<Box<? extends Number>> boxes; boxes.add(boxedInt); class Box<T>(val foo: T) fun <T> box(unboxed: T) = Box<T>(unboxed) fun <T> unbox(box: Box<T>) = box.foo val boxedInt: Box<Int> = box(3) val boxed = mutableListOf<Box<Number>>() boxed.add(boxedInt)
  57. Wildcards class Box<T> { T foo; }B <T> Box<T> box(T

    unboxed) { return new Box<>(unboxed); }D <T> T unbox(Box<T> box) { return box.foo; }F Box<Integer> boxedInt = box(3); List<Box<? extends Number>> boxes; boxes.add(boxedInt); class Box<T>(val foo: T) fun <T> box(unboxed: T) = Box<T>(unboxed) fun <T> unbox(box: Box<T>) = box.foo val boxedInt: Box<Int> = box(3) val boxed = mutableListOf<Box<Number>>() boxed.add(boxedInt)
  58. Wildcards class Box<T> { T foo; }B <T> Box<T> box(T

    unboxed) { return new Box<>(unboxed); }D <T> T unbox(Box<T> box) { return box.foo; }F Box<Integer> boxedInt = box(3); List<Box<? extends Number>> boxes; boxes.add(boxedInt); class Box<out T>(val foo: T) fun <T> box(unboxed: T) = Box<T>(unboxed) fun <T> unbox(box: Box<T>) = box.foo val boxedInt: Box<Int> = box(3) val boxed = mutableListOf<Box<Number>>() boxed.add(boxedInt)
  59. Wildcards • So out is roughly equivalent to extends •

    And in is roughly equivalent to super • Kotlin "fakes" declaration-site variance for Java by generating wildcards for all variant generics in parameters • Return types remain invariant • Final covariant types remain invariant
  60. Wildcards • To override the default generic behavior: • @JvmWildcard

    if you want variance where there is none • @JvmSuppressWildcards if you don't want variance
  61. Data classes • Tuple-like classes; properties declared in constructor •

    Auto-generation of hashCode(), equals(), toString() • These work perfectly in Java • Auto-generation of copy(...) • This works okay in Java • Lack of default + named params makes it clunky
  62. Data classes data class User( val id: String? = null,

    val name: String? = null, val username: String? = null, val gender: String? = null, val points: Int = 0 ) u.copy(u.getId(), u.getName(), u.getUsername(), u.getGender(), u.getPoints() + 1);
  63. Data classes data class User( val id: String? = null,

    val name: String? = null, val username: String? = null, val gender: String? = null, val points: Int = 0 )
  64. data class User( val id: String? = null, val name:

    String? = null, val username: String? = null, val gender: String? = null, val points: Int = 0 ) { fun toBuilder() = Builder(this) class Builder(private var user: User = User()) { fun id(id: String?) = apply { user = user.copy(id = id) } fun name(name: String?) = apply { user = user.copy(name = name) // ... fun build() = user } } Data classes
  65. data class User( val id: String? = null, val name:

    String? = null, val username: String? = null, val gender: String? = null, val points: Int = 0 ) { fun toBuilder() = Builder(this) class Builder(private var user: User = User()) { fun id(id: String?) = apply { user = user.copy(id = id) } fun name(name: String?) = apply { user = user.copy(name = name) // ... fun build() = user } } Data classes
  66. data class User( val id: String? = null, val name:

    String? = null, val username: String? = null, val gender: String? = null, val points: Int = 0 ) { fun toBuilder() = Builder(this) class Builder(private var user: User = User()) { fun id(id: String?) = apply { user = user.copy(id = id) } fun name(name: String?) = apply { user = user.copy(name = name) // ... fun build() = user } } Data classes
  67. data class User( val id: String? = null, val name:

    String? = null, val username: String? = null, val gender: String? = null, val points: Int = 0 ) { fun toBuilder() = Builder(this) class Builder(private var user: User = User()) { fun id(id: String?) = apply { user = user.copy(id = id) } fun name(name: String?) = apply { user = user.copy(name = name) // ... fun build() = user } } Data classes
  68. data class User( val id: String? = null, val name:

    String? = null, val username: String? = null, val gender: String? = null, val points: Int = 0 ) { fun toBuilder() = Builder(this) class Builder(private var user: User = User()) { fun id(id: String?) = apply { user = user.copy(id = id) } fun name(name: String?) = apply { user = user.copy(name = name) // ... fun build() = user } } Data classes
  69. Overall Interop Thoughts • kt ❤ java • Most of

    the time, interop Just Works™ • But when writing non-private members, say to yourself: • When writing Kotlin: "How will this look in Java?" • When writing Java: "How will this look in Kotlin?"
  70. Quick Tip • Write (at least some) tests in the

    other language • If you use Java, write some Kotlin tests • If you write Kotlin, write some Java tests • Gives you insight into the ergonomics of your public API