במעבדה זו תוכלו ללמוד איך לכתוב או להתאים את קוד Kotlin שלכם כדי שיהיה קל יותר לקרוא לו בקוד Java.
מה תלמדו
- איך משתמשים ב-
@JvmField
, ב-@JvmStatic
ובהערות אחרות? - הגבלות על גישה לתכונות מסוימות של Kotlin בקוד Java.
דברים שחשוב לדעת
שיעור Lab זה נכתב על מתכנתים ונניח שידע בסיסי של Java ו-Kotlin.
קוד Lab זה מדמה העברה של פרויקט גדול יותר שכתוב בשפת השפה של Java, כדי לשלב קוד Kotlin חדש.
כדי לפשט את הדברים, יהיה לנו קובץ .java
יחיד בשם UseCase.java
, שמייצג את בסיס הקודים הקיים.
אנחנו חושבים שעדכנו את הפונקציות שנכתבו במקור ב-Java בגרסה החדשה של Kotlin, ואנחנו צריכים לסיים את השילוב שלה.
ייבוא הפרויקט
אפשר לשכפל את הקוד של הפרויקט מפרויקט GitHub כאן: GitHub
לחלופין, אפשר להוריד ולחלץ את הפרויקט מארכיון ה-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
}