קריאה לקוד קוטלין מ-Java

במעבדה זו תוכלו ללמוד איך לכתוב או להתאים את קוד Kotlin שלכם כדי שיהיה קל יותר לקרוא לו בקוד Java.

מה תלמדו

  • איך משתמשים ב-@JvmField, ב-@JvmStatic ובהערות אחרות?
  • הגבלות על גישה לתכונות מסוימות של Kotlin בקוד Java.

דברים שחשוב לדעת

שיעור Lab זה נכתב על מתכנתים ונניח שידע בסיסי של Java ו-Kotlin.

קוד Lab זה מדמה העברה של פרויקט גדול יותר שכתוב בשפת השפה של Java, כדי לשלב קוד Kotlin חדש.

כדי לפשט את הדברים, יהיה לנו קובץ .java יחיד בשם UseCase.java, שמייצג את בסיס הקודים הקיים.

אנחנו חושבים שעדכנו את הפונקציות שנכתבו במקור ב-Java בגרסה החדשה של Kotlin, ואנחנו צריכים לסיים את השילוב שלה.

ייבוא הפרויקט

אפשר לשכפל את הקוד של הפרויקט מפרויקט GitHub כאן: GitHub

לחלופין, אפשר להוריד ולחלץ את הפרויקט מארכיון ה-ZIP שנמצא כאן:

להורדת קובץ Zip

אם אתם משתמשים ב- IntelliJ IDEA, יש לבחור באפשרות "Import Project".

אם אתם משתמשים ב-Android Studio, בוחרים באפשרות "ייבוא הפרויקט (Gredle , 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.

אנחנו רואים שהמאגר שלנו הוא סינגל אחד שהוכרז עליו באמצעות מילת המפתח של האובייקט. הבעיה היא שקוטלין יוצרת מופע סטטי בתוך הכיתה, ולא חושפת אותם כנכסים ושיטות.

לדוגמה, אפשר להפנות לRepository.getNextGuestId() באמצעות Repository.INSTANCE.getNextGuestId(), אבל יש דרך טובה יותר.

ניתן להשיג את Kotlin ליצור שיטות ומאפיינים סטטיים על ידי סימון הנכסים והשיטות הציבוריים של המאגר באמצעות @JvmStatic:

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 אין פונקציות תוספים, כך ש-Cotlin מהדר את השיטות האלה כפונקציות סטטיות.

לצערנו, אם נבחן את השיטה של registerGuest() בתוך UseCase.java, נוכל לראות שמשהו לא תקין:

User guest = new User(Repository.getNextGuestId(), StringUtils.nameToLogin(name), name);

הסיבה לכך היא שקוטלין ממקם את הפונקציות האלה &ברמה העליונה" או ברמת החבילה בתוך מחלקה ששמה מבוסס על שם הקובץ. במקרה הזה, שם הקובץ הוא StringUillas.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() נפתרה.

לצערנו, השגיאה הזו הוחלפה בפרמטר חדש בנוגע לפרמטרים שמועברים ל-constructor עבור 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. כדי לתקן את הבעיה, יש לאפשר ל-Kotlin ליצור עומס יתר על ה-constructor באמצעות ההערה @JvmOverloads.

תחילה, אנחנו צריכים לבצע עדכון קל לגבי User.kt.

מכיוון שלמחלקה User יש רק בונה יחיד, והבנאי אינו כולל הערות, מילת המפתח 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 תייצר עומס יתר על פונקציות, כולל constructors, אבל היא תיצור רק עומס יתר אחד לכל פרמטר עם ערך ברירת מחדל.

נבחן את 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().

כדי לפתור את הבעיה, אנחנו רוצים שקוטלין תיצור שם אחר לנכס 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 ללא הצורך להגדיר במפורש את getter.

למרות ההבחנה הזו, אפשר להשתמש בכל מה שמתאים לכם. שניהם יגרמו ל-Cotlin ליצור getter בשם hasSystemAccess().

אם נחזיר אותך אל UseCase.java, נוכל לוודא שמעכשיו אפליקציית getSystemUsers() היא שגיאה!

השגיאה הבאה מופיעה ב-formatUser(), אבל אם ברצונך לקרוא עוד על המוסכמה למתן שמות ב-Kotlin getter, יש להמשיך לקרוא כאן לפני המעבר לשלב הבא.

קבלת שמות וסטטר

כשאנחנו כותבים קוטלין, קל לזכור את קוד הכתיבה הזה, למשל:

val myString = "Logged in as ${user.displayName}")

קריאה בפועל לפונקציה כדי לקבל את הערך displayName. כדי לאמת זאת, יש לעבור אל כלים > Kotlin > להציג את Kotlin Bytecode בתפריט וללחוץ על לחצן הניקוי:

String myString = "Logged in as " + user.getDisplayName();

כשאנחנו רוצים לגשת לקטעים האלה מ-Java, עלינו לכתוב במפורש את שם getter.

ברוב המקרים, שם ה-Java של getter עבור נכסים של Kotlin הוא get + שם הנכס, כפי שראינו עם User.getHasSystemAccess() ו-User.getDisplayName(). החריגה היחידה היא נכסים שהשמות שלהם מתחילים ב-"is". במקרה זה, שם ה-Java עבור getter הוא השם של נכס Kotlin.

לדוגמה, נכס ב-User כגון:

val isAdmin get() = //...

ניתן יהיה לגשת מ-Java עם:

boolean userIsAnAdmin = user.isAdmin();

על ידי שימוש בהערה @JvmName, קודלין יוצר קוד בייט שמכיל את השם שצוין, במקום את ברירת המחדל, עבור ההערה.

ההגדרה הזו פועלת גם עבור משתמשים שהגדירו את השם שלהם תמיד: set + שם הנכס. לדוגמה, עליך ללמוד את המחלקה הבאה:

class Color {
   var red = 0f
   var green = 0f
   var blue = 0f
}

בואו נניח שאנחנו רוצים לשנות את שם המתנדב מ-setRed() ל-updateRed(), ולהשאיר את המצבים האלה לבד. נשתמש בגרסה @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.

בקוטלין, נכסים בדרך כלל נחשפים דרך גיטרים וממירים. כולל val נכסים.

ניתן לשנות את ההתנהגות הזו באמצעות ההערה @JvmField. כשמחילים את ההגדרה הזאת על נכס בכיתה, מערכת Kotlin תדלג על יצירת getter (וכלי להגדרה של נכסי 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:

בקוטלין, הסוגים היחידים שיכולים להיות 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 יש קונספט של "מסומן חריג". אלה מקרים חריגים שניתן לשחזר בעזרתם, כגון המשתמש שמזין שם קובץ באופן שגוי, או שהרשת אינה זמינה באופן זמני. לאחר תיעוד של חריגה מחריגה, המפתח יכול לספק משוב למשתמש בנוגע לפתרון הבעיה.

מכיוון שחריגים שנבדקו נבדקים בזמן ההידור, אתם מצהירים בהם בחתימת השיטה:

public void openFile(File file) throws FileNotFoundException {
   // ...
}

לעומת זאת, ל-Cotlin אין חריגים ושהסיבה לכך היא הבעיה.

הפתרון הוא לבקש מ-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 עכשיו?

לא! חשוב לזכור: קוטלין לא בדק חריגים, והוספת @Throws בשיטה לא משנה את זה:

fun saveFromKotlin(path: String) {
   Repository.saveAs(path)
}

עדיין כדאי לאתר חריגים בעת טיפול ב, אבל קוטלין לא מאלץ אותך לטפל בהם.

בשיעור הקוד הזה ריכזנו את העקרונות הבסיסיים לכתיבה של קוד קוטלין, שתומך גם בכתיבת קוד Java אידיומטי.

דיברנו על האופן שבו אנחנו יכולים להשתמש בהערות כדי לשנות את האופן שבו קודלין יוצר קוד בייט מסוג JVM, למשל:

  • @JvmStatic כדי ליצור חברים קבועים ושיטות.
  • @JvmOverloads כדי ליצור שיטות עומס יתר עבור פונקציות עם ערכי ברירת מחדל.
  • @JvmName כדי לשנות את שם המצבים והסדרים.
  • @JvmField כדי לחשוף נכס ישירות כשדה, ולא דרך getter ומגדירים.
  • @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)
   }
}

StringUillas.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
}