המסמך הזה הוא אוסף של כללים ליצירת ממשקי API ציבוריים ב-Java וב-Kotlin, במטרה שהקוד ייראה אידיומטי כשמשתמשים בו בשפה השנייה.
Java (לשימוש ב-Kotlin)
אין מילות מפתח קשיחות
אסור להשתמש במילות מפתח שמורות של Kotlin כשמות של שיטות או שדות. כדי להשתמש בהם ב-Kotlin, צריך להוסיף גרש הפוך לפני כל אחד מהם. מותר להשתמש במילות מפתח גמישות, במילות מפתח לשינוי ובמזהים מיוחדים.
לדוגמה, הפונקציה when של Mockito דורשת שימוש בגרשיים הפוכים כשמשתמשים בה מ-Kotlin:
val callable = Mockito.mock(Callable::class.java)
Mockito.`when`(callable.call()).thenReturn(/* … */)
לא להשתמש בשמות של תוספים Any
מומלץ להימנע משימוש בשמות של פונקציות הרחבה ב-Any לשיטות או בשמות של מאפייני הרחבה ב-Any לשדות, אלא אם זה ממש הכרחי. למרות שלשיטות ולשדות של חברים תמיד תהיה עדיפות על פני פונקציות או מאפיינים של הרחבות של Any, כשקוראים את הקוד קשה לדעת איזו מהן נקראת.
הערות לגבי אפשרות של ערך null
כל פרמטר, ערך מוחזר ושדה לא פרימיטיבי ב-API ציבורי צריכים לכלול אנוטציית מאפיין המציין אם ערך יכול להיות ריק (nullability). סוגים ללא אנוטציות מפורשים כסוגים של "פלטפורמה", שמאפיין המציין אם ערך יכול להיות ריק (nullability) שלהם לא ברור.
כברירת מחדל, הקומפיילר של Kotlin מכבד את ההערות של JSR 305, אבל מסמן אותן באזהרות. אפשר גם להגדיר דגל כדי שהקומפיילר יתייחס להערות כשגיאות.
הפרמטרים של Lambda מופיעים בסוף
סוגי הפרמטרים שמתאימים להמרת SAM צריכים להיות אחרונים.
לדוגמה, חתימת השיטה Flowable.create() של RxJava 2 מוגדרת כך:
public static <T> Flowable<T> create(
FlowableOnSubscribe<T> source,
BackpressureStrategy mode) { /* … */ }
מכיוון ש-FlowableOnSubscribe עומד בדרישות להמרה ל-SAM, קריאות לפונקציה של המתודה הזו מ-Kotlin נראות כך:
Flowable.create({ /* … */ }, BackpressureStrategy.LATEST)
אבל אם הפרמטרים היו הפוכים בחתימת השיטה, אפשר היה להשתמש בתחביר של trailing-lambda בקריאות לפונקציה:
Flowable.create(BackpressureStrategy.LATEST) { /* … */ }
קידומות של נכסים
כדי ששיטה תיוצג כמאפיין ב-Kotlin, צריך להשתמש בקידומת בסגנון 'bean' באופן מדויק.
לשיטות גישה נדרש הקידומת get או הקידומת is לשיטות שמחזירות ערך בוליאני.
public final class User {
public String getName() { /* … */ }
public boolean isActive() { /* … */ }
}
val name = user.name // Invokes user.getName()
val active = user.isActive // Invokes user.isActive()
לשיטות מוטציה משויכות נדרשת תחילית set.
public final class User {
public String getName() { /* … */ }
public void setName(String name) { /* … */ }
public boolean isActive() { /* … */ }
public void setActive(boolean active) { /* … */ }
}
user.name = "Bob" // Invokes user.setName(String)
user.isActive = true // Invokes user.setActive(boolean)
אם רוצים שהשיטות יוצגו כמאפיינים, לא משתמשים בקידומות לא סטנדרטיות כמו has, set או בשיטות גישה ללא קידומת get. עדיין אפשר להפעיל מתודות עם קידומות לא סטנדרטיות כפונקציות, וזה יכול להיות מקובל בהתאם להתנהגות של המתודה.
העמסת יתר של אופרטורים
שימו לב לשמות של שיטות שמאפשרות תחביר מיוחד של אתר הקריאה (כמו העמסת אופרטורים ב-Kotlin). מוודאים ששמות השיטות, כמו אלה, מתאימים לשימוש בתחביר המקוצר.
public final class IntBox {
private final int value;
public IntBox(int value) {
this.value = value;
}
public IntBox plus(IntBox other) {
return new IntBox(value + other.value);
}
}
val one = IntBox(1)
val two = IntBox(2)
val three = one + two // Invokes one.plus(two)
Kotlin (לצריכה של Java)
שם קובץ
אם קובץ מכיל פונקציות או מאפיינים ברמה העליונה, תמיד צריך להוסיף לו את ההערה @file:JvmName("Foo") כדי לתת לו שם.
כברירת מחדל, חברים ברמה העליונה בקובץ MyClass.kt יסתיימו במחלקה שנקראת
MyClassKt, וזה לא רצוי כי זה חושף את השפה כפרט הטמעה.
כדאי להוסיף @file:JvmMultifileClass כדי לשלב את החברים ברמה העליונה מכמה קבצים בכיתה אחת.
ארגומנטים של Lambda
אפשר להטמיע ממשקי שיטה יחידה (SAM) שמוגדרים ב-Java גם ב-Kotlin וגם ב-Java באמצעות תחביר lambda, שמטמיע את ההטמעה בצורה אידיומטית. ב-Kotlin יש כמה אפשרויות להגדרת ממשקים כאלה, וכל אחת מהן שונה מעט.
הגדרה מועדפת
פונקציות מסדר גבוה שמיועדות לשימוש מ-Java לא יכולות לקבל סוגי פונקציות שמחזירות Unit, כי זה ידרוש ממי שקורא לפונקציה ב-Java להחזיר Unit.INSTANCE. במקום להוסיף את סוג הפונקציה בשורה אחת עם החתימה, משתמשים בממשקי SAM פונקציונליים. בנוסף, מומלץ להשתמש בממשקים פונקציונליים (SAM) במקום בממשקים רגילים כשמגדירים ממשקים שצפויים לשמש כפונקציות למדא, וכך לאפשר שימוש אידיומטי מ-Kotlin.
בואו נבחן את ההגדרה הבאה ב-Kotlin:
fun interface GreeterCallback {
fun greetName(String name)
}
fun sayHi(greeter: GreeterCallback) = /* … */
כשמפעילים את הפונקציה מ-Kotlin:
sayHi { println("Hello, $it!") }
כשמפעילים את הפונקציה מ-Java:
sayHi(name -> System.out.println("Hello, " + name + "!"));
גם אם סוג הפונקציה לא מחזיר Unit, עדיין כדאי להפוך אותה לממשק בעל שם כדי לאפשר למפעילים להטמיע אותה באמצעות מחלקה בעלת שם ולא רק באמצעות פונקציות למבדה (גם ב-Kotlin וגם ב-Java).
class MyGreeterCallback : GreeterCallback {
override fun greetName(name: String) {
println("Hello, $name!");
}
}
הימנעות מסוגי פונקציות שמחזירות Unit
בואו נבחן את ההגדרה הבאה ב-Kotlin:
fun sayHi(greeter: (String) -> Unit) = /* … */
היא מחייבת שמתקשרים ב-Java יחזירו Unit.INSTANCE:
sayHi(name -> {
System.out.println("Hello, " + name + "!");
return Unit.INSTANCE;
});
מומלץ להימנע מממשקים פונקציונליים כשההטמעה אמורה לכלול מצב
אם ההטמעה של הממשק אמורה לכלול מצב, אין טעם להשתמש בתחביר של lambda. דוגמה בולטת היא Comparable, כי היא נועדה להשוות בין this לבין other, ול-lambda אין this. אם לא מוסיפים את הקידומת fun לממשק, המתקשר נאלץ להשתמש בתחביר object : ..., שמאפשר לו לשמור מצב, וכך מספק רמז למתקשר.
בואו נבחן את ההגדרה הבאה ב-Kotlin:
// No "fun" prefix.
interface Counter {
fun increment()
}
היא מונעת שימוש בתחביר lambda ב-Kotlin, ולכן צריך להשתמש בגרסה הארוכה יותר:
runCounter(object : Counter {
private var increments = 0 // State
override fun increment() {
increments++
}
})
אל תשתמשו ב-Nothing גנריים
סוג שהפרמטר הגנרי שלו הוא Nothing נחשף כסוגים גולמיים ב-Java. ב-Java, משתמשים לעיתים רחוקות בסוגים גולמיים, ועדיף להימנע מהם.
חריגים במסמך
פונקציות שיכולות להחזיר חריגים מסומנים צריכות לתעד אותם באמצעות @Throws. חריגות בזמן ריצה צריכות להיות מתועדות ב-KDoc.
חשוב לשים לב לממשקי ה-API שהפונקציה מעבירה אליהם את ההרשאות, כי הם עלולים להחזיר חריגים מסוג checked, ש-Kotlin מאפשרת להם להתפשט בשקט.
עותקים להגנה
כשמחזירים אוספים משותפים או אוספים לקריאה בלבד שלא בבעלותכם מממשקי API ציבוריים, צריך לעטוף אותם במאגר שלא ניתן לשינוי או לבצע העתקה מגוננת. למרות ש-Kotlin אוכפת את המאפיין לקריאה בלבד, אין אכיפה כזו בצד של Java. ללא העטיפה או העותק המגן, אפשר להפר את התנאים הבלתי משתנים על ידי החזרת הפניה לאוסף לטווח ארוך.
פונקציות נלוות
פונקציות ציבוריות באובייקט משני צריכות להיות מסומנות ב-@JvmStatic כדי שיוצגו כשיטה סטטית.
בלי ההערה, הפונקציות האלה זמינות רק כשיטות מופע בשדה סטטי Companion.
לא נכון: אין הערה
class KotlinClass {
companion object {
fun doWork() {
/* … */
}
}
}
public final class JavaClass {
public static void main(String... args) {
KotlinClass.Companion.doWork();
}
}
נכון: @JvmStatic annotation
class KotlinClass {
companion object {
@JvmStatic fun doWork() {
/* … */
}
}
}
public final class JavaClass {
public static void main(String... args) {
KotlinClass.doWork();
}
}
קבועים של מודעות נלוות
מאפיינים ציבוריים שאינם const, שהם קבועים יעילים ב-companion
object, צריכים להיות מסומנים ב-@JvmField כדי שיוצגו כשדה סטטי.
בלי ההערה, הנכסים האלה זמינים רק כפונקציות getter של מופע עם שם מוזר בשדה הסטטי Companion. השימוש ב-@JvmStatic במקום ב-@JvmField מעביר את ה-getters עם השמות המוזרים לשיטות סטטיות במחלקה, וזה עדיין לא נכון.
לא נכון: אין הערה
class KotlinClass {
companion object {
const val INTEGER_ONE = 1
val BIG_INTEGER_ONE = BigInteger.ONE
}
}
public final class JavaClass {
public static void main(String... args) {
System.out.println(KotlinClass.INTEGER_ONE);
System.out.println(KotlinClass.Companion.getBIG_INTEGER_ONE());
}
}
שגוי: @JvmStatic הערה
class KotlinClass {
companion object {
const val INTEGER_ONE = 1
@JvmStatic val BIG_INTEGER_ONE = BigInteger.ONE
}
}
public final class JavaClass {
public static void main(String... args) {
System.out.println(KotlinClass.INTEGER_ONE);
System.out.println(KotlinClass.getBIG_INTEGER_ONE());
}
}
נכון: @JvmField annotation
class KotlinClass {
companion object {
const val INTEGER_ONE = 1
@JvmField val BIG_INTEGER_ONE = BigInteger.ONE
}
}
public final class JavaClass {
public static void main(String... args) {
System.out.println(KotlinClass.INTEGER_ONE);
System.out.println(KotlinClass.BIG_INTEGER_ONE);
}
}
שמות אידיומטיים
ל-Kotlin יש מוסכמות שונות לקריאה לפונקציות בהשוואה ל-Java, ולכן יכול להיות שתצטרכו לשנות את השמות של הפונקציות. אפשר להשתמש ב-@JvmName כדי לתכנן שמות שיתאימו למוסכמות של שתי השפות או לשמות של הספריות הסטנדרטיות שלהן.
המצב הזה קורה הכי הרבה בפונקציות של תוספים ובמאפיינים של תוספים, כי המיקום של סוג המקבל שונה.
sealed class Optional<T : Any>
data class Some<T : Any>(val value: T): Optional<T>()
object None : Optional<Nothing>()
@JvmName("ofNullable")
fun <T> T?.asOptional() = if (this == null) None else Some(this)
// FROM KOTLIN:
fun main(vararg args: String) {
val nullableString: String? = "foo"
val optionalString = nullableString.asOptional()
}
// FROM JAVA:
public static void main(String... args) {
String nullableString = "Foo";
Optional<String> optionalString =
Optionals.ofNullable(nullableString);
}
העמסת פונקציות לברירות מחדל
בפונקציות עם פרמטרים שיש להם ערך ברירת מחדל, צריך להשתמש ב-@JvmOverloads.
בלי ההערה הזו, אי אפשר להפעיל את הפונקציה באמצעות ערכי ברירת מחדל.
כשמשתמשים ב-@JvmOverloads, חשוב לבדוק את השיטות שנוצרו כדי לוודא שכל אחת מהן הגיונית. אם לא, מבצעים אחת או את שתי הפעולות הבאות של שינוי מבנה הקוד עד שמרוצים מהתוצאה:
- משנים את סדר הפרמטרים כך שהפרמטרים עם ערכי ברירת מחדל יהיו בסוף.
- מעבירים את ברירות המחדל לעומסים של פונקציות ידניות.
תשובה שגויה: לא @JvmOverloads
class Greeting {
fun sayHello(prefix: String = "Mr.", name: String) {
println("Hello, $prefix $name")
}
}
public class JavaClass {
public static void main(String... args) {
Greeting greeting = new Greeting();
greeting.sayHello("Mr.", "Bob");
}
}
נכון: @JvmOverloads annotation.
class Greeting {
@JvmOverloads
fun sayHello(prefix: String = "Mr.", name: String) {
println("Hello, $prefix $name")
}
}
public class JavaClass {
public static void main(String... args) {
Greeting greeting = new Greeting();
greeting.sayHello("Bob");
}
}
בדיקות Lint
דרישות
- גרסת Android Studio: 3.2 Canary 10 ואילך
- גרסת פלאגין של Android Gradle: 3.2 ואילך
בדיקות נתמכות
עכשיו יש בדיקות Android Lint שיעזרו לכם לזהות ולסמן חלק מהבעיות בתאימות שתיארנו קודם. מזוהות רק בעיות ב-Java (לשימוש ב-Kotlin). באופן ספציפי, הבדיקות הנתמכות הן:
- Unknown Nullness
- גישה לנכס
- אין מילות מפתח קשיחות ב-Kotlin
- הפרמטרים של Lambda מופיעים אחרונים
Android Studio
כדי להפעיל את הבדיקות האלה, עוברים אל קובץ > העדפות > עורך > בדיקות ומסמנים את הכללים שרוצים להפעיל בקטע 'תאימות של Kotlin':

איור 1. הגדרות ליכולת פעולה הדדית של Kotlin ב-Android Studio.
אחרי שמסמנים את הכללים שרוצים להפעיל, הבדיקות החדשות יפעלו כשמריצים את הבדיקות של הקוד (Analyze > Inspect Code…)
קומפילציות משורת הפקודה
כדי להפעיל את הבדיקות האלה מגרסאות build של שורת הפקודה, מוסיפים את השורה הבאה לקובץ build.gradle:
מגניב
android { ... lintOptions { enable 'Interoperability' } }
Kotlin
android { ... lintOptions { enable("Interoperability") } }
רשימה מלאה של ההגדרות שנתמכות ב-lintOptions זמינה בחומר העזר בנושא Android Gradle DSL.
לאחר מכן, מריצים את הפקודה ./gradlew lint משורת הפקודה.