Slide 1

Slide 1 text

Kevin Most Idiomatic Interop

Slide 2

Slide 2 text

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

Slide 3

Slide 3 text

Who should be thinking about this? • Java library developers • Kotlin library developers • Anyone working in a mixed codebase

Slide 4

Slide 4 text

Your interop story should be • Safe • Performant • Readable • Discoverable

Slide 5

Slide 5 text

@JvmHappiness

Slide 6

Slide 6 text

@Jvm Annotations • Annotations that tell the Kotlin compiler how to expose code to Java

Slide 7

Slide 7 text

@JvmOverloads fun Date.format( formatString: String, locale: Locale = defaultLocale() ): String format(date, "yyyyMMdd");

Slide 8

Slide 8 text

@JvmOverloads fun Date.format( formatString: String, locale: Locale = defaultLocale() ): String format( date, "yyyyMMdd", defaultLocale()) );;

Slide 9

Slide 9 text

@JvmOverloads @JvmOverloads fun Date.format( formatString: String, locale: Locale = defaultLocale() ): String format( date, "yyyyMMdd", defaultLocale()) );;

Slide 10

Slide 10 text

@JvmOverloads @JvmOverloads fun Date.format( formatString: String, locale: Locale = defaultLocale() ): String format(date, "yyyyMMdd"); defaultLocale()

Slide 11

Slide 11 text

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 )

Slide 12

Slide 12 text

"Consider a builder when faced with many constructor parameters" - Joshua Bloch

Slide 13

Slide 13 text

@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

Slide 14

Slide 14 text

@JvmStatic User.Companion.create(); class User {A companion object {B fun create(): User }C }D

Slide 15

Slide 15 text

@JvmStatic class User {A companion object {B @JvmStatic fun create(): User }C }D User.Companion.create();

Slide 16

Slide 16 text

@JvmStatic User.create(); class User {A companion object {B @JvmStatic fun create(): User }C }D

Slide 17

Slide 17 text

A less nested way? User.create(); class User {A companion object {B @JvmStatic fun create(): User }C }D

Slide 18

Slide 18 text

A less nested way? User.create(); fun create(): User class User {A }D

Slide 19

Slide 19 text

A less nested way? User.create(); fun create(): User class User {A }D

Slide 20

Slide 20 text

A less nested way? User.create(); fun create(): User class User {A }D

Slide 21

Slide 21 text

A less nested way? User.create(); @file:JvmName("User") fun create(): User class User {A }D

Slide 22

Slide 22 text

@Throws

Slide 23

Slide 23 text

• For Kotlin functions, property getters/setters, constructors • Adds throws FooException to generated method header @Throws

Slide 24

Slide 24 text

@Throws interface Repository { /** * @throws IOException if can't save */ fun save(obj: T) }D

Slide 25

Slide 25 text

@Throws interface Repository { /** * @throws IOException if can't save */ fun save(obj: T) }D

Slide 26

Slide 26 text

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

Slide 27

Slide 27 text

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

Slide 28

Slide 28 text

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

Slide 29

Slide 29 text

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

Slide 30

Slide 30 text

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

Slide 31

Slide 31 text

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

Slide 32

Slide 32 text

Extension functions

Slide 33

Slide 33 text

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

Slide 34

Slide 34 text

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?

Slide 35

Slide 35 text

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)

Slide 36

Slide 36 text

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)

Slide 37

Slide 37 text

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)

Slide 38

Slide 38 text

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

Slide 39

Slide 39 text

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 {}

Slide 40

Slide 40 text

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 {}

Slide 41

Slide 41 text

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 {}

Slide 42

Slide 42 text

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 {}

Slide 43

Slide 43 text

Inline functions

Slide 44

Slide 44 text

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

Slide 45

Slide 45 text

Reified generics workaround inline fun View.firstViewOfType(): T? {}

Slide 46

Slide 46 text

Reified generics workaround inline fun View.firstViewOfType(): T? = firstViewOfType(T::class.java) fun View.firstViewOfType(type: Class): T? {}

Slide 47

Slide 47 text

Visibility

Slide 48

Slide 48 text

Visibility • public, protected, private behave as expected • Java package-local has no equivalent in Kotlin • Kotlin internal has no equivalent in Java

Slide 49

Slide 49 text

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

Slide 50

Slide 50 text

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

Slide 51

Slide 51 text

private • Java classes can access an inner class' private member • Kotlin classes cannot

Slide 52

Slide 52 text

!

Slide 53

Slide 53 text

! • 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

Slide 54

Slide 54 text

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

Slide 55

Slide 55 text

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

Slide 56

Slide 56 text

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

Slide 57

Slide 57 text

Nullability Annotations! static List getUsers(); getUsers() // (Mutable)List!

Slide 58

Slide 58 text

Nullability Annotations! @NonNull static List getUsers(); getUsers() // (Mutable)List!

Slide 59

Slide 59 text

Nullability Annotations! @NonNull static List getUsers(); getUsers() // (Mutable)List

Slide 60

Slide 60 text

Nullability Annotations! @NonNull static List<@NonNull String> getUsers(); getUsers() // (Mutable)List

Slide 61

Slide 61 text

Nullability Annotations! @NonNull static List<@NonNull String> getUsers(); getUsers() // (Mutable)List

Slide 62

Slide 62 text

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

Slide 63

Slide 63 text

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

Slide 64

Slide 64 text

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

Slide 65

Slide 65 text

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

Slide 66

Slide 66 text

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

Slide 67

Slide 67 text

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

Slide 68

Slide 68 text

Nullability Annotations

Slide 69

Slide 69 text

Default Nullability Annotations • Added in Kotlin 1.1.50 • Specify default annotations: • Per package • Per class • Per method

Slide 70

Slide 70 text

Default Nullability Annotations dependencies { compile "com.google.code.findbugs:jsr305:3.0.2" } compileKotlin.kotlinOptions.freeCompilerArgs = [ "-Xjsr305-annotations=enable" ]

Slide 71

Slide 71 text

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

Slide 72

Slide 72 text

Annotating a package package-info.java @ParametersAreNonnullByDefault package com.example.kotlinconf;

Slide 73

Slide 73 text

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

Slide 74

Slide 74 text

ParametersAreNonnullByDefault.java @Nonnull @TypeQualifierDefault(ElementType.PARAMETER) public @interface ParametersAreNonnullByDefault {} DIY Default Nullability Annotations

Slide 75

Slide 75 text

ParametersAreNonnullByDefault.java @Nonnull @TypeQualifierDefault(ElementType.PARAMETER) public @interface ParametersAreNonnullByDefault {} DIY Default Nullability Annotations

Slide 76

Slide 76 text

FieldsAreNonnullByDefault.java @Nonnull @TypeQualifierDefault(ElementType.FIELD) public @interface FieldsAreNonnullByDefault {} DIY Default Nullability Annotations

Slide 77

Slide 77 text

FieldsAreNonnullByDefault.java @Nonnull @TypeQualifierDefault(ElementType.FIELD) public @interface FieldsAreNonnullByDefault {} DIY Default Nullability Annotations

Slide 78

Slide 78 text

FieldsAreNullableByDefault.java @Nullable @TypeQualifierDefault(ElementType.FIELD) public @interface FieldsAreNullableByDefault {} DIY Default Nullability Annotations

Slide 79

Slide 79 text

FieldsAreNullableByDefault.java @Nullable @TypeQualifierDefault(ElementType.FIELD) public @interface FieldsAreNullableByDefault {} // not quite DIY Default Nullability Annotations

Slide 80

Slide 80 text

FieldsAreNullableByDefault.java @CheckForNull @TypeQualifierDefault(ElementType.FIELD) public @interface FieldsAreNullableByDefault {} // not quite DIY Default Nullability Annotations

Slide 81

Slide 81 text

Living the dream package-info.java @ParametersAreNonnullByDefault @FieldsAreNullableByDefault @MethodsReturnNullableByDefault package com.example.kotlinconf;

Slide 82

Slide 82 text

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

Slide 83

Slide 83 text

Nulls in libraries • Ask your library maintainers

Slide 84

Slide 84 text

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

Slide 85

Slide 85 text

Lambdas and SAMs

Slide 86

Slide 86 text

Lambdas and SAMs • Kotlin lambdas compile to a functional interface in Java • () -> R becomes Function0 • (T) -> R becomes Function1 • Java SAMs compile to a special syntax in Kotlin • SAMName { ... }

Slide 87

Slide 87 text

SAMs • Unfortunately, Kotlin SAMs currently don't offer SAM syntax in Kotlin • Right now, it's best to keep your SAM types in Java

Slide 88

Slide 88 text

SAMs public interface JavaSAM {
 void onClick(View view);
 } val sam = JavaSAM { view -> ... }

Slide 89

Slide 89 text

SAMs interface KotlinSAM {
 fun onClick(view: View)
 }B val sam = JavaSAM { view -> ... }

Slide 90

Slide 90 text

SAMs interface KotlinSAM {
 fun onClick(view: View)
 }B val sam = KotlinSAM { view -> ... }

Slide 91

Slide 91 text

SAMs interface KotlinSAM {
 fun onClick(view: View)
 }B val sam = KotlinSAM { view -> ... }

Slide 92

Slide 92 text

SAMs interface KotlinSAM {
 fun onClick(view: View)
 }B val sam = object : KotlinSAM {
 override fun onClick(view: View) { ...
 }
 }

Slide 93

Slide 93 text

No content

Slide 94

Slide 94 text

"The other two [collection literals and SAM conversions] seem tractable in the foreseeable future"

Slide 95

Slide 95 text

No content

Slide 96

Slide 96 text

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

Slide 97

Slide 97 text

Special Types

Slide 98

Slide 98 text

Special Types • Most types are mapped between Java and Kotlin • There are exceptions: • Unit • Nothing

Slide 99

Slide 99 text

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 • Java has to: return Unit.INSTANCE;

Slide 100

Slide 100 text

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

Slide 101

Slide 101 text

Nothing • Generics of Nothing become raw types • List in Kotlin becomes List in Java • Actual Nothings become Void • Consumers just have to know the method will never return

Slide 102

Slide 102 text

Wildcards

Slide 103

Slide 103 text

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

Slide 104

Slide 104 text

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

Slide 105

Slide 105 text

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

Slide 106

Slide 106 text

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

Slide 107

Slide 107 text

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

Slide 108

Slide 108 text

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

Slide 109

Slide 109 text

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

Slide 110

Slide 110 text

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

Slide 111

Slide 111 text

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

Slide 112

Slide 112 text

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

Slide 113

Slide 113 text

Wildcards • To override the default generic behavior: • @JvmWildcard if you want variance where there is none • @JvmSuppressWildcards if you don't want variance

Slide 114

Slide 114 text

Data classes

Slide 115

Slide 115 text

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

Slide 116

Slide 116 text

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);

Slide 117

Slide 117 text

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 )

Slide 118

Slide 118 text

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

Slide 119

Slide 119 text

"Consider a builder when faced with many constructor parameters" - Joshua Bloch

Slide 120

Slide 120 text

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

Slide 121

Slide 121 text

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

Slide 122

Slide 122 text

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

Slide 123

Slide 123 text

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

Slide 124

Slide 124 text

Overall Interop Thoughts

Slide 125

Slide 125 text

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?"

Slide 126

Slide 126 text

Quick Tip

Slide 127

Slide 127 text

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

Slide 128

Slide 128 text

Resources • Calling Kotlin from Java: https://kotlinlang.org/docs/ reference/java-to-kotlin-interop.html • Calling Java from Kotlin: https://kotlinlang.org/docs/ reference/java-interop.html

Slide 129

Slide 129 text

#kotlinconf17 Kevin Most Thank you!