ב-codelab הזה תלמדו איך לכתוב או להתאים את קוד Kotlin כדי שיהיה קל יותר לקרוא לו מקוד Java.
מה תלמדו
- איך משתמשים בהערות
@JvmField
,@JvmStatic
ואחרות. - מגבלות בגישה לתכונות מסוימות של שפת Kotlin מקוד Java.
מה צריך לדעת לפני שמתחילים
ה-codelab הזה מיועד למתכנתים, והוא מניח שיש לכם ידע בסיסי ב-Java וב-Kotlin.
ב-codelab הזה נדגים איך להעביר חלק מפרויקט גדול יותר שנכתב בשפת התכנות Java, כדי לשלב קוד חדש של Kotlin.
כדי לפשט את הדברים, נשתמש בקובץ .java
יחיד בשם UseCase.java
, שייצג את בסיס הקוד הקיים.
נניח שהחלפנו פונקציונליות שנכתבה במקור ב-Java בגרסה חדשה שנכתבה ב-Kotlin, ואנחנו צריכים לסיים את השילוב שלה.
ייבוא הפרויקט
אפשר לשכפל את הקוד של הפרויקט מ-GitHub כאן: GitHub
אפשרות נוספת היא להוריד ולחלץ את הפרויקט מארכיון ZIP שנמצא כאן:
אם אתם משתמשים ב-IntelliJ IDEA, בוחרים באפשרות Import Project (ייבוא פרויקט).
אם אתם משתמשים ב-Android Studio, בוחרים באפשרות'ייבוא פרויקט (Gradle, Eclipse ADT וכו')'.
בואו נפתח את UseCase.java
ונתחיל לטפל בשגיאות שמופיעות.
הפונקציה הראשונה עם בעיה היא registerGuest
:
public static User registerGuest(String name) {
User guest = new User(Repository.getNextGuestId(), StringUtils.nameToLogin(name), name);
Repository.addUser(guest);
return guest;
}
השגיאות שמופיעות גם ב-Repository.getNextGuestId()
וגם ב-Repository.addUser(...)
הן זהות: "לא ניתן לגשת ללא סטטי מתוך הקשר סטטי".
עכשיו נסתכל על אחד מקובצי Kotlin. פותחים את הקובץ Repository.kt
.
אנחנו רואים שהמאגר שלנו הוא סינגלטון שמוצהר באמצעות מילת המפתח object. הבעיה היא ש-Kotlin יוצרת מופע סטטי בתוך המחלקה שלנו, במקום לחשוף אותם כמאפיינים ושיטות סטטיים.
לדוגמה, אפשר להפנות אל Repository.getNextGuestId()
באמצעות Repository.INSTANCE.getNextGuestId()
, אבל יש דרך טובה יותר.
אפשר להגדיר ש-Kotlin תיצור מאפיינים ושיטות סטטיים על ידי הוספת ההערה @JvmStatic
למאפיינים ולשיטות הציבוריים של Repository:
object Repository {
val BACKUP_PATH = "/backup/user.repo"
private val _users = mutableListOf<User>()
private var _nextGuestId = 1000
@JvmStatic
val users: List<User>
get() = _users
@JvmStatic
val nextGuestId
get() = _nextGuestId++
init {
_users.add(User(100, "josh", "Joshua Calvert", listOf("admin", "staff", "sys")))
_users.add(User(101, "dahybi", "Dahybi Yadev", listOf("staff", "nodes")))
_users.add(User(102, "sarha", "Sarha Mitcham", listOf("admin", "staff", "sys")))
_users.add(User(103, "warlow", groups = listOf("staff", "inactive")))
}
@JvmStatic
fun saveAs(path: String?):Boolean {
val backupPath = path ?: return false
val outputFile = File(backupPath)
if (!outputFile.canWrite()) {
throw FileNotFoundException("Could not write to file: $backupPath")
}
// Write data...
return true
}
@JvmStatic
fun addUser(user: User) {
// Ensure the user isn't already in the collection.
val existingUser = users.find { user.id == it.id }
existingUser?.let { _users.remove(it) }
// Add the user.
_users.add(user)
}
}
מוסיפים את ההערה @JvmStatic לקוד באמצעות ה-IDE.
אם נחזור ל-UseCase.java
, המאפיינים והשיטות ב-Repository
לא יגרמו יותר לשגיאות, למעט Repository.BACKUP_PATH
. נחזור לזה בהמשך.
ננסה לתקן את השגיאה הבאה בשיטה registerGuest()
.
נניח שיש לנו מחלקה StringUtils
עם כמה פונקציות סטטיות לפעולות על מחרוזות. כשביצענו המרה ל-Kotlin, המרנו את השיטות לפונקציות הרחבה. ב-Java אין פונקציות הרחבה, ולכן Kotlin מבצע הידור של השיטות האלה כפונקציות סטטיות.
לצערנו, אם נסתכל על השיטה registerGuest()
בתוך UseCase.java
, נראה שמשהו לא בסדר:
User guest = new User(Repository.getNextGuestId(), StringUtils.nameToLogin(name), name);
הסיבה לכך היא שב-Kotlin, הפונקציות האלה ברמה העליונה או ברמת החבילה ממוקמות בתוך מחלקה שהשם שלה מבוסס על שם הקובץ. במקרה הזה, מכיוון שהקובץ נקרא StringUtils.kt, השם של המחלקה המתאימה הוא StringUtilsKt
.
אנחנו יכולים לשנות את כל ההפניות שלנו ל-StringUtils
ל-StringUtilsKt
ולתקן את השגיאה הזו, אבל זה לא אידיאלי כי:
- יכול להיות שיהיה צורך לעדכן הרבה מקומות בקוד שלנו.
- השם עצמו מוזר.
לכן, במקום לשנות את קוד Java, נעדכן את קוד Kotlin כדי להשתמש בשם אחר לפונקציות האלה.
פותחים את StringUtils.Kt
ומחפשים את הצהרת החבילה הבאה:
package com.google.example.javafriendlykotlin
אפשר להגדיר ב-Kotlin שם אחר לשיטות ברמת החבילה באמצעות ההערה @file:JvmName
. נשתמש בהערה הזו כדי לתת שם למחלקה StringUtils
.
@file:JvmName("StringUtils")
package com.google.example.javafriendlykotlin
אם נחזור עכשיו אל UseCase.java
, נראה שהשגיאה שקשורה ל-StringUtils.nameToLogin()
נפתרה.
לצערי, השגיאה הזו הוחלפה בשגיאה חדשה לגבי הפרמטרים שמועברים לבונה של User
. נעבור לשלב הבא ונפתור את השגיאה האחרונה ב-UseCase.registerGuest()
.
Kotlin תומכת בערכי ברירת מחדל לפרמטרים. כדי לראות איך משתמשים בהם, אפשר לבדוק את הבלוק init
של Repository.kt
.
Repository.kt:
_users.add(User(102, "sarha", "Sarha Mitcham", listOf("admin", "staff", "sys")))
_users.add(User(103, "warlow", groups = listOf("staff", "inactive")))
אפשר לראות שכשמדובר במשתמש warlow, אפשר לדלג על הזנת ערך עבור displayName
כי ב-User.kt
צוין ערך ברירת מחדל עבורו.
User.kt:
data class User(
val id: Int,
val username: String,
val displayName: String = username.toTitleCase(),
val groups: List<String> = listOf("guest")
)
לצערנו, אי אפשר להשתמש באותה שיטה כשקוראים לה מ-Java.
UseCase.java:
User guest = new User(Repository.getNextGuestId(), StringUtils.nameToLogin(name), name);
ערכי ברירת מחדל לא נתמכים בשפת התכנות Java. כדי לפתור את הבעיה, נשתמש בהערה @JvmOverloads כדי להנחות את Kotlin ליצור עומסים יתרים עבור הקונסטרוקטור שלנו.
קודם צריך לעדכן קצת את User.kt
.
מכיוון שלכיתה User
יש רק constructor ראשי אחד, וה-constructor לא כולל הערות, מילת המפתח constructor
הושמטה. אבל עכשיו אנחנו רוצים להוסיף לה הערה, ולכן צריך לכלול את מילת המפתח constructor
:
data class User constructor(
val id: Int,
val username: String,
val displayName: String = username.toTitleCase(),
val groups: List<String> = listOf("guest")
)
אם מילת המפתח constructor
מופיעה, אפשר להוסיף את ההערה @JvmOverloads
:
data class User @JvmOverloads constructor(
val id: Int,
val username: String,
val displayName: String = username.toTitleCase(),
val groups: List<String> = listOf("guest")
)
אם נחזור אל UseCase.java
, נראה שאין יותר שגיאות בפונקציה registerGuest
.
השלב הבא הוא לתקן את הקריאה השגויה אל user.hasSystemAccess()
ב-UseCase.getSystemUsers()
. אפשר להמשיך לשלב הבא או לקרוא את ההמשך כדי להבין לעומק מה @JvmOverloads
עשה כדי לתקן את השגיאה.
@JvmOverloads
כדי להבין טוב יותר מה עושה @JvmOverloads
, ניצור שיטת בדיקה ב-UseCase.java
:
private void testJvmOverloads() {
User syrinx = new User(1001, "syrinx");
User ione = new User(1002, "ione", "Ione Saldana");
List<String> groups = new ArrayList<>();
groups.add("staff");
User beaulieu = new User(1002, "beaulieu", groups);
}
אפשר ליצור User
עם שני פרמטרים בלבד, id
ו-username
:
User syrinx = new User(1001, "syrinx");
אפשר גם ליצור User
על ידי הוספת פרמטר שלישי ל-displayName
, תוך שימוש בערך ברירת המחדל של groups
:
User ione = new User(1002, "ione", "Ione Saldana");
אבל אי אפשר לדלג על displayName
ולספק רק ערך ל-groups
בלי לכתוב קוד נוסף:
לכן נמחק את השורה הזו או נוסיף לה את התו'//' כדי להפוך אותה לתגובה.
ב-Kotlin, אם רוצים לשלב בין פרמטרים של ברירת מחדל לבין פרמטרים שאינם ברירת מחדל, צריך להשתמש בפרמטרים עם שמות.
// This doesn't work...
User(104, "warlow", listOf("staff", "inactive"))
// But using named parameters, it does...
User(104, "warlow", groups = listOf("staff", "inactive"))
הסיבה לכך היא ש-Kotlin תיצור עומסים יתרים לפונקציות, כולל בנאים, אבל היא תיצור רק עומס יתר אחד לכל פרמטר עם ערך ברירת מחדל.
נחזור אל UseCase.java
ונטפל בבעיה הבאה: הקריאה אל user.hasSystemAccess()
בשיטה UseCase.getSystemUsers()
:
public static List<User> getSystemUsers() {
ArrayList<User> systemUsers = new ArrayList<>();
for (User user : Repository.getUsers()) {
if (user.hasSystemAccess()) { // Now has an error!
systemUsers.add(user);
}
}
return systemUsers;
}
זו שגיאה מעניינת! אם משתמשים בתכונת ההשלמה האוטומטית של סביבת הפיתוח המשולבת (IDE) בכיתה User
, אפשר לראות ששם הכיתה hasSystemAccess()
שונה ל-getHasSystemAccess()
.
כדי לפתור את הבעיה, אנחנו רוצים ש-Kotlin ייצור שם אחר למאפיין val
hasSystemAccess
. כדי לעשות את זה, אפשר להשתמש בהערה @JvmName
. נחזור ל-User.kt
ונראה איפה צריך להחיל אותו.
יש שתי דרכים להוסיף את ההערה. האפשרות הראשונה היא להחיל את ההגדרה ישירות על שיטת get()
, כך:
val hasSystemAccess
@JvmName("hasSystemAccess")
get() = "sys" in groups
הסימון הזה מאותת ל-Kotlin לשנות את החתימה של ה-getter שהוגדר באופן מפורש לשם שסופק.
אפשרות אחרת היא להחיל את התכונה על הנכס באמצעות קידומת get:
באופן הבא:
@get:JvmName("hasSystemAccess")
val hasSystemAccess
get() = "sys" in groups
השיטה החלופית שימושית במיוחד למאפיינים שמשתמשים בשיטת getter שמוגדרת כברירת מחדל באופן מרומז. לדוגמה:
@get:JvmName("isActive")
val active: Boolean
כך אפשר לשנות את השם של ה-getter בלי להגדיר אותו באופן מפורש.
למרות ההבדל הזה, אתם יכולים להשתמש בשיטה שנוחה לכם יותר. בשני המקרים, Kotlin תיצור getter בשם hasSystemAccess()
.
אם נחזור ל-UseCase.java
, נוכל לוודא ש-getSystemUsers()
לא מכיל שגיאות.
השגיאה הבאה היא ב-formatUser()
, אבל אם רוצים לקרוא עוד על מוסכמות למתן שמות לפונקציות getter ב-Kotlin, אפשר להמשיך לקרוא כאן לפני שעוברים לשלב הבא.
שמות של Getter ו-Setter
כשכותבים ב-Kotlin, קל לשכוח שכשכותבים קוד כמו:
val myString = "Logged in as ${user.displayName}")
בעצם קוראת לפונקציה כדי לקבל את הערך של displayName
. כדי לוודא את זה, עוברים לתפריט כלים > Kotlin > הצגת קוד בייט של Kotlin ולוחצים על הלחצן פירוק:
String myString = "Logged in as " + user.getDisplayName();
כדי לגשת למאפיינים האלה מ-Java, צריך לכתוב במפורש את השם של שיטת ה-getter.
ברוב המקרים, שם ה-getter ב-Java של מאפייני Kotlin הוא פשוט get
+ שם המאפיין, כמו שראינו בדוגמאות User.getHasSystemAccess()
ו-User.getDisplayName()
. יוצא מן הכלל הוא מאפיינים שהשם שלהם מתחיל ב-'is'. במקרה כזה, השם של ה-getter ב-Java הוא השם של המאפיין ב-Kotlin.
לדוגמה, נכס ב-User
כמו:
val isAdmin get() = //...
הגישה אליו מ-Java תהיה באמצעות:
boolean userIsAnAdmin = user.isAdmin();
כשמשתמשים בהערה @JvmName
, שפת Kotlin יוצרת קוד בייט עם השם שצוין, ולא עם שם ברירת המחדל, עבור הפריט שמוער.
הדבר נכון גם לגבי פונקציות setter, שהשמות שנוצרים שלהן הם תמיד set
+ שם המאפיין. לדוגמה, נניח שיש לכם את הכיתה הבאה:
class Color {
var red = 0f
var green = 0f
var blue = 0f
}
נניח שאנחנו רוצים לשנות את שם הפונקציה setter מ-setRed()
ל-updateRed()
, בלי לשנות את הפונקציות getter. אפשר להשתמש בגרסה @set:JvmName
כדי לעשות את זה:
class Color {
@set:JvmName("updateRed")
var red = 0f
@set:JvmName("updateGreen")
var green = 0f
@set:JvmName("updateBlue")
var blue = 0f
}
מ-Java, נוכל לכתוב:
color.updateRed(0.8f);
UseCase.formatUser()
משתמש בגישה ישירה לשדה כדי לקבל את ערכי המאפיינים של אובייקט User
.
ב-Kotlin, מאפיינים נחשפים בדרך כלל באמצעות getters ו-setters. למשל, נכסי val
.
אפשר לשנות את ההתנהגות הזו באמצעות ההערה @JvmField
. כשמחילים את ההגדרה הזו על נכס במחלקה, Kotlin מדלג על יצירת שיטות getter (ו-setter לנכסי var
), ואפשר לגשת ישירות לשדה הגיבוי.
מכיוון שאובייקטים מסוג User
הם בלתי ניתנים לשינוי, אנחנו רוצים לחשוף כל אחד מהמאפיינים שלהם כשדות, ולכן נציין כל אחד מהם באמצעות @JvmField
:
data class User @JvmOverloads constructor(
@JvmField val id: Int,
@JvmField val username: String,
@JvmField val displayName: String = username.toTitleCase(),
@JvmField val groups: List<String> = listOf("guest")
) {
@get:JvmName("hasSystemAccess")
val hasSystemAccess
get() = "sys" in groups
}
אם נחזור ל-UseCase.formatUser()
, נראה שהשגיאות תוקנו.
@JvmField או const
בנוסף, יש שגיאה דומה בקובץ UseCase.java
:
Repository.saveAs(Repository.BACKUP_PATH);
אם נשתמש כאן בהשלמה אוטומטית, נראה שיש Repository.getBACKUP_PATH()
, ולכן יכול להיות שנרצה לשנות את ההערה ב-BACKUP_PATH
מ-@JvmStatic
ל-@JvmField
.
בוא ננסה את זה. חוזרים אל Repository.kt
ומעדכנים את ההערה:
object Repository {
@JvmField
val BACKUP_PATH = "/backup/user.repo"
אם נסתכל עכשיו על UseCase.java
, נראה שהשגיאה נעלמה, אבל יש גם הערה לגבי BACKUP_PATH
:
ב-Kotlin, הסוגים היחידים שיכולים להיות const
הם פרימיטיבים, כמו int
, float
ו-String
. במקרה הזה, מכיוון ש-BACKUP_PATH
הוא מחרוזת, אפשר לשפר את הביצועים באמצעות const val
במקום val
עם ההערה @JvmField
, ועדיין תהיה אפשרות לגשת לערך כשדה.
נשנה את זה עכשיו בקובץ Repository.kt:
object Repository {
const val BACKUP_PATH = "/backup/user.repo"
אם נחזור אל UseCase.java
, נראה שנותרה רק שגיאה אחת.
השגיאה הסופית היא Exception: 'java.io.IOException' is never thrown in the corresponding try block.
אם נסתכל על הקוד של Repository.saveAs
ב-Repository.kt
, נראה שהוא אכן יוצר חריגה. מה הבעיה?
ב-Java יש מושג שנקרא "חריגה שנבדקה". אלה חריגים שאפשר להתאושש מהם, כמו משתמש שהקליד שם קובץ לא נכון, או שהרשת לא זמינה באופן זמני. אחרי שתפיסת חריגה מסומנת, המפתח יכול לספק למשתמש משוב על אופן התיקון של הבעיה.
מכיוון שחריגים מסומנים נבדקים בזמן ההידור, צריך להצהיר עליהם בחתימה של ה-method:
public void openFile(File file) throws FileNotFoundException {
// ...
}
לעומת זאת, ב-Kotlin אין חריגים מסוג checked, וזו הסיבה לבעיה כאן.
הפתרון הוא לבקש מ-Kotlin להוסיף את IOException
שעשוי להיות מושפע מהחתימה של Repository.saveAs()
, כדי שקוד הבייט של JVM יכלול אותו כחריגה שנבדקה.
אנחנו עושים את זה באמצעות ההערה @Throws
של Kotlin, שעוזרת בתאימות בין Java ל-Kotlin. ב-Kotlin, חריגים מתנהגים באופן דומה ל-Java, אבל בניגוד ל-Java, ב-Kotlin יש רק חריגים לא מסומנים. לכן, אם רוצים להודיע לקוד Java שפונקציית Kotlin זורקת חריג, צריך להשתמש בהערה @Throws בחתימת הפונקציה של Kotlin. צריך לעבור אל Repository.kt file
ולעדכן את saveAs()
כך שיכלול את ההערה החדשה:
@JvmStatic
@Throws(IOException::class)
fun saveAs(path: String?) {
val outputFile = File(path)
if (!outputFile.canWrite()) {
throw FileNotFoundException("Could not write to file: $path")
}
// Write data...
}
אחרי שמוסיפים את ההערה @Throws
, אפשר לראות שכל שגיאות הקומפיילר ב-UseCase.java
תוקנו. מעולה!
יכול להיות שתהיתם אם תצטרכו להשתמש בחסימות try
ו-catch
כשמתקשרים ל-saveAs()
מ-Kotlin.
לא! חשוב לזכור שב-Kotlin אין חריגים מסוג checked, והוספת @Throws
ל-method לא משנה את זה:
fun saveFromKotlin(path: String) {
Repository.saveAs(path)
}
עדיין כדאי לטפל בחריגים כשאפשר, אבל ב-Kotlin לא חייבים לטפל בהם.
ב-codelab הזה הסברנו את העקרונות הבסיסיים של כתיבת קוד Kotlin שתומך גם בכתיבת קוד Java אידיומטי.
הסברנו איך אפשר להשתמש בהערות כדי לשנות את האופן שבו Kotlin יוצרת את בייטקוד ה-JVM שלה, למשל:
-
@JvmStatic
כדי ליצור חברים ושיטות סטטיים. -
@JvmOverloads
כדי ליצור שיטות עמוסות מדי לפונקציות שיש להן ערכי ברירת מחדל. @JvmName
כדי לשנות את השם של getters ו-setters.-
@JvmField
כדי לחשוף מאפיין ישירות כשדה, ולא באמצעות getters ו-setters. -
@Throws
כדי להצהיר על חריגים שנבדקו.
התוכן הסופי של הקבצים הוא:
User.kt
data class User @JvmOverloads constructor(
@JvmField val id: Int,
@JvmField val username: String,
@JvmField val displayName: String = username.toTitleCase(),
@JvmField val groups: List<String> = listOf("guest")
) {
val hasSystemAccess
@JvmName("hasSystemAccess")
get() = "sys" in groups
}
Repository.kt
object Repository {
const val BACKUP_PATH = "/backup/user.repo"
private val _users = mutableListOf<User>()
private var _nextGuestId = 1000
@JvmStatic
val users: List<User>
get() = _users
@JvmStatic
val nextGuestId
get() = _nextGuestId++
init {
_users.add(User(100, "josh", "Joshua Calvert", listOf("admin", "staff", "sys")))
_users.add(User(101, "dahybi", "Dahybi Yadev", listOf("staff", "nodes")))
_users.add(User(102, "sarha", "Sarha Mitcham", listOf("admin", "staff", "sys")))
_users.add(User(103, "warlow", groups = listOf("staff", "inactive")))
}
@JvmStatic
@Throws(IOException::class)
fun saveAs(path: String?):Boolean {
val backupPath = path ?: return false
val outputFile = File(backupPath)
if (!outputFile.canWrite()) {
throw FileNotFoundException("Could not write to file: $backupPath")
}
// Write data...
return true
}
@JvmStatic
fun addUser(user: User) {
// Ensure the user isn't already in the collection.
val existingUser = users.find { user.id == it.id }
existingUser?.let { _users.remove(it) }
// Add the user.
_users.add(user)
}
}
StringUtils.kt
@file:JvmName("StringUtils")
package com.google.example.javafriendlykotlin
fun String.toTitleCase(): String {
if (isNullOrBlank()) {
return this
}
return split(" ").map { word ->
word.foldIndexed("") { index, working, char ->
val nextChar = if (index == 0) char.toUpperCase() else char.toLowerCase()
"$working$nextChar"
}
}.reduceIndexed { index, working, word ->
if (index > 0) "$working $word" else word
}
}
fun String.nameToLogin(): String {
if (isNullOrBlank()) {
return this
}
var working = ""
toCharArray().forEach { char ->
if (char.isLetterOrDigit()) {
working += char.toLowerCase()
} else if (char.isWhitespace() and !working.endsWith(".")) {
working += "."
}
}
return working
}